Terraform: Managing Cloud Infrastructure as Code

Terraform: Managing Cloud Infrastructure as Code

Kite Eugine

Kite Eugine • Nov 29, 2025

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 terraform

Verify installation:

terraform version

Step 2: Project Structure

Create your project structure:

my-infrastructure/
├── terraform/
│   ├── main.tf
│   ├── variables.tf
│   ├── outputs.tf
│   └── terraform.tfvars
└── .github/
    └── workflows/
        └── terraform.yml

Step 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 = false

Important: Never commit terraform.tfvars to Git if it contains secrets. Add it to .gitignore.

Step 8: Deploy Your Infrastructure

Initialize Terraform:

cd terraform
terraform init

See what Terraform will create:

terraform plan

This shows you every resource that will be created, modified, or deleted. Review it carefully!

Apply the configuration:

terraform apply

Terraform 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 show

See Output Values:

terraform output
terraform output -json

Update Infrastructure:

Edit your .tf files, then:

terraform plan   # See what will change
terraform apply  # Apply the changes

Destroy Everything (when needed):

terraform destroy

This 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 token
  • DOMAIN_NAME: Your domain name
  • SSH_PUBLIC_KEY: Your public SSH key
  • ALERT_EMAIL: Email for alerts

Now, infrastructure changes go through pull requests. The workflow:

  1. Open a PR with infrastructure changes
  2. GitHub Actions runs terraform plan and shows what will change
  3. Team reviews the plan
  4. Merge to main
  5. 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.sh

Now you can run Ansible to configure the server that Terraform created:

ansible-playbook -i ansible/inventory.ini ansible/playbook.yml

Why 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.

Comments (0)

No comments yet. Be the first to comment!

Related Posts