Bootstrap AWS IAM and an S3 Backend for Terraform
This guide creates the first AWS identity used to run Terraform and a private Amazon S3 bucket used to store Terraform state.
[!IMPORTANT] Use an IAM user only as a bootstrap option for a personal learning account or an environment that cannot use temporary credentials. For production, prefer AWS IAM Identity Center, OIDC federation, IAM Roles Anywhere, or an IAM role. Never create access keys for the AWS root user.
Target setup
| Component | Example |
|---|---|
| IAM user | terraform-bootstrap |
| Console access | Disabled |
| AWS CLI profile | terraform |
| Region | ap-south-1 |
| S3 bucket | naren-terraform-state-<account-id>-ap-south-1 |
| State object | learning/dev/terraform.tfstate |
| Encryption | SSE-S3 (AES256) |
| Versioning | Enabled |
| Public access | Fully blocked |
| State locking | S3 native lock file |
1. Secure the root user
Before creating the IAM user:
- Sign in to AWS as the root user.
- Enable MFA for the root user.
- Verify the account email address and phone number.
- Do not create root-user access keys.
- Use the root user only for tasks that specifically require it.
2. Create the bootstrap IAM user
In the AWS Management Console:
- Open IAM.
- Go to Users and select Create user.
- Enter
terraform-bootstrap. - Do not enable AWS Management Console access.
- Add the tag
Purpose = TerraformBootstrap.
Initial permissions
For a temporary personal sandbox setup, attach the AWS-managed AdministratorAccess policy. Remove it after the initial environment is working and replace it with policies restricted to:
- The S3 state bucket and state path.
- The AWS services managed by the Terraform configuration.
sts:AssumeRoleif Terraform assumes a separate execution role.
Do not use AdministratorAccess for a production Terraform identity.
3. Create the access key
- Open IAM > Users > terraform-bootstrap.
- Select Security credentials.
- Under Access keys, select Create access key.
- Choose Command Line Interface.
- Save the access key ID and secret access key in a secure password manager.
The secret access key is displayed only once. Never place credentials in:
- Terraform
.tfor.tfvarsfiles. - Source-code repositories.
- README files.
- Container images.
- Unencrypted shell scripts.
4. Configure and verify the AWS CLI
Configure a named profile:
Enter the access key, secret key, region ap-south-1, and output format json.
Verify the identity:
The ARN should end with:
Use the profile in the current terminal:
5. Create the S3 state bucket
Get the AWS account ID and construct a globally unique bucket name:
TF_ACCOUNT_ID="$(aws sts get-caller-identity --profile terraform --query Account --output text)"
TF_STATE_REGION="ap-south-1"
TF_STATE_BUCKET="naren-terraform-state-${TF_ACCOUNT_ID}-${TF_STATE_REGION}"
echo "$TF_STATE_BUCKET"
Create the bucket:
aws s3api create-bucket \
--bucket "$TF_STATE_BUCKET" \
--region "$TF_STATE_REGION" \
--create-bucket-configuration "LocationConstraint=$TF_STATE_REGION" \
--profile terraform
[!NOTE] For
us-east-1, omit--create-bucket-configuration. The command above is correct forap-south-1.
Block all public access
aws s3api put-public-access-block \
--bucket "$TF_STATE_BUCKET" \
--public-access-block-configuration \
'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true' \
--profile terraform
Enable versioning
Versioning helps recover a previous state after accidental deletion, replacement, or corruption.
aws s3api put-bucket-versioning \
--bucket "$TF_STATE_BUCKET" \
--versioning-configuration Status=Enabled \
--profile terraform
Configure default encryption
aws s3api put-bucket-encryption \
--bucket "$TF_STATE_BUCKET" \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}' \
--profile terraform
SSE-S3 is sufficient for this learning setup. Use a customer-managed AWS KMS key when organizational requirements demand control over key policies, rotation, and audit boundaries.
6. Deny non-TLS access
Create state-bucket-policy.json. Replace both bucket-name placeholders with the actual bucket name.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::naren-terraform-state-ACCOUNT_ID-ap-south-1",
"arn:aws:s3:::naren-terraform-state-ACCOUNT_ID-ap-south-1/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
Apply the policy:
aws s3api put-bucket-policy \
--bucket "$TF_STATE_BUCKET" \
--policy file://state-bucket-policy.json \
--profile terraform
7. Configure the Terraform backend
Create backend.tf in the Terraform project. Backend blocks do not support normal Terraform variables, so replace the bucket name with the real value.
terraform {
required_version = ">= 1.10.0"
backend "s3" {
bucket = "naren-terraform-state-ACCOUNT_ID-ap-south-1"
key = "learning/dev/terraform.tfstate"
region = "ap-south-1"
encrypt = true
use_lockfile = true
}
}
use_lockfile = true enables S3-native state locking. DynamoDB-based locking is deprecated for new Terraform S3 backend configurations.
Initialize the backend:
If Terraform already has local state, confirm the prompt to migrate that state into S3.
Validate the configuration:
Do not run Terraform with -lock=false during normal operations.
8. Least-privilege access to the state backend
After bootstrapping, replace broad permissions with a restricted policy. Update the bucket name if necessary.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListTerraformStatePrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::naren-terraform-state-ACCOUNT_ID-ap-south-1",
"Condition": {
"StringLike": {
"s3:prefix": [
"learning/dev/*"
]
}
}
},
{
"Sid": "ReadWriteTerraformState",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::naren-terraform-state-ACCOUNT_ID-ap-south-1/learning/dev/terraform.tfstate"
},
{
"Sid": "ManageTerraformStateLock",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::naren-terraform-state-ACCOUNT_ID-ap-south-1/learning/dev/terraform.tfstate.tflock"
}
]
}
This policy grants access only to the remote backend. Add separate permissions for the infrastructure Terraform needs to create, update, and delete.
9. Verify the bucket controls
aws s3api get-public-access-block \
--bucket "$TF_STATE_BUCKET" \
--profile terraform
aws s3api get-bucket-versioning \
--bucket "$TF_STATE_BUCKET" \
--profile terraform
aws s3api get-bucket-encryption \
--bucket "$TF_STATE_BUCKET" \
--profile terraform
aws s3api get-bucket-policy \
--bucket "$TF_STATE_BUCKET" \
--profile terraform
After the first successful Terraform operation, the bucket should contain:
During a state-changing operation, Terraform may temporarily create:
Operational recommendations
- Keep the backend bucket outside the lifecycle of the workload stack using it.
- Do not run
terraform destroyagainst the backend bucket as part of an application stack. - Use a separate state key for each environment, such as
learning/dev,learning/test, andlearning/prod. - Never manually edit or upload a Terraform state file.
- Do not commit
.terraform/, crash logs, local state, plan files, or credentials. - Review and remove unused access keys.
- Enable CloudTrail and billing alerts.
- Move CI/CD to OIDC federation or an IAM role instead of static access keys.
- Move human access to IAM Identity Center or another temporary-credential mechanism.
Suggested .gitignore
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
crash.log
crash.*.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json
.terraformrc
terraform.rc
*.pem
*.key
.env