Solutions — Homework 08
Lesson 08 · Cloud Infrastructure & DevOps · Worked solutions with common mistakes
Task 1 — Cloudflare DNS A record via Terraform
Add the Cloudflare provider to versions.tf:
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
cloudflare = { source = "cloudflare/cloudflare", version = "~> 4.0" }
}
backend "s3" {
bucket = "url-shortener-tfstate-<your-handle>"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "url-shortener-tflock"
encrypt = true
}
}
provider "aws" {
region = var.region
}
provider "cloudflare" {
# Reads CLOUDFLARE_API_TOKEN from the environment automatically.
# Never put the token inline here.
}
Add a cf_zone variable to variables.tf:
variable "cf_zone" {
type = string
description = "Cloudflare zone name, e.g. 'example.com'."
}
Append the DNS resources to main.tf:
data "cloudflare_zone" "primary" {
name = var.cf_zone
}
resource "cloudflare_record" "vm" {
zone_id = data.cloudflare_zone.primary.id
name = var.domain # e.g. "urls.example.com"
value = aws_eip.vm.public_ip
type = "A"
proxied = false
ttl = 60
}
Set the API token and run:
export CLOUDFLARE_API_TOKEN=<your-token>
terraform init # downloads the cloudflare provider
terraform apply
Common mistake — token inline in the provider block
Putting api_token = "cf-..." directly inside provider "cloudflare" {} means the token is stored in the state file and visible in plan output. Always supply it via the CLOUDFLARE_API_TOKEN environment variable (the provider reads it automatically) so the secret never reaches .tf files, the state, or CI logs.
Task 2 — Tighten SSH to your IP only
Add the HTTP provider to versions.tf’s required_providers block:
http = { source = "hashicorp/http", version = "~> 3.0" }
Add the data source, variable, and local to main.tf:
data "http" "myip" {
url = "https://ipv4.icanhazip.com"
}
variable "ssh_admin_cidr" {
type = string
default = ""
}
locals {
effective_admin_cidr = var.ssh_admin_cidr != "" ? var.ssh_admin_cidr : "${trimspace(data.http.myip.response_body)}/32"
user_data = templatefile("${path.module}/user_data.sh", {
ghcr_owner = var.ghcr_owner
domain = var.domain
acme_email = var.acme_email
})
}
Replace the SSH ingress rule inside aws_security_group "web":
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [local.effective_admin_cidr]
}
Apply:
terraform init # loads the http provider
terraform apply
Common mistake — stale CIDR after home IP changes
data.http.myip is evaluated at apply time, not continuously. If your home IP changes (DHCP renewal, travelling, VPN), the security group becomes stale and your next SSH attempt will be refused. For a stable home network, set ssh_admin_cidr explicitly in terraform.tfvars rather than relying on the icanhazip lookup. Reserve the dynamic lookup for quick iterative sessions where you know the IP is stable throughout the work window.
Task 3 — Two environments via workspaces
Modify the tags map on aws_instance.vm in main.tf:
resource "aws_instance" "vm" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
key_name = var.ssh_key_name
vpc_security_group_ids = [aws_security_group.web.id]
user_data = local.user_data
tags = {
Name = "url-shortener-${terraform.workspace}"
Env = terraform.workspace
}
}
Create and apply the dev environment:
terraform workspace new dev
terraform apply
Create and apply the prod environment with a larger instance type:
terraform workspace new prod
terraform apply -var instance_type=t3.small
Verify both instances are running:
aws ec2 describe-instances \
--filters 'Name=tag:Name,Values=url-shortener-*' \
--query 'Reservations[].Instances[].[Tags[?Key==`Name`].Value | [0], InstanceType, State.Name]' \
--output table
Expected output: two rows, one url-shortener-dev / t3.micro / running and one url-shortener-prod / t3.small / running.
Workspace state paths in S3 (the S3 backend handles this automatically):
default→prod/terraform.tfstatedev→env:/dev/prod/terraform.tfstateprod→env:/prod/prod/terraform.tfstate
Tear down both environments when finished:
terraform workspace select dev
terraform destroy
terraform workspace select prod
terraform destroy
Common mistake — applying in the wrong workspace
terraform destroy reads the currently selected workspace. If your shell has the wrong workspace active, you may destroy prod when you meant dev. Two practical defences: (a) add terraform.workspace to your shell prompt (via a cd hook or PS1 customisation) so the active workspace is always visible; (b) prefix every destructive command with terraform workspace show and read the output before confirming. The AWS EC2 console is the second check: always verify the Name tag on the instance before confirming destroy.
You have now closed the loop on single-server deployment: the VM and everything around it — compose, Caddy, DNS, secrets, and now even the infrastructure lifecycle — is code or generated by code. Module 4 graduates to Kubernetes: the t3.micro becomes a kind cluster on your laptop, then a managed EKS cluster in the capstone, and docker compose is replaced by Helm charts.