Overview
Pushing to main starts 9 stages / 22 jobs in GitLab CI: lint, build, test, security scan, keyless signing, tag bump, dev sync, and smoke test. Staging promotion is a manual gate preceded by a cryptographic re-verification of the image signature; the Argo Rollouts canary then steps 10% → 50% → 100%, each step gated by a Prometheus app_error_rate AnalysisRun. No kubectl between commit and canary.
gitops-pipeline is an end-to-end GitOps delivery pipeline: GitLab CI builds, scans, and keyless-signs a container image; a Helm chart in the same repo is the source of truth; Argo CD reconciles two local Kubernetes environments (dev + staging) to it; staging releases are Argo Rollouts canaries gated by real Prometheus error-rate analysis. Cloud-hosted GitLab runners reach the local cluster through ngrok and cloudflared tunnels.
This writeup covers the architecture, the security decisions, and the roadblocks that shaped the system. The flow in one line: push → lint → build → test → scan → cosign+sbom → bump values → argocd sync dev → smoke test → manual approve → re-verify signature → sync staging → canary (10 → 50 → 100) with Prometheus gates. Signed end-to-end and gated by real metrics, with zero cloud infrastructure spend.
Verified Run Snapshot
| Metric | Recorded Value |
|---|---|
| Pipeline ID |
#2824754659 (9 stages, 22/22 jobs green) |
| Application Version |
v0.2.0 (Image tag: 0891d917) |
| GitOps Controller | Argo CD v3.5.2 (App-of-Apps + ApplicationSet) |
| Environment State |
gitops-dev & gitops-staging (Healthy / Synced) |
| Canary Rollout | Step 7/7 (ActualWeight: 100%, 2/2 AnalysisRuns Successful) |
| Canary Gate Metric |
app_error_rate{job="app-chart-staging"} == 0 (Threshold: ≤ 0.05) |
What Makes This Different from a Basic CI/CD Pipeline
- Git is the Desired-State Authority: CI never applies manifests directly to Kubernetes. CI updates the desired state in git, and Argo CD reconciles actual cluster state against it.
-
Closed-Loop Tag Updates: CI commits the built SHA back into the Helm chart's
values.yaml([skip ci]), ensuring the repository reflects exactly what is running. - Cryptographic Promotion Gate: Staging promotion requires human approval AND an automated cryptographic re-verification of the Cosign image signature.
-
Metric-Driven Canaries: Rollouts don't just rely on sleep timers; they query live Prometheus metrics (
app_error_rate) to abort and self-heal on failure. - Zero Plaintext Secrets: Secrets stay SOPS-encrypted at rest in git and are only decrypted in-memory during sync by an Argo CD ConfigManagementPlugin (CMP).
Architecture & Mental Model
Seven moving parts, one repo: GitLab CI drives the pipeline; Helm renders one chart with three value sets; Argo CD (app-of-apps → ApplicationSet) reconciles gitops-dev and gitops-staging; SOPS + age + a CMP sidecar decrypt secrets at apply time; Argo Rollouts stages releases as canaries; Prometheus passes judgement on them; ngrok + cloudflared let Cloud-hosted GitLab runners reach the local cluster.
[ git push main ]
│
▼
GitLab CI/CD (.gitlab-ci.yml — 9 stages, 22 jobs)
├── validate ── hadolint, kubeval (manifest validation)
├── build ── Docker build → GitLab Registry (SHA tag: 0891d917)
├── test ── Pytest with coverage threshold gate (70%)
├── scan ── Trivy, Semgrep SAST, pip-audit, SOPS encryption detect
├── sign ── Cosign keyless (Sigstore Fulcio) + Syft CycloneDX SBOM
├── push ── tag :latest + commit image tag to helm/app-chart/values.yaml [skip ci]
├── deploy-dev ── Argo CD sync gitops-pipeline-dev (via ngrok tunnel)
├── smoke-test ── curl /health via cloudflared tunnel
└── promote (manual gate → re-verify signature)
└── deploy-staging ── Argo CD sync gitops-pipeline-staging
│
▼
Argo Rollouts Canary
10% → AnalysisRun (Prometheus) → 50% → AnalysisRun → 100%
Key Technical Decisions & Deep Dive
One repository, one commit, one provenance trail: the app source, the CI definition, the Helm chart, the SOPS-encrypted secrets, and the Argo CD bootstrap manifests all share a single history — so every image tag traces back to a specific commit a human reviewed. Three decisions explain the rest:
1. Keyless Supply-Chain Security with Cosign & Sigstore
Rather than managing static private keys and rotation overhead, signing uses short-lived OIDC workload identity tokens issued by GitLab CI.
- Cosign exchanges the GitLab OIDC token with Sigstore Fulcio to obtain an ephemeral X.509 certificate.
- The signature and transparency log entry (Rekor) are attached directly to the registry image.
- At the staging gate,
promote:verify-signaturere-validates:
cosign verify \
--certificate-identity-regexp ".*gitops-pipeline.*" \
--certificate-oidc-issuer "https://gitlab.com" \
registry.gitlab.com/cypher682/gitops-pipeline:0891d917
2. Secrets Management: SOPS + age with CMP Decrypt-at-Apply
Secrets in secrets/app-secrets.enc.yaml are encrypted using age. Plaintext never exists in git.
- An Argo CD
sops-helmsidecar plugin runs insideargocd-repo-server. - During sync, the plugin executes
helm templateand pipes decrypted SOPS data into the manifest stream, dynamically adjusting the namespace to${ARGOCD_APP_NAMESPACE}.
3. Progressive Canary Delivery with Prometheus
Staging deployments run via Argo Rollouts:
strategy:
canary:
steps:
- setWeight: 10
- pause: {duration: 120s}
- analysis:
templates:
- templateName: error-rate-check
- setWeight: 50
- pause: {duration: 120s}
- analysis:
templates:
- templateName: error-rate-check
- setWeight: 100
Each AnalysisRun executes an in-cluster Prometheus query against the app's /metrics endpoint:
app_error_rate{job="app-chart-staging"}
If app_error_rate > 0.05 at any check, the release immediately halts and rolls back. Two gated steps exist instead of one so a bad release can fail at 10% (minimal blast radius) and again at 50% (under real load) before the final cutover — the cost is a few extra minutes, the payoff is two independent chances to abort. Note that in this setup without a Service Mesh traffic router, weights apply across the ReplicaSet pod distribution.
Engineering Roadblocks & Solutions
Three infrastructure bugs cost the most time — each with a fix you'll reuse:
-
Argo CD + ngrok 307 Redirect Loop: ngrok terminates TLS and forwards cleartext; Argo CD's TLS listener issued a 307 redirect loop.
Fix: Set
server.insecure: "true"inargocd-cmd-params-cm. -
The argocd CLI URL Parsing Trap:
argocd login https://…dials a bogustcp///host:portproxy;--server https://hostbecomeshttps://https//host. Fix: Use barehost:port+ per-command--auth-token, bypassing persistent login. -
CMP Context Blindspot: With
source.path: helm/app-chart, the repo server only shipped the chart subdirectory — breaking../secrets/*.enc.yaml— and anAnalysisTemplatereferenced outside the chart made the rollout instantly degrade. Fix: Setsource.path: "."so the CMP sees the repo root, and bundleanalysis-template.yamlinto the chart templates. -
ngrok Free-Tier URL Collision: Free accounts only provide one static domain.
Fix: Used
cloudflaredquick tunnels (accountless, ephemeral) for the second exposed app endpoint.
Try It (and Steal It)
Everything is one repo: app + CI + Helm chart + Argo CD bootstrap config + SOPS-encrypted secrets. git push → green pipeline → canary. Fork it, swap the namespace/image names, and follow the README Quickstart + docs/.
Curious what you'd add — a Service Mesh traffic router for true weighted HTTP splits? Kyverno for cluster-level signature enforcement? Multi-cluster Argo CD? Let me know in the comments.
- GitHub Repository: https://github.com/cypher682/gitops-pipeline
- Tech Stack: GitLab CI, Argo CD, Argo Rollouts, Prometheus, SOPS, age, Cosign, Helm, Kubernetes, ngrok, cloudflared.
Top comments (0)