Links#
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/s31. 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 accountFor 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 in2. Recommended Pattern#
普通 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-1If 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-1why 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 account3. 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-1This 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 blockThink 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-anConcrete 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>-anwhat 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 namespaceOfficial document:
https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.htmlExample:
acme-aws-terraform-uat-tfstate-222222222222-ap-northeast-1-anwhen 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 nameThe 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 sampleTerraform 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 name7. 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-tfstatewhy:
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 strategyThe 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-18. 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 riskRecommended 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 active9. 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 accessThe 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.tflockimportant:
.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 itS3 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 unlocksMinimal 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 adoptedApply:
# 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.jsonpolicy 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 ARNs10. 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"