掲題の件を試してみたのでメモ。今回Lambda側の実装はGoにしてみた。Lambda で Goが使えるようになってからまだ一度も Lambda function を Go で書いてみたことがなかったので......。
以下手順。Mackerel や Backlog を使っていなくても、Lambda function の Go実装という観点でも多少参考にはなるかも?
Backlog で API Key を取得しておく
Lambda function から Backlog API を叩くことになるので、API Key が必要になる。
個人に紐付くもののようなので、「個人設定」から取得しておく。
Lambda function の作成
今回の連携のための function(関数)を新たに作成する。
AWSコンソールで Lambda を開くとあちこちにある↓のようなボタンから function の作成フローに入る。
一から作成
というメニューから、以下のように項目を埋めていく。
「ロール」(IAM Role)のところはそれぞれでいいかんじにしてもらえればよさそう。Lambda から各種 AWS リソースにアクセスする必要がある場合はいろいろカスタマイズする必要があると思うのだけど、今回のようなケースではここらへんをどうするのがいいのか、よくわかっていない。今回ここでは、新しくロールを作成して Basic Edge Lambda アクセス権限
のひとつだけをテンプレートから選んで付与しておいてみた。
関数の作成
を押すと↓こんなかんじに。
「トリガー」には、API Gateway を選ぶ。↓
するとその下部で「トリガーの設定」として各種項目を入力できるようになる。
今回はテスト的にやってみるという位置づけなので「セキュリティ」はオープンにしているけど、実際の運用での利用を考えている場合には、適切な認証を設定してあげるのがよさそう。
「追加」ボタンを押して追加完了。
function の実装を作成してアップロードする
次に Lamba function の実装をおこなう。今回のケースで必要な function の仕様としては、
- Mackerel からのアラート通知 Webhook リクエスト(POST)を受けられる
- それにより API Gateway Endpoint から Lambda function が起動される
- リクエストボディをパースできる
- パースした結果をもとに Backlog の課題登録APIをリクエストできる
というシンプルなもの。
Mackerel からのアラート通知 Webhook リクエストの仕様は以下のヘルプページに記載があるので、これをもとに function の実装を Go でおこなう。
package main import ( "context" "encoding/json" "fmt" "log" "net/url" "os" "time" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" backlog "github.com/griffin-stewie/go-backlog" ) type Roles struct { Fullname string `json:"fullname"` ServiceName string `json:"serviceName"` ServiceUrl string `json:"serviceUrl"` RoleName string `json:"roleName"` RoleUrl string `json:"roleUrl"` } type Host struct { Id string `json:"id"` Name string `json:"name"` Url string `json:"url"` Type string `json:"type"` Status string `json:"status"` Memo string `json:"memo"` IsRetired bool `json:"isRetired"` Roles []Roles `json:"roles"` } type Alert struct { CreatedAt int64 `json:"createdAt"` WarningThreshold float64 `json:"warningThreshold"` CriticalThreshold float64 `json:"criticalThreshold"` Duration int `json:"duration"` IsOpen bool `json:"isOpen"` MetricLabel string `json:"metricLabel"` MetricValue float64 `json:"metricValue"` MonitorName string `json:"monitorName"` MonitorOperator string `json:"monitorOperator"` Status string `json:"status"` Trigger string `json:"trigger"` Url string `json:"url"` } type Notification struct { OrgName string `json:"orgName"` Event string `json:"event"` Host Host `json:"host"` Alert Alert `json:"alert"` } // 今回の実装はAPI Gateway「統合リクエスト」の「Lambda プロキシ統合の使用」を利用する前提。 // see also: https://github.com/aws/aws-lambda-go/blob/master/events/README_ApiGatewayEvent.md func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { var notification Notification json.Unmarshal([]byte(request.Body), ¬ification) // Mackerel は復旧時も通知がおこなわれる・復旧時通知はスルーする if notification.Alert.Status == "ok" { return events.APIGatewayProxyResponse{Body: "ok", StatusCode: 200}, nil } apikey := os.Getenv("BACKLOG_APIKEY") if apikey == "" { log.Fatalln("Backlog access API Key is required.") return events.APIGatewayProxyResponse{Body: "Backlog access API Key is required.", StatusCode: 400}, nil } // backlog.com なのか backlog.jp なのかはそれぞれごとに違うようなので注意 URL, err := url.Parse("https://<space_id>.backlog.com") if err != nil { log.Fatalf("ERROR: %s", err.Error()) return events.APIGatewayProxyResponse{Body: fmt.Sprintf("ERROR: %s", err.Error()), StatusCode: 500}, err } tm := time.Unix(notification.Alert.CreatedAt/1000, 0) // Alert.CreatedAt is msec // 課題登録 : https://developer.nulab-inc.com/ja/docs/backlog/api/2/add-issue/ // projectId : https://developer.nulab-inc.com/ja/docs/backlog/api/2/get-project-list/ // issueTypeId : https://developer.nulab-inc.com/ja/docs/backlog/api/2/get-issue-type-list/ // priorityId : https://developer.nulab-inc.com/ja/docs/backlog/api/2/get-priority-list/ client := backlog.NewClient(URL, apikey) response, err := client.Post("/api/v2/issues", values( map[string][]string{ "projectId": []string{"12345"}, "summary": []string{fmt.Sprintf("%s@%s - %s (occured at %s)", notification.Alert.Status, notification.Host.Name, notification.Alert.MonitorName, tm)}, "issueTypeId": []string{"123456"}, "priorityId": []string{"2"}, "description": []string{fmt.Sprintf("Alert URL : %s \nHost URL : %s", notification.Alert.Url, notification.Host.Url)}})) if err != nil { log.Fatalf("ERROR: %s", err.Error()) return events.APIGatewayProxyResponse{Body: fmt.Sprintf("ERROR: %s", err.Error()), StatusCode: 500}, err } return events.APIGatewayProxyResponse{Body: string(response), StatusCode: 200}, nil } func main() { lambda.Start(handleRequest) } func values(kv map[string][]string) url.Values { pairs := url.Values{} for k, v := range kv { for _, s := range v { pairs.Add(k, s) } } return pairs }
今回のような連携における注意点は上記実装内コメントにも表記しているが、以下のような点。
- 今回の実装はAPI Gateway「統合リクエスト」の「Lambda プロキシ統合の使用」を利用する前提
- ここまでの手順で API Gateway を自動で作成すると、デフォルトがその状態になっているので(多分)
- この実装だと、Lambda コンソールを使ってのテストはできない(多分)ので注意
- Mackerel は復旧時も通知がおこなわれることを考慮する
- Backlog API の Base URI(ドメイン)は利用しているスペースにより異なる?
- 課題登録APIをリクエストするにあたり必須な項目がいくつかあり、その項目はまた別にAPIで問い合わせることにより知ることができる
- 今回は事前に手作業で把握しておいた
ちなみに、Backlog API クライアントの Go 実装が GitHub - griffin-stewie/go-backlog: Backlog API Client for Golang. としてあったので今回はそちらを使わせていただいた。ありがたや。
この実装を以下のコマンドによりビルド・zip圧縮する。
$ GOOS=linux GOARCH=amd64 go build -o m2b $ zip handler.zip ./m2b
AWSコンソール内の「Designer」で Lambda function を選択すると↓、
圧縮したバイナリをアップロードできるメニューが表示されるので、↓
上記のようにファイル選択&ハンドラ名を入力。「ハンドル名」はバイナリビルド時に指定したバイナリファイル名っぽい。
さらにその下にある「環境変数」で Backlog API Key を設定しておくことを忘れずに。
そして、画面右上の「保存」ボタンでバイナリのアップロードと設定の保存が完了。
OK。
Mackerel の通知チャンネルとして設定する
ここまで準備してきた Lambda function、それを起動するトリガーとなる API Gateway のエンドポイントを、Mackerel からの Webhook 通知先として登録する。
API Gateway のエンドポイントは AWSコンソールで API Gateway を開いてダッシュボードとか開けば出てるので、それで。
ここで確認できる URL に、「リソース」で確認できる「メソッド」を指定するのを忘れずに。
https://xxx.execute-api.ap-northeast-1.amazonaws.com/mackerel2backlog-001/mackerel2backlog
こんなかんじの形式になるはず。
Mackerel の通知チャンネルはチャンネル設定画面 から作ることができる。
画面右上の「通知グループ/通知チャンネルを追加」を押して...
Webhook 設定項目を追加する。
Mackerel では、通知チャンネルを新たに作成するとそのチャンネルは自動的に Default 通知グループにも設定される。通知設定漏れを防げて安心。
Mackerel 側の設定は以上。
動作確認
アラート発報によりちゃんとBacklogに課題登録がされるかどうか、動作確認してみる。
死活監視アラートを上げるために、手持ちのとあるサーバーで動作している mackerel-agent を stop させる。
$ sudo systemctl stop mackerel-agent
Mackerel では、エージェントが導入されているサーバーであれば毎分期待されるはずのリクエストが一定期間途絶えた場合、死活監視アラートを上げる。
なので、しばし待つ。少しすると、slackが鳴動する。
きたきた。
Backlog の方を見てみる。
よっしゃよっしゃ。課題本文に Mackerel のアラート画面と該当ホストの URL も埋め込んでおいたおかげで、Backlog から Mackerel への遷移もスムーズで良いかんじ。
このあと一応エージェントを起動しなおしてみたけど、復旧時通知は意図通りスルーできていることも確認できた。
まとめ
「Mackerel Webhook 通知」「Backlog 課題登録API」「AWS Lambda」の3つを組み合わせて、Mackerel のアラートを Backlog に自動でインシデント登録する仕組みを構築してみた。Webhook リクエストの仕組みと Lambda のようなサーバーレスな仕組みのおかげで、少しの実装で様々なオペレーションを自動化できるようになったその利便性を、改めて実感した。そして、SaaS 側にプログラマブルな仕組みの構築を可能とするインターフェースがあること(Webhook であったり Web API であったり)の重要性も再認識した。
ちなみに今回、一番ハマったのが API Gateway の「統合リクエスト」「Lambda プロキシ統合の使用」のあたり。API Gateway や Lambda は触るその都度つまみ食いをしている状態なので、このあたりも少しきちっと学んでおいたほうがいいのかもしれない......。。
実践AWS Lambda ~「サーバレス」を実現する新しいアプリケーションのプラットフォーム~
- 作者:西谷 圭介
- 発売日: 2017/06/09
- メディア: 単行本(ソフトカバー)