100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Terraform & Infrastructure as Code
45 minintermediate

Foundations Practice — Provision a VM

What You'll Build

You will write Terraform configuration from scratch to provision a virtual machine on a cloud provider, along with the networking components it needs: a VPC, a public subnet, an internet gateway, and a security group. You will use the CricketPulse API server as the target use case — a t3.micro EC2 instance in AWS running Amazon Linux that will serve cricket scores. You will run `terraform init`, `terraform plan`, and `terraform apply` to provision the infrastructure, then modify a resource attribute and observe how `terraform plan` identifies the change, and finally run `terraform destroy` to clean up. This exercise builds the muscle memory for the core Terraform workflow that underlies all subsequent lessons.

Analogy🏏Cricket
🏏 Think of it like cricket: This exercise is your first bat at the nets — not a match, but the first real contact between bat and ball. You're not yet managing complex match strategies or multi-ground tournaments; you're simply practicing the forward defensive — the fundamental stroke. Provisioning a single VM with its network prerequisites is the forward defensive of Terraform: not glamorous, but the foundation of every subsequent shot. Each command (init, plan, apply) is a distinct movement to practise until it becomes automatic, because every future Terraform operation builds on this same three-step sequence.

Prerequisites

  • An AWS account with IAM credentials having EC2, VPC, and security group permissions (or use a free-tier account — t3.micro qualifies for AWS Free Tier).
  • Terraform CLI installed — `brew install terraform` on macOS, or download from developer.hashicorp.com/terraform/downloads.
  • AWS CLI configured with credentials — `aws configure` with access key and secret, OR set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.
  • A text editor and terminal — all Terraform operations happen in the terminal with .tf files edited in any editor.
  • Understanding of Lessons 1–3 — IaC principles, declarative vs imperative, providers and resources.

Setup & Project Structure

Create a dedicated directory for the CricketPulse infrastructure and set up the Terraform configuration files. Using separate files for different concerns (providers, network, compute) is a common convention that keeps configurations readable.

Analogy🏏Cricket
🏏 Think of it like cricket: setting up separate files for providers, network, and compute is like organising your kit bag before a match with distinct compartments — pads in one, gloves in another, bat and helmet each in their place. Just as a well-ordered kit bag lets you grab exactly the gear you need without rummaging, splitting your Terraform config by concern lets you find the network or provider settings instantly. Just as every player knows the batting order is written down so nobody argues about who's next in, the file-per-concern convention means any teammate reading your CricketPulse config knows precisely where the compute or networking lives. And just as a tidy kit bag survives a chaotic changeroom, a readable structure survives a growing configuration. The payoff: like a professional who never scrambles for a missing glove at the crease, you keep the config navigable and calm as the infrastructure grows.
bash
# Create project directory
mkdir cricketpulse-infra && cd cricketpulse-infra

# Create file structure
touch main.tf network.tf compute.tf outputs.tf

# Set AWS credentials via environment variables
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_DEFAULT_REGION="us-east-1"

# Verify AWS access
aws sts get-caller-identity
# Should return your account ID, not an error

Step 1 — Foundation

Write the provider configuration and network resources — VPC, subnet, internet gateway, and route table. The network layer must exist before the compute layer, and Terraform will infer this order from the attribute references between resources.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 1 builds the ground infrastructure before the match begins. The VPC is the stadium boundary walls — the outer container that defines the playing area. The subnet is the designated field within the boundary. The internet gateway is the public entrance gate that allows spectators (traffic) to enter. You build the ground infrastructure before placing the players (EC2 instances) on the field.
hcl
# main.tf — provider configuration
terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

# network.tf — networking resources
resource "aws_vpc" "cricketpulse" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  tags = { Name = "cricketpulse-vpc", ManagedBy = "terraform" }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.cricketpulse.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true
  tags = { Name = "cricketpulse-public-1a" }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.cricketpulse.id
  tags   = { Name = "cricketpulse-igw" }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.cricketpulse.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
  tags = { Name = "cricketpulse-public-rt" }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# Initialise and plan the network
# terraform init
# terraform plan

Step 2 — Core Logic

Add the security group and EC2 instance. The security group defines what network traffic the instance allows; the instance itself is the compute resource. Run `terraform apply` to provision everything and observe the dependency-ordered creation sequence.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 2 is placing the players on the prepared ground. The security group is the access accreditation policy — which players (traffic sources) are allowed through which gates (ports). The EC2 instance is the opening batsman taking the crease. The apply command is the toss coin flip that starts the match — the moment the plan becomes real infrastructure.
hcl
# compute.tf — security group and EC2 instance

resource "aws_security_group" "api" {
  name        = "cricketpulse-api-sg"
  description = "Security group for CricketPulse API server"
  vpc_id      = aws_vpc.cricketpulse.id

  ingress {
    description = "HTTP from internet"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "SSH for administration"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]  # Restrict SSH to internal network only
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "cricketpulse-api-sg" }
}

# Look up the latest Amazon Linux 2023 AMI
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "cricketpulse_api" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.api.id]

  user_data = <<-EOF
    #!/bin/bash
    yum update -y
    yum install -y nodejs npm
    echo '{"status":"CricketPulse API running"}' > /tmp/scores.json
    python3 -m http.server 80 --directory /tmp &
  EOF

  tags = {
    Name        = "cricketpulse-api"
    Environment = "dev"
    ManagedBy   = "terraform"
  }
}

