[toc]
本記事はMackerel Advent Calendar 2016とSansan Advent Calendar 2016 の17日目の投稿記事です。本当は2つ記事書こうと思ったけど、書く手間を惜しんで一つにまとめてしまいました。
タイトルの通り、本記事はMackerelのWebhookの通知先をAmazon API Gateway(以下、APIGateway)とAWS Lambnda(以下、Lambda)にすることで、任意のアクション(今回はEC2インスタンスの再起動による自動復旧)を取ることができたのでその紹介です。
この仕組みを作ろうと思った理由
EC2インスタンスが落ちたときに自動的に復旧する仕組みが欲しかったからです。
Auto Recoveryの採用を見送った理由
AWSにはAuto Revoveryという機能が提供されています。しかし、以下の三点の理由により採用しませんでした。
1. 適用できるインスタンスに条件がある
ドキュメントを読むと以下の条件を満たしたEC2インスタンスでしかAuto Recoveryが設定できません。
- C3、C4、M3、M4、R3、T2、または X1 インスタンスタイプを使用している VPC (EC2-Classic 以外) で実行されている
- 共有テナンシーを使用している (テナンシー属性が default に設定されている)
- 暗号化された EBS ボリューム (インスタンスストアボリュームではない) を含む EBS ボリュームを使用している
インスタンスタイプはまだしも、インスタンスストアが付いているインスタンスでも適用できないのは残念です。
2. CloudWatchアラームを設定するのが面倒
Auto Recoveryを有効にするためには、CloudWatchのアラームを設定する必要があります。EC2インスタンス毎にCloudWatchアラームを設定しなければならず、とても面倒です。また、EC2インスタンスをTerminateした後はCloudWatchアラームも削除したいのですが、手動で行う必要があります。
3. システムステータスがFailedである場合のみ作動する
EC2を使っている人ならご存知かと思いますが、インスタンスのステータスチェックには、システムステータスとインスタンスステータスがあります。
Auto Recoveryが作動するのは、システムステータスがFailedになった場合のみで、インスタンスステータスがFailedになっても作動しません。
インスタンスステータスがFailedになるのは、インスタンス内部に問題が生じた場合なので、本来であれば、きちんと原因調査して復旧すべきだと思います。しかし、稼働中のサーバが突然落ちた場合は、SSHもできない状況がほとんどで結局は再起動せざるを得ない状況になると予想されるため、インスタンスステータスがFailedになった場合でも自動的に再起動してほしい場合もあります(全部のサーバがそうではありません)。
Mackerel Webhook + API Gateway + Lambda のメリット
1. どのインスタンスにも適用できる
上記の「1. 適用できるインスタンスに条件がある」をクリアできます。 Maclerelの監視に引っかかったインスタンスが対象になるため、インスタンスタイプなどの条件は関係ありません。
2. 設定が楽
上記の「2. Auto Revoeryを設定するのが面倒」をクリアできます。 最初の構築はかなり面倒ですが、構築さえしてしまえば、あとはMackerelの監視設定次第で、サーバの増減に関係なく自動復旧の設定が可能になります。
3. 復旧条件を設定できる
Mackerelの監視設定次第で、上記の「3. システムステータスがFailedである場合のみ作動する」をクリアできます。
4. 運用の負荷が軽くなる(放置可能になる)
API Gateway + Lambda にするメリットです。同じ仕組みをEC2インスタンスを立てて実現できますが、ほぼ使われることのない機能ために常にインスタンスを稼働させるのはお金がかかりますし、そのインスタンスが正常に稼働しているかを監視しなければなりません。 一方、API Gateway + Lambdaにすることで、実行時のみ費用がかかるのと監視もほぼ不要になります。
構築手順
前置きが長くなりましたが、次から手順を紹介します。
Mackerel APIキーの暗号化
セキュリティ的観点から、APIキーをAWS Key Management Service (KMS)で暗号化し、Lambda Function実行時に復号するようにします。
KMSへの暗号化手順は以下の記事を参考にしました。
KMSで認証情報を暗号化しLambda実行時に復号化する | Developers.IO
$ aws kms encrypt --key-id alias/mackerel_api_key --plaintext 'APIキー' --region ap-northeast-1
{
"KeyId": "arn:aws:kms:ap-northeast-1:xxxxxxxxxxxx:key/xxxxxxxxxxxxxxxxxxxxxxxx",
"CiphertextBlob": "xxxxxxxxxxxxxxxxxxxxxx"
}
CiphertextBlob
をメモっておいてください。
IAMロールの作成
二つのIAMロールを作成しました。
一つ目はMackerelからのWebhookを受け付けるLambda Function用で、以下のようにしました。 別のLambda Functionを実行、KMSから復号する権限を付けます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"lambda:*"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": [
"arn:aws:kms:ap-northeast-1:xxxxxxxxxxxx:key/xxxxxxxxxxxxxxxxxxxxxxxx"
]
}
]
}
二つ目は、EC2インスタンスを再起動(Stop&Start)する用のIAMロールです。 EC2の停止・起動と、Lambda Functionを実行する権限を付けます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:Describe*"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"lambda:*"
],
"Resource": [
"*"
]
}
]
}
Lambda Functionの作成
こちらも二つ作成します。
一つ目は、MackerelからのWebhookを受け付けるLambda Functionで、Gistに公開しています。 https://gist.github.com/ohsawa0515/f06073a3bc443ba592c7692d879f72dd
ENCRYPTED_MACKEREL_API_KEY
に先程メモったCiphertextBlob
を、ORGANIZATION_ID
にMackerel上で取得できるオーガニゼーションIDを設定します。
このLambda FunctionをMackerel-Incoming-Webhook
という名前で作成し、先程作成した一つ目のIAMロールを付与します。
二つ目は、EC2インスタンスを再起動(Stop&Start)するLambda FunctionでこちらもGistに公開しています。 https://gist.github.com/ohsawa0515/cf3632f1d369470c220a76be63ac1445
このLambda FunctionをMackerel-Webhook-Restart-Instance
という名前で作成し、先程作成した二つ目のIAMロールを付与します。
API Gatewayの作成
設定項目が多いので、所々省略しながら説明します。
APIキーの作成
API Gateway用のAPIキーを作成します。作ったAPIキーは後ほど、Amazon CloudFront(以下、CloudFront)のヘッダに組み込みます。 なぜ、CloudFrontを使うかというと、IPアドレス制限をしたいためです。API GatewayだけではIPアドレス制限などの認証ができないため、AWS WAFとCloudFrontを組み合わせる必要があります。
Method
POSTのみ作成します。
Method Request
- API Key Required ・・・ true
Integration Request
- Integration type ・・・ Lambda Function
- Lambda Function ・・・ Mackerel-Incoming-Webhook
Body Mapping Templates
- Content-Type ・・・ application/json
- template ・・・ 下記の通り
{
"body" : $input.json('$'),
"params" : {
#foreach($type in $allParams.keySet())
#set($params = $allParams.get($type))
"$type" : {
#foreach($paramName in $params.keySet())
"$paramName" : "$util.escapeJavaScript($params.get($paramName))"
#if($foreach.hasNext),#end
#end
}
#if($foreach.hasNext),#end
#end
},
"context" : {
"account-id" : "$context.identity.accountId",
"api-id" : "$context.apiId",
"api-key" : "$context.identity.apiKey",
"authorizer-principal-id" : "$context.authorizer.principalId",
"caller" : "$context.identity.caller",
"cognito-authentication-provider" : "$context.identity.cognitoAuthenticationProvider",
"cognito-authentication-type" : "$context.identity.cognitoAuthenticationType",
"cognito-identity-id" : "$context.identity.cognitoIdentityId",
"cognito-identity-pool-id" : "$context.identity.cognitoIdentityPoolId",
"http-method" : "$context.httpMethod",
"stage" : "$context.stage",
"source-ip" : "$context.identity.sourceIp",
"user" : "$context.identity.user",
"user-agent" : "$context.identity.userAgent",
"user-arn" : "$context.identity.userArn",
"request-id" : "$context.requestId",
"resource-id" : "$context.resourceId",
"resource-path" : "$context.resourcePath"
}
}
Method Response
HTTP Statusで200, 400, 500を作成します。
Integration Response
200, 400, 500用に作成します。
200
- Lambda Error Regex ・・・
-
- Body Mapping Templates
#set($inputRoot = $input.path('$'))
{
"message" : "OK"
}
400
- Lambda Error Regex ・・・
.*Bad Request.*
- Body Mapping Templates
#set($inputRoot = $input.path('$'))
{
"message" : "Bad Request"
}
500
- Lambda Error Regex ・・・
.*Internal Server Error.*
- Body Mapping Templates
#set($inputRoot = $input.path('$'))
{
"message" : "Internal Server Error"
}
CloudFront
設定方法は以下の記事を参考にしました。 AWSサーバレスアーキテクチャでCloudFrontからWAFをかけてAPI Gatewayを呼ぶ
設定項目が多すぎて一つ一つ説明するのが大変なので、編集画面のキャプチャで勘弁してください。
AWS WAF
IPアドレス数の上限緩和申請
MackerelのWebhookから来るIPアドレスは、FAQに書かれていますが、一つ問題があります。MackerelのIPアドレスレンジが/26
なのですが、AWS WAFが登録できるIPアドレスレンジは、/8
、/16
、24
、/32
のみ(IPv4の場合)なのでそのままで登録ができません。そのため、大変面倒ですが/32
に分けたIPアドレスを一つ一つ登録しなければなりません。
また、登録できる上限数が50までなので、緩和申請する必要があります。
設定
上限緩和申請が完了したら設定していきます。 以下の記事が参考になりましたので設定方法はここでは紹介しません。 AWS WAFでCloudFront経由でのアクセスにIPアドレス制限を設定する
Mackerelの設定
AWS側の設定が完了したら次はMackerel側の設定に進みます。
AWSインテグレーション(EC2)に対応させる
Status Check Failedメトリクスを取得する必要があります。AWSインテグレーションのドキュメントを見ながら設定していきます。 また、mackerel-agentのバージョンを v0.34.0 以降にする必要があります。
監視設定
- 監視対象のメトリック ・・・ custom.ec2.status_check_failed.total
- 監視対象の絞込 ・・・ 各々の環境で設定してください
アラートの発生条件 お好みですが、私の場合、以下のように設定しています。 3分間ステータスチェックに失敗したらアラート発生するようになります。
- Warning ・・・ > 0.5
- Critical ・・・ > 0.5
- 条件の持続時間 ・・・ 5分間の平均値
通知チャンネル
Webhookで、URLは登録したもの(CloudFront挟んでいるのでhttps://xxxxx.cloudfront.net
)を登録します。
通知グループ
追加した監視設定と通知チャンネルを指定します。
テスト
通知グループのテスト送信で疎通確認を行います。 正常に実行できない場合は、API Gatewayまで届いている場合は、Lambdaのログ(CloudWatch Logs)を確認します。API Gatewayまで届いていない場合は、AWS WAFとCloudFrontの設定を見直します。
最後に
長くなりましたが、このようにしてMackerelとAPI Gateway + Lambdaでアラート発生時にEC2インスタンスを再起動することができるようになりました。 この仕組みを作ったのはAWS re:Invent前だったのですが、今やるならAWS Step Functionsを使っても良いのかもしれません。Lambda Functionのスクリプトを見ればわかりますが、Lambda FunctionからLambda Functionを呼び出しており、密結合で可読性もよくありません。AWS Step Functionsを使うことでより管理しやすくなるのではないかと思います。
元記事はこちら。