A fully automated, end-to-end CI/CD pipeline built as part of a DevSecOps training program for engineers supporting the U.S. Department of Education's Federal Student Aid (FSA) systems, under the Accenture Federal Services (AFS) engagement. This project served as a hands-on capstone for ops engineers learning modern DevSecOps workflows — from source control to container security scanning to production deployment.
This pipeline was developed as part of DevOps Camp, an internal DevSecOps training initiative run by Accenture Federal Services (AFS) for engineers supporting the U.S. Department of Education's Federal Student Aid (FSA) portfolio. AFS was a government IT contractor, and the engineers going through this program were ops staff — including subcontractors — responsible for managing the systems that process and deliver federal student aid to millions of Americans.
The goal of the program was to upskill those engineers on the tools and practices that define a modern, security-conscious software delivery pipeline: containerization, CI/CD automation, private artifact management, container image vulnerability scanning, and Kubernetes-based deployments.
Each participant built and maintained their own version of this pipeline as part of the curriculum, connecting a live application repository to a fully automated delivery system backed by Jenkins, Harbor, Nexus, and Kubernetes.
This repository contains the pipeline definitions and supporting automation scripts that drive the build, scan, and deployment lifecycle for a Python web application backed by a PostgreSQL database.
The pipeline is split into two distinct flows:
| Pipeline | File | Purpose |
|---|---|---|
| Application Pipeline | devops-camp-jenkinsfile |
Builds, scans, and deploys the app container |
| Database Pipeline | devops-camp-db-jenkinsfile |
Conditionally builds, scans, and deploys the DB container only when the schema has changed |
GitHub (afs-labs-student)
|
v
Jenkins Agent
/ \
/ \
App Pipeline DB Pipeline
| |
v v
Docker Build Check Harbor for DB changes (check_harbor_db.py)
| |
v (if changed)
Push to |
Harbor Docker Build
Registry |
| v
v Push to Harbor Registry
Harbor |
Security v
Scan Harbor Security Scan
(harbor_scanner.py)
| |
v v
Kubernetes Kubernetes
app-deployment db-deployment
| Component | Role |
|---|---|
| Jenkins | Orchestrates all pipeline stages via agents |
Harbor (harbor.dev.afsmtddso.com) |
Private container registry + vulnerability scanning (AFS MTDDSO-hosted) |
Nexus (nexus.dev.afsmtddso.com) |
Private PyPI proxy for Python dependency management |
| Kubernetes | Container orchestration — deploys to namespace jmedina |
| GitHub | Source of truth for application and database source code |
devops-camp-pipeline/
├── devops-camp-jenkinsfile # CI/CD pipeline for the application
├── devops-camp-db-jenkinsfile # CI/CD pipeline for the database
├── harbor_scanner.py # Triggers and reports on Harbor vulnerability scans
├── check_harbor_db.py # Determines whether a new DB image build is needed
├── app/
│ └── Dockerfile # Container definition for the Python web app
└── db/
└── Dockerfile # Container definition for the PostgreSQL database
The application pipeline runs on every push and handles the full lifecycle of the web application container.
-
Application Repository Clones the student application repo (
afs-labs-student) and captures the current Git commit hash. This hash is used to tag the resulting Docker image, creating a direct traceability link between the deployed image and the source commit. -
Application Docker Build Builds the app container image using
./app/Dockerfileagainst the cloned source. The image is tagged with a compound version identifier:<app-commit-hash>-<pipeline-commit-hash>This dual-hash strategy ensures both the application source version and the pipeline version are captured in every image tag. The image is pushed to Harbor.
-
Security Scanning Invokes
harbor_scanner.pyto trigger a Trivy-backed vulnerability scan of the pushed image via Harbor's API. The pipeline polls for scan completion and prints a JSON summary of findings. See the Harbor Security Scanning section for full details. -
Test Placeholder stage for integration and unit tests.
-
Deploy Performs a rolling update in Kubernetes by patching the
app-deploymentimage reference:kubectl -n jmedina set image deployment/app-deployment app-deployment=<image> kubectl -n jmedina apply -f ./afs-labs-student/kubernetes/config-map.yaml
-
Cleanup Wipes the Jenkins workspace after every run to prevent stale artifacts from affecting future builds.
The database pipeline introduces an important optimization: it only rebuilds and redeploys the database container when the underlying schema has actually changed. This avoids unnecessary downtime and image churn for a component that changes far less frequently than the application.
-
Application Repository Clones the application repo and runs
check_harbor_db.pyto compare the current commit hash fordatabase/database.sqlagainst the hash embedded in the latest DB image tag already in Harbor. -
DB Changes: true (conditional) This entire nested block only executes if
check_harbor_db.pyreturnstrue— meaning the database schema has changed since the last build.- Database Docker Build — Builds the DB image using
./db/Dockerfileand pushes it to Harbor with the same dual-hash tagging scheme used by the app pipeline. - Security Scanning — Runs
harbor_scanner.pyagainst the new DB image. - Deploy — Performs a rolling image update on the
db-deploymentin Kubernetes.
- Database Docker Build — Builds the DB image using
-
Cleanup Same workspace wipe as the application pipeline.
File: harbor_scanner.py
This script acts as the bridge between Jenkins and Harbor's container scanning API (backed by Trivy). It is invoked as a pipeline stage for both the app and database images.
- Queries Harbor's REST API (
/api/v2.0) to retrieve the SHA256 digest of the most recently pushed image artifact. - POSTs a scan initiation request to Harbor. Returns a non-zero exit code and fails the build if the scan request is rejected.
- Polls the scan status endpoint every 4 seconds (up to 5 attempts) until the scan completes.
- Prints the vulnerability summary as formatted JSON to the build log.
python harbor_scanner.py \
-r <registry-host> \
-p <harbor-project-name> \
-i <image-name> \
-c <username>:<password>| Flag | Description |
|---|---|
-r |
Harbor registry hostname |
-p |
Harbor project name |
-i |
Image/repository name |
-c |
Credentials in user:password format |
{
"total": 12,
"fixable": 8,
"critical": 0,
"high": 3,
"medium": 7,
"low": 2,
"none": 0,
"unknown": 0
}File: check_harbor_db.py
This script prevents redundant database rebuilds by comparing the current Git commit hash of the database schema file against the hash stored in the image tag of the latest DB artifact in Harbor.
- Accepts the current commit hash of
database/database.sql(extracted by the pipeline viagit log --follow). - Queries Harbor to retrieve the tag of the latest existing DB image.
- Parses the
<db-hash>-<pipeline-hash>tag format to extract the stored DB hash. - Compares the two hashes:
- If they differ → prints
true(rebuild needed) - If they match → prints
false(skip build) - If any error occurs (image doesn't exist yet, API failure) → prints
true(default to rebuild)
- If they differ → prints
python check_harbor_db.py \
-r <registry-host> \
-p <harbor-project-name> \
-i <image-name> \
-c <username>:<password> \
-h <database-commit-hash>| Flag | Description |
|---|---|
-r |
Harbor registry hostname |
-p |
Harbor project name |
-i |
Image/repository name |
-c |
Credentials in user:password format |
-h |
Git commit hash of the database schema file |
FROM python:3.9
COPY / ./app/
WORKDIR app
RUN pip install --no-cache-dir \
--index-url https://nexus.dev.afsmtddso.com/repository/jmedina-nexus-repo/simple \
-r requirements.txt
ENTRYPOINT ["python"]
CMD ["server.py"]- Based on the official
python:3.9image. - Pulls Python dependencies from a private Nexus repository rather than the public PyPI index — a common enterprise pattern for dependency governance and air-gap compatibility.
- Runs
server.pyas the application entrypoint.
FROM postgres:9.6
COPY database/database.sql /docker-entrypoint-initdb.d/- Based on the official
postgres:9.6image. - Seeds the database on first container startup by placing
database.sqlinto PostgreSQL'sdocker-entrypoint-initdb.d/directory, which is automatically executed when the container initializes for the first time.
Both pipelines use a compound tag format:
<image-name>:<source-commit-hash>-<pipeline-commit-hash>
For example:
harbor.dev.afsmtddso.com/jmedina-harbor-project/app:a1b2c3d-e4f5g6h
This provides two layers of traceability:
- Source commit hash — links the image directly back to the application code that was built.
- Pipeline commit hash — identifies which version of the pipeline definition produced the build, useful for auditing and debugging CI changes over time.
For the database pipeline specifically, the source commit hash is scoped to the last commit that touched database/database.sql, not the overall repo head. This is what enables check_harbor_db.py to make accurate change detection decisions.
| Credential ID | Type | Used For |
|---|---|---|
jmedina-harbor-auth |
Username + Password | Harbor registry authentication (push + API) |
All pipeline stages run on agents labeled jenkins-agent.
| Variable | Value |
|---|---|
HARBOR_REGISTRY |
harbor.dev.afsmtddso.com |
HARBOR_PROJECT |
jmedina-harbor-project |
APP_IMAGE_NAME |
app |
DB_IMAGE_NAME |
db |
This pipeline was part of a hands-on DevSecOps curriculum where engineers were guided through building each component incrementally:
| Module | Skills Covered |
|---|---|
| Containerization | Writing Dockerfiles, building and tagging images |
| Private Registries | Pushing to and pulling from Harbor, managing projects |
| CI/CD Automation | Writing Jenkins declarative pipelines, agents, stages |
| Artifact Management | Nexus as a private PyPI proxy |
| Security Scanning | Integrating Trivy via Harbor API into CI gates |
| Kubernetes Deployment | kubectl set image, rolling updates, config maps |
| Pipeline Scripting | Python automation scripts for registry interaction and conditional logic |
The separation of the application and database pipelines — and the smart change-detection logic in the DB pipeline — reflects a real-world concern that participants were expected to understand and solve as part of their training: don't rebuild and redeploy things that haven't changed.
This project was developed as part of an internal government training program. All code is intended for educational use.