deployment.yaml 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. # Stateless multi-replica Deployment (task 46).
  2. #
  3. # - 3 replicas sharing one Postgres + one Redis: no replica holds state
  4. # another needs (tokens live in Redis under `{state}`-tagged keys).
  5. # - `serve` NEVER migrates; the separate `migrate-job.yaml` owns the schema.
  6. # Readiness (`/health/ready`) compares `_sqlx_migrations` against the
  7. # binary's embedded migrations, so new replicas stay out of traffic until
  8. # the Job has applied the schema (expand/contract keeps N-1 serving).
  9. # - Probes hit the PLAINTEXT pod-IP health listener (`health_addr`,
  10. # default `0.0.0.0:8081`); the public listener stays TLS-only.
  11. # - `terminationGracePeriodSeconds: 45` covers the server's 1s propagation
  12. # grace + 30s drain budget (`src/serve.rs`: PROPAGATION_GRACE, DRAIN_DEADLINE).
  13. apiVersion: apps/v1
  14. kind: Deployment
  15. metadata:
  16. name: d-oauth-server
  17. namespace: d-oauth
  18. labels:
  19. app.kubernetes.io/name: d-oauth-server
  20. app.kubernetes.io/part-of: d-oauth
  21. spec:
  22. replicas: 3
  23. strategy:
  24. type: RollingUpdate
  25. rollingUpdate:
  26. # Keep every old replica serving while the migration Job + new
  27. # replicas converge (expand/contract upgrades).
  28. maxUnavailable: 0
  29. maxSurge: 1
  30. selector:
  31. matchLabels:
  32. app.kubernetes.io/name: d-oauth-server
  33. template:
  34. metadata:
  35. labels:
  36. app.kubernetes.io/name: d-oauth-server
  37. app.kubernetes.io/part-of: d-oauth
  38. spec:
  39. terminationGracePeriodSeconds: 45
  40. securityContext:
  41. runAsNonRoot: true
  42. runAsUser: 65532
  43. runAsGroup: 65532
  44. seccompProfile:
  45. type: RuntimeDefault
  46. containers:
  47. - name: server
  48. # Release pinning: CI resolves this tag to an immutable digest
  49. # (`kustomize edit set image ...@sha256:<digest>`) before rollout;
  50. # see `.github/workflows/supply-chain.yml` and `kustomization.yaml`.
  51. image: ghcr.io/d-oauth/d-oauth-server:latest
  52. imagePullPolicy: IfNotPresent
  53. ports:
  54. - name: public-tls
  55. containerPort: 8080
  56. protocol: TCP
  57. - name: health
  58. containerPort: 8081
  59. protocol: TCP
  60. envFrom:
  61. - configMapRef:
  62. name: d-oauth-config
  63. env:
  64. - name: D_OAUTH_DATABASE__URL
  65. valueFrom:
  66. secretKeyRef:
  67. name: d-oauth-secrets
  68. key: database-url
  69. - name: D_OAUTH_REDIS__URL
  70. valueFrom:
  71. secretKeyRef:
  72. name: d-oauth-secrets
  73. key: redis-url
  74. - name: D_OAUTH_TOKEN_PEPPER
  75. valueFrom:
  76. secretKeyRef:
  77. name: d-oauth-secrets
  78. key: token-pepper
  79. - name: D_OAUTH_TOTP_ENCRYPTION_KEY
  80. valueFrom:
  81. secretKeyRef:
  82. name: d-oauth-secrets
  83. key: totp-encryption-key
  84. # Liveness is dependency-free by construction (`/health/live`
  85. # touches no Postgres/Redis), so a dependency outage never
  86. # triggers a restart storm — readiness absorbs it instead.
  87. startupProbe:
  88. httpGet:
  89. path: /health/live
  90. port: 8081
  91. failureThreshold: 30
  92. periodSeconds: 2
  93. livenessProbe:
  94. httpGet:
  95. path: /health/live
  96. port: 8081
  97. periodSeconds: 10
  98. timeoutSeconds: 2
  99. failureThreshold: 3
  100. # Fail-closed: 503 while Postgres/Redis are unreachable OR the
  101. # `_sqlx_migrations` version does not match the binary (i.e. the
  102. # migrate Job has not run yet on a fresh/rolling deploy).
  103. readinessProbe:
  104. httpGet:
  105. path: /health/ready
  106. port: 8081
  107. periodSeconds: 5
  108. timeoutSeconds: 2
  109. failureThreshold: 3
  110. successThreshold: 1
  111. resources:
  112. requests:
  113. cpu: 100m
  114. memory: 128Mi
  115. limits:
  116. cpu: "1"
  117. memory: 512Mi
  118. securityContext:
  119. allowPrivilegeEscalation: false
  120. readOnlyRootFilesystem: true
  121. capabilities:
  122. drop:
  123. - ALL
  124. volumeMounts:
  125. - name: certs
  126. mountPath: /certs
  127. readOnly: true
  128. volumes:
  129. - name: certs
  130. secret:
  131. secretName: d-oauth-tls
  132. defaultMode: 0o400
  133. topologySpreadConstraints:
  134. - maxSkew: 1
  135. topologyKey: topology.kubernetes.io/zone
  136. whenUnsatisfiable: ScheduleAnyway
  137. labelSelector:
  138. matchLabels:
  139. app.kubernetes.io/name: d-oauth-server
  140. - maxSkew: 1
  141. topologyKey: kubernetes.io/hostname
  142. whenUnsatisfiable: ScheduleAnyway
  143. labelSelector:
  144. matchLabels:
  145. app.kubernetes.io/name: d-oauth-server