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

github.com/luiszaragh/DevOps-CICD-Pipeline-Project

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_modules tree. Running first means the report describes code that was actually written.
Scanners report, they do not block
Trivy runs with --exit-code 0 and 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 1 or 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 explicit docker save into the node's containerd, deployments fail with ErrImageNeverPull while the image sits right there in docker images.
The post always block 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.

Ask about Luis

AI assistant — it can be wrong.