open atlas
↑ Back to track
AWS, hands-on AWS · 06 · 02

Terraform on AWS: providers, state, and the remote backend

Terraform is cloud-agnostic IaC over HCL: the AWS provider maps resources to AWS APIs, but unlike CloudFormation you own a state file. Master remote backends, locking, and reading a plan, or state corruption and silent replaces will own you.

AWS Senior ◷ 20 min
Level
FoundationsJuniorMiddleSenior

Two engineers run terraform apply against the same config within the same minute. There is no lock, because state lives in a single S3 bucket with locking switched off “to keep things simple.” Both reads see the same starting state; both writes race to overwrite the object. The second write wins, the first engineer’s newly created NAT gateway is now an orphan no longer tracked in state, and the state file’s serial counter is inconsistent with reality. The next apply tries to “create” resources that already exist and errors on duplicate names — while the untracked NAT gateway quietly bills $32 a month that no one will find until the cost report. With CloudFormation this class of bug is largely impossible: AWS holds the stack state server-side and serializes the changes. Terraform handed you that responsibility, and you skipped it.

Providers: HCL over any cloud’s API

You met CloudFormation and CDK in the previous lesson — AWS-native tools where AWS parses your template and tracks the stack for you. Terraform, by HashiCorp, takes a different bet: one tool, one language (HCL, HashiCorp Configuration Language), across every cloud. The bridge to each platform is a provider — a plugin that maps HCL resource blocks to a vendor’s API calls. The aws provider turns an aws_instance block into EC2 RunInstances calls; the cloudflare or google provider does the same for their APIs. One configuration can declare resources across several providers at once, which is the whole pitch: a multi-cloud or multi-vendor estate described in a single language and workflow.

You declare the provider and a resource, and that is the unit of work. The block below pins the AWS provider version, sets a region, and asks for a bucket:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "eu-central-1"
}

resource "aws_s3_bucket" "assets" {
  bucket = "acme-prod-assets"
  tags   = { Team = "platform" }
}

That ~> 5.0 version constraint matters more than it looks: providers ship breaking changes, and an unpinned provider can rewrite half your plan after an unrelated upgrade. Pin it, the same way you pin a CloudFormation transform.

State: the thing CloudFormation hides and Terraform hands you

Here is the senior-defining difference. When you apply, Terraform writes a state file that records the bindings between objects in the real cloud and the resource instances in your config — the actual resource IDs, attributes, and dependency metadata. On the next run Terraform reads that state, refreshes it against the live API to detect drift (real world diverging from what state believes), computes the difference against your config, and plans only the delta. State is Terraform’s memory; without it the tool cannot tell a “create” from an “update.”

CloudFormation has the same memory, but AWS keeps it server-side and you never touch it. Terraform makes it a file you are responsible for — and that responsibility has sharp edges:

  • Local state is dangerous for teams. A terraform.tfstate on one laptop cannot be shared, is lost if that laptop dies, and serializes nothing — two people cannot collaborate safely.
  • State can contain secrets. An RDS password or a generated key lands in state in plaintext. Committing state to git leaks it; the backend must encrypt it at rest.
  • Never hand-edit state. It is a derived index, not source. Editing it by hand desynchronizes it from reality; use terraform state subcommands or terraform import to adopt an existing resource into management instead.

Together these three rules add up to one principle: treat state as a shared, encrypted, locking database — because that’s what it is. Skip any one of them and you’re one concurrent apply or one git push away from leaking credentials or losing resources.

The remote backend and state locking

The fix for every state hazard is a remote backend. The S3 backend stores the state object in an S3 bucket (bucket + key + region) so the whole team reads and writes one shared, durable copy, and it encrypts that state at rest. The second half is state locking: before mutating state, Terraform acquires a lock so a concurrent apply must wait instead of racing. Historically you supplied a DynamoDB table (a partition key named LockID) via dynamodb_table; that path still works but is now deprecated in favor of S3-native locking — set use_lockfile = true and Terraform writes a tflock object alongside the state to coordinate, with no separate table required.

