GitOps Is _Not_ Trivial
Overview
Recently I've been working on revamping my home lab setup with some more dedicated hardware and I've been replacing a lot of custom scripts and processes with more or less industry standard workflows (e.g. Runners, K8s, Flux).
In doing this, I've really learned that it is not trivial to setup a complete tech stack for GitOps/DevOps at home. Many tools, like Flux, with provide documentation or demos on setting up their system for only the simplest cases, and those cases will usually involve some cloud service like Github or Dockerhub to handle a bunch of the configuration.
The topic that we're referring to is "vendor debt". Law Insider defines "vendor debt" as money owed to a company for good or services already provided. I'm not talking about that. I'm referring to the replacement cost or implementation cost of those goods and services if they suddenly dried up from existing vendors. If Github suddenly decided to charge for every clone of your code base, you might find yourself having to implement your own source code revision control service. Do you have an idea of the level of effort to make that happen?
Maybe "technical vendor debt" is the term I'm after?
If you're like me and you do not want to depend on upstream vendor services (but don't mind depending on all of the open source code available on the net), you may find yourself developing a DevOps workflow for your various projects. While fundamentally this could be seen as a trivial set of git hooks and scripts with SSH access ... the reality is that you'll likely work your way into wiring up more common frameworks like Git Actions, Kubernetes, and FluxCD to:
- Prevent from reinventing what's already been done.
- Provide yourself an environment that can be picked up by other users of the same framework.
Below, I've listed an outline of things that I see as being required to properly setup a simple (Docusaurus) blog project so that when I commit, its automatically deployed to a local K8s cluster. Note that my intention is not to specify how to do this, but to visualize what needs to be done. We want to understand the various moving parts and the level of effort involved. (That said, if you want to know how to accomplish a lot of this, I have documented that as well.)
Assumptions
A couple assumptions:
- A machine can be a VM or a bare metal device.
- A dedicated machine with its own dedicated memory to support building code and docker images.
- A dedicated machine with its own dedicated memory to support deployment and serving applications.
- I'll be using DigitalOcean DNS
- I'll be using Let's Encrypt ACME certificates.
Note: Setting up a CA with Caddy and a DNS with dnsmasq is relatively easy. But configuring DNS and CA's across all intranet infrastructure is too much unless you have Ansible or Puppet fully integrated into all systems. (I do not.)
The Outline
Disclaimer: The below items are written from memory and not tested. While I've attempted to make it as linear as possible, there may be steps that are optimally moved around. That said, as you work your way through the various items, its always going to change as you test what things are working and what things you may have fat fingered. In summary, this list is not precise, but rather accurate enough to express the complexity involved with setting up a simple modern blog site with full and production quality CI/CD principles and practices.
Setup Build System Machine
Summary: The build system machine is responsible for building container images. We deliberately keep it separate from the k3s machine because it runs with root permissions and can easily consume resources.
- Install machine operating system.
- Create the user that will be capable of doing the CI/CD tasks (e.g.
cicd). - Install a VPN (e.g. tailscale) for global access to LAN.
- Install
docker-ce, Docker Compose v2,sudo,vim,curl,git. - Install and configure
anacron/cronie. - Add cronjob to prune unused container images.
Setup K3s System Machine
Summary: The K3s system machine is a kubernetes cluster node that serves deployed applications. While we'd like to GitOps all the things, this does not include critical services like Gitea.
- Install machine operating system. (This could be a bare metal machine or a VM.)
- Create the user that will be capable of doing the CI/CD tasks (e.g.
cicd). - Configure OpenSSH to listen on port 2222 (to prevent confliction with Gitea).
- From the
cicdunix account, create SSH keys. - Copy SSH keys to build system's
cicdaccount. - Install a VPN (e.g. tailscale) for global access to LAN.
- Install
k3s,k9s,curl,sudo, andvim. - Configure
k3sto listen to the VPN address. - Download and add
fluxto PATH. - Optionally add
cicdtosudogroup.
Setup DNS and CA
Summary: With the guilty knowledge of the dependencies for this whole process, I already know that I'll need 4 distinct hostnames to accomplish the configuration we're aiming for.
- Point domain nameserver
vinnie.workfrom GoDaddy to DigitalOcean - Create DigitalOcean API key that permits
certbotto renew certificate unattended. - Add
-K3s-VPN-IP- git.vinnie.workentry to DigitalOcean DNS. - Add
-K3s-VPN-IP- blog.vinnie.workentry to DigitalOcean DNS. - Add
-Build-VPN-IP- build.vinnie.workentry to DigitalOcean DNS. - Add
-K3s-VPN-IP- flux.vinnie.workentry to DigitalOcean DNS.
Setup Gitea
- Create and apply kustomization of Gitea deployment for k3s server.
- This will start the Gitea deployment with initial configuration.
- Create monthly cronjob to renew certificate for
git.vinnie.work. - Create initial certificate for
git.vinnie.workwith certbot. - The Gitea service should be listening on port 80 with a ClusterIP.
- The Gitea service should be listening on port 22 with a LoadBalancer IP.
- Create and apply kustomization of Caddy.
- Configure caddy with the
git.vinnie.workcertificate and a reverse proxy to the Gitea deployment associated service. - The Caddy service should be listening on port 443 with a LoadBalancer IP.
- Deployment Name:
proxy-deployment - Namespace:
work-vinnie-proxy-ns
- Configure caddy with the
- Open
git.vinnie.workin a browser and setup the Gitea service (w/ defaults). - Do the initial account sign-up on Gitea to create the administrator account.
- With the administrator account, create a CI/CD account (e.g.
cicd). - Copy the public SSH key from the
cicdunix account into thecicdGitea account SSH keys. - Fetch the action runner registration key from site administration in Gitea.
- Create an organization
vinnie.workin Gitea.- Create a (mono) project
everythingin Gitea organizationvinnie.work.- Note: I'm a fan of small projects in contrast to mono-projects, but there is no reason to over complicate this procedure with that bias.
- Enable Actions for the
everythingproject. - Create a
cicdgroup in theeverythingproject with the ability to commit and write packages. - Add
cicdas a member of thecicdgroup.
- Create a (mono) project
- Logout of admin and login to
cicd. - Add
cicdunix account public SSH key tocicdSSH keys in Gitea
From Builder
-
Login as
cicd -
SCP a copy of
/etc/rancher/k3s/k3s.yamlto builder for kubectl access. -
Set the
cicdprofile so KUBECONFIG is set tok3s.yamlfile. -
Modify
k3s.yamlto point at correct IP or hostname. -
Install k3s-
kubectlandk9sfrom GitHub to builder. -
git clone [email protected]:vinnie.work/everything.git && cd everything
-
Construct Certbot Container Project:
- Create docker-compose to build and run
certbotin a container.- Tagged as:
git.vinnie.work/vinnie.work/certbot
- Tagged as:
- Git Add/Commit/Push Updates upstream
- Build & Push the container image to
git.vinnie.work
- Create docker-compose to build and run
-
Initialize a Docusaurus project (as our blog)
- Construct docker-compose.yaml to build image.
- Tagged as:
git.vinnie.work/vinnie.work/blog
- Tagged as:
- Git Add/Commit/Push Updates upstream
- Build & Push container image to
git.vinnie.work - Create and apply kustomization of Docusaurus deployment for k3s server.
- Service name:
blog-service - Namespace:
work-vinnie-blog-ns
- Service name:
- Git Add/Commit/Push Updates upstream
- Point
blog.vinnie.workendpoint in Caddy athttp://blog-service.work-vinnie-blog-ns.svc.cluster.local - Git Add/Commit/Push Updates upstream
kubectl -n work-vinnie-proxy-ns rollout restart deploy proxy-deployment
- Construct docker-compose.yaml to build image.
From k3s
- Login as
cicd - Do flux prechecks
- Bootstrap flux (w/ Image Automation Plugins)
- Use the
vinnie.work/everythingproject with aflux-configpath. - Use
flux-systemnamespace.
- Use the
- Use kubectl or k9s to wait for flux to be completely up and Running.
- Create and apply kustomization for flux integration for
blog.vinnie.work.- Register
vinnie.work/everythingGitRepository. - Register
vinnie.workImageRespository. - Register ImagePolicy
^deploy-(?P<ts>.*)-[a-fA-F0-9]+(Determines how to select latest image) - Register ImageUpdateAutomation (w/ updates kustomization manifests based on ImagePolicy)
- Register Kustomization (re-deploys updates to compiled kustomization manifests)
- Add ImageUpdateAutomation annotations to Yaml manifests.
- Register Receiver (web hook to trigger flux reconciliation)
- Register Secret (for Receiver)
- Git Add/Commit/Push Updates upstream
- Point
flux.vinnie.workendpoint in Caddy athttp://webhook-receiver.flux-system.svc.cluster.local - Git Add/Commit/Push Updates upstream
kubectl -n work-vinnie-proxy-ns rollout restart deploy proxy-deployment
- Register
From builder
-
Mirror several action repositories from Github to local Gitea instance (make them public!):
https://github.com/actions/checkout.git -> git.vinnie.work/actions/checkout.githttps://github.com/docker/login-action.git -> git.vinnie.work/docker/login-action.githttps://github.com/docker/build-push-action.git -> git.vinnie.work/docker/build-push-action.git
-
Construct gitea_runner container project
- Create docker-compose to build and run
gitea_runnerin a container.- Tagged as:
git.vinnie.work/vinnie.work/nodejs_runner - Persist runner registrations in storage volume.
- Restart: unless-stopped
- Tagged as:
- Include
docker-ce, Docker Compose v2, and other required software (e.g. nodejs). - Git Add/Commit/Push Updates upstream
- Build & Push the container image to
git.vinnie.work
- Create docker-compose to build and run
-
Start
git.vinnie.work/vinnie.work/nodejs_runnerand register it with the Gitea Actions Registration key. -
Within Docusaurus/blog.vinnie.work project:
- Create
.giteaaction/workflow folder. - Create the actions script to clone, build, push, and call flux webhook.
- This is usually contained within a build/do/cicd script in the project folder.
- Git Add/Commit/Push Updates upstream
- Create
-
Make documentation changes to Docusaurus project.
-
Git Add/Commit/Push Updates upstream
-
Check updates were applied. (Note: This can take 10s of minutes the first time.)
-
Yay, All done! Wasn't that simple? (It was only, roughly, 105 steps.)

What You'll See In Practice
Often I see over trivialized environments that lean heavily on Github, Dockerhub, and/or other cloud services. It may resemble something like:
- Create a Github project (from scratch or with a template)
- Copy and paste some yaml or install some helm template.
- Viola, you have CI/CD. Pffft! Good luck.
This is perfectly normal for getting a project quickly off the ground, but in the spirit of making deliberate decisions, as an engineer or developer, I want to know what the replacement cost is if those services: go away, change price models, change policies, or any other number of decisions businesses have every right to make on a whim. If you want to remain properly prepared for eventualities, know the actual dependency debt you are incurring.