Terraform v0.8.2でAmazon SESを設定してみた – イベント発行を利用したKinesis Firehose及びCloudWatchとの連携

Terraform

はじめに

こんにちは、中山です。

前回のエントリに引き続き、Terraformを利用したAmazon Simple Email Service(以下SES)の設定方法についてご紹介します。今回は新規に導入されたリソースの中からメール送信関連のものについてです。このタイプのリソースでは以下2つのリソースに対応しています。

執筆時点(2016/12/28)では設定セット及びその中で定義するイベント発行先の指定にのみ対応しています。SESには イベント発行 という機能があり、SESから送信されたメールの各種イベントを契機としてKinesis Firehose及びCloudWatchと連携することが可能です。詳細なドキュメントはこちらです。私自身あまり馴染みのない概念だったので、自分の整理も兼ねてまずはその機能をご紹介します。

検証に利用したTerraformのバージョンは現在の最新版である0.8.2です。バージョンによって内容が異なる場合があります。その点ご了承ください。

イベント発行とは何か

イベント発行とは一言で言うと SESから送信したメールを監視/分析する仕組み です。メールについているタグやヘッダの情報などに基づき、メールがどの程度正常に送信できたのか、バウンスメールがどれくらい発生しているのかといった情報を収集することが可能です。事前に定義されたイベントが発生した際にイベント発行先となるKinesis Firehose及びCloudWatchへその内容を通知することができます。イベントで定義する内容は設定セットというグループの中で指定し、設定セットの中に複数のイベント及びその発行先を定義することが可能です。現時点でイベントとして定義されているのは以下の5つです。先程のドキュメントから引用します。

イベント 意味
Send(送信) Amazon SES への API コールに成功したため、Amazon SES がメールの配信を試みます。
Reject(拒否) Amazon SES は、最初にメールを受け入れましたが、後でウイルスを検出して拒否しました。
Bounce(バウンス) 受取人のメールサーバーにより、メールは完全に拒否されました。このイベントはハードバウンスに該当します。ソフトバウンスは、Amazon SES が一定期間にわたって再試行してもメールを配信できなかった場合に限ります。
Complaint(苦情) 受取人がメールをスパムとしてマークしました。
Delivery(配信) Amazon SES が受取人のメールサーバーに E メールを正常に送信しました。

イベント発行先の使い分けはどう考えればよいでしょうか。例えば、特定のタグ(SESのタグにはAPI発行時に自分で追加するものと、SESが自動的に追加する自動タグの2つがある)が付いたメールの数をカウントし、しきい値を超過した場合にアラートを上げたいのであればCloudWatchが最適です。各種イベントが発生した場合に、CloudWatchへ通知するようにすればタグ毎にディメンションを作成してその数をカウントできます。また、CloudWatch Alarmでしきい値及びアラームの設定が可能です。各種イベントの内容をより詳細に分析または可視化したいのであればKinesis Firehoseが選択肢になります。Kinesis FirehoseからS3/Amazon ES/DynamoDB/Kinesis Analyticsと連携させることによりイベントの内容を分析/可視化することができます。もちろん、それらサービスを別のサービスと連携させてより複雑な構成にすることも可能です。

tf-ses-1

Terraformのリソースで指定可能な引数

イベント発行の概要についてご紹介したので、Terraformに戻ります。それぞれのリソースで設定可能な引数は以下の通りです。

  • aws_ses_configuration_set
設定 内容 必須の有無
name 設定セット名 Yes
  • aws_ses_event_destination
設定 内容 必須の有無
name イベント発行先名 Yes
configuration_set_name 関連付ける設定セット名 Yes
enabled このイベント発行先を有効化するかどうか No
matching_types イベント発行先へ通知させる条件( send / reject / bounce / complaint / delivery から選択) Yes
cloudwatch_destination イベント発行先としてのCloudWatchの設定(下記参照) No
kinesis_destination イベント発行先としてのKinesis Firehoseの設定(下記参照) No

各イベント通知先で設定可能な引数は以下の通りです。 cloudwatch_destinationkinesis_destination は同時に指定できないようなのでご注意ください。

  • cloudwatch_destination
設定 内容 必須の有無
default_value dimension_name で指定したキーのバリューに特に何も指定されてない場合にデフォルトで利用される文字列 Yes
dimension_name ディメンション名として利用される文字列 Yes
value_source ディメンションの値をメールのどこから探すかを指定( messageTag または emailHeader が指定可能) Yes

