Skip to main content
Cloud DevOps Solutions
Get a quote Talk to an engineer

Terraform / IaC

Writing AWS and Azure Infrastructure in Terraform with Safe Remote State

We write your AWS or Azure infrastructure in Terraform and store state remotely: an encrypted, versioned S3 bucket on AWS or a Blob container on Azure, with locking so two applies cannot collide. Access is limited to the pipeline and a few admins, and state is split into small stacks.

Key takeaways

  • State is the record of what Terraform owns, so it needs versioning, encryption, locking and tight access from day one.
  • S3 now supports native lockfiles in recent Terraform releases; DynamoDB locking still works but is being phased out.
  • Azure Blob locks through blob leases, so there is no separate lock table to run.
  • Small state files per stack keep plans fast and limit the blast radius of a bad apply.
  • The backend is bootstrapped once, in a separate tiny configuration, and then left alone.

Why remote state decides whether Terraform is safe

Terraform is only as trustworthy as its state. The state file maps every resource in your code to a real object in your cloud account. If it is lost, Terraform proposes to create things that already exist. If two engineers write to it at once, it can be corrupted. If it sits on a laptop, only one person can safely change production.

Most teams meet this the hard way: a first project grows, a second engineer joins, and suddenly there is a terraform.tfstate file in a shared drive. Setting up Terraform remote state on S3 or Azure Blob is the first thing we do on any engagement, because everything else (modules, pipelines, drift checks) depends on it. This page covers how we do it as part of our wider Terraform and infrastructure as code service.

What a good backend looks like

A production-grade backend has five properties, and we check each one:

  • Remote and shared. State lives in your cloud account, not on a machine.
  • Versioned. Every write is recoverable, so a corrupted or wrongly edited state can be rolled back.
  • Encrypted. Server-side encryption at rest, with a customer-managed key where policy requires it.
  • Locked. Only one write operation runs at a time per state.
  • Restricted. Read and write access is limited to the CI role and a short list of administrators, because state files can hold secrets in plain text.

How it works on AWS

The backend bucket is created by a small, separate bootstrap configuration, so the main code never has to manage the place its own state lives. A typical backend block looks like this:

terraform {
  required_version = ">= 1.10"

  backend "s3" {
    bucket       = "example-tfstate-eu-west-1"
    key          = "network/prod/terraform.tfstate"
    region       = "eu-west-1"
    encrypt      = true
    use_lockfile = true
  }
}

On older Terraform versions the same job was done with a DynamoDB table (dynamodb_table = "tf-locks") keyed on LockID. Both approaches work. We use native S3 locking for new builds because it removes a moving part, and we keep DynamoDB only where a client is pinned to an older release. Check the options available for the Terraform version you run, as backend behaviour has changed across releases.

The bucket itself gets versioning, default encryption, a public access block, a lifecycle rule to expire old noncurrent versions, and a policy that denies insecure transport.

How it works on Azure

The azurerm backend writes state to a container in a storage account and uses blob leases for locking, so there is no equivalent of the DynamoDB table to maintain. The equivalent configuration is:

terraform {
  backend "azurerm" {
    resource_group_name  = "rg-tfstate"
    storage_account_name = "exampletfstate001"
    container_name       = "tfstate"
    key                  = "network.prod.tfstate"
    use_azuread_auth     = true
  }
}

We prefer Microsoft Entra ID authentication over storage account keys, enable blob versioning and soft delete, and turn off shared key access where the organisation allows it. If you need detail on provider specifics, see our dedicated pages on Terraform for Azure and Terraform for AWS.

Decisions and trade-offs

Self-managed backend or a hosted one

A cloud bucket costs very little and keeps data in your account. A hosted backend such as HCP Terraform adds a UI, run history and policy features, but brings a per-resource or per-seat bill and another vendor. We cover that choice on our page about Atlantis and Terraform Cloud pipelines.

How small should a state be

There is no magic number, but a useful test is: if a plan takes more than a couple of minutes, or if two unrelated teams are blocked by the same lock, split it. Going too far has a cost too. Dozens of tiny states with tangled cross-references are hard to reason about. We aim for stacks that match how your team actually works.

When not to bother with all this

If you have a throwaway proof of concept used by one person, local state is fine. The moment a second person or any production workload is involved, move to a remote backend.

