apiVersion: apps/v1 kind: StatefulSet metadata: name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs labels: app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs spec: serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service replicas: {{ include "sure.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.sure.replicas) }} selector: matchLabels: app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs template: metadata: name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs labels: app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs annotations: # The chart has no ConfigMap, so there is no checksum/config here. What # these annotations cover is the SOURCE of the secrets, not their # content: the Kubernetes Secrets are materialized by the 1Password # operator, so Helm never sees what is in them. Repointing one of the # secrets.*SecretRef values at a different item therefore changes the # pod template and rolls the pod, instead of silently leaving the old # credentials in the running container. A change of an item's CONTENT # inside the vault is not covered - see the comment on secrets in # values.yaml for why that gap cannot be closed here. # The name differs from checksum/config on purpose - that one promises # "the config changed", this one only "the config source changed". checksum/secret-ref: {{ include (print $.Template.BasePath "/sure.secret.yaml") . | sha256sum }} checksum/secret-ref-db: {{ include (print $.Template.BasePath "/sure-db.secret.yaml") . | sha256sum }} checksum/secret-ref-redis: {{ include (print $.Template.BasePath "/sure-redis.secret.yaml") . | sha256sum }} spec: automountServiceAccountToken: false securityContext: {{- include "sure.podSecurityContext" .Values.sure.securityContext | nindent 8 }} containers: # The web process. Its command is the image's default # (CMD ["./bin/rails", "server"]), which is exactly what makes the # entrypoint run "./bin/rails db:prepare" first - the migration only # happens for that command, so it happens here and nowhere else. - name: {{ .Release.Name }}-{{ .Chart.Name }}-web image: "ghcr.io/we-promise/sure:{{ .Chart.AppVersion }}" imagePullPolicy: IfNotPresent env: {{- include "sure.commonEnv" . | nindent 12 }} {{- include "sure.env" (dict "root" $ "env" .Values.sure.web.env) | nindent 12 }} ports: - containerPort: 3000 protocol: TCP name: http # Probes, resources and both security contexts go through the # null-safe helpers (#99): an erased block means "chart defaults # apply" instead of a vanished probe or an unhardened container. # Never wrap these in an "if" - a guard hides the erased case # completely instead of rendering a visible null. livenessProbe: {{- include "sure.webLivenessProbe" .Values.sure.web.livenessProbe | nindent 12 }} readinessProbe: {{- include "sure.webReadinessProbe" .Values.sure.web.readinessProbe | nindent 12 }} startupProbe: {{- include "sure.webStartupProbe" .Values.sure.web.startupProbe | nindent 12 }} resources: {{- include "sure.webResources" .Values.sure.web.resources | nindent 12 }} securityContext: {{- include "sure.containerSecurityContext" (dict "block" .Values.sure.web.securityContext "path" "sure.web.securityContext") | nindent 12 }} volumeMounts: # Active Storage's local disk service, i.e. every uploaded file and # import, lives here (config/storage.yml: Rails.root.join("storage")). - name: storage mountPath: /rails/storage # Writable paths, so readOnlyRootFilesystem: true can stay on. # Each container gets its own emptyDirs: nothing has to be shared # between the two processes, and a shared scratch directory would # only create coupling nobody asked for. # # /tmp: Rack writes multipart uploads (CSV imports, attachments) to # Dir.tmpdir before Active Storage ever sees them. - name: web-tmp mountPath: /tmp # /rails/tmp: Rails' own scratch directory - the Bootsnap cache, # Puma's pidfile when PIDFILE is set, and the combined CA bundle the # 00_ssl initializer writes when SSL_CA_FILE is given. Note the # trade-off, read off the Dockerfile: the image ships a Bootsnap # cache precompiled at build time under this path, and mounting an # emptyDir over it hides that cache, so every pod start recompiles # it. Boot time cost, not correctness; the alternative is a # writable image layer, which the hardening standard rules out. - name: web-rails-tmp mountPath: /rails/tmp # /rails/log: production logs go to STDOUT (config/environments/ # production.rb), so under normal operation nothing is written here # at all. It stays writable because Rails' default logger path is # log/.log and any rake or console path that falls back to it # would otherwise hit a read-only filesystem. Not measured. - name: web-rails-log mountPath: /rails/log # The Sidekiq process. Same image, different command - and because that # command is NOT "./bin/rails server", the image entrypoint skips # db:prepare here, so the two containers never migrate against each # other. # # args, NOT command, and that distinction is load-bearing: Kubernetes' # "command" replaces the image's ENTRYPOINT, which here is # /rails/bin/docker-entrypoint - the script that preloads jemalloc. # Upstream counts jemalloc among the three things that make the # documented memory figures hold, so replacing the entrypoint would # silently give this container a different memory profile than the one # its requests are sized for. "args" replaces only the CMD, which is # what upstream's compose file does too. - name: {{ .Release.Name }}-{{ .Chart.Name }}-worker image: "ghcr.io/we-promise/sure:{{ .Chart.AppVersion }}" imagePullPolicy: IfNotPresent args: - bundle - exec - sidekiq env: {{- include "sure.commonEnv" . | nindent 12 }} {{- include "sure.env" (dict "root" $ "env" .Values.sure.worker.env) | nindent 12 }} livenessProbe: {{- include "sure.workerLivenessProbe" .Values.sure.worker.livenessProbe | nindent 12 }} readinessProbe: {{- include "sure.workerReadinessProbe" .Values.sure.worker.readinessProbe | nindent 12 }} startupProbe: {{- include "sure.workerStartupProbe" .Values.sure.worker.startupProbe | nindent 12 }} resources: {{- include "sure.workerResources" .Values.sure.worker.resources | nindent 12 }} securityContext: {{- include "sure.containerSecurityContext" (dict "block" .Values.sure.worker.securityContext "path" "sure.worker.securityContext") | nindent 12 }} volumeMounts: # The worker reads and writes the same Active Storage files as the # web process (imports are processed as background jobs), which is # why upstream's compose file mounts one volume into both and why # both containers live in one pod here: a single ReadWriteOnce PVC # cannot be attached to two pods. - name: storage mountPath: /rails/storage - name: worker-tmp mountPath: /tmp - name: worker-rails-tmp mountPath: /rails/tmp - name: worker-rails-log mountPath: /rails/log restartPolicy: Always volumes: - name: storage persistentVolumeClaim: claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc - name: web-tmp emptyDir: {} - name: web-rails-tmp emptyDir: {} - name: web-rails-log emptyDir: {} - name: worker-tmp emptyDir: {} - name: worker-rails-tmp emptyDir: {} - name: worker-rails-log emptyDir: {}