Infrastructure as Code with Terraform
Adopt infrastructure as code with Terraform — CI/CD foundations.
Focus: adopt infrastructure as code with terraform
You've automated your tests and deployments, but your infrastructure is still a sprawl of console clicks, shell scripts, and untracked manual changes. Every environment drifts further from every other, and the 'works on my machine' problem has metastasized into 'works in my cloud account'. It's time to adopt infrastructure as code with Terraform — the practice that turns your cloud resources into versioned, reviewable, repeatable declarations.
The problem this lesson solves
When infrastructure is managed by hand — pointing and clicking in a cloud console or running ad-hoc commands — a few painful symptoms appear in every team:
- Drift: one person tweaks a security group in production, another changes a subnet in staging, and no one remembers when or why.
- No review: a change to a server or database is deployed without a pull request, so mistakes go unnoticed until they break something.
- Mystery state: you can't answer the question 'what exactly is running in account 447?'. You'd have to log in and click around for an hour.
- Cold-start fear: setting up a new environment takes days, and getting it wrong means a security hole or a surprise bill.
- Rollback nightmares: when an incident happens, you can't 'undo' a console change — you have to re-create it by hand.
This is exactly the problem that adopting infrastructure as code with Terraform solves. Terraform is the industry-standard tool that lets you define your entire infrastructure — networks, load balancers, database instances, IAM roles — as declarative configuration files in a directory you can commit to Git. The state of your infrastructure becomes a file, your changes become pull requests, and your environments become reproducible from a single command.
The pain is real: a 2021 survey found that over 60% of organizations struggle with environment drift, and most trace it back to manual infrastructure changes.
Core concept / mental model
Think of infrastructure as code the way you already think about application code — but for your cloud resources. With normal code, you write a function, commit it, and deploy. With IaC, you write a description of what your infrastructure should look like, commit it, and let a tool make reality match that description.
Terraform's mental model has three pieces:
- Desired state: you write
.tffiles that describe the resources you want — e.g., "an S3 bucket namedbuilds-prodwith encryption enabled". - Current state: Terraform keeps a snapshot of everything it created in a state file (
terraform.tfstate), either locally or in a remote backend. - Reconciliation: when you run
terraform apply, it compares desired vs. current state and produces a plan of what to create, modify, or destroy. You approve it, and it makes the changes.
This is different from imperative scripting (like a shell script that runs aws s3api create-bucket ...). The script says how to get there; Terraform says what should exist. If someone deletes a bucket manually, running terraform apply will recreate it — because the desired state is the source of truth.
A useful mental picture: Terraform is a contractor with a blueprint. The blueprint (your .tf files) says what the building should look like. The contractor (Terraform) assesses what's already there, tells you exactly what it will change, and then does the work. If you change the blueprint, the contractor adjusts the building — not by tearing it down and rebuilding, but by making the minimal changes needed.
How it works step by step
Adopting infrastructure as code with Terraform follows a predictable workflow. Here's the high-level sequence you'll repeat on every infrastructure change:
- Write or edit configuration — create
.tffiles that declare resources. Each resource has a type (likeaws_instance) and a name (likeweb). - Initialize — run
terraform initto download provider plugins (the AWS provider, for example) and set up the backend. This is yournpm installfor Terraform. - Format and validate — run
terraform fmtto ensure consistent styling, andterraform validateto catch syntax errors and invalid references. - Preview the plan — run
terraform planto see a detailed diff of what Terraform will create, change, or destroy. This is your code review for infrastructure. - Apply — run
terraform applywith your approval to execute the plan. Terraform makes API calls to your cloud provider to reach the desired state. - Commit — save your
.tffiles and the relevant state (or a remote state reference) to Git. Now the change is versioned and reviewable.
A common pattern for teams is to treat terraform plan like a CI pipeline stage: every pull request that touches .tf files runs terraform plan in CI, and a human reviews the output before merging. Then terraform apply runs only after merge, either manually or via a CI/CD pipeline.
Pro tip: Never store
terraform.tfstatein Git. It can contain secrets and drift. Use a remote backend like AWS S3 with DynamoDB locking so your team shares one source of truth for state.
Hands-on walkthrough
Let's make this concrete. Imagine you're setting up a small web application on AWS. You want an S3 bucket to store build artifacts. Here's how you'd adopt infrastructure as code with Terraform.
Step 0 — Install Terraform (if you haven't): download the binary from the official site, or use a package manager.
# macOS with Homebrew
brew install hashicorp/tap/terraform
# Verify
terraform version
Step 1 — Create your configuration directory
mkdir my-infra
cd my-infra
Step 2 — Write a main.tf file with your provider and resource:
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "builds" {
bucket = "my-builds-bucket"
}
Step 3 — Initialize and preview
terraform init
terraform plan
You'll see output like:
Terraform will perform the following actions:
# aws_s3_bucket.builds will be created
+ resource "aws_s3_bucket" "builds" {
+ bucket = "my-builds-bucket"
+ force_destroy = false
+ id = (known after apply)
...
}
Plan: 1 to add, 0 to change, 0 to destroy.
Step 4 — Apply (with your AWS credentials in environment variables or a credentials file)
export AWS_ACCESS_KEY_ID=your_key
export AWS_SECRET_ACCESS_KEY=your_secret
terraform apply
Type yes when prompted. Terraform will create the bucket and write a state file. If you now run terraform plan again, you'll see "No changes. Infrastructure is up-to-date." That's your desired state achieved.
Step 5 — Make a change — for example, enable versioning on the bucket:
resource "aws_s3_bucket" "builds" {
bucket = "my-builds-bucket"
}
resource "aws_s3_bucket_versioning" "builds_versioning" {
bucket = aws_s3_bucket.builds.id
versioning_configuration {
status = "Enabled"
}
}
Run terraform plan again to see the incremental diff: it will add a new resource, not destroy the bucket. That's exactly the behavior you want.
Pro tip: Use
terraform importif you already have resources created manually — Terraform can adopt them into your state so you don't have to start from scratch.
Compare options / when to choose what
Terraform isn't the only infrastructure-as-code tool. Here's how it stacks up against the common alternatives.
| Tool | Language | State management | Best for |
|---|---|---|---|
| Terraform | HCL (declarative) | Built-in (local or remote) | Multi-cloud, broad provider ecosystem |
| AWS CloudFormation | JSON/YAML (declarative) | AWS-managed | AWS-only teams |
| Pulumi | Python/TypeScript/Go | Managed service or self-hosted | Developers who prefer writing real code |
| Ansible | YAML (imperative) | No inherent state | Config management + orchestration |
- Choose Terraform when you need a provider-agnostic approach (works with AWS, GCP, Azure, and 800+ others), or you want a mature ecosystem and community.
- Choose CloudFormation if you're 100% AWS and want AWS-native integration (like drift detection built-in). But you'll be locked into AWS.
- Choose Pulumi if you're a developer who wants to use Python or TypeScript for infrastructure. It offers more programming power but has a smaller ecosystem.
- Avoid Ansible for pure IaC: it's great for configuring software on existing servers, but it doesn't manage the resource lifecycle like Terraform does.
Terraform also shines in CI/CD integration: it can be run in any pipeline (GitHub Actions, GitLab CI, Jenkins) with a simple CLI, and the plan output is easy to post as a pull-request comment.
Troubleshooting & edge cases
You'll hit issues when adopting infrastructure as code with Terraform. Here are the most common ones and how to fix them.
1. "Error: No valid credential sources found"
Terraform can't find your cloud credentials. Fix: set environment variables (like AWS_ACCESS_KEY_ID), configure a credentials file, or use IAM roles in CI. Never put secrets in .tf files.
2. State lock errors
When using a remote backend with locking, you might see Error: Error acquiring the state lock. This happens when another process is running terraform apply. Fix: wait for it to finish, or force-unlock with the lock ID shown in the error (use caution).
3. Resource already exists
If you create a bucket and then someone manually creates the same-named resource, your next apply will fail. Fix: import the existing resource with terraform import aws_s3_bucket.builds my-builds-bucket, or change the resource name in your config.
4. "Provider not found"
You forgot to run terraform init. Fix: run terraform init in the directory.
5. Destruction is scary
Running terraform destroy will delete all managed resources. In production, protect critical infrastructure with prevent_destroy = true in your resource block, or use state environments (separate state for prod vs dev) so you never accidentally wipe production.
6. Drift you didn't cause
If someone manually changes a resource, Terraform will detect it on the next apply and revert it. That's the desired behavior. If you want to allow manual changes, use the lifecycle { ignore_changes = [...] } block — but use it sparingly.
Pro tip: Always run
terraform planin your CI pipeline for pull requests. It gives your team a preview of infrastructure changes without actually applying them.
What you learned & what's next
Congratulations — you've taken the first big step toward a modern infrastructure workflow. In this lesson, you learned:
- What it means to adopt infrastructure as code with Terraform: describing your infrastructure as versionable configuration files instead of manual actions.
- The core concepts: desired state vs. current state, the provider plugin model, and the plan/apply loop.
- The step-by-step workflow:
init,plan,apply, and how CI can gate changes. - How to write a basic configuration for an AWS resource (S3 bucket) and make incremental changes safely.
- How to choose between Terraform and alternatives like CloudFormation and Pulumi.
- Common pitfalls and how to fix them — credentials, state locks, and drift.
This is a foundation, and the natural next step in your CI/CD foundations track is to wire Terraform into your pipeline — for example, a GitHub Actions workflow that runs terraform plan on every pull request and terraform apply on merge. That's exactly what you'll tackle in the next lesson: "Automating Terraform in CI/CD". You'll move from running commands locally to letting your pipeline manage infrastructure safely.
Keep this mental model: your .tf files are the source of truth, and terraform plan is your guardrail. Everything else — remote state, CI integration, module reuse — builds on this core loop.
Practice recap
Create a simple EC2 instance (or another provider resource like a DigitalOcean droplet) using Terraform. Run terraform plan, apply it, then modify the instance type and apply again to see the incremental change. Finally, run terraform destroy to clean up — and note how Terraform tracks every resource you created.
Common mistakes
- Storing
terraform.tfstatein Git — it can contain sensitive data and causes conflicts when multiple teammates apply changes. Use a remote backend with locking instead. - Running
terraform applywithout reviewing the plan output first, especially in production. Always scrutinize the 'will destroy' lines. - Forgetting to run
terraform initin a new clone, leading to confusing 'provider not found' errors. - Using
terraform destroyon shared infrastructure without realising it will wipe all resources. Protect critical resources withprevent_destroy = true. - Mixing manual console changes with Terraform-managed resources, creating drift that breaks the next apply. Import existing resources instead.
Variations
- Use Terraform Cloud or another remote backend (e.g. S3 + DynamoDB) to centralize state and enable team collaboration.
- Adopt a module-based structure to reuse infrastructure components across environments (dev/staging/prod) rather than duplicating code.
- Combine Terraform with Pulumi or AWS CDK if you prefer writing infrastructure in a general-purpose language like Python or TypeScript.
Real-world use cases
- Provisioning a new staging environment for every pull request, so tests run against isolated infrastructure that is automatically destroyed afterward.
- Managing network security groups and IAM roles for a multi-account AWS setup, ensuring consistent security rules across all teams.
- Teardown of temporary CI/CD infrastructure (e.g., test databases, build servers) after pipeline completion to save costs, using
terraform destroy.
Key takeaways
- Adopting infrastructure as code with Terraform means describing your desired infrastructure in
.tffiles, then letting Terraform reconcile reality to match. - The core loop is init → plan → apply: always preview changes with
terraform planbefore applying them. - Use remote state backends to share infrastructure state across your team and avoid conflicts.
- Terraform is provider-agnostic, working across AWS, GCP, Azure, and many others — unlike CloudFormation's AWS lock-in.
- Import existing resources to bring them under Terraform management instead of deleting and recreating.
- Integrate
terraform planinto CI to gate infrastructure changes with peer review.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.