Deploy to Kubernetes #103
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/" |