Infrastructure as Code in Practice: Terraform, Ansible, and GitOps for Delivery Pipelines

Introduction

When I first started managing my Proxmox homelab alongside production Node.js services, I quickly realized that clicking through dashboards to provision VMs was a recipe for inconsistency — what worked on Tuesday’s fresh Ubuntu install had no guarantee of matching Wednesday’s. That frustration is exactly what makes Infrastructure as Code (IaC) such a fundamental shift in how serious developers and operations teams work.

IaC is not merely about writing scripts to automate a task; it is about defining the desired state of your entire ecosystem in a declarative or imperative language. This approach allows teams to version control their environments, perform peer reviews on infrastructure changes, and achieve a level of consistency that eliminates the dreaded “it works on my machine” syndrome. In this post, we will explore the practical applications of IaC, moving past the theory to examine how tools like Terraform and Ansible integrate into a high-performing software delivery lifecycle.

The Shift from Snowflake Servers to Immutable Infrastructure

One of the most significant practical benefits of IaC is the elimination of “Snowflake Servers” — unique configurations that have been manually tweaked over time until no one truly knows how they work. When infrastructure is defined as code, every change is documented in a repository. This transition supports the concept of Immutable Infrastructure, where instead of patching an existing server, you replace it entirely with a new one generated from your updated code. The old server is not upgraded; it is discarded and rebuilt from a known-good definition.

Key Benefits of the IaC Approach

  • Idempotency: The ability to run the same code multiple times and achieve the same result without changing the state after the first application. This is what makes automated pipelines safe to re-run on failure.
  • Version Control: Using Git to track infrastructure changes allows for easy rollbacks and a clear audit trail of who changed what and when. A diff on a Terraform file is as meaningful as a diff on a TypeScript module.
  • Speed and Safety: Automated provisioning reduces the lead time for new environments from weeks to minutes, while automated validation of the code catches errors before they reach production.

Practical Application: Building a Multi-Tier Environment

To understand the power of IaC, consider a real-world scenario: a team needs to deploy a microservices architecture consisting of a load balancer, an auto-scaling group of compute instances, and a managed database. Doing this through a cloud console — sometimes called “ClickOps” — is prone to human error and nearly impossible to replicate exactly across staging and production environments.

Step 1: Declarative Provisioning with Terraform

Using Terraform, the team defines the Virtual Private Cloud and its subnets as code. Because Terraform is provider-agnostic, the same logic can be applied across AWS, Azure, or GCP. The code becomes the single source of truth. When a developer needs a new environment, they run a plan, and Terraform calculates the exact difference between the current live state and the desired state before touching anything.

Here is a minimal but practical Terraform snippet that provisions a VPC and a public subnet on AWS — the kind of foundation you would build a Node.js application server on top of:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name        = "main-vpc"
    Environment = var.environment
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true
  availability_zone       = "${var.aws_region}a"

  tags = {
    Name = "public-subnet"
  }
}

variable "aws_region" {
  default = "eu-west-1"
}

variable "environment" {
  description = "Deployment environment (staging, production)"
  type        = string
}

Notice that the VPC and subnet are not described procedurally — you are not telling Terraform how to create them, only what they should look like. Terraform resolves the how. This declarative style is what makes the code readable by anyone on the team, not just the person who wrote it.

Step 2: Configuration Management with Ansible

While Terraform excels at provisioning the physical or cloud resources themselves, configuration management tools like Ansible handle what happens once those resources are alive. Once a server is up, Ansible connects over SSH to install specific software packages, apply security hardening, and deploy application binaries. In practice, this means Terraform creates the EC2 instance, and an Ansible playbook immediately configures it to run your Node.js application under PM2, sets up log rotation, and locks down the SSH configuration — all without a human touching the machine.

This combination ensures that infrastructure is not just present, but functional and secure from the moment it boots. The separation of concerns matters here: Terraform owns the resource lifecycle, Ansible owns the software state inside it.

