S3 Bucket


https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html
https://docs.aws.amazon.com/AmazonS3/latest/userguide/add-bucket-policy.html
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html
https://developer.hashicorp.com/terraform/language/backend/s3

1. Important Points#

AWS Terraform state bucket 没有全球统一命名标准,但有稳定的工程习惯:名字要唯一、可读、能追踪归属、能看出用途,并且不要泄露敏感信息。

core facts:
    S3 general purpose bucket name is globally unique within an AWS partition
    bucket name is visible in URLs
    bucket name cannot be changed after creation
    bucket region cannot be changed after creation
    deleted shared-global bucket names may be reused by another AWS account

For Terraform state bucket:

name must answer:
    which organization owns it
    which cloud/provider context it belongs to
    which AWS account or account alias it belongs to
    what it stores
    which region the bucket itself lives in

普通 S3 bucket 使用 shared global namespace 时,推荐使用这个格式:

<company>-aws-terraform-<account_alias>-<account_id_or_suffix>-tfstate-<bucket_region>

Examples:

acme-aws-terraform-dev-111111111111-tfstate-ap-east-1
acme-aws-terraform-uat-222222222222-tfstate-ap-northeast-1
acme-aws-terraform-prod-333333333333-tfstate-ap-east-1

If the full account ID feels too long, use the last 4-6 digits, but only if the organization also keeps a clear account registry:

acme-aws-terraform-uat-223333-tfstate-ap-northeast-1
acme-aws-terraform-prod-445555-tfstate-ap-east-1
why include account id:
    S3 bucket names are not only unique inside your AWS account
    account alias like uat/prod is not globally unique
    account id strongly reduces naming collision
    incident response can quickly map bucket to an AWS account

3. Compact Pattern#

For small teams with very few AWS accounts:

<company>-tfstate-<account_alias>-<account_id>-<bucket_region>

Examples:

acme-tfstate-dev-111111111111-ap-east-1
acme-tfstate-uat-222222222222-ap-northeast-1
acme-tfstate-prod-333333333333-ap-east-1

This is shorter than the recommended pattern, but still keeps account ID. Do not drop account ID from Terraform state bucket names unless the bucket is only a disposable local demo.

4. Account Regional Namespace#

AWS also supports creating general purpose buckets in your account regional namespace. This gives you a predictable suffix controlled by the account and region:

what it means:
    AWS reserves a bucket-name suffix for one account + one region
    only that AWS account can create bucket names using that reserved suffix
    other AWS accounts cannot create bucket names ending with your account/regional suffix

what it does not mean:
    it does not mean the bucket is only visible inside that region
    it does not make the bucket private by itself
    it does not replace IAM policy, bucket policy, or public access block

Think of it as a reserved part of the S3 bucket naming system:

shared global namespace:
    acme-tfstate-prod-333333333333-ap-east-1
    must be globally unique in the AWS partition
    if deleted, another account may later create the same name

account regional namespace:
    acme-tfstate-prod-333333333333-ap-east-1-an
    AWS treats 333333333333-ap-east-1-an as a reserved suffix
    that suffix belongs to account 333333333333 in ap-east-1
    account 444444444444 cannot create a bucket ending with 333333333333-ap-east-1-an

Concrete example:

account:
    333333333333

region:
    ap-east-1

reserved suffix:
    -333333333333-ap-east-1-an

this account can create:
    acme-tfstate-prod-333333333333-ap-east-1-an
    order-logs-333333333333-ap-east-1-an
    platform-artifacts-333333333333-ap-east-1-an

another account cannot create:
    anything-333333333333-ap-east-1-an

another account must use its own suffix:
    anything-444444444444-ap-east-1-an
<bucket_name_prefix>-<aws_account_id>-<aws_region>-an
what is -an:
    -an is the fixed ending of the AWS account regional namespace suffix
    AWS documents the full suffix as:
        -<AWS-Account-ID>-<AWS-Region>-an

    it is not an environment name
    it is not an account alias
    it is not a region code
    do not replace it with your own value
    do not use it unless the bucket is created in account regional namespace

Official document:

https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html

Example:

acme-aws-terraform-uat-tfstate-222222222222-ap-northeast-1-an
when to use:
    you want predictable bucket names
    you want assurance that only your account can own names in that account/regional namespace
    your tooling and AWS CLI version support account regional namespace creation

