サーバーレスでピタゴラスイッチ。どうも、かわしんです。イベントをサーバレスで繋げてピタゴラスイッチを作るのって案外楽しいもんですね、GUI コンソールで作ってる限りは。
さて、今回は AWS Batch のジョブ実行が失敗した時に Slack に通知する機能を作りたかったのですが、断片的な記事しか見当たらなかったのでこの記事でまとめようと思います。また、今回はインフラ構築ツールとして Terraform を使います。
多分、断片的な記事を普通に繋げてると動かないハマりポイントがあるので、後学の為に注意喚起するという目的もあります。
全体のアーキテクチャ
全体の流れはこんな感じでイベントを繋げていきたいと思います。
Batch -> CloudWatch -> SNS -> Lambda -> Slack
AWS Batch ではジョブの状態が変わるたびにイベントが発生します。 CloudWatch Event Rule を設定してイベントの中から FAILED
になったイベントのみをフィルタリングして SNS Topic に流します。
AWS SNS はイベントを受け取ったら Lambda を起動して、Lambda に登録した Node のコードがイベントを整形して Slack に Webhook を叩いて通知するという流れです。
CloudWatch から Lambda を直接起動することもできますが、SNS を経由することで Slack の他にもメールなどの他のチャネルへの通知を拡張することができる為、SNS を経由することにしました。
今回は、この CloudWatch と SNS と Lambda を Terraform を使ってセットアップしていきます。
Lambda で動かすパッケージ
さて、CloudWatch のイベントを Slack へ通知する Lambda Function は標準では用意されていません。イベントの JSON を整形して Slack のコメントとして Webhook を叩く処理を実装する必要がありますが、一から書くのは面倒です。
そこで、aws-to-slack というツールスタックを利用することにしました。
aws-to-slack は CloudWatch や Lambda のセットアップまでを含めて make deploy
を使って簡単に完了させることができるツールスタックです。しかし、AWS リソースのセットアップには CloudFormation を使っている為 Terraform との相性は悪く、勝手に AWS リソースを作られるのも気に入りません。
make package
コマンドを実行すれば Lambda で実行する Node のパッケージがコンパイルされる為、Lambda 上で動かすコードだけを aws-to-slack では利用することにしました。
事前に以下のコマンドを実行してコンパイル結果である release.zip
を生成します。
$ git clone https://github.com/arabold/aws-to-slack.git $ cd aws-to-slack # 生成物 release.zip ができる $ make package
aws-to-slack
ディレクトリに生成された release.zip
を任意の S3 バケットにアップロードしておきます。
構築する Terraform
S3
S3 にアップロードした aws-to-slack の release.zip
を参照する為に data.aws_s3_bucket_object.lambda_to_slack
を作ります。
/* * https://www.terraform.io/docs/providers/aws/d/s3_bucket_object.html */ data "aws_s3_bucket_object" "lambda_to_slack" { bucket = "<bucket_name>" key = "<key_prefix>/release.zip" }
IAM Role
aws-to-slack の CloudFormation で指定されているような IAM ロールを作ります。
/* * https://www.terraform.io/docs/providers/aws/r/iam_role.html */ resource "aws_iam_role" "iam_for_lambda" { name = "iam-for-lambda" assume_role_policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Action": "sts:AssumeRole", "Principal": { "Service": "lambda.amazonaws.com" }, "Effect": "Allow", "Sid": "" } ] } EOF } /* * https://www.terraform.io/docs/providers/aws/r/iam_role_policy_attachment.html */ // https://github.com/arabold/aws-to-slack/blob/1451c1beae7b8f635c42161587bddbce04442857/cloudformation.yaml#L68-L70 resource "aws_iam_role_policy_attachment" "iam_for_lambda1" { role = aws_iam_role.iam_for_lambda.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } resource "aws_iam_role_policy_attachment" "iam_for_lambda2" { role = aws_iam_role.iam_for_lambda.name policy_arn = "arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess" } resource "aws_iam_role_policy_attachment" "iam_for_lambda3" { role = aws_iam_role.iam_for_lambda.name policy_arn = "arn:aws:iam::aws:policy/AWSCodeCommitReadOnly" }
Lambda
Lambda のセットアップをします。Lambda Function の具体的な設定は aws-to-slack の CloudFormation を参考にしています。
Slack の Webhook URL は https://my.slack.com/apps/manage/custom-integrations にアクセスして生成します。
/* * https://www.terraform.io/docs/providers/aws/r/lambda_function.html */ // https://github.com/arabold/aws-to-slack/blob/1451c1beae7b8f635c42161587bddbce04442857/cloudformation.yaml#L46-L63 resource "aws_lambda_function" "lambda_to_slack" { s3_bucket = data.aws_s3_bucket_object.lambda_to_slack.bucket s3_key = data.aws_s3_bucket_object.lambda_to_slack.key s3_object_version = data.aws_s3_bucket_object.lambda_to_slack.version_id function_name = "lambda-to-slack" handler = "src/index.handler" role = aws_iam_role.iam_for_lambda.arn memory_size = 256 runtime = "nodejs10.x" # Cross-region metrics lookup requires at least 10s timeout = 15 environment { variables = { SLACK_CHANNEL = "<channel_name>" SLACK_HOOK_URL = "https://hooks.slack.com/<hook_api_path>" } } } /* * https://www.terraform.io/docs/providers/aws/r/lambda_permission.html */ resource "aws_lambda_permission" "sns_to_lambda_to_slack" { statement_id = "lambda-to-slack" action = "lambda:InvokeFunction" function_name = aws_lambda_function.lambda_to_slack.function_name principal = "sns.amazonaws.com" source_arn = aws_sns_topic.cloudwatch_slack.arn }
CloudWatch
resource.aws_cloudwatch_event_rule.batch_failed_job
を設定して Batch から発生するイベントをフィルタリングします。これは以下の AWS 公式のチュートリアルを参考にしました。
参考 : チュートリアル: 失敗したジョブイベントに関する Amazon Simple Notification Service アラートを送信する
/* * https://www.terraform.io/docs/providers/aws/r/cloudwatch_event_rule.html */ resource "aws_cloudwatch_event_rule" "batch_failed_job" { name = "batch-failed-job" event_pattern = <<PATTERN { "detail-type": [ "Batch Job State Change" ], "source": [ "aws.batch" ], "detail": { "status": [ "FAILED" ] } } PATTERN } /* * https://www.terraform.io/docs/providers/aws/r/cloudwatch_event_target.html */ resource "aws_cloudwatch_event_target" "sns_slack" { rule = aws_cloudwatch_event_rule.batch_failed_job.name arn = aws_sns_topic.cloudwatch_slack.arn }
SNS
SNS の設定はこんな感じになります。resource.aws_sns_topic.cloudwatch_slack.policy
については後述します。
/* * https://www.terraform.io/docs/providers/aws/r/sns_topic.html */ resource "aws_sns_topic" "cloudwatch_slack" { name = "cloudwatch-to-slack" // https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/resource-based-policies-cwe.html#sns-permissions policy = <<POLICY { "Version": "2012-10-17", "Id": "__default_policy_ID", "Statement": [ { "Sid": "__default_statement_ID", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": [ "SNS:GetTopicAttributes", "SNS:SetTopicAttributes", "SNS:AddPermission", "SNS:RemovePermission", "SNS:DeleteTopic", "SNS:Subscribe", "SNS:ListSubscriptionsByTopic", "SNS:Publish", "SNS:Receive" ], "Resource": "arn:aws:sns:<region_name>:<account_id>:cloudwatch-to-slack", "Condition": { "StringEquals": { "AWS:SourceOwner": "<account_id>" } } }, { "Sid": "TrustCWEToPublishEventsToMyTopic", "Effect": "Allow", "Principal": { "Service": "events.amazonaws.com" }, "Action": "sns:Publish", "Resource": "arn:aws:sns:<region_name>:<account_id>:cloudwatch-to-slack" } ] } POLICY } /* * https://www.terraform.io/docs/providers/aws/r/sns_topic_subscription.html */ resource "aws_sns_topic_subscription" "cloudwatch_slack" { topic_arn = aws_sns_topic.cloudwatch_slack.arn protocol = "lambda" endpoint = aws_lambda_function.lambda_to_slack.arn }
ハマりどころ:SNS トピックの Policy
さて、最初の時点では aws_sns_topic
の policy
は指定していませんでした。その状態で試してみると、CloudWatch Event Rule から SNS Topic への送信に失敗します。
さらに、AWS の GUI コンソールから CloudWatch Event Rule を作ると Terraform で作ったルールと主導で作ったルールの両方でイベントの送信に成功するようになります。そのあとに主導で作ったものを削除して Terraform で作ったものだけに戻すと失敗するようになります。
ここで Terraform の aws_cloudwatch_event_target のドキュメントをよく読むと以下のように注意書きがされています。
Note: In order to be able to have your AWS Lambda function or SNS topic invoked by a CloudWatch Events rule, you must setup the right permissions using aws_lambda_permission or aws_sns_topic.policy . More info here .
https://www.terraform.io/docs/providers/aws/r/cloudwatch_event_target.html
つまり、SNS トピックに送信する場合には aws_sns_topic
の policy
を指定する必要があるということです。しかし、具体的に何を書けばいいのかは教えてくれず無造作に AWS のドキュメント へのリンクが置かれているだけです。
このドキュメントを読むと、一度 SNS トピックを作った後でその Policy に以下の Policy を追加するように書かれていました。
{ "Sid": "TrustCWEToPublishEventsToMyTopic", "Effect": "Allow", "Principal": { "Service": "events.amazonaws.com" }, "Action": "sns:Publish", "Resource": "arn:aws:sns:region:account-id:topic-name" }
そこで実際に作成した SNS トピックの Policy を調べて前述の resource.aws_sns_topic.cloudwatch_slack.policy
が完成しました。
しかし、この Policy の中には SNS トピックの ARN が含まれています。Terraform のように1度のオペレーションで Immutable にリソースを作成する場合は事前にリソースの ARN を参照することは自己参照になりできません。
つまり、これを綺麗に Terraform で実現することはできないのです。だから、Terraform のドキュメントでは具体的な手法を説明するのではなく無造作にリンクが置かれているだけなのだと理解しました。不親切ですが。
一方で、アカウント名と SNS トピックの名前がわかっている場合はリソースの生成前に ARN を予測することができるため、今回は直接指定して事なきを得ました。
最後に
これで AWS Batch の失敗ジョブを Slack に通知することができるようになりました。初めてサーバーレスというものを使ってみましたが案外面白かったです。
ただ、GUI だと裏側でよしなにやっているごにょごにょした部分を自分で実装する必要があり Terraform などを使うと案外めんどくさいことがわかりました。
AWS Batch はジョブ起動のレイテンシーを考えない場合はコスト低くできる(低いとはいってない)方法なので使ってみてはいかがでしょうか?(AWS Batch ジョブの SSM 連携がないのでハマりました)