feat(infra): add K3s GitOps infrastructure and Packer golden image build (#25)

### Summary of Changes

This PR introduces a declarative, GitOps-managed cloud infrastructure
for the monorepo using K3s, Flux v2, and HashiCorp Packer:

1. **Packer Golden Image Builder (`ci/packer/`)**:
- Automated Debian 12 minimal base OS image creation using QEMU builder,
UEFI firmware, GPT partition scheme, and 2M cluster size `.qcow2`
conversion.
- Cloud-init, `virtio` kernel drivers, and serial console (`ttyS0`,
115200 baud).
- Pre-installed K3s binary (`v1.31.2+k3s1`) with isolated first-boot
token and cert generation.
   - Full identity sanitization (`machine-id` and SSH host key resets).

2. **First-Boot Cloud-Init Bootstrapping
(`ci/cloud-init/user-data.yaml`)**:
- Configures K3s with `servicelb` disabled and TLS SANs for custom
domains and static IP.
- Waits for API readiness, initializes `flux-system` namespace, injects
`sops-age` secret, and triggers Flux sync.

3. **SOPS & GitOps Root Orchestration (`.sops.yaml`,
`ci/k8s/clusters/production/`)**:
- SOPS encryption configuration targeting Kubernetes secrets with `age`
key recipient.
- Flux v2 `GitRepository` and `Kustomization` manifests tracking `next`.

4. **Infrastructure & Networking (`ci/k8s/base/`,
`ci/k8s/infrastructure/`)**:
   - Dedicated `com-ermnvldmr` namespace.
- `cert-manager` HTTP-01 `ClusterIssuer` (`letsencrypt-prod` &
`letsencrypt-staging`).
- Traefik Ingress routing for `ermnvldmr.com`, `www.ermnvldmr.com`,
`docs.ermnvldmr.com`, and `analytics.ermnvldmr.com`.

5. **PostgreSQL & Applications (`ci/k8s/apps/`)**:
- PostgreSQL 16 StatefulSet on 5Gi `local-path` persistent storage with
low-memory tuning.
- Deployments for `www` (pinned `sha-6d33b71`), `docs` (pinned
`sha-91f5245`), and `umami` (pinned `postgresql-v2.14.0`) with
zero-downtime rolling update strategy.

6. **CI/CD Workflows (`.github/workflows/`)**:
- `ci - next/build-golden-image`: Workflow to build and upload
`com-ermnvldmr-debian-12-k3s-amd64-<tag>.qcow2` to static S3
`/dev/images/`.
- Updated `services-www-releases-dispatch-release.yml` and
`services-docs-next-required.yml` with GitOps deployment steps.
This commit is contained in:
deytenit authored and GitHub committed 2026-08-23 02:11:54 +03:00
1 parent 6d33b71405
commit 710873e72c
38 files changed
+1106 -28

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-config
namespace: com-ermnvldmr
data:
postgresql.conf: |
# Low-memory tuned configuration for single-node K3s VM
listen_addresses = '*'
max_connections = 50
shared_buffers = 64MB
effective_cache_size = 128MB
maintenance_work_mem = 16MB
checkpoint_completion_target = 0.9
wal_buffers = 2MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 4MB
min_wal_size = 80MB
max_wal_size = 512MB
+9
View File
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: com-ermnvldmr
resources:
- configmap.yaml
- secret.template.yaml
- pvc.yaml
- statefulset.yaml
- service.yaml
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: com-ermnvldmr
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gi
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: postgres-secrets
namespace: com-ermnvldmr
type: Opaque
stringData:
POSTGRES_DB: "umami"
POSTGRES_USER: "umami"
POSTGRES_PASSWORD: "CHANGE_ME_IN_ENCRYPTED_SOPS_SECRET"
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: postgres-service
namespace: com-ermnvldmr
spec:
type: ClusterIP
selector:
app: postgres
ports:
- name: postgres
port: 5432
targetPort: 5432
+83
View File
@@ -0,0 +1,83 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: com-ermnvldmr
spec:
serviceName: postgres-service
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-secrets
key: POSTGRES_DB
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secrets
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secrets
key: POSTGRES_PASSWORD
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- name: postgres
containerPort: 5432
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: config
mountPath: /etc/postgresql/postgresql.conf
subPath: postgresql.conf
readinessProbe:
exec:
command:
- pg_isready
- -U
- $(POSTGRES_USER)
- -d
- $(POSTGRES_DB)
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
exec:
command:
- pg_isready
- -U
- $(POSTGRES_USER)
- -d
- $(POSTGRES_DB)
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 5
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-pvc
- name: config
configMap:
name: postgres-config