AWS SESにおいてクロスアカウントでOpenSearchに連携してみた

Akira Yagishita
nextbeat-engineering
22 min readAug 31, 2023

挨拶・ブログ執筆の経緯

こんにちは。

ネクストビートでSREを担当している八木下です。

この度、掲題の通り、AWS SESにおいてクロスアカウントでOpenSearchに連携する作業を行いましたので、自身の備忘も兼ねて本ブログを執筆します。

「AWS SES」って何だ?

超絶ざっくり説明すると「メールサーバ」です。既存のドメインを使用してセットアップでき、一つのAWSアカウントで複数ドメインのメール送受信が可能です。

また、Amazon S3に受信メールを保存できる他、AWS Lambdaと連携し、メール受信時に自動でLambda実行させることも可能です。

なぜ、本作業を行うことになったのか。

弊社のAWS SESについては、共通アカウントで複数ドメインを送信していましたが、サービスが増えるに従い、メールの送信件数が増え、将来的に送信制限に到達してしまう可能性が懸念されていました。

また、特段設定を行わなければ、Cloudwatchメトリクス上ではドメイン別のメール送信件数が見れず、どのドメインからのメール送信が多いのか確認できていなかったため、これを改善するため、本作業を実施しました。

設定手順と構成

元々、分析用途で使用しているAWSアカウント上にOpenSearchが存在し、各AWSアカウントから各種ログを転送して閲覧できるようにしていたため、これに準じる形で、設定を行います。

今回実施したのは、以下のとおりとなります。

■共通アカウント側

・AWS SESにおける設定セットの作成(イベント送信先の追加)

・Kinesis firehoseの作成

・IAMロールの作成

■分析用アカウント側

・OpenSearchのアクセスポリシーの修正

今回Terraformで実装しましたので、コードを紹介します。

「共通アカウント側」

/**
* 「共通アカウント側」各種設定
*/

################################################################################
# Amazon Simple Email Service
################################################################################

//// 検証済みID
## reference: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sesv2_email_identity
resource "aws_sesv2_email_identity" "domain_a" {
email_identity = "yagishita.com"
configuration_set_name = aws_sesv2_configuration_set.domain_a.configuration_set_name
}

//// 設定セット
/// デフォルト設定セット
## reference: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sesv2_configuration_set
resource "aws_sesv2_configuration_set" "domain_a" {
configuration_set_name = "yagishita_com"
}

/// イベント送信先
## reference: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sesv2_configuration_set_event_destination
resource "aws_sesv2_configuration_set_event_destination" "domain_a_kinesis_firehose" {
configuration_set_name = aws_sesv2_configuration_set.domain_a.configuration_set_name

// '.(ドット)'が使用できないため、'_(アンダースコア)'に置換しています。
event_destination_name = "kinesis-firehose-${replace(aws_sesv2_email_identity.domain_a.email_identity, ".", "_")}"

//
event_destination {
kinesis_firehose_destination {
delivery_stream_arn = aws_kinesis_firehose_delivery_stream.kinesis_firehose_for_ses.arn
iam_role_arn = aws_iam_role.for_ses_log_transfer.arn
}

enabled = true
matching_event_types = [
"BOUNCE",
"CLICK",
"COMPLAINT",
"DELIVERY",
"DELIVERY_DELAY",
"OPEN",
"REJECT",
"RENDERING_FAILURE",
"SEND",
"SUBSCRIPTION",
]
}


depends_on = [
aws_kinesis_firehose_delivery_stream.kinesis_firehose_for_ses,
]
}

################################################################################
# Amazon Kinesis
################################################################################

//// Amazon Kinesis Data Firehose
## reference: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kinesis_firehose_delivery_stream
resource "aws_kinesis_firehose_delivery_stream" "kinesis_firehose_for_ses" {
name = "prod-ses-log-transfer"
destination = "elasticsearch"

elasticsearch_configuration {
// 送信先の設定
cluster_endpoint = "https://${data.terraform_remote_state.analysis.outputs.stats_es_log_domain_endpoint}"
index_name = "ses-yagishita"
index_rotation_period = "OneWeek"
retry_duration = 300
buffering_size = 5
buffering_interval = 300

// バックアップの設定
s3_backup_mode = "FailedDocumentsOnly"
s3_configuration {
role_arn = aws_iam_role.for_ses_log_transfer.arn
bucket_arn = module.s3_bucket_for_kinesis_firehose.s3_bucket_arn
buffering_size = 128
buffering_interval = 128
compression_format = "GZIP"
}

// サービスアクセス(IAMロール)
role_arn = aws_iam_role.for_ses_log_transfer.arn
}

//// 詳細設定
// サーバー側の暗号化(SSE)
server_side_encryption {
enabled = true
key_type = "AWS_OWNED_CMK"
}
}

