· · 7 min read Intermediate

Infrastructure as Code for People Who Need to Understand

Why Infrastructure as Code appeals to people who need to understand systems. Making the implicit explicit, and why that matters for control and sovereignty.

A machine wired to a blueprint that shows every knob, she bins a creature's magic wand

Someone opens a cloud console, clicks through a wizard, accepts a few defaults, and a resource appears. The dashboard goes green and everyone moves on. That’s still how a lot of infrastructure gets built in 2026, and I’ve built some of it that way myself.

I can’t work like that for long. When I click a button and infrastructure appears, it feels borrowed. I want the configuration written down somewhere I can read it back, and I want to know why something exists, not only that it exists. The console gives me a green tick. It doesn’t give me any of that.

So this post is about the distance between infrastructure that happens to you and infrastructure you can hold in your head.

What click-ops costs

Click your way to a working system and you get a working system. You also get a pile of decisions nobody wrote down.

Every resource that comes out of a GUI carries settings you never consciously chose and dependencies you never saw form. State piles up where nobody audits it. None of that hurts on day one. It hurts six months later, when someone asks why a security group has 47 rules, or whether that orphaned load balancer is safe to delete, and the honest answer is that nobody knows.

Won’t the documentation save you? It won’t. Docs describe what things should be. The moment you change something in the console without updating the wiki, the two drift apart, and they always drift apart. I’ve done this myself, more than once. After a while you’re reading the wiki and hoping it’s still true.

When everything is written down

Now picture the same system, except every piece of it exists as text you can read and recreate.

# This exists because we need to allow traffic from the monitoring namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-prometheus
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: monitoring
      ports:
        - port: 9090
          protocol: TCP

The comment explains why. The code shows what. Both live in the same file, under version control, readable by anyone who clones the repo. Nobody has to do archaeology later, because the reason never got separated from the thing.

That one property, explicit rather than implicit, is why I trust Infrastructure as Code. Best practice or not, I can read it.

The cost is in your head

Let me go back to the messy side, because the cost I described above is the cheap one. The expensive one is what click-ops does to your head.

You made a configuration decision for a reason. Months pass. Was that timeout value load-bearing? Did that IP whitelist protect against something specific, or did someone add it during an incident and forget? Nobody remembers, so the setting stays forever and slowly turns into the kind of technical debt everyone is a bit afraid to touch.

And there’s always one person on the team who happens to remember why the network looks the way it does. When something breaks, everyone turns to that person. That’s a single point of failure, and it’s a human. Humans forget things. I know, because I’ve been that person.

Written-down infrastructure moves that knowledge from one tired person into a file the whole team can read.

What you get back

Once the system lives in Git, a few things stop being heroics.

History is no longer a guess. Every change carries a commit message and an author, so you can trace a decision back through time:

git log --oneline -- network-policy.yaml

a1b2c3d Add network policy for Prometheus scraping
d4e5f6g Restrict to specific port (was allowing all)
g7h8i9j Initial creation for monitoring access

A second pair of eyes gets a look before production does. The classic “allow all” mistake is hard to spot in a console. In a diff it jumps off the screen:

# Pull request diff
- cidrBlocks:
-   - 0.0.0.0/0  # DANGER: allows all traffic
+ cidrBlocks:
+   - 10.0.0.0/8  # Internal network only

Recovery is a command rather than an anxious afternoon. When a cluster dies, you don’t rebuild it from memory:

# Recreate everything
terraform apply
kubectl apply -k ./infrastructure
argocd app sync --all

The declarative part is what holds this together. You write down the state you want, and making reality match it is the system’s job.

# I declare: there should be 3 replicas
apiVersion: apps/v1
kind: Deployment
spec:
  replicas: 3

A pod dies, Kubernetes notices there are two instead of three, and starts a new one without anyone clicking anything. You declared the contract. The system enforces it.

