// Your cloud, version controlled.
SERVERS SHOULD BE LIKE CASSETTES.
Provisioning infrastructure manually is error-prone and impossible to reproduce. Terraform treats your infrastructure as code—version it, review it, and deploy it automatically.
WHY TERRAFORM
Terraform uses a declarative approach. You define what you want, not how to get there. It figures out the right order, handles dependencies, and creates a plan before making changes.
ONE CODEBASE, ANY CLOUD.
12 lessons. Complete Terraform mastery.
What is IaC, how Terraform works, and why it matters
BeginnerInstalling Terraform, providers, and authentication
BeginnerCreating resources and querying existing infrastructure
BeginnerMaking configurations flexible and reusable
IntermediateHow Terraform tracks resources and remote state
IntermediateOrganizing and reusing infrastructure code
IntermediateManaging multiple environments (dev, staging, prod)
IntermediateRunning scripts on created resources
IntermediateAutomating Terraform in your pipelines
AdvancedTerratest, sentinel, and infrastructure testing
AdvancedManaging AWS, GCP, Azure with single config
AdvancedPolicy as Code, Terragrunt, and scaling Terraform
AdvancedTerraform is the industry standard for infrastructure as code. It supports over 1000 providers and works across all major cloud platforms.
What you'll master: AWS, GCP, Azure provisioning, state management, modules, workspaces, and enterprise patterns. By the end, you'll provision entire environments with a single command.
Define once, deploy anywhere.
IaC is the practice of managing infrastructure through code rather than manual processes. Your servers, networks, and databases are defined in configuration files that can be versioned, reviewed, and automated.
# Terraform uses a declarative approach
# You define the desired state, not the steps
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
1. Write → Define infrastructure in .tf files
2. Plan → Terraform shows what will change
3. Apply → Terraform makes the changes
4. Repeat → Infrastructure evolves with code
What type of approach does Terraform use?
Hint: Define the ??? state
What command shows what Terraform will change?
Hint: terraform ???
# macOS with Homebrew
brew install terraform
# Linux - download binary
wget https://releases.hashicorp.com/terraform/1.7.0/terraform_1.7.0_linux_amd64.zip
unzip terraform_1.7.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/
# Verify installation
terraform --version
# main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
# Can use environment variables
# AWS_ACCESS_KEY_ID
# AWS_SECRET_ACCESS_KEY
}
# Initialize providers and modules
terraform init
# Downloaded providers are stored in .terraform/
What command initializes Terraform?
Hint: terraform ???
Where are Terraform providers downloaded?
Hint: Hidden folder
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-server"
Environment = "production"
}
}
resource "aws_security_group" "web" {
name = "web-sg"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Query existing infrastructure without creating it
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
}
owners = ["099720109477"] # Canonical
}
# Use the data source
resource "aws_instance" "server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
}
# Terraform automatically handles dependencies
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
# This depends on aws_security_group.web implicitly
}
resource "aws_security_group" "web" {
name = "web-sg"
}
What block queries existing infrastructure?
Hint: ??? source
What block creates new infrastructure?
Hint: ??? block
# variables.tf
variable "instance_type" {
type = string
default = "t3.micro"
description = "EC2 instance type"
}
variable "environment" {
type = string
description = "Environment name"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be dev, staging, or prod"
}
}
variable "tags" {
type = map(string)
default = {}
description = "Tags to apply to resources"
}
# Using variables in resources
resource "aws_instance" "web" {
instance_type = var.instance_type
ami = "ami-0c55b159cbfafe1f0"
tags = merge(var.tags, {
Environment = var.environment
})
}
# outputs.tf
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web.id
sensitive = false
}
output "instance_public_ip" {
description = "Public IP address"
value = aws_instance.web.public_ip
}
What block defines configurable values?
Hint: ??? block
What exposes resource values to the user?
Hint: ??? block
Terraform uses state to map real-world resources to your configuration. It tracks resource IDs and properties so it knows what to update or delete.
# Default - stored in terraform.tfstate
# Not recommended for teams
# Configure remote state in S3
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# DynamoDB table for state locking
# Prevents concurrent modifications
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
terraform show # Show current state
terraform state list # List resources
terraform state show RESOURCE # Show resource details
terraform state mv # Move resource
terraform state rm # Remove from state
Where does Terraform store resource mapping?
Hint: terraform.???
What prevents concurrent Terraform runs?
Hint: State ???
Modules are reusable Terraform configurations. They let you group resources, hide complexity, and share infrastructure patterns.
# modules/ec2/main.tf
variable "instance_type" {
type = string
default = "t3.micro"
}
variable "name" {
type = string
}
resource "aws_instance" "this" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.instance_type
tags = {
Name = var.name
}
}
output "instance_id" {
value = aws_instance.this.id
}
# main.tf
module "web_server" {
source = "./modules/ec2"
instance_type = "t3.medium"
name = "web-server"
}
# Reference module output
output "server_id" {
value = module.web_server.instance_id
}
# Local
source = "./modules/vpc"
# Public registry
source = "terraform-aws-modules/vpc/aws"
# Private registry
source = "app.terraform.io/org-name/vpc/aws"
What allows reusing Terraform configurations?
Hint: ???
What attribute references module outputs?
Hint: module.???.output
Workspaces let you manage multiple environments from the same configuration. Each workspace has its own state file.
# List workspaces
terraform workspace list
# Create new workspace
terraform workspace new dev
# Switch workspace
terraform workspace select prod
# Delete workspace
terraform workspace delete staging
# Use workspace in resource names
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.instance_type
tags = {
Name = "web-${terraform.workspace}"
}
}
# Conditional based on workspace
resource "aws_instance" "prod_db" {
count = terraform.workspace == "prod" ? 1 : 0
# ...
}
# Each workspace should have separate state
# dev/terraform.tfstate
# prod/terraform.tfstate
# Use separate state backends
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "${terraform.workspace}/terraform.tfstate"
}
}
What separates state between environments?
Hint: terraform.???
What command creates a new workspace?
Hint: terraform workspace ???
Provisioners let you execute scripts on local or remote machines after resource creation.
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
provisioner "local-exec" {
command = "echo ${self.private_ip} > inventory.txt"
}
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx",
"sudo systemctl enable nginx",
"sudo systemctl start nginx"
]
}
}
provisioner "file" {
content = templatefile("nginx.conf.tpl", { domain = var.domain })
destination = "/etc/nginx/sites-available/${var.domain}"
}
What runs a script on the machine running Terraform?
Hint: ??? provisioner
What runs a script on the created resource?
Hint: ??? provisioner
Use CI/CD pipelines to automate Terraform runs with proper approvals and security.
name: Terraform
on:
push:
branches: [main]
pull_request:
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
- name: Terraform Init
run: terraform init
- name: Terraform Plan
run: terraform plan
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Require approval for production
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && needs.plan.outputs.approved == 'true'
run: terraform apply -auto-approve
What action sets up Terraform in GitHub Actions?
Hint: hashicorp/???
What flag auto-approves Terraform changes?
Hint: terraform apply ???
# Syntax validation
terraform validate
# Format check
terraform fmt -check
# Check for breaking changes
terraform plan -detailed-exitcode
// Example Terratest in Go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
)
func TestTerraformWebServer(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../examples/web-server",
Vars: map[string]interface{}{
"instance_type": "t3.micro",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
instanceID := terraform.Output(t, terraformOptions, "instance_id")
t.Logf("Instance ID: %s", instanceID)
}
# Sentinel policy example
import "tfrun"
# Prevent production deployments on Fridays
main = rule {
tfrun.workspace.name != "prod" or
time.now.weekday != "Friday"
}
What command validates Terraform syntax?
Hint: terraform ???
What testing framework uses Go?
Hint: ???
Terraform's provider-agnostic approach lets you manage multiple clouds with the same tool.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
google = {
source = "hashicorp/google"
version = "~> 4.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
provider "google" {
region = "us-central1"
}
provider "azurerm" {
features {}
}
# Use variables for cloud-agnostic configs
variable "cloud_provider" {
type = string
default = "aws"
}
locals {
vm_settings = var.cloud_provider == "aws" ? {
ami = "ami-0c55b159cbfafe1f0"
} : var.cloud_provider == "gcp" ? {
source_image = "ubuntu-2204"
} : {}
}
What lets Terraform work with multiple clouds?
Hint: ???
What block creates cloud-agnostic configurations?
Hint: ??? block
# terragrunt.hcl
terraform {
source = "git::https://github.com/org/modules//web-app?ref=v1.0.0"
}
inputs = {
instance_type = "t3.medium"
desired_capacity = 3
}
dependency "vpc" {
config_path = "../vpc"
}
# OPA Rego policy
package terraform
deny[msg] {
input.resource.aws_instance.type == "t2.micro"
msg = "Use t3 instances instead of t2"
}
deny[msg] {
input.resource.aws_s3_bucket.public_read
msg = "S3 buckets must not be public"
}
What tool wraps Terraform for better organization?
Hint: ???
What enforces policies in Terraform Enterprise?
Hint: ??? policies