kawasin73のブログ

技術記事とかいろんなことをかくブログです

失敗した AWS Batch ジョブを Slack に通知する (Terraform を使って)

サーバーレスでピタゴラスイッチ。どうも、かわしんです。イベントをサーバレスで繋げてピタゴラスイッチを作るのって案外楽しいもんですね、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 というツールスタックを利用することにしました。

github.com

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_topicpolicy は指定していませんでした。その状態で試してみると、CloudWatch Event Rule から SNS Topic への送信に失敗します。

さらに、AWSGUI コンソールから 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_topicpolicy を指定する必要があるということです。しかし、具体的に何を書けばいいのかは教えてくれず無造作に 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 連携がないのでハマりました)