Appearance
18.6 — Infrastructure as Code with Terraform
1. What is it?
Terraform is a tool for defining infrastructure (the VPC, ECS service, RDS instance, and every other resource from 18.3–18.5) as version- controlled configuration files, rather than clicked-together manually in a cloud console — and for applying, tracking, and safely changing that infrastructure over time using that configuration as the single source of truth.
2. Why does it exist?
18.3–18.5 described a real, non-trivial AWS architecture — a VPC with six subnets across two AZs, an ALB, ECS/EKS, RDS, ElastiCache, SQS/SNS, IAM roles, security groups. Building that by hand in the console, for even one environment, is slow and error-prone; rebuilding it identically for a second environment (staging, or a customer's own AWS account, Part 14.1's "customer's cloud account" deployment model) by hand is worse — inevitably drifting into two subtly different, undocumented configurations. Terraform exists to make infrastructure declarative (you describe the desired end state; Terraform figures out how to get there), versioned (the same code review and Git history discipline Part 1.7 taught for application code, applied to infrastructure), and repeatable (the same configuration produces the same infrastructure, every time, in any account).
3. What problem does it solve?
It solves "how do I create, change, and reproduce infrastructure reliably, with a reviewable history of what changed and why" — directly analogous to how Git (Part 1.7) solves the same problem for application code. For an AI FDE specifically, it also solves a very concrete, recurring problem: deploying the same AI system architecture into a new customer's AWS account (Part 14.1) without re-deriving the whole design from memory or screenshots each time.
4. How does it work internally?
The core building blocks
A provider is a plugin telling Terraform how to talk to a specific API — the aws provider, for instance. A resource is one concrete infrastructure object you want to exist (aws_vpc, aws_ecs_service, aws_db_instance). Variables parameterize a configuration (which environment, which region, which instance size) so the same code deploys differently for staging vs. production without duplicating files. Outputs expose values from your infrastructure (a load balancer's DNS name, say) for use elsewhere. Locals are named, computed values within a configuration — convenient intermediate values, not user-facing inputs. A module is a reusable, packaged group of resources (e.g., "a standard VPC" or "a standard ECS service") that can be instantiated multiple times with different variables — the infrastructure equivalent of a function.
State — the concept that makes Terraform genuinely different from a script
Terraform's state is a file (terraform.tfstate) recording what infrastructure Terraform believes currently exists and how it maps to your configuration. This is the single most important concept to understand correctly: Terraform is not simply "a script that creates resources" — on every plan or apply, it compares three things — your configuration (desired state), its state file (last-known actual state), and (by querying the provider's API) the real current infrastructure — computing exactly the minimal set of changes needed to reconcile them. Without state, Terraform would have no way to know that the aws_db_instance block in your config corresponds to a specific, already-existing RDS instance rather than a new one to create.
Remote state stores this file in a shared location (commonly an S3 bucket) instead of a local file on one engineer's laptop — required the moment more than one person or one CI pipeline needs to run Terraform against the same infrastructure, since two people applying from two different local state files would each have an incomplete, conflicting picture of reality. State locking (commonly implemented via a DynamoDB table alongside the S3 backend) prevents two concurrent apply operations from corrupting the state by both writing to it at once — a real, not theoretical, failure mode in any team using Terraform without locking.
The core workflow: init, plan, apply, destroy
bash
terraform init # downloads providers, configures the backend (remote state)
terraform plan # shows what WOULD change — read-only, safe to run anytime
terraform apply # actually makes the changes (after confirmation)
terraform destroy # tears down everything the configuration managesterraform plan is the single most important habit this tool teaches: it is a dry run, always safe to run, that shows exactly what Terraform believes needs to change before anything actually happens — read it carefully every time, specifically looking for any resource marked for replacement (destroy-then-recreate) rather than a simple in-place update, since a replacement of, say, an RDS instance is a very different (and far more disruptive) operation than an update to one of its parameters.
Dependencies
Terraform automatically infers most dependencies from configuration references — an aws_ecs_service block that references aws_subnet.app_private_a.id tells Terraform the subnet must exist first, without you writing an explicit ordering. Explicit depends_on is needed only for dependencies Terraform can't infer from a direct reference (a resource whose creation must wait on another for a reason not visible in its own arguments).
Workspaces and environment management
Terraform workspaces let one configuration manage multiple, separate instances of the same infrastructure (e.g., staging and production) using the same code with different variable values and separate state per workspace. A common, often-preferred alternative for genuinely different environments is simply separate state files/directories per environment (sometimes with shared modules) — workspaces are convenient for close variants of the same infrastructure, but many teams find fully separate configurations per environment clearer and safer once environments diverge meaningfully (different account, different region, materially different sizing) — a real, debated trade-off, not a settled rule.
Secrets in Terraform
Terraform configuration and state can both end up containing sensitive values (a database password set as a resource argument, for instance) — state files are not encrypted by default and can contain sensitive data in plaintext, which is why remote state backends should themselves be encrypted (S3 server-side encryption, restricted IAM/bucket policy access) and why secrets should generally be created out-of-band (e.g., in Secrets Manager, 18.5) and referenced by ARN in Terraform, rather than having Terraform generate and store the secret value itself where practical.
Infrastructure drift
Drift is when real infrastructure no longer matches what Terraform's state believes exists — most commonly caused by someone making a manual change directly in the console (an emergency fix during an incident, say) without updating the Terraform configuration to match. terraform plan surfaces drift as an unexpected diff the next time it runs; the fix is either to update the configuration to reflect the manual change deliberately, or to revert the manual change and let Terraform bring infrastructure back to the configured state — but drift left unaddressed is a recurring, real source of confusing, hard-to-explain plan output over time.
Terraform vs. CloudFormation vs. Pulumi
CloudFormation is AWS's own native IaC tool — deeply integrated with AWS specifically, with no multi-cloud story, and configuration written in JSON/YAML. Terraform uses its own HCL language, is cloud-agnostic (the same tool manages AWS, Azure, GCP, and many other providers, relevant to 18.20's multi-cloud chapter), and has become something close to a de-facto standard for multi-cloud or cloud-agnostic infrastructure teams. Pulumi lets you write infrastructure definitions in a general-purpose programming language (Python, TypeScript) instead of a declarative domain-specific language — appealing to teams wanting to use familiar language constructs (loops, functions, real type-checking) but a smaller ecosystem and community than Terraform's as of this writing. For an AI FDE working across many different customers' cloud environments, Terraform's cloud-agnostic reach and broad ecosystem make it the most commonly encountered choice in practice — but this is a real, current-state observation, not a claim that alternatives are technically inferior.
IaC security scanning
Static-analysis tools (e.g., tfsec, checkov, or cloud-provider-native options) scan Terraform configuration before it's applied, flagging patterns like an S3 bucket without encryption, a security group with an overly broad 0.0.0.0/0 ingress rule, or an IAM policy with a wildcard resource — catching exactly the kind of issues 18.2/18.5's security considerations sections warn about, as an automated gate in CI (18.8) rather than relying on manual review to catch every instance.
5. Simple mental model
Terraform is like an architect's blueprint plus a general contractor combined: the blueprint (your .tf files) describes the finished building precisely; the contractor (terraform apply) figures out the actual sequence of construction steps needed to get from the current state of the lot to that blueprint, and — critically — keeps a detailed record (state) of exactly what's already been built, so a change to the blueprint next year results in only the necessary modifications, not tearing down and rebuilding the whole building from scratch.
6. Real-world example
Deploying the Enterprise AI Assistant into a brand-new customer's AWS account (Part 14.1) is, with Terraform, a matter of running the same module set against a different AWS account/variables file — the VPC, subnets, security groups, ECS service, RDS instance, and IAM roles from 18.3–18.5 are recreated identically, correctly, without re-deriving the architecture from a design doc or screenshots each time. This is precisely the reproducibility that turns a one-off, error-prone manual deployment into a repeatable FDE engagement pattern.
7. Architecture diagram
modules/
vpc/ → 18.5's VPC, subnets, route tables, NAT Gateways
ecs-service/ → 18.3's ECS task/service, IAM task role
rds/ → 18.4's RDS instance, Multi-AZ, security group
elasticache/ → 18.4's Redis replication group
messaging/ → 18.4's SQS/SNS + DLQ
environments/
staging/main.tf → instantiates modules with staging variables
production/main.tf → instantiates modules with production variables
Remote state (S3 + DynamoDB lock) ← shared source of truth,
one state file per environment, never a local laptop file8. Production considerations
- Always use remote state with locking for any infrastructure more than one person touches — a local state file is a single-laptop bottleneck and a data-loss risk.
- Always run
planand review its output — specifically checking for unexpected replacements — beforeapply, and treat this as a required step in any CI/CD pipeline applying Terraform (18.8), not just a local habit. - Structure configuration into reusable modules per logical component (VPC, ECS service, RDS) so environments differ only in variable values, not duplicated, drifting copies of the same resource blocks.
9. Common mistakes
- Manually editing infrastructure in the console "just this once" during an incident, then never reconciling the resulting drift — the next
planproduces a confusing, unexpected diff for someone who doesn't know about the manual change. - Committing a
terraform.tfstatefile (containing potentially sensitive values) into a public or even shared Git repository instead of using a properly access-controlled remote backend. - Applying
terraform applywithout reading the plan output first, especially in production — a resource replacement can mean real downtime (an RDS instance recreation, for instance) that a carelessapplytriggers without anyone noticing beforehand. - Using one giant, unmodularized configuration file for an entire complex system, making review and reuse across environments far harder than necessary.
10. Security considerations
- Remote state should be encrypted at rest and access-restricted via IAM — state can contain secrets and a complete map of your infrastructure, both sensitive from a security standpoint.
- IaC security scanning (section 4) as an automated CI gate catches common misconfigurations (public S3 buckets, overly broad security groups) before they ever reach real infrastructure — cheaper and more reliable than catching them after the fact via a security review.
- The IAM credentials Terraform itself runs as should follow least privilege (18.3) scoped to what that specific pipeline/environment actually needs to manage — not a single all-powerful credential shared across every environment.
11. Performance considerations
Terraform's own "performance" consideration is mostly about plan/apply speed on very large configurations — heavily modularized, well-organized configurations with appropriately scoped state (e.g., separate state per environment or major component rather than one monolithic state for an entire organization's infrastructure) plan and apply faster and with less blast radius per change.
12. Cost considerations
Terraform itself has no direct cost, but it is the tool that makes cost visible and reviewable before it's incurred — terraform plan shows exactly what will be created, letting a cost review happen before an expensive resource (a large RDS instance, an underused NAT Gateway per 18.18) is actually provisioned, rather than discovering it on next month's bill.
13. When to use it
Any infrastructure intended to be reproducible, reviewable, or deployed into more than one environment or account — which describes essentially every real production AI deployment and certainly every customer deployment (Part 14.1) an AI FDE is likely to be involved in.
14. When NOT to over-apply it
A single, throwaway prototype resource for local experimentation (Part 13.2) doesn't need to be Terraformed — the value of IaC comes from reproducibility and change tracking over time, neither of which matters for infrastructure you'll tear down in an afternoon.
15. Alternatives and trade-offs
See section 4's Terraform-vs-CloudFormation-vs-Pulumi comparison — each is a genuine, currently-viable choice with different trade-offs (AWS- native integration vs. cloud-agnostic reach vs. general-purpose-language ergonomics), not a settled "one correct tool" answer.
16. Practical example — the VPC module from 18.5, in Terraform
hcl
# modules/vpc/main.tf
variable "cidr_block" { type = string }
variable "availability_zones" { type = list(string) }
variable "environment" { type = string }
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
tags = { Name = "${var.environment}-ai-assistant-vpc" }
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_subnet" "public" {
for_each = toset(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.cidr_block, 8, index(var.availability_zones, each.key) + 1)
availability_zone = each.key
tags = { Tier = "public", Name = "${var.environment}-public-${each.key}" }
}
resource "aws_nat_gateway" "main" {
for_each = aws_subnet.public
subnet_id = each.value.id
allocation_id = aws_eip.nat[each.key].id
tags = { Name = "${var.environment}-nat-${each.key}" }
}
output "vpc_id" { value = aws_vpc.main.id }
output "public_subnet_ids" { value = [for s in aws_subnet.public : s.id] }for_each over the availability-zones list (rather than hardcoding two subnet blocks) is what makes the exercise from 18.5 section 18 — extending to three AZs — a one-line variable change instead of a configuration rewrite; this is exactly the reproducibility/maintainability benefit section 2 describes, made concrete.
17. Production-quality example — remote state backend configuration
hcl
# environments/production/backend.tf
terraform {
backend "s3" {
bucket = "acme-terraform-state-prod"
key = "ai-assistant/production/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock" # state locking, section 4
}
}
# environments/production/main.tf
module "vpc" {
source = "../../modules/vpc"
cidr_block = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b"]
environment = "production"
}
module "rds" {
source = "../../modules/rds"
vpc_id = module.vpc.vpc_id
data_subnet_ids = module.vpc.data_subnet_ids
app_security_group = module.ecs_service.security_group_id
multi_az = true # 18.5's availability requirement — production only
environment = "production"
}Note multi_az = true is a variable, not hardcoded into the module itself — the same rds module can be instantiated for staging with multi_az = false (cheaper, acceptable for a non-production environment) without duplicating the module's resource definitions, directly demonstrating section 4's environment-management point.
18. Short exercise
Take the VPC module in section 16 and extend it to also create the private application and data subnets from 18.5's design, each correctly tagged by tier, using the same for_each-over-availability-zones pattern — then run terraform plan (against a real or sandbox AWS account) and verify the plan matches your expectation before ever running apply.
19. Interview questions
- What is Terraform state, and why can't Terraform work correctly without it?
- Why does Terraform require remote state with locking for team use?
- Walk through what "drift" is, how it happens, and how you'd detect and resolve it.
- When would you choose CloudFormation or Pulumi over Terraform, and why?
20. FDE/customer scenario
A customer says: "We need this AI system deployed entirely inside our own AWS account, and we want to review the infrastructure before anything is created." A strong response walks through delivering the Terraform configuration itself for review (not just an architecture diagram), running terraform plan against their account to show the exact, concrete set of resources that will be created before apply — giving the customer's team a precise, reviewable artifact rather than asking them to trust a description, directly demonstrating the reproducibility and transparency this chapter's tooling provides.
Key takeaways
- Terraform's state file — not just its configuration syntax — is the concept that makes it fundamentally different from a deployment script; understanding state is understanding Terraform.
terraform planbeforeapply, every time, especially checking for unexpected resource replacement, is the single most important operating habit this tool teaches.- Modularizing infrastructure (VPC, ECS service, RDS as separate, reusable modules) is what makes the same architecture reproducible across environments and customer accounts (Part 14.1) without drift.
Things you should be able to explain
- What Terraform state is and why remote state with locking matters for team use.
- The difference between Terraform, CloudFormation, and Pulumi.
- What infrastructure drift is and how it's detected and resolved.
Things you should be able to build
- A modularized Terraform configuration reproducing 18.5's VPC design.
- A remote state backend configuration with encryption and locking.
Common mistakes
- Manual console changes causing undetected, unreconciled drift.
- Committing state files (with potential secrets) into shared Git repos.
- Running
applywithout readingplanoutput first.
Recommended next chapter
07-docker-for-ai-systems-deep-dive.md