Infrastructure as Code Explained: Tools, Benefits, GitOps

Somebody builds a server by clicking through a cloud console. It works. Six months later nobody remembers which checkboxes they ticked, the staging copy behaves differently, and the person who built it left the company. That’s the problem infrastructure as code (IaC) exists to fix.

This guide covers the whole picture without turning into a textbook: what IaC is, declarative vs imperative, provisioning vs configuration, mutable vs immutable servers, the infrastructure lifecycle, the real limits, how Terraform, OpenTofu, Ansible, CloudFormation, Bicep and Pulumi compare, and where GitOps fits. The demos ran on Ubuntu 24.04 and RHEL 9.6 using local files as the “infrastructure”, so you can repeat them anywhere without a cloud bill.

What is infrastructure as code and why use it?

Infrastructure as code means you describe servers, networks, databases and permissions in text files, keep those files in version control, and let a tool build the real thing from them. The files become the source of truth. The console becomes a place you look, not a place you change things.

Why bother? Four reasons come up again and again:

  • Repeatable. The same files build dev, staging and production, so they stop drifting apart.
  • Reviewable. A change is a pull request someone can read, not a click nobody saw.
  • Recoverable. Lost a region or a server? Rebuild from the repo.
  • Documented. The code is the documentation, and it can’t go stale because it’s what actually runs.

Declarative vs imperative infrastructure, with real examples

An imperative script lists steps: do this, then this. A declarative definition lists the end result and lets the tool work out the steps. Here’s the imperative version, a tiny script that creates a folder and adds a config line:

#!/usr/bin/env bash
mkdir ~/iac/demo/imperative
echo "port=8080" >> ~/iac/demo/imperative/app.conf

Run it twice, like a real pipeline would after a retry:

bash imp.sh
bash imp.sh
mkdir: /home/asif/iac/demo/imperative: File exists
cat ~/iac/demo/imperative/app.conf
port=8080
port=8080

The second run complained about the folder and quietly added a duplicate line. The script describes actions, so running it again repeats the actions. Now the declarative version, in Terraform:

resource "local_file" "app_conf" {
  filename = "${path.module}/app.conf"
  content  = "port=8080\nworkers=2\n"
}
terraform apply -auto-approve
Plan: 1 to add, 0 to change, 0 to destroy.
local_file.app_conf: Creating...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

terraform apply -auto-approve
local_file.app_conf: Refreshing state...
No changes. Your infrastructure matches the configuration.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Same command twice, no mess. That property is called idempotency, and it’s the real reason declarative tools took over. I got the identical result on the RHEL 9.6 box. Terraform also compares reality against the file, so if someone edits the file by hand, the next plan notices and puts it back. That’s drift detection, and you get it for free.

Infrastructure automation vs configuration management

People mix these up constantly. Provisioning tools (Terraform, CloudFormation, Bicep) create the things: VMs, networks, load balancers. Configuration management tools (Ansible, Chef, Puppet) set up what’s inside those things: packages, users, config files. Automation is the umbrella term that covers both.

Ansible is a good example of the second job. This playbook makes sure a folder exists and a line is in a file:

- hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: App directory exists
      ansible.builtin.file:
        path: ~/iac/demo/ansible
        state: directory
        mode: "0755"
    - name: Port is set
      ansible.builtin.lineinfile:
        path: ~/iac/demo/ansible/app.conf
        line: "port=8080"
        create: true
ansible-playbook -i localhost, site.yml
TASK [App directory exists] ***
changed: [localhost]
TASK [Port is set] ***
changed: [localhost]
PLAY RECAP ***
localhost : ok=2    changed=2    unreachable=0    failed=0

ansible-playbook -i localhost, site.yml
TASK [App directory exists] ***
ok: [localhost]
TASK [Port is set] ***
ok: [localhost]
PLAY RECAP ***
localhost : ok=2    changed=0    unreachable=0    failed=0

(I trimmed one Python interpreter warning from that output.) Look at the second run: changed=0. Ansible is idempotent too, as long as you use proper modules like file and lineinfile instead of raw shell commands. Most teams use both kinds of tool: Terraform builds the server, Ansible configures it. Our Ansible directory structure guide shows how to lay that out.

