TerraformでS3サーバーアクセスログを有効にする

2022.05.21

しばたです。

Amazon S3のアクセスログを保存するS3サーバーアクセスログ機能について、シンプルな機能であるものの環境の構築が地味にめんどうなのでサンプルとして使えるTerraformを書いてみました。

本記事では

  • アクセスログを保存するS3バケット (ターゲットバケット)
  • アクセスログを送信するサンプルバケット (ソースバケット)

をTerraformで作っていきます。

S3サーバーアクセスログの基本

S3サーバーアクセスログを有効にする方法は以下のドキュメントに全て書いてあります。

基本的にはアクセスログを保存するS3バケットにAWS内部からログのPutを受け付ける様に適切な権限を与えておけばよく、

  • ターゲットバケットに対し バケットポリシー or ACL でログの配信許可を与える
    • 2022年現在ACLの利用は非推奨
    • ただし、一部サービス向け用途ではACLを使わざるを得ないケースがある
  • ターゲットバケットは 暗号化無し、またはSSE-S3によるデフォルト暗号化のみサポート
    • SSE-KMSは非サポート
  • ターゲットバケットのS3オブジェクトロックは無効にしておく
  • ログの保存期間はライフサイクルで設定する
  • ソースバケットとターゲットバケットは同じリージョンにある必要がある
  • ソースバケットとターゲットバケットを同じバケットにしない
    • ログ転送の無限ループが発生してしまう

あたりを気を付ければ大丈夫な感じです。

配信許可設定はACLの方が単純ではあるのですが2022年現在ACLの使用は非推奨です。
バケットポリシーを使い以下の様にAWSのサービスlogging.s3.amazonaws.comに対してPutを許可する権限を与える様にすると良いです。

ターゲットバケットのバケットポリシー例

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "S3ServerAccessLogsPolicy",
            "Effect": "Allow",
            "Principal": {
                "Service": "logging.s3.amazonaws.com"
            },
            "Action": [
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::<ログ保存先バケット名>/<バケットのPrefix>*",
            "Condition": {
                "ArnLike": {
                    "aws:SourceArn": "arn:aws:s3:::<ログ送信元バケット名>"
                },
                "StringEquals": {
                    "aws:SourceAccount": "<ログ送信元AWSアカウントID>"
                }
            }
        }
    ]
}

今回構築する内容

ここまでを踏まえて今回は私の検証用アカウントの東京リージョンに下図の構成を構築します。

  • ターゲットバケット : s3-server-access-logs-AWSアカウントID
  • ソースバケット : s3-sample-bucket-AWSアカウントID

の2バケットを構築し、ターゲットバケットの権限周りはバケットポリシーで設定します。
併せて両バケットとも

  • デフォルト暗号化 (SSE-S3) 有効
  • ACL無効
  • パブリックアクセス無効

の必要最低限のセキュリティまわりの設定を実施しておきます。

0. Terraformのバージョン

今回試すTerraformのバージョンは最新のVer.1.2.0とします。
AWS ProviderはVersion.4系、本日時点で最新のVer.4.15.1を使います。

AWS Provider Ver.3とVer.4ではS3関連のリソースの記述がガラッと変わっており、本記事の内容はVer.3系では使えませんのでご注意ください。

1. provider.tf

Terraformの設定周りはこんな感じにしてます。

provider.tf

terraform {
  required_version = "~> 1.2.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.15.1"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

2. target-log-s3.tf

ターゲットバケットの定義はこんな感じになります。

地味にめんどうなのがバケットポリシー設定ですね...
今回はサンプルということで少しポリシーを緩めにし、許可するソースバケットのARN(aws:SourceArn)を全S3バケット許可しています。
あと保存期間は1年(365日)としています。

target-log-s3.tf

data "aws_caller_identity" "current" {}

//
// アクセスログ保存用バケット
//
// 基本設定
resource "aws_s3_bucket" "server_access_logs" {
  // 今回は s3-server-access-logs-<アカウントID> なバケット名に
  bucket = "s3-server-access-logs-${data.aws_caller_identity.current.account_id}"
}
// パブリックアクセス無効
resource "aws_s3_bucket_public_access_block" "server_access_logs" {
  bucket                  = aws_s3_bucket.server_access_logs.bucket
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
// デフォルト暗号化 (SSE-S3)
resource "aws_s3_bucket_server_side_encryption_configuration" "server_access_logs" {
  bucket = aws_s3_bucket.server_access_logs.bucket
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256" // SSE-S3
    }
  }
}
// ACL無効 (バケット所有者の強制)
resource "aws_s3_bucket_ownership_controls" "server_access_logs" {
  bucket = aws_s3_bucket.server_access_logs.id
  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}