補足すると、 messageTag を選択した場合、ディメンション名をSES固有のヘッダまたはAPIに渡したパラメータから検索します。すでにメールのヘッダに対して固有の設定をしている場合は emailHeader を選択してください。メールのヘッダを解析してディメンション名を決定します。より詳細な情報についてはこちらのドキュメントを参照してください。

  • kinesis_destination
設定 内容 必須の有無
stream_arn イベント通知先として関連付けるKinesis FirehoseのARN Yes
role_arn SESに関連付けるIAM RoleのARN Yes

やってみる

今回はこのイベント発行を利用して以下の設定をしてみます。イベントタイプにはSend(送信)を利用します。

  • Kinesis Firehoseにイベントを通知してS3バケットにSendとなったメール情報をput
  • CloudWatchにイベントを通知して特定のタグが付いたSend数をカウント

図にすると以下の通りです。

tf-ses-2

本エントリ用に作成したコードはGitHubにアップロードしておいたので、ご自由にお使いください。

前回のエントリでも書きましたが、現在のところTerraformでは検証用トークンの生成に対応してないのでアイデンティティの作成ができません。本エントリではすでに検証済みのアイデンティティが存在し、かつメール送受信のための各種レコードセットが設定済みの前提で進めます。設定方法については以下のエントリを参照してください。

Terraformのコード

SESは以下のコードで作成しました。

  • configuration-set/ses.tf
resource "aws_ses_configuration_set" "ses" {
  name = "tf-configuration-set"
}

resource "aws_ses_event_destination" "firehose" {
  name                   = "tf-firehose-destination"
  configuration_set_name = "${aws_ses_configuration_set.ses.name}"
  enabled                = true

  matching_types = [
    "send",
  ]

  kinesis_destination = {
    stream_arn = "${aws_kinesis_firehose_delivery_stream.firehose.arn}"
    role_arn   = "${aws_iam_role.ses.arn}"
  }
}

resource "aws_ses_event_destination" "cloudwatch" {
  name                   = "tf-cloudwatch-destination"
  configuration_set_name = "${aws_ses_configuration_set.ses.name}"
  enabled                = true

  matching_types = [
    "send",
  ]

  cloudwatch_destination = {
    default_value  = "unknown"
    dimension_name = "campaign"
    value_source   = "messageTag"
  }
}

aws_ses_configuration_set リソースで設定セットを作成、 aws_ses_event_destination でKinesis Firehose及びCloudWatch用のイベント発行を定義しています。他のAWSリソースについてもご紹介します。まずはKinesis Firehoseです。設定は以下のようにしました。

  • configuration-set/kinesis.tf
resource "aws_cloudwatch_log_group" "cloudwatch" {
  name = "/aws/kinesisfirehose/tf-firehose-log-group"
}

resource "aws_cloudwatch_log_stream" "cloudwatch" {
  name           = "tf-firehose-log-stream"
  log_group_name = "${aws_cloudwatch_log_group.cloudwatch.name}"
}

resource "aws_kinesis_firehose_delivery_stream" "firehose" {
  name        = "tf-delivery-stream"
  destination = "s3"

  s3_configuration {
    role_arn        = "${aws_iam_role.firehose.arn}"
    bucket_arn      = "${aws_s3_bucket.s3.arn}"
    prefix          = "ses"
    buffer_interval = 60

    cloudwatch_logging_options = {
      enabled         = true
      log_group_name  = "${aws_cloudwatch_log_group.cloudwatch.name}"
      log_stream_name = "${aws_cloudwatch_log_stream.cloudwatch.name}"
    }
  }
}

CloudWatch Logsのロググループとログストリームを作成し、それをKinesis Frehoseで利用しています。 s3_configuration でメール情報のput先となるS3バケットの設定をしています。 aws_kinesis_firehose_delivery_stream リソースのより詳細な情報についてはドキュメントを参照してください。

続いてSES及びKinesis Firehoseに関連付けているIAM Roleについてご紹介します。まずはSESのIAM Roleからご紹介します。

  • configuration-set/iam.tf (SES関連部分を抜粋)
data "aws_caller_identity" "current" {}

data "aws_iam_policy_document" "ses_assume_role" {
  statement {
    sid    = "SESAssumeRole"
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals = {
      type = "Service"

      identifiers = [
        "ses.amazonaws.com",
      ]
    }

    condition {
      test     = "StringEquals"
      variable = "sts:ExternalId"

      values = [
        "${data.aws_caller_identity.current.account_id}",
      ]
    }
  }
}

