This repo was initially built to teach me Docker and Compose skills as part of my self-hosting and homelab journey. But it has since evolved into a Kubernetes and Ansible learning project with a focus on production-like patterns and practices.
I chose a vanilla approach with kubeadm instead of something like k3s or talos because I am studying for the CKA and want to understand the underlying components and processes. I also want to practice building out the cluster with Ansible and then managing it through GitOps.
The cluster gets bootstrapped through Ansible and then fully reconciled through ArgoCD. The GitOps repo is the single source of truth for cluster state after bootstrap. Since I'm studying for the CKA, the cluster is built with kubeadm to stay vanilla. I use Longhorn for replicated block storage on local disks, MetalLB for as an L2 load balancer, Calico for networking, and the control-plane is highly available with kube-vip.
The cluster has three layers. The platform layer includes the CNI, load balancer, ingress controller, cert manager, and storage operator. The shared services layer includes databases and messaging systems used by multiple applications. The application layer includes user-facing workloads like the CMS, PocketBase, Gitea forge, n8n workflow automation, ntfy notification system, and a placeholder dev-pages nginx deployment.
---
config:
theme: 'forest'
---
graph TB
subgraph K8s_Cluster ["Kubernetes Cluster (kubeadm)"]
direction TB
subgraph Ingress_Layer ["Ingress & LB Layer"]
MLB["MetalLB (L2 LoadBalancer)"]
TRA["Traefik (Ingress Controller)"]
CM["cert-manager"]
end
subgraph Storage_Layer ["Storage Layer"]
LH["Longhorn (Distributed Block Storage)"]
LH_PVCs[("Replicated Volumes")]
end
subgraph Shared_Services ["Shared Services Layer"]
PG[(PostgreSQL)]
RMQ[[RabbitMQ]]
RED[(Redis)]
KFK[[Kafka]]
CAS[(Cassandra)]
end
subgraph Apps ["Application Layer"]
CMS["Strapi (dev-cms)"]
PKT["PocketBase (dev-pocket)"]
FORGE["Gitea (ops-forge)"]
N8N["n8n (srv-n8n)"]
NTFY["ntfy (srv-ntfy)"]
DP["dev-pages (nginx)"]
end
end
%% Connections
User -->|HTTPS| MLB
MLB --> TRA
TRA --> CMS & PKT & FORGE & N8N & NTFY & DP
CMS & PKT & FORGE & N8N --> Shared_Services
Shared_Services & Apps --- LH
LH --- LH_PVCs
Inventory is defined in playbooks/inventory
Each host declares:
ansible_host: SSH targetmanagement_ip: node IP used by kubeadm and kubeletstorage_disks: Longhorn disk paths and tags
Cluster-wide variables declare:
api_virtual_ip: shared API endpoint managed by kube-vipapi_virtual_ip_interface: interface advertising the VIPgitops_repo_url,gitops_revision,gitops_overlay_path: ArgoCD source of truthmetallb_address_pool,longhorn_backup_target,longhorn_backup_endpoint: platform defaults
- Update the inventory with the real control-plane nodes, worker nodes, management IPs, storage disk paths, and API VIP.
- Run the supported bootstrap playbook.
- Wait for ArgoCD to reconcile the platform, shared services, and application layers.
ansible-playbook -i playbooks/inventory ansible/deploy-k8s.ymlansible/deploy-k8s.yml does only three things:
- prepares the nodes and bootstraps an HA kubeadm control plane
- joins workers through the stable API VIP
- installs Calico and ArgoCD, then seeds the root application
The GitOps entrypoint is k8s/overlays/dev.
- k8s/base/platform: ingress, certs, load balancing, storage, operators, RBAC, and baseline platform defaults
- k8s/base/shared-services: PostgreSQL, RabbitMQ, Redis, Kafka, and Cassandra managed through maintained charts and operators
- k8s/base/applications: migrated user-facing workloads, currently
dev-pages,srv-ntfy,dev-cms,dev-pocket,ops-forge, andsrv-n8n
The overlay creates three ArgoCD projects and three layer applications so reconciliation order is explicit.
- MetalLB L2 pool is
192.168.1.240-192.168.1.250 - Longhorn default replica count
3with recurring snapshot and backup jobs - Traefik is default ingress class with HTTPS redirect and security headers
- Cert-manager cluster issuers for self-signed bootstrap and Let’s Encrypt
- Readonly RBAC for operators and default deny network policies for shared-service and application namespaces
- Pod Disruption Budgets for Traefik, Redis, RabbitMQ, and the stateless web workloads
Common tasks are documented in these runbooks: