S3イベント通知→SNS→SQSをTerraformで作る

2020.03.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

S3イベント通知機能で、S3へのファイルアップロードをSNS→SQSへ通知する処理をTerraformで作成する機会がありましたので、レポートします。

作成するリソースの概要

  • もちろんですが、S3、SNS、SQSを作成します。SNSについてはトピックとサブスクリプションが必要です。
  • 各リソース間のアクセス許可が必要です。
    • S3イベント通知からSNSトピックへイベントを送信するには、SNSトピックのアクセスポリシーでS3がSNSメッセージを発行するのを許可する必要があります。
    • SNSからSQSへメッセージを渡すのにも、SQSキューのアクセス許可設定にてSNSからのメッセージ送信を許可する必要があります。

コード内容解説

S3

resource aws_s3_bucket main {
  bucket_prefix = "event-notification"
  acl    = "private"
  force_destroy = true
}
resource aws_s3_bucket_public_access_block main {
  bucket                  = aws_s3_bucket.main.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

aws_s3_bucket_public_access_block はS3イベント通知とは関係ありませんが、以下のエントリにあるように基本的に設定しておくべきだと思っているので、設定しています。

SNSトピック

resource aws_sns_topic s3_put_object_notification {
  name = "event-notification-test-topic"
}

特に解説することはありません。キチンとロギングしたいのであれば xxx_success_feedback_role_arn xxx_failure_feedback_role_arn xxx_success_feedback_sample_rateといったargumentを指定してCloudWatch Logsに実行ログを送ることを検討しましょう。(xxxにはサブスクライブするリソースの種類、つまりlambda/sqs/http/applicationが入ります。)

SNSトピックのアクセスポリシー

S3イベント通知からSNSメッセージを発行できるようにするポリシーの設定です。

resource aws_sns_topic_policy from-s3 {
  arn = aws_sns_topic.s3_put_object_notification.arn
  policy = templatefile(
    "./sns_policy_triggered_by_s3_event_notification.json",
    {
      sns-arn    = aws_sns_topic.s3_put_object_notification.arn,
      bucket-arn = aws_s3_bucket.main.arn
    }
  )
}

sns_policy_triggered_by_s3_event_notification.json

{
    "Version":"2012-10-17",
    "Statement":[{
        "Effect": "Allow",
        "Principal": {"Service":"s3.amazonaws.com"},
        "Action": "SNS:Publish",
        "Resource":  "${sns-arn}",
        "Condition":{
            "ArnLike":{"aws:SourceArn":"${bucket-arn}"}
        }
    }]
}

今気づきましたが、リソースベースのポリシーでResourceを指定する(7行目)の、意味ないですね…

S3イベント通知

resource aws_s3_bucket_notification put_object {
  bucket = aws_s3_bucket.main.bucket
  topic {
    topic_arn = aws_sns_topic.s3_put_object_notification.arn
    events = [
      "s3:ObjectCreated:Put",
    ]
  }
}

ここまでで、S3イベント通知→SNSが実現できました。

以降はSNS→SQSの部分です。

SQSキュー

resource aws_sqs_queue s3_put_object_notification {
  name                       = "put-object-notification-queue"
  visibility_timeout_seconds = 300
  message_retention_seconds  = 60 * 60 * 24 * 7 * 2
  receive_wait_time_seconds  = 20
  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.s3_put_object_notification_dlq.arn
    maxReceiveCount     = 4
  })
}
resource aws_sqs_queue s3_put_object_notification_dlq {
  name                       = "put-object-notification-dlq"
  visibility_timeout_seconds = 300
  message_retention_seconds  = 60 * 60 * 24 * 7 * 2
  receive_wait_time_seconds  = 20
}
resource aws_cloudwatch_metric_alarm dlq_recieved_message {
  alarm_name                = "put-object-notification-dlq-recieved-message"
  comparison_operator       = "GreaterThanOrEqualToThreshold"
  evaluation_periods        = "1"
  metric_name               = "ApproximateNumberOfMessagesVisible"
  namespace                 = "AWS/SQS"
  period                    = "60"
  statistic                 = "Sum"
  threshold                 = "1"
  alarm_description         = "dead letter queue received message"
  alarm_actions             = [aws_sns_topic.dlq_recieved_message_notification.arn]
  insufficient_data_actions = []
  dimensions = {
    QueueName = aws_sqs_queue.s3_put_object_notification_dlq.name
  }
}
resource aws_sns_topic dlq_recieved_message_notification {
  name = "dlq-recieved-message-notification-topic"
}

エラーを補足するためredrive_policy でデッドレターキューを設定しています。

さらにデッドレターキューにメッセージが入ったことに気づくためCloudWatchAlarm→SNSでメール通知をします。

SNSトピックのEメールサブスクリプションはTerraformでは実装できません。メールでの承認後にしかARNが生成されず、それがTerraformの仕様に合わないためだとのことです。こちらのページのProtocols supported欄最下部に説明があります。そのためEメールサブスクリプションはTerraform外で作成してください。

SQSキューのアクセス許可

SNSにSQSメッセージを送信できる権限を与えます。