resource "aws_iam_role" "ses" {
  name               = "tf-ses-role"
  assume_role_policy = "${data.aws_iam_policy_document.ses_assume_role.json}"
}

data "aws_iam_policy_document" "ses_policy" {
  statement {
    sid = "GiveSESPermissionToPutFirehose"

    actions = [
      "firehose:PutRecord",
      "firehose:PutRecordBatch",
    ]

    resources = [
      "${aws_kinesis_firehose_delivery_stream.firehose.arn}",
    ]
  }
}

resource "aws_iam_policy" "ses" {
  name   = "tf-ses-policy"
  path   = "/"
  policy = "${data.aws_iam_policy_document.ses_policy.json}"
}

resource "aws_iam_policy_attachment" "ses" {
  name       = "GiveSESPermissionToPutFirehose"
  roles      = ["${aws_iam_role.ses.name}"]
  policy_arn = "${aws_iam_policy.ses.arn}"
}

AWSアカウントIDのようなハードコードしたくない情報はaws_caller_identityデータソースで取得するようにしましょう。SESからKinesis Firehoseに対してレコードをputする必要があるため、 firehose:PutRecord 及び firehose:PutRecordBatch 権限を付与しています。IAM Roleに関連付けるポリシーの作成にはaws_iam_role_policyリソースが使えるのですが、JSONを生で書くのはシンドいのでaws_iam_policy_documentデータソースを利用すると便利です。HCLと同じ書式で記述できます。

  • configuration-set/iam.tf (Kinesis Firehose関連部分を抜粋)
data "aws_iam_policy_document" "firehose_assume_role" {
  statement {
    sid    = "KinesisAssumeRole"
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals = {
      type = "Service"

      identifiers = [
        "firehose.amazonaws.com",
      ]
    }

    condition {
      test     = "StringEquals"
      variable = "sts:ExternalId"

      values = [
        "${data.aws_caller_identity.current.account_id}",
      ]
    }
  }
}

resource "aws_iam_role" "firehose" {
  name               = "tf-firehose-role"
  assume_role_policy = "${data.aws_iam_policy_document.firehose_assume_role.json}"
}

data "aws_iam_policy_document" "firehose_policy" {
  statement {
    sid    = "GiveFirehosePermissionToPutS3Bucket"
    effect = "Allow"

    actions = [
      "s3:AbortMultipartUpload",
      "s3:GetBucketLocation",
      "s3:GetObject",
      "s3:ListBucket",
      "s3:ListBucketMultipartUploads",
      "s3:PutObject",
    ]

    resources = [
      "${aws_s3_bucket.s3.arn}",
      "${aws_s3_bucket.s3.arn}/*",
    ]
  }

  statement = {
    sid    = "GiveFirehosePermissionToPutLogEvents"
    effect = "Allow"

    actions = [
      "logs:PutLogEvents",
    ]

    resources = [
      "${aws_cloudwatch_log_stream.cloudwatch.arn}",
    ]
  }
}

resource "aws_iam_policy" "firehose" {
  name   = "tf-firehose-policy"
  path   = "/"
  policy = "${data.aws_iam_policy_document.firehose_policy.json}"
}

resource "aws_iam_policy_attachment" "firehose" {
  name       = "GiveFirehosePermissionToS3AndCWLogs"
  roles      = ["${aws_iam_role.firehose.name}"]
  policy_arn = "${aws_iam_policy.firehose.arn}"
}

Kinesis FirehoseからS3バケットに対してオブジェクトをputする必要があるのでその権限を付与しています。put以外で指定してるアクションがどう利用されるのか正直良く分かってないですが(ごめんなさい。。。)、マネジメントコンソールからS3を宛先としてKinesis Firehoseを設定した際に作成されるIAMポリシーを参考にしてこの形にしました。また、CloudWatch Logsへログをputさせるために logs:PutLogEvents 権限を付与しています。

動作確認

Terraformの実行後、意図した動作をするのか確認してみます。ちなみにですが、SESに関連付けるIAM Roleの反映に時間が掛るためなのか、初回 apply 時にKinesis Firehoseとの関連付けに失敗すると思います。。。その場合、もう一度 apply すれば成功するかと思います。

今回はSendイベントを契機に各種イベント発行先へ通知がいくかを確認します。メールの送信には aws ses send-email サブコマンドを利用します。 --configuration-set-name オプションで利用する設定セットを、 --tags オプションでディメンションとして利用されるタグを指定可能です。

