Dev Kubernetes manifests
This runbook explains what lives under k8s/dev and k8s/prod in the app repo and how the manifests work together: Secrets, ConfigMaps, database, app Deployment, Service, Ingress and SOPS encryption.
Blaster GitOps series
- Blaster GitOps summary
- Blaster repo and branches
- Dockerfile & GitLab CI
- Clerk authentication & user setup
- Google OAuth for Clerk
- Blaster prep for automation
- Dev app k8s manifests - you are here
- Dev flux sources & Kustomizations
- Dev image automation
- Dev SOPS & age
- Dev verification & troubleshooting
- Dev full runbook
- Prod overview
- Prod app k8s manifests and deployment
- Prod Flux GitOps and image automation
- Prod Cloudflare, Origin CA and tunnel routing
- Prod full runbook
- Post development branches
1. Context
Dev repo layout in games/blaster:
games/blaster/
├── .dockerignore
├── .gitlab-ci.yml
├── .sops.yaml
├── app
├── database
├── db
├── Dockerfile
├── k8s
│ └── dev
│ ├── 10-secret-db.enc.yaml
│ ├── 20-db-statefulset.yaml
│ ├── 30-secret-app.enc.yaml
│ ├── 40-app-config.yaml
│ ├── 50-app-deployment.yaml
│ ├── 60-ingress.yaml
│ └── kustomization.yaml
├── lib
└── package.json
Environments:
- Dev
- Namespace:
blaster-dev - Host:
blaster.reids.net.au - Branch:
develop
- Namespace:
- Prod
- Namespace:
blaster - Host:
blaster.muppit.au - Branch:
main
- Namespace:
Flux Kustomizations in the infra repo point at these directories. For repo and branches see Blaster repo and branches.
2. SOPS policy in the app repo
All Secrets under k8s/ are committed encrypted using SOPS with age.
File: games/blaster/.sops.yaml
# .sops.yaml
creation_rules:
- path_regex: k8s/.*\.enc\.ya?ml$
encrypted_regex: '^(data|stringData)$'
age: ['AGE_PUBLIC_KEY_HERE']
- Any file matching
k8s/**.enc.yamlork8s/**.enc.ymlwill havedataorstringDataencrypted. - Replace
AGE_PUBLIC_KEY_HEREwith your age public key (starts withage1).
Secrets are edited with:
sops -e -i k8s/dev/10-secret-db.enc.yaml
sops -e -i k8s/dev/30-secret-app.enc.yaml
Do not edit encrypted files without sops. You will end up with broken YAML or plain text secrets in Git.
3. Dev manifests overview (k8s/dev)
3.1 Kustomization
File: k8s/dev/kustomization.yaml
# k8s/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: blaster-dev
resources:
- 10-secret-db.enc.yaml
- 20-db-statefulset.yaml
- 30-secret-app.enc.yaml
- 40-app-config.yaml
- 50-app-deployment.yaml
- 60-ingress.yaml
This:
- Sets the default namespace to
blaster-dev. - Applies Secrets, ConfigMap, StatefulSet, Deployment, Service and Ingress as a unit.
3.2 Database Secret
File: k8s/dev/10-secret-db.enc.yaml
---
# k8s/dev/10-secret-db.enc.yaml
apiVersion: v1
kind: Secret
metadata:
name: blaster-db-secret
namespace: blaster-dev
type: Opaque
stringData:
POSTGRES_DB: blaster_game
POSTGRES_USER: blaster_user
POSTGRES_PASSWORD: "REPLACE_WITH_STRONG_PASSWORD"
Use sops to encrypt this file. The same Secret is consumed by both the database and the app.
3.3 PostgreSQL StatefulSet and Service
File: k8s/dev/20-db-statefulset.yaml
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: blaster-db
namespace: blaster-dev
spec:
serviceName: blaster-db
replicas: 1
selector:
matchLabels:
app: blaster-db
template:
metadata:
labels:
app: blaster-db
spec:
containers:
- name: postgres
image: postgres:15.8
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5432
name: postgres
envFrom:
- secretRef:
name: blaster-db-secret
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
initialDelaySeconds: 30
periodSeconds: 30
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: blaster-db
namespace: blaster-dev
spec:
type: ClusterIP
selector:
app: blaster-db
ports:
- name: postgres
port: 5432
targetPort: 5432
Notes:
- Uses
blaster-db-secretas single source of truth. - Small but non-trivial resource requests and limits for dev.
Readinessandlivenessboth callpg_isready.
3.4 App Secrets and Config
Files before encryption:
---
# k8s/dev/30-secret-app.enc.yaml
apiVersion: v1
kind: Secret
metadata:
name: blaster-app-secret
namespace: blaster-dev
type: Opaque
stringData:
CLERK_SECRET_KEY: "REPLACE_WITH_REAL_CLERK_SECRET_KEY"
---
# k8s/dev/40-app-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: blaster-app-config
namespace: blaster-dev
data:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "REPLACE_WITH_PUBLIC_KEY"
30-secret-app.enc.yamlis encrypted with SOPS.40-app-config.yamlis plain text because it only contains public configuration.
3.5 App Deployment and Service
File: k8s/dev/50-app-deployment.yaml
---
# k8s/dev/50-app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: blaster-app
namespace: blaster-dev
spec:
replicas: 2
selector:
matchLabels:
app: blaster-app
template:
metadata:
labels:
app: blaster-app
spec:
imagePullSecrets:
- name: blaster-dev-registry
containers:
- name: blaster
image: registry.reids.net.au/games/blaster:dev-20251115.42 # {"$imagepolicy": "flux-system:blaster-dev-policy"}
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
name: http
envFrom:
- secretRef:
name: blaster-db-secret # DB name/user/password
- secretRef:
name: blaster-app-secret # Clerk secret
- configMapRef:
name: blaster-app-config # public config
env:
- name: POSTGRES_HOST
value: "blaster-db"
- name: POSTGRES_PORT
value: "5432"
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 60
periodSeconds: 20
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
---
apiVersion: v1
kind: Service
metadata:
name: blaster-app
namespace: blaster-dev
spec:
type: ClusterIP
selector:
app: blaster-app
ports:
- name: http
port: 80
targetPort: 3000
Important pieces:
imagePullSecretsreferences a registry Secret created in the infra repo namespaceflux-systemand mirrored intoblaster-dev(see Image automation runbook).envFrompulls in:- Database credentials.
- Clerk secret.
- Public config from ConfigMap.
- Explicit probes and resource requests/limits help Flux and the scheduler see health correctly.
3.6 Ingress for dev
File: k8s/dev/60-ingress.yaml
---
# k8s/dev/60-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: blaster-ingress
namespace: blaster-dev
spec:
ingressClassName: nginx
tls:
- hosts:
- blaster.reids.net.au
rules:
- host: "blaster.reids.net.au"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: blaster-app
port:
number: 80
- Uses the
nginxingress class. - Exposes the app as
https://blaster.reids.net.auvia Ingress NGINX. - TLS is handled by the cluster side (cert-manager wildcard certificate) rather than this page.
4. Prod manifests overview (k8s/prod)
Prod manifests follow the same pattern as dev with these differences:
- Namespace:
blasterinstead ofblaster-dev. - Image tag:
prod-YYYYMMDD.Ntags referenced in the Deployment. - Ingress host:
blaster.muppit.au. - Resources: can be higher (more replicas, more CPU and memory).
Skeleton layout:
k8s/prod/
kustomization.yaml
10-secret-db.enc.yaml
20-db-statefulset.yaml
30-secret-app.enc.yaml
40-app-config.yaml
50-app-deployment.yaml
60-ingress.yaml
Example k8s/prod/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: blaster
resources:
- 10-secret-db.enc.yaml
- 20-db-statefulset.yaml
- 30-secret-app.enc.yaml
- 40-app-config.yaml
- 50-app-deployment.yaml
- 60-ingress.yaml
Ingress for prod would be the same as dev but with blaster.muppit.au as the host.
Keep the file names and structure identical between dev and prod where possible. It makes diffing and promotion much easier.
5. SOPS workflow for app secrets
5.1 Environment variables
Set SOPS_AGE_KEY_FILE on your Mac, for example in ~/.zshrc:
export SOPS_AGE_KEY_FILE="$HOME/.sops/age.key"
5.2 Editing a secret
To edit the dev DB Secret:
sops k8s/dev/10-secret-db.enc.yaml
SOPS will:
- Decrypt the file to a temp buffer.
- Open it in
$EDITOR. - Re-encrypt on save.
You can run the same command for app secrets and for prod equivalents under k8s/prod.
6. How Flux applies these manifests
Flux sees the manifests via a Kustomization in the infra repo:
# clusters/my-cluster/blaster/dev/kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: blaster-dev
namespace: flux-system
spec:
interval: 1m
path: ./k8s/dev
prune: true
sourceRef:
kind: GitRepository
name: blaster-dev
wait: true
timeout: 5m
decryption:
provider: sops
secretRef:
name: sops-age
Key points:
path: ./k8s/devtells Flux to use the app repo Kustomization.decryption.provider: sopsallows SOPS-encrypted files to be applied.prune: trueremoves objects deleted from Git.
Prod will use a similar Kustomization pointing at ./k8s/prod and the main branch.
For full Flux configuration, see Flux sources & Kustomizations and Image automation.
7. Verification commands
7.1 Check resources in blaster-dev
kubectl -n blaster-dev get pods,svc,ingress
kubectl -n blaster-dev get deploy,sts
kubectl -n blaster-dev get cm,secret
You should see:
- One StatefulSet
blaster-dbwith one ready replica. - One Deployment
blaster-appwith the expected replicas. - Services
blaster-appandblaster-db. - Ingress
blaster-ingresspointing atblaster-app.
7.2 Check that images match the expected tag
kubectl -n blaster-dev get deploy blaster-app -o jsonpath='{.spec.template.spec.containers[0].image}'
echo
For dev, expect something like:
registry.reids.net.au/games/blaster:dev-20251115.51
7.3 Check SOPS-encrypted files
grep -n 'sops:' k8s/dev/*.enc.yaml
head -n 20 k8s/dev/10-secret-db.enc.yaml
You should see an sops: section and no raw passwords.
8. Verification checklist
-
games/blaster/k8s/devexists with the files listed above. -
games/blaster/k8s/prodexists and mirrors the dev structure. -
.sops.yamlat repo root matches the SOPS rule for*.enc.yaml. - All Secrets in Git are encrypted and edited via
sops. -
blaster-devnamespace has:- Running
blaster-dbStatefulSet. - Running
blaster-appDeployment. - Services and Ingress as expected.
- Running
-
https://blaster.reids.net.auserves the dev game. -
https://blaster.muppit.auserves the prod game when prod manifests are configured.
Once this checklist is true, the app side of the Blaster Kubernetes manifests is in good shape, and the Flux runbooks can take over for GitOps reconciliation and image automation.