CloudTrail + S3 Bucket Policy Circular Dependency
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.tf301-playball-terraform/modules/cloudtrail/main.tf