################################################################################
# Amazon S3
################################################################################

//// S3 Bucket
/// For Amazon Kinesis Data Firehose
## Reference: https://registry.terraform.io/modules/terraform-aws-modules/s3-bucket/aws/latest
module "s3_bucket_for_kinesis_firehose" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "~> 3.14"

bucket = "yagishita-prod-ses-log-transfer"
acl = "private"

control_object_ownership = true
object_ownership = "ObjectWriter"
}

################################################################################
# AWS Identity and Access Management
################################################################################

//// IAM Role
## Reference: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role
resource "aws_iam_role" "for_ses_log_transfer" {
name = "prod-ses-log-transfer"
assume_role_policy = data.aws_iam_policy_document.for_ses_log_transfer_assume_role.json
}

data "aws_caller_identity" "self" {}

data "aws_iam_policy_document" "for_ses_log_transfer_assume_role" {
statement {
actions = ["sts:AssumeRole"]
effect = "Allow"
principals {
type = "Service"
identifiers = ["firehose.amazonaws.com"]
}
}

statement {
actions = ["sts:AssumeRole"]
effect = "Allow"
principals {
type = "Service"
identifiers = ["ses.amazonaws.com"]
}
condition {
test = "StringEquals"
values = [data.aws_caller_identity.self.account_id]
variable = "AWS:SourceAccount"
}
condition {
test = "StringEquals"
values = [
aws_sesv2_configuration_set.domain_a.arn,
]
variable = "AWS:SourceArn"
}
}
}

//// IAM Role (Inline Policy-1)
## Reference: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy
resource "aws_iam_role_policy" "for_ses_log_transfer_for_opensearch" {
name = "for_opensearch"
role = aws_iam_role.for_ses_log_transfer.id
policy = data.aws_iam_policy_document.for_ses_log_transfer_for_opensearch.json
}

data "aws_iam_policy_document" "for_ses_log_transfer_for_opensearch" {
statement {
actions = [
"s3:*",
]
effect = "Allow"
resources = [
module.s3_bucket_for_kinesis_firehose.s3_bucket_arn,
"${module.s3_bucket_for_kinesis_firehose.s3_bucket_arn}/*",
]
}

statement {
actions = [
"es:*",
]
effect = "Allow"
resources = [
"${data.terraform_remote_state.stats.outputs.stats_es_log_domain_arn}",
"${data.terraform_remote_state.stats.outputs.stats_es_log_domain_arn}/*",
]
}
}

//// IAM Role (Inline Policy-2)
resource "aws_iam_role_policy" "for_ses_log_transfer_for_kinesis_firehose" {
name = "for_kinesis_firehose"
role = aws_iam_role.for_ses_log_transfer.id
policy = data.aws_iam_policy_document.for_ses_log_transfer_for_kinesis_firehose.json
}

data "aws_iam_policy_document" "for_ses_log_transfer_for_kinesis_firehose" {
statement {
actions = [
"firehose:PutRecordBatch",
]
effect = "Allow"
resources = [
"arn:aws:firehose:*:${data.aws_caller_identity.self.account_id}:deliverystream/${aws_kinesis_firehose_delivery_stream.kinesis_firehose_for_ses.name}",
]
}
}

################################################################################
# (クロスアカウントでstateファイル参照するための設定)
################################################################################

//// AWSアカウント「analysis」を参照するためのコード
data "terraform_remote_state" "analysis" {
backend = "s3"

config = {
bucket = "terraform-state-analysis-xxx"
key = "state"
region = "ap-northeast-1"
}
}

################################################################################
# (クロスアカウントで設定を連携するための設定)
################################################################################

//// 他のAWSアカウントへ連携するためのコード
output "ses_firehose_delivery_role_arn" {
description = "analysisアカウントのOpenSearchにおけるポリシーで使用する。"
value = aws_iam_role.for_ses_log_transfer.arn
}
/**
* 「分析用アカウント側」各種設定
*/

