1. Why Infrastructure as Code
Every command you ran in Week 8 worked, but none of it is repeatable without a human retyping it, none of it is reviewable before it runs, and nothing tells you two weeks from now whether the security group you're looking at in the console still matches what you intended. Infrastructure as Code (IaC) tools like Terraform fix this by describing the desired end state in a file, checked into Git like any other code, that a tool then reconciles reality against.
That reconciliation is the key difference from a shell script of CLI commands. A
script describes steps — run it twice and you might create two buckets or
get an error on the second run. A Terraform configuration describes a desired
state — run it twice and the second run does nothing, because the infrastructure
already matches what you asked for. When someone changes a resource by hand in the
console, that's drift, and Terraform's next plan will
show you exactly what's different.
Once a resource is managed by Terraform, stop clicking it in the AWS console. A manual change works until the next apply either reverts it or fights it — the same "someone edited main.js directly on the server" problem Git branching in Week 2 exists to prevent, just for infrastructure instead of application code.
2. Providers & Resources
Terraform itself knows nothing about AWS, Azure, or any specific platform — that
knowledge lives in a provider, a plugin that translates HCL
(HashiCorp Configuration Language) into API calls. You declare which providers you
need and which versions are acceptable, then Terraform downloads them during
init:
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
A resource block is the actual thing you want to exist —
resource "<provider_type>" "<local_name>" followed by its
arguments. The local name is only used to reference this resource elsewhere in your
configuration; it never appears in AWS itself:
resource "aws_s3_bucket" "app_logs" {
bucket = "codeverse-app-logs-ada"
tags = {
Environment = "dev"
Owner = "ada"
ManagedBy = "terraform"
}
}
Resources can reference each other's computed attributes — aws_s3_bucket.app_logs.arn,
for instance — and Terraform automatically works out the order to create them in
based on those references, without you writing that order down yourself.
3. The Workflow: init, plan, apply, destroy
Four commands cover almost everything you'll do day to day. terraform init
downloads the providers a configuration references and sets up its local working
directory — run it once per configuration, and again any time you add a provider:
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.42.0...
Terraform has been successfully initialized!
terraform plan compares your configuration against real infrastructure
and prints exactly what would change, without changing anything yet — this is the
step you actually read carefully, every time:
$ terraform plan
Terraform will perform the following actions:
# aws_s3_bucket.app_logs will be created
+ resource "aws_s3_bucket" "app_logs" {
+ arn = (known after apply)
+ bucket = "codeverse-app-logs-ada"
+ id = (known after apply)
+ tags = {
+ "Environment" = "dev"
+ "ManagedBy" = "terraform"
+ "Owner" = "ada"
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
The prefix on each line is the whole vocabulary: + creates a new
resource, - destroys one, ~ updates one in place, and
-/+ means Terraform has to destroy and recreate it because you changed
an attribute that can't be updated live (an S3 bucket's name, for example).
terraform apply shows you that same plan and asks for confirmation
before executing it:
$ terraform apply
Enter a value: yes
aws_s3_bucket.app_logs: Creating...
aws_s3_bucket.app_logs: Creation complete after 2s [id=codeverse-app-logs-ada]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ terraform destroy
Enter a value: yes
aws_s3_bucket.app_logs: Destroying... [id=codeverse-app-logs-ada]
aws_s3_bucket.app_logs: Destruction complete after 1s
Destroy complete! Resources: 1 destroyed.
An unexpected -/+ next to a database resource is how people accidentally destroy production data — Terraform will do exactly what the plan says with no further warning once you type "yes". In a CI pipeline, save the plan output as an artifact and require a human to approve it before the apply job runs, the same required-reviewer pattern from Week 7's GitHub Environments.
4. Terraform State
Terraform needs to remember which real-world resources correspond to which blocks in
your configuration, and it stores that mapping — including resource IDs and, often,
sensitive attribute values — in a state file, terraform.tfstate,
written in JSON. Every plan and apply reads and updates it.
terraform state list # every resource Terraform is tracking
terraform state show aws_s3_bucket.app_logs # full recorded attributes for one resource
terraform show # human-readable dump of the whole state
Without an accurate state file, Terraform has no way to know a resource already
exists — it would try to create a duplicate, or fail because a bucket with that name
is already taken. State is also how destroy knows what to tear down: it
isn't re-reading your .tf files' history, only the current configuration
plus whatever state already recorded.
*.tfstate
*.tfstate.*
.terraform/
.terraform.lock.hcl
You should never hand-edit a state file, and — critically — you should never commit it to Git either. It's a database, not a config file, and it routinely contains plaintext secrets (database passwords set via a resource argument, for instance). This week you'll keep state locally; Week 10 replaces that with a remote backend a team can share without emailing a JSON file around.
If terraform.tfstate is deleted, Terraform doesn't know your S3 bucket or EC2 instance exists anymore — it will happily try to create a new one, leaving the old one running and billing you, unmanaged and invisible to future plans. Back up or, better, remotely store state before you ever run apply against anything you care about.
5. Provisioning a Real AWS Resource End to End
Let's put it together: a versioned S3 bucket, driven by a variable so the same configuration can be reused with a different name, with an output so you can see the bucket's ARN after applying.
variable "bucket_suffix" {
description = "Unique suffix appended to the S3 bucket name"
type = string
}
resource "aws_s3_bucket" "app_logs" {
bucket = "codeverse-app-logs-${var.bucket_suffix}"
tags = {
Environment = "dev"
ManagedBy = "terraform"
}
}
resource "aws_s3_bucket_versioning" "app_logs" {
bucket = aws_s3_bucket.app_logs.id
versioning_configuration {
status = "Enabled"
}
}
output "bucket_arn" {
description = "ARN of the app logs bucket"
value = aws_s3_bucket.app_logs.arn
}
terraform fmt # auto-format .tf files consistently
terraform validate # catch syntax/type errors before planning
terraform plan -var="bucket_suffix=ada-0417"
terraform apply -var="bucket_suffix=ada-0417"
terraform output bucket_arn
# "arn:aws:s3:::codeverse-app-logs-ada-0417"
That's the full loop: write HCL, initialize providers, preview the plan, apply it, and read back an output — the same four-step shape you'll use for every resource in this course from here on, whether it's one S3 bucket or an entire Kubernetes cluster.
6. Hands-on Exercise
Codify last week's S3 bucket in Terraform
Recreate one piece of Week 8's manual environment declaratively, and practice reading a plan before you trust it.
Requirements:
- Create a project directory with
main.tfdeclaring thehashicorp/awsprovider pinned to~> 5.0, and runterraform init. - Define a
bucket_suffixinput variable, and use it to name anaws_s3_bucketresourcecodeverse-app-logs-<suffix>. - Add an
aws_s3_bucket_versioningresource enabling versioning, and tag the bucket withEnvironmentandManagedBy. - Run
terraform plan, read the output line by line, and confirm you understand every attribute marked(known after apply)before runningterraform apply. - Add a
bucket_arnoutput and confirm it withterraform output bucket_arn, then verify the bucket exists withaws s3api get-bucket-versioning --bucket <name>. - Run
terraform destroyto clean up, and confirm the bucket is gone fromaws s3 ls.
Add .gitignore before your first init, not after — it's easy to forget and accidentally stage terraform.tfstate in your very first commit, exactly the mistake the state section above warns about.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why should you always read a terraform plan's output before running apply, even for a change you're confident about?
Why should you always read a terraform plan's output before running apply, even for a change you're confident about?
An attribute you didn't expect to be immutable can force a -/+ destroy-and-recreate instead of an in-place ~ update, and drift from a manual console change can make the plan include actions you never intended. Reading the plan is the only checkpoint before Terraform actually executes changes against real infrastructure — after you type "yes" there's no undo.
Q2
What actually breaks if Terraform's state file doesn't accurately reflect real infrastructure?
What actually breaks if Terraform's state file doesn't accurately reflect real infrastructure?
Terraform has no other way to know a resource already exists, so a missing or stale state entry makes it try to create a duplicate (which fails for globally unique names like S3 buckets, or silently doubles cost for things like EC2 instances) and makes destroy unable to find resources it should tear down, leaving them orphaned and unmanaged.
Q3
Why should terraform.tfstate never be committed to Git, even in a private repository?
Why should terraform.tfstate never be committed to Git, even in a private repository?
State files commonly contain sensitive resource attributes in plaintext — database passwords, connection strings, keys — because Terraform has to record every attribute it manages, sensitive or not. It's also a database that multiple people would be editing concurrently through Git, which conflicts and corrupts far more easily than it would through a proper backend built for concurrent access.
Q4
In a plan's output, what's the practical difference between a ~ update and a -/+ destroy-and-recreate?
In a plan's output, what's the practical difference between a ~ update and a -/+ destroy-and-recreate?
A ~ update changes the resource in place without deleting it — an EC2 instance's tags, say — while a -/+ means the changed attribute can't be modified live (an S3 bucket's name, for example), so Terraform destroys the existing resource entirely and creates a new one with a new ID. That distinction matters enormously for anything stateful, since a destroy-and-recreate on a database means data loss unless it's backed up first.