GitOps & deploy
The image is in GHCR; now make Argo CD deliver it via
GitOps. Copy workloads/penvoice/ (base + overlays/staging + overlays/prod) and add two
entries to the list generator in apps/workloads.yaml. The root app already watches apps/.
Work through the manifests in base, then overlay-patch host, Infisical environmentSlug, namespace, and image tag.
flowchart TD Root[root Application] --> AS[apps/workloads.yaml] AS --> Stg[workloads/app/overlays/staging] AS --> Prod[workloads/app/overlays/prod] Stg --> RO[Rollout + Redis + Ingress] Prod --> RO2[Rollout + Redis + Ingress]
apps/workloads.yaml
Section titled “apps/workloads.yaml”Add two list elements (env: staging / env: prod) for the new app. Generated Applications use
path: workloads/<app>/overlays/<env> and namespace <app>-<env>.
workloads/<app>/ skeleton
Section titled “workloads/<app>/ skeleton”workloads/<app>/├── base/│ ├── rollout.yaml│ ├── service.yaml│ ├── ingress.yaml│ ├── redis.yaml│ └── infisical-*.yaml└── overlays/{staging,prod}/ ├── kustomization.yaml # namespace, images.newTag, patches └── namespace.yamlAdd postgres-cluster.yaml + scheduledbackup.yaml if the app needs a database —
see Database & migrations. Do not add ServiceMonitor / AnalysisTemplate until a
metrics stack exists — Canary. The three infisical-*.yaml files are in
Secrets — do that page first; the Rollout envFroms the Secret they produce.
overlays/<env>/namespace.yaml
Section titled “overlays/<env>/namespace.yaml”apiVersion: v1kind: Namespacemetadata: name: <app>-<env>Rollout
Section titled “Rollout”A drop-in replacement for a Deployment that adds canary steps
(Argo Rollouts provides the Rollout kind). Pin the
image to the sha-<short> tag from CI — the overlay images.newTag is the deploy trigger. Almost all env comes
via
envFrom the Infisical-managed Secret; only values an operator mints (DB_URL) or that are coupled to this manifest
(API_PORT) stay as literal/secretKeyRef entries — see Secrets for why.
apiVersion: argoproj.io/v1alpha1kind: Rolloutmetadata: name: <app>-api namespace: <app>-<env>spec: replicas: 2 revisionHistoryLimit: 3 selector: matchLabels: app: <app> template: metadata: labels: app: <app> annotations: <domain>/redeploy: "init" # bump this value to trigger a canary spec: containers: - name: <app> image: ghcr.io/<owner>/<app>:sha-XXXXXXX ports: - name: http containerPort: 8080 envFrom: - secretRef: name: <app>-api-kc # Infisical-managed; see Secrets env: - name: DB_URL valueFrom: secretKeyRef: name: <app>-pg-app # CNPG auto-generated; see Database & migrations key: uri - name: API_PORT # coupled to containerPort below; stays a literal, not in Infisical value: "8080" readinessProbe: httpGet: { path: /readyz, port: http } initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 livenessProbe: httpGet: { path: /healthz, port: http } initialDelaySeconds: 5 periodSeconds: 10 resources: requests: { cpu: 50m, memory: 64Mi } limits: { memory: 128Mi } strategy: canary: steps: - setWeight: 25 - pause: { duration: 60 } - setWeight: 50 - pause: { duration: 60 } - setWeight: 75 - pause: { duration: 60 }Prod overlays keep these steps. Staging typically uses one replica and setWeight: 100. No analysis block until a
metrics stack exists.
Service
Section titled “Service”One Service in front of the Rollout selects both stable and canary pods.
apiVersion: v1kind: Servicemetadata: name: <app> namespace: <app>-<env> labels: app: <app>spec: selector: app: <app> ports: - name: http port: 80 targetPort: httpIngress
Section titled “Ingress”Public HTTPS via the Ingress handled by
Traefik, with cert-manager issuing the cert from the prod ClusterIssuer
(letsencrypt-prod). The DNS A record (<app>.<domain> → node public IP) must exist before the cert can issue (HTTP-01
challenge).
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: <app> namespace: <app>-<env> annotations: cert-manager.io/cluster-issuer: letsencrypt-prod traefik.ingress.kubernetes.io/router.entrypoints: websecurespec: ingressClassName: traefik tls: - hosts: [<app>.<domain>] secretName: <app>-tls rules: - host: <app>.<domain> http: paths: - path: / pathType: Prefix backend: service: name: <app> port: number: 80If the app has a database, set it up in Database & migrations. Canary shape: Canary.