Manual cloud provisioning vs versioned infrastructure

Clicking in a console is fast the first time and painful every time after. There’s no history, no review, no easy undo. Versioned infrastructure flips it. Say we want four workers instead of two. We edit one line, and Git shows exactly what changed:

git diff main.tf
-  content  = "port=8080\nworkers=2\n"
+  content  = "port=8080\nworkers=4\n"

Then the tool tells us what it will do before doing it:

terraform plan
  # local_file.app_conf must be replaced
-/+ resource "local_file" "app_conf" {
      ~ content = <<-EOT # forces replacement
            port=8080
          - workers=2
          + workers=4
        EOT
    }
Plan: 1 to add, 0 to change, 1 to destroy.

Read that plan before you apply. “Must be replaced” on a database would be a bad afternoon. Once it’s applied, git log --oneline gives you the audit trail (app.conf: 4 workers on top of app.conf: 2 workers). If you’re new to Git itself, start with Mastering Git.

Mutable vs immutable infrastructure for Linux teams

Mutable servers get changed in place: SSH in, apt upgrade or dnf update, edit a config, restart. It’s familiar and quick. It’s also how two “identical” servers end up with different package versions after a year.

Immutable servers are never patched. You build a new image with the change baked in (Packer, container builds, a fresh cloud image), launch replacements, and delete the old ones. Rollback is just launching the previous image.

Mutable Immutable
Update method Patch the running server Replace it with a new image
Drift risk High over time Low, servers are identical
Rollback Undo changes, hope it works Redeploy the old image
Best for Pets: databases, legacy apps Cattle: web tiers, containers, autoscaling groups

Most Linux teams run a mix. Stateless web servers go immutable, the database server stays mutable and gets careful, scripted changes. Don’t force a purity you don’t need.

The infrastructure lifecycle: request to retirement

IaC changes how each stage of an infrastructure’s life looks:

  1. Request. Someone opens a ticket or pull request describing what’s needed.
  2. Define. The change is written as code, usually a few lines.
  3. Review. Teammates read the diff and the plan output.
  4. Provision. The tool builds it (apply).
  5. Configure. A configuration tool sets up packages and services if needed.
  6. Operate. Monitoring runs, and changes go through the same loop. Drift gets corrected.
  7. Retire. Remove the code, run destroy, and nothing gets left behind.

That last step is the one manual setups forget, and it’s where orphaned servers and surprise bills come from:

terraform destroy -auto-approve
local_file.app_conf: Destroying...
Destroy complete! Resources: 1 destroyed.

ls app.conf
ls: cannot access 'app.conf': No such file or directory

The tool tracked what it created, so it knows exactly what to remove.

Benefits and limits of infrastructure as code

The benefits are the ones above: repeatability, review, speed, recovery. Here’s the part vendor pages skip, the limits:

  • State is a real thing to manage. Terraform and OpenTofu keep a state file. Lose it or let two people write it at once and you’re in trouble. Use remote state with locking.
  • Drift still happens. Someone with console access will click something at 2 a.m. IaC finds it, but it doesn’t stop it unless you also lock down permissions.
  • Secrets leak easily. State files and code can contain passwords. Keep secrets in a vault and keep state private.
  • The learning curve is real. You need the tool, Git, and the cloud’s own concepts, all at once.
  • A bad change scales fast. One wrong line can replace a hundred servers. That’s why the plan step and code review matter.
  • Not everything fits. One-off experiments and tiny setups are sometimes faster by hand.

Terraform vs Ansible vs CloudFormation vs Bicep vs Pulumi vs OpenTofu

Tool Main job Language Where it works Licence and notes
Terraform Provisioning HCL Any cloud, thousands of providers Business Source License since 2023. Largest ecosystem.
OpenTofu Provisioning HCL Any cloud, same providers Open source (MPL 2.0), community fork under the Linux Foundation. Built-in state encryption.
Ansible Configuration, some provisioning YAML Servers over SSH, plus cloud modules Open source, agentless.
CloudFormation Provisioning YAML or JSON AWS only Built into AWS, state kept by AWS as stacks.
Bicep Provisioning Bicep DSL Azure only Open source, compiles to ARM templates.
Pulumi Provisioning Python, TypeScript, Go, C#, Java Any cloud Open source (Apache 2.0). Real programming languages, state in Pulumi Cloud or your own bucket.