resource aws_sqs_queue_policy s3-put-s3_put_object_notification-notification {
  queue_url = aws_sqs_queue.s3_put_object_notification.id
  policy = templatefile(
    "./sqs_policy.json",
    {
      sqs-arn = aws_sqs_queue.s3_put_object_notification.arn,
      sns-arn = aws_sns_topic.s3_put_object_notification.arn
    }
  )
}

sqs_policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "sns.amazonaws.com"
      },
      "Action": "sqs:SendMessage",
      "Resource": "${sqs-arn}",
      "Condition": {
        "ArnEquals": {
          "aws:SourceArn": "${sns-arn}"
        }
      }
    }
  ]
}

これも別にResource句要らないですね。

SQSのSNSトピックサブスクリプション

resource aws_sns_topic_subscription sqs {
  topic_arn = aws_sns_topic.s3_put_object_notification.arn
  protocol  = "sqs"
  endpoint  = aws_sqs_queue.s3_put_object_notification.arn
}

このSNSトピックサブスクリプションに対してデッドレターキューを設定することも可能です。が、Terraformは2020年3月末現在(aws provider version 2.55.0)未対応のようです。(おそらくPRはこれ)

SNSトピックサブスクリプションのデッドレターキューについては以下を参照ください。

コード全体

# S3
resource aws_s3_bucket main {
  bucket_prefix = "event-notification"
  acl    = "private"
  force_destroy = true
}
resource aws_s3_bucket_public_access_block main {
  bucket                  = aws_s3_bucket.main.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# SNSトピック
resource aws_sns_topic s3_put_object_notification {
  name = "event-notification-test-topic"
}

# SNSトピックのアクセスポリシー
resource aws_sns_topic_policy from_s3 {
  arn = aws_sns_topic.s3_put_object_notification.arn
  policy = templatefile(
    "./sns_policy_triggered_by_s3_event_notification.json",
    {
      sns-arn    = aws_sns_topic.s3_put_object_notification.arn,
      bucket-arn = aws_s3_bucket.main.arn
    }
  )
}

# S3イベント通知
resource aws_s3_bucket_notification put_object {
  bucket = aws_s3_bucket.main.bucket
  topic {
    topic_arn = aws_sns_topic.s3_put_object_notification.arn
    events = [
      "s3:ObjectCreated:Put",
    ]
  }
}

# SQSキュー
resource aws_sqs_queue s3_put_object_notification {
  name                       = "put-object-notification-queue"
  visibility_timeout_seconds = 300
  message_retention_seconds  = 60 * 60 * 24 * 7 * 2
  receive_wait_time_seconds  = 20
  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.s3_put_object_notification_dlq.arn
    maxReceiveCount     = 4
  })
}
resource aws_sqs_queue s3_put_object_notification_dlq {
  name                       = "put-object-notification-dlq"
  visibility_timeout_seconds = 300
  message_retention_seconds  = 60 * 60 * 24 * 7 * 2
  receive_wait_time_seconds  = 20
}
resource aws_cloudwatch_metric_alarm dlq_recieved_message {
  alarm_name                = "put-object-notification-dlq-recieved-message"
  comparison_operator       = "GreaterThanOrEqualToThreshold"
  evaluation_periods        = "1"
  metric_name               = "ApproximateNumberOfMessagesVisible"
  namespace                 = "AWS/SQS"
  period                    = "60"
  statistic                 = "Sum"
  threshold                 = "1"
  alarm_description         = "dead letter queue received message"
  alarm_actions             = [aws_sns_topic.dlq_recieved_message_notification.arn]
  insufficient_data_actions = []
  dimensions = {
    QueueName = aws_sqs_queue.s3_put_object_notification_dlq.name
  }
}
resource aws_sns_topic dlq_recieved_message_notification {
  name = "dlq-recieved-message-notification-topic"
}

# SQSキューのアクセス許可
resource aws_sqs_queue_policy s3_put_object_notification {
  queue_url = aws_sqs_queue.s3_put_object_notification.id
  policy = templatefile(
    "./sqs_policy.json",
    {
      sqs-arn = aws_sqs_queue.s3_put_object_notification.arn,
      sns-arn = aws_sns_topic.s3_put_object_notification.arn
    }
  )
}

# SQSのSNSトピックサブスクリプション
resource aws_sns_topic_subscription sqs {
  topic_arn = aws_sns_topic.s3_put_object_notification.arn
  protocol  = "sqs"
  endpoint  = aws_sqs_queue.s3_put_object_notification.arn
}

sns_policy_triggered_by_s3_event_notification.json

{
    "Version":"2012-10-17",
    "Statement":[{
        "Effect": "Allow",
        "Principal": {"Service":"s3.amazonaws.com"},
        "Action": "SNS:Publish",
        "Resource":  "${sns-arn}",
        "Condition":{
            "ArnLike":{"aws:SourceArn":"${bucket-arn}"}
        }
    }]
}

sqs_policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "sns.amazonaws.com"
      },
      "Action": "sqs:SendMessage",
      "Resource": "${sqs-arn}",
      "Condition": {
        "ArnEquals": {
          "aws:SourceArn": "${sns-arn}"
        }
      }
    }
  ]
}

あわせて読みたい

S3イベント通知とSQSの間にSNSを挟んだ理由は以下をご参照ください。