Skip to main content
DevOps7 min readMarch 3, 2026

Infrastructure as Code: Why Your Config Should Live in Git

A practical guide to Infrastructure as Code with Terraform — versioning, modules, remote state, and why treating config as code is non-negotiable.

James Ross Jr.

James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

Infrastructure as Code: Why Your Config Should Live in Git

I have inherited enough manually managed infrastructure to have opinions about this. The tell-tale signs are always the same: a server nobody understands fully, a firewall rule added "temporarily" two years ago that everyone is afraid to touch, a database configuration that differs between staging and production in ways nobody can explain because it was set up by a developer who left eighteen months ago.

Manual infrastructure is technical debt that compounds. It accumulates undocumented decisions, implicit dependencies, and configuration drift until the day something breaks and nobody knows how to restore it. Infrastructure as Code (IaC) is the solution, and Terraform is the tool I reach for most often.

What Infrastructure as Code Actually Means

The premise is simple: your infrastructure configuration — servers, databases, DNS records, load balancers, security groups, storage buckets — is defined in files that live in a Git repository. You apply changes by running a command, not by clicking through a web console. History is preserved in commits. Changes go through code review. Rollback is a git revert.

This is not about following a trendy practice. It is about making your infrastructure reproducible. When your production server dies at 2am, reproducible infrastructure means you can recreate it in thirty minutes. Without IaC, recreating an environment means archaeology — reading through AWS console history, trying to remember what you configured six months ago, hoping the staging environment is close enough to serve as reference.

Why Terraform

Terraform is the dominant IaC tool for a reason. It supports virtually every cloud provider through a provider ecosystem, its HCL (HashiCorp Configuration Language) is readable, and the plan/apply workflow makes changes explicit before you execute them. You can see exactly what will change before it changes.

Alternatives exist. Pulumi lets you write infrastructure in TypeScript or Python if you prefer. AWS CloudFormation handles AWS-specific infrastructure natively. For Kubernetes specifically, Helm and Kustomize are better tools. But for general-purpose multi-cloud infrastructure, Terraform is the baseline.

A Real Terraform Module

Here is a simple Terraform configuration for a VPS setup on DigitalOcean — the kind of thing a small production deployment actually needs:

terraform {
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
  }

  backend "s3" {
    endpoint = "https://nyc3.digitaloceanspaces.com"
    bucket   = "my-terraform-state"
    key      = "production/terraform.tfstate"
    region   = "us-east-1"
    skip_credentials_validation = true
    skip_metadata_api_check     = true
  }
}

variable "do_token" {
  description = "DigitalOcean API token"
  type        = string
  sensitive   = true
}

variable "ssh_key_fingerprint" {
  description = "SSH key fingerprint for server access"
  type        = string
}

provider "digitalocean" {
  token = var.do_token
}

resource "digitalocean_droplet" "api_server" {
  name   = "api-production"
  size   = "s-2vcpu-4gb"
  image  = "ubuntu-22-04-x64"
  region = "nyc3"
  ssh_keys = [var.ssh_key_fingerprint]

  tags = ["production", "api"]
}

resource "digitalocean_firewall" "api" {
  name = "api-production-firewall"
  droplet_ids = [digitalocean_droplet.api_server.id]

  inbound_rule {
    protocol         = "tcp"
    port_range       = "22"
    source_addresses = ["your.office.ip/32"]
  }

  inbound_rule {
    protocol         = "tcp"
    port_range       = "443"
    source_addresses = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
    protocol              = "tcp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }
}

output "server_ip" {
  value = digitalocean_droplet.api_server.ipv4_address
}

This defines a server, a firewall, and an output. Run terraform plan to see what it will create. Run terraform apply to create it. Run terraform destroy to tear it all down. The entire configuration lives in git — version controlled, reviewable, and reproducible.

Remote State Is Non-Negotiable

Terraform tracks what it has created in a state file. By default, this file lives locally. This is fine for solo experimentation and a disaster for anything shared.

Local state means only one person can safely run Terraform at a time. If two team members run terraform apply simultaneously, you get state corruption. If the machine holding the state file dies, you lose the ability to manage your infrastructure through Terraform.

Use remote state from day one. The configuration above uses DigitalOcean Spaces (S3-compatible) as a backend. AWS S3 with DynamoDB locking is the more common pattern for AWS deployments:

backend "s3" {
  bucket         = "my-company-terraform-state"
  key            = "production/terraform.tfstate"
  region         = "us-east-1"
  dynamodb_table = "terraform-state-lock"
  encrypt        = true
}

The DynamoDB table provides state locking — only one operation can modify state at a time. The encrypt = true ensures state is encrypted at rest, which matters because state files contain sensitive data including resource IDs, IPs, and sometimes passwords.

Modules for Reusability

As your infrastructure grows, you will duplicate patterns. Every environment needs a similar server configuration. Every service needs similar security groups. Terraform modules let you extract these patterns into reusable components.

infrastructure/
  modules/
    web-server/
      main.tf
      variables.tf
      outputs.tf
  environments/
    production/
      main.tf
    staging/
      main.tf

Your environment-specific main.tf calls the module:

module "api_server" {
  source      = "../../modules/web-server"
  environment = "production"
  size        = "s-2vcpu-4gb"
  ssh_keys    = [var.ssh_key_fingerprint]
}

The same module, called with different variables, creates staging and production environments. When you need to update the server configuration, you update the module once and both environments stay in sync.

The Plan Review Workflow

The IaC equivalent of a pull request review is reviewing the Terraform plan before applying. Never run terraform apply in production without reviewing the plan output first. The plan shows exactly what will be created, modified, or destroyed.

In CI, run terraform plan on every pull request and post the plan output as a PR comment. Tools like Atlantis automate this workflow. Reviewers can see the infrastructure changes alongside the code changes. Infrastructure modifications require the same scrutiny as application code modifications.

# GitHub Actions step for plan output
- name: Terraform Plan
  run: terraform plan -out=tfplan
  env:
    TF_VAR_do_token: ${{ secrets.DO_TOKEN }}

- name: Comment Plan Output
  uses: actions/github-script@v7
  with:
    script: |
      const plan = require('fs').readFileSync('plan-output.txt', 'utf8');
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: '## Terraform Plan\n```\n' + plan + '\n```'
      });

Starting Small

You do not need to migrate all your infrastructure to Terraform overnight. Start with the next thing you would have clicked through a console to create. A new S3 bucket, a DNS record, a security group rule. Define it in Terraform, apply it, check it into git.

Incremental adoption is fine. Gradually expand your coverage. Import existing resources into Terraform state when it makes sense. The goal is that eventually, your production environment can be described completely in code and recreated from scratch with a single command.

The day you need that — and eventually, every production system needs that — you will be grateful you started early.


Want help setting up Terraform for your infrastructure? Let's design an IaC strategy that fits your stack. Book a session at https://calendly.com/jamesrossjr.


Keep Reading