How to pick: on one cloud only, the native tool (CloudFormation or Bicep) is the shortest path. Multi-cloud or lots of existing modules, Terraform or OpenTofu. Developers who’d rather write a loop in Python than learn HCL, Pulumi. Servers to configure, add Ansible. As for Terraform vs OpenTofu, here’s the nice part: the same file ran on both.

tofu apply -auto-approve
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
tofu plan
No changes. Your infrastructure matches the configuration.

That was the unchanged main.tf from earlier, run with OpenTofu 1.13.1 next to Terraform 1.16.5. For everyday work the two feel nearly identical, so the choice is mostly about licensing and which ecosystem you want to be part of. We cover the install and workflow in our Terraform guide, and AWS VPC Explained is a good first thing to try building with it.

How GitOps applies to cloud infrastructure

GitOps takes the declarative idea one step further. Git holds the desired state, and an automated process continuously compares it with reality and fixes any difference. Nobody runs apply from a laptop. You merge a pull request and the system catches up. In Kubernetes this is Argo CD or Flux. For cloud infrastructure it’s usually a pipeline running Terraform or OpenTofu on every merge (the self-hosted GitHub Actions runner guide covers one way to run that).

The loop itself is simple enough to show in a few lines of shell. This tiny reconciler pulls the repo, compares the replica count in Git with what’s “running”, and fixes any difference:

#!/usr/bin/env bash
cd ~/iac/demo/gitops-clone || exit 1
git pull -q origin main
want=$(grep '^replicas=' desired.conf | cut -d= -f2)
have=$(cat ~/iac/demo/running-replicas 2>/dev/null || echo 0)
if [[ $want != "$have" ]]; then
  echo "drift: running=$have, repo says $want -> applying"
  echo "$want" > ~/iac/demo/running-replicas
else
  echo "in sync: $have replicas"
fi
bash reconcile.sh
drift: running=0, repo says 2 -> applying
bash reconcile.sh
in sync: 2 replicas

git commit -am "scale to 3 replicas" && git push
bash reconcile.sh
drift: running=2, repo says 3 -> applying
bash reconcile.sh
in sync: 3 replicas

That’s the whole idea. Change Git, and the environment follows. Real GitOps tools add health checks, rollbacks and access control around the same loop. The big win is that Git becomes your audit log and your undo button: git revert and the infrastructure rolls back too.

Frequently asked questions

Is Ansible infrastructure as code?

Yes. Its playbooks are code in version control, and it’s idempotent when you use proper modules. It just leans toward configuring servers rather than creating cloud resources.

Should I learn Terraform or OpenTofu first?

Either. They share the same language and most commands, and what you learn transfers directly. If licensing matters to your employer, check that before choosing.

What does idempotent mean?

Running the same thing many times gives the same result as running it once. A second terraform apply that reports “No changes” is the classic example.

Is Kubernetes YAML infrastructure as code?

It’s declarative configuration kept in Git, so in practice yes. GitOps tools like Argo CD and Flux were built for exactly this.

Can I use IaC without a cloud account?

Absolutely. Everything in this post ran against local files and local VMs. Practice that way first, then point the same workflow at a cloud provider.

Asif Khan

Asif Khan

DevOps, Linux, and Cloud engineer with 10+ years building infrastructure that stays up, and automation so I do not have to fix it at 2am. My focus is automation, security, and systems built to survive failure, using Infrastructure as Code, CI/CD, containers, monitoring, and cloud platforms instead of manual fixes. This blog is where I write down what worked, and just as often what didn't the first time around. Real tutorials you can run yourself, troubleshooting notes pulled from actual outages, and the kind of detail you only get once you've hit the problem firsthand. If you're trying to get Linux, DevOps, and cloud infrastructure to behave, that's what you'll find here: reproducible solutions you can understand, test, and apply in your own environment.