Enable auth-service/api-gateway on production, build their images in CI

This commit is contained in:
2026-08-18 14:11:00 +07:00
parent e5afedfa2f
commit b68005be1c
70 changed files with 6781 additions and 263 deletions
@@ -0,0 +1,56 @@
{{- if .Values.apiGateway.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "medical-chatbot.fullname" . }}-api-gateway
labels:
{{- include "medical-chatbot.labels" . | nindent 4 }}
app.kubernetes.io/component: api-gateway
spec:
replicas: {{ .Values.apiGateway.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: api-gateway
template:
metadata:
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: api-gateway
annotations:
checksum/runtime-config: {{ dict "port" .Values.apiGateway.service.port | toJson | sha256sum | quote }}
spec:
serviceAccountName: {{ include "medical-chatbot.serviceAccountName" . }}
imagePullSecrets:
{{- toYaml .Values.global.imagePullSecrets | nindent 8 }}
containers:
- name: api-gateway
image: {{ include "medical-chatbot.image" (dict "image" .Values.apiGateway.image "name" "apiGateway") | quote }}
imagePullPolicy: {{ .Values.apiGateway.image.pullPolicy }}
ports:
- { name: http, containerPort: {{ .Values.apiGateway.service.port }} }
env:
- name: PORT
value: {{ .Values.apiGateway.service.port | quote }}
- name: AUTH_SERVICE_URL
value: {{ printf "http://%s-auth-service:%d" (include "medical-chatbot.fullname" .) (.Values.authService.service.port | int) | quote }}
resources:
{{- toYaml .Values.apiGateway.resources | nindent 12 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "medical-chatbot.fullname" . }}-api-gateway
labels:
{{- include "medical-chatbot.labels" . | nindent 4 }}
app.kubernetes.io/component: api-gateway
spec:
type: {{ .Values.apiGateway.service.type }}
selector:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: api-gateway
ports:
- name: http
port: {{ .Values.apiGateway.service.port }}
targetPort: http
{{- end }}
@@ -0,0 +1,122 @@
{{- if .Values.authService.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "medical-chatbot.fullname" . }}-auth-service
labels:
{{- include "medical-chatbot.labels" . | nindent 4 }}
data:
JWT_EXPIRES_IN: {{ .Values.authService.config.jwtExpiresIn | quote }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "medical-chatbot.fullname" . }}-auth-service
labels:
{{- include "medical-chatbot.labels" . | nindent 4 }}
app.kubernetes.io/component: auth-service
spec:
replicas: {{ .Values.authService.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: auth-service
template:
metadata:
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: auth-service
annotations:
checksum/runtime-config: {{ .Values.authService.config | toJson | sha256sum | quote }}
spec:
serviceAccountName: {{ include "medical-chatbot.serviceAccountName" . }}
imagePullSecrets:
{{- toYaml .Values.global.imagePullSecrets | nindent 8 }}
{{- if or .Values.authService.migration.enabled .Values.authService.seed.enabled }}
initContainers:
{{- if .Values.authService.migration.enabled }}
- name: migrate
image: {{ include "medical-chatbot.image" (dict "image" .Values.authService.image "name" "authService") | quote }}
imagePullPolicy: {{ .Values.authService.image.pullPolicy }}
command: ["node", "dist/migrate.js"]
env:
- name: POSTGRES_DSN
valueFrom:
secretKeyRef:
name: {{ include "medical-chatbot.secretName" . }}
key: postgres-dsn
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "medical-chatbot.secretName" . }}
key: jwt-secret
{{- end }}
{{- if .Values.authService.seed.enabled }}
- name: seed
image: {{ include "medical-chatbot.image" (dict "image" .Values.authService.image "name" "authService") | quote }}
imagePullPolicy: {{ .Values.authService.image.pullPolicy }}
command: ["node", "dist/seed.js"]
env:
- name: POSTGRES_DSN
valueFrom:
secretKeyRef:
name: {{ include "medical-chatbot.secretName" . }}
key: postgres-dsn
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "medical-chatbot.secretName" . }}
key: jwt-secret
- name: ADMIN_SEED_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "medical-chatbot.secretName" . }}
key: admin-seed-password
- name: DEMO_SEED_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "medical-chatbot.secretName" . }}
key: demo-seed-password
{{- end }}
{{- end }}
containers:
- name: auth-service
image: {{ include "medical-chatbot.image" (dict "image" .Values.authService.image "name" "authService") | quote }}
imagePullPolicy: {{ .Values.authService.image.pullPolicy }}
ports:
- { name: http, containerPort: {{ .Values.authService.service.port }} }
envFrom:
- configMapRef: { name: {{ include "medical-chatbot.fullname" . }}-auth-service }
env:
- name: PORT
value: {{ .Values.authService.service.port | quote }}
- name: POSTGRES_DSN
valueFrom:
secretKeyRef:
name: {{ include "medical-chatbot.secretName" . }}
key: postgres-dsn
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "medical-chatbot.secretName" . }}
key: jwt-secret
resources:
{{- toYaml .Values.authService.resources | nindent 12 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "medical-chatbot.fullname" . }}-auth-service
labels:
{{- include "medical-chatbot.labels" . | nindent 4 }}
app.kubernetes.io/component: auth-service
spec:
type: {{ .Values.authService.service.type }}
selector:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: auth-service
ports:
- name: http
port: {{ .Values.authService.service.port }}
targetPort: http
{{- end }}
@@ -10,4 +10,20 @@ stringData:
postgres-password: {{ .Values.secret.postgresPassword | quote }}
postgres-dsn: {{ printf "postgresql://duoc_thu:%s@%s:5432/duoc_thu" .Values.secret.postgresPassword (default (printf "%s-postgres" (include "medical-chatbot.fullname" .)) .Values.secret.postgresHost) | quote }}
grafana-admin-password: {{ .Values.secret.grafanaAdminPassword | quote }}
{{- if and .Values.aws.staticCredentials.enabled .Values.aws.staticCredentials.accessKeyId }}
aws-access-key-id: {{ .Values.aws.staticCredentials.accessKeyId | quote }}
aws-secret-access-key: {{ .Values.aws.staticCredentials.secretAccessKey | quote }}
{{- end }}
{{- if or .Values.authService.enabled .Values.apiGateway.enabled }}
{{/* Required (not defaulted) once either service is turned on — same
fail-closed posture as the image-tag guard above: a guessable or empty
secret here would let anyone forge an admin JWT. */}}
jwt-secret: {{ required "secret.jwtSecret is required when authService or apiGateway is enabled" .Values.secret.jwtSecret | quote }}
{{- end }}
{{- if .Values.authService.seed.enabled }}
{{/* Required (not defaulted) once the seed job runs — see the comment on
secret.adminSeedPassword in values.yaml for why "1" must never reach here. */}}
admin-seed-password: {{ required "secret.adminSeedPassword is required when authService.seed.enabled" .Values.secret.adminSeedPassword | quote }}
demo-seed-password: {{ required "secret.demoSeedPassword is required when authService.seed.enabled" .Values.secret.demoSeedPassword | quote }}
{{- end }}
{{- end }}
@@ -27,6 +27,20 @@ spec:
env:
- name: AI_SERVICE_URL
value: {{ printf "http://%s-ai-service:%v" (include "medical-chatbot.fullname" .) .Values.aiService.service.port | quote }}
{{- if .Values.apiGateway.enabled }}
- name: API_GATEWAY_URL
value: {{ printf "http://%s-api-gateway:%d" (include "medical-chatbot.fullname" .) (.Values.apiGateway.service.port | int) | quote }}
{{- end }}
{{- if or .Values.authService.enabled .Values.apiGateway.enabled }}
{{/* Only middleware.ts needs this (verifies the admin JWT locally
at the edge) — the BFF routes never see it, they just forward the
cookie's raw token to the gateway. */}}
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "medical-chatbot.secretName" . }}
key: jwt-secret
{{- end }}
ports:
- { name: http, containerPort: 3000 }
readinessProbe:
@@ -53,6 +53,19 @@ aiService:
answerModelId: qwen.qwen3-next-80b-a3b
rerankEnabled: true
# First real-auth rollout to production (2026-08-18). The seed `admin`/`demo`
# passwords are NOT set here — they're secret, so they go inline on the live
# Application the same way secret.grafanaAdminPassword already does (see the
# comment in infra/argocd/applications/medical-chatbot-app.yaml). The chart
# fails closed via `required` if secret.jwtSecret / adminSeedPassword /
# demoSeedPassword are missing, so an inline-values update that forgets one
# of them breaks sync loudly instead of seeding "1".
authService:
enabled: true
apiGateway:
enabled: true
observability:
grafana:
# Dashboards stay open so a demo needs no credentials, but read-only: this
+82
View File
@@ -18,6 +18,46 @@ secret:
# data/app split pattern) — overrides the default in-release host.
postgresHost: ""
grafanaAdminPassword: change-me
# Required (chart render fails without it) once authService or apiGateway
# is enabled — signs/verifies every JWT. Must be the same value both
# services see, which sharing one Secret key already guarantees.
jwtSecret: ""
# Required (chart render fails without it) once authService.seed.enabled is
# true — the seed job's ON CONFLICT DO NOTHING means whatever password goes
# in on the first run is what `admin`/`demo` keep, permanently. Forcing this
# to be set explicitly (no "1" default) stops a real deployment from ever
# seeding the guessable dev password. Local/Compose dev is unaffected: that
# path calls seed.js directly with no env vars set, which still falls back
# to "1" in apps/auth-service/src/config.ts.
adminSeedPassword: ""
demoSeedPassword: ""
# AWS credentials for Bedrock (query embedding, rerank, generation).
#
# Leave `staticCredentials.enabled: false` on AWS-hosted nodes. Both the
# Compose host and the k3s node run on EC2, where boto3 resolves credentials
# from the instance role over IMDS and no key material exists on disk or in
# any manifest — that is the posture every deployment has used so far, and it
# is the safer one. `adapters/bedrock_converse.py` builds its client without
# passing credentials, so boto3's own chain applies: environment variables
# first, then shared config, then the instance role.
#
# Turn this on only for a cluster with no instance role and no IRSA — an
# on-prem cluster, for example — where that chain would find nothing and every
# Bedrock call would fail to authenticate. Because environment variables win
# over the instance role, leaving this off keeps current behaviour exactly.
aws:
# Overrides config.py's `aws_region` default (us-east-1); empty leaves the
# application default in place. The adapter passes this region to boto3
# explicitly, and pydantic-settings reads this same variable name.
region: ""
staticCredentials:
enabled: false
# Used when `secret.create` is true. When pointing at an existing Secret
# (`secret.existingSecret`), leave these empty and add the same two keys —
# `aws-access-key-id` and `aws-secret-access-key` — to that Secret instead.
accessKeyId: ""
secretAccessKey: ""
aiService:
enabled: true
@@ -68,6 +108,48 @@ web:
requests: { cpu: 50m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }
# Both default OFF: this first slice's code exists and can be deployed, but
# turning it on for real production traffic is a separate, later, explicit
# decision — see the plan this was built from. A fresh `helm install` with
# every other default is unaffected either way.
authService:
enabled: false
replicaCount: 1
image:
repository: duocthu-auth-service
tag: local
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 3010
config:
jwtExpiresIn: 12h
migration:
enabled: true
# Idempotent (ON CONFLICT DO NOTHING) — safe to leave on every deploy.
# Disable once real registration replaces the two seed accounts, or if the
# `admin`/`demo` passwords have been rotated and must not be reset back to
# `"1"` by a future rollout.
seed:
enabled: true
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { cpu: 250m, memory: 256Mi }
apiGateway:
enabled: false
replicaCount: 1
image:
repository: duocthu-api-gateway
tag: local
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 3000
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { cpu: 250m, memory: 256Mi }
ingress:
enabled: false
className: nginx