Skip to content

Argo CD and Helm patterns

Tested with: Argo CD 2.12 · Helm 3.16 · External Secrets Operator 0.10

Keep the credential out of values. The chart takes the name of a Secret and the instance host; ESO creates the Secret (see Secrets).

values-prod.yaml
database:
existingSecret: zb-pg-prod # created by ExternalSecret
host: pg-7f3k.us-east.databasezy.com
port: 5432
name: app
sslmode: verify-full
caConfigMap: databasezy-ca
templates/deployment.yaml (excerpt)
env:
- name: PGHOST
value: {{ .Values.database.host | quote }}
- name: PGPORT
value: {{ .Values.database.port | quote }}
- name: PGDATABASE
value: {{ .Values.database.name | quote }}
- name: PGSSLMODE
value: {{ .Values.database.sslmode | quote }}
- name: PGSSLROOTCERT
value: /etc/databasezy/ca.crt
- name: PGUSER
valueFrom: { secretKeyRef: { name: {{ .Values.database.existingSecret }}, key: username } }
- name: PGPASSWORD
valueFrom: { secretKeyRef: { name: {{ .Values.database.existingSecret }}, key: password } }
volumeMounts:
- { name: databasezy-ca, mountPath: /etc/databasezy, readOnly: true }
volumes:
- name: databasezy-ca
configMap: { name: {{ .Values.database.caConfigMap }} }

Ship the ExternalSecret in the chart so an environment is one values file:

templates/externalsecret.yaml
{{- if .Values.database.externalSecret.enabled }}
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: {{ .Values.database.existingSecret }}
spec:
refreshInterval: 5m
secretStoreRef: { kind: ClusterSecretStore, name: {{ .Values.database.externalSecret.store }} }
target: { name: {{ .Values.database.existingSecret }} }
dataFrom:
- extract: { key: {{ .Values.database.externalSecret.key }} }
{{- end }}

Argo CD Application with a PreSync migration

Section titled “Argo CD Application with a PreSync migration”
application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: api-prod
namespace: argocd
spec:
project: prod
source:
repoURL: https://github.com/acme/charts
path: api
targetRevision: main
helm:
valueFiles: [values-prod.yaml]
destination:
server: https://kubernetes.default.svc
namespace: app
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [CreateNamespace=true, ServerSideApply=true]
templates/job-migrate.yaml (excerpt)
metadata:
name: migrate-{{ .Values.image.tag }}
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation

Argo waits for the PreSync Job before applying the rest; a failed migration leaves the previous Deployment running.

One instance per environment, never one database per environment inside a shared instance, so quotas, backups, allow-lists and audit are separate. On the free tier use f0 instances for preview; on paid plans create them from CI with the CLI and delete them when the branch closes:

ci (preview environment)
zb instances create --engine postgres --size s1 --region us-east --name "pg-pr-$PR" --project previews --wait
zb --json instances credentials reveal "pg-pr-$PR" | aws secretsmanager put-secret-value \
--secret-id "databasezy/pg-pr-$PR" --secret-string file:///dev/stdin
# ... on close:
zb instances delete "pg-pr-$PR" --yes