Terraform Plan, State, Modules, and Physical Effects
Follow Terraform from reviewed configuration through state and AWS APIs to real compute, memory, storage, and network effects.
Run it in the public monorepo
This course is built around the public Zheta Kubernetes Lab monorepo. The excerpt below is runnable source, not pseudocode.
Source: infra/stacks/prod/main.tf
provider "aws" {
region = var.region
allowed_account_ids = [var.account_id]
assume_role {
role_arn = var.deployer_role_arn
}
Code to reality
- Declared intent
- Bind production infrastructure changes to one expected AWS account and an explicit deployer role.
- Interpreter
- Terraform configures the AWS provider, assumes the role, and rejects credentials for any other account.
- Software effect
- The plan is scoped to the production state and account boundary before resource reconciliation begins.
- Hardware effect
- Only an authorized apply can allocate billable AWS networking, control-plane, compute, and data resources.
- Observable evidence
- Caller identity, reviewed plan, remote state lock, and AWS resource tags agree on the production account.
Start with the people and the result they need
The source tables below remain the detailed contract. Begin with these customer paths:
- D19-UC-01
- Person: Infrastructure engineer
- Job: Change dev EKS capacity through a reusable reviewed module
- Observable result: Approved plan creates or updates the expected AWS resources and Zheta Forge remains reachable
- D19-UC-02
- Person: Incident commander
- Job: Reconcile Terraform state with independently observed AWS reality
- Observable result: Team chooses refresh, import, recreate, or last-resort state removal based on exact resource identity
Turn each customer job into a testable story
Now turn each customer job into a story with a result that an engineer can check:
- D19-US-01
- Story: As an infrastructure engineer, I want a fresh saved plan reviewed before apply, so that replacement, cost, availability, and physical effects are…
- Observable acceptance: Commit, module/provider versions, state version, plan checksum, policy, approver, identity, resource IDs, environment, run, and probes are recorded
- D19-US-02
- Story: As an incident commander, I want state repair gated by independent provider inspection, so that removing a record cannot orphan a real paid resource
- Observable acceptance: Exact address and AWS identity are resolved, empty or ambiguous matches fail, chosen repair is recorded, replan converges, and positive control passes
Add real state and observable proof
Finally trace each story through the system that owns its state and the evidence that proves the outcome:
- D19-FLOW-01
- Trigger: Engineer opens a dev capacity pull request
- Responsible systems: Terraform CLI/runner, module registry or Git source, policy gate, approval service, AWS provider and APIs, probe runner
- Authoritative state: Git for intent; remote backend for mapping; AWS APIs for resource reality
- Owned record: TerraformRoot
- Observable evidence: Actor, commit, plan checksum, state version, approval, expected and observed resources, environment, timestamps, AWS IDs, run, cluster, and trace IDs
- Failure signal: Stale plan, unlocked state, unpinned provider, replacement surprise, API partial failure, or failed customer probe
- D19-FLOW-02
- Trigger: Plan references a resource absent from the expected AWS inventory
- Responsible systems: Read-only state reader, AWS inventory, audit logs, repair workflow, Terraform runner
- Authoritative state: Versioned backend for state history; AWS APIs and audit logs for existence and attribution
- Owned record: SavedPlan
- Observable evidence: Provider inventory, state backup/version, decision, denied unsafe attempt, repaired plan, new or adopted AWS identity, customer probes, environment,…
- Failure signal: Wrong account or region, ambiguous target, live resource removed from state, failed import, or repeated drift
The enterprise problem and today’s slice
Enterprise problem: A small Terraform edit—Terraform is a stateful Infrastructure as Code (IaC) engine—can replace an Elastic Kubernetes Service (EKS) node group, expose a secret through state, race another operator, or leave paid AWS resources partly changed while Zheta Forge remains unavailable. Whole-course context: The incoming evidence assigns one owner to infrastructure, cluster lifecycle, Kubernetes workloads, and GitOps; today examines Terraform as the IaC implementation for the infrastructure boundary. Today’s slice: We connect HashiCorp Configuration Language (HCL), modules, plan, approval, locked remote state, providers, AWS APIs, physical effects, drift, import, and guarded state repair. End-of-day evidence: A dev run binds commit, fresh plan, policy, approval, state version, execution identity, AWS resource IDs, cluster observation, customer probe, denied unsafe change, environment, timestamp, run, and trace IDs. Still unsolved: Separate AWS account foundations and production Identity and Access Management (IAM) guardrails remain deferred.
Customer use cases
Terraform is safe only when reviewers can connect an abstract diff to actual resource and customer effects. These use cases cover planned infrastructure change and recovery from stale mapping.
| Use case ID | Actor | Customer job | Success outcome | Denial or recovery evidence |
|---|---|---|---|---|
| D19-UC-01 | Infrastructure engineer | Change dev EKS capacity through a reusable reviewed module | Approved plan creates or updates the expected AWS resources and Zheta Forge remains reachable | Policy denies public exposure, broad replacement, plaintext secret, or production-scoped identity before apply |
| D19-UC-02 | Incident commander | Reconcile Terraform state with independently observed AWS reality | Team chooses refresh, import, recreate, or last-resort state removal based on exact resource identity | Ambiguous or still-existing resource blocks state removal; replanning proves the repaired mapping and unaffected control |
Actor-centred user stories
State is not the infrastructure itself; it is Terraform’s durable mapping between resource addresses and provider objects. These stories keep that mapping recoverable and auditable.
| Story ID | Use case IDs | User story | Observable acceptance conditions |
|---|---|---|---|
| D19-US-01 | D19-UC-01 | As an infrastructure engineer, I want a fresh saved plan reviewed before apply, so that replacement, cost, availability, and physical effects are visible | Commit, module/provider versions, state version, plan checksum, policy, approver, identity, resource IDs, environment, run, and probes are recorded |
| D19-US-02 | D19-UC-02 | As an incident commander, I want state repair gated by independent provider inspection, so that removing a record cannot orphan a real paid resource | Exact address and AWS identity are resolved, empty or ambiguous matches fail, chosen repair is recorded, replan converges, and positive control passes |
End-to-end product flows
Running apply directly hides the proposal and can use stale assumptions, so a safe flow separates read, plan, approval, execution, and observation. State repair requires stronger proof because it changes Terraform’s memory without changing AWS.
| Flow ID | Use case IDs | Path | Trigger | Numbered steps | Terminal evidence |
|---|---|---|---|---|---|
| D19-FLOW-01 | D19-UC-01 | Happy | Engineer opens a dev capacity pull request | 1. Initialise pinned providers and locked backend. 2. Validate module inputs. 3. Refresh provider observations. 4. Save plan. 5. Review replacements and physical effects. 6. Approve exact checksum. 7. Apply with bounded identity. 8. Observe AWS, Kubernetes, and customer path. | Actor, commit, plan checksum, state version, approval, expected and observed resources, environment, timestamps, AWS IDs, run, cluster, and trace IDs |
| D19-FLOW-02 | D19-UC-02 | Recovery | Plan references a resource absent from the expected AWS inventory | 1. Resolve exact state address. 2. Inspect expected account and region independently. 3. Reject empty, ambiguous, wrong-owner, or still-existing targets. 4. Prefer refresh or import when reality exists. 5. Remove state only when absence is proven. 6. Replan and observe recreation or adoption. 7. Probe affected and unaffected apps. | Provider inventory, state backup/version, decision, denied unsafe attempt, repaired plan, new or adopted AWS identity, customer probes, environment, time, and run ID |
The engineer proposes or repairs one infrastructure change and must finish with both AWS reality and a customer observation. Terraform exit status alone is not the outcome.
System design derived from the flows
Modules reduce repeated design decisions, but a shared module must not collapse environment state or credentials. Each environment root invokes reusable modules through a separately locked backend and execution role.
| Use case ID | Entry point | Responsible services | Authoritative store | Failure evidence |
|---|---|---|---|---|
| D19-UC-01 | Environment-root pull request | Terraform CLI/runner, module registry or Git source, policy gate, approval service, AWS provider and APIs, probe runner | Git for intent; remote backend for mapping; AWS APIs for resource reality | Stale plan, unlocked state, unpinned provider, replacement surprise, API partial failure, or failed customer probe |
| D19-UC-02 | State discrepancy incident | Read-only state reader, AWS inventory, audit logs, repair workflow, Terraform runner | Versioned backend for state history; AWS APIs and audit logs for existence and attribution | Wrong account or region, ambiguous target, live resource removed from state, failed import, or repeated drift |
Terraform owns cloud foundation and a minimal Argo CD bootstrap, not GitOps-managed Zheta Forge Deployments. AWS APIs own live resource reality, while state stores Terraform’s mapping; generated-app data and roles remain in their own plane.
Data model and ownership
State may contain sensitive values and resource identities, so treating it as an ordinary build artifact creates security and concurrency failures. The model separates intent, proposed change, state version, and provider observation.
Generated-application database: Not created in this slice — Terraform manages infrastructure records and evidence; generated-application databases remain independently owned workloads on that infrastructure.
| Record or entity | Store and owner | Primary key | Foreign key or opaque reference | Tenant key | Material constraint | Lifecycle and deletion | Use case IDs |
|---|---|---|---|---|---|---|---|
| TerraformRoot | Git repository, owned by Infrastructure Platform | commit_sha plus root_path | Pinned module and provider refs | AWS account and environment ID | One root, backend key, and execution identity per environment; plaintext secrets forbidden | Propose, review, apply, supersede, retire after dependants | D19-UC-01, D19-UC-02 |
| SavedPlan | Encrypted run store, owned by IaC Delivery | plan_checksum | Opaque root commit, state version, identity, and policy refs | AWS account and environment ID | Apply inputs must exactly match approved plan context; plan expires on any input change | Generate, approve or deny, apply once, expire, retain metadata for audit | D19-UC-01 |
| TerraformStateVersion | Encrypted locked backend, owned by Infrastructure Platform | backend_key plus version_id | AWS provider resource IDs | AWS account and environment ID | Lock serialises writers; access is least-privilege; state is never committed to Git | Create versions on apply/import/repair, recover prior version, delete after environment retention | D19-UC-01, D19-UC-02 |
| ResourceObservation | AWS inventory evidence, owned by Operations | aws_resource_arn plus observed_at | Opaque Terraform address and run refs | AWS account and environment ID | Account, region, immutable owner labels, role, and freshness are mandatory | Capture read-only, seal with run, expire by evidence policy | D19-UC-01, D19-UC-02 |
The state mapping informs an explicit lifecycle decision, but AWS observation constrains it. The resulting evidence spans software records, physical allocation, Kubernetes readiness, and the Zheta Forge customer path.
Trace HCL to physical reality
Changing a node-group shape is not merely editing text: the provider may replace virtual machines, drain pods, allocate memory, and move network traffic. Review the plan for these effects before execution.
module "environment" {
source = "../../modules/environment"
environment = "dev"
expected_account_id = var.account_id
deployer_role_arn = var.deployer_role_arn
monthly_budget_usd = 250
region = var.region
vpc_cidr = "10.10.0.0/16"
kubernetes_version = var.kubernetes_version
node_instance_types = ["m7i.large"]
node_min_size = 2
node_max_size = 5
deletion_protection = false
}
| Effect field | What happens |
|---|---|
| Declared intent | Dev EKS should have bounded worker capacity through a reusable environment module |
| Interpreter | Terraform evaluates HCL; AWS provider translates resource changes into AWS API calls |
| Software effect | EKS node-group configuration, instances, kubelets, runtime processes, and scheduled pods may change |
| Hardware effect | AWS supplies vCPU, DRAM-backed memory, disks, NICs, addresses, and network paths |
| Evidence | Saved plan, state version, AWS resource IDs/events, ready nodes, pod rollout, and customer response |
Repair state only after proving reality
terraform state rm does not delete AWS infrastructure; it only removes Terraform’s mapping and can therefore orphan a live resource. Inspect exact identity first, back up versioned state, and prefer import when the resource exists.
: "${AWS_PROFILE:?set the approved AWS profile}" "${AWS_REGION:?set the intended AWS region}" "${EXPECTED_ACCOUNT_ID:?set the intended 12-digit AWS account}"
actual_account="$(aws --profile "$AWS_PROFILE" --region "$AWS_REGION" sts get-caller-identity --query Account --output text)"
[ "$actual_account" = "$EXPECTED_ACCOUNT_ID" ] || { echo "AWS account mismatch" >&2; exit 1; }
terraform -chdir=infra/stacks/dev state show 'module.environment.aws_eks_cluster.this'
aws --profile "$AWS_PROFILE" --region "$AWS_REGION" eks describe-cluster --name zheta-forge-dev --region eu-west-2
terraform -chdir=infra/stacks/dev plan -out=verified.tfplan
Do not run a removal command from this lesson. The guarded decision is: if AWS returns the exact expected resource, refresh or import; if independent account-and-region inventory proves it absent, use the approved repair workflow, then re-plan and verify the recreated or adopted identity.
| Effect field | What happens |
|---|---|
| Declared intent | Compare one exact Terraform address with one expected AWS cluster before choosing a repair |
| Interpreter | Terraform reads locked state; AWS CLI reads the named account, region, and EKS API |
| Software effect | No state or AWS resource changes; a saved plan and inspection evidence are created |
| Hardware effect | Control-plane APIs consume small CPU, storage, and network capacity to answer reads |
| Evidence | State address, AWS cluster identity, account, region, fresh plan, caller identity, timestamp, and run ID |
Key takeaways
Terraform connects configuration to physical resources through a reviewed plan, state mapping, provider, and observable AWS effects.
- State is sensitive durable mapping, not the infrastructure itself.
- Modules reuse design while separate roots preserve environment isolation.
- State surgery is last-resort record repair after exact independent inspection.
Checklist
An apply is complete only when its real effects and customer outcome are known.
- [ ] Plan is fresh, saved, policy-checked, and approved by checksum
- [ ] Backend is encrypted, locked, versioned, and environment-scoped
- [ ] Review names replacement, capacity, cost, and availability effects
- [ ] AWS, Kubernetes, customer, negative, and unaffected evidence are recorded