What Is Infrastructure as Code?
Infrastructure as Code (IaC) means managing servers, networks, and cloud services through configuration files instead of manual console clicks. Terraform, created by HashiCorp, is the most widely adopted IaC tool because it works across every major cloud provider: AWS, Azure, GCP, and dozens of others using the same language and workflow.
Terraform uses a declarative language called HCL (HashiCorp Configuration Language). You describe the desired end state of your infrastructure, and Terraform figures out the steps to get there. It tracks the current state in a state file, computes the difference between desired and actual, and applies only the necessary changes. This guide will take you from installing Terraform to deploying a complete VPC with EC2 instances on AWS.
Installation and Setup
Install Terraform and configure AWS credentials before writing any code.
# Install Terraform (Linux/macOS)
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
# Verify installation
terraform version
# Configure AWS CLI (Terraform uses these credentials)
aws configure
# Enter: AWS Access Key ID, Secret Key, Region (ap-south-1 for Mumbai)
Project Structure
Organize your Terraform files by concern. Here’s the structure we’ll build:
infra/
main.tf # Provider and backend configuration
variables.tf # Input variable declarations
vpc.tf # VPC, subnets, route tables
security.tf # Security groups
compute.tf # EC2 instances
outputs.tf # Output values
terraform.tfvars # Variable values (DO NOT commit secrets)
Provider and Backend Configuration
Start with main.tf. This defines which cloud provider to use and where to store state:
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# Remote state storage (recommended for teams)
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "prod/infrastructure.tfstate"
region = "ap-south-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project_name
}
}
}
Variables and Configuration
Define all configurable values in variables.tf:
variable "aws_region" {
description = "AWS region for resources"
type = string
default = "ap-south-1"
}
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "project_name" {
description = "Project name used for resource naming"
type = string
default = "byteyogi"
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "key_pair_name" {
description = "Name of the SSH key pair"
type = string
}
Set values in terraform.tfvars:
aws_region = "ap-south-1"
environment = "dev"
project_name = "byteyogi"
key_pair_name = "my-ssh-key"
instance_type = "t3.micro"
Building the VPC
A VPC is the foundation of your AWS network. Save this as vpc.tf:
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-${var.environment}-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-${var.environment}-igw"
}
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-${count.index + 1}"
}
}
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.project_name}-private-${count.index + 1}"
}
}
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_name}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
Security Groups and EC2 Instances
Define firewall rules in security.tf and launch an instance in compute.tf:
# security.tf
resource "aws_security_group" "web" {
name = "${var.project_name}-web-sg"
description = "Allow HTTP, HTTPS, and SSH"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["YOUR_IP/32"] # Replace with your IP
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# compute.tf
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-*-24.04-amd64-server-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
key_name = var.key_pair_name
subnet_id = aws_subnet.public[0].id
vpc_security_group_ids = [aws_security_group.web.id]
root_block_device {
volume_size = 20
volume_type = "gp3"
encrypted = true
}
user_data = <<-EOF
#!/bin/bash
apt-get update -y
apt-get install -y nginx
systemctl start nginx
systemctl enable nginx
EOF
tags = {
Name = "${var.project_name}-web-server"
}
}
Outputs and the Terraform Workflow
Define useful outputs in outputs.tf, then run the standard Terraform workflow:
# outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
output "web_server_public_ip" {
value = aws_instance.web.public_ip
}
output "web_server_public_dns" {
value = aws_instance.web.public_dns
}
# Initialize Terraform (downloads providers, sets up backend)
terraform init
# Format code consistently
terraform fmt -recursive
# Validate syntax
terraform validate
# Preview changes (ALWAYS do this before apply)
terraform plan -out=tfplan
# Apply the plan
terraform apply tfplan
# View current state
terraform state list
# Destroy everything when done
terraform destroy
Conclusion
You’ve built a complete AWS infrastructure with Terraform: VPC with public and private subnets, an internet gateway, security groups, and an EC2 instance running Nginx. Every resource is version-controlled, reproducible, and can be torn down with a single command. The key habits to build are: always run plan before apply, never modify infrastructure manually, use remote state with locking for team collaboration, and organize code into focused files. From here, explore Terraform modules for reusable components, workspaces for managing multiple environments, and the terraform import command for adopting existing infrastructure.