GitOps & deploy
The image is in GHCR; now make Argo CD deliver it via
GitOps. Add one Application in apps/ pointed at a
workloads/<app>/ folder. Because the root app watches apps/,
the new file is picked up on the next sync — no manual registration.
Work through the manifests below in order: namespace, ConfigMap, Rollout, Service, Ingress.
flowchart TD Root[root Application] --> App[apps/app.yaml] App --> WL[workloads/app/] WL --> NS[namespace] WL --> RO[Rollout] WL --> SVC[Service] WL --> ING[Ingress] ING -->|app.domain| Traefik[Traefik + cert-manager]
apps/<app>.yaml
Section titled “apps/<app>.yaml”The Application that points Argo CD at the folder. selfHeal reverts manual drift;
prune deletes resources you remove from Git; CreateNamespace=true makes the
destination namespace if it’s missing.
apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: <app> namespace: argocdspec: project: default source: repoURL: https://github.com/<owner>/gitops.git targetRevision: main path: workloads/<app> destination: server: https://kubernetes.default.svc namespace: <app> syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=trueworkloads/<app>/ skeleton
Section titled “workloads/<app>/ skeleton”workloads/<app>/├── namespace.yaml├── configmap.yaml # non-secret env (see Secrets for the rest)├── rollout.yaml├── service.yaml└── ingress.yamlAdd postgres-cluster.yaml + migrations-job.yaml + scheduledbackup.yaml if the
app needs a database — see Database & migrations.
Add servicemonitor.yaml + analysistemplate.yaml for the canary gate — see
Canary & metrics.
namespace.yaml
Section titled “namespace.yaml”apiVersion: v1kind: Namespacemetadata: name: <app>configmap.yaml
Section titled “configmap.yaml”Non-secret env only. Secrets (DB_URL, client secrets) are injected from
Secrets in the Rollout, never here.
apiVersion: v1kind: ConfigMapmetadata: name: <app>-config namespace: <app>data: HTTP_ADDR: ":8080" LOG_LEVEL: "info" LOG_FORMAT: "json" APP_ENV: "staging" # ...app-specific non-secret config...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 — bumping this tag is the deploy trigger.
Env splits into non-secret (envFrom the ConfigMap) and secret (secretKeyRef).
apiVersion: argoproj.io/v1alpha1kind: Rolloutmetadata: name: <app> namespace: <app>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: - configMapRef: name: <app>-config env: - name: DB_URL valueFrom: secretKeyRef: name: <app>-pg-app # CNPG auto-generated; see Database & migrations key: uri 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 }The analysis block that gates these steps on a Prometheus
metric is added in Canary & metrics. Without it the
canary just steps on a timer.
Service
Section titled “Service”One Service in front of the Rollout selects both stable and canary pods (sufficient
for a replica-based canary). The app: <app> label lets the ServiceMonitor select
it later.
apiVersion: v1kind: Servicemetadata: name: <app> namespace: <app> labels: app: <app> # so the ServiceMonitor can select this Servicespec: 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> 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. Otherwise jump to Canary & metrics to wire the success-rate gate.