Recovering from a bad state

Versioning is only useful if the recovery path has been rehearsed. The drill we run with clients is simple: list the earlier versions of the state object, restore the last known-good one as the current version, run a plan and confirm it matches expectations. Doing this once in a sandbox, before it is needed, turns a stressful incident into a ten-minute task. We also keep a copy of critical state in a separate account or region where policy calls for it, so a single compromised account cannot remove both the infrastructure and its record.

Failure modes we design out

  • Secrets in state. Database passwords and keys can end up in plain text. We restrict bucket access, mark outputs sensitive, and keep real secrets in a secrets manager.
  • Stale locks. A cancelled pipeline can leave a lock behind. Our runbook explains how to confirm nobody is applying and then release it with terraform force-unlock.
  • Hand-edited state. Direct edits are replaced with terraform state mv, moved blocks and removed blocks, each followed by a plan that must show no changes.
  • A state bucket managed by the state it holds. Bootstrapping separately avoids the circular dependency.
  • Shared credentials. Long-lived access keys in CI are replaced with OIDC federation, so a leaked pipeline log gives an attacker nothing reusable.

Delivery and handover

You get the repository, the backend configuration, a state layout diagram and a runbook covering restore, unlock, migration and access changes. Everything lives in your accounts. We train your engineers to run state operations themselves, which fits with our team training and code review work.

How to tell it is working

Look for these signs:

  • Every environment’s plan runs from the pipeline, never from a laptop.
  • No local .tfstate files exist anywhere.
  • A previous state version can be restored in minutes, and you have tested it.
  • Lock errors are rare and have a documented fix.
  • New engineers can run a plan on day one without being sent credentials over chat.

If your state is currently on someone’s machine, or you are unsure who can read it, book a consultation and we will review it with you.

References

How we deliver it

  1. 01

    Inventory and scope

    We list what exists in the AWS account or Azure subscription, decide what moves into code first, and agree stack boundaries before writing anything.

  2. 02

    Bootstrap the backend

    A small separate configuration creates the bucket or storage account with versioning, encryption, public access blocked and a narrow access policy.

  3. 03

    Write the infrastructure

    Networking, identity, compute and data are written as Terraform, one stack at a time, each with its own state key.

  4. 04

    Wire in locking and credentials

    Locking is enabled and the pipeline authenticates with short-lived federated credentials rather than stored access keys.

  5. 05

    Prove recovery

    We rehearse restoring a previous state version and breaking a stale lock, so the first time is not during an incident.

  6. 06

    Hand over

    You receive the repository layout, backend documentation and a short runbook for state operations.

Tools and technologies

TerraformOpenTofuAmazon S3DynamoDBAzure StorageAzure Blob leasesAWS KMSAzure Key VaultGitHub OIDCTerragrunt

Frequently asked questions

Do we still need DynamoDB for Terraform state locking?
Not on recent Terraform releases. The S3 backend supports native locking with a lockfile next to the state object, enabled with use_lockfile. DynamoDB locking still works but is deprecated, so we use the native option for new work and migrate existing backends when you upgrade Terraform.
How does Terraform state locking work on Azure?
The azurerm backend stores state as a blob and takes a lease on it during operations, which prevents concurrent writes. There is no separate table to provision. We still recommend blob versioning and soft delete so a bad write can be rolled back.
Is it safe to store Terraform state in S3?
Yes, if the bucket is configured properly. That means versioning, server-side encryption, all public access blocked, a bucket policy limited to the pipeline role and named administrators, and access logging. State can contain secrets in plain text, so bucket access should be treated like production database access.
Should we use one state file or many?
Many. A single state for everything makes plans slow and a mistake touches unrelated resources. We split by change frequency and ownership, for example network, data platform and each application stack, and share values through outputs or data sources.
Can you move our local state to a remote backend?
Yes. Adding a backend block and running terraform init with the migrate option copies the existing state across. We back it up first, verify that a plan shows no changes afterwards, and then remove every local copy.

Talk to an engineer about terraform remote state s3

Book a free 30-minute call. We review your current setup and tell you honestly whether and how we can help.

Written by Saddique Khan, DevOps / SRE / Cloud Consultant Engineer. Last updated 2026-10-08.