Initial commit
This commit is contained in:
284
README.md
Normal file
284
README.md
Normal 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
68
apps/README.md
Normal 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.
|
||||||
98
apps/gitea/install/gitea.yaml
Normal file
98
apps/gitea/install/gitea.yaml
Normal 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
|
||||||
7
apps/gitea/install/kustomization.yaml
Normal file
7
apps/gitea/install/kustomization.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: gitea
|
||||||
|
resources:
|
||||||
|
- secrets.yaml
|
||||||
|
- postgresql.yaml
|
||||||
|
- gitea.yaml
|
||||||
56
apps/gitea/install/postgresql.yaml
Normal file
56
apps/gitea/install/postgresql.yaml
Normal 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
|
||||||
14
apps/gitea/install/secrets.yaml
Normal file
14
apps/gitea/install/secrets.yaml
Normal 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
|
||||||
20
bootstrap/kustomization/apps/gitea/gitea-install.yaml
Normal file
20
bootstrap/kustomization/apps/gitea/gitea-install.yaml
Normal 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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
20
bootstrap/kustomization/infrastructure/metallb-config.yaml
Normal file
20
bootstrap/kustomization/infrastructure/metallb-config.yaml
Normal 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
|
||||||
14
bootstrap/kustomization/infrastructure/metallb-install.yaml
Normal file
14
bootstrap/kustomization/infrastructure/metallb-install.yaml
Normal 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
|
||||||
20
bootstrap/kustomization/infrastructure/routes.yaml
Normal file
20
bootstrap/kustomization/infrastructure/routes.yaml
Normal 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
|
||||||
16
bootstrap/kustomization/infrastructure/traefik-install.yaml
Normal file
16
bootstrap/kustomization/infrastructure/traefik-install.yaml
Normal 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
4
bootstrap/ns/apps.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: gitea
|
||||||
4
bootstrap/ns/infrastructure.yaml
Normal file
4
bootstrap/ns/infrastructure.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: infrastructure
|
||||||
8
bootstrap/repositories/jetstack.yaml
Normal file
8
bootstrap/repositories/jetstack.yaml
Normal 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
|
||||||
8
bootstrap/repositories/metallb.yaml
Normal file
8
bootstrap/repositories/metallb.yaml
Normal 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
|
||||||
8
bootstrap/repositories/traefik.yaml
Normal file
8
bootstrap/repositories/traefik.yaml
Normal 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
209
docs/adding-an-app.md
Normal 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
54
infrastructure/README.md
Normal 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.
|
||||||
10
infrastructure/cert-manager-install/cert-override.yaml
Normal file
10
infrastructure/cert-manager-install/cert-override.yaml
Normal 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
|
||||||
19
infrastructure/cert-manager-install/helmrelease.yaml
Normal file
19
infrastructure/cert-manager-install/helmrelease.yaml
Normal 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
|
||||||
22
infrastructure/cert-manager-issuer/issuer.yaml
Normal file
22
infrastructure/cert-manager-issuer/issuer.yaml
Normal 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
|
||||||
13
infrastructure/cert-manager-issuer/secrets.yaml
Normal file
13
infrastructure/cert-manager-issuer/secrets.yaml
Normal 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>
|
||||||
13
infrastructure/metallb-config/ipaddresspool.yaml
Normal file
13
infrastructure/metallb-config/ipaddresspool.yaml
Normal 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
|
||||||
8
infrastructure/metallb-config/l2.yaml
Normal file
8
infrastructure/metallb-config/l2.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: metallb.io/v1beta1
|
||||||
|
kind: L2Advertisement
|
||||||
|
metadata:
|
||||||
|
name: metallb-l2
|
||||||
|
namespace: infrastructure
|
||||||
|
spec:
|
||||||
|
ipAddressPools:
|
||||||
|
- metallb-pool
|
||||||
17
infrastructure/metallb-install/helmrelease.yaml
Normal file
17
infrastructure/metallb-install/helmrelease.yaml
Normal 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
|
||||||
56
infrastructure/routes/gitea.yaml
Normal file
56
infrastructure/routes/gitea.yaml
Normal 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>
|
||||||
21
infrastructure/traefik-install/helmrelease.yaml
Normal file
21
infrastructure/traefik-install/helmrelease.yaml
Normal 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
|
||||||
25
infrastructure/traefik-install/traefik-override.yaml
Normal file
25
infrastructure/traefik-install/traefik-override.yaml
Normal 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"
|
||||||
Reference in New Issue
Block a user