Initial commit

This commit is contained in:
sarodz
2026-02-14 10:46:57 -05:00
commit 4426274448
30 changed files with 1149 additions and 0 deletions

284
README.md Normal file
View File

@@ -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 <YOUR_GIT_REPO_URL>
git push -u origin main
```
### 2. Replace placeholder values
Search for all `<YOUR_...>` placeholders and replace them with your values:
| Placeholder | File(s) | Description |
|---|---|---|
| `<YOUR_IP_RANGE>` | `infrastructure/metallb-config/ipaddresspool.yaml` | LAN IP range for LoadBalancer services, e.g. `192.168.1.200-192.168.1.210` |
| `<YOUR_LAN_CIDR>` | `infrastructure/traefik-install/traefik-override.yaml` | Your local network CIDR, e.g. `192.168.1.0/24` |
| `<YOUR_EMAIL>` | `infrastructure/cert-manager-issuer/issuer.yaml` | Email for Let's Encrypt notifications |
| `<YOUR_DNS_API_TOKEN>` | `infrastructure/cert-manager-issuer/secrets.yaml` | Cloudflare API token (see below) |
| `<YOUR_DOMAIN>` | `infrastructure/routes/gitea.yaml`, `apps/gitea/install/gitea.yaml` | e.g. `git.example.com` |
| `<YOUR_DB_PASSWORD>` | `apps/gitea/install/secrets.yaml`, `apps/gitea/install/gitea.yaml` | A strong password for PostgreSQL |
```bash
# Quick way to find all placeholders
grep -r '<YOUR_' --include='*.yaml' .
```
**Why is a Cloudflare API token needed?** Cert-manager uses [DNS-01 challenges](https://cert-manager.io/docs/configuration/acme/dns01/) to prove domain ownership to Let's Encrypt. Unlike HTTP-01 challenges, DNS-01 doesn't require your server to be publicly reachable — ideal for homelabs behind NAT. It works by automatically creating a DNS TXT record on your domain, which requires API access to your DNS provider. Create a Cloudflare API token with **Zone > 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 `<YOUR_AGE_PUBLIC_KEY>` 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=<YOUR_GIT_REPO_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://<YOUR_DOMAIN>"
```
## 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
```

68
apps/README.md Normal file
View File

@@ -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.

View File

@@ -0,0 +1,98 @@
# Gitea deployment.
# Replace <YOUR_DOMAIN> with your domain (e.g. git.example.com).
# Replace <YOUR_DB_PASSWORD> 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: <YOUR_DB_PASSWORD>
- name: GITEA__server__DOMAIN
value: <YOUR_DOMAIN>
- name: GITEA__server__ROOT_URL
value: https://<YOUR_DOMAIN>/
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 1000m
volumes:
- name: data
persistentVolumeClaim:
claimName: gitea-data

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: gitea
resources:
- secrets.yaml
- postgresql.yaml
- gitea.yaml

View File

@@ -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

View File

@@ -0,0 +1,14 @@
# PostgreSQL credentials.
# Replace <YOUR_DB_PASSWORD> 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: <YOUR_DB_PASSWORD>
POSTGRES_DB: gitea

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

4
bootstrap/ns/apps.yaml Normal file
View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: gitea

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: infrastructure

View File

@@ -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

View File

@@ -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

View File

@@ -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

209
docs/adding-an-app.md Normal file
View File

@@ -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

54
infrastructure/README.md Normal file
View File

@@ -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.

View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: cert-manager-chart-overrides
namespace: infrastructure
data:
values.yaml: |-
namespace: infrastructure
crds:
enabled: true

View File

@@ -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

View File

@@ -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 <YOUR_EMAIL> 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: <YOUR_EMAIL>
privateKeySecretRef:
name: letsencrypt-dns-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-credentials
key: api-token

View File

@@ -0,0 +1,13 @@
# DNS provider API token for cert-manager DNS-01 challenge.
# Replace <YOUR_DNS_API_TOKEN> 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: <YOUR_DNS_API_TOKEN>

View File

@@ -0,0 +1,13 @@
# MetalLB IP Address Pool
# Replace <YOUR_IP_RANGE> 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:
- <YOUR_IP_RANGE>
autoAssign: true

View File

@@ -0,0 +1,8 @@
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: metallb-l2
namespace: infrastructure
spec:
ipAddressPools:
- metallb-pool

View File

@@ -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

View File

@@ -0,0 +1,56 @@
# Gitea ingress routes via Traefik.
# Replace <YOUR_DOMAIN> 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(`<YOUR_DOMAIN>`)
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(`<YOUR_DOMAIN>`)
kind: Rule
services:
- name: gitea-http
namespace: gitea
port: 3000
tls:
secretName: gitea-tls
domains:
- main: <YOUR_DOMAIN>
sans:
- <YOUR_DOMAIN>

View File

@@ -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

View File

@@ -0,0 +1,25 @@
# Traefik Helm chart value overrides.
# Replace <YOUR_LAN_CIDR> 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:
- <YOUR_LAN_CIDR>
additionalArguments:
- "--api.dashboard=true"
- "--api.insecure=true"