// ★ バケットポリシー ★ 
data "aws_iam_policy_document" "server_access_logs" {
  // アクセスログのPutを許可
  statement {
    sid = "S3ServerAccessLogsPolicy"
    actions = [
      "s3:PutObject",
    ]
    resources = [
      "${aws_s3_bucket.server_access_logs.arn}/*"
    ]
    principals {
      type = "Service"
      identifiers = [
        "logging.s3.amazonaws.com"
      ]
    }
    condition {
      test     = "ArnLike"
      variable = "aws:SourceArn"
      values = [
        "arn:aws:s3:::*" // 今回は許可する送信元S3を絞らない構成に(本番環境では絞るべき) 
      ]
    }
    condition {
      test     = "StringEquals"
      variable = "aws:SourceAccount"
      values = [
        "${data.aws_caller_identity.current.account_id}" // 自アカウントからのみ許可
      ]
    }
  }
}
resource "aws_s3_bucket_policy" "server_access_logs" {
  bucket = aws_s3_bucket.server_access_logs.id
  policy = data.aws_iam_policy_document.server_access_logs.json
}
// ライフサイクル設定 : 1年保存設定
resource "aws_s3_bucket_lifecycle_configuration" "server_access_logs" {
  bucket = aws_s3_bucket.server_access_logs.id
  rule {
    id = "expiration-rule"
    status = "Enabled"
    expiration {
      days = 365
    }
  }
}

3. source-sample-s3.tf

ソースバケットとなるサンプルバケットの定義はこんな感じです。
ターゲットバケットの指定をaws_s3_bucket_loggingリソースで行うくらいで、それ以外に特筆すべき事項はありません。

source-sample-s3.tf

//
// アクセスログ送信用 (サンプルバケット)
//
// 基本設定
resource "aws_s3_bucket" "sample" {
  // 今回は s3-sample-bucket-<アカウントID> なバケット名に
  bucket = "s3-sample-bucket-${data.aws_caller_identity.current.account_id}"
}
// パブリックアクセス無効
resource "aws_s3_bucket_public_access_block" "sample" {
  bucket                  = aws_s3_bucket.sample.bucket
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
// デフォルト暗号化 (SSE-S3)
resource "aws_s3_bucket_server_side_encryption_configuration" "sample" {
  bucket = aws_s3_bucket.sample.bucket
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256" // SSE-S3
    }
  }
}
// ACL無効 (バケット所有者の強制)
resource "aws_s3_bucket_ownership_controls" "sample" {
  bucket = aws_s3_bucket.sample.id
  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}
// ★ アクセスログ送信設定 ★
resource "aws_s3_bucket_logging" "sample" {
  bucket        = aws_s3_bucket.sample.id
  target_bucket = aws_s3_bucket.server_access_logs.id // ログ送信先バケット
  target_prefix = "${aws_s3_bucket.sample.id}/"       // ログ送信先Prefix
}

補足 : Gist

一連のソースをGistにもまとめておきます。

構築結果

この定義で環境構築すると下図の様な感じで2つのバケットが作成されます。

ターゲットバケットのバケットポリシーはこんな感じになります。

ACLは無効です。

ソースバケット側のログ設定は下図の様になり、ターゲットバケットとPrefixを指定した形で有効となります。

この状態で設定は完了となっており、ソースバケットに適当にアクセスして *1

しばらく待つとターゲットバケットにログが配信されます。

余談 : マネジメントコンソールから配信設定を更新すると...

ちなみにこの状態でソースバケットの配信設定をマネジメントコンソールから更新すると、ターゲットバケットのバケットポリシーに自動生成された定義が追記されてしまいます。

(「変更の保存」ボタンを押して更新すると下図の様に自動生成されたポリシー定義が追記される)

{
    "Sid": "S3PolicyStmt-DO-NOT-MODIFY-1653107214868",
    "Effect": "Allow",
    "Principal": {
        "Service": "logging.s3.amazonaws.com"
    },
    "Action": "s3:PutObject",
    "Resource": "arn:aws:s3:::s3-server-access-logs-xxxxxxxxxxxx/*"
}

追記される分には実害は無いのですが、その内容がドキュメントに記載されているものよりも若干緩い権限であるのとTerraformの管理上差分が出てしまうためTerraformを使う場合はマネジメントコンソールから更新しない様にした方が良いでしょう。

最後に

以上となります。

S3サーバーアクセスログのターゲットバケット側を構築する記事が意外と見つからなかったのでブログにしてみました。
本記事の構成は非常にシンプルなものですが、こちらをベースに皆さんの環境に合った形にカスタマイズして利用してもらえばと思います。

脚注

  1. 今回は適当なファイルをアップロードしています