Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
95c5e71
fix deployment by making database in memory
raphael-frank Jun 9, 2026
a739b5f
fix resource rules from cluster
raphael-frank Jun 9, 2026
4064b29
fix keycloak for cluster and vm
raphael-frank Jun 9, 2026
f7806c1
fix keycloak again for vm
raphael-frank Jun 9, 2026
82f745f
fix playbook to let remote git pull
raphael-frank Jun 9, 2026
a63e634
fix quotas for cluster
raphael-frank Jun 9, 2026
fe78f83
fix pre cluster cleanup
raphael-frank Jun 9, 2026
e05bbb6
fix playbook branch thing
raphael-frank Jun 9, 2026
3946545
fix vm forward
raphael-frank Jun 9, 2026
9bb5ff2
fix again
raphael-frank Jun 9, 2026
dcb468c
fix rate limits
raphael-frank Jun 9, 2026
b656c12
fix debug logs
raphael-frank Jun 9, 2026
cf968d0
fix again again
raphael-frank Jun 9, 2026
ad98512
fix: pass VITE_KEYCLOAK_URL build arg to web-client on VM
raphael-frank Jun 9, 2026
b4032af
fix cluster finally
raphael-frank Jun 9, 2026
ef123b7
fix: add resource limits to keycloak wait-for-db init container for q…
raphael-frank Jun 9, 2026
559f3e9
fix: increase probe timeouts and JVM heap for k8s spring services
raphael-frank Jun 9, 2026
f066b41
fix: replace liveness initialDelay with startupProbe for spring services
raphael-frank Jun 9, 2026
748fdf7
fix: keycloak-database service selector uses wrong label key
raphael-frank Jun 9, 2026
a883faf
fix: add startupProbe to keycloak to prevent liveness kill during slo…
raphael-frank Jun 9, 2026
9f17a76
fix: keycloak probes must use management port 9000, not 8080
raphael-frank Jun 9, 2026
5197253
fix: set KC_HTTP_MANAGEMENT_RELATIVE_PATH=/ so health is at port 9000…
raphael-frank Jun 9, 2026
e140146
fix: also delete failed helm release secrets in unlock step
raphael-frank Jun 9, 2026
890f109
fix: keycloak service selector uses wrong label key
raphael-frank Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
-i /tmp/inventory.yml \
infra/ansible/playbook.yml \
-e "repo_url=https://github.com/${{ github.repository }}.git" \
-e "branch=${{ github.ref_name }}" \
-e "genai_env_file=/tmp/genai.env"

# ------------------------------------------------------------------
Expand Down Expand Up @@ -92,6 +93,8 @@ jobs:

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker

- name: Log in to GHCR
uses: docker/login-action@v3
Expand All @@ -113,8 +116,6 @@ jobs:
${{ steps.img.outputs.repo }}:${{ github.sha }}
${{ steps.img.outputs.repo }}:latest
build-args: ${{ matrix.build_args || '' }}
cache-from: type=gha
cache-to: type=gha,mode=max

# ------------------------------------------------------------------
# Deploy to the RKE2 Kubernetes cluster via Helm.
Expand Down Expand Up @@ -147,10 +148,22 @@ jobs:
--from-env-file=/tmp/genai.env \
--dry-run=client -o yaml | kubectl apply -f -

- name: Unlock stuck Helm release (if any)
run: |
for secret in $(kubectl -n "$NAMESPACE" get secret \
-l "owner=helm,name=team-devoops" -o name 2>/dev/null); do
status=$(kubectl -n "$NAMESPACE" get "$secret" \
-o jsonpath='{.metadata.labels.status}' 2>/dev/null || echo "")
if [[ "$status" == pending-* || "$status" == "failed" ]]; then
echo "Deleting stuck Helm secret $secret (status=$status)"
kubectl -n "$NAMESPACE" delete "$secret"
fi
done

