Skip to content

Deploy to Kubernetes #103

Deploy to Kubernetes

Deploy to Kubernetes #103

Workflow file for this run

name: Deploy to Kubernetes
on:
# Auto-deploy after any per-service CI workflow succeeds on main.
workflow_run:
workflows:
- "Spring API"
- "Python Help Service"
- "Python Recipe Service"
- "Web Client"
types: [completed]
branches: [main]
# Also trigger on K8s manifest changes and for manual/release runs.
push:
branches: [main]
paths:
- 'infra/k8s/**'
- '.github/workflows/deploy-k8s.yml'
workflow_call:
workflow_dispatch:
# Queue concurrent deploys rather than running them in parallel.
concurrency:
group: deploy-k8s
cancel-in-progress: false
jobs:
deploy:
# For workflow_run: skip if the upstream CI failed or was cancelled.
# For all other triggers (push, workflow_call, workflow_dispatch): always run.
if: >
github.event_name != 'workflow_run' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'workflow_call')
runs-on: ubuntu-latest
environment: kubernetes
steps:
- uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v4
- name: Configure kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
- name: Apply namespaces
run: kubectl apply -f infra/k8s/namespace.yaml
# --dry-run=client -o yaml | kubectl apply -f - is idempotent: creates on first
# run, updates on subsequent runs, never errors if the secret already exists.
#
# Required GitHub Secrets (Settings → Secrets and variables → Actions):
# DB_PASSWORD — PostgreSQL password
# JWT_SECRET — JWT signing key
# INTERNAL_AUTH_SECRET — shared HMAC key between spring-api and python services
# HELP_SERVICE_KEY — Gemini API key for py-help-service
# RECIPE_SERVICE_KEY — Gemini API key for py-recipe-service
# GRAFANA_ADMIN_PASSWORD — Grafana admin password
# DISCORD_WEBHOOK_URL — Discord webhook URL for Grafana alert notifications
# KUBE_CONFIG — base64-encoded kubeconfig (base64 -w0 on Linux)
- name: Upsert K8s secrets
run: |
kubectl create secret generic postgres-secret -n app \
--from-literal=password="${{ secrets.DB_PASSWORD }}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic spring-api-secret -n app \
--from-literal=DB_PASSWORD="${{ secrets.DB_PASSWORD }}" \
--from-literal=JWT_SECRET="${{ secrets.JWT_SECRET }}" \
--from-literal=INTERNAL_AUTH_SECRET="${{ secrets.INTERNAL_AUTH_SECRET }}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic py-recipe-secret -n app \
--from-literal=PROVIDER="${{ vars.PROVIDER }}" \
--from-literal=LOGOS_MODEL="${{ vars.LOGOS_MODEL }}" \
--from-literal=LOGOS_BASE_URL="${{ vars.LOGOS_BASE_URL }}" \
--from-literal=LOGOS_KEY="${{ secrets.LOGOS_KEY }}" \
--from-literal=GEMINI_MODEL="${{ vars.GEMINI_MODEL }}" \
--from-literal=GEMINI_RECIPE_SERVICE_KEY="${{ secrets.GEMINI_RECIPE_SERVICE_KEY }}" \
--from-literal=INTERNAL_AUTH_SECRET="${{ secrets.INTERNAL_AUTH_SECRET }}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic py-help-secret -n app \
--from-literal=PROVIDER="${{ vars.PROVIDER }}" \
--from-literal=LOGOS_MODEL="${{ vars.LOGOS_MODEL }}" \
--from-literal=LOGOS_BASE_URL="${{ vars.LOGOS_BASE_URL }}" \
--from-literal=LOGOS_KEY="${{ secrets.LOGOS_KEY }}" \
--from-literal=GEMINI_MODEL="${{ vars.GEMINI_MODEL }}" \
--from-literal=GEMINI_HELP_SERVICE_KEY="${{ secrets.GEMINI_HELP_SERVICE_KEY }}" \
--from-literal=INTERNAL_AUTH_SECRET="${{ secrets.INTERNAL_AUTH_SECRET }}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic grafana-secret -n monitoring \
--from-literal=admin-password="${{ secrets.GRAFANA_ADMIN_PASSWORD }}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic grafana-discord-secret -n monitoring \
--from-literal=webhook-url="${{ secrets.DISCORD_WEBHOOK_URL }}" \
--dry-run=client -o yaml | kubectl apply -f -
- name: Deploy PostgreSQL (StatefulSet)
run: kubectl apply -f infra/k8s/postgres/
- name: Wait for PostgreSQL to be ready
run: kubectl rollout status statefulset/postgres -n app --timeout=180s
- name: Deploy application services
run: |
kubectl apply -f infra/k8s/spring-api/
kubectl apply -f infra/k8s/py-recipe-service/
kubectl apply -f infra/k8s/py-help-service/
kubectl apply -f infra/k8s/web-client/
kubectl apply -f infra/k8s/ingress.yaml
- name: Deploy monitoring
run: kubectl apply -f infra/k8s/monitoring/
- name: Restart monitoring to reload ConfigMap-based config
run: |
kubectl rollout restart deployment/grafana -n monitoring
kubectl rollout restart deployment/loki -n monitoring
kubectl rollout restart deployment/alloy -n monitoring
- name: Wait for monitoring rollouts
run: |
kubectl rollout status deployment/prometheus -n monitoring --timeout=300s
kubectl rollout status deployment/grafana -n monitoring --timeout=180s
kubectl rollout status deployment/loki -n monitoring --timeout=180s
kubectl rollout status deployment/alloy -n monitoring --timeout=120s
- name: Restart deployments to pull latest images
if: github.event_name != 'push'
run: |
kubectl rollout restart deployment/spring-api -n app
kubectl rollout restart deployment/py-recipe-service -n app
kubectl rollout restart deployment/py-help-service -n app
kubectl rollout restart deployment/web-client -n app
- name: Wait for rollouts
run: |
kubectl rollout status deployment/spring-api -n app --timeout=300s
kubectl rollout status deployment/py-recipe-service -n app --timeout=180s
kubectl rollout status deployment/py-help-service -n app --timeout=180s
kubectl rollout status deployment/web-client -n app --timeout=120s
- name: Show deployment summary
run: |
echo "=== Running image digests ==="
kubectl get pods -n app -o json | jq -r '
.items[] |
"\(.metadata.name):\n image: \(.status.containerStatuses[0].image // "unknown")\n digest: \(.status.containerStatuses[0].imageID // "pending")"
'
echo ""
echo "=== Pods ==="
kubectl get pods -n app -o wide
echo ""
echo "=== Ingress ==="
kubectl get ingress -n app
echo ""
echo "URL: https://devsecops.stud.k8s.aet.cit.tum.de/"