Picture this: You're launching your startup's new application. You manually log into your cloud provider's dashboard, click through menus to create a server, set up a database, configure networking, add a load balancer, and adjust security settings. After an hour of clicking around, everything's finally working.
Three months later, you need to recreate the exact same setup for a staging environment. You try to remember everything you did, but you're not quite sure about some settings. You end up with something close but not identical, and now bugs appear in staging that don't show up in production.
Or worse—you accidentally delete something critical, and you're frantically trying to recreate it while your application is down and users are waiting.
Terraform solves all of this by letting you define your entire cloud infrastructure in code files. Instead of clicking through dashboards, you write exactly what you want, and Terraform creates it all for you—reliably, repeatably, and trackably.
The Pain Point: Managing Cloud Infrastructure is Complex and Fragile
Let's talk about what managing cloud infrastructure without Terraform actually feels like:
The Clicky-Click Nightmare: Cloud provider dashboards are powerful but overwhelming. You're clicking through endless menus, trying to remember which checkboxes you ticked and which dropdown options you selected. Miss one setting and things break mysteriously.
The "What Did I Create?" Problem: After a few months of adding resources here and there, you have dozens of servers, databases, networks, and other resources scattered across your cloud account. You're not entirely sure what's being used, what can be deleted, or how everything connects together.
The Drift Dilemma: Someone on your team manually tweaks a setting directly in the cloud console to fix an urgent issue. Now your production environment doesn't match what you thought it was, and you have no record of what changed or when.
The Duplication Headache: You need to create a development environment that matches production. Do you manually recreate everything and hope you got it right? Do you clone the production environment and pray you didn't accidentally keep some production settings?
The Dependency Dance: Your application server depends on a database. The database depends on a network. The network depends on security groups. Creating everything in the right order manually is tedious and error-prone.
The Disaster Recovery Panic: Your cloud account gets compromised, or you accidentally delete critical infrastructure. How do you recreate everything exactly as it was? Do you even know everything that existed?
The Cost Mystery: You're getting a huge cloud bill but can't easily see what resources are costing you money or which ones aren't being used anymore.
What Terraform Solves
Terraform is an infrastructure as code tool that lets you define all your cloud resources in configuration files, then creates and manages those resources for you. Instead of clicking through dashboards, you write what you want, and Terraform makes it happen.
Declarative Configuration: You describe what you want your infrastructure to look like, not how to create it. Terraform figures out the steps needed to make it happen.
Resource Graph: Terraform automatically understands dependencies between resources and creates them in the correct order. Need a database before the app server? Terraform handles that automatically.
State Management: Terraform keeps track of what exists in your cloud account, so it knows what to create, update, or delete when you make changes.
Plan Before Apply: Before making any changes, Terraform shows you exactly what will happen. No surprises, no accidental deletions.
Provider Agnostic: The same Terraform concepts work across AWS, Google Cloud, Azure, and hundreds of other services. Learn once, use everywhere.
Collaboration Ready: Your infrastructure configuration lives in Git alongside your application code. Changes are tracked, reviewed through pull requests, and can be rolled back if needed.
Reproducibility: Need to create an identical environment? Run the same Terraform configuration, and you get exactly the same infrastructure.
Real-World Example: Deploying Infrastructure for a Next.js App
Let's walk through a complete example: setting up the cloud infrastructure needed for your Next.js application with a domain from Namecheap and hosting on a cloud provider like DigitalOcean. We'll use Terraform to create everything: the server, networking, firewall rules, and more.
Step 1: Install Terraform
Install Terraform on your local machine:
# On Mac
brew install terraform
# On Ubuntu/Debian
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
# On Windows (use Chocolatey)
choco install terraformVerify installation:
terraform versionStep 2: Project Structure
Create your project structure:
my-infrastructure/
├── terraform/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── terraform.tfvars
└── .github/
└── workflows/
└── terraform.ymlStep 3: Configure Your Provider
Create terraform/main.tf:
# Configure Terraform
terraform {
required_version = ">= 1.0"
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
# Store state remotely (optional but recommended)
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
}
}
# Configure the DigitalOcean provider
provider "digitalocean" {
token = var.do_token
}
# Create an SSH key for server access
resource "digitalocean_ssh_key" "default" {
name = "${var.project_name}-ssh-key"
public_key = var.ssh_public_key
}
# Create a VPC for network isolation
resource "digitalocean_vpc" "main" {
name = "${var.project_name}-vpc"
region = var.region
description = "VPC for ${var.project_name}"
ip_range = "10.10.0.0/16"
}
# Create a firewall
resource "digitalocean_firewall" "web" {
name = "${var.project_name}-firewall"
# Apply to droplets with this tag
tags = [digitalocean_tag.web.id]
# Allow SSH
inbound_rule {
protocol = "tcp"
port_range = "22"
source_addresses = ["0.0.0.0/0", "::/0"]
}
# Allow HTTP
inbound_rule {
protocol = "tcp"
port_range = "80"
source_addresses = ["0.0.0.0/0", "::/0"]
}
# Allow HTTPS
inbound_rule {
protocol = "tcp"
port_range = "443"
source_addresses = ["0.0.0.0/0", "::/0"]
}
# Allow all outbound traffic
outbound_rule {
protocol = "tcp"
port_range = "1-65535"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
outbound_rule {
protocol = "udp"
port_range = "1-65535"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
}
# Create a tag for organizing resources
resource "digitalocean_tag" "web" {
name = "${var.project_name}-web"
}
# Create the main application server (droplet)
resource "digitalocean_droplet" "web" {
name = "${var.project_name}-web-01"
image = "ubuntu-22-04-x64"
size = var.droplet_size
region = var.region
vpc_uuid = digitalocean_vpc.main.id
ssh_keys = [digitalocean_ssh_key.default.id]
tags = [digitalocean_tag.web.id]
# User data script to run on first boot
user_data = templatefile("${path.module}/cloud-init.yml", {
hostname = var.domain_name
})
# Ensure firewall is created first
depends_on = [digitalocean_firewall.web]
}
# Create a floating IP for the droplet
resource "digitalocean_floating_ip" "web" {
region = var.region
}
# Assign the floating IP to the droplet
resource "digitalocean_floating_ip_assignment" "web" {
ip_address = digitalocean_floating_ip.web.ip_address
droplet_id = digitalocean_droplet.web.id
}
# Create a domain record (if managing DNS with DigitalOcean)
resource "digitalocean_domain" "main" {
name = var.domain_name
ip_address = digitalocean_floating_ip.web.ip_address
}
# Create A record for www subdomain
resource "digitalocean_record" "www" {
domain = digitalocean_domain.main.name
type = "A"
name = "www"
value = digitalocean_floating_ip.web.ip_address
ttl = 300
}
# Create a managed database (optional)
resource "digitalocean_database_cluster" "postgres" {
count = var.enable_database ? 1 : 0
name = "${var.project_name}-db"
engine = "pg"
version = "15"
size = var.database_size
region = var.region
node_count = 1
tags = [digitalocean_tag.web.id]
}
# Allow droplet to access database
resource "digitalocean_database_firewall" "postgres_firewall" {
count = var.enable_database ? 1 : 0
cluster_id = digitalocean_database_cluster.postgres[0].id
rule {
type = "droplet"
value = digitalocean_droplet.web.id
}
}
# Create a load balancer (for production scaling)
resource "digitalocean_loadbalancer" "web" {
count = var.enable_load_balancer ? 1 : 0
name = "${var.project_name}-lb"
region = var.region
vpc_uuid = digitalocean_vpc.main.id
forwarding_rule {
entry_protocol = "https"
entry_port = 443
target_protocol = "http"
target_port = 80
certificate_name = var.ssl_certificate_name
}
forwarding_rule {
entry_protocol = "http"
entry_port = 80
target_protocol = "http"
target_port = 80
}
healthcheck {
port = 80
protocol = "http"
path = "/"
}
droplet_tag = digitalocean_tag.web.name
}
# Create monitoring alerts
resource "digitalocean_monitor_alert" "cpu_alert" {
alerts {
email = [var.alert_email]
}
window = "5m"
type = "v1/insights/droplet/cpu"
compare = "GreaterThan"
value = 80
enabled = true
entities = [digitalocean_droplet.web.id]
description = "Alert when CPU usage exceeds 80%"
}
resource "digitalocean_monitor_alert" "memory_alert" {
alerts {
email = [var.alert_email]
}
window = "5m"
type = "v1/insights/droplet/memory_utilization_percent"
compare = "GreaterThan"
value = 90
enabled = true
entities = [digitalocean_droplet.web.id]
description = "Alert when memory usage exceeds 90%"
}Step 4: Define Variables
Create terraform/variables.tf:
variable "do_token" {
description = "DigitalOcean API token"
type = string
sensitive = true
}
variable "project_name" {
description = "Name of the project"
type = string
default = "nextjs-app"
}
variable "region" {
description = "DigitalOcean region"
type = string
default = "nyc3"
}
variable "droplet_size" {
description = "Size of the droplet"
type = string
default = "s-2vcpu-4gb"
}
variable "domain_name" {
description = "Domain name for the application"
type = string
}
variable "ssh_public_key" {
description = "SSH public key for server access"
type = string
}
variable "enable_database" {
description = "Enable managed PostgreSQL database"
type = bool
default = false
}
variable "database_size" {
description = "Size of the database cluster"
type = string
default = "db-s-1vcpu-1gb"
}
variable "enable_load_balancer" {
description = "Enable load balancer"
type = bool
default = false
}
variable "ssl_certificate_name" {
description = "Name of SSL certificate (if using load balancer)"
type = string
default = ""
}
variable "alert_email" {
description = "Email for monitoring alerts"
type = string
}Step 5: Define Outputs
Create terraform/outputs.tf:
output "droplet_ip" {
description = "IP address of the main droplet"
value = digitalocean_droplet.web.ipv4_address
}
output "floating_ip" {
description = "Floating IP address"
value = digitalocean_floating_ip.web.ip_address
}
output "domain" {
description = "Domain name"
value = digitalocean_domain.main.name
}
output "database_uri" {
description = "Database connection URI"
value = var.enable_database ? digitalocean_database_cluster.postgres[0].uri : "N/A"
sensitive = true
}
output "load_balancer_ip" {
description = "Load balancer IP address"
value = var.enable_load_balancer ? digitalocean_loadbalancer.web[0].ip : "N/A"
}
output "ssh_command" {
description = "SSH command to connect to the droplet"
value = "ssh root@${digitalocean_floating_ip.web.ip_address}"
}Step 6: Create cloud-init Configuration
Create terraform/cloud-init.yml:
#cloud-config
# Set hostname
hostname: ${hostname}
# Update packages
package_update: true
package_upgrade: true
# Install required packages
packages:
- curl
- git
- nginx
- ufw
# Configure firewall
runcmd:
- ufw allow OpenSSH
- ufw allow 'Nginx Full'
- ufw --force enable
- systemctl enable nginx
- systemctl start nginx
# Create a basic Nginx configuration
write_files:
- path: /etc/nginx/sites-available/default
content: |
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
index index.html;
server_name _;
location / {
try_files $uri $uri/ =404;
}
}Step 7: Set Your Variables
Create terraform/terraform.tfvars:
do_token = "your-digitalocean-api-token"
project_name = "my-nextjs-app"
region = "nyc3"
droplet_size = "s-2vcpu-4gb"
domain_name = "yourdomain.com"
ssh_public_key = "ssh-rsa AAAAB3Nza... your-public-key"
alert_email = "your-email@example.com"
# Optional features
enable_database = true
database_size = "db-s-1vcpu-1gb"
enable_load_balancer = falseImportant: Never commit terraform.tfvars to Git if it contains secrets. Add it to .gitignore.
Step 8: Deploy Your Infrastructure
Initialize Terraform:
cd terraform
terraform initSee what Terraform will create:
terraform planThis shows you every resource that will be created, modified, or deleted. Review it carefully!
Apply the configuration:
terraform applyTerraform will show you the plan again and ask for confirmation. Type yes to proceed.
In a few minutes, Terraform creates:
- A VPC for network isolation
- An SSH key for secure access
- A droplet (server) with Ubuntu
- A firewall with proper security rules
- A floating IP address
- DNS records for your domain
- Optional: A managed PostgreSQL database
- Optional: A load balancer with SSL
- Monitoring alerts
Step 9: Managing Your Infrastructure
View Current State:
terraform showSee Output Values:
terraform output
terraform output -jsonUpdate Infrastructure:
Edit your .tf files, then:
terraform plan # See what will change
terraform apply # Apply the changesDestroy Everything (when needed):
terraform destroyThis cleanly removes all resources Terraform created.
Step 10: GitHub Actions for Infrastructure Changes
Create .github/workflows/terraform.yml:
name: Terraform Infrastructure
on:
push:
branches: [ main ]
paths:
- 'terraform/**'
pull_request:
branches: [ main ]
paths:
- 'terraform/**'
env:
TF_VERSION: '1.6.0'
jobs:
terraform:
name: Terraform Plan & Apply
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./terraform
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format Check
run: terraform fmt -check
continue-on-error: true
- name: Terraform Init
run: terraform init
env:
DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan -no-color
env:
DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
TF_VAR_do_token: ${{ secrets.DIGITALOCEAN_TOKEN }}
TF_VAR_domain_name: ${{ secrets.DOMAIN_NAME }}
TF_VAR_ssh_public_key: ${{ secrets.SSH_PUBLIC_KEY }}
TF_VAR_alert_email: ${{ secrets.ALERT_EMAIL }}
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve
env:
DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
TF_VAR_do_token: ${{ secrets.DIGITALOCEAN_TOKEN }}
TF_VAR_domain_name: ${{ secrets.DOMAIN_NAME }}
TF_VAR_ssh_public_key: ${{ secrets.SSH_PUBLIC_KEY }}
TF_VAR_alert_email: ${{ secrets.ALERT_EMAIL }}Add these secrets to your GitHub repository:
DIGITALOCEAN_TOKEN: Your DigitalOcean API tokenDOMAIN_NAME: Your domain nameSSH_PUBLIC_KEY: Your public SSH keyALERT_EMAIL: Email for alerts
Now, infrastructure changes go through pull requests. The workflow:
- Open a PR with infrastructure changes
- GitHub Actions runs
terraform planand shows what will change - Team reviews the plan
- Merge to main
- GitHub Actions automatically applies the changes
Step 11: Connecting Terraform and Ansible
Terraform creates the infrastructure, but you still need to configure the software. Use Terraform outputs to automatically generate an Ansible inventory:
Create a script generate-inventory.sh:
#!/bin/bash
# Get the droplet IP from Terraform
DROPLET_IP=$(cd terraform && terraform output -raw floating_ip)
# Generate Ansible inventory
cat > ansible/inventory.ini <<EOF
[webservers]
production ansible_host=${DROPLET_IP} ansible_user=root
[webservers:vars]
ansible_python_interpreter=/usr/bin/python3
EOF
echo "Inventory generated with IP: ${DROPLET_IP}"Make it executable and run it:
chmod +x generate-inventory.sh
./generate-inventory.shNow you can run Ansible to configure the server that Terraform created:
ansible-playbook -i ansible/inventory.ini ansible/playbook.ymlWhy This Matters
With Terraform managing your infrastructure:
Complete Visibility: Every resource is defined in code. You know exactly what exists, why it exists, and how it's configured. No more mystery resources consuming your budget.
Risk-Free Changes: The terraform plan command shows you exactly what will happen before you commit. No surprises, no accidental deletions.
Instant Replication: Need another environment? Change a few variables and run terraform apply. You get an identical copy of your infrastructure in minutes.
Disaster Recovery: If your infrastructure is destroyed, you can recreate it exactly by running your Terraform configuration again. The code is the backup.
Team Collaboration: Infrastructure changes go through pull requests and code review, just like application code. Everyone can see what's changing and why.
Cost Control: When you're done with an environment, terraform destroy removes everything cleanly. No orphaned resources billing you forever.
Multi-Cloud Strategy: The same Terraform skills work across AWS, GCP, Azure, and others. You're not locked into one provider's way of doing things.
Compliance and Auditing: Every infrastructure change is tracked in Git. You have a complete audit trail of who changed what and when.
Terraform vs Ansible: When to Use Each
You might be wondering: when should I use Terraform versus Ansible?
Use Terraform for:
- Creating cloud infrastructure (servers, networks, databases, load balancers)
- Managing DNS records
- Setting up cloud storage buckets
- Configuring cloud provider services
- Anything that's about provisioning resources
Use Ansible for:
- Installing software on servers
- Configuring applications
- Managing files and permissions
- Running commands on servers
- Deploying applications
- Anything that's about configuration management
Use them together:
- Terraform creates the infrastructure
- Ansible configures the software on that infrastructure
- GitHub Actions orchestrates both for complete automation
This is exactly what we did in our example: Terraform created the server, networking, and infrastructure, then Ansible configured Node.js, Nginx, PM2, and deployed the application.
Conclusion
Terraform transforms cloud infrastructure from something you click together manually into something you define as code, version, review, and deploy reliably. Every resource is tracked, every change is visible, and everything can be recreated at will.
The learning curve exists, but the benefits are immediate. Start with a simple setup—maybe just a single server and firewall—and gradually expand as you get comfortable. Your first terraform apply that creates your entire infrastructure in minutes will make all the learning worthwhile.
Combined with Ansible for configuration management and GitHub Actions for automation, you have a complete DevOps pipeline that's reliable, transparent, and collaborative. Welcome to infrastructure that's actually manageable.