################################################################################
# Amazon OpenSearch Service
################################################################################

//// ElasticSearch
///    弊社では旧来よりElasticSearchを使用しており、OpenSearchに移行していないため、以下のコードとなっています。
## reference: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elasticsearch_domain
resource "aws_elasticsearch_domain" "log" {
domain_name = "analysis-log"
elasticsearch_version = "7.9"

cluster_config {
instance_type = "m6g.large.elasticsearch"
}

access_policies = <<CONFIG
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": [
"${data.terraform_remote_state.yagishita.outputs.ses_firehose_delivery_role_arn}",
]
},
"Action": [
"es:*",
],
"Resource": [
"arn:aws:es:ap-northeast-1:${data.aws_caller_identity.self.account_id}:domain/analysis-log/",
"arn:aws:es:ap-northeast-1:${data.aws_caller_identity.self.account_id}:domain/analysis-log/*"
]
},
]
}
CONFIG

}

################################################################################
# (クロスアカウントでstateファイル参照するための設定)
################################################################################

//// AWSアカウント「yagishita」を参照するためのコード
data "terraform_remote_state" "yagishita" {
backend = "s3"

config = {
bucket = "terraform-state-yagishita-xyz"
key = "state"
region = "ap-northeast-1"
}
}

################################################################################
# (クロスアカウントで設定を連携するための設定)
################################################################################

//// 他のAWSアカウントへ連携するためのコード
output "analysis_es_log_domain_endpoint" {
value = aws_elasticsearch_domain.log.endpoint
}

output "analysis_es_log_domain_arn" {
value = aws_elasticsearch_domain.log.arn
}

※注意点※

実際には他にも設定を入れていますが、コード量が膨大になってしまうので最小限のものとしています。特にアクセスポリシーについては過剰となっていますので、実際に導入する際は必要なもののみ許可するようにしてください。

また、実際にTerraform Applyする際、一度では作成できず、以下の流れで作成していきます。

1.(共通アカウント)IAMロール作成
このとき、ElasticSearch側の許可ポリシーがなく作成できない旨のエラーが吐かれますが、IAMロール自体は作成されています。
※output.tfで記述した通り、分析用アカウントからIAMロールのARNが参照できるようになります。
2.(分析用アカウント)OpenSearchアクセスポリシー更新
共通アカウント側で新たに作成したIAMロールのARNが参照でき、更新が可能になります。
3.(共通アカウント)IAMロール更新・Kinesis Firehose作成・AWS SES設定セット更新

動作確認の仕方

設定が完了したら、アプリケーションからメール送信を実施します。

最終的にOpenSearchに到達するまで、最長5分程度ラグが出ます。

ブラウザでKibanaを開き、「Stack Management>Index patterns>Create index pattern」より、Kinesis Firehose作成するときに指定したIndexを指定します。

作成したIndex patternsを指定し、テスト送信した時間帯で検索するとログが表示されるようになります。

これで、目的としていたどのドメインのメールの件数が多いのかが見れるようになりました。

最後に

IAMロールにおけるポリシーの設定に手間取りましたが、概ね実現したいことができました。今までAWS SESにおける設定セットは活用していなかったのですが、送ったメールの開封率の測定もすること可能になるため、他に活用できる要素がないか掘り下げてみます。

ネクストビートでは、日々物凄いスピードで新技術の導入等を行っており、毎日のように新しい技術の導入が行えたりと、エンジニアとしての経験を大きく積むことができます。引き続き日々の情報のキャッチアップに努め、弊社提供サービスの更なる品質向上に繋げていきます。

ここまで読んでいただきありがとうございました。

We are Hiring!

本記事をご覧いただき、ネクストビートの技術や組織についてもっと話を聞いてみたいと思われた方、カジュアルにお話しませんか?

・今後のキャリアについて悩んでいる

・記事だけでなく、より詳しい内容について知りたい

・実際に働いている人の声を聴いてみたい

など、まだ転職を決められていない方でも、ネクストビートに少しでもご興味をお持ちいただけましたら、ぜひカジュアルにお話しましょう!

🔽申し込みはこちら
https://hrmos.co/pages/nextbeat/jobs/1000008

また、ネクストビートについてはこちらもご覧ください。

🔽エントランスブック
https://note.nextbeat.co.jp/n/nd6f64ba9b8dc

--

--