There’s room to write down intent too, and not only mechanics. Tags and comments turn configuration into a message to whoever reads it next, usually a future version of you:

# Terraform: Why this VPC exists
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name        = "production"
    Environment = "production"
    Purpose     = "Primary VPC for production workloads"
    Owner       = "platform-team"
    CostCenter  = "infrastructure"
  }
}

This is roughly how my homelab is built. Everything that runs there is defined in one repo:

homelab/
├── infrastructure/
│   ├── terraform/        # VMs, networks, DNS
│   └── kubernetes/       # Cluster configuration
├── applications/
│   ├── argocd/          # GitOps controller
│   ├── monitoring/      # Prometheus, Grafana, Loki
│   └── services/        # Home Assistant, Forgejo, etc.
└── secrets/
    └── external-secrets/  # ExternalSecret manifests (secrets live in Vault)

If the whole thing burned down tomorrow, I could rebuild it with three commands and a fresh disk, because I never trusted my own memory to hold the configuration in the first place.

Why people don’t switch

If this is so clearly better, why does click-ops survive? Because the objections feel real in the moment. Here’s what I hear most, and what I make of each.

“It’s slower than clicking.” At first, sure. Writing the YAML once takes ten minutes against a five-minute click. But you write the YAML once and it works in every environment. The clicking you repeat by hand, every time, and it transfers to nothing.

“Not everything can be Infrastructure as Code.” True. Some steps need a human, and the honest answer is to write those down instead of pretending they don’t exist:

# manual-steps.md

## Setting up external DNS provider

1. Log into Cloudflare dashboard
2. Create API token with Zone:Edit permission
3. Add token to Vault at `secret/cloudflare/api-token`

Note: This is manual because Cloudflare doesn't support
organization-level tokens via API.

Last performed: 2026-03-15 by @tom

The goal is that nothing goes undocumented, including the parts you can’t automate.

“My team won’t adopt it.” Then don’t ask them to adopt all of it. Automate one painful thing, do it well, and let the result do the talking. The three hours you didn’t spend rebuilding a server. The outage a code review would’ve caught. That convinces people faster than any argument I’ve made.

Start from where you are

You don’t have to rewrite your infrastructure to get here. Start by capturing what already exists.

# Export existing Kubernetes resources
kubectl get deployment my-app -o yaml > deployments/my-app.yaml

# Import existing Terraform resources
terraform import aws_instance.web i-1234567890abcdef0

Then give it some structure, split by environment, so the differences are visible instead of buried:

infrastructure/
├── base/                    # Common configuration
│   ├── namespaces.yaml
│   ├── rbac.yaml
│   └── network-policies.yaml
├── environments/
│   ├── development/
│   │   └── kustomization.yaml  # Overlays for dev
│   ├── staging/
│   │   └── kustomization.yaml
│   └── production/
│       └── kustomization.yaml  # Production-specific settings
└── applications/
    ├── api/
    ├── frontend/
    └── database/

And stop applying it by hand. Wire it into CI, or better, hand it to GitOps and let a controller keep reality in sync with the repo:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: infrastructure
spec:
  source:
    repoURL: https://gitlab.internal/infrastructure
    path: environments/production
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated:
      selfHeal: true

Where this touches sovereignty

For me this lands on something bigger than tidy YAML. Digital sovereignty goes well past owning the hardware. It means understanding and controlling the systems you depend on, and you can’t control what you can’t see.

Infrastructure as Code is what makes that control real. Moving providers becomes a config change. Recreating everything from scratch becomes a command. And for any piece of the stack I can answer “why does this exist” without hunting down whoever last clicked the buttons and hoping they remember.

That’s the part I care about. I understand my infrastructure because I wrote it down, line by line, in files I can read. When something breaks, I don’t open a console. I open the repo, and it tells me what should be running and why. For a brain that treats an unexplained system as a low-grade itch, the afternoon it took to write it all down was cheap.