# 検証済みドメインと送信先メールアドレスを変数に代入
$ domain=<_YOUR_VERIFICATION_DOMAIN_>
$ email=<_YOUR_EMAIL_ADDRESS_>
# タグを付けてテストメールの送信
$ aws ses send-email \
  --from aaa@$domain \
  --to $email \
  --subject subject \
  --text body \
  --configuration-set-name tf-configuration-set \
  --tags Name=campaign,Value=test
{
    "MessageId": "0100015948b0f878-f789b690-827d-458d-ab91-4d73c7078214-000000"
}
# Kinesis FirehoseからS3バケットにputされていることを確認
$ aws s3 ls s3://<_YOUR_S3_BUCKET_> \
  --recursive
2016-12-29 12:49:04       1023 ses2016/12/29/03/tf-delivery-stream-1-2016-12-29-03-48-02-5fbcfa56-ae28-403d-977e-ca3eb99c73d9
# Sendイベントが発生し、かつタグが付いていることを確認
$ aws s3 cp s3://<_YOUR_S3_BUCKET_>/ses2016/12/29/03/tf-delivery-stream-1-2016-12-29-03-48-02-5fbcfa56-ae28-403d-977e-ca3eb99c73d9 - \
  | jq .
{
  "eventType": "Send",
  "mail": {
    "timestamp": "2016-12-29T03:47:54.616Z",
    "source": "aaa@<_YOUR_VERIFICATION_DOMAIN_>",
    "sourceArn": "arn:aws:ses:us-east-1:************:identity/<_YOUR_VERIFICATION_DOMAIN_>",
    "sendingAccountId": "************",
    "messageId": "0100015948b0f878-f789b690-827d-458d-ab91-4d73c7078214-000000",
    "destination": [
      "<_YOUR_EMAIL_ADDRESS_>"
    ],
    "headersTruncated": false,
    "headers": [
      {
        "name": "From",
        "value": "aaa@<_YOUR_VERIFICATION_DOMAIN_>"
      },
      {
        "name": "To",
        "value": "<_YOUR_EMAIL_ADDRESS_>"
      },
      {
        "name": "Subject",
        "value": "subject"
      },
      {
        "name": "MIME-Version",
        "value": "1.0"
      },
      {
        "name": "Content-Type",
        "value": "text/plain; charset=UTF-8"
      },
      {
        "name": "Content-Transfer-Encoding",
        "value": "7bit"
      }
    ],
    "commonHeaders": {
      "from": [
        "aaa@<_YOUR_VERIFICATION_DOMAIN_>"
      ],
      "to": [
        "<_YOUR_EMAIL_ADDRESS_>"
      ],
      "messageId": "0100015948b0f878-f789b690-827d-458d-ab91-4d73c7078214-000000",
      "subject": "subject"
    },
    "tags": {
      "ses:configuration-set": [
        "tf-configuration-set"
      ],
      "ses:source-ip": [
        "**************"
      ],
      "ses:from-domain": [
        "<_YOUR_VERIFICATION_DOMAIN_>"
      ],
      "campaign": [
        "test"
      ],
      "ses:caller-identity": [
        "dev"
      ]
    }
  },
  "send": {}
}

続いてCloudWatchへのイベント通知を検証してみます。今回の設定だと以下の内容でメトリックが収集されます。

  • メトリック名: Send
  • ディメンション名: campaign
  • ディメンション値: test

AWS CLIだと以下のコマンドで確認できます。

$ aws cloudwatch get-metric-statistics \
  --namespace AWS/SES \
  --metric-name Send \
  --start-time "$(date -v1d '+%FT%T%z')" \
  --end-time "$(date '+%FT%T%z')" \
  --period 3600 \
  --statistics Sum \
  --dimensions Name=campaign,Value=test
{
    "Datapoints": [
        {
            "Timestamp": "2016-12-29T02:55:00Z",
            "Sum": 1.0,
            "Unit": "Count"
        }
    ],
    "Label": "Send"
}

正常にカウントされているようです。やりましたね。

まとめ

いかがだったでしょうか。

Terraformを利用したSESのイベント発行でKinesis Firehose及びCloudWatchと連携する方法をご紹介しました。やはり、宣言的なコードとしてSESを管理できると簡単に同じ環境を複製でき便利ですね。個人的にはServerless FrameworkでもSESを利用したいと考えているので、CloudFormationもSES対応して欲しいです。AWSさんぜひお願いします!!!111

本エントリがみなさんの参考になれば幸いに思います。