With the environment set up, I wrote my first real Terraform code — a complete VPC with a public subnet, security group, and an EC2 instance running nginx.

What I Built

  • VPC (10.0.0.0/16)
  • Public subnet with Internet Gateway
  • Security group (SSH + HTTP)
  • EC2 instance (Ubuntu 24.04)
  • SSH key pair

File Structure

exercise-02-variables-outputs/
├── main.tf
├── variables.tf
├── locals.tf
├── outputs.tf
├── data.tf
├── key.tf
└── terraform.tfvars

Separating resources into logical files keeps things clean and readable.

Key Patterns

Data source for AMI — no hardcoded IDs:

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu-pro-server/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-pro-*"]
  }
}

Locals for consistent naming:

locals {
  exercise    = "exer-02"
  name_prefix = "sqp-${local.exercise}"
}

Mistakes I Made

.id missing on resource references:

# Wrong — passes entire object
vpc_id = aws_vpc.vpc

# Correct — passes just the ID string
vpc_id = aws_vpc.vpc.id

No public IP on EC2 — subnets don’t auto-assign public IPs by default:

map_public_ip_on_launch = true

SSH port not open — security groups are explicit, nothing is open by default. Port 22 needs its own ingress rule.

Outputs

output "ssh_command" {
  value = "ssh -i ~/.ssh/aws_key ubuntu@${aws_instance.server.public_ip}"
}

Small thing, big convenience — the exact SSH command printed after every terraform apply.

Always Destroy When Done

terraform destroy

Even small EC2 instances add up. Good habit to build early.