migrate-job.yaml 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. # Exactly-once schema migration Job (task 46).
  2. #
  3. # Run BEFORE the Deployment rolls (CI applies the Job, waits for completion,
  4. # then rolls the Deployment). `migrate` applies forward-only sqlx migrations
  5. # under Postgres advisory lock, so concurrent starts serialize and the schema
  6. # is applied exactly once — replicas MUST NOT migrate (they share one
  7. # database; per-replica migration races and double-applies). The Deployment's
  8. # readiness gate (`_sqlx_migrations` version match) keeps new replicas out of
  9. # traffic until this Job succeeds.
  10. #
  11. # Hardened like the Deployment: non-root 65532,
  12. # read-only rootfs, ALL capabilities dropped, `automountServiceAccountToken:
  13. # false`, and the token pepper / TOTP key mounted as 0400 tmpfs files under
  14. # `/run/secrets` (read through the `*_FILE` variables) instead of env vars.
  15. # The image carries the same placeholder digest; `pin-digest` rewrites the
  16. # real digest in `kustomization.yaml` for every workload.
  17. apiVersion: batch/v1
  18. kind: Job
  19. metadata:
  20. name: d-oauth-migrate
  21. namespace: d-oauth
  22. labels:
  23. app.kubernetes.io/name: d-oauth-server
  24. app.kubernetes.io/part-of: d-oauth
  25. spec:
  26. backoffLimit: 3
  27. activeDeadlineSeconds: 300
  28. ttlSecondsAfterFinished: 3600
  29. template:
  30. metadata:
  31. labels:
  32. app.kubernetes.io/name: d-oauth-server
  33. app.kubernetes.io/part-of: d-oauth
  34. app.kubernetes.io/component: migrate
  35. spec:
  36. restartPolicy: Never
  37. automountServiceAccountToken: false
  38. securityContext:
  39. runAsNonRoot: true
  40. runAsUser: 65532
  41. runAsGroup: 65532
  42. fsGroup: 65532
  43. fsGroupChangePolicy: OnRootMismatch
  44. seccompProfile:
  45. type: RuntimeDefault
  46. containers:
  47. - name: migrate
  48. # Same pinning rule as the Deployment: placeholder digest, never a
  49. # shippable `:latest`; CI resolves it via `kustomization.yaml`.
  50. image: ghcr.io/d-oauth/d-oauth-server@sha256:0000000000000000000000000000000000000000000000000000000000000000
  51. imagePullPolicy: IfNotPresent
  52. # Overrides the image ENTRYPOINT (["/d-oauth-server","serve"]).
  53. command: ["/d-oauth-server", "migrate"]
  54. envFrom:
  55. - configMapRef:
  56. name: d-oauth-config
  57. env:
  58. - name: D_OAUTH_DATABASE__URL
  59. valueFrom:
  60. secretKeyRef:
  61. name: d-oauth-secrets
  62. key: database-url
  63. - name: D_OAUTH_REDIS__URL
  64. valueFrom:
  65. secretKeyRef:
  66. name: d-oauth-secrets
  67. key: redis-url
  68. # File-mounted key material, same pattern as the Deployment; the
  69. # inline forms remain supported by `Config` but are not used here.
  70. - name: D_OAUTH_TOKEN_PEPPER_FILE
  71. value: /run/secrets/token-pepper
  72. - name: D_OAUTH_TOTP_ENCRYPTION_KEY_FILE
  73. value: /run/secrets/totp-encryption-key
  74. resources:
  75. requests:
  76. cpu: 50m
  77. memory: 64Mi
  78. limits:
  79. cpu: 500m
  80. memory: 256Mi
  81. securityContext:
  82. allowPrivilegeEscalation: false
  83. readOnlyRootFilesystem: true
  84. capabilities:
  85. drop:
  86. - ALL
  87. volumeMounts:
  88. - name: certs
  89. mountPath: /certs
  90. readOnly: true
  91. - name: secrets
  92. mountPath: /run/secrets
  93. readOnly: true
  94. volumes:
  95. - name: certs
  96. secret:
  97. secretName: d-oauth-tls
  98. defaultMode: 0o400
  99. - name: secrets
  100. secret:
  101. secretName: d-oauth-secrets
  102. defaultMode: 0o400
  103. items:
  104. - key: token-pepper
  105. path: token-pepper
  106. - key: totp-encryption-key
  107. path: totp-encryption-key