Taiwo Shobo
Back to blog

31 October 2026, 2 min read

Terraform Remote State on AWS: Setting Up an S3 Backend

Local Terraform state works until you need a second machine or a second engineer. Here is how to move it to a versioned, encrypted S3 backend and keep it safe.

Terraform keeps track of what it has built in a state file. By default, that file sits on your own machine, which is fine for a first experiment and a problem for everything after it. Lose the laptop, and you lose the record of your infrastructure. Add a second engineer, or a CI pipeline, and now two copies of the truth can drift apart.

The fix is remote state. In my AWS infrastructure project, I store state in an S3 backend, and this article walks through the setup and the decisions behind it.

Why local state breaks down

Terraform compares your configuration against the state file to decide what to create, change, or destroy. If that file is wrong or missing, Terraform plans against a picture of the world that isn’t real, and it may try to recreate resources that already exist.

Local state also holds sensitive values in plain text, such as generated passwords and resource attributes. Keeping it in a Git repository is a common mistake that exposes them.

Create the bucket first

The backend bucket has to exist before Terraform can use it, so you create it outside the configuration that depends on it. A few CLI commands are enough. Replace the bucket name with your own, since S3 names are globally unique.

bash
aws s3api create-bucket \
  --bucket my-terraform-state-bucket \
  --region eu-west-1 \
  --create-bucket-configuration LocationConstraint=eu-west-1

aws s3api put-bucket-versioning \
  --bucket my-terraform-state-bucket \
  --versioning-configuration Status=Enabled

aws s3api put-public-access-block \
  --bucket my-terraform-state-bucket \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Versioning matters most here. Every change to state is kept as a version, so a bad apply or a corrupted file can be rolled back. Blocking public access keeps the state, and the secrets inside it, private.

Point Terraform at the bucket

Add a backend block to your configuration.

backend.tf
terraform {
  required_version = ">= 1.10"

  backend "s3" {
    bucket       = "my-terraform-state-bucket"
    key          = "envs/dev/terraform.tfstate"
    region       = "eu-west-1"
    encrypt      = true
    use_lockfile = true
  }
}

Each setting does a job:

Callout, tone: warning, text: The backend block can’t use variables. Bucket, key, and region must be written as literal values, or supplied with -backend-config at init time.

Initialise and migrate

If you already have local state, run init with the migrate flag. Terraform asks to copy your existing state into the bucket.

terminal
terraform init -migrate-state

After it finishes, confirm the state file exists in the bucket, then delete the local terraform.tfstate and its backup so there’s only one source of truth.

Give access with least privilege

The identity running Terraform, whether a person or a pipeline, needs only a few permissions on the bucket and the state path.

state-access-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::my-terraform-state-bucket"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::my-terraform-state-bucket/envs/*"
    }
  ]
}

Scoping the object permissions to the envs/ path means the same bucket can serve several projects without every identity seeing every state file.

What I’d check before relying on it