Files
atelier/examples/bad/terraform-unlocked-state.md
Jon Chery d1aa5daf2b docs(milestone): complete v0.2 — infrastructure-as-code + kubernetes
---ci---
project: atelier
phase: 5
milestone: v0.2
status: complete
requirements:
  covered: [ATELIER-36, ATELIER-37, ATELIER-38, ATELIER-39, ATELIER-40, ATELIER-41, ATELIER-42, ATELIER-43, ATELIER-44, ATELIER-45, ATELIER-46, ATELIER-47, ATELIER-48, ATELIER-49, ATELIER-50, ATELIER-51, ATELIER-52, ATELIER-53, ATELIER-54, ATELIER-55, ATELIER-56, ATELIER-57, ATELIER-58, ATELIER-59]
  partial: []
---/ci---
2026-08-05 02:20:17 +00:00

4.1 KiB

Bad Example: Unlocked Local State

An IaC configuration that violates Atelier's Infrastructure as Code principles. Each violation is cited.

The Code

# main.tf — committed to the repo
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      # no version pin
    }
  }
}

provider "aws" {
  region     = "us-east-1"
  access_key = "AKIAIOSFODNN7EXAMPLE"   # committed
  secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"  # committed
}

resource "aws_s3_bucket" "logs" {
  bucket = "app-logs-prod"
}

resource "aws_db_instance" "primary" {
  identifier           = "app-prod"
  username             = "admin"
  password             = "hunter2"        # committed, in plaintext, in state
  allocated_storage    = 20
  engine               = "postgres"
  instance_class       = "db.t3.micro"
  skip_final_snapshot  = true
}

The team runs terraform apply from their laptops. The state file terraform.tfstate is committed alongside main.tf "so everyone is on the same page."

What Makes It Bad

Local State in a Shared Environment (IaC P8 Remote State with Locking)

  • State is terraform.tfstate on each laptop. Two team members run terraform apply simultaneously; the second to finish silently overwrites the first's changes. There is no lock.
  • The state file is committed to the repo. It contains the DB password in plaintext. It is a secret-bearing artifact in version control.
  • Fix: use a remote backend with locking (S3 + DynamoDB, GCS, etc.). Never commit state. See domains/infrastructure-as-code/state.md.

Hardcoded Secrets (IaC P10 Secrets Never in Code)

  • access_key and secret_key are committed in main.tf. The DB password is committed and also written to state in plaintext.
  • The secrets are now in the git history. Rotating them is not optional; the history must be scrubbed or the credentials rotated and the old ones revoked.
  • Fix: credentials from environment, a secrets manager, or a data source (aws_secretsmanager_secret_version). Mark sensitive attributes sensitive = true. See domains/security/secrets.md.

Unpinned Provider (IaC P5 Version Everything)

  • The aws provider has no version. The next terraform init pulls whatever is latest — a different provider version can change resource behavior with no review.
  • Fix: pin version = "~> 5.0". Commit the lock file (.terraform.lock.hcl). See domains/infrastructure-as-code/terraform.md.

Manual Drift, No Plan Review (IaC P4 Plan Before Apply, P9 Drift is Recoverable)

  • The team applies from laptops with no plan review. When the DB password is wrong, someone SSHes in and changes it manually — drift that plan will later report as a surprise.
  • Manual changes to managed resources are an incident, not a shortcut. Each one is a future plan diff that no one can explain.
  • Fix: run terraform plan in CI; review the diff; apply from CI on merge. Treat every drift report as an incident to investigate. See domains/infrastructure-as-code/state.md (Drift and Reconciliation).

No Module Composition (IaC P6 Modules Compose)

  • The S3 bucket and DB instance are inline. When the team needs a second bucket, they copy-paste the block and rename it. The two copies drift over time.
  • Fix: a versioned module for each reusable pattern. The difference is a variable, not a copy. See domains/infrastructure-as-code/modules.md (the module-vs-copy boundary).

The Cascade

The violations compound. Unlocked local state lets two apply runs race. Committed secrets mean the race loser's changes — and the secrets — are in the repo. Manual drift hides the corruption until a plan surfaces a diff no one can explain. The unpinned provider means that diff might be the provider's fault, not the team's, and no one can tell which.

  • domains/security/secrets.md — secret hygiene is non-tradeable; this example violates it in three places.
  • domains/security/supply-chain.md — committed credentials in git are a supply-chain incident.
  • domains/devops/P6 Configuration as Code — config in the repo is correct; committed state and secrets is the violation.