Problem#

Running terraform apply on stacks/audit-security/ to enable CloudTrail.

Error#

Error: creating CloudTrail Trail (playball-audit-trail): InsufficientS3BucketPolicyException: 
Incorrect S3 bucket policy is detected for bucket: playball-audit-logs

Root Cause Analysis#

Circular Dependency:

Creating CloudTrail → requires S3 bucket policy with write permission for CloudTrail
  ↕
S3 bucket policy → needs to reference the CloudTrail ARN (module.cloudtrail.source_arn)
  ↕
CloudTrail ARN → only exists after CloudTrail is created

The code used a dynamic "statement" to add the CloudTrail permission only when module.cloudtrail.source_arn != null. But since CloudTrail hasn’t been created yet, source_arn = null → dynamic block is skipped → bucket policy has no CloudTrail permission → CloudTrail creation fails.

dynamic "statement" {
  for_each = module.cloudtrail.source_arn != null ? [1] : []  # ← null, so empty list
  content {
    sid    = "AWSCloudTrailAclCheck"
    ...
    values = [module.cloudtrail.source_arn]  # ← null
  }
}

Fix#

CloudTrail ARNs follow a predictable format even before the resource is created, so we can pre-construct it in locals:

locals {
  trail_arn = "arn:aws:cloudtrail:${local.aws_region}:${local.account_id}:trail/${local.project_name}-audit-trail"
}

Use local.trail_arn in the bucket policy instead of the module output:

# After fix - removed dynamic block, reference local ARN directly
statement {
  sid    = "AWSCloudTrailAclCheck"
  effect = "Allow"
  principals {
    type        = "Service"
    identifiers = ["cloudtrail.amazonaws.com"]
  }
  actions   = ["s3:GetBucketAcl"]
  resources = [aws_s3_bucket.audit_logs.arn]
  condition {
    test     = "StringEquals"
    variable = "aws:SourceArn"
    values   = [local.trail_arn]
  }
}

Lessons Learned#

Several AWS resource ARNs are deterministic and can be constructed before the resource exists:

arn:aws:cloudtrail:{region}:{account}:trail/{name}
arn:aws:s3:::{bucket-name}
arn:aws:iam::{account}:role/{role-name}

In these cases, to break circular module output references, construct the ARN directly in locals.

Additional Issue: S3 Bucket Name Conflict#

Initially used project_name = "goormgb" → tried to create bucket goormgb-audit-logs → got BucketAlreadyExists error. The name was already taken in the main account (497012402578). S3 bucket names are globally unique, so each account needs a distinct prefix (playball-).

goormgb-audit-logs      → already in use by the main account
playball-audit-logs     → changed to this for the CA account

Files#

  • 301-playball-terraform/stacks/audit-security/main.tf
  • 301-playball-terraform/modules/cloudtrail/main.tf