Integrating IaC into the CI/CD Pipeline

The real value emerges when IaC is wired into your Continuous Integration and Continuous Delivery (CI/CD) pipeline. In a mature setup, an infrastructure change follows the same review and deployment path as an application feature. A typical pipeline stage order looks like this:

  1. Linting and Validation: The pipeline checks IaC files for syntax errors and adherence to naming conventions using tools like terraform validate or tflint.
  2. Security Scanning: Tools like Checkov or Terrascan analyze the code for vulnerabilities — open S3 buckets, overly permissive security groups — before a single resource is created. Catching this in CI is orders of magnitude cheaper than finding it in production.
  3. Plan and Preview: The pipeline generates a plan output showing exactly what will be added, changed, or destroyed. This output is attached to the pull request so senior engineers can review it the same way they would review application code.
  4. Automated Deployment: Upon approval, the pipeline executes the code, provisioning the infrastructure in a sandbox first for integration testing before promoting to production.

Automating these steps means security policies are enforced programmatically on every change — a practice often referred to as DevSecOps. It also means a junior developer making an infrastructure change gets the same guardrails as a senior one.

The Role of GitOps in IaC

A significant evolution in IaC practice is GitOps, where the Git repository becomes the single source of truth for infrastructure state. When a pull request is merged, a controller like ArgoCD or Flux automatically reconciles the live environment with the declared state in Git. Every infrastructure change is now a reviewed, auditable code change — no more undocumented console clicks making it into production.

Overcoming Common Implementation Challenges

The benefits are substantial, but implementing IaC professionally means confronting a few non-obvious problems early.

Managing State Files

Terraform maintains a state file that maps your code to real-world resources. In a team environment, this file is critical infrastructure in its own right. If two engineers run Terraform simultaneously without a locked state file, the resulting conflicts can corrupt your infrastructure state in ways that are painful to untangle. Using a Remote State backend — such as AWS S3 with a DynamoDB table for locking — is not optional in a team context. It is the baseline for safe collaborative IaC.

Handling Secrets Safely

One of the most common and damaging mistakes in IaC is accidentally committing sensitive data — database passwords, API keys, MongoDB connection strings — directly into version control. Your IaC should never contain a hardcoded secret. Instead, reference values stored in a dedicated Secrets Management solution such as HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault. In a Node.js context, this pairs naturally with reading environment variables at runtime, so neither your Terraform code nor your Express application ever has a plaintext credential in the repository.

The Drift Problem

Configuration drift occurs when someone makes a manual change to live infrastructure — tweaking a security group rule in the AWS console, for instance — that is never reflected back in the code. Over time, your code no longer describes what actually exists, and the next Terraform apply can produce surprising results. High-maturity teams run automated drift detection on a schedule, alerting the team whenever the live environment diverges from the defined state. The fix is usually either updating the code to match reality or reverting the manual change, but finding the drift quickly is what keeps it from becoming a crisis.

Conclusion: The Path to Infrastructure Maturity

Infrastructure as Code is more than a technical choice; it is a cultural shift that allows developers and operations teams to collaborate on the same artifact using the same tools they already use for application development. By treating infrastructure as a first-class citizen in the software development lifecycle, teams achieve consistency, auditability, and speed that manual processes simply cannot match — whether you are managing a homelab with a handful of Proxmox VMs or a distributed system running dozens of Docker containers across multiple cloud regions.

The practical path forward is incremental: codify one resource, commit it to Git, wire it into a basic pipeline, and iterate. The goal is not to rewrite everything overnight, but to ensure that every new resource you touch is defined in code before it is touched in production. Start with what hurts most — the environment that is hardest to recreate, the server configuration nobody remembers — and work outward from there.

Related Posts

3 thoughts on “Infrastructure as Code in Practice: Terraform, Ansible, and GitOps for Delivery Pipelines

Leave a Reply

Your email address will not be published. Required fields are marked *