DevSecOps CI/CD pipeline
A Jenkins pipeline that scans, tests, builds, deploys and observes a containerised app on Kubernetes — fifteen stages, five security and quality tools, and a dashboard fed by the pipeline’s own output.
- Jenkins
- Docker
- Kubernetes
- Trivy
- Gitleaks
- SonarQube
- Prometheus
- Grafana
- Node.js
The problem
Most CI/CD portfolio projects demonstrate a pipeline by building an application. That gets the emphasis backwards: the interesting engineering is in the pipeline, and a large application buries it.
So the application here is deliberately small — a Node.js service whose only job is to be something worth building, scanning, deploying and observing. Every stage needs a real target; none of them need a complicated one. The tooling is the subject.
Architecture
Fifteen stages in a declarative Jenkins pipeline, grouped into six phases. Each phase either produces an artifact the next one consumes, or refuses to continue.
Prepare clean workspace → checkout → preflight
(fails fast if docker, kubectl, trivy,
gitleaks or node is missing)
Security gitleaks secrets, including git history
(source) trivy fs HIGH/CRITICAL on source
Build/test npm ci reproducible install from the lock file
jest 53 tests, ~100% coverage — gates the build
Quality sonarqube static analysis + quality gate poll
Security docker production image, Alpine, non-root
(image) trivy HIGH/CRITICAL on the built image
Deploy import image into the cluster node
kubectl apply — app (2 replicas), Prometheus, Grafana
rolling update to the new build number
Verify pod readiness → /health in-cluster
Prometheus must answer app_uptime_seconds
Grafana must report its database healthy
post always writes pipeline-status.json into a ConfigMap
the app reads — even when the build failed The last stage is the one worth pointing at. The pipeline publishes its own results — stage outcomes, scan counts, pod counts, build history — into a Kubernetes ConfigMap that the deployed app reads and renders. The dashboard is not a mock; it is the pipeline describing what it just did.
Decisions worth defending
- Trivy runs before
npm ci -
Scanning after dependency install buries findings in a freshly downloaded
node_modulestree. Running first means the report describes code that was actually written. - Scanners report, they do not block
-
Trivy runs with
--exit-code 0and SonarQube marks the build unstable rather than aborting. That is the right call for a lab where the point is to see every stage run, and the wrong call in production — where it should be--exit-code 1or a policy gate. The tradeoff is recorded rather than hidden. - The image is imported into the cluster node
-
With
imagePullPolicy: Never, an image built on the host is invisible to the kubelet — they use different container stores. Without an explicitdocker saveinto the node's containerd, deployments fail withErrImageNeverPullwhile the image sits right there indocker images. - The
post alwaysblock does real work - Publishing status only on success means a failed build leaves the dashboard showing stale, misleading data. Writing it unconditionally means the dashboard is always describing the most recent run, including a broken one.
What broke
A failed SonarQube quality gate was displaying as Passed on the dashboard. Jenkins knew the truth; the dashboard did not.
The cause was scoping. The gate result was being read inside the catchError
block that keeps a failed gate from aborting the pipeline — so when the gate failed, the
step that recorded the result was itself skipped, and the dashboard fell back to the last
good value. The fix was to read the result from sonar-qg.env outside the catchError, so the recorded outcome is the real one whether
the gate passed or not.
It surfaced by cross-checking the live dashboard against the Jenkins console output rather than trusting either on its own — the same instinct that later found the real cause of an OIDC failure on this site by reading CloudTrail instead of the client's error message.
A quality gate that reports success when it failed is worse than having no quality gate. It converts a safety mechanism into a source of false confidence, which is the failure mode every gate in this repository's own CI was designed against.
Outcome
- Fifteen stages running end to end on macOS and Ubuntu, from clean checkout to verified deployment.
- Five security and quality tools integrated: Gitleaks, Trivy filesystem, Trivy image, SonarQube, and Jest.
- 53 tests at roughly 100% coverage, gating the build.
- Smoke tests that check the app in-cluster and require Prometheus and Grafana to answer before the build is called good.
- Documented as ADRs, a runbook, a recovery checklist and a security model — not just a README.
What it is not: a production pipeline. There is no registry, no immutable digests, no staged environments and no GitOps. Those are named as the production alternative for every demo shortcut taken, which is the honest version of a portfolio project.