when not required:
    most existing Terraform backend setups still use normal general purpose buckets
    if your platform standard already uses shared global namespace with account ID in name

The normal Terraform S3 backend bucket value is still just the final bucket name:

terraform {
  backend "s3" {
    bucket       = "acme-aws-terraform-uat-tfstate-222222222222-ap-northeast-1-an"
    key          = "order/uat/default/ap-northeast-1/network/terraform.tfstate"
    region       = "ap-northeast-1"
    encrypt      = true
    use_lockfile = true
  }
}

5. Evaluate This Name#

Given:

bucket_name = "aws-terraform-account-uat-state-ap-northeast-1"

This is acceptable as a local sample, but not ideal for a real organization.

Part Meaning Problem
aws cloud/provider too generic at the beginning
terraform tool useful
account-uat account/environment alias not globally unique
state purpose useful, but tfstate is more specific
ap-northeast-1 bucket region useful

Recommended rewrite:

bucket_name = "acme-aws-terraform-uat-222222222222-tfstate-ap-northeast-1"

Shorter variant:

bucket_name = "acme-aws-terraform-uat-223333-tfstate-ap-northeast-1"

Account regional namespace variant:

bucket_name = "acme-aws-terraform-uat-tfstate-222222222222-ap-northeast-1-an"

6. Naming Rules#

rules:
    use lowercase letters, numbers, and hyphen
    do not use underscore
    avoid periods unless this bucket is only for static website hosting
    do not format the name like an IP address
    do not start or end with hyphen
    do not put secrets, internal project codenames, customer names, or ticket IDs
    avoid names starting with aws / amazon / amzn unless it is only a sample
Terraform state bucket rules:
    use tfstate or terraform-state as purpose
    include company/org prefix
    include account alias
    include AWS account ID or suffix
    include bucket region
    keep workload region in backend key, not necessarily in bucket name

7. Region Position#

Put the bucket region at the end.

recommended:
    acme-aws-terraform-prod-333333333333-tfstate-ap-east-1

less readable:
    acme-ap-east-1-aws-terraform-prod-333333333333-tfstate
why:
    easy to scan bucket purpose first
    easy to group all tfstate buckets by prefix
    region is still visible for operations
    works well with per-account fixed-home-region state bucket strategy

The bucket region is the region where the state bucket lives. It is not necessarily the same as the workload region in the Terraform state key.

Example:

bucket = "acme-aws-terraform-prod-333333333333-tfstate-ap-east-1"
key    = "order/prod/default/ap-southeast-1/ecs/terraform.tfstate"
region = "ap-east-1"
meaning:
    state bucket is in ap-east-1
    Terraform-managed workload is in ap-southeast-1

8. Bucket Reuse#

Do not delete old Terraform state buckets just to reuse their names.

reason:
    after a shared-global bucket name is deleted, another AWS account may create the same name
    old clients, scripts, or documentation may still point to that bucket name
    for state buckets, this is a serious operational risk

Recommended retirement pattern:

retire:
    migrate state out
    empty old objects only after backup and retention review
    keep the bucket name reserved
    block all public access
    deny normal writes if no longer active

9. Bucket Policy#

This policy is for a manually created Terraform state bucket used by a small set of Terraform roles. Replace account IDs, role names, bucket name, and state prefixes.

Important principal rule:

bucket policy Principal:
    must be the identity that actually reads/writes the S3 backend

not always:
    the AWS provider assume_role target

why:
    Terraform S3 backend is initialized before provider configuration
    provider assume_role does not automatically grant backend access

The state object ARN and lockfile ARN come from the backend config:

terraform {
  backend "s3" {
    bucket       = "acme-aws-terraform-prod-333333333333-tfstate-ap-east-1"
    key          = "order/prod/default/ap-east-1/network/terraform.tfstate"
    region       = "ap-east-1"
    use_lockfile = true
  }
}
state object:
    s3://<bucket>/<key>

    s3://acme-aws-terraform-prod-333333333333-tfstate-ap-east-1/order/prod/default/ap-east-1/network/terraform.tfstate

state object ARN:
    arn:aws:s3:::<bucket>/<key>

    arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1/order/prod/default/ap-east-1/network/terraform.tfstate

lockfile object:
    s3://<bucket>/<key>.tflock

    s3://acme-aws-terraform-prod-333333333333-tfstate-ap-east-1/order/prod/default/ap-east-1/network/terraform.tfstate.tflock