- name: Helm upgrade
run: |
helm upgrade --install team-devoops infra/helm/team-devoops \
--namespace "$NAMESPACE" \
--set global.image.tag=${{ github.sha }} \
--set keycloak.hostname=ge83mom-devops26.stud.k8s.aet.cit.tum.de \
--atomic --timeout 15m
--set keycloak.hostname=https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/auth \
--rollback-on-failure --timeout 15m
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The Spring Boot services and the GenAI service share a **PostgreSQL** database.
| GenAI Service | `/api/v1/helper/…` | 5000 | Python 3.12, Flask, LangChain |
| Web Client | `/` | 8080 | React, Vite |
| Swagger UI | `/docs` | 8080 | swaggerapi/swagger-ui |
| Keycloak | `/auth` | 8080 | Keycloak 26 |
| Traefik dashboard | `http://localhost:8080` (local only) | — | Traefik v3 |
| PostgreSQL | internal only | 5432 | postgres:15 |

Expand Down Expand Up @@ -250,20 +251,24 @@ All services are protected by [Keycloak 26](https://www.keycloak.org) via OIDC/J

### Local login

When running with Docker Compose, Keycloak is available at <http://localhost:8081>. The realm is auto-imported on first start from [`infra/keycloak/realm-config.json`](infra/keycloak/realm-config.json).
When running with Docker Compose, Keycloak is available at <http://localhost:8081/auth>. The realm is auto-imported on first start from [`infra/keycloak/realm-config.json`](infra/keycloak/realm-config.json).

The web client redirects to Keycloak automatically (`login-required` strategy). Log in with any of the test users above.

### Production admin console

Keycloak is publicly accessible via Traefik at <https://team-devoops.uaenorth.cloudapp.azure.com/auth>. Admin console: `/auth/admin`.

### Spring services — JWT validation

Each Spring service is a stateless OAuth2 resource server. It validates Bearer JWTs against Keycloak's JWK set and extracts roles from the `realm_access.roles` claim, mapping them to Spring `ROLE_*` authorities (e.g. `"admin"` → `ROLE_admin`).

| Environment variable | Purpose |
| Property | Purpose |
|---|---|
| `SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI` | Validates the `iss` claim in incoming JWTs |
| `SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI` | URL to fetch Keycloak's public signing keys |
| `spring.security.oauth2.resourceserver.jwt.issuer-uri` | Validates the `iss` claim in incoming JWTs |
| `spring.security.oauth2.resourceserver.jwt.jwk-set-uri` | URL to fetch Keycloak's public signing keys |

Docker Compose sets these to `http://keycloak:8080/auth/realms/devops/`. On Kubernetes they are injected via the `env:` block in `infra/helm/team-devoops/values.yaml` using the internal `keycloak` ClusterIP DNS name.
These are set in each service's `src/main/resources/application.properties` as defaults (pointing at the local Keycloak on `localhost:8081/auth`). On the Azure VM, `docker-compose.yml` overrides `SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI` with the public HTTPS issuer so it matches the `iss` claim in tokens issued by production Keycloak. The JWK set URI always uses the internal Docker hostname `http://keycloak:8080/auth/realms/devops/protocol/openid-connect/certs`. On Kubernetes they are injected via the `env:` block in `infra/helm/team-devoops/values.yaml` using the internal `keycloak` ClusterIP DNS name.

## Docs

Expand Down
9 changes: 9 additions & 0 deletions infra/ansible/playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,20 @@
force: true
update: true

- name: Set repository ownership to ansible_user
file:
path: "{{ app_dir }}"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
recurse: true

- name: Write py-genai-helper .env file
copy:
src: "{{ genai_env_file }}"
dest: "{{ app_dir }}/services/py-genai-helper/.env"
mode: "0600"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"

- name: Deploy with docker compose
shell: >
Expand Down
19 changes: 19 additions & 0 deletions infra/docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ services:
- "traefik.http.services.py-genai-helper.loadbalancer.server.port=5000"

organization-service:
environment:
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://localhost:8081/auth/realms/devops
labels: !override
- "traefik.enable=true"
- "traefik.http.routers.organization-service.entrypoints=web"
Expand All @@ -49,6 +51,8 @@ services:
- "traefik.http.services.organization-service.loadbalancer.server.port=8080"

member-service:
environment:
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://localhost:8081/auth/realms/devops
labels: !override
- "traefik.enable=true"
- "traefik.http.routers.member-service.entrypoints=web"
Expand All @@ -58,6 +62,8 @@ services:
- "traefik.http.services.member-service.loadbalancer.server.port=8080"

event-service:
environment:
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://localhost:8081/auth/realms/devops
labels: !override
- "traefik.enable=true"
- "traefik.http.routers.event-service.entrypoints=web"
Expand All @@ -67,6 +73,8 @@ services:
- "traefik.http.services.event-service.loadbalancer.server.port=8080"

feedback-service:
environment:
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://localhost:8081/auth/realms/devops
labels: !override
- "traefik.enable=true"
- "traefik.http.routers.feedback-service.entrypoints=web"
Expand All @@ -76,6 +84,8 @@ services:
- "traefik.http.services.feedback-service.loadbalancer.server.port=8080"

finance-service:
environment:
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://localhost:8081/auth/realms/devops
labels: !override
- "traefik.enable=true"
- "traefik.http.routers.finance-service.entrypoints=web"
Expand All @@ -85,6 +95,8 @@ services:
- "traefik.http.services.finance-service.loadbalancer.server.port=8080"

letter-service:
environment:
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://localhost:8081/auth/realms/devops
labels: !override
- "traefik.enable=true"
- "traefik.http.routers.letter-service.entrypoints=web"
Expand All @@ -107,6 +119,13 @@ services:
- "traefik.http.routers.web-client.rule=PathPrefix(`/`)"
- "traefik.http.services.web-client.loadbalancer.server.port=8080"

keycloak:
environment:
KC_HOSTNAME: "http://localhost:8081/auth"

traefik-forward-auth:
labels: !override
- "traefik.enable=false"
environment:
- PROVIDERS_OIDC_ISSUER_URL=http://localhost:8081/auth/realms/devops
- INSECURE_COOKIE=true
40 changes: 28 additions & 12 deletions infra/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ services:
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
- "traefik.http.routers.organization-service.entrypoints=websecure"
Expand All @@ -59,6 +60,7 @@ services:
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
- "traefik.http.routers.member-service.entrypoints=websecure"
Expand All @@ -85,6 +87,7 @@ services:
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
- "traefik.http.routers.event-service.entrypoints=websecure"
Expand All @@ -111,6 +114,7 @@ services:
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
- "traefik.http.routers.feedback-service.entrypoints=websecure"
Expand All @@ -137,6 +141,7 @@ services:
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
- "traefik.http.routers.finance-service.entrypoints=websecure"
Expand All @@ -163,6 +168,7 @@ services:
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
- "traefik.http.routers.letter-service.entrypoints=websecure"
Expand Down Expand Up @@ -197,7 +203,10 @@ services:
- proxy

web-client:
build: ../web-client/
build:
context: ../web-client/
args:
VITE_KEYCLOAK_URL: https://team-devoops.uaenorth.cloudapp.azure.com/auth
container_name: web-client
expose:
- 8080
Expand All @@ -211,8 +220,6 @@ services:
- py-genai-helper
labels:
- "traefik.enable=true"
- "traefik.http.routers.web-client.entrypoints=web"
- "traefik.http.routers.web-client.rule=PathPrefix(`/`)"
- "traefik.http.routers.web-client.entrypoints=websecure"
- "traefik.http.routers.web-client.rule=Host(`team-devoops.uaenorth.cloudapp.azure.com`)"
- "traefik.http.routers.web-client.tls=true"
Expand Down Expand Up @@ -261,24 +268,22 @@ services:
container_name: traefik-forward-auth
environment:
- DEFAULT_PROVIDER=oidc
- PROVIDERS_OIDC_ISSUER_URL=http://localhost:8081/realms/devops
- PROVIDERS_OIDC_ISSUER_URL=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
- PROVIDERS_OIDC_CLIENT_ID=traefik-forward-auth
- PROVIDERS_OIDC_CLIENT_SECRET=traefik-forward-auth-secret
- SECRET=a-random-32-char-secret-changeme!
- INSECURE_COOKIE=true
- INSECURE_COOKIE=false
- LOG_LEVEL=debug
extra_hosts:
- "localhost:host-gateway"
- "team-devoops.uaenorth.cloudapp.azure.com:host-gateway"
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik-forward-auth.rule=Path(`/_oauth`)"
- "traefik.http.routers.traefik-forward-auth.entrypoints=web"
- "traefik.http.routers.traefik-forward-auth.middlewares=forward-auth@file"
- "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"
- "traefik.enable=false"
depends_on:
keycloak:
condition: service_healthy
networks:
- proxy
restart: on-failure

keycloak:
image: quay.io/keycloak/keycloak:26.0.0
Expand All @@ -295,14 +300,25 @@ services:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_HEALTH_ENABLED: "true"
KC_HTTP_RELATIVE_PATH: /auth
KC_HTTP_MANAGEMENT_RELATIVE_PATH: /
KC_HOSTNAME: https://team-devoops.uaenorth.cloudapp.azure.com/auth
KC_PROXY_HEADERS: xforwarded
labels:
- "traefik.enable=true"
- "traefik.http.routers.keycloak.entrypoints=websecure"
- "traefik.http.routers.keycloak.rule=Host(`team-devoops.uaenorth.cloudapp.azure.com`) && PathPrefix(`/auth`)"
- "traefik.http.routers.keycloak.tls=true"
- "traefik.http.routers.keycloak.tls.certresolver=le"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
healthcheck:
test:
- "CMD-SHELL"
- "exec 3<>/dev/tcp/localhost/9000; echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3; cat <&3 | grep -q '200 OK'"
interval: 10s
timeout: 5s
retries: 20
start_period: 90s
start_period: 30s
volumes:
- ./keycloak/realm-config.json:/opt/keycloak/data/import/realm-config.json
ports:
Expand Down
10 changes: 8 additions & 2 deletions infra/helm/team-devoops/files/realm-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,14 @@
"secret": "traefik-forward-auth-secret",
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": ["http://localhost/_oauth"],
"webOrigins": ["http://localhost"]
"redirectUris": [
"https://team-devoops.uaenorth.cloudapp.azure.com/_oauth",
"http://localhost/_oauth"
],
"webOrigins": [
"https://team-devoops.uaenorth.cloudapp.azure.com",
"http://localhost"
]
}
]
}
18 changes: 15 additions & 3 deletions infra/helm/team-devoops/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ metadata:
{{- include "team-devoops.labels" (dict "name" $name "root" $root) | nindent 4 }}
spec:
replicas: {{ $svc.replicas | default 1 }}
strategy:
{{- toYaml $root.Values.strategy | nindent 4 }}
selector:
matchLabels:
{{- include "team-devoops.selectorLabels" (dict "name" $name) | nindent 6 }}
Expand Down Expand Up @@ -48,23 +50,33 @@ spec:
{{- end }}
{{- end }}
{{- if $svc.health }}
startupProbe:
httpGet:
path: {{ $svc.health }}
port: {{ $svc.port }}
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
readinessProbe:
httpGet:
path: {{ $svc.health }}
port: {{ $svc.port }}
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: {{ $svc.health }}
port: {{ $svc.port }}
initialDelaySeconds: 60
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
{{- else }}
readinessProbe:
tcpSocket:
port: {{ $svc.port }}
initialDelaySeconds: 15
initialDelaySeconds: 30
periodSeconds: 10
{{- end }}
resources:
Expand Down
Loading
Loading