From 442627444863967cdd31f549ec9982482b0cd564 Mon Sep 17 00:00:00 2001 From: sarodz Date: Sat, 14 Feb 2026 10:46:57 -0500 Subject: [PATCH] Initial commit --- README.md | 284 ++++++++++++++++++ apps/README.md | 68 +++++ apps/gitea/install/gitea.yaml | 98 ++++++ apps/gitea/install/kustomization.yaml | 7 + apps/gitea/install/postgresql.yaml | 56 ++++ apps/gitea/install/secrets.yaml | 14 + .../apps/gitea/gitea-install.yaml | 20 ++ .../infrastructure/cert-manager-install.yaml | 14 + .../infrastructure/cert-manager-issuer.yaml | 19 ++ .../infrastructure/metallb-config.yaml | 20 ++ .../infrastructure/metallb-install.yaml | 14 + .../kustomization/infrastructure/routes.yaml | 20 ++ .../infrastructure/traefik-install.yaml | 16 + bootstrap/ns/apps.yaml | 4 + bootstrap/ns/infrastructure.yaml | 4 + bootstrap/repositories/jetstack.yaml | 8 + bootstrap/repositories/metallb.yaml | 8 + bootstrap/repositories/traefik.yaml | 8 + docs/adding-an-app.md | 209 +++++++++++++ infrastructure/README.md | 54 ++++ .../cert-manager-install/cert-override.yaml | 10 + .../cert-manager-install/helmrelease.yaml | 19 ++ .../cert-manager-issuer/issuer.yaml | 22 ++ .../cert-manager-issuer/secrets.yaml | 13 + .../metallb-config/ipaddresspool.yaml | 13 + infrastructure/metallb-config/l2.yaml | 8 + .../metallb-install/helmrelease.yaml | 17 ++ infrastructure/routes/gitea.yaml | 56 ++++ .../traefik-install/helmrelease.yaml | 21 ++ .../traefik-install/traefik-override.yaml | 25 ++ 30 files changed, 1149 insertions(+) create mode 100644 README.md create mode 100644 apps/README.md create mode 100644 apps/gitea/install/gitea.yaml create mode 100644 apps/gitea/install/kustomization.yaml create mode 100644 apps/gitea/install/postgresql.yaml create mode 100644 apps/gitea/install/secrets.yaml create mode 100644 bootstrap/kustomization/apps/gitea/gitea-install.yaml create mode 100644 bootstrap/kustomization/infrastructure/cert-manager-install.yaml create mode 100644 bootstrap/kustomization/infrastructure/cert-manager-issuer.yaml create mode 100644 bootstrap/kustomization/infrastructure/metallb-config.yaml create mode 100644 bootstrap/kustomization/infrastructure/metallb-install.yaml create mode 100644 bootstrap/kustomization/infrastructure/routes.yaml create mode 100644 bootstrap/kustomization/infrastructure/traefik-install.yaml create mode 100644 bootstrap/ns/apps.yaml create mode 100644 bootstrap/ns/infrastructure.yaml create mode 100644 bootstrap/repositories/jetstack.yaml create mode 100644 bootstrap/repositories/metallb.yaml create mode 100644 bootstrap/repositories/traefik.yaml create mode 100644 docs/adding-an-app.md create mode 100644 infrastructure/README.md create mode 100644 infrastructure/cert-manager-install/cert-override.yaml create mode 100644 infrastructure/cert-manager-install/helmrelease.yaml create mode 100644 infrastructure/cert-manager-issuer/issuer.yaml create mode 100644 infrastructure/cert-manager-issuer/secrets.yaml create mode 100644 infrastructure/metallb-config/ipaddresspool.yaml create mode 100644 infrastructure/metallb-config/l2.yaml create mode 100644 infrastructure/metallb-install/helmrelease.yaml create mode 100644 infrastructure/routes/gitea.yaml create mode 100644 infrastructure/traefik-install/helmrelease.yaml create mode 100644 infrastructure/traefik-install/traefik-override.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..3888fb3 --- /dev/null +++ b/README.md @@ -0,0 +1,284 @@ +# Homelab Starter + +A minimal GitOps homelab setup using **K3s** and **Flux CD**. This starter gives you a working Kubernetes cluster with core infrastructure and one example app (Gitea), all managed declaratively through Git. Secrets are encrypted at rest in Git using **SOPS** with **age**. + +## Architecture + +``` + ┌──────────────────────────────────────────────────────────┐ + │ Git Repository │ + │ │ + │ bootstrap/ Infrastructure & apps/ │ + │ ├── ns/ app definitions └── gitea │ + │ ├── repositories/ are organized │ │ + │ └── kustomization/ into layers │ │ + └──────────┬─────────────────────────────────────┘ │ + │ │ + ▼ │ + ┌──────────────────┐ Flux watches your Git repo │ + │ Flux CD │ and continuously reconciles │ + │ (flux-system) │ the cluster state to match. │ + └──────────┬───────┘ │ + │ │ + ▼ │ + ┌──────────────────────────────────────────────────────────┐ + │ Kubernetes Cluster (K3s) │ + │ │ + │ Infrastructure layer Application layer │ + │ ┌────────────┐ ┌────────────┐ │ + │ │ MetalLB │─── IPs ──────▶ │ Gitea │ │ + │ │ Traefik │─── Routing ──▶ │ PostgreSQL │ │ + │ │ Cert-Mgr │─── TLS ─────▶ │ │ │ + │ └────────────┘ └────────────┘ │ + └──────────────────────────────────────────────────────────┘ +``` + +## What's Included + +| Component | Purpose | +|-----------|---------| +| **MetalLB** | Assigns real LAN IPs to LoadBalancer services | +| **Traefik** | Ingress controller — routes HTTP/HTTPS traffic to apps | +| **Cert-Manager** | Automatically provisions TLS certificates via Let's Encrypt | +| **Gitea** | Self-hosted Git service (example app) with PostgreSQL | + +## Prerequisites + +- A Linux machine (or VM) with [K3s](https://k3s.io) installed +- `kubectl` configured to talk to your cluster +- [Flux CLI](https://fluxcd.io/flux/installation/) installed locally +- [SOPS](https://github.com/getsops/sops) and [age](https://github.com/FiloSottile/age) installed locally +- A Git repository (GitHub, Gitea, GitLab, etc.) to host this code +- A domain name pointed at your cluster (for TLS certificates) +- A Cloudflare account (or other DNS provider) for DNS-01 challenges + +## Quick Start + +### 1. Copy this starter into your own repository + +```bash +# Create a new repo and copy the starter contents into it +# (include hidden files like .sops.yaml) +cp -r starter/{.,}* ~/my-homelab/ +cd ~/my-homelab +git init && git add -A && git commit -m "Initial homelab setup" +git remote add origin +git push -u origin main +``` + +### 2. Replace placeholder values + +Search for all `` placeholders and replace them with your values: + +| Placeholder | File(s) | Description | +|---|---|---| +| `` | `infrastructure/metallb-config/ipaddresspool.yaml` | LAN IP range for LoadBalancer services, e.g. `192.168.1.200-192.168.1.210` | +| `` | `infrastructure/traefik-install/traefik-override.yaml` | Your local network CIDR, e.g. `192.168.1.0/24` | +| `` | `infrastructure/cert-manager-issuer/issuer.yaml` | Email for Let's Encrypt notifications | +| `` | `infrastructure/cert-manager-issuer/secrets.yaml` | Cloudflare API token (see below) | +| `` | `infrastructure/routes/gitea.yaml`, `apps/gitea/install/gitea.yaml` | e.g. `git.example.com` | +| `` | `apps/gitea/install/secrets.yaml`, `apps/gitea/install/gitea.yaml` | A strong password for PostgreSQL | + +```bash +# Quick way to find all placeholders +grep -r ' DNS > Edit** permission for your domain at [dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens). + +### 3. Set up secret encryption (SOPS) + +Secrets are stored in dedicated `secrets.yaml` files and must be encrypted before committing to Git. This starter uses [SOPS](https://github.com/getsops/sops) with [age](https://github.com/FiloSottile/age) encryption so that Flux can decrypt them in-cluster while they stay encrypted at rest in your repository. + +#### Generate an age key pair + +```bash +age-keygen -o age.agekey +# Output: Public key: age1xxxxxxxxx... +``` + +> **Important:** Add `age.agekey` to `.gitignore` — never commit your private key. + +#### Update `.sops.yaml` + +Replace `` in `.sops.yaml` with the public key from the previous step. This file tells SOPS which files to encrypt and which YAML fields to target: + +```yaml +creation_rules: + - path_regex: infrastructure/.*/secrets\.yaml$ + age: age1xxxxxxxxx... # your public key + encrypted_regex: "^(data|stringData)$" + - path_regex: apps/.*/secrets\.yaml$ + age: age1xxxxxxxxx... # your public key + encrypted_regex: "^(data|stringData)$" +``` + +Only `data` and `stringData` fields are encrypted — metadata stays readable so Git diffs remain useful. + +#### Encrypt your secret files + +After filling in your actual values (step 2), encrypt each secrets file: + +```bash +sops --encrypt --in-place infrastructure/cert-manager-issuer/secrets.yaml +sops --encrypt --in-place apps/gitea/install/secrets.yaml +``` + +To edit an encrypted file later: + +```bash +sops infrastructure/cert-manager-issuer/secrets.yaml +``` + +#### Load the private key into the cluster + +Flux needs the age private key to decrypt secrets during reconciliation: + +```bash +kubectl create namespace flux-system + +kubectl create secret generic sops-age \ + --namespace=flux-system \ + --from-file=age.agekey=age.agekey +``` + +The Flux Kustomizations in this starter already reference this secret via `decryption.secretRef.name: sops-age`. + +### 4. Bootstrap Flux + +```bash +flux bootstrap git \ + --url= \ + --branch=main \ + --path=bootstrap +``` + +This command: +- Installs Flux components into the `flux-system` namespace +- Creates `bootstrap/flux-system/` with auto-generated Flux manifests +- Commits and pushes these files to your repo +- Starts the reconciliation loop + +### 5. Watch Flux reconcile + +```bash +# Watch all kustomizations converge +flux get kustomizations --watch + +# Check pod status across all namespaces +kubectl get pods -A +``` + +### 6. Access Gitea + +Once everything is reconciled and the TLS certificate is issued: + +```bash +# Check the certificate status +kubectl get certificate -n gitea + +# Open Gitea in your browser +echo "https://" +``` + +## Directory Structure + +``` +.sops.yaml # SOPS encryption rules (which files, which fields) + +bootstrap/ # Flux bootstrap layer +├── flux-system/ # Auto-generated by `flux bootstrap` (don't edit) +├── kustomization/ # Flux Kustomization CRs — tell Flux what to deploy +│ ├── infrastructure/ # One file per infrastructure component +│ └── apps/ # One directory per app +│ └── gitea/ +├── ns/ # Namespace definitions +└── repositories/ # Helm chart repository sources + +infrastructure/ # Core cluster services +├── metallb-install/ # MetalLB Helm release +├── metallb-config/ # MetalLB IP pool and L2 advertisement +├── traefik-install/ # Traefik Helm release + config overrides +├── cert-manager-install/ # cert-manager Helm release + config overrides +├── cert-manager-issuer/ # Let's Encrypt ClusterIssuer + DNS credentials +│ ├── issuer.yaml +│ └── secrets.yaml # encrypted with SOPS +└── routes/ # Traefik IngressRoutes (one per app) + +apps/ # Application deployments +└── gitea/ + └── install/ # Gitea + PostgreSQL manifests + ├── gitea.yaml + ├── postgresql.yaml + └── secrets.yaml # encrypted with SOPS +``` + +## How Flux Works + +Flux follows a **reconciliation loop**: + +1. **Source Controller** watches your Git repository for changes +2. **Kustomize Controller** applies Kustomization CRs (in `bootstrap/kustomization/`) +3. **Helm Controller** installs/upgrades Helm releases (MetalLB, Traefik, Cert-Manager) +4. If you push a change to Git, Flux detects it and updates the cluster automatically + +The `dependsOn` fields in the Flux Kustomizations ensure things install in the right order: + +``` +MetalLB Install → MetalLB Config → Traefik Install → Routes +Cert-Manager Install → Cert-Manager Issuer +Traefik Install → Gitea Install +``` + +Kustomizations that reference encrypted secrets include a `decryption` block pointing to the `sops-age` secret, so Flux can decrypt them transparently during apply. + +## Next Steps + +- **Add a backup system** — Set up Restic + Rclone to back up your PVCs and databases to cloud storage. +- **Add your second app** — See [`docs/adding-an-app.md`](docs/adding-an-app.md) for a step-by-step walkthrough. + +## Troubleshooting + +**Flux not reconciling?** +```bash +flux logs --level=error +flux get sources git # Check if Flux can reach your repo +flux reconcile source git flux-system # Force a sync +``` + +**MetalLB not assigning IPs?** +```bash +kubectl get ipaddresspool -n infrastructure +kubectl get svc -n infrastructure # Check if Traefik has an EXTERNAL-IP +``` + +**Traefik not routing?** +```bash +kubectl get ingressroute -A # Check if routes exist +kubectl logs -n infrastructure -l app.kubernetes.io/name=traefik +``` + +**TLS certificate not issuing?** +```bash +kubectl get certificate -A # Check certificate status +kubectl get certificaterequest -A # Check pending requests +kubectl describe clusterissuer cert-issuer # Check issuer health +kubectl logs -n infrastructure -l app=cert-manager +``` + +**SOPS decryption failing?** +```bash +# Check if the sops-age secret exists +kubectl get secret sops-age -n flux-system + +# Check Flux kustomization status for decryption errors +flux get kustomizations +kubectl describe kustomization cert-manager-issuer--infra -n flux-system +``` + +**Gitea not starting?** +```bash +kubectl get pods -n gitea # Check pod status +kubectl logs -n gitea -l app=gitea # Check Gitea logs +kubectl logs -n gitea -l app=postgresql # Check DB logs +``` diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 0000000..61c3e66 --- /dev/null +++ b/apps/README.md @@ -0,0 +1,68 @@ +# Apps + +Application deployments managed by Flux. Each app lives in its own directory with a Kustomize-based layout. + +## Gitea (Example App) + +The included Gitea deployment consists of: + +| File | Contents | +|------|----------| +| `gitea/install/kustomization.yaml` | Lists the resources Flux should apply | +| `gitea/install/postgresql.yaml` | PostgreSQL Secret, Service, and StatefulSet | +| `gitea/install/gitea.yaml` | Gitea PVC, HTTP/SSH Services, and Deployment | + +The IngressRoute for Gitea lives in `infrastructure/routes/gitea.yaml` (routes are managed at the infrastructure layer). + +## Adding Your Own App + +Here's the checklist for adding a new app. For a full walkthrough with example files, see [`../docs/adding-an-app.md`](../docs/adding-an-app.md). + +### 1. Create a namespace + +Add your namespace to `bootstrap/ns/apps.yaml`: + +```yaml +--- +apiVersion: v1 +kind: Namespace +metadata: + name: my-app +``` + +### 2. Create app manifests + +Create `apps/my-app/install/` with: +- `kustomization.yaml` listing your resource files +- Your Kubernetes manifests (Deployments, Services, PVCs, Secrets, etc.) + +### 3. Create a Flux Kustomization + +Add `bootstrap/kustomization/apps/my-app/my-app-install.yaml`: + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: install-my-app--app + namespace: flux-system +spec: + interval: 5m + timeout: 4m + dependsOn: + - name: install-traefik--infra + path: ./apps/my-app/install + prune: true + wait: true + sourceRef: + kind: GitRepository + name: flux-system +``` + +### 4. Create an IngressRoute + +Add `infrastructure/routes/my-app.yaml` with your Traefik IngressRoute (use `gitea.yaml` as a template). + +### 5. Commit and push + +Flux will detect the changes and deploy your app automatically. diff --git a/apps/gitea/install/gitea.yaml b/apps/gitea/install/gitea.yaml new file mode 100644 index 0000000..762af53 --- /dev/null +++ b/apps/gitea/install/gitea.yaml @@ -0,0 +1,98 @@ +# Gitea deployment. +# Replace with your domain (e.g. git.example.com). +# Replace with the same password used in postgresql.yaml. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: gitea-data + namespace: gitea +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: gitea-http + namespace: gitea +spec: + type: ClusterIP + ports: + - port: 3000 + targetPort: 3000 + selector: + app: gitea +--- +apiVersion: v1 +kind: Service +metadata: + name: gitea-ssh + namespace: gitea +spec: + type: ClusterIP + ports: + - port: 22 + targetPort: 22 + selector: + app: gitea +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gitea + namespace: gitea +spec: + replicas: 1 + selector: + matchLabels: + app: gitea + template: + metadata: + labels: + app: gitea + spec: + initContainers: + - name: wait-for-db + image: busybox:1.36 + command: ['sh', '-c', 'until nc -z postgresql 5432; do sleep 2; done'] + containers: + - name: gitea + image: gitea/gitea:1.23 + ports: + - containerPort: 3000 + name: http + - containerPort: 22 + name: ssh + env: + - name: GITEA__database__DB_TYPE + value: postgres + - name: GITEA__database__HOST + value: postgresql:5432 + - name: GITEA__database__NAME + value: gitea + - name: GITEA__database__USER + value: gitea + - name: GITEA__database__PASSWD + value: + - name: GITEA__server__DOMAIN + value: + - name: GITEA__server__ROOT_URL + value: https:/// + volumeMounts: + - name: data + mountPath: /data + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 1Gi + cpu: 1000m + volumes: + - name: data + persistentVolumeClaim: + claimName: gitea-data diff --git a/apps/gitea/install/kustomization.yaml b/apps/gitea/install/kustomization.yaml new file mode 100644 index 0000000..919e7d0 --- /dev/null +++ b/apps/gitea/install/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: gitea +resources: + - secrets.yaml + - postgresql.yaml + - gitea.yaml diff --git a/apps/gitea/install/postgresql.yaml b/apps/gitea/install/postgresql.yaml new file mode 100644 index 0000000..9064d15 --- /dev/null +++ b/apps/gitea/install/postgresql.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgresql + namespace: gitea +spec: + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + selector: + app: postgresql +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgresql + namespace: gitea +spec: + serviceName: postgresql + replicas: 1 + selector: + matchLabels: + app: postgresql + template: + metadata: + labels: + app: postgresql + spec: + containers: + - name: postgresql + image: postgres:17-alpine + ports: + - containerPort: 5432 + envFrom: + - secretRef: + name: postgresql-credentials + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 512Mi + cpu: 500m + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: local-path + resources: + requests: + storage: 5Gi diff --git a/apps/gitea/install/secrets.yaml b/apps/gitea/install/secrets.yaml new file mode 100644 index 0000000..b74af1c --- /dev/null +++ b/apps/gitea/install/secrets.yaml @@ -0,0 +1,14 @@ +# PostgreSQL credentials. +# Replace with a strong password. +# +# Encrypt this file with: sops --encrypt --in-place secrets.yaml +apiVersion: v1 +kind: Secret +metadata: + name: postgresql-credentials + namespace: gitea +type: Opaque +stringData: + POSTGRES_USER: gitea + POSTGRES_PASSWORD: + POSTGRES_DB: gitea diff --git a/bootstrap/kustomization/apps/gitea/gitea-install.yaml b/bootstrap/kustomization/apps/gitea/gitea-install.yaml new file mode 100644 index 0000000..0ad12de --- /dev/null +++ b/bootstrap/kustomization/apps/gitea/gitea-install.yaml @@ -0,0 +1,20 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: install-gitea--app + namespace: flux-system +spec: + interval: 5m + timeout: 4m + dependsOn: + - name: install-traefik--infra + path: ./apps/gitea/install + prune: true + wait: true + sourceRef: + kind: GitRepository + name: flux-system + decryption: + provider: sops + secretRef: + name: sops-age diff --git a/bootstrap/kustomization/infrastructure/cert-manager-install.yaml b/bootstrap/kustomization/infrastructure/cert-manager-install.yaml new file mode 100644 index 0000000..be7007a --- /dev/null +++ b/bootstrap/kustomization/infrastructure/cert-manager-install.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cert-manager-install--infra + namespace: flux-system +spec: + interval: 10m + timeout: 2m + path: ./infrastructure/cert-manager-install + prune: true + wait: true + sourceRef: + kind: GitRepository + name: flux-system diff --git a/bootstrap/kustomization/infrastructure/cert-manager-issuer.yaml b/bootstrap/kustomization/infrastructure/cert-manager-issuer.yaml new file mode 100644 index 0000000..978d995 --- /dev/null +++ b/bootstrap/kustomization/infrastructure/cert-manager-issuer.yaml @@ -0,0 +1,19 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cert-manager-issuer--infra + namespace: flux-system +spec: + interval: 1m + timeout: 2m + dependsOn: + - name: cert-manager-install--infra + path: ./infrastructure/cert-manager-issuer + prune: true + sourceRef: + kind: GitRepository + name: flux-system + decryption: + provider: sops + secretRef: + name: sops-age diff --git a/bootstrap/kustomization/infrastructure/metallb-config.yaml b/bootstrap/kustomization/infrastructure/metallb-config.yaml new file mode 100644 index 0000000..b9ddf6f --- /dev/null +++ b/bootstrap/kustomization/infrastructure/metallb-config.yaml @@ -0,0 +1,20 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: config-metallb--infra + namespace: flux-system +spec: + interval: 5m + timeout: 2m + dependsOn: + - name: install-metallb--infra + path: ./infrastructure/metallb-config + prune: true + sourceRef: + kind: GitRepository + name: flux-system + healthChecks: + - apiVersion: apps/v1 + kind: Deployment + name: metallb-controller + namespace: infrastructure diff --git a/bootstrap/kustomization/infrastructure/metallb-install.yaml b/bootstrap/kustomization/infrastructure/metallb-install.yaml new file mode 100644 index 0000000..cd03959 --- /dev/null +++ b/bootstrap/kustomization/infrastructure/metallb-install.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: install-metallb--infra + namespace: flux-system +spec: + interval: 10m + timeout: 5m + path: ./infrastructure/metallb-install + prune: true + wait: true + sourceRef: + kind: GitRepository + name: flux-system diff --git a/bootstrap/kustomization/infrastructure/routes.yaml b/bootstrap/kustomization/infrastructure/routes.yaml new file mode 100644 index 0000000..b367cb9 --- /dev/null +++ b/bootstrap/kustomization/infrastructure/routes.yaml @@ -0,0 +1,20 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: routing--infra + namespace: flux-system +spec: + interval: 2m + timeout: 2m + dependsOn: + - name: install-traefik--infra + path: ./infrastructure/routes + prune: true + sourceRef: + kind: GitRepository + name: flux-system + healthChecks: + - apiVersion: apps/v1 + kind: Deployment + name: traefik + namespace: infrastructure diff --git a/bootstrap/kustomization/infrastructure/traefik-install.yaml b/bootstrap/kustomization/infrastructure/traefik-install.yaml new file mode 100644 index 0000000..01b670d --- /dev/null +++ b/bootstrap/kustomization/infrastructure/traefik-install.yaml @@ -0,0 +1,16 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: install-traefik--infra + namespace: flux-system +spec: + interval: 10m + timeout: 5m + dependsOn: + - name: config-metallb--infra + path: ./infrastructure/traefik-install + prune: true + wait: true + sourceRef: + kind: GitRepository + name: flux-system diff --git a/bootstrap/ns/apps.yaml b/bootstrap/ns/apps.yaml new file mode 100644 index 0000000..09a988f --- /dev/null +++ b/bootstrap/ns/apps.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: gitea diff --git a/bootstrap/ns/infrastructure.yaml b/bootstrap/ns/infrastructure.yaml new file mode 100644 index 0000000..d3757ad --- /dev/null +++ b/bootstrap/ns/infrastructure.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: infrastructure diff --git a/bootstrap/repositories/jetstack.yaml b/bootstrap/repositories/jetstack.yaml new file mode 100644 index 0000000..f3d2527 --- /dev/null +++ b/bootstrap/repositories/jetstack.yaml @@ -0,0 +1,8 @@ +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: jetstack + namespace: flux-system +spec: + interval: 60m + url: https://charts.jetstack.io diff --git a/bootstrap/repositories/metallb.yaml b/bootstrap/repositories/metallb.yaml new file mode 100644 index 0000000..28f9fe9 --- /dev/null +++ b/bootstrap/repositories/metallb.yaml @@ -0,0 +1,8 @@ +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: metallb + namespace: flux-system +spec: + interval: 60m + url: https://metallb.github.io/metallb diff --git a/bootstrap/repositories/traefik.yaml b/bootstrap/repositories/traefik.yaml new file mode 100644 index 0000000..89c8c99 --- /dev/null +++ b/bootstrap/repositories/traefik.yaml @@ -0,0 +1,8 @@ +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: traefik + namespace: flux-system +spec: + interval: 60m + url: https://traefik.github.io/charts diff --git a/docs/adding-an-app.md b/docs/adding-an-app.md new file mode 100644 index 0000000..51186aa --- /dev/null +++ b/docs/adding-an-app.md @@ -0,0 +1,209 @@ +# Adding an App: Step-by-Step + +This guide walks through adding a new app to your cluster, using a simple `whoami` test service as an example. By the end, you'll have a working app accessible at `https://whoami.example.com`. + +## Overview + +Adding an app requires touching 4 places: + +1. **Namespace** — `bootstrap/ns/apps.yaml` +2. **App manifests** — `apps/whoami/install/` +3. **Flux Kustomization** — `bootstrap/kustomization/apps/whoami/` +4. **IngressRoute** — `infrastructure/routes/whoami.yaml` + +## Step 1: Create the namespace + +Edit `bootstrap/ns/apps.yaml` and add: + +```yaml +--- +apiVersion: v1 +kind: Namespace +metadata: + name: whoami +``` + +## Step 2: Create the app manifests + +Create `apps/whoami/install/kustomization.yaml`: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: whoami +resources: + - deployment.yaml +``` + +Create `apps/whoami/install/deployment.yaml`: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: whoami + namespace: whoami +spec: + replicas: 1 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami:latest + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: whoami +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + selector: + app: whoami +``` + +## Step 3: Create the Flux Kustomization + +Create `bootstrap/kustomization/apps/whoami/whoami-install.yaml`: + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: install-whoami--app + namespace: flux-system +spec: + interval: 5m + timeout: 4m + dependsOn: + - name: install-traefik--infra + path: ./apps/whoami/install + prune: true + wait: true + sourceRef: + kind: GitRepository + name: flux-system +``` + +## Step 4: Create the IngressRoute + +Create `infrastructure/routes/whoami.yaml`: + +```yaml +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: redirect-https + namespace: whoami +spec: + redirectScheme: + scheme: https + permanent: true +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: whoami-ingress-http + namespace: whoami +spec: + entryPoints: + - web + routes: + - match: Host(`whoami.example.com`) + kind: Rule + middlewares: + - name: redirect-https + services: + - name: whoami + namespace: whoami + port: 80 +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: whoami-ingress + namespace: whoami + annotations: + cert-manager.io/issuer: "cert-issuer" +spec: + entryPoints: + - websecure + routes: + - match: Host(`whoami.example.com`) + kind: Rule + services: + - name: whoami + namespace: whoami + port: 80 + tls: + secretName: whoami-tls + domains: + - main: whoami.example.com + sans: + - whoami.example.com +``` + +## Step 5: Commit and push + +```bash +git add -A +git commit -m "Add whoami app" +git push +``` + +Flux will automatically detect the change and deploy your app. Watch it happen: + +```bash +# Watch Flux pick up the change +flux get kustomizations --watch + +# Verify the pod is running +kubectl get pods -n whoami + +# Test it +curl https://whoami.example.com +``` + +## Using a Helm Chart Instead + +If your app has a Helm chart, the pattern is slightly different: + +1. Add a HelmRepository in `bootstrap/repositories/` pointing to the chart source +2. In your app's install directory, use a HelmRelease instead of raw manifests: + +```yaml +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: my-app-release + namespace: my-app +spec: + chart: + spec: + chart: my-app + version: 1.0.0 + sourceRef: + kind: HelmRepository + name: my-app-repo + namespace: flux-system + interval: 15m + releaseName: my-app + valuesFrom: + - kind: ConfigMap + name: my-app-chart-overrides + valuesKey: values.yaml +``` + +3. Create a ConfigMap with your chart value overrides (same pattern as Traefik/MetalLB) +4. Everything else (namespace, Flux Kustomization, IngressRoute) stays the same diff --git a/infrastructure/README.md b/infrastructure/README.md new file mode 100644 index 0000000..54d5f40 --- /dev/null +++ b/infrastructure/README.md @@ -0,0 +1,54 @@ +# Infrastructure + +Core cluster services that apps depend on. These are installed before any apps via Flux `dependsOn` ordering. + +## Dependency Chain + +``` +MetalLB Install ──▶ MetalLB Config ──▶ Traefik Install ──▶ Routes + │ +Cert-Manager Install ──▶ Cert-Manager Issuer │ + │ + Apps depend on ─┘ +``` + +## Components + +| Directory | What it does | +|-----------|-------------| +| `metallb-install/` | Installs MetalLB via Helm — gives LoadBalancer services real LAN IPs | +| `metallb-config/` | Configures the IP address pool and L2 advertisement | +| `traefik-install/` | Installs Traefik via Helm — reverse proxy and ingress controller | +| `cert-manager-install/` | Installs cert-manager via Helm — automates TLS certificate provisioning | +| `cert-manager-issuer/` | Configures Let's Encrypt ClusterIssuer with DNS-01 challenge | +| `routes/` | Traefik IngressRoutes — one file per app defining how traffic reaches it | + +## How Helm Releases Work Here + +Each Helm-based service follows the same pattern: + +1. **HelmRelease** (`helmrelease.yaml`) — Points to a chart and version from a HelmRepository defined in `bootstrap/repositories/` +2. **ConfigMap override** (`*-override.yaml`) — Contains chart values as a YAML string under `data.values.yaml`. Referenced via `valuesFrom` in the HelmRelease. + +This pattern keeps chart values separate from the release definition, making them easier to review and modify. + +## Adding a New Infrastructure Service + +1. Create a HelmRepository in `bootstrap/repositories/` (if the chart source is new) +2. Create a directory under `infrastructure/` (e.g. `infrastructure/my-service-install/`) +3. Add a `helmrelease.yaml` and optionally a ConfigMap override +4. Create a Flux Kustomization in `bootstrap/kustomization/infrastructure/` pointing to your new directory +5. Set `dependsOn` appropriately (most infra services should depend on MetalLB being configured) +6. Commit and push — Flux handles the rest + +## Adding SOPS Secret Encryption + +The `cert-manager-issuer/secret.yaml` file currently contains a plain-text secret. To encrypt it: + +1. Install [age](https://github.com/FiloSottile/age) and generate a key pair +2. Create a `.sops.yaml` at the repo root with creation rules for your paths +3. Encrypt secret files: `sops --encrypt --in-place infrastructure/cert-manager-issuer/secret.yaml` +4. Add `spec.decryption` to the relevant Flux Kustomizations in `bootstrap/kustomization/` +5. Create a `sops-age` Secret in `flux-system` namespace with your age private key + +See the [Flux SOPS guide](https://fluxcd.io/flux/guides/mozilla-sops/) for full instructions. diff --git a/infrastructure/cert-manager-install/cert-override.yaml b/infrastructure/cert-manager-install/cert-override.yaml new file mode 100644 index 0000000..e72eaf6 --- /dev/null +++ b/infrastructure/cert-manager-install/cert-override.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: cert-manager-chart-overrides + namespace: infrastructure +data: + values.yaml: |- + namespace: infrastructure + crds: + enabled: true diff --git a/infrastructure/cert-manager-install/helmrelease.yaml b/infrastructure/cert-manager-install/helmrelease.yaml new file mode 100644 index 0000000..1fa71c3 --- /dev/null +++ b/infrastructure/cert-manager-install/helmrelease.yaml @@ -0,0 +1,19 @@ +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: cert-manager-release + namespace: infrastructure +spec: + chart: + spec: + chart: cert-manager + version: 1.19.2 + sourceRef: + kind: HelmRepository + name: jetstack + namespace: flux-system + interval: 10m + valuesFrom: + - kind: ConfigMap + name: cert-manager-chart-overrides + valuesKey: values.yaml diff --git a/infrastructure/cert-manager-issuer/issuer.yaml b/infrastructure/cert-manager-issuer/issuer.yaml new file mode 100644 index 0000000..63c5c89 --- /dev/null +++ b/infrastructure/cert-manager-issuer/issuer.yaml @@ -0,0 +1,22 @@ +# ClusterIssuer for Let's Encrypt using DNS-01 challenge. +# This example uses Cloudflare as the DNS provider. If you use a different +# provider, see: https://cert-manager.io/docs/configuration/acme/dns01/ +# +# Replace with your email address for Let's Encrypt notifications. +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: cert-issuer + namespace: infrastructure +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: + privateKeySecretRef: + name: letsencrypt-dns-key + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-credentials + key: api-token diff --git a/infrastructure/cert-manager-issuer/secrets.yaml b/infrastructure/cert-manager-issuer/secrets.yaml new file mode 100644 index 0000000..fc6be79 --- /dev/null +++ b/infrastructure/cert-manager-issuer/secrets.yaml @@ -0,0 +1,13 @@ +# DNS provider API token for cert-manager DNS-01 challenge. +# Replace with your Cloudflare API token +# (or adjust for your DNS provider). +# +# Encrypt this file with: sops --encrypt --in-place secrets.yaml +apiVersion: v1 +kind: Secret +metadata: + name: cloudflare-credentials + namespace: infrastructure +type: Opaque +stringData: + api-token: diff --git a/infrastructure/metallb-config/ipaddresspool.yaml b/infrastructure/metallb-config/ipaddresspool.yaml new file mode 100644 index 0000000..7097493 --- /dev/null +++ b/infrastructure/metallb-config/ipaddresspool.yaml @@ -0,0 +1,13 @@ +# MetalLB IP Address Pool +# Replace with a range of unused IPs on your LAN. +# These IPs will be assigned to LoadBalancer services (e.g. Traefik). +# Example: 192.168.1.200-192.168.1.210 +apiVersion: metallb.io/v1beta1 +kind: IPAddressPool +metadata: + name: metallb-pool + namespace: infrastructure +spec: + addresses: + - + autoAssign: true diff --git a/infrastructure/metallb-config/l2.yaml b/infrastructure/metallb-config/l2.yaml new file mode 100644 index 0000000..272f358 --- /dev/null +++ b/infrastructure/metallb-config/l2.yaml @@ -0,0 +1,8 @@ +apiVersion: metallb.io/v1beta1 +kind: L2Advertisement +metadata: + name: metallb-l2 + namespace: infrastructure +spec: + ipAddressPools: + - metallb-pool diff --git a/infrastructure/metallb-install/helmrelease.yaml b/infrastructure/metallb-install/helmrelease.yaml new file mode 100644 index 0000000..fc96fa1 --- /dev/null +++ b/infrastructure/metallb-install/helmrelease.yaml @@ -0,0 +1,17 @@ +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: metallb-release + namespace: infrastructure +spec: + chart: + spec: + chart: metallb + version: 0.14.9 + sourceRef: + kind: HelmRepository + name: metallb + namespace: flux-system + interval: 15m + timeout: 10m + releaseName: metallb diff --git a/infrastructure/routes/gitea.yaml b/infrastructure/routes/gitea.yaml new file mode 100644 index 0000000..f6903df --- /dev/null +++ b/infrastructure/routes/gitea.yaml @@ -0,0 +1,56 @@ +# Gitea ingress routes via Traefik. +# Replace with your domain (e.g. git.example.com). +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: redirect-https + namespace: gitea +spec: + redirectScheme: + scheme: https + permanent: true +--- +# HTTP entrypoint — redirects all traffic to HTTPS +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: gitea-ingress-http + namespace: gitea +spec: + entryPoints: + - web + routes: + - match: Host(``) + kind: Rule + middlewares: + - name: redirect-https + services: + - name: gitea-http + namespace: gitea + port: 3000 +--- +# HTTPS entrypoint — serves Gitea with TLS +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: gitea-ingress + namespace: gitea + annotations: + cert-manager.io/issuer: "cert-issuer" +spec: + entryPoints: + - websecure + routes: + - match: Host(``) + kind: Rule + services: + - name: gitea-http + namespace: gitea + port: 3000 + tls: + secretName: gitea-tls + domains: + - main: + sans: + - diff --git a/infrastructure/traefik-install/helmrelease.yaml b/infrastructure/traefik-install/helmrelease.yaml new file mode 100644 index 0000000..6b5d08d --- /dev/null +++ b/infrastructure/traefik-install/helmrelease.yaml @@ -0,0 +1,21 @@ +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: traefik-release + namespace: infrastructure +spec: + chart: + spec: + chart: traefik + version: 39.0.0 + sourceRef: + kind: HelmRepository + name: traefik + namespace: flux-system + interval: 15m + timeout: 10m + releaseName: traefik + valuesFrom: + - kind: ConfigMap + name: traefik-chart-overrides + valuesKey: values.yaml diff --git a/infrastructure/traefik-install/traefik-override.yaml b/infrastructure/traefik-install/traefik-override.yaml new file mode 100644 index 0000000..af501fd --- /dev/null +++ b/infrastructure/traefik-install/traefik-override.yaml @@ -0,0 +1,25 @@ +# Traefik Helm chart value overrides. +# Replace with your local network range (e.g. 192.168.1.0/24). +apiVersion: v1 +kind: ConfigMap +metadata: + name: traefik-chart-overrides + namespace: infrastructure +data: + values.yaml: |- + deployment: + enabled: true + replicas: 1 + ingressClass: + enabled: true + isDefaultClass: true + service: + type: LoadBalancer + ports: + websecure: + forwardedHeaders: + trustedIPs: + - + additionalArguments: + - "--api.dashboard=true" + - "--api.insecure=true"