Terraform の Data Source を使って Amazon S3 各種サービスログ用のアクセス許可を設定してみた

2022.08.10

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

はじめに

こんにちは、アノテーション構築チームの荒川です。

S3 バケットのアクセス許可を設定する際に ARN などが必要になるケースがよくあります。
Terraform では Data Source を使って ARN などの構成要素を動的に取得できます。
今回は AWS サービスのログ設定毎に使えるバケットのアクセス許可例を紹介します。

Terraform のコードのみ見たい方は GitHub をご覧ください。

今回紹介するサービス

環境

$ terraform -v
Terraform v1.2.6
on darwin_arm64
+ provider registry.terraform.io/hashicorp/aws v3.75.2

Terraform 記載例

ALB

AWS 公式ドキュメントの ALB ログを S3 へ転送するポリシー例は以下です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::elb-account-id:root"
      },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::bucket-name/prefix/AWSLogs/your-aws-account-id/*"
    }
  ]
}

動的に取得したい要素を挙げます。

  • elb-account-id
  • bucket-name
  • prefix
  • your-aws-account-id

elb-account-id はリージョン毎に固定の ELB アカウント ID となります。
東京リージョンの ELB アカウント ID: 582318560864 を直接記載することもできますが、こちらも Data Source を使って取得することで、リージョンが変わっても書き直す手間がなくなります。

data.tf

# AWS ELB のサービス AWS アカウント(リージョン共通)
data "aws_elb_service_account" "service_account" {}

# 現在の AWS アカウント
data "aws_caller_identity" "current" {}

便宜上 data.tf としていますが locals.tfmain.tf へ記載しても問題ありません。

バケットポリシーは以下のようになります。

main.tf

resource "aws_s3_bucket" "alb_logs" {
  bucket = var.alb_logs_bucket_name # グローバルで一意なバケット名
}

resource "aws_s3_bucket_policy" "alb" {
  bucket = aws_s3_bucket.alb_logs.bucket
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      # ALB
      {
        Sid    = "ALBAccessLogWrite",
        Effect = "Allow",
        Principal = {
          AWS = data.aws_elb_service_account.service_account.arn // リージョンで固定の ELB サービス AWS アカウント ID
        },
        Action = "s3:PutObject",
        Resource = [
          "arn:aws:s3:::${var.alb_logs_bucket_name}/${var.alb_logs_prefix}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"
          # ALB が複数ある場合はここに列挙する
        ]
      },
    ]
  })
}

prefix の箇所は var.alb_logs_prefix としていますが、こちらは ALB のログ設定プレフィックスへ変更してください。
ALB 名がシンプルでわかりやすくオススメです。

これでアカウント ID が変わったり、ALB を配置したリージョンが変わっても、Terraform の Provider を変える対応のみで、バケットポリシーを変更する必要がなくなります。

CloudFront

CloudFront のログを S3 へ転送するにはバケット ACL が必要です。

AWS 公式ドキュメントにしたがって FULL_CONTROL を付けましょう。

バケットの S3 アクセスコントロールリスト (ACL) は FULL_CONTROL を付与する必要があります。バケット所有者のアカウントには、デフォルトでこのアクセス許可があります。権限がない場合、バケット所有者はバケットの ACL を更新する必要があります。

バケット ACL で必要な Canonical User ID(被付与者 ID)も Data Source で動的に取得できます。

data.tf

# バケット ACL に付与する CloudFront の被付与者 ID
data "aws_cloudfront_log_delivery_canonical_user_id" "cloudfront" {}

# 現在の AWS アカウントの被付与者 ID
data "aws_canonical_user_id" "current" {}

Terraform の例は以下です。

main.tf

resource "aws_s3_bucket" "cloudfront_logs" {
  bucket = var.cloudfront_logs_bucket_name # グローバルで一意なバケット名
}

resource "aws_s3_bucket_acl" "cloudfront_acl" {
  bucket = aws_s3_bucket.cloudfront_logs.bucket

  access_control_policy {
    # CloudFront からのアクセスを許可
    grant {
      grantee {
        id   = data.aws_cloudfront_log_delivery_canonical_user_id.cloudfront.id
        type = "CanonicalUser"
      }
      permission = "FULL_CONTROL"
    }

    owner {
      id = data.aws_canonical_user_id.current.id
    }
  }
}

GuardDuty

GuardDuty はこれまで使用した Data Source とリージョンの情報を使います。

data.tf

# 現在のリージョン
data "aws_region" "current" {}

AWS 公式ドキュメントにしたがってバケットポリシーを作成しましょう。(ALB と同じバケットを使う場合は、ALB のバケットポリシーの Statement 箇所に追記してください。)

執筆時点(2022-08-09)の注意点として、Terraform で作成した Detector の ARN は参照できますが、既存 Detector の ARN や ID を取得する方法がなさそうです。

参考: aws_guardduty_detector | Data Sources | hashicorp/aws | Terraform Registry

また、既存の Detector がある場合、新しい Detector のリソースを作ると失敗します。
そのため、あらかじめ Detector を Terraform 管理外で作った場合は、手動で削除してから Terraform で作成するか、マネジメントコンソール(東京リージョンの場合) から探知機 IDをコピーしてバケットポリシーを変更してください。

main.tf

resource "aws_guardduty_detector" "main" {} // Detector がある場合はコメントアウトする
resource "aws_kms_key" "main" {}

resource "aws_s3_bucket" "guardduty_logs" {
  bucket = var.guardduty_logs_bucket_name # グローバルで一意なバケット名
}

resource "aws_s3_bucket_policy" "guardduty" {
  bucket = aws_s3_bucket.guardduty_logs.bucket
  policy = jsonencode({
    Version : "2012-10-17",
    Statement : [
      {
        Sid : "AllowGuardDutygetBucketLocation",
        Effect : "Allow",
        Principal : {
          Service : "guardduty.amazonaws.com"
        },
        Action : "s3:GetBucketLocation",
        Resource : "arn:aws:s3:::${var.guardduty_logs_bucket_name}",
        Condition : {
          StringEquals : {
            "aws:SourceAccount" : data.aws_caller_identity.current.account_id,
            // Detector がある場合は <SourceDetectorID> 箇所を変更してコメントを外す
            # "aws:SourceArn" : "arn:aws:guardduty:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:detector/<SourceDetectorID>"
            // Detector を Terraform で作る場合、既存の場合はコメントにする
            "aws:SourceArn" : aws_guardduty_detector.main.arn
          }
        }
      },
      {
        Sid : "AllowGuardDutyPutObject",
        Effect : "Allow",
        Principal : {
          Service : "guardduty.amazonaws.com"
        },
        Action : "s3:PutObject",
        Resource : "arn:aws:s3:::${var.guardduty_logs_bucket_name}/${var.guardduty_logs_prefix}/*",
        Condition : {
          StringEquals : {
            "aws:SourceAccount" : data.aws_caller_identity.current.account_id,
            // Detector がある場合は <SourceDetectorID> 箇所を変更してコメントを外す
            # "aws:SourceArn" : "arn:aws:guardduty:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:detector/<SourceDetectorID>"
            // Detector を Terraform で作る場合、既存の場合はコメントにする
            "aws:SourceArn" : aws_guardduty_detector.main.arn
          }
        }
      },
      {
        Sid : "DenyUnencryptedUploadsThis is optional",
        Effect : "Deny",
        Principal : {
          Service : "guardduty.amazonaws.com"
        },
        Action : "s3:PutObject",
        Resource : "arn:aws:s3:::${var.guardduty_logs_bucket_name}/${var.guardduty_logs_prefix}/*",
        Condition : {
          StringNotEquals : {
            "s3:x-amz-server-side-encryption" : "aws:kms"
          }
        }
      },
      {
        Sid : "DenyIncorrectHeaderThis is optional",
        Effect : "Deny",
        Principal : {
          Service : "guardduty.amazonaws.com"
        },
        Action : "s3:PutObject",
        Resource : "arn:aws:s3:::${var.guardduty_logs_bucket_name}/${var.guardduty_logs_prefix}/*",
        Condition : {
          StringNotEquals : {
            "s3:x-amz-server-side-encryption-aws-kms-key-id" : aws_kms_key.main.arn
          }
        }
      },
      {
        Sid : "DenyNon-HTTPS",
        Effect : "Deny",
        Principal : "*",
        Action : "s3:*",
        Resource : "arn:aws:s3:::${var.guardduty_logs_bucket_name}/${var.guardduty_logs_prefix}/*",
        Condition : {
          Bool : {
            "aws:SecureTransport" : "false"
          }
        }
      }
    ]
  })
}

作成したリソースの確認

ALB

CloudFront

GuardDuty

おわりに

Terraform のコードは汎用性を持たせることで、別環境でもスムーズにリソースを作成できます。
ARN などは固定で書いても問題ないし、むしろその方が早いと思われるかもしれません。
IaC は作っては壊してが基本ですので、将来のメンテナンス性を考えると汎用的に作る方が時間を短縮できるはずです。

今回ブログにしてあらためて感じましたが、サービスによってはログ保存に長いポリシーを必要とします。
1 サービスのログにつき 1 バケットへまとめると、アクセス許可の範囲が明確化して管理しやすいです。

他にも多くのサービスで AWS 公式ドキュメントのバケットポリシー例がありますが、Data Source をできるだけ活用していきましょう。

参考

アノテーション株式会社について

アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、さまざまな背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社WEBサイトをご覧ください。