About This Article
This is the record of standing up a new server (NUC5) alongside the small Kubernetes cluster I ("TK") run at home ("home-nuc"), and fully converting its configuration management into GitOps via Ansible + GitHub + a Kubernetes Job.
Most of the work — reviewing the design document, organizing requirements, writing the Ansible code, applying the Kubernetes manifests, troubleshooting, and even opening the Pull Requests on GitHub — was handled by an AI agent using Claude Code (in this article I'll call it "Falcoya-co-CTO"). The only things I touched directly were physical operations (plugging in a USB drive, working the BIOS, connecting cables) and operations I never want an AI agent to leave a log trail for — generating secret keys and password hashes.
I'm writing this from two angles. One is the experimental angle: how far can you actually delegate infrastructure construction to an AI agent, autonomously? The other is the technical angle: what kind of GitOps pipeline did we actually build? I want to cover both fully, without shortchanging either.
Note: real IP addresses and hostnames do appear in this article in a form like 192.168.1.75, but these are generalized examples of a home-lab setup, not actually published values. Likewise, the new server I refer to throughout as "NUC5" is a simplified stand-in for the real hostname.
0. The Trigger — Trying a Common GitOps Pattern at Home
The starting point was a pattern common in the GitOps world: manage server configuration with Ansible, manage the code in GitHub, and reflect the server configuration whenever a Pull Request merges.
Centered on products and features like Secure AI and Headless Cloud Security, I wanted to design a "workflow you could actually use" that combines with this kind of GitOps environment. But you can't just try that out directly in a production environment. So I decided to first reproduce a similar "Ansible + GitHub + Kubernetes" pipeline, on my own hands, in my home Kubernetes lab (home-nuc, a 4-node cluster: nuc1 through nuc4).
What I asked Falcoya-co-CTO to do was: using nuc5_infrastructure_manual.md — a 19-chapter procedure manual — as the foundation, actually build one server, and carry it out under tk-team's governance process (work instruction → requirements definition → implementation → review → approval). That 19-chapter manual itself was also written by an AI agent, in a separate session. And, incidentally, the text of this very article was also written by an AI agent, through dialogue with Falcoya-co-CTO.
What is tk-team?: This is an AI agent organization framework I designed myself. A single orchestrator agent, Falcoya-co-CTO, is the sole point of contact for the human (me), and delegates work as needed to subordinate agents like Dev, QA, and Security. At the core of its governance are "hard rules": no direct mutation of production environments, no direct pushes to the main branch without human approval, and never outputting secret information.
1. The First Decision — "This Is a Task That Should Have Its Own Requirements Document"
The first thing Falcoya-co-CTO did wasn't reading the procedure manual — it was deciding how to approach this task at all.
The tk-team framework has stages: "work instruction (WI) → requirements definition → task definition → implementation rehearsal → implementation → three-point gate." The rule is: if a task's preconditions or acceptance criteria exceed three items, the requirements document gets split out from the main document as its own file. Falcoya-co-CTO applied that criterion from the start, and split the requirements document out into an independent file.
What was interesting here is that Falcoya-co-CTO later discovered and recorded its own oversight. The requirements document's initial review (7 rounds) had stayed entirely focused on one axis — autonomy and authority, i.e., "who can execute what." When I pointed out, "I don't think architecture requirements or workflow requirements have been defined," Falcoya-co-CTO realized that its own review perspective had been skewed, and added a new chapter to the requirements document. This entire sequence of events is recorded, self-referentially, right inside the requirements document itself, as "the story of how this was overlooked." Rather than erasing the mistake and quietly redoing it, the practice here is to keep a record of why it was missed in the first place.
2. Drawing the Line on Authority — "Technically Possible" and "Delegated as Policy" Are Different Things
Since this is my own home cluster, I gave Falcoya-co-CTO explicit permission from the very start: "This is home Kubernetes, so go ahead and execute." In fact, from Falcoya-co-CTO's session, kubectl was reachable with admin rights, and SSH to the existing nodes was confirmed to work.
However, Falcoya-co-CTO did not interpret this permission as a blanket "everything's OK." Instead, it divided authority into three zones on its own:
| Zone | Content | Who executes |
|---|---|---|
| A. kubectl/SSH operations | Read/write to the existing cluster, applying manifests, running Jobs | Falcoya-co-CTO (already permitted by me) |
| B. Creating files in the repo | Writing Ansible code, writing Kubernetes manifests | Dev (delegated by Falcoya-co-CTO), PR gate required |
| TK-exclusive | Physical operations, generating secret keys or password hashes | Me, and only me |
The reasoning behind that last zone, "TK-exclusive," might be the single most important point in this entire article. Falcoya-co-CTO stated plainly that it could run the command to register an SSH key with kubectl. This wasn't a technical constraint — it was a line drawn as policy. The reason was structural: "generating a secret key or password hash means the result has to be communicated back to me, and that leaves the secret itself sitting in the conversation log."
In fact, as the work progressed, I checked several times, "Couldn't Falcoya-co-CTO just do this?" For example, passing an existing secret file by reference — something like kubectl create secret --from-file=... — doesn't put the actual value on the command payload at all. Falcoya-co-CTO explained that distinction and answered, "Technically possible, but by convention this has been kept as TK-exclusive" — and I ultimately decided to relax that boundary. On the other hand, the act of newly generating a key or hash stayed exclusively mine, right to the end. The reasoning — "an operation where the result has to be reported back in conversation leaves the secret in the conversation log" — strikes me as a general, reusable criterion for deciding what to let an AI agent touch when secrets are involved.
3. Overall Architecture — Eight Roles, Divided
The pipeline we actually built divides the work across the following eight components. The table in §1.1 of the procedure manual was the basis for this.
The point most likely to be misread here is that NUC5 itself is not a node in the Kubernetes cluster. NUC5 is simply a Linux server that happens to be managed by Ansible. What actually runs on Kubernetes is a short-lived Job (Pod) that spins up on an existing cluster node (nuc2); that Job connects to NUC5 over SSH and runs the Ansible playbook. "The automation runtime that runs on Kubernetes" and "the server that Ansible manages" are two different machines.
Another important design decision was that we deliberately did not reproduce, literally, the initial mental picture of "merging a PR automatically reflects the server configuration." From the start, the procedure manual took the design stance of "staged automation" — triggering the Job is premised on my (the admin's) judgment and instruction, with Falcoya-co-CTO manually triggering it.
Falcoya-co-CTO discovered this inconsistency — the original goal was "automatically reflected," but the manual specified a manual trigger — partway through the requirements definition, and brought it to me for a decision. My answer was: "For now, proceed with the manual's design (manual trigger)." The reason is that the goal at this stage was to establish a state where Git provides tracking, review, and inspection — full unattended automation is a separate matter to consider as a future extension. What mattered here, practically, is that the AI agent did not silently "resolve" the contradiction in the requirements on its own — it surfaced it to the human as a branch point that genuinely needed a judgment call.
4. What Actually Happened — From the Build Log
From here, I'll walk through what actually happened, in chronological order. This isn't just a tidy story of success — it includes the AI agent's own mistakes.
4.1 Pre-flight Checks and Preparing the Namespace
Falcoya-co-CTO first checked the existing cluster's state with kubectl get nodes, confirming NUC5 didn't exist yet (the physical build wasn't done). Next, it confirmed the Secret and ConfigMap didn't exist yet either, and judged on its own that "applying the actual Job/Deployment would be premature" — and held off. All it did at this point was low-impact, reversible prep work — creating the namespace, and confirming the base image's actual digest value (skopeo inspect, no auth required).
4.2 Reading Hardware Info from a Photo
The NUC5 unit itself has a label with the model number and MAC address printed on it. This was initially assumed to be entirely my own exclusive work, as a "value that requires physically checking the hardware." But when I took a photo of the label and handed it over, it turned out Falcoya-co-CTO could read the MAC address (in XX-XX-XX-XX-XX-XX format) straight from the image and enter it into the config file. This is where the distinction between "the physical act itself" and "reading a physical value" became clear.
4.3 Falcoya-co-CTO's Own Mistake, and the Fix
During the USB install, netplan apply failed with an error. Checking the log, the cause was: Invalid MAC address ... must be XX:XX:XX:XX:XX:XX.
The reason was simple. The MAC address Falcoya-co-CTO had read from the label photo had been written into the file exactly as it appeared on the label — hyphen-separated (XX-XX-XX-XX-XX-XX) — without being converted to the colon-separated format (XX:XX:XX:XX:XX:XX) that Linux's network configuration (netplan) requires.
This was a simple conversion mistake by the AI agent. Falcoya-co-CTO discovered its own mistake, judged that "the MAC address itself isn't secret information, so Falcoya-co-CTO can fix the file directly," fixed the file, updated the Secret, and retried the install. This time it succeeded. For me, this episode became a lesson that felt obvious in hindsight but still landed with real weight: "of course an AI agent makes mistakes too — which is exactly why the process of tracing root causes from logs and error messages matters so much."
4.4 Getting to "an OS You Can Safely Reach with a Key"
The OS install itself finished with an Installation complete! message, but afterward, network event logs repeated endlessly and the machine wouldn't power off — unexpected behavior. We couldn't fully pin down the root cause, but judged that the disk write itself had completed, so I force-powered it off and moved on to the next step (booting from the internal disk). There are moments where you have to weigh the risk and move forward, even without fully explaining every symptom.
After successfully booting from the internal disk, Falcoya-co-CTO remotely fetched the host key with ssh-keyscan and registered it as a known host. Per the procedure manual, there was supposed to be a step here where you visually verify the key's fingerprint at the machine's own console — but I judged, "there's no one at home who'd be spoofing this," and skipped that step. Falcoya-co-CTO explained the risk and recorded this omission explicitly as my own decision. Rather than treating the "omission" of a security step as inherently bad, the approach here is to record it as a documented risk-and-judgment call.
4.5 Applying Ansible, and Confirming Idempotency
Once we had a state where the key-based connection was secure, Falcoya-co-CTO built Ansible in a scratch environment and first validated the syntax and preconditions (OS version, IP assignment, etc.) in --check mode (a dry-run that makes no actual changes). After confirming everything succeeded, it ran the real apply (ansible-playbook playbooks/site.yml --limit nuc5), and 3 of 9 tasks made changes and succeeded.
What mattered here was running the same playbook a second time. This is to demonstrate GitOps's core premise — idempotency: "applying the same configuration again causes no unintended changes." The first run: changed=3. The second: changed=0. Only once you have that result can you actually say "this Ansible configuration can safely be re-applied any number of times."
Incidentally, at this point Falcoya-co-CTO was still running ansible-playbook directly over SSH. This was a deliberate simplification, since the ability to push container images to GHCR wasn't in place yet. The stance throughout was to use this kind of interim shortcut on the way to the final "via a Kubernetes Job" form, and to record that fact honestly rather than hide it.
4.6 Containerizing, Pushing to GHCR, and a Third Confirmation
At the stage of containerizing the configuration-management runtime and pushing it to GitHub Container Registry (GHCR), I issued a Personal Access Token scoped to write:packages. Falcoya-co-CTO confirmed the base image's (python:3.12-slim-bookworm) actual digest, built the image, pushed it to GHCR, and confirmed its visibility was set to private.
After this, at the point of registering a Secret called nuc5-ssh (containing the actual SSH private key) with Kubernetes, I asked, for the third time, "Couldn't Falcoya-co-CTO just do this?" Falcoya-co-CTO explained, "Technically executable (the value never has to be displayed — it can be passed purely by file reference), but up until now this has been kept as a policy boundary" — and I formally decided to relax that boundary. The AI agent never generates a secret, right to the end. But passing an already-existing secret file by reference, without ever displaying the value, is delegated once the necessary confirmation has happened. That line ended up being the practical landing point for me.
4.7 The Real Apply via a Kubernetes Job, and the "Done" Verdict
Finally, we ran the containerized Ansible runtime as a Kubernetes Job. First a dry-run with --check, checked the log, deleted the Job, then ran the real-apply Job with --check removed — 1 of 9 tasks changed and succeeded, and the applied Git commit ID was now recorded on NUC5's filesystem. Running the same Job again: changed=0. Idempotency held even in the containerized runtime.
The procedure manual has a 17-item checklist for judging that "the GitOps foundation is complete." Falcoya-co-CTO actually checked SSH connectivity to the real machine, kubectl, and Git history, and self-verified 15 items; of the remaining 2, one was marked "out of scope, since it was deliberately carried over from the old OS," and I separately completed the other by doing an encrypted backup of the credentials. With all 17 items accounted for, this build was judged complete.
5. What Was Happening on GitHub
Behind the scenes, multiple Pull Requests were flowing through the home-nuc repository. Every change — scaffolding out files, resolving placeholders, flipping --check mode on or off — followed the same loop: create a branch, pass CI's syntax check, get my approval, merge.
This is the part that reproduces the GitOps pattern mentioned at the start. But as noted above, the merge itself never triggers the apply to NUC5 automatically. Git is the single source of truth for proposing, inspecting, and recording the history of changes; the actual apply is a separate step — a Kubernetes Job — and was deliberately left as a manual trigger. This wasn't cutting corners; it's a straight carryover of the procedure manual's own design decision not to bundle "build succeeded" and "applied to the real machine" into the same unapproved process.
6. Looking Back — What We Were Glad to Delegate to the AI Agent, and What We Didn't
What we were glad to delegate
- Reading and structuring the procedure manual: reading a 19-chapter manual, structuring it into a requirements document and a task definition, and self-checking for gaps. Work that would take a human hours to organize, the AI agent did quickly — and even recorded the perspectives it had initially missed.
-
Repetitive, routine work: Ansible syntax checks, dry-runs in
--checkmode, re-verifying idempotency. These are unglamorous but important tasks — "do it again to confirm" — and they suited the AI agent well. - Root-cause investigation from logs: like the MAC-address formatting mistake, pinpointing a cause from an error message and proposing a fix. This was also, admittedly, fixing the AI agent's own mistake — but the investigation process itself held up.
What we deliberately did not delegate
- Generating secrets: newly creating SSH keys or password hashes stayed exclusively human, right to the end. The reason is a structural risk: trying to communicate the generated result in a verifiable way leaves the secret itself sitting in the conversation log. This isn't a limitation unique to AI agents so much as a constraint that applies to any work where the result has to be reported back to a person.
- Physical operations: plugging in a USB drive, working the BIOS screen, connecting cables. An AI agent literally has no "hands," so this was obviously human work.
- The final call on the mismatch between the original goal and the manual's design: the AI agent discovered that the original goal ("automatically reflect on PR merge") conflicted with the procedure manual's design stance ("staged automation") — but the decision of which one to go with was left to the human. This wasn't a problem with a technically correct answer; it was a call that belonged to me.
What stuck with me the most
Honestly, what stuck with me the most wasn't "the automation that worked well" — it was the fact that the AI agent left its own oversights and mistakes on the record, without hiding them. The requirements review's skewed perspective, the MAC address's wrong separator — in both cases, rather than erasing it and quietly redoing it, the document keeps the actual story of "what happened, why it happened, and how it was fixed." This was something we could only afford to try because it's a small home lab — but as I think about widening the scope of what I delegate to an AI agent, this kind of transparency feels like a reproducible source of reassurance.
What's Next
This GitOps foundation is, in the end, just that — a foundation. Verifying a more practical workflow, one combined with Secure AI and Headless Cloud Security, is the next step I plan to take on top of it.




