🐏

【Terraform】SNSの通知をLambdaでわかりやすくする

2023/06/25に公開

背景

スポットインスタンスの停止通知を受け取るためにEventBridge+SNSの構成を導入したのですが
どうも内容がわかりにくいと感じました。

通知をSNSでメール通知した場合

メールだとjsonが見づらいです

通知をEventBridge→SNS→ChatBotでSlackに通知した場合

まだ見やすいのですが、中断通知の時間はせめてほしいかな...

どうする?

SNSの通知内容をわかりやすく加工できればよいので、
EventBridgeとSNSの間にLambdaを挟む構成を試してみようと思います。

イメージ図

この構成のメリット

このように見やすい通知が届きます。

方針

先日スポットインスタンスの停止通知をSNSで飛ばす構成の紹介記事を書きました。
ちょうどいいので、この環境のコードにLambda周りの設定を追加するようにします。
https://github.com/not75743/FIS-Terminate-SpotInstance

リソース間の連携が増えるため、IAMポリシーには注意しましょう。
プロビジョニングは引き続きTerraformで行います。

環境

Terraform 1.4.6

検証

eventbridgeターゲットの指定

snsトピックがターゲットとなっている場合、Lambda関数に変更します。

resource "aws_cloudwatch_event_target" "sns" {
  rule = aws_cloudwatch_event_rule.spot-interruption-rule.name
  arn  = aws_lambda_function.eventbridge-lambda-sns.arn
}

Lambda関数周辺リソースの作成

長いので折りたたみます

lambda.tf
resource "aws_lambda_function" "eventbridge-lambda-sns" {
  function_name    = "eventbridge-lambda-sns"
  handler          = "main.lambda_handler"
  runtime          = "python3.10"
  filename         = data.archive_file.example_zip.output_path
  source_code_hash = data.archive_file.example_zip.output_base64sha256

  role = aws_iam_role.lambda_role.arn
  environment {
    variables = {
      SNS_TOPIC = aws_sns_topic.topic.arn
    }
  }
}

resource "aws_iam_role" "lambda_role" {
  name = "example-lambda-role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
    aws_iam_policy.lambda_sns_publish.arn
  ]
}

resource "aws_iam_policy" "lambda_sns_publish" {
  name        = "lambda_sns_publish"
  path        = "/"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sns:Publish",
      "Resource": "${aws_sns_topic.topic.arn}"
    }
  ]
}
EOF
}

resource "aws_cloudwatch_log_group" "example_log_group" {
  name = "/aws/lambda/${aws_lambda_function.eventbridge-lambda-sns.function_name}"
  retention_in_days = 30
}

ポイントは

  • Lambdaのログを格納するロググループを作成する
    • 同時にログ転送できるようにAWSLambdaBasicExecutionRoleのマネージドポリシーを使用する
  • 指定したSNSへpublish出来るようにIAMポリシーを作成する
    • こちらはSNSトピックへのpublishのみが必要であるため、自身で作成する
  • SNSトピックの指定には環境変数を用いる

ことです

Lambdaを操作する権限をEventBridgeに与える

aws_lambda_permissionを使うことで、EventBridgeがLambdaを呼び出せるようにします。
リソースポリシーを使用する、というやつです。

resource "aws_lambda_permission" "allow_eventbridge" {
  statement_id  = "allow_eventbridge"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.eventbridge-lambda-sns.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.spot-interruption-rule.arn
}

この設定をするとLambda関数のトリガーにEventBridgeの表示が現れます。

設定をしないとこのようにトリガーとしてEventBridgeが表示されません。

Lambda関数コード

言語はpython 3.10です。長いので折りたたみます

main.py
import json
import os
from datetime import datetime, timedelta, timezone

import boto3


def lambda_handler(event, context):
    sns_topic_arn = os.environ.get('SNS_TOPIC')

    detail = event['detail']
    instance_id = detail['instance-id']

    # Parse the termination time and convert it to JST
    termination_time_utc = datetime.strptime(event['time'], '%Y-%m-%dT%H:%M:%SZ')
    termination_time_jst = termination_time_utc.replace(tzinfo=timezone.utc).astimezone(timezone(timedelta(hours=9)))

    # Format the received time without timezone
    received_time = termination_time_jst.strftime('%Y-%m-%d %H:%M:%S')

    # Add 2 minutes to the termination time
    termination_time_jst += timedelta(minutes=2)

    # Format the termination time without timezone
    termination_time = termination_time_jst.strftime('%Y-%m-%d %H:%M:%S')

    # Combine instance ID, received time, and termination time in one message
    message = f'Instance ID: {instance_id}\nSpot Interruption Notice: {received_time}\nTermination Time: {termination_time}'

    sns = boto3.client('sns')
    sns.publish(
        TopicArn=sns_topic_arn,
        Message=message,
        Subject='Spot Instance Interruption Warning'
    )

    return {
        'statusCode': 200,
        'body': json.dumps('Success!')
    }

ポイントは以下

動作確認

このようなメールが届きます。

停止予定時間(Termination Time)も通知されるのがお気に入りです。
このメールをトリガーに後続の対処をしましょう。

終わりに

わかりやすいメールを作成・通知させることができました。
今回はメールで試しましたが、webhookなどでチャットツールに連携するのも容易です。

参考

AWS各リソースのTerraformドキュメント

構成

https://zenn.dev/nakam_aws/articles/cc37dcd8ab8792

Lambdaリソースポリシー

https://dev.classmethod.jp/articles/policies-for-lambda/
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/access-control-resource-based.html

Discussion