# Bad Example: Unlocked Local State > An IaC configuration that violates Atelier's Infrastructure as Code principles. Each violation is cited. ## The Code ```hcl # 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. ## Cross-Domain Links - `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.