Exercises and projects
Objectives
- Consolidate your Terraform knowledge
- Put all the concepts into practice
- Develop operational skills
Exercise 1: First deployment
Objective
Create a simple EC2 instance with Terraform.
Files to create
# main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.region
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = {
Name = "web-server"
Environment = var.environment
}
}
# variables.tf
variable "region" {
description = "AWS region"
type = string
default = "eu-west-1"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "environment" {
description = "Environment name"
type = string
default = "development"
}
# outputs.tf
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web.id
}
output "public_ip" {
description = "Public IP of the EC2 instance"
value = aws_instance.web.public_ip
}
Commands
terraform init
terraform plan
terraform apply
terraform destroy
Exercise 2: Complete VPC
Objective
Create a VPC with public and private subnets.
Architecture
Code
# vpc.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project}-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project}-igw"
}
}
resource "aws_subnet" "public" {
count = length(var.public_subnets)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnets[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project}-public-${count.index + 1}"
Type = "public"
}
}
resource "aws_subnet" "private" {
count = length(var.private_subnets)
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnets[count.index]
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.project}-private-${count.index + 1}"
Type = "private"
}
}
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "${var.project}-nat-eip"
}
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id
tags = {
Name = "${var.project}-nat"
}
depends_on = [aws_internet_gateway.main]
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project}-public-rt"
}
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
tags = {
Name = "${var.project}-private-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(var.public_subnets)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
count = length(var.private_subnets)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}
# variables.tf
variable "project" {
default = "myproject"
}
variable "vpc_cidr" {
default = "10.0.0.0/16"
}
variable "availability_zones" {
default = ["eu-west-1a", "eu-west-1b"]
}
variable "public_subnets" {
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "private_subnets" {
default = ["10.0.10.0/24", "10.0.11.0/24"]
}
Exercise 3: Reusable module
Objective
Create a reusable EC2 module.
Structure
modules/
└── ec2-instance/
├── main.tf
├── variables.tf
└── outputs.tf
Module code
# modules/ec2-instance/variables.tf
variable "name" {
description = "Name of the instance"
type = string
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "subnet_id" {
description = "Subnet ID"
type = string
}
variable "security_group_ids" {
description = "List of security group IDs"
type = list(string)
default = []
}
variable "tags" {
description = "Additional tags"
type = map(string)
default = {}
}
# modules/ec2-instance/main.tf
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_instance" "this" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = var.subnet_id
vpc_security_group_ids = var.security_group_ids
tags = merge(var.tags, {
Name = var.name
})
}
# modules/ec2-instance/outputs.tf
output "id" {
description = "Instance ID"
value = aws_instance.this.id
}
output "private_ip" {
description = "Private IP"
value = aws_instance.this.private_ip
}
output "public_ip" {
description = "Public IP"
value = aws_instance.this.public_ip
}
Usage
# main.tf
module "web_server" {
source = "./modules/ec2-instance"
name = "web-server"
instance_type = "t3.small"
subnet_id = module.vpc.public_subnet_ids[0]
security_group_ids = [aws_security_group.web.id]
tags = {
Environment = "production"
Role = "webserver"
}
}
module "api_server" {
source = "./modules/ec2-instance"
name = "api-server"
instance_type = "t3.medium"
subnet_id = module.vpc.private_subnet_ids[0]
security_group_ids = [aws_security_group.api.id]
tags = {
Environment = "production"
Role = "api"
}
}
Project 1: 3-tier application
Architecture
Structure
project-3tier/
├── main.tf
├── variables.tf
├── outputs.tf
├── backend.tf
├── vpc.tf
├── security.tf
├── alb.tf
├── ec2.tf
├── rds.tf
└── terraform.tfvars
Main files
# main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.region
default_tags {
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
}
}
}
locals {
name_prefix = "${var.project_name}-${var.environment}"
}
# ec2.tf
resource "aws_launch_template" "app" {
name_prefix = "${local.name_prefix}-app-"
image_id = data.aws_ami.ubuntu.id
instance_type = var.app_instance_type
vpc_security_group_ids = [aws_security_group.app.id]
user_data = base64encode(<<-EOF
#!/bin/bash
apt-get update
apt-get install -y nginx
systemctl start nginx
EOF
)
tag_specifications {
resource_type = "instance"
tags = {
Name = "${local.name_prefix}-app"
}
}
}
resource "aws_autoscaling_group" "app" {
name = "${local.name_prefix}-app-asg"
desired_capacity = var.app_desired_count
max_size = var.app_max_count
min_size = var.app_min_count
vpc_zone_identifier = aws_subnet.private[*].id
target_group_arns = [aws_lb_target_group.app.arn]
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
tag {
key = "Name"
value = "${local.name_prefix}-app"
propagate_at_launch = true
}
}
# rds.tf
resource "aws_db_subnet_group" "main" {
name = "${local.name_prefix}-db-subnet"
subnet_ids = aws_subnet.private[*].id
}
resource "aws_db_instance" "main" {
identifier = "${local.name_prefix}-db"
engine = "postgres"
engine_version = "15"
instance_class = var.db_instance_class
allocated_storage = 20
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
skip_final_snapshot = true
tags = {
Name = "${local.name_prefix}-db"
}
}
Project 2: Multi-environment
Structure
terraform-multi-env/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── backend.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ ├── main.tf
│ │ ├── backend.tf
│ │ └── terraform.tfvars
│ └── production/
│ ├── main.tf
│ ├── backend.tf
│ └── terraform.tfvars
└── modules/
├── vpc/
├── compute/
└── database/
Configuration per environment
# environments/dev/terraform.tfvars
environment = "dev"
instance_type = "t3.micro"
instance_count = 1
db_instance_class = "db.t3.micro"
# environments/production/terraform.tfvars
environment = "production"
instance_type = "t3.large"
instance_count = 3
db_instance_class = "db.r6g.large"
Validation quiz
Questions
-
Which file stores the infrastructure state?
- a) main.tf
- b) terraform.tfstate
- c) variables.tf
- d) providers.tf
-
Which command previews the changes?
- a) terraform init
- b) terraform apply
- c) terraform plan
- d) terraform validate
-
How do you create several similar resources?
- a) Copy-paste the resource block
- b) Use count or for_each
- c) Create several files
- d) Use while loops
-
Where do you store the state in production?
- a) Locally
- b) In Git
- c) In a remote backend (S3, GCS)
- d) In the /tmp directory
-
How do you reuse Terraform code?
- a) With bash scripts
- b) With modules
- c) With templates
- d) With includes
Answers
- b) terraform.tfstate
- c) terraform plan
- b) Use count or for_each
- c) In a remote backend
- b) With modules
Additional resources
Documentation
Tools
Community
Conclusion
Congratulations! You have completed the Terraform course.
You now master:
- HCL syntax and providers
- Creating and managing resources
- Variables, outputs and state
- Reusable modules
- Production best practices
Keep practicing with real cloud projects!