Infrastructure as Code (IaC) means describing your servers, networks, DNS records and configuration in files — kept in Git — and letting tools create and update the real infrastructure from them. Instead of a sequence of half-remembered SSH sessions and dashboard clicks, you have a repository that says exactly how everything is set up.

Why bother?

  • Reproducibility — rebuild a server or a whole environment from code after a failure, or create an identical staging copy.
  • Documentation that can’t go stale — the code is the configuration.
  • Review and history — every change is a commit you can review, discuss and revert.
  • Consistency — ten web servers configured identically, no snowflakes.
  • Speed — new servers in minutes instead of an afternoon.

Declarative vs imperative

  • Declarative: describe the desired end state (“Nginx installed, this config file present, port 443 open”); the tool works out the steps. Terraform, Ansible modules, Kubernetes manifests, Docker Compose.
  • Imperative: a sequence of commands (a bash script). Easy to start, but harder to make safe to re-run.

Declarative tools are idempotent: running them twice changes nothing the second time. That’s what makes them safe to apply repeatedly.

The layers and the tools

Layer What Typical tools
Provisioning Servers, networks, DNS, firewalls, buckets Terraform / OpenTofu, Pulumi
First boot Users, SSH keys, packages on a new server cloud-init
Configuration Packages, files, services, users, hardening Ansible, Salt, Chef, Puppet
Applications Containers and their settings Docker Compose, Kubernetes manifests, Helm
Delivery Applying changes automatically from Git CI/CD pipelines, GitOps (Argo CD, Flux)

You don’t need all of them. Many VPS setups do very well with Ansible + Docker Compose in Git.

What it looks like on a VPS

A repository like:

infra/
├── terraform/          # DNS records, maybe servers if your provider has an API
│   └── dns.tf
├── ansible/
│   ├── inventory.yml
│   ├── site.yml        # base hardening, Docker, users
│   └── roles/
└── stacks/
    ├── traefik/compose.yaml
    ├── app/compose.yaml
    └── monitoring/compose.yaml
  • New server: create it (panel or API), add it to the inventory, run ansible-playbook site.yml, deploy the stacks.
  • Change: edit files, open a pull request, review, merge, apply (manually or via CI).

Guides: Terraform basics, Ansible playbooks, cloud-init, Docker Compose.

Secrets

Never commit passwords or keys in plain text. Use Ansible Vault, SOPS (with age/GPG), sealed secrets for Kubernetes, or an external secret manager. Keep .env files out of Git (.gitignore) and document which variables are needed. See secrets management.

State and drift

  • Terraform tracks what it created in a state file — store it remotely and back it up. See Terraform state.
  • Drift happens when someone changes things by hand. Make manual changes the exception, and re-apply your code regularly (or in CI with a plan/--check job) to detect differences.

Start small

You don’t have to codify everything at once:

  1. Put your Docker Compose files and Nginx configs in Git.
  2. Write an Ansible playbook for the base setup of a new server (users, SSH, firewall, updates).
  3. Manage DNS with Terraform.
  4. Add CI: lint, terraform plan, ansible-playbook --check.
  5. Automate applies once you trust the process.

Each step pays off on its own — the first time you rebuild a server in fifteen minutes instead of a day, you’ll be convinced.

Common pitfalls

  • Clicking “just this once” in a dashboard — the code no longer matches reality.
  • Giant, untested playbooks — split them into roles and test on a throwaway VPS.
  • Hard-coded values everywhere — use variables per environment.
  • No backups of state and secrets — losing them can be worse than losing a server.

Related: CI/CD pipelines explained and Ansible server hardening. A couple of small KVM VPS instances make a perfect, inexpensive lab for practising IaC.

  • Infrastructure as Code
  • DevOps
  • Automation