# outputs.tf
output "instance_public_ip" {
  description = "Public IP of the CricketPulse API server"
  value       = aws_instance.cricketpulse_api.public_ip
}

# Apply!
# terraform apply
# (type 'yes' to confirm)
# After apply: note the instance public IP from outputs

Step 3 — Integration & Enhancement

Modify the EC2 instance type (change `t3.micro` to `t3.small`) and run `terraform plan` to see how Terraform detects and presents the change. Observe the difference between in-place modifications and forced replacements (destroy + create).

Analogy🏏Cricket
🏏 Think of it like cricket: Step 3 is making a tactical adjustment mid-match — changing the batting position (instance type) of an already-fielded player. Some changes are in-place (the batsman changes their stance without leaving the crease — `~` in the plan). Others require the player to leave and re-enter (the player goes back to the pavilion and returns — `-/+` replace in the plan). Terraform's plan output tells you which type of change is required before you commit to it.
bash
# Modify instance type in compute.tf
# Change: instance_type = "t3.micro"  →  instance_type = "t3.small"

# Run terraform plan to see the change
terraform plan
# Output will show:
#   ~ resource "aws_instance" "cricketpulse_api" {
#       ~ instance_type = "t3.micro" -> "t3.small"
#         # (forces replacement)
#       - id             = "i-0abc123..."
#       + id             = (known after apply)
#     }
# Plan: 1 to add, 0 to change, 1 to destroy.
#
# Note: AWS requires stopping/recreating an instance to change its type
# Terraform shows this as a replacement (-/+) not an in-place update (~)

# Apply the change
terraform apply

# Check the state
terraform show  # Shows full current state of all resources
terraform state list  # Lists all managed resource addresses

Step 4 — Testing & Verification

Verify the provisioned infrastructure works, explore the state file, and then destroy everything cleanly.

Analogy🏏Cricket
🏏 Think of it like cricket: verifying the provisioned VM works, inspecting the state file, then destroying it cleanly is like the post-innings routine — confirm the scoreboard matches what happened on the field, review the official scorebook ball by ball, then clear and cover the ground so nothing is left running overnight. Just as you'd check the batsman's recorded runs actually reflect the shots played, you verify the infrastructure Terraform claims to have built is genuinely reachable and functioning. Just as the scorebook is the authoritative record you consult to settle any dispute, the state file is where you inspect exactly what Terraform tracks as existing. And just as a responsible side clears its kit and covers the pitch after stumps to avoid damage and cost, `terraform destroy` tears everything down cleanly so no resources keep billing you. The payoff: like a match properly closed out and the ground restored, you confirm the work, understand the record, and leave a clean slate behind.
bash
# Verify the instance is reachable
INSTANCE_IP=$(terraform output -raw instance_public_ip)
curl http://$INSTANCE_IP/scores.json
# {"status":"CricketPulse API running"}

# Explore the Terraform state
terraform state list
# aws_instance.cricketpulse_api
# aws_security_group.api
# aws_subnet.public
# aws_vpc.cricketpulse
# ...

# Show details of a specific resource in state
terraform state show aws_instance.cricketpulse_api

# Plan a destroy (see what will be removed)
terraform plan -destroy
# Plan: 0 to add, 0 to change, 6 to destroy.

# Destroy all managed resources
terraform destroy
# type 'yes' to confirm
# All resources destroyed — no orphaned AWS resources left behind

Warning: EC2 instance type changes require instance replacement (stop, terminate, create new) — they are not in-place modifications. Any data stored on the instance's ephemeral disk is lost during replacement. If your instance has stateful data (application state, local files), always use persistent storage (EBS volumes, S3) and ensure your data is on attached volumes (which survive instance replacement) rather than the root volume alone. For this exercise, the instance is stateless, so replacement is safe. In production, understand which attributes cause replacement vs in-place update before applying changes.

Extension Challenge: Add an EBS data volume to the CricketPulse API instance and attach it using `aws_volume_attachment`. Notice that the volume is a separate Terraform resource from the instance — it can be replaced independently. Then add a second EC2 instance in a second availability zone (us-east-1b) using the same configuration but a different subnet. Finally, add an Application Load Balancer in front of both instances using `aws_lb`, `aws_lb_target_group`, and `aws_lb_listener` resources — making the CricketPulse API highly available.

  • The complete Terraform workflow: `init` → `plan` → `apply` → modify code → `plan` → `apply` → `destroy`. Build this sequence into muscle memory.
  • Data sources (`data "aws_ami" ...`) look up existing resources without owning them — use them for dynamic values like the latest AMI ID rather than hard-coding AMI IDs.
  • Terraform plan output uses `+` (create), `~` (modify in-place), `-` (destroy), and `-/+` (destroy and recreate) — read the plan carefully before applying, especially `-/+` which implies data loss risk.
  • All resources are automatically destroyed with their dependencies by `terraform destroy` in the correct order — no manual cleanup of orphaned resources needed.
  • `terraform state list` and `terraform state show` are essential debugging tools — they show exactly what Terraform knows about each managed resource.
  • Attribute changes that require resource replacement (like `instance_type` for EC2) are shown as `-/+` in the plan — always check whether a planned change causes replacement before applying to production.
Lesson 4 of 24
0% complete