Mission 7.4 · Stage 7 — Team Workflow
Day 2: Drift Detection, Upgrades, Stuck Locks, and Teardown
Goal: A nightly job reports drift in each environment, you know how to upgrade Terraform and providers safely and clear a stuck lock, and you finish by tearing all of ShopLite down in the right order.
By the end of this mission
- Detect drift on a schedule with
plan -detailed-exitcode - Upgrade providers and Terraform deliberately, with a reviewable lock-file diff
- Recover from a stale lock left by a killed run
- Destroy a multi-environment, multi-config project in the correct order
Part 1
Understand it first
Drift doesn't announce itself
Someone fixes an incident in the console at 3 a.m.; AWS changes a default; another tool edits a tag. Nobody notices until the next unrelated PR's plan shows surprising changes, or reverts the fix. A scheduled plan per environment with -detailed-exitcode turns drift into an alert: exit code 0 means no changes, 2 means changes exist, 1 means an error. The job opens an issue or posts to chat when it sees a 2.
Upgrades are changes too
Provider minor versions add resources and fix bugs, but can also change defaults, so a new provider can produce a different plan for the same code. Treat upgrades as their own PRs: terraform init -upgrade rewrites the lock file, the PR shows the lock-file diff plus plans for every environment, and it merges only when those plans are clean or explained. Terraform CLI upgrades likewise: bump required_version, the CI version, and laptops together.
Make the lock file cover every platform your team uses (terraform providers lock -platform=...), or a teammate on a different OS gets checksum surprises.
Teardown order matters
Dependencies run across configurations too: analytics depends on ShopLite's published parameters, apps depend on the network, and everything depends on the state bucket. Destroy consumers before producers, and the bootstrap state bucket LAST, since without it no other configuration can even plan its own destroy. Guardrails you added on purpose (prevent_destroy, deletion protection, force_destroy = false) must be lifted deliberately, one reviewed change at a time.
Part 2
Your project after this mission · 3 files change
- .github/
- workflows/
- drift.ymlnew
- bootstrap/
- main.tf
- infra/
- envs/
- dev.s3.tfbackend
- dev.tfvars
- prod.s3.tfbackend
- prod.tfvars
- .terraform.lock.hclmodified
- database.tf
- ecs.tf
- main.tf
- providers.tf
- security.tf
- tf
- versions.tfmodified
- modules/
- ecs-service/
- main.tf
- variables.tf
- network/
- main.tf
Part 3
Build it, step by step
- 1
Nightly drift detection
Runs at 02:30 UTC per environment with the READ-ONLY plan role. It needs a trust statement for scheduled runs on main: add
repo:you/shoplite:ref:refs/heads/mainto the plan role'ssubvalues. Exit code 2 opens an issue with the plan attached. Note the careful exit-code capture; see Break it for why..github/workflows/drift.ymlwhole fileyaml name: drift on: schedule: [{ cron: "30 2 * * *" }] workflow_dispatch: {} permissions: id-token: write contents: read issues: write jobs: drift: runs-on: ubuntu-latest strategy: fail-fast: false matrix: { env: [dev, prod] } steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 with: { terraform_version: 1.13.3, terraform_wrapper: false } - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::${{ vars[format('{0}_ACCOUNT_ID', matrix.env)] }}:role/shoplite-ci-plan aws-region: ap-south-1 - name: plan id: plan working-directory: infra run: | set +e ./tf ${{ matrix.env }} plan -detailed-exitcode -no-color -lock=false > plan.txt code=$? set -e echo "exitcode=$code" >> "$GITHUB_OUTPUT" [ "$code" -eq 1 ] && exit 1 || exit 0 - name: open issue on drift if: steps.plan.outputs.exitcode == '2' env: { GH_TOKEN: "${{ github.token }}" } run: | gh issue create --title "Drift detected in ${{ matrix.env }} ($(date -u +%F))" \ --label drift --body-file infra/plan.txt - 2
Upgrade providers deliberately
On a branch: re-select the newest allowed versions and record checksums for every platform the team and CI use. The lock-file diff shows exactly which versions changed. The PR's plans then show whether the upgrade changes anything real.
terminal$ cd infraterraform init -upgrade -backend=false >/dev/nullterraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64git diff --stat .terraform.lock.hcl── expected output ──- Fetching hashicorp/aws 6.21.0 for linux_amd64...- Retrieved hashicorp/aws 6.21.0 for linux_amd64 (signed by HashiCorp)- Fetching hashicorp/aws 6.21.0 for darwin_arm64......Success! Terraform has updated the lock file.infra/.terraform.lock.hcl | 46 +++++++++++++++++++++++-----------------------1 file changed, 23 insertions(+), 23 deletions(-) - 3
Tear it all down, in order
When you're done with the course, remove everything so nothing keeps billing. First lift the guardrails: remove
prevent_destroyfrom the database, and in prod setdeletion_protection/final snapshots as you see fit. Then destroy consumers first, and bootstrap last, where you also removeprevent_destroyon the state bucket and empty its versions.terminal$ (cd analytics && terraform destroy -auto-approve)(cd infra && ./tf prod destroy) # if you applied prod(cd infra && ./tf dev destroy)# bootstrap last: remove prevent_destroy, empty all object versions, then:(cd bootstrap && terraform destroy)── expected output ──Destroy complete! Resources: 1 destroyed....Destroy complete! Resources: 83 destroyed....Destroy complete! Resources: 11 destroyed.Also delete the ECR images' repository if it survived (prod had force_delete = false), the GitHub OIDC provider in each account, and the billing SNS subscription.
Checkpoint — you should now have
- ✓
drift.ymlruns nightly per environment and opens an issue when exit code is 2. - ✓Provider upgrades happen in their own PR with a multi-platform lock file.
- ✓You can clear a stale lock safely (see Break it).
- ✓ShopLite is fully destroyed, bootstrap last, and your AWS bill for it has stopped.
Part 4
Break it on purpose
Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.
Break #1
Let `set -e` swallow drift
Simplify the drift step to just ./tf ${{ matrix.env }} plan -detailed-exitcode > plan.txt (GitHub runs bash -e by default), then create some drift and run the workflow.
Break #2
Clear a lock left by a killed run
Reproduce Mission 7.1's cancelled apply (or kill a local ./tf dev apply with kill -9 mid-run), then run ./tf dev plan.
Part 5
Interview questions from this mission
How do you detect infrastructure drift with Terraform?
How do you upgrade the AWS provider across many environments safely?
A CI job was killed during terraform apply. What do you do?