はじめに
こんにちは。ニフティ株式会社の添野翔太です。今回はAmazon RDSのメンテナンスイベントをSlackに通知する機構を作成した話を共有します。
背景
先日、Amazon RDSのメンテナンスを原因として、とあるアプリケーションが一時的にダウンしました。振り返る中でメンテナンスイベントを見逃していたという課題が上がり、再発防止策の一環として、メンテナンスイベントの通知を行う機構を作成しました。Amazon RDSのメンテナンスイベントをSlackに通知する
チームで慣れている人が多いGoを利用してプログラムを作成しました。またAWS Lambdaを基盤とし、定期的に発火させるためにAmazon EventBridgeを利用しました。アーキテクチャは、以下の通りです。

1 2 3 4 5 6 7 8 |
src ├─lib │ ├─get_events.go │ ├─logger.go │ └─slack.go ├─go.sum ├─go.mod └─main.go |
まずは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) } |
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") } |
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 } |
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に通知する機構を作成した話について述べました。この機構によりメンテナンスイベントを見逃すリスクが減ります。参考記事
We are hiring!
ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です!ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! Tech TalkやMeetUpも開催しております!
こちらもお気軽にご応募ください!