はじめに
こんにちは。ニフティ株式会社の添野翔太です。
今回はAmazon RDSのメンテナンスイベントをSlackに通知する機構を作成した話を共有します。
背景
先日、Amazon RDSのメンテナンスを原因として、とあるアプリケーションが一時的にダウンしました。振り返る中でメンテナンスイベントを見逃していたという課題が上がり、再発防止策の一環として、メンテナンスイベントの通知を行う機構を作成しました。
Amazon RDSのメンテナンスイベントをSlackに通知する
チームで慣れている人が多いGoを利用してプログラムを作成しました。またAWS Lambdaを基盤とし、定期的に発火させるためにAmazon EventBridgeを利用しました。
アーキテクチャは、以下の通りです。
ディレクトリ構成は以下の通りです。なお単体テスト用のファイルやREADME.mdなどは除いています。
1 2 3 4 5 6 7 8 |
src ├─lib │ ├─get_events.go │ ├─logger.go │ └─slack.go ├─go.sum ├─go.mod └─main.go |
次に、src/lib/logger.go、src/go.sum以外のコードを以下に示します。
まずはsrc/main.goです。こちらではセッション作成など実施しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
package main import ( "context" "os" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/rds" lib "github.com/sample/lib" "go.uber.org/zap" ) func handler(ctx context.Context, event events.CloudWatchEvent) { // NOTE: ログの設定 logger := lib.LoggerInit() defer logger.Sync() // NOTE: Slack WebHook URLを環境変数から取得 webhookURL := os.Getenv("SLACK_WEBHOOK_URL") if webhookURL == "" { logger.Warn("msg", zap.String("id", "DB-MAINTENANCE-NOTIFICATION-001"), zap.String("body", "Slack WebHook URLが設定されていない"), ) return } // NOTE: セッションを作成 sess, err := session.NewSessionWithOptions(session.Options{ Config: aws.Config{ Region: aws.String("ap-northeast-1"), // NOTE: 適切なリージョンに置き換えてください }, }) if err != nil { logger.Fatal("msg", zap.String("id", "DB-MAINTENANCE-NOTIFICATION-002"), zap.String("body", "セッション作成エラー"), zap.Error(err), ) return } rdsClient := &lib.RDSClient{ Client: rds.New(sess), } maintenanceActions, err := rdsClient.DescribePendingMaintenanceActions() if err != nil { logger.Fatal("msg", zap.String("id", "DB-MAINTENANCE-NOTIFICATION-003"), zap.String("body", "メンテナンス情報取得エラー"), zap.Error(err), ) return } lib.NotifySlackWithWebhook(maintenanceActions, webhookURL) } func main() { lambda.Start(handler) } |
次にsrc/lib/get_events.goを示します。こちらではDescribePendingMaintenanceActionsを呼び出し、データ形式を整えた上で呼び出し元にデータを返却します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
package lib import ( "strconv" "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/service/rds/rdsiface" ) type RDSClient struct { Client rdsiface.RDSAPI } // NOTE: DescribePendingMaintenanceActionsを呼び出して、メンテナンス情報を取得する関数 func (c *RDSClient) DescribePendingMaintenanceActions() ([]*rds.ResourcePendingMaintenanceActions, error) { // NOTE: すべてのイベントデータを取ってくる input := &rds.DescribePendingMaintenanceActionsInput{} result, err := c.Client.DescribePendingMaintenanceActions(input) if err != nil { return nil, err } return result.PendingMaintenanceActions, nil } // NOTE: 複数アイテムが有る場合は結合して返却する関数。プロパティが欠損している場合は空白となる func getDescriptionAndTime(details []*rds.PendingMaintenanceAction) (string, string) { var descriptions []string var currentApplyDates []string for i, detail := range details { description := strconv.Itoa(i+1) + ": " currentApplyDate := strconv.Itoa(i+1) + ": " if detail.Description != nil { description += aws.StringValue(detail.Description) } if detail.CurrentApplyDate != nil { currentApplyDate += aws.TimeValue(detail.CurrentApplyDate).Format(time.RFC3339) } descriptions = append(descriptions, description) currentApplyDates = append(currentApplyDates, currentApplyDate) } return strings.Join(descriptions, "\n"), strings.Join(currentApplyDates, "\n") } |
次にsrc/lib/slack.goを示します。こちらでは取得できたメンテナンスイベントをもとにSlackへ通知を行います。なお情報を瞬時に確認できるよう、DescriptionとCurrentApplyDateのみを返却するようにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
package lib import ( "bytes" "encoding/json" "fmt" "net/http" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/rds" ) func NotifySlackWithWebhook(maintenanceActions []*rds.ResourcePendingMaintenanceActions, webhookURL string) error { // NOTE: イベントがない場合の代替メッセージ if len(maintenanceActions) == 0 { message := map[string]interface{}{ "text": "No pending RDS maintenance actions at the moment.", } payload, err := json.Marshal(message) if err != nil { return err } resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(payload)) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("Slack notification failed with status code %d", resp.StatusCode) } } // NOTE: イベントがある場合、メンテナンスアクションの通知を送信 for _, action := range maintenanceActions { description, currentApplyDate := getDescriptionAndTime(action.PendingMaintenanceActionDetails) message := map[string]interface{}{ "text": "Pending RDS Maintenance Action", "attachments": []map[string]interface{}{ { "title": "Pending RDS Maintenance Action", "text": "The following maintenance action is pending:", "fields": []map[string]interface{}{ { "title": "DB Instance ID", "value": aws.StringValue(action.ResourceIdentifier), "short": true, }, { "title": "Description", "value": description, "short": false, }, { "title": "Current Apply Date", "value": currentApplyDate, "short": false, }, }, }, }, } payload, err := json.Marshal(message) if err != nil { return err } resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(payload)) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("Slack notification failed with status code %d", resp.StatusCode) } } return nil } |
最後にsrc/go.modを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
module github.com/sample go 1.19 require ( github.com/aws/aws-lambda-go v1.41.0 github.com/aws/aws-sdk-go v1.45.24 go.uber.org/zap v1.26.0 ) require ( github.com/jmespath/go-jmespath v0.4.0 // indirect go.uber.org/multierr v1.10.0 // indirect ) |
通知の見た目は以下の通りです。
もしメンテナンスイベントがある場合は、
となり、無い場合には以下の通りです。
おわりに
本記事では、Amazon RDSのメンテナンスイベントをSlackに通知する機構を作成した話について述べました。この機構によりメンテナンスイベントを見逃すリスクが減ります。