The Problem

Managing a personal homelab with dozens of services, databases, and networking rules quickly becomes a manual, error‑prone process. Each change—adding a new container, updating a Helm chart, or rotating a secret—needs to be applied consistently across Docker, Kubernetes, Terraform, and Ansible layers while keeping secrets safe.

What This Does

The repository provides a single source of truth for the entire stack:

  • Kubernetes – 389 YAML files under kubernetes/ define Helm releases, Kustomize overlays, and external‑secret objects (e.g., kubernetes/apps/arr/radarr/app/helmrelease.yaml).
  • Terraform – 70 .tf files in terraform/ provision cloud resources such as Cloudflare DNS (terraform/cloudflare/) and the underlying Proxmox/VPS hosts.
  • Dockerdocker/ holds per‑service docker-compose.yml files (e.g., docker/jellyfin/docker-compose.yml) and reusable Dockerfiles (docker/Dockerfiles/actions-runner/Dockerfile).
  • Ansible – Playbooks in ansible/playbooks/ automate host‑level tasks like Docker login, package updates, and NTP sync.

The GitHub Actions CI (.github/workflows/) runs a Flux‑based GitOps pipeline that continuously reconciles the cluster with the manifests, while Trivy scans container images for CVEs.

How It Is Wired

Entry point: .github/workflows/CD.yml (GitHub Actions). The workflow checks out the repo, runs flux diff (via fluxcd/flux2/action@v2.9.0), applies changes with flux apply, and triggers Trivy (aquasecurity/trivy-action@v0.36.0).

Control flow:

  1. CI trigger → GitHub Actions job.
  2. Flux diff reads the kubernetes/ directory, resolves Kustomize bases (kustomization.yaml) and HelmRelease specs, then produces a manifest diff.
  3. Flux apply pushes the diff to the cluster’s GitOps controller, which creates/updates Kubernetes resources (Deployments, Services, ExternalSecrets).
  4. External‑Secrets controller (kubernetes/cluster/tofu-controller/app/external-secrets.yaml) fetches secrets from SOPS‑encrypted files or Terraform outputs.
  5. Terraform runs are invoked manually (or via a separate workflow) inside terraform/; the state is stored locally or in a remote backend.
  6. Docker compose files are independent of the GitOps loop; they are used to spin up local or edge services (docker/npm/docker-compose.yml).

Blast radius: The Flux controller is the hub; any change to a HelmRelease or Kustomization can affect multiple services. Terraform files are isolated per provider, so failures are scoped to the targeted cloud resource. Docker compose changes affect only the host where docker compose up is executed.

No circular import dependencies were detected; the static analysis reports 0 internal modules and 0 import edges.

How To Use It

# 1. Clone the repo
git clone https://github.com/moses-y/iac.git
cd iac

# 2. Set up SOPS key (required for encrypted secrets)
#    Place the key in the default location or export SOPS_KMS_ARN, etc.

# 3. Apply Terraform (example for Cloudflare)
cd terraform/cloudflare
terraform init
terraform apply   # approve plan interactively

# 4. Deploy the Kubernetes stack (GitOps)
#    The GitHub Actions workflow does this automatically on push.
#    To run locally:
flux install
flux reconcile source git iac --with-source
flux reconcile kustomization iac --with-source

# 5. Start edge services with Docker Compose
cd docker/jellyfin
docker compose up -d

Configuration files:

  • docker/secret-mappings.yml maps secret names to Docker‑compose variables.
  • kubernetes/apps/*/app/external-secret.yaml reference SOPS‑encrypted secret files.
  • terraform/*.tf contain provider blocks and required variables.

Real‑World Use

A homelab operator adds a new media server:

  1. Create a HelmRelease manifest in kubernetes/apps/media/plex/app/helmrelease.yaml.
  2. Commit the file; the CI workflow runs, Flux diff shows the new release, and Flux apply deploys Plex automatically.
  3. If Plex needs a DNS record, add it to terraform/cloudflare/plex.tf; run terraform apply to provision the record.

All three layers stay in sync without manual kubectl or docker commands.

Code Health & Issues

  • HIGH – GitHub Actions use version tags instead of commit SHAs (.github/workflows/*).
  • HIGHdocker/npm/docker-compose.yml runs containers in privileged mode with network_mode: host.
  • MEDIUM – Workflows do not declare least‑privilege permissions for GITHUB_TOKEN (.github/workflows/CD.yml).
  • MEDIUM – Base image in docker/Dockerfiles/actions-runner/Dockerfile is not pinned by digest.
  • LOW – No timeout-minutes defined on workflow jobs (.github/workflows/CD.yml).
  • SECURITY – Secret‑shaped paths appear in kubernetes/cluster/tofu-controller/app/external-secrets.yaml, kubernetes/components/volsync/external-secrets.yaml, and terraform/cloudflare/secrets.tf.

Other observations: tests exist (1 test file), a LICENSE is present, but no lockfile for Docker images; documentation is limited to README and a handful of Markdown files.

The Bottom Line

The repo delivers a well‑structured, GitOps‑driven homelab IaC stack that integrates Kubernetes, Terraform, Docker, and Ansible. It is functional out of the box, but the CI pipeline needs hardening (action pinning, privileged‑mode removal, token scoping) before it can be trusted in a production‑grade environment. Engineers comfortable with YAML‑centric pipelines will find it easy to extend; teams less familiar with GitOps may need to invest in Flux and SOPS onboarding.