TPTito Pinardo Gutiérrez
← Back to the rack
Español
1U · IACTerraform · Ansible · Git In production

Slot 18 of 25 in the rack · In production

1U unit · Infrastructure as code

The homelab as code

Rebuilding a service: one command

The home services had been set up by hand over years and nobody, me included, knew how to rebuild one. Now Terraform creates each container on Proxmox and an Ansible role configures it; secrets are encrypted, every change is reviewed before it's applied and everything sits in Git with an automatic backup.

How I work
From a control container on the server itself, with the AI agents I work with in terminals I can open from my laptop or the browser.
The rule
Nothing is applied without first seeing what will change: «terraform plan» or «--check --diff». And pinned versions: rerunning a playbook updates nothing by surprise.
The public part
A cleaned copy of the repository on GitHub, generated and checked automatically on every change and published when I approve it: structure and roles, with no real IPs, secrets or firewall rules.
  • 1Terraform file per container or VM
  • 1Ansible role per service
  • 4 → 0containers a plan would have destroyed, not applied
  • 2copies of the repository on every commit

Its code, in the open Public version of the repo: no IPs or secrets.

Inside the control node

Everything else is created, configured and updated from here. Pick a flow to see how a service is born and what stops it before something breaks.

Hermesvisiting the unit

Click a part or pick a flow. If you came from a skill, the parts where I use it are already lit.

      A service is born
      1. My laptop — From my laptop I open the terminals panel.
      2. Terminals panel — I reach the control node's session.
      3. AI agents — With an agent, I write the Terraform file for the new container.
      4. Terraform — Terraform works out the change.
      5. terraform plan — The plan says: 1 to add, 0 to destroy.
      6. Review — I read it. If something unexpected shows up, I stop.
      7. Terraform — Terraform applies.
      8. Proxmox API — Through the Proxmox API.
      9. LXC containers — And the container exists.
      10. Roles — I write the service's role.
      11. Secrets — Its secrets, into OpenBao; in the repository, only their names.
      12. Dry run with diff — Dry run: which files would change.
      13. Review — I review it.
      14. Roles — I apply the role.
      15. Hardening role — Hardening and monitoring, the same as everywhere else.
      16. Service list — I add the service to the common list.
      17. Cloudflare DNS — And its DNS is created only if it's public.
      18. Git hook — Commit.
      19. Repository on Forgejo — To Forgejo, where CI runs.
      20. Backup repository — And to the backup, in the background.
      I update the fleet
      1. Terminals panel — Every Wednesday before dawn, packages update on their own; for the rest, a button in Forgejo: what (packages, containers or everything) and on which machines.
      2. Updates — First, a simulation per machine.
      3. Dry run with diff — Kernel, driver or critical-service changes are rejected.
      4. Updates — It's applied machine by machine.
      5. LXC containers — Each container has to come back healthy before moving to the next.
      6. Hardening role — The monitoring agents confirm it, and the summary reaches my phone: what changed and which machines want a restart. That's my call.
      A plan that would have deleted data
      1. Terraform — I centralise SSH keys into a single list for every container.
      2. terraform plan — The plan comes out: 4 to add, 4 to destroy.
      3. Changes that recreate — That field can't be changed in place: it would recreate the containers.
      4. Review — Among them, messaging with its whole database and the public door.
      5. LXC containers — Not applied. The keys are added by hand to existing ones and the list applies to new ones.
      The public copy, automatic but reviewed
      1. Repository on Forgejo — Every merge generates the copy: only files on a whitelist; IPs, domains, emails and keys swapped for examples; secrets reduced to their names with no value; functions cut down to their signature and description.
      2. Review — A leak check goes through all of it and stops any real data; also in every PR's CI. If it passes, I get a link to what would change and review it.
      3. Cleaned public copy — With my approval it's published to GitHub, only if it's exactly what I reviewed and without rewriting its history.
      Rebuilding a service
      1. Terraform — A container breaks beyond repair.
      2. Proxmox API — Terraform creates it again.
      3. LXC containers — Just as it was.
      4. Data kept apart — Its storage with the data is mounted.
      5. Roles — The role configures it.
      6. LXC containers — And the service is back. It used to be an afternoon of trial and error.
      1. My laptop

        From home or the VPN, I open the terminals panel.

      2. Repository on Forgejo

        All the infrastructure: Terraform, inventory, roles, playbooks, scripts and docs. Changes come in by PR with CI and their plan in view. It has its own unit.

        • Git
        • Forgejo · Actions
      3. Backup repository

        A hook pushes every commit to a second repository inside the house. It has its own unit.

        • Git
      4. Cleaned public copy

        The version I show, generated on every merge: a whitelist of files, documentation IPs, example domains and functions cut down to their description. It's only published if I approve it.

        • Git
        • Linux / SSH hardening
      5. Proxmox API

        Terraform talks to it with a least-privilege token.

        • Proxmox VE
      6. Cloudflare DNS

        Public records are created and deleted from the same service list.

        • Technitium DNS
      7. Terminals panel

        Each agent's real sessions, in tmux, visible from the browser behind the internal door.

        • Linux (Debian, Alpine, Arch)
      8. AI agents

        The ones I work with: they propose and run things, and I review. On subscriptions, never loose API keys.

      9. Git hook

        After every commit, it pushes to both remotes in the background.

        • Git
      10. Review

        The step that never gets skipped: read the plan or the diff before applying.

      11. Terraform

        Containers and VMs, their CPU, memory, disks, network and firewall.

        • Terraform
      12. terraform plan

        What would be created, changed or destroyed. If it says «destroy» on something with data, it stops.

        • Terraform
      13. Terraform state

        What Terraform believes exists. It was moved between machines after checking the plan came out identical on both.

        • Terraform
      14. Changes that recreate

        Some fields can't be changed in place: changing them destroys and recreates the resource.

        • Terraform
      15. Roles

        One per service, reusable: packages, configuration, service and a final check.

        • Ansible
      16. Secrets

        No more secret files: each group reads its own from OpenBao in 15-minute tokens, and the repository only has their names. The public one, not even that: examples.

        • Ansible
        • Linux / SSH hardening
      17. Dry run with diff

        Which files would change and how, without changing anything.

        • Ansible
      18. Service list

        A single list for Traefik routes, Cloudflare DNS and the start page.

        • Ansible
        • Traefik
      19. LXC containers

        One container per service, almost all unprivileged.

        • LXC
        • Linux (Debian, Alpine, Arch)
      20. Hardening role

        Key-only SSH from specific sources, spare services removed and monitoring agents installed.

        • Linux / SSH hardening
        • Ansible
      21. Updates

        Packages across the fleet, on their own every Wednesday before dawn; Docker images and the rest, with a button in Forgejo. Nothing restarts on its own.

        • Ansible
        • Linux (Debian, Alpine, Arch)
      22. Data kept apart

        Data lives on ZFS and is mounted into the container. Roles manage configuration, not data.

        • ZFS

      What I use from my skill tree

      Pick a skill: the parts where I use it light up on the map and I tell you what for.

      Terraform
      One file per container or VM with the Proxmox provider, a plan before every apply and care with fields that recreate.
      Ansible
      One role per service, secrets read from OpenBao, dry runs with --check --diff and fleet updates machine by machine.
      Proxmox VE
      Terraform talks to its API with a least-privilege token; the control node lives in a container on the server itself.
      LXC
      One container per service, almost all unprivileged.
      ZFS
      Each service's data lives on ZFS and is mounted; roles don't touch it.
      Git
      Everything versioned, with a hook pushing to two remotes and a cleaned public copy.
      Forgejo · Actions
      The repository's origin: protected main branch, CI with no secrets, the PR's plan and one-click deploys.
      Renovate
      One PR for every pinned version with a new release, with its official checksum; applied with the same plan and the same click.
      Linux (Debian, Alpine, Arch)
      Debian, Ubuntu and Alpine containers configured uniformly.
      Traefik
      Its routes are generated from the common service list.
      Technitium DNS
      Cloudflare records are created and deleted from the same list; the home DNS is configured from here too.
      Linux / SSH hardening
      A common role: key-only SSH from specific sources, spare services removed and monitoring everywhere.
      OpenBao · Vault
      Every secret and the Terraform credentials come from OpenBao, not from the repository or the control node's disk.
      CrowdSec
      Its role installs the engine, its sources and the daily summary; the real IP behind Cloudflare and the plugin, in Traefik's.
      Proxmox Backup Server
      The backup server, its datastore, the nightly job and the separate copy of the game worlds, all from code.

      Incident log

      Real failures. Read the symptom, pick the cause you think it was and see how it was fixed.

      1. #1

        An innocent change that destroyed four containers

        16-09-2026 · Terraform

        I wanted to add my laptop's SSH key to every container from a common list. The plan said «4 to add, 4 to destroy».

        What do you think was going on?

        Show the fix
        Cause
        In the Proxmox provider, the container account's keys can't be changed in place: any change destroys and recreates it. It would have wiped the messaging database and taken down the public door.
        Fix
        Don't apply that plan. Add the keys by hand on existing containers; the common list applies to those created from now on.
        Lesson
        The plan is the brake. A «destroy» on something with data gets read twice, however innocent the change looks.
      2. #2

        Renaming a resource

        September 2026 · Terraform

        Giving a resource a clearer name in the code, the plan wanted to destroy the container and create an identical one.

        What do you think was going on?

        Show the fix
        Cause
        Terraform identifies each resource by its address in the code. If it changes, it thinks the old one is surplus and a new one is missing.
        Fix
        Move the resource in the state to its new name before applying. The plan then shows no changes.
        Lesson
        In Terraform, renaming isn't cosmetic. Move it in the state first, then check the plan is empty.
      3. #3

        Quotes breaking on the way

        16-09-2026 · scripts

        A script registering containers in monitoring failed differently every time: truncated quotes, lost newlines.

        What do you think was going on?

        Show the fix
        Cause
        To reach the container the code travelled through three shell layers, and each one reinterpreted quotes and newlines.
        Fix
        Always send the code in base64 and decode it on the other side before running it. The problem disappears at the root.
        Lesson
        When you patch case by case and it keeps failing, change the transport instead of escaping harder.
      4. #4

        The backups stopped getting commits

        09-10-2026 · Git

        Commits went through fine, but both remotes hadn't received anything for a while. No visible error.

        What do you think was going on?

        Show the fix
        Cause
        The hook pushes in the background, so its errors aren't seen. After fixing a commit that was already on the remotes, the histories diverged and every later push was rejected.
        Fix
        Check what each remote had and force-push with a lease (only if nobody pushed anything else meanwhile).
        Lesson
        What runs in the background needs a visible check. After rewriting history, look at the remotes.

      How it was built

      1. September 2026

        First resources

        • A least-privilege Proxmox token and a test container later retired.
        • The public door, first cloned and tested, then migrated.
        • Messaging, start page and Cloudflare DNS from the same list.
      2. September 2026

        The whole fleet

        • Plex, automations, documents and monitoring moved to code, always with the original stopped as a safety net.
        • A common hardening role and monitoring registration in one command.
      3. 22-09-2026

        Control from the server

        • The control node moves from my PC to a container on the server, with an identical plan on both.
        • A terminals panel with the AI agents, behind the internal door.
        • A cleaned public copy on GitHub.

      Decisions (and why)

      • What each tool does

        Terraform creates, Ansible configures

        Terraform knows what exists and compares it with the code; Ansible knows how to bring a machine to a state. Mixing them means neither fully knows what's going on.

      • Versions

        Pinned

        Rerunning a playbook mustn't update anything by surprise. Updating is an explicit change, with its commit.

      • Data

        Kept apart from the roles

        A role that touches data is a role that can delete it. Data lives on mounted storage and is restored on its own.

      • Migrating without risk

        The original stopped, not destroyed

        Each migration leaves the old container switched off as a way back until the new one has been running for days.

      What I learned

      • The plan is the handbrake: an unexpected «destroy» gets read before going on.
      • Keep what exists (Terraform), how it is (Ansible) and the data separate.
      • Migrate with the original stopped next to it.

      What's next

      • Move what's still manual into code: the Docker stack manager with all its services.
      • Move the router configuration into code too.

      Shall we talk?

      I can walk through any of these decisions in detail in an interview.

      Download CV Get in touch ← Back to the rack

      There's a public, cleaned copy of the repository on my GitHub. No addresses, tokens or firewall rules appear here.