Enterprise Multibranch CI/CD Pipeline with SonarQube & Trivy
Make a commit build, get scanned for code and image vulnerabilities, and deploy itself — with gates that block.
- Time
- 31 min
- Level
- Advanced
- Objectives
- 4 objectives
- Cost
- Low cost
Where this fits in the platform
Already built
This lab adds
- Gates that stop a vulnerable image before it is published
Before you start
Cost — Low cost
for the Jenkins instance itself; SonarQube wants ~2 GB of RAM, so a `t3.small` (~$15/month) is realistic. ECR storage for the images the pipeline pushes is inside the free tier at this scale.
The scenario#
The pipeline is green. It is also building latest, pushing before it scans, and finishing the moment kubectl set image returns — so a deploy that never becomes ready reports success.
A gate that reports instead of blocking is a dashboard, not a gate.
Hands-on environment
Run it on your own machine
Run this lab on your own machine. One command starts the environment, with everything the lab needs already installed:
Stages 1–7 run locally: checkout, build, unit tests, SonarQube analysis, quality gate, image build and Trivy scan all work against the local registry. The last stages push to ECR and deploy to EKS, and those need an AWS account.
You will need:
- docker
git clone https://github.com/EgyKode/EgyKode-lab.git
cd EgyKode-lab
./egykode start cicd
./egykode shellYou need Docker and Git installed. Everything else runs inside the environment. The first start downloads it and takes a few minutes; later starts are seconds.
Not sure what you already have? Run: npm run doctor — it checks and changes nothing.
Run it on AWS
This lab builds real cloud infrastructure, so it needs your own AWS account. Follow the cost and cleanup notes above — the resources are yours, and so is the bill.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
The Jenkinsfile
Step 1 of 3
What you are building#
checkout -> unit tests -> SonarQube gate -> build image -> Trivy scan
|
CRITICAL+fixable? ------+--> FAIL, no push
|
v
push to ECR -> deploy -> waitOrder is the design. Scan before the push, because scanning afterwards means the vulnerable image is already in the registry and someone can pull it. Cheap checks first, so an expensive scan only runs on code that compiles.
Build it#
What you are proving: You can build, test, scan and deploy from one commit with no manual step, and fail the pipeline when a gate or a rollout does not pass
Marking this settles success criteria 1 and 3 and 4.
pipeline {
agent any
environment {
REGISTRY = "111122223333.dkr.ecr.us-east-1.amazonaws.com"
IMAGE = "platform/api"
TAG = "${env.BUILD_NUMBER}-${env.GIT_COMMIT.take(7)}"
}
options {
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '30'))
}
stages {
stage('Unit tests') {
steps {
sh 'docker run --rm -v "$PWD":/src -w /src python:3.12-slim sh -c "pip install -q -r requirements.txt && pytest -q --junitxml=report.xml"'
}
post { always { junit 'report.xml' } }
}
stage('SonarQube') {
steps {
withSonarQubeEnv('sonarqube') {
sh 'sonar-scanner -Dsonar.projectKey=platform-api'
}
}
}
stage('Quality gate') {
steps {
timeout(time: 10, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true // <- the gate
}
}
}
stage('Build') {
steps { sh 'docker build -t "$REGISTRY/$IMAGE:$TAG" .' }
}
stage('Scan') {
steps {
sh '''
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 --format table "$REGISTRY/$IMAGE:$TAG"
'''
}
}
stage('Push') {
steps {
sh '''
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin "$REGISTRY"
docker push "$REGISTRY/$IMAGE:$TAG"
'''
}
}
stage('Deploy') {
steps {
sh '''
aws eks update-kubeconfig --name platform --region us-east-1
helm upgrade --install api ./chart -n platform --set image.tag="$TAG" --atomic --wait --timeout 5m
kubectl rollout status deploy/api -n platform --timeout=5m
'''
}
}
}
post {
always { sh 'docker rmi "$REGISTRY/$IMAGE:$TAG" || true' }
failure { echo "Build ${env.BUILD_NUMBER} failed at ${env.STAGE_NAME}" }
}
}Four details that separate this from a pipeline that only looks finished:
abortPipeline: trueis what makes the quality gate a gate. Without it, Sonar reports and the build continues.--exit-code 1is the same idea for Trivy. Without it the scan prints a table nobody reads.--ignore-unfixedkeeps the gate actionable. Failing on a CVE with no available patch gives the team no move except to disable the check, which is how gates die.--atomic --waitplusrollout status --timeoutis what makes the pipeline fail when the deploy fails.helm upgradealone returns as soon as the API accepts the manifest.
What you are proving: You can authenticate to a registry with no credential stored in Jenkins at all
Marking this settles success criterion 2.
// Avoid this:
withCredentials([string(credentialsId: 'aws-key', variable: 'AWS_SECRET')]) { ... }The Jenkins host runs with an IAM instance profile, so aws and docker login work with no stored key at all. A credential that does not exist cannot
leak from the credential store.
What you are proving: You can put the branch policy in the file everyone reviews, rather than in job configuration nobody sees
This step settles no success criterion on its own.
stage('Deploy') {
when { branch 'main' }
steps { ... }
}A multibranch job discovers every branch and runs its Jenkinsfile. Feature
branches get tests, Sonar and a scan; only main deploys. One file, and the
policy is visible in it rather than living in job configuration nobody can
review.
Verify it worked#
# The image was tagged by commit, never latest
aws ecr describe-images --repository-name platform/api \
--query 'sort_by(imageDetails,&imagePushedAt)[-1].imageTags'
# The running image matches what the pipeline pushed
kubectl get deploy api -n platform \
-o jsonpath='{.spec.template.spec.containers[0].image}'Prove each gate blocks, rather than trusting it:
# 1. A knowingly vulnerable base — the build must FAIL at Scan, with no push
# A frozen point release, NOT an end-of-life distribution: an EOL distro
# ships no fixes, so --ignore-unfixed discards nearly everything it carries.
# Measured: debian:12.5-slim 15 findings, debian:10 exactly 1, ubuntu:18.04 none.
echo "FROM debian:12.5-slim" > Dockerfile.vuln
docker build -f Dockerfile.vuln -t probe:vuln .
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 probe:vuln
echo "exit=$?" # 1
# 2. The image must NOT be in the registry after a failed build
aws ecr describe-images --repository-name platform/api \
--query 'imageDetails[].imageTags' | grep vuln || echo "never pushed — correct"
# 3. Break the quality gate deliberately and confirm the pipeline stops
# (add a blocker-level issue, or lower the gate threshold in SonarQube)A gate you have never seen fail is a gate you cannot claim works.
waitForQualityGate hangs until the timeout
SonarQube cannot reach Jenkins to post the webhook. Configure the webhook in
SonarQube, pointing at <jenkins>/sonarqube-webhook/.
Trivy fails every build after a while
New CVEs are published against a base image that has not moved. Pin and update
the base deliberately, and keep --ignore-unfixed so the gate stays
actionable.
no basic auth credentials on push
The ECR token expired — they last 12 hours. get-login-password must be a
pipeline step, not something a human ran once.
The pipeline is green and nothing deployed
kubectl set image or helm upgrade returned immediately. Add
rollout status --timeout and --atomic --wait.
You must be logged in to the server
IAM let you fetch a kubeconfig; EKS has not authorised the principal. Add an access entry for the Jenkins instance role.
Two builds of the same branch corrupt each other
disableConcurrentBuilds().
Clean up#
docker image prune -af
aws ecr list-images --repository-name platform/api --filter tagStatus=UNTAGGED \
--query 'imageIds[]' --output json > /tmp/untagged.json
aws ecr batch-delete-image --repository-name platform/api --image-ids file:///tmp/untagged.jsonCost of this lab: Low. The Jenkins host and the cluster bill anyway. ECR storage is $0.10/GB-month, which is why the lifecycle policy from the ECR lab matters.
Maintained by others, on Killercoda. Useful for extra repetition on one tool — it does not complete this lab or settle any criterion above.
- Trivy scenariosthe scanner this pipeline gates on
Success criteria
0 of 4
The concept behind it
Next up
Lab 46 of 59 on the project path