Install with ArgoCD (GitOps)
An alternative to terraform apply: let ArgoCD install and reconcile the same umbrella chart from the OCI registry, onto a cluster you already have ready. Not tied to one cloud the way the Terraform flavors are — bring your own ingress controller, your own cache, your own Postgres.
The #1 gotcha — read first. ArgoCD renders the chart and applies the result — it
never runs helm install, so Helm’s lookup function (which the chart uses to reuse an
already-generated Secret) never runs. With the chart’s default secrets.provider: generated,
ArgoCD would emit a fresh random JWT_SECRET + KMS_LOCAL_KEY on every single sync —
invalidating all sessions and making every stored credential permanently undecryptable. The
ArgoCD path therefore requires secrets.provider: existing: seed one stable Secret once,
ArgoCD only references it.
Prerequisites specific to this path
- ArgoCD installed in the cluster (namespace
argocd) and theargocdCLI logged in. - Your shared pull-through image cache running, if you’re using one — the Application points
global.registryat it. - A Postgres 16 DSN the cluster can reach.
Register the OCI chart repo credential in ArgoCD
argocd repo add registry.marshal.codes/marshal-charts \
--type helm --enable-oci \
--username dep_ab12cd --password 'mrpt_9f3c…'The proxy ignores the username and authenticates on the mrpt_ token in the password. Declarative equivalent: an ArgoCD repository Secret in the argocd namespace (keep the real token out of git — sealed-secret / SOPS / External Secrets).
Seed the stable Secrets — ONCE (ArgoCD never manages these)
kubectl create namespace marshal
# a) App secrets — the CRITICAL one. Generated ONCE, never rotated. BACK THIS UP.
kubectl create secret generic marshal-app-secrets -n marshal \
--from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
--from-literal=KMS_LOCAL_KEY="$(openssl rand -hex 32)"
# b) License — your installation JWT.
kubectl create secret generic marshal-license -n marshal --from-literal=jwt="eyJhbGc…"
# c) Datastore DSN — external Postgres.
kubectl create secret generic marshal-db -n marshal \
--from-literal=database-url="postgres://marshal:PW@HOST:5432/marshal?sslmode=require"Prefer not to run this by hand? A SealedSecret or External Secrets Operator works too — the point is only that they’re stable and not chart-generated.
Commit + apply the Application
Edit argocd/application.yaml — set targetRevision (chart version), global.registry (your cache), ingress.className (whatever ingress controller your cluster runs — this path isn’t cloud-specific), ingress.host, and the Postgres/redis posture — then:
kubectl apply -f application.yaml
argocd app sync marshal
argocd app get marshal # Healthy + SyncedSync is manual by default. The shipped syncPolicy has no automated block — install
and every upgrade need an explicit sync, the same posture as Terraform needing an explicit
apply. Opt into full GitOps (automated: {prune: true, selfHeal: true}) once you trust the
path at your site — be aware selfHeal will then revert any manual kubectl fix applied
mid-incident.
Verify
kubectl get pods -n marshal
kubectl logs -n marshal -l app.kubernetes.io/component=api | grep "license verified"Day-2
- Upgrade — bump
targetRevisioninapplication.yaml, commit, thenargocd app sync marshal. Roll back by reverting the commit + syncing, orargocd app rollback marshal. - Renew the license — update the
marshal-licenseSecret with the new JWT, then restart the api. The Application is unchanged. - Never flip
secrets.providerback togenerated, and never deletemarshal-app-secrets.
Values from a git file, instead of inline
Keep values in a git-committed values.yaml instead of inline helm.valuesObject: use a multi-source Application — one source is the OCI chart, another is your git repo, and helm.valueFiles references $values/path/to/values.yaml. Keep secret material OUT of that file — it stays in the seeded Secrets.