Amazon SESのメール送信ログの保存をCloudFormationで設定・構築してみました。
目的
メールサービスであるAmazon SESですが、メール送信ログの保存には少し手間がかかります。
また色々探してみてもコンソール経由で設定される方が多く、CloudFormationで設定・構築できたら楽だなと思い、作ってみました。
前提
Amazon SESの構築自体の手順は省略します。
本記事は、すでにSESを構築されている前提になります。
登場サービス
- SES
- Configuration Sets含む
- Kinesis Data Firehose
- S3
- IAMロール
- CloudWatch Logs
参考
SESのログの保存にはKinesis Data Firehoseを使用するのですが、実はAWS WAFのログの保存も同じくKinesis Data Firehoseで行います。
そこでWAFのログの方であれば、以前にクラスメソッド様のDevelopers IOで以下のような素晴らしい記事を拝見し自分も構築の参考にさせて頂いたことがあったため、今回こちらの方法を参考にしてSESの方でも使えるようにしてみました。
方法
概要
全体的な流れは以下のようになります。
- CloudFormationでKinesis Data Firehoseなどログ送信の基盤を作成する
- SESのConfiguration Setsを作成する
- SESにConfiguration Setsを紐付ける
- デフォルト設定セットとして設定する
コード
ディレクトリ構成
. ├── cfn_yaml │ └── ses_log.yaml └── ses_log.sh
※GitHubにもあります。
ses_log.yaml
AWSTemplateFormatVersion: "2010-09-09" Description: SES Log Metadata: "AWS::CloudFormation::Interface": ParameterGroups: - Label: default: "ConfigurationSetArn" Parameters: - ConfigurationSetArn - Label: default: "Kinesis Firehose" Parameters: - SizeInMBs - IntervalInSeconds - CompressionFormat - Label: default: "S3" Parameters: - SesLogsBucketName - ExpirationInDays # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: ConfigurationSetArn: Type: String SesLogsBucketName: Description: "A name for logs bucket. " Type: String ExpirationInDays: Description: "Indicates the number of days after creation when objects are deleted from Amazon S3" Type: Number Default: 90 SizeInMBs: Description: "The size of the buffer, in MBs, that Kinesis Data Firehose uses for incoming data before delivering it to the destination." Type: Number Default: 5 MinValue: 1 MaxValue: 128 IntervalInSeconds: Description: The length of time, in seconds, that Kinesis Data Firehose buffers incoming data before delivering it to the destination. Type: Number Default: 300 MinValue: 60 MaxValue: 900 CompressionFormat: Description: "The type of compression that Kinesis Data Firehose uses to compress the data that it delivers to the Amazon S3 bucket. " Type: String Default: "ZIP" AllowedValues: ["GZIP", "Snappy", "UNCOMPRESSED", "ZIP"] Resources: # ------------------------------------------------------------# # IAM Role for SES to Kinesis # ------------------------------------------------------------# SesLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${AWS::StackName}-SesLogRole" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: ses.amazonaws.com Condition: StringEquals: AWS:SourceAccount: !Ref AWS::AccountId AWS:SourceArn: !Ref ConfigurationSetArn Policies: - PolicyDocument: Version: "2012-10-17" Statement: - Action: - firehose:PutRecordBatch Effect: Allow Resource: !GetAtt SesLogDeliveryStream.Arn PolicyName: ses_log_role_policy # ------------------------------------------------------------# # SES Logging # ------------------------------------------------------------# SesLogsS3Bucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: BucketName: !Ref SesLogsBucketName BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 LifecycleConfiguration: Rules: - Id: !Sub "ExpirationIn-${ExpirationInDays}Days" ExpirationInDays: !Ref "ExpirationInDays" Status: Enabled PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True VersioningConfiguration: Status: Enabled SesLogsS3BucketPolicy: Type: "AWS::S3::BucketPolicy" Properties: Bucket: Ref: SesLogsS3Bucket PolicyDocument: Statement: - Action: - "s3:AbortMultipartUpload" - "s3:GetBucketLocation" - "s3:GetObject" - "s3:ListBucket" - "s3:ListBucketMultipartUploads" - "s3:PutObject" - "s3:PutObjectAcl" Effect: Allow Resource: - "Fn::Join": - "" - - "arn:aws:s3:::" - Ref: SesLogsS3Bucket - /* - "Fn::Join": - "" - - "arn:aws:s3:::" - Ref: SesLogsS3Bucket Principal: AWS: !Sub "${SesLogFirehoseRole.Arn}" SesLogDeliveryStream: Type: AWS::KinesisFirehose::DeliveryStream Properties: DeliveryStreamName: !Sub "aws-ses-logs-${AWS::StackName}" DeliveryStreamType: DirectPut S3DestinationConfiguration: BucketARN: !Sub "${SesLogsS3Bucket.Arn}" BufferingHints: SizeInMBs: !Ref SizeInMBs IntervalInSeconds: !Ref IntervalInSeconds CloudWatchLoggingOptions: Enabled: true LogGroupName: !Sub "/aws/kinesisfirehose/aws-ses-logs-${AWS::StackName}" LogStreamName: S3Delivery CompressionFormat: !Ref CompressionFormat EncryptionConfiguration: NoEncryptionConfig: NoEncryption ErrorOutputPrefix: "ses-logs-error-" Prefix: "ses-logs-" RoleARN: !Sub "${SesLogFirehoseRole.Arn}" SesLogDeliveryStreamLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/kinesisfirehose/aws-ses-logs-${AWS::StackName}" RetentionInDays: 400 SesLogDeliveryStreamLogStream: Type: AWS::Logs::LogStream Properties: LogGroupName: !Ref SesLogDeliveryStreamLogGroup LogStreamName: S3Delivery SesLogFirehoseRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${AWS::StackName}-SesLogFirehoseRole" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: firehose.amazonaws.com Policies: - PolicyDocument: Version: "2012-10-17" Statement: - Action: - glue:GetTable - glue:GetTableVersion - glue:GetTableVersions Effect: Allow Resource: "*" - Action: - s3:AbortMultipartUpload - s3:GetBucketLocation - s3:GetObject - s3:ListBucket - s3:ListBucketMultipartUploads - s3:PutObject Effect: Allow Resource: - !Sub "${SesLogsS3Bucket.Arn}" - !Sub "${SesLogsS3Bucket.Arn}/*" - arn:aws:s3:::%FIREHOSE_BUCKET_NAME% - arn:aws:s3:::%FIREHOSE_BUCKET_NAME%/* - Action: kms:Decrypt Effect: Allow Resource: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/%SSE_KEY_ID%" - Action: - lambda:InvokeFunction - lambda:GetFunctionConfiguration Effect: Allow Resource: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:%FIREHOSE_DEFAULT_FUNCTION%:%FIREHOSE_DEFAULT_VERSION%" - Action: logs:PutLogEvents Effect: Allow Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kinesisfirehose/aws-ses-logs-${AWS::StackName}:log-stream:*" - Action: - kinesis:DescribeStream - kinesis:GetShardIterator - kinesis:GetRecords Effect: Allow Resource: !Sub "arn:aws:kinesis:${AWS::Region}:${AWS::AccountId}:stream/%FIREHOSE_STREAM_NAME%" PolicyName: firehose_delivery_role_policy # ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: SesLogDeliveryStreamArn: Value: !GetAtt SesLogDeliveryStream.Arn Export: Name: !Sub "SesLogDeliveryStream-Arn-${AWS::StackName}" SesLogRoleArn: Value: !GetAtt SesLogRole.Arn Export: Name: !Sub "SesLogRole-Arn-${AWS::StackName}"
ses_log.sh
#!/bin/bash set -eu cd $(dirname $0) CfnTemplate="./cfn_yaml/ses_log.yaml" Region=${AWS_DEFAULT_REGION:-ap-northeast-1} ### -------------------------------- ### ### ドメインなどを記入 ### -------------------------------- ### StackPrefix="example" Domain="example.com" ### -------------------------------- ### ### SES Log用のKinesis Firehose作成 ### -------------------------------- ### CfnStackName="${StackPrefix}-SES-Log" AccountID=$(aws sts get-caller-identity --query "Account" --output text) SesLogsBucketName=`echo "${CfnStackName}-${Region}" | tr '[:upper:]' '[:lower:]'` ConfigurationSetName=$(echo ${Domain} | sed -e "s/\./-/g") ConfigurationSetArn="arn:aws:ses:${Region}:${AccountID}:configuration-set/${ConfigurationSetName}" ### Kinesis設定項目 ExpirationInDays="400" SizeInMBs="5" IntervalInSeconds="300" CompressionFormat="GZIP" aws cloudformation deploy \ --region ${Region} \ --stack-name ${CfnStackName} \ --template-file ${CfnTemplate} \ --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \ --no-fail-on-empty-changeset \ --parameter-overrides \ SesLogsBucketName=${SesLogsBucketName} \ ExpirationInDays=${ExpirationInDays} \ SizeInMBs=${SizeInMBs} \ IntervalInSeconds=${IntervalInSeconds} \ CompressionFormat=${CompressionFormat} \ ConfigurationSetArn=${ConfigurationSetArn} ### -------------------------------- ### ### Configuration Setの設定 ### ※東京でCFn対応していないのでCLIで ### -------------------------------- ### if [[ -z $(aws ses describe-configuration-set \ --configuration-set-name "${ConfigurationSetName}" \ --output json \ 2>/dev/null || :) ]]; then # configuration set作成 aws ses create-configuration-set \ --configuration-set Name=${ConfigurationSetName} \ > /dev/null CfnOutput=$(aws cloudformation describe-stacks --stack-name "${CfnStackName}") DeliveryStreamARN=$(echo "${CfnOutput}" \ | jq '.Stacks[].Outputs[] | select(.ExportName == "SesLogDeliveryStream-Arn-'${CfnStackName}'") | .OutputValue' \ | tr -d '"') IAMRoleARN=$(echo "${CfnOutput}" \ | jq '.Stacks[].Outputs[] | select(.ExportName == "SesLogRole-Arn-'${CfnStackName}'") | .OutputValue' \ | tr -d '"') # event destination EventDestination="{ \"Name\": \"${ConfigurationSetName}-KinesisFirehose\", \"Enabled\": true, \"MatchingEventTypes\": [\"send\", \"reject\", \"bounce\", \"complaint\", \"delivery\", \"open\", \"click\", \"renderingFailure\"], \"KinesisFirehoseDestination\": { \"IAMRoleARN\": \"${IAMRoleARN}\", \"DeliveryStreamARN\": \"${DeliveryStreamARN}\" } }" # configuration setにevent destination(配信先情報)を設定 aws ses create-configuration-set-event-destination \ --configuration-set-name ${ConfigurationSetName} \ --event-destination "${EventDestination}" \ > /dev/null # SESにconfiguration setを紐付け(デフォルト設定セット) aws sesv2 put-email-identity-configuration-set-attributes \ --email-identity "${Domain}" \ --configuration-set-name ${ConfigurationSetName} \ > /dev/null fi echo "Finished."
補足
ses_log.yaml
WAFのログ保存と同じKinesis FirehoseとそのIAMロールが肝ではあるのですが、それだけではなくSESがKinesis Firehoseに配信するためのIAMロールを追加で作成しています。
こちらに関してはAWSのリファレンスに記載があります。
# ------------------------------------------------------------# # IAM Role for SES to Kinesis # ------------------------------------------------------------# SesLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${AWS::StackName}-SesLogRole" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: ses.amazonaws.com Condition: StringEquals: AWS:SourceAccount: !Ref AWS::AccountId AWS:SourceArn: !Ref ConfigurationSetArn Policies: - PolicyDocument: Version: "2012-10-17" Statement: - Action: - firehose:PutRecordBatch Effect: Allow Resource: !GetAtt SesLogDeliveryStream.Arn PolicyName: ses_log_role_policy
また、シェルの方でそのIAMロールとKinesis FirehoseのARNを使用してConfiguration SetのEvent destinationを作成するため、OutputsとしてそれらのARNを出力しています。
# ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: SesLogDeliveryStreamArn: Value: !GetAtt SesLogDeliveryStream.Arn Export: Name: !Sub "SesLogDeliveryStream-Arn-${AWS::StackName}" SesLogRoleArn: Value: !GetAtt SesLogRole.Arn Export: Name: !Sub "SesLogRole-Arn-${AWS::StackName}"
ses_log.sh
まず、SESのConfiguration SetをなぜCloudFormationではなくCLIで作成しているかという点です。
これは、東京リージョンでのCloudFormationでConfiguration Setまわり(ConfigurationSetEventDestination)の作成が対応していなかったからです。
実際にCloudFormationテンプレートを書いて東京リージョンで実行してみたら・・・
An error occurred (ValidationError) when calling the ValidateTemplate operation: Template format error: Unrecognized resource types: [AWS::SES::ConfigurationSetEventDestination]
その後バージニアリージョンで同じテンプレートを実行したら走りました。
今回自分はSESを東京リージョンで構築していたため、CLIで実行することにしました。
SESの東京リージョン対応は比較的最近なので、このようにCloudFormationで対応していなかったり、またCognito統合(CognitoでSESを使用してメール送信する機能)にも対応していなかったりというような点があるので注意が必要です。
ちなみにConfiguration Set自体の作成に関しては、CloudFormationドキュメントにこう書いてありました。そのため、こちらはCloudFormationでの実行を試してすらいません。
Not currently supported by AWS CloudFormation.
また、event destinationというログの配信情報などの設定ですが、ここでは配信できる情報全て(send, reject, bounce, complaint, delivery, open, click, renderingFailure)を設定しています。
必要に応じて絞ると良いかと思います。
さらにここで先ほどCloudFormationで構築したKinesis FirehoseとSES用のIAMロールをConfiguration Setに紐付けます。
# event destination EventDestination="{ \"Name\": \"${ConfigurationSetName}-KinesisFirehose\", \"Enabled\": true, \"MatchingEventTypes\": [\"send\", \"reject\", \"bounce\", \"complaint\", \"delivery\", \"open\", \"click\", \"renderingFailure\"], \"KinesisFirehoseDestination\": { \"IAMRoleARN\": \"${IAMRoleARN}\", \"DeliveryStreamARN\": \"${DeliveryStreamARN}\" } }"
また、最後のSESにConfiguration Setを紐づける点ですが、これだけsesv2 apiを実行しています。
これはただ調べた時にこのapiが出てそのまま使っただけです・・・
できれば全部v2で統一したかったですが、少し大変だったので今後の課題にしました。
# SESにconfiguration setを紐付け(デフォルト設定セット) aws sesv2 put-email-identity-configuration-set-attributes \ --email-identity "${Domain}" \ --configuration-set-name ${ConfigurationSetName} \ > /dev/null
最後に
SESのメール送信ログの保存の設定をCloudFormationで行いました。
コンソールでポチポチやるのが大変な場合はきっと使えるのではないかと思います。