diff --git a/helm/ezscale-website/README.md b/helm/ezscale-website/README.md index 46d9b2a..65bd9a9 100644 --- a/helm/ezscale-website/README.md +++ b/helm/ezscale-website/README.md @@ -37,6 +37,7 @@ openssl rsa -in "$TMP_KEYS/oauth-private.key" -pubout -out "$TMP_KEYS/oauth-publ # 3. Gather production secrets. Read each one and store in shell vars. DB_PASS=$(openssl rand -base64 32 | tr -d '/=+' | cut -c1-32) +REDIS_PASS=$(openssl rand -base64 32 | tr -d '/=+' | cut -c1-32) read -sp "STRIPE_KEY (pk_live_...): " STRIPE_KEY; echo read -sp "STRIPE_SECRET (sk_live_...): " STRIPE_SECRET; echo read -sp "STRIPE_WEBHOOK_SECRET (whsec_...): " STRIPE_WEBHOOK; echo @@ -54,6 +55,7 @@ kubectl create secret generic ezscale-website-secrets \ --namespace ezscale \ --from-literal=APP_KEY="$APP_KEY" \ --from-literal=DB_PASSWORD="$DB_PASS" \ + --from-literal=REDIS_PASSWORD="$REDIS_PASS" \ --from-literal=STRIPE_KEY="$STRIPE_KEY" \ --from-literal=STRIPE_SECRET="$STRIPE_SECRET" \ --from-literal=STRIPE_WEBHOOK_SECRET="$STRIPE_WEBHOOK" \ diff --git a/helm/ezscale-website/templates/_helpers.tpl b/helm/ezscale-website/templates/_helpers.tpl index d0f3a30..58fbb94 100644 --- a/helm/ezscale-website/templates/_helpers.tpl +++ b/helm/ezscale-website/templates/_helpers.tpl @@ -35,7 +35,8 @@ Usage: {{ include "ezscale-website.image" (dict "ctx" . "role" "app") }} */}} {{- define "ezscale-website.image" -}} {{- $ctx := .ctx -}} -{{- printf "%s/%s:%s-%s" $ctx.Values.image.registry $ctx.Values.image.repository .role $ctx.Values.image.tag -}} +{{- $tag := required "image.tag is required (set via --set image.tag=vX.Y.Z)" $ctx.Values.image.tag -}} +{{- printf "%s/%s:%s-%s" $ctx.Values.image.registry $ctx.Values.image.repository .role $tag -}} {{- end -}} {{/* Secret name (existing or generated) */}} diff --git a/helm/ezscale-website/templates/configmap-nginx.yaml b/helm/ezscale-website/templates/configmap-nginx.yaml index 84f7ce3..14d9908 100644 --- a/helm/ezscale-website/templates/configmap-nginx.yaml +++ b/helm/ezscale-website/templates/configmap-nginx.yaml @@ -34,7 +34,10 @@ data: fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; fastcgi_param HTTP_PROXY ""; - fastcgi_param HTTPS $http_x_forwarded_proto; + # Pass HTTPS to PHP only when X-Forwarded-Proto is non-empty. + # if_not_empty avoids the param being set to "" when the header + # is missing (which would falsely satisfy isset($_SERVER['HTTPS'])). + fastcgi_param HTTPS $http_x_forwarded_proto if_not_empty; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; fastcgi_read_timeout 300; diff --git a/helm/ezscale-website/templates/deployment-app.yaml b/helm/ezscale-website/templates/deployment-app.yaml index a31cdd9..f51bda0 100644 --- a/helm/ezscale-website/templates/deployment-app.yaml +++ b/helm/ezscale-website/templates/deployment-app.yaml @@ -51,11 +51,19 @@ spec: - name: nginx-config mountPath: /etc/nginx/conf.d readOnly: true + # Startup probe gives the app up to 100s for first-boot work + # (config:cache + route:cache + view:cache + opcache warmup) before + # liveness takes over. + startupProbe: + httpGet: + path: {{ .Values.healthCheck.livenessPath }} + port: http + failureThreshold: 20 + periodSeconds: 5 livenessProbe: httpGet: path: {{ .Values.healthCheck.livenessPath }} port: http - initialDelaySeconds: {{ .Values.healthCheck.initialDelaySeconds }} periodSeconds: {{ .Values.healthCheck.periodSeconds }} timeoutSeconds: {{ .Values.healthCheck.timeoutSeconds }} failureThreshold: {{ .Values.healthCheck.failureThreshold }} @@ -63,7 +71,6 @@ spec: httpGet: path: {{ .Values.healthCheck.readinessPath }} port: http - initialDelaySeconds: 5 periodSeconds: 5 - name: app image: {{ include "ezscale-website.image" (dict "ctx" . "role" "app") }} diff --git a/helm/ezscale-website/templates/deployment-horizon.yaml b/helm/ezscale-website/templates/deployment-horizon.yaml index a20b318..30a2cc8 100644 --- a/helm/ezscale-website/templates/deployment-horizon.yaml +++ b/helm/ezscale-website/templates/deployment-horizon.yaml @@ -41,6 +41,8 @@ spec: readOnly: true resources: {{- toYaml .Values.horizon.resources | nindent 12 }} + # horizon:status hits Redis. failureThreshold: 5 + periodSeconds: 30 + # gives 150s of tolerance for transient Valkey blips before restart. livenessProbe: exec: command: @@ -50,6 +52,7 @@ spec: initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 10 + failureThreshold: 5 volumes: - name: oauth-keys secret: diff --git a/helm/ezscale-website/templates/job-migrate.yaml b/helm/ezscale-website/templates/job-migrate.yaml index 32999eb..fc6e2b4 100644 --- a/helm/ezscale-website/templates/job-migrate.yaml +++ b/helm/ezscale-website/templates/job-migrate.yaml @@ -41,4 +41,18 @@ spec: name: {{ include "ezscale-website.fullname" . }}-env - secretRef: name: {{ include "ezscale-website.secretName" . }} + volumeMounts: + - name: oauth-keys + mountPath: /var/www/html/secrets + readOnly: true + volumes: + - name: oauth-keys + secret: + secretName: {{ include "ezscale-website.secretName" . }} + items: + - key: oauth-private.key + path: oauth-private.key + - key: oauth-public.key + path: oauth-public.key + optional: true {{- end }} diff --git a/helm/ezscale-website/templates/statefulset-valkey.yaml b/helm/ezscale-website/templates/statefulset-valkey.yaml index 0349a05..a2c2d34 100644 --- a/helm/ezscale-website/templates/statefulset-valkey.yaml +++ b/helm/ezscale-website/templates/statefulset-valkey.yaml @@ -1,18 +1,4 @@ {{- if .Values.valkey.enabled }} -{{- $secretName := printf "%s-valkey" (include "ezscale-website.fullname" .) }} -{{- if .Values.valkey.password }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ $secretName }} - labels: {{- include "ezscale-website.labels" . | nindent 4 }} - annotations: - helm.sh/resource-policy: keep -type: Opaque -stringData: - password: {{ .Values.valkey.password | quote }} ---- -{{- end }} apiVersion: v1 kind: Service metadata: @@ -53,6 +39,10 @@ spec: containers: - name: valkey image: {{ .Values.valkey.image }} + # When requirePassword is true, REDIS_PASSWORD is sourced from the + # main chart Secret (same Secret the app/horizon/scheduler pods read + # via envFrom). Local dev can put it in secret.values; prod + # bootstraps it manually per the chart README. command: - valkey-server - --appendonly @@ -61,17 +51,17 @@ spec: - {{ .Values.valkey.maxmemory | quote }} - --maxmemory-policy - allkeys-lru - {{- if .Values.valkey.password }} + {{- if .Values.valkey.requirePassword }} - --requirepass - $(REDIS_PASSWORD) {{- end }} - {{- if .Values.valkey.password }} + {{- if .Values.valkey.requirePassword }} env: - name: REDIS_PASSWORD valueFrom: secretKeyRef: - name: {{ $secretName }} - key: password + name: {{ include "ezscale-website.secretName" . }} + key: REDIS_PASSWORD {{- end }} ports: - name: redis @@ -81,7 +71,11 @@ spec: mountPath: /data livenessProbe: exec: + {{- if .Values.valkey.requirePassword }} + command: ["sh", "-c", "valkey-cli -a \"$REDIS_PASSWORD\" --no-auth-warning ping"] + {{- else }} command: ["valkey-cli", "ping"] + {{- end }} initialDelaySeconds: 10 periodSeconds: 10 volumeClaimTemplates: diff --git a/helm/ezscale-website/values-us-prod.yaml b/helm/ezscale-website/values-us-prod.yaml index 9b55835..ee0d7e8 100644 --- a/helm/ezscale-website/values-us-prod.yaml +++ b/helm/ezscale-website/values-us-prod.yaml @@ -43,9 +43,11 @@ mariadb: database: ezscale_billing username: ezscale_billing_app -# Per-app Valkey for sessions/cache/queue. +# Per-app Valkey for sessions/cache/queue. requirePassword=true means +# REDIS_PASSWORD must be present in ezscale-website-secrets. valkey: enabled: true + requirePassword: true storage: size: 10Gi storageClassName: longhorn diff --git a/helm/ezscale-website/values.yaml b/helm/ezscale-website/values.yaml index 06f1f36..7aa94b5 100644 --- a/helm/ezscale-website/values.yaml +++ b/helm/ezscale-website/values.yaml @@ -77,7 +77,11 @@ mariadb: valkey: enabled: true image: valkey/valkey:9-alpine - password: "" # if empty, chart generates a random secret + # When true, valkey runs with --requirepass and reads REDIS_PASSWORD from + # the main chart Secret (same key the app/horizon/scheduler pods read via + # envFrom). The Secret MUST contain a REDIS_PASSWORD key — bootstrap it + # alongside APP_KEY in production, or include it in secret.values for dev. + requirePassword: false maxmemory: "1gb" storage: size: 5Gi