terraform {
  backend "s3" {
    bucket       = "acme-tf-state"
    key          = "prod/network/terraform.tfstate"
    region       = "eu-central-1"
    encrypt      = true
    use_lockfile = true
  }
}

With this in place, the Hook’s race cannot happen: the second engineer’s apply blocks on the lock until the first finishes and releases it. This is the configuration the “to keep things simple” team skipped.

ConcernCloudFormation / CDKTerraform
Cloud scopeAWS-onlyMulti-cloud via providers
State ownershipAWS-managed, server-sideYou own a state file
Concurrency safetySerialized by AWSYou configure locking
AuthoringYAML/JSON (CFN) or real language (CDK)HCL DSL
Secrets in stateHidden by AWSIn state; must encrypt backend

Reading the plan, and modules

terraform plan is the safety gate, and reading it is a senior skill. It prints the diff with four actions: + create, - destroy, ~ update in place, and -/+ replace — destroy then recreate. That last symbol is the dangerous one. A field that cannot be changed in place (renaming a DB identifier, changing an immutable attribute) forces a replace, and a replace of a database means it is destroyed and a fresh empty one is created. The plan tells you in plain text — -/+ destroy and then create replacement — and an engineer who applies without reading it loses the data. Always read the plan before you apply; that is the entire reason plan is a separate command.

terraform init        # download providers, configure the backend
terraform plan        # refresh state, show the create/update/replace/destroy diff
terraform apply       # acquire lock, execute, write state back

The last building block is modules — reusable, parameterized bundles of resources, pulled from the public registry or a local path, so you write a VPC once and call it across environments instead of copy-pasting HCL. Modules are how a Terraform codebase stays DRY at scale.

Why this works

Why is “you own state” a feature and not just a burden? Because the state file is also a portable, inspectable record of your entire estate that is not locked to one vendor’s control plane. You can terraform import existing hand-built resources into management, refactor across providers, and run the exact same workflow whether the resource is in AWS, Cloudflare, or a SaaS API. CloudFormation’s server-side state is safer by default but only ever speaks AWS. The state file is the price of, and the mechanism behind, Terraform’s portability.

Pick the best fit

A team runs production across AWS, Cloudflare, and a Postgres SaaS. The platform engineers are fluent in HCL, comfortable owning a remote backend, and want one tool and one workflow across all three vendors. Pick the IaC.

Quiz

What is the core operational difference between Terraform and CloudFormation that drives most Terraform-specific failures?

Quiz

A plan shows `-/+ aws_db_instance.primary (destroy and then create replacement)`. What should you do?

Recall before you leave
  1. 01
    Why is the Terraform state file the central concept, and what three rules does owning it impose?
  2. 02
    What does a remote backend with locking give you, and how do you read a plan safely?
Recap

Terraform is HashiCorp’s cloud-agnostic IaC, written in HCL, where a provider plugin maps resource blocks to a vendor’s API — the aws provider to AWS, others to Cloudflare, Google, and SaaS APIs — so one config and one workflow can span multiple clouds. The senior-defining difference from CloudFormation and CDK, which you met in the previous lesson, is state: Terraform records a state file mapping your config to real resource IDs, refreshes it to detect drift, and plans the delta, whereas AWS keeps that state server-side for you. Owning state imposes three rules — do not use local state for teams, encrypt the backend because state can hold secrets, and never hand-edit state (use terraform import to adopt existing resources). The fix is a remote backend, typically S3 (bucket + key + region, encrypted), plus state locking via a DynamoDB LockID table or the newer S3-native use_lockfile, which serializes concurrent applies and prevents the corruption race. The workflow is write HCL, terraform plan, then terraform apply, and reading the plan is non-negotiable: + creates, - destroys, ~ updates in place, and -/+ replaces — destroy and recreate, which for a stateful resource means data loss. Modules keep large codebases DRY. The decision rule is simple: reach for Terraform when you are multi-cloud or want one tool across providers and accept managing state; reach for CloudFormation or CDK when you are all-in on AWS and want AWS to manage state for you. Now when you open a new Terraform repo and see terraform.tfstate sitting next to the .tf files with no backend config, you’ll know exactly what to fix before you let anyone else touch it.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.