Deployment
How the platform ships: Kubernetes with GitOps via ArgoCD, manifests that live with each app, and a branch-and-tag promotion flow from Dev to Prod. There is no kubectl apply in the pipeline — Argo syncs from git.
GitOps with ArgoCD
A single ApplicationSet in VENIZIA-AI/infra enumerates every (app, environment) pair and generates one Application per pair, syncing deploy/k8s/overlays/<env> from that app's own repo into namespace veni-<env> with auto-sync, prune and self-heal.
- Manifests live with the app —
deploy/k8s/base+overlays/{dev,uat,prod}, assembled with Kustomize (not Helm). Bumping a release is a one-lineimages[].newTagchange. Images are published toregistry.venizia.ai; UIs inject env at runtime rather than baking it in. - Secrets are never in git — Vault + the External Secrets Operator materialize them into Kubernetes Secrets the app consumes via
envFrom. - Migrations run as an initContainer (small apps) or a one-shot Job (large apps, to avoid OOM). All migration SQL is idempotent.
Environments
| Environment | Namespace | Host pattern |
|---|---|---|
| Dev | veni-dev | <app>.dev.venizia.ai |
| UAT | veni-uat | <app>.uat.venizia.ai |
| Prod | veni-prod | <app>.veni.ai |
Promotion
Promotion flows through branches and tags:
feature/* → develop deploys to dev; an rc tag promotes to UAT and stamps the passing commit; tagging that same commit for prod runs release-to-prod.yml, which builds prod images and opens a PR in VENIZIA-AI/infra — merging it lets Argo sync. Prod is gated on the matching UAT-passed commit.
Deploying by hand when CI can't run
The pipeline builds the image, pushes it, and bumps the overlay tag for you. When CI is unavailable — a blocked GitHub Actions account, a runner outage — you do those steps yourself. The trap is that every stage fails silently: skip one and Argo still reports Synced/Healthy while serving the old build.
Do all four, in order, for a dev deploy of <app>:
# 1 · Build from the branch you're deploying — NOT the release branch,
# which doesn't have your commit yet. Tag = dev-<short-sha of that commit>.
git switch develop && SHA=$(git rev-parse --short=7 HEAD)
docker build --build-arg APP_VERSION=dev-$SHA -f api/Dockerfile -t registry.venizia.ai/<app>-api:dev-$SHA .
# 2 · Verify the image carries your change before pushing — grep the built
# artifact, don't trust "build succeeded" (a wrong-branch build also succeeds).
docker run --rm --entrypoint sh registry.venizia.ai/<app>-api:dev-$SHA -c 'grep -c <marker> <path-in-image>'
# 3 · Push it — the tag must exist in the registry, or the bump ImagePullBackOffs.
docker push registry.venizia.ai/<app>-api:dev-$SHA
# 4 · Bump the overlay tag on release/dev — a NEW tag, never overwrite a live one
# (same tag = no manifest change = Argo does nothing).
git switch release/dev && git merge --ff-only develop # or open the promotion PR
sed -i "s|newTag: dev-.*|newTag: dev-$SHA|" deploy/k8s/overlays/dev/kustomization.yaml
git commit -am "chore(k8s/dev): <app> → dev-$SHA [skip ci]" && git pushArgo can lag a few minutes; force it with kubectl -n argocd annotate application <app>-dev argocd.argoproj.io/refresh=hard --overwrite. Then confirm the live tag and a content marker on the running pod — the deploy isn't done until the pod actually serves the new bytes.
Common snags
| Symptom | Fix |
|---|---|
| A new image didn't roll out | Nothing auto-detects a new tag — bump images[].newTag in that env's overlay and commit; Argo syncs the one-line change. |
Argo says Synced/Healthy but the app serves old content | The tag in the overlay points at an image whose bytes are stale — commonly a tag that was overwritten, or a manual build off the wrong branch. Argo verifies the tag matches, not that its content is fresh. Use a new tag per build and verify the image before pushing. |
| Prod won't take a commit | Prod is gated on the matching UAT-passed commit — promote through UAT first (an rc tag), then tag that commit for prod. |
| A secret is missing at runtime | It must exist in Vault and be wired through the External Secrets Operator — envFrom only reads the materialized Secret, it can't invent one. |
| A large migration OOMs | Run it as a one-shot Job, not an initContainer, so it isn't bounded by the pod's memory. |
See also
- Build a satellite → Ship — the app author's side: your repo, your pipeline.
- Platform architecture — the model this deploys.
- Services — every service and its
docker-compose, ports and databases.