Production Checklist
The idea in one sentence
Section titled “The idea in one sentence”Shipping Terraform and Terragrunt to production is not one big decision but a checklist of smaller ones — each covered earlier in this course — that all have to hold true at the same time.
The checklist
Section titled “The checklist”None of these are new ideas at this point. What is new is seeing them side by side as one list, because production readiness is really just all of them being true simultaneously, not any single one of them done especially well.
- Remote S3 backend with locking configured from day one.
use_lockfile = trueon the S3 backend, set up before the firstapply, not retrofitted after a corrupted state file forces the issue. Covered in State Management. - Provider and module versions pinned, not floating. The committed
.terraform.lock.hclpins exact provider versions and checksums; modulesourcereferences use a specific tag or version constraint, never a branch name that can move underneath you. Covered in Foundations and Modules. - Every
planreviewed in CI before everyapply, with reviewers specifically watching for unexpected-/+resource replacements rather than skimming the plan like a code diff. Covered in this module’s CI/CD lesson. - Sensitive values kept out of variables and, where possible, out of state, pulled from a secrets manager via a
datasource instead of typed into atfvarsfile, with the state backend itself encrypted at rest. Covered in State Management and this module’s secrets lesson. tflint, a security scanner, and ideally policy-as-code wired into CI, so known-bad patterns and organizational rules are enforced mechanically instead of depending on a human catching every case by eye. Covered in this module’s policy-as-code lesson.- Terragrunt
dependencyblocks driving rollout order, not a person remembering that the VPC has to apply before the database. Covered in Terragrunt Fundamentals. terraform test/.tftest.hclcoverage for any module doing something non-trivial, so a refactor of a module’s internals gets caught by an assertion instead of by a surprised downstream consumer. Covered in Modules.
# vpc/terragrunt.hcl and rds/terragrunt.hcl both use dependency blocks# so rollout order is driven by Terragrunt, not a person's memory.dependency "vpc" { config_path = "../vpc"
mock_outputs = { vpc_id = "mock-vpc-id" private_subnet_ids = ["mock-subnet-id"] } mock_outputs_allowed_terraform_commands = ["plan"]}
inputs = { vpc_id = dependency.vpc.outputs.vpc_id subnet_id = dependency.vpc.outputs.private_subnet_ids[0]}run "sets_expected_engine" { command = plan
variables { identifier = "app-db-test" }
assert { condition = aws_db_instance.this.engine == "postgres" error_message = "RDS module did not default to the postgres engine" }}One coherent system: a real repo layout
Section titled “One coherent system: a real repo layout”Put all of it together and this course adds up to a single, concrete shape — not a list of disconnected facts, but one repository that actually looks like this:
infra/├── root.hcl├── vpc/│ └── terragrunt.hcl├── ec2/│ └── terragrunt.hcl└── rds/ └── terragrunt.hclroot.hcl at the top holds the shared backend configuration and provider generation, included by every unit below it:
remote_state { backend = "s3" config = { bucket = "acme-terraform-state" key = "${path_relative_to_include()}/terraform.tfstate" region = "us-east-1" use_lockfile = true }}include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "../../modules/rds"}
dependency "vpc" { config_path = "../vpc" mock_outputs = { vpc_id = "mock-vpc-id" private_subnet_ids = ["mock-subnet-id"] } mock_outputs_allowed_terraform_commands = ["plan"]}
inputs = { vpc_id = dependency.vpc.outputs.vpc_id subnet_id = dependency.vpc.outputs.private_subnet_ids[0]}Every piece here traces back to an earlier lesson: use_lockfile = true is the State Management module’s locking story; root.hcl plus include is Terragrunt Fundamentals’ DRY backend pattern; the dependency block is what lets rds reference vpc’s real output without a person remembering which unit has to apply first; and the ../../modules/rds source, pinned in a real repo to a tag rather than left floating, is Modules’ versioning discipline.
The whole tree ships through exactly the CI/CD shape from earlier in this module: terragrunt run --all plan runs on every pull request touching infra/, posts its output for review, and terragrunt run --all apply runs only after merge to main, using the same OIDC-federated role rather than a stored key. Nothing here is a new mechanism — it is the same pieces from every earlier module, wired together into one system that deploys vpc, then ec2 and rds behind it, in the order the dependency blocks describe, gated by a plan a human actually read.
flowchart TB root["root.hcl: shared S3 backend, use_lockfile = true"] root --> vpc["vpc unit"] root --> ec2["ec2 unit"] root --> rds["rds unit"] vpc -->|dependency block: vpc_id, subnet_ids| ec2 vpc -->|dependency block: vpc_id, subnet_ids| rds pr["Pull request"] -->|terragrunt run --all plan| review["Reviewed for -/+ replacements"] review -->|merge to main| apply["terragrunt run --all apply"] apply --> vpc apply --> ec2 apply --> rds