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/--checkjob) to detect differences.
Start small
You don’t have to codify everything at once:
- Put your Docker Compose files and Nginx configs in Git.
- Write an Ansible playbook for the base setup of a new server (users, SSH, firewall, updates).
- Manage DNS with Terraform.
- Add CI: lint,
terraform plan,ansible-playbook --check. - 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.