🏗️ Infrastructure as Code: Core Concepts
You've spent this bootcamp learning to describe applications in code. This lesson teaches you to describe the servers, networks, and databases those applications run on in code too — versioned, reviewed, and rebuildable on demand. No more clicking through a console and hoping you remember what you did.
Week 13 · Wednesday: Infrastructure as Code · Lecture 1
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Define Infrastructure as Code (IaC) and explain how it differs from manual "click-ops"
- Distinguish declarative from imperative approaches and pick the right one
- Explain idempotency and why it makes IaC safe to re-run
- Describe immutable infrastructure and how it eliminates configuration drift
- List the core practices — version control, code review, testing, secrets handling — that make IaC trustworthy
- Read a simple Terraform snippet and predict what it will provision
Estimated Time: 55 minutes
Practice: Classify real operations as declarative or imperative, and draft your first declarative resource block.
In This Lesson
What Is Infrastructure as Code?
Infrastructure as Code (IaC) is the practice of defining your servers, networks, load balancers, databases, and DNS records in text files that live in version control — then letting a tool create and update the real resources to match those files. Instead of logging into a cloud console and clicking "Launch Instance," you write a few lines describing the instance you want and run one command.
Think of it as the difference between assembling furniture from memory versus following the printed instructions. The console lets you build one bookshelf by hand. IaC is the instruction sheet: precise, repeatable, and reviewable before a single screw is turned. Give the same sheet to a teammate and they build an identical shelf, every time.
main.tf"] --> B["Plan
preview the diff"] B --> C["Apply
make it real"] C --> D["Provisioned infrastructure
VPC, servers, DB"] D --> E["State file
records reality"] E --> B
That loop is the heartbeat of IaC: you edit code, preview what will change, apply it, and the tool records the new reality so the next preview is accurate. Every arrow is automated. Every change is a text diff you can read in a pull request.
📖 The one-sentence definition
IaC means your infrastructure's desired state is declared in version-controlled files, and a tool reconciles reality to match — reproducibly, idempotently, and without anyone clicking through a UI.
Why IaC Matters
Before IaC, infrastructure was built by hand and remembered in people's heads (or a stale wiki page). That approach breaks down the moment you need a second identical environment, or the person who set it up leaves, or a server dies at 3 a.m. and nobody knows exactly how to rebuild it.
The problems IaC solves
| Manual "click-ops" | Infrastructure as Code |
|---|---|
| Setup lives in someone's memory | Setup lives in a Git repo everyone can read |
| Staging and production silently diverge ("snowflakes") | Every environment built from the same code |
| Provisioning takes hours of clicking | Provisioning takes one command |
| Disaster recovery is a scramble | Rebuild from code in minutes |
| No history of who changed what | Full audit trail in commit history |
| Changes reviewed by nobody | Changes reviewed in pull requests |
The payoff compounds. Because infrastructure is now code, it inherits every good habit software teams already have: branching, code review, automated tests, CI/CD, and the ability to roll back a bad change by reverting a commit.
✅ A concrete win: disaster recovery
Imagine your entire production region goes down. With click-ops, recovery means someone rebuilding dozens of resources by hand under pressure. With IaC, you point the same code at a new region and run apply. What was a multi-day outage becomes a coffee break.
Declarative vs. Imperative
There are two fundamentally different ways to tell a computer to build something. Understanding the split is the single most important idea in this lesson.
- Imperative = you spell out the steps. "Check if the server exists. If not, create it. Then attach a disk. Then open port 80." You own the logic and the order.
- Declarative = you describe the desired end state. "There should be one server with a 20 GB disk and port 80 open." The tool figures out the steps to get there — creating, updating, or leaving things alone as needed.
A good analogy: an imperative recipe says "crack two eggs, whisk for 30 seconds, fold in flour." A declarative order says "I'd like an omelette." Declarative tools are the waiter who knows how to reach that end state no matter what's already in the kitchen.
The same goal, two ways
Here's a declarative Terraform resource that says "there should be a web server":
# Declarative: describe WHAT you want, not the steps.
# Run this once or a hundred times — the result is identical.
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0" # the machine image
instance_type = "t3.micro" # size of the VM
tags = {
Name = "WebServer"
Environment = "Production"
}
}
And the imperative equivalent as a shell script — notice how you must check state and branch:
#!/bin/bash
# Imperative: YOU write the "does it exist?" logic and the ordering.
INSTANCE_ID=$(aws ec2 describe-instances \
--filters "Name=tag:Name,Values=WebServer" \
--query "Reservations[].Instances[].InstanceId" \
--output text)
if [ -z "$INSTANCE_ID" ]; then
aws ec2 run-instances \
--image-id ami-0c55b159cbfafe1f0 \
--instance-type t3.micro \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=WebServer}]'
else
echo "Instance already exists: $INSTANCE_ID" # you must handle this case
fi
⚠️ Why declarative usually wins
The imperative script is longer, easy to get wrong, and you have to anticipate every state the world might be in. The declarative block says the goal once; the tool handles "already exists," "needs updating," and "doesn't exist yet" for you. Less code, fewer bugs, safe to re-run.
Idempotency
Idempotency is a fancy word for a simple promise: running the same operation many times has the same effect as running it once. Pressing a floor button in an elevator is idempotent — jab it ten times, you still go to floor 5 once. Pouring another cup of coffee is not idempotent; do it ten times and you have a mess.
Declarative IaC is idempotent by design. If your code says "one server should exist" and the server already exists, a second apply does nothing. That's what makes IaC safe to run in automation, on a schedule, or after a failed deploy — it always converges toward the declared state without piling up duplicates.
# Idempotent: the FIRST apply creates the bucket.
# Every apply after that is a no-op unless the code changes.
resource "aws_s3_bucket" "example" {
bucket = "my-company-reports-bucket"
tags = {
Environment = "Production"
Team = "Engineering"
}
}
What you'd see on a second run
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your
configuration and found no differences, so no changes are needed.
Contrast that with a non-idempotent script that increments a counter file every run: each execution changes the outcome. That unpredictability is exactly what IaC eliminates.
Immutable Infrastructure & Drift
Once infrastructure is code, a powerful pattern becomes possible: immutable infrastructure. Instead of logging into a running server to patch or reconfigure it, you never modify a live resource at all. When something needs to change, you build a brand-new resource from updated code and retire the old one.
Configuration drift — the enemy
Drift happens when the real infrastructure quietly diverges from what the code says — usually because someone made a "quick fix" by hand in the console. Now your code lies about reality, the next apply may undo the fix, and nobody can trust the repo. Immutable infrastructure prevents drift by design: nobody edits servers, so servers can't drift.
e.g. new app version"] --> B{Mutable or immutable} B -->|Mutable| C["SSH in and edit the live server"] C --> D["Config drifts from code"] D --> E["Hard to reproduce, risky rollback"] B -->|Immutable| F["Build a new server from new code"] F --> G["Shift traffic over, retire the old one"] G --> H["Zero drift, trivial rollback"]
| Mutable (traditional) | Immutable (modern) |
|---|---|
| Servers live for years, patched in place | Servers are replaced, never edited |
| Drift accumulates silently | Drift is impossible by design |
| Rollback is scary and manual | Rollback = redeploy the previous image |
| Every box has a unique history | Every box is identical and disposable |
💡 How it's done in practice
You bake a machine image (an AMI or a Docker image) with everything pre-installed, keep application data in external stores like databases and object storage, and use blue-green or rolling deploys to swap old resources for new. The server itself becomes cattle, not a pet.
Core Practices
IaC is only as good as the discipline around it. These practices turn "files that happen to describe servers" into a trustworthy engineering workflow.
1. Version control everything
Your infrastructure code lives in Git, right next to (or beside) your application. Changes flow through branches and pull requests, so every modification is reviewed and every version is recoverable.
# Infrastructure changes follow the same flow as app code
git checkout -b feature/add-redis-cache
# ...edit main.tf to add a cache...
terraform plan # preview the diff BEFORE committing intent
git add main.tf
git commit -m "Add Redis cache for session storage"
git push origin feature/add-redis-cache
# Open a pull request → teammate reviews the plan → merge → apply
2. Always plan before you apply
Every serious IaC tool can show you a dry run — the exact list of resources it will add, change, or destroy — before touching anything. Reading that plan is your seatbelt. A plan that says "will destroy: production_database" is a plan you stop and question.
3. Never edit infrastructure by hand
The moment you make a manual change in the console, you've introduced drift and broken the promise that the code is the source of truth. If it's worth changing, it's worth changing in code.
4. Keep secrets out of state and out of Git
Infrastructure often needs passwords, API keys, and certificates. These must never be committed to version control, and you must be careful because tool state files can capture sensitive values. Use a dedicated secrets manager and reference secrets at deploy time.
# DON'T hardcode secrets. DO pull them from a secrets manager.
data "aws_secretsmanager_secret_version" "db" {
secret_id = "prod/database/credentials"
}
resource "aws_db_instance" "database" {
engine = "postgres"
username = "app_admin"
# Referenced at apply time — not stored in your .tf files
password = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)["password"]
}
⚠️ State files are sensitive
The state file that IaC tools keep can contain plaintext values pulled during an apply — including that database password. Store state in an encrypted, access-controlled remote backend, and never commit *.tfstate to Git. You'll go deep on state in the next lesson.
5. Test and scan your infrastructure
Because it's code, you can lint it, validate syntax, scan it for security misconfigurations, and even spin up real resources in a test account to verify behavior — all in CI before anything reaches production.
Practice & Quiz
🏋️ Exercise 1: Declarative or imperative?
Goal: For each operation below, decide whether it's declarative (describes an end state) or imperative (spells out steps). Jot your answer before revealing the key.
- A Terraform block:
resource "aws_s3_bucket" "logs" { bucket = "app-logs" } - A bash loop that SSHes into each server and runs
apt upgrade - A CloudFormation template listing a VPC, subnet, and instance
- A script that checks whether a DNS record exists, then creates it if missing
💡 Hint
Ask: "Does this describe the goal and let a tool find the path (declarative), or does it list the actions and their order (imperative)?"
✅ Solution
1) Declarative — it names the desired resource, not steps. 2) Imperative — explicit loop and commands. 3) Declarative — a template of desired resources. 4) Imperative — you write the "if exists, else create" logic yourself.
🏋️ Exercise 2: Write your first resource block
Goal: Draft a declarative Terraform resource for an S3 bucket named my-portfolio-site tagged with Environment = "Production". Don't worry about running it — focus on the shape.
✅ Solution
resource "aws_s3_bucket" "portfolio" {
bucket = "my-portfolio-site"
tags = {
Environment = "Production"
}
}
Notice you never wrote a single "create" command — you described the bucket you want and let the tool make it real, idempotently.
🎯 Quick Quiz
Question 1: What does it mean for an IaC operation to be idempotent?
Question 2: Which best describes a declarative approach?
Question 3: Why do we say "never edit infrastructure by hand"?
Best Practices & Pitfalls
✅ Do
- Keep all infrastructure code in version control and review it in pull requests
- Always run
plan(a dry run) and read it beforeapply - Prefer declarative tools so re-runs are safe and idempotent
- Treat servers as immutable and disposable — rebuild, don't patch
- Store secrets in a dedicated manager and keep them out of Git and state
❌ Don't
- Make "quick" manual changes in the console — that's how drift starts
- Commit
*.tfstatefiles or hardcoded passwords to a repository - Apply changes to production without a peer-reviewed plan
- Assume two hand-built environments are identical — they never are
⚠️ The snowflake trap
A "snowflake" server is one that was tweaked so many times by hand that no one can reproduce it. When it dies, so does the knowledge of how to rebuild it. IaC's entire purpose is to make snowflakes impossible: if it isn't in code, it doesn't exist.
Summary
🎉 Key Takeaways
- IaC declares infrastructure in version-controlled files and lets a tool reconcile reality to match — no click-ops
- Declarative states the goal and is idempotent; imperative spells out ordered steps
- Idempotency makes re-running safe: same code, same result, no duplicates
- Immutable infrastructure replaces resources instead of editing them, eliminating drift
- The discipline — version control, plan-before-apply, no hand edits, secrets out of state — is what makes IaC trustworthy
📚 Additional Resources
- HashiCorp — What is Terraform? (Intro)
- AWS CloudFormation — overview
- AWS — What is Infrastructure as Code?
🚀 What's Next?
You now understand the why and the core principles. Next, you'll get hands-on with the most popular provider-agnostic IaC tool and install it locally: Terraform Basics & Setup — HCL syntax, providers, resources, and the init/plan/apply/destroy cycle.
🎉 Great start!
You've made the mental shift from clicking buttons to declaring intent. Everything you build in cloud engineering from here rests on these ideas.