lockfile object ARN:
    arn:aws:s3:::<bucket>/<key>.tflock

    arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1/order/prod/default/ap-east-1/network/terraform.tfstate.tflock
important:
    .tflock is created by Terraform S3 backend when use_lockfile = true
    it is derived from backend key by appending .tflock
    it is not a separate custom name you choose
    if key changes, the lockfile path changes with it
S3 Object Lock:
    not required for Terraform S3 backend use_lockfile
    not the same thing as Terraform state locking
    do not enable retention/legal hold on .tflock objects
    Terraform must delete the .tflock object when the run unlocks

Minimal bucket policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1",
        "arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1/*"
      ],
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "false"
        }
      }
    },
    {
      "Sid": "AllowTerraformStateList",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::333333333333:role/TerraformStateAccessRole",
          "arn:aws:iam::333333333333:role/PlatformAdminRole"
        ]
      },
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1",
      "Condition": {
        "StringLike": {
          "s3:prefix": [
            "order/prod/default/ap-east-1/network/*"
          ]
        }
      }
    },
    {
      "Sid": "AllowTerraformStateReadWrite",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::333333333333:role/TerraformStateAccessRole",
          "arn:aws:iam::333333333333:role/PlatformAdminRole"
        ]
      },
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1/order/prod/default/ap-east-1/network/terraform.tfstate"
    },
    {
      "Sid": "AllowTerraformLockfile",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::333333333333:role/TerraformStateAccessRole",
          "arn:aws:iam::333333333333:role/PlatformAdminRole"
        ]
      },
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1/order/prod/default/ap-east-1/network/terraform.tfstate.tflock"
    }
  ]
}

IAM user principal variant:

Use this only when IAM users directly run Terraform and directly access the S3 backend. This example grants the listed IAM users access across the state bucket instead of one specific state prefix.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1",
        "arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1/*"
      ],
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "false"
        }
      }
    },
    {
      "Sid": "AllowTerraformStateList",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::333333333333:user/rick",
          "arn:aws:iam::333333333333:user/alice"
        ]
      },
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1"
    },
    {
      "Sid": "AllowTerraformStateReadWrite",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::333333333333:user/rick",
          "arn:aws:iam::333333333333:user/alice"
        ]
      },
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1/*"
    },
    {
      "Sid": "AllowTerraformLockfile",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::333333333333:user/rick",
          "arn:aws:iam::333333333333:user/alice"
        ]
      },
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::acme-aws-terraform-prod-333333333333-tfstate-ap-east-1/*/terraform.tfstate.tflock"
    }
  ]
}
notes:
    IAM user Principal is supported
    AllowTerraformStateList has no prefix condition because these IAM users can list the state bucket
    AllowTerraformStateReadWrite can read/write all objects under the bucket
    AllowTerraformLockfile can operate on terraform.tfstate.tflock under any directory
    every user join/leave requires editing bucket policy
    prefer role Principal once TerraformProvisionRole is adopted

Apply:

# Save the complete bucket policy JSON above to this file first.
vi /tmp/tfstate-bucket-policy.json

aws s3api put-bucket-policy \
  --bucket acme-aws-terraform-prod-333333333333-tfstate-ap-east-1 \
  --policy file:///tmp/tfstate-bucket-policy.json
policy notes:
    terraform.tfstate object path is backend key
    terraform.tfstate.tflock object path is backend key + ".tflock"
    s3:DeleteObject is needed for the .tflock object when use_lockfile = true
    s3:DeleteObject is not required on the terraform.tfstate object
    S3 Object Lock is not required for use_lockfile
    do not apply Object Lock retention/legal hold to .tflock objects
    add more state prefixes or roles explicitly when more root modules use the bucket
    if same-account IAM identity policy already grants access, bucket policy can still add deny guardrails
    if cross-account Terraform roles use this bucket, bucket policy Principal must include those role ARNs

10. Final Recommendation#

For most teams:

<company>-aws-terraform-<account_alias>-<account_id>-tfstate-<bucket_region>

For your example:

bucket_name = "acme-aws-terraform-uat-222222222222-tfstate-ap-northeast-1"

If the platform has adopted AWS account regional namespace:

bucket_name = "acme-aws-terraform-uat-tfstate-222222222222-ap-northeast-1-an"