CI/CD Pipeline with Jenkins and ArgoCD

Build a secure CI/CD pipeline on AWS with Jenkins, Trivy, and ArgoCD on EKS.

Introduction

30 Second Summary

Every time a developer pushes code, someone has to build it, check it for security issues, package it, and deploy it. Doing all of that by hand is slow and error-prone. Automated pipelines handle the entire flow in minutes, every single time.

In this project, you will build a complete CI/CD pipeline on AWS using Jenkins, Trivy, and ArgoCD. You will push code to GitHub, have Jenkins automatically build a Docker image, scan it for vulnerabilities, push it to Docker Hub, and watch ArgoCD deploy it to a live Kubernetes cluster on EKS.

What You'll Build

By the end, you will push a single commit and watch an attendance tracker app flow through build, security scan, registry push, and GitOps deployment to a live Kubernetes cluster you can open in your browser.

By the end of this project, you'll have:

  • A working Jenkins CI pipeline that builds a Docker image from your code, scans it with Trivy for critical CVEs, and pushes the clean image to Docker Hub.
  • A security gate that blocks vulnerable container images from ever reaching your registry, so you can see exactly which CVEs Trivy catches before deployment.
  • An ArgoCD-managed Kubernetes deployment on EKS that auto-syncs from GitHub. Update a manifest, push it, and watch your attendance tracker update live within seconds.
  • Secret Mission: Add a GitHub webhook so every push triggers the full pipeline automatically, with zero manual clicks.

Are there any prerequisites?

This project assumes you already have an AWS account with the CLI configured, a GitHub account, and a Docker Hub account. You should also have prior experience working with Docker, Kubernetes, and Jenkins.

The project provisions cloud resources that cost approximately $3-5 if you tear everything down within 2 hours.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters.

You're constructing a full CI/CD pipeline on AWS where a single git push flows through build, security scan, registry push, and GitOps deployment to a live Kubernetes cluster.

Launch EC2 and Install CI Tools

Every automated pipeline needs a home. Before Jenkins can build your Docker images or Trivy can scan them, you need a server where all these tools live and run together.

In this step, you will launch an EC2 instance on AWS and install the full CI toolchain: Java, Jenkins, Docker, and Trivy. By the end, you will have a Jenkins server ready to unlock in your browser.

In this step, get ready to:
  • Launch an EC2 instance with Ubuntu 24.04 and configure its security group.
  • Install Java 21, Jenkins, and Docker on the instance.
  • Install Trivy for container image security scanning.
Launch an EC2 instance and SSH in

Your Jenkins server needs an EC2 instance with enough resources to run builds. A t2.medium instance gives you 2 vCPUs and 4GB RAM, which is the minimum for running Jenkins alongside Docker.

  • Sign in to the AWS Management Console.
  • Navigate to the EC2 service by typing EC2 in the search bar and selecting it.
  • Click Launch instance.
  • Enter a name for your instance, such as jenkins-server.
  • Under Application and OS Images, select Ubuntu. Choose Ubuntu Server 24.04 LTS from the dropdown.
  • Under Instance type, select t2.medium.
  • Under Key pair, select an existing key pair or create a new one. Download the .pem file if creating new.
  • Under Network settings, click Edit.

You need to open three ports for your tools to be accessible from outside the instance.

  • Add a security group rule for port 22 (SSH) with source 0.0.0.0/0.
  • Add a second rule for port 8080 (Jenkins) with source 0.0.0.0/0.
  • Add a third rule for port 443 (HTTPS) with source 0.0.0.0/0.
  • Under Configure storage, change the root volume size to 30 GB.
  • Click Launch instance.

Why 30GB of storage?

Docker images can be large. Jenkins stores build artifacts and workspace files. 30GB gives enough room for multiple builds without running into disk space errors mid-pipeline.

  • Wait for the instance state to show Running in the EC2 dashboard.
  • Copy the Public IPv4 address from the instance details.
  • SSH into the instance by running this command:
ssh -i [[PEMPATH="path/to/your-key.pem"]] ubuntu@[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]

You should see the Ubuntu welcome message and a command prompt showing ubuntu@ip-....

Connection refused or timed out?

  • Confirm the instance state is Running (not Pending).
  • Check that your security group has port 22 open.
  • Verify you are using the correct .pem file and the correct public IP.

If your key has permission errors, run chmod 400 your-key.pem first.

Help me troubleshoot SSH connection issues to my EC2 instance.

Install Java 21, Jenkins, and Docker

Jenkins requires a Java runtime to operate. Docker is what your pipeline will use to build container images. You will install both now so Jenkins can orchestrate Docker builds.

  • Update the package index and install Java 21 by running these commands:
sudo apt update
sudo apt install fontconfig openjdk-21-jre -y
  • Verify the Java installation by running:
java -version

You should see output containing openjdk version "21.

Why Java 21?

Jenkins LTS requires Java 17 or Java 21 since version 2.479.1. Java 21 is the current recommended long-term support release and ensures compatibility with the latest Jenkins features.

  • Add the Jenkins repository key and source list by running these commands:
sudo wget -O /usr/share/keyrings/jenkins-keyring.asc https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null

What do these commands do?

  • The first command downloads Jenkins' GPG signing key so your system trusts the Jenkins package repository.
  • The second command adds the Jenkins LTS (Long Term Support) repository to your apt sources.
  • Install Jenkins by running:
sudo apt-get update
sudo apt-get install jenkins -y
  • Start Jenkins and verify it is running:
sudo systemctl start jenkins
sudo systemctl status jenkins

You should see active (running) in green text. Press q to exit the status view.

Now install Docker so Jenkins can build container images.

  • Set up Docker's official apt repository by running these commands:
sudo apt update
sudo apt install ca-certificates curl -y
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
  • Add the Docker repository to your apt sources by running:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
  • Install Docker Engine by running:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
  • Add the jenkins user to the docker group so Jenkins can run Docker commands without sudo:
sudo usermod -aG docker jenkins
  • Restart Jenkins to pick up the new group membership:
sudo systemctl restart jenkins
  • Verify Docker is running by running:
sudo docker run hello-world

You should see a message that says Hello from Docker! confirming Docker is installed and working.

Docker command not found?

  • Verify the Docker apt repository was added correctly by checking /etc/apt/sources.list.d/docker.sources exists.
  • Re-run sudo apt update and check for errors about the Docker repository.

Help me fix Docker installation issues on Ubuntu 24.04.

Install Trivy and verify the Jenkins unlock page

Trivy is a vulnerability scanner that will act as your security gate. It inspects container images for known CVEs before they reach your Docker Hub registry.

  • Install prerequisites and add the Trivy apt repository by running these commands:
sudo apt-get install wget apt-transport-https gnupg -y
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
  • Install Trivy by running:
sudo apt-get update
sudo apt-get install trivy -y
  • Verify Trivy installed correctly by running:
trivy --version

You should see the Trivy version number printed in the output.

What does Trivy do?

Trivy scans container images, filesystems, and Git repositories for known vulnerabilities (CVEs). In your pipeline, it will check every Docker image before it gets pushed to Docker Hub. If it finds critical vulnerabilities, it blocks the push.

Before you run the final check, what do you think you will see when you open your EC2 instance's public IP on port 8080 in a browser?

  • Open your browser and navigate to http://your EC2 public IPv4 address:8080.

You should see the Jenkins unlock page asking for an initial admin password. This confirms Jenkins is running and accessible from outside the instance.

Retrieve the Jenkins initial admin password

When you are ready to unlock Jenkins in the next step, run this command to get the password:

sudo cat /var/lib/jenkins/secrets/initialAdminPassword

Can't reach the Jenkins page in your browser?

  • Confirm Jenkins is running with sudo systemctl status jenkins.
  • Double-check your security group has port 8080 open to 0.0.0.0/0.
  • Make sure you are using http:// (not https://) in the URL.

Help me troubleshoot why I can't access Jenkins on port 8080.

Your CI server is ready to go. You have Java, Jenkins, Docker, and Trivy all installed on one machine. Next up, you will build and containerize the attendance tracker application that will flow through your pipeline.

Build the Attendance Tracker Docker Image

Your Jenkins server is ready to automate builds. But a pipeline needs something to build.

The attendance tracker is a lightweight Flask app with an HTML dashboard for marking employee attendance. Building and running it locally on your EC2 instance first proves the app works before any automation touches it.

In this step, get ready to:
  • Fork and clone the attendance tracker repository to your EC2 instance.
  • Write a Dockerfile to containerize the Flask application.
  • Build the Docker image and run the container to verify the app works.
Fork and clone the attendance tracker repository

Your CI pipeline will pull source code from GitHub. You need a copy of the attendance tracker app in your own GitHub account so Jenkins can access it later.

  • Open the NextWork attendance tracker repository in your browser (the link is provided on the project page).
  • In the top-right corner of the repository page, click Fork.
  • Under Owner, select your GitHub account from the dropdown.
  • Leave the repository name as attendance-tracker.
  • Click Create fork.

Once the fork is ready, you have your own copy containing app.py, templates/index.html, and requirements.txt.

  • Switch back to your SSH session on the EC2 instance and clone your fork by running this command:
git clone https://github.com/[[YOUR_GITHUB_USERNAME="your-github-username"]]/attendance-tracker.git
  • Move into the cloned directory by running:
cd attendance-tracker
  • Confirm the app files are present by running:
ls

You should see app.py, requirements.txt, and a templates folder in the output.

Not seeing the files?

Make sure you replaced your GitHub username in the clone URL. The repository name should be attendance-tracker.

If you see a fatal: repository not found error, double-check that you forked the repository to your own account first.

Help me debug my git clone issue

Create the Dockerfile

A Dockerfile is a recipe that tells Docker how to package your application into a container image. You need one so that Jenkins can build identical, portable images on every pipeline run.

  • Create a new file called Dockerfile in the attendance-tracker directory by running:
nano Dockerfile
  • Paste the following content into the file:
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

What does this Dockerfile do?

  • FROM python:3.11-slim starts from a lightweight Python 3.11 base image.
  • WORKDIR /app sets the working directory inside the container.
  • COPY requirements.txt . copies the dependency list first so Docker can cache the install layer.
  • RUN pip install --no-cache-dir -r requirements.txt installs Flask.
  • COPY . . copies the rest of your application files into the container.
  • EXPOSE 5000 documents that the container listens on port 5000.
  • CMD ["python", "app.py"] runs the Flask app when the container starts.
  • Save and exit nano by pressing Ctrl+O, then Enter, then Ctrl+X.
  • Confirm the file was created by running:
cat Dockerfile

You should see the full Dockerfile contents printed to the terminal.

Seeing a blank file or errors?

Make sure you pressed Ctrl+O (write out) before Ctrl+X (exit). If the file is empty, run nano Dockerfile again and re-paste the content.

Help me fix my Dockerfile

✔️ Awesome, I've got everything!

Great. Make sure you saved your Dockerfile in the attendance-tracker directory.

ⓧ I'd like to double check the full code

Your Dockerfile should look exactly like this:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]
Build and run the Docker image

With the Dockerfile in place, you can build the image and spin up a container. This is the moment you prove the app runs in a container before handing it off to Jenkins.

  • Build the Docker image by running this command:
docker build -t attendance-tracker:v1 .

What does this command do?

  • docker build reads the Dockerfile and assembles the image layer by layer.
  • -t attendance-tracker:v1 tags the image with the name attendance-tracker and version v1.
  • . tells Docker to use the current directory as the build context.

The build downloads the python:3.11-slim base image and installs Flask. This takes a minute or two the first time.

  • Confirm the image was built by running:
docker images

You should see attendance-tracker with the tag v1 in the list.

Before running the container, you need to allow traffic on port 5000 through your EC2 security group so you can view the app in your browser.

  • Switch to the AWS Console in your browser.
  • Navigate to EC2, then Security Groups.
  • Select the security group attached to your Jenkins EC2 instance.
  • Click Edit inbound rules.
  • Click Add rule.
  • Set Type to Custom TCP, Port range to 5000, and Source to 0.0.0.0/0.
  • Click Save rules.

Now run the container. Before you do, predict what you expect to see when you open the app in a browser.

  • Run the container by executing:
docker run -d -p 5000:5000 attendance-tracker:v1

What does this command do?

  • -d runs the container in detached mode so it does not lock your terminal.
  • -p 5000:5000 maps port 5000 on the EC2 host to port 5000 inside the container.
  • Open a new browser tab and go to:
http://[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]:5000

You should see the attendance tracker dashboard with a table of five employees, Present and Absent buttons, and stat cards showing today's attendance counts.

Not seeing the dashboard?

Make sure you added port 5000 to your security group's inbound rules. Without this rule, your browser cannot reach the container.

Check the container is running with docker ps. If no container is listed, check the logs with docker logs $(docker ps -aq | head -1) for error messages.

Also confirm you are using the correct public IPv4 address from the EC2 console, not the private IP.

Help me troubleshoot my Docker container

Your app is containerized and running on EC2. Next up, you'll create a Jenkins pipeline that automates the build and pushes the image to Docker Hub on every run.

Create the Jenkins CI Pipeline

You proved your attendance tracker runs as a Docker container in the last step. But the process of building and pushing that image required SSH-ing in and running manual commands. That approach falls apart the moment your team grows or you need to release more often.

Jenkins automates the entire build-and-push workflow into a repeatable pipeline. In this step, you'll configure Jenkins with the plugins it needs, store your Docker Hub credentials securely, write a Jenkinsfile that defines your pipeline stages, and run your first automated build.

In this step, get ready to:
  • Install the Docker Pipeline and Git plugins in Jenkins.
  • Store your Docker Hub credentials securely in Jenkins.
  • Create and run a Jenkinsfile pipeline that builds and pushes your image to Docker Hub.
Install the Docker Pipeline and Git plugins

Jenkins needs additional plugins to interact with Docker and pull code from Git repositories. Without the Docker Pipeline plugin, your Jenkinsfile cannot run Docker commands.

  • Return to the Jenkins dashboard in your browser at http://<EC2-PUBLIC-IP>:8080.
  • Click Manage Jenkins in the left sidebar.
  • Click Plugins.
  • Select the Available plugins tab.
  • Search for Docker Pipeline and check the box next to it.
  • Search for Git and check the box next to it (if not already installed).
  • Click Install without restart.
  • Wait for the installation to complete. You should see green checkmarks next to each plugin.

Why these two plugins?

The Docker Pipeline plugin lets Jenkins execute Docker build and push commands inside pipeline stages. The Git plugin enables Jenkins to check out your source code from GitHub at the start of each build.

Available plugins list is empty?

If you see no plugins listed, Jenkins has not yet downloaded metadata from its update center. Click the Check now button to force a refresh, wait a moment, then search again.

Help me fix my Jenkins plugin installation.

Add your Docker Hub credentials to Jenkins

Your pipeline needs to authenticate with Docker Hub to push images. Jenkins stores credentials encrypted so they never appear in your pipeline logs.

  • First, create a Docker Hub access token. In a new browser tab, go to https://hub.docker.com/settings/security and sign in.
  • Click New Access Token.
  • Enter a description like jenkins-cicd and click Generate.
  • Copy the generated token and keep it handy. You will not be able to see it again.

Now store these credentials in Jenkins.

  • Back in Jenkins, click Manage Jenkins in the left sidebar.
  • Click Credentials.
  • Under Stores scoped to Jenkins, click System.
  • Click Global credentials (unrestricted).
  • Click Add Credentials.
  • In the Kind dropdown, select Username with password.
  • Set Scope to Global.
  • Enter your Docker Hub username in the Username field.
  • Paste the access token you just copied into the Password field.
  • In the ID field, type dockerhub-creds.
  • Click Create.

You should see your new credential listed with the ID dockerhub-creds in the Global credentials table.

Why an access token instead of your password?

Docker Hub access tokens are scoped and revocable. If the token is compromised, you revoke just that token without changing your account password. This is a security best practice for CI systems.

Don't see the Add Credentials option?

Make sure you navigated into System and then Global credentials (unrestricted). The Add Credentials link appears only inside a credential domain.

Help me add credentials in Jenkins.

Create the Jenkinsfile

A Jenkinsfile is a text file that defines your pipeline stages as code. Jenkins reads it from your repository and executes each stage in order.

  • Switch back to your SSH terminal connected to the EC2 instance.
  • Navigate to your repository directory by running:
cd ~/attendance-tracker
  • Create the Jenkinsfile by running:
nano Jenkinsfile
  • Paste the following pipeline structure, which sets up the environment and checks out your code:
pipeline {
    agent any

    environment {
        // Jenkins credential binding for Docker Hub login
        DOCKERHUB_CREDENTIALS = credentials('dockerhub-creds')
        // Your Docker Hub image path
        IMAGE_NAME = "[[DOCKERHUB_USERNAME="your-dockerhub-username"]]/attendance-tracker"
    }

    stages {
        stage('Checkout') {
            steps {
                // Pull the latest code from GitHub
                git branch: 'main', url: 'https://github.com/[[GITHUB_USERNAME="your-github-username"]]/attendance-tracker.git'
            }
        }

What does this code do?

  • credentials('dockerhub-creds') binds the credential you just created. Jenkins automatically exposes DOCKERHUB_CREDENTIALS_USR and DOCKERHUB_CREDENTIALS_PSW environment variables.
  • IMAGE_NAME stores your Docker Hub image path so every stage can reference it consistently.
  • The Checkout stage pulls your latest code from GitHub at the start of each build.
  • Continuing directly below in the same file, paste the build, push, and cleanup stages:

        stage('Build Docker Image') {
            steps {
                // Build and tag with the Jenkins build number
                sh "docker build -t ${IMAGE_NAME}:${BUILD_NUMBER} ."
                sh "docker tag ${IMAGE_NAME}:${BUILD_NUMBER} ${IMAGE_NAME}:latest"
            }
        }

        stage('Push to Docker Hub') {
            steps {
                // Login and push both tags to Docker Hub
                sh "echo ${DOCKERHUB_CREDENTIALS_PSW} | docker login -u ${DOCKERHUB_CREDENTIALS_USR} --password-stdin"
                sh "docker push ${IMAGE_NAME}:${BUILD_NUMBER}"
                sh "docker push ${IMAGE_NAME}:latest"
            }
        }
    }

    post {
        always {
            // Always logout from Docker Hub when done
            sh "docker logout"
        }
    }
}

What does this code do?

  • The Build stage creates a Docker image tagged with the BUILD_NUMBER (Jenkins increments this on every build) and also tags it as latest.
  • The Push stage logs into Docker Hub using the bound credentials and pushes both tags.
  • The post { always } block runs docker logout after every build regardless of success or failure. This prevents credentials from lingering on the build agent.
  • Save and exit nano by pressing Ctrl+X, then Y, then Enter.
  • Confirm the file was created by running:
ls Jenkinsfile

You should see Jenkinsfile listed in the output.

  • Push the Jenkinsfile to GitHub by running:
git add Jenkinsfile
git commit -m "Add Jenkinsfile for CI pipeline"
git push origin main

Git pushes your Jenkinsfile to GitHub where Jenkins can read it.

Git push asking for authentication?

If Git prompts for credentials, use your GitHub username and a personal access token (not your GitHub password). Generate a token at https://github.com/settings/tokens with repo scope.

Help me push to GitHub from my EC2 instance.

✔️ Awesome, I've got everything!

Great. Make sure you saved your Jenkinsfile and pushed it to GitHub. You are ready to create the Jenkins pipeline job.

ⓧ I'd like to double check the full code

Your Jenkinsfile should contain both chunks above combined into a single file. Scroll up and confirm you have: the pipeline declaration with environment block, the Checkout stage, the Build Docker Image stage, the Push to Docker Hub stage, and the post { always } block with docker logout. Ensure the opening pipeline { has a matching closing } at the very end.

Create the Pipeline job and run your first build

With plugins installed, credentials stored, and the Jenkinsfile in GitHub, you can now create the Pipeline job that ties everything together.

  • In Jenkins, click the Jenkins logo (top left) to return to the dashboard.
  • Click New Item in the left sidebar.
  • Enter attendance-tracker-pipeline as the item name.
  • Select Pipeline as the project type.
  • Click OK.

You are now in the pipeline configuration page.

  • Scroll down to the Pipeline section at the bottom.
  • Change the Definition dropdown to Pipeline script from SCM.
  • Set SCM to Git.
  • In the Repository URL field, enter your GitHub repo URL:
https://github.com/[[GITHUB_USERNAME="your-github-username"]]/attendance-tracker.git
  • Leave Credentials as - none - (your repo is public).
  • Under Branches to build, change */master to */main.
  • Ensure the Script Path field says Jenkinsfile.
  • Click Save.

Before you run the build, what do you think will happen? The pipeline will pull your code, build a Docker image, and push it to Docker Hub. Do you expect all three stages to pass on the first attempt?

  • Click Build Now in the left sidebar.
  • Click the build number (#1) that appears under Build History.
  • Click Console Output to watch the build log in real time.

You should see each stage complete with green checkmarks: Checkout, Build Docker Image, and Push to Docker Hub. The console log ends with Finished: SUCCESS.

  • Verify the image reached Docker Hub. Open https://hub.docker.com/repositories in your browser.
  • You should see your attendance-tracker repository with a tag matching the build number and a latest tag.

What is pipeline-as-code?

By storing your pipeline definition in a Jenkinsfile alongside your application code, you version-control your CI process. Any team member can see exactly how builds work, and changes to the pipeline go through the same review process as code changes.

Build failed at the Push stage?

Double-check that the credential ID in your Jenkinsfile is exactly dockerhub-creds (matching what you entered in Jenkins). Also verify the IMAGE_NAME in your Jenkinsfile uses your actual Docker Hub username, not the placeholder.

If you see a permission denied error for Docker, run sudo chmod 666 /var/run/docker.sock on your EC2 instance and try the build again.

Help me fix my Jenkins pipeline build failure.

Your CI pipeline is fully operational. Every time you click Build Now, Jenkins builds a fresh Docker image and pushes it to Docker Hub without any manual intervention. Next up, you'll add a security gate that scans your images for vulnerabilities before they reach Docker Hub.

Add Trivy Security Scanning to the Pipeline

In the previous step, you pushed a Docker image straight to Docker Hub. Your pipeline built it, tagged it, and shipped it without a second thought.

But what if that image contains known security vulnerabilities? Right now, you have no way of knowing. Any CVE hiding inside your base image or dependencies went straight to your registry, ready to be pulled into production. Trivy adds a security gate that catches these vulnerabilities before they leave your pipeline.

In this step, get ready to:
  • Run a manual Trivy scan to reveal the CVEs hiding in your Docker image.
  • Add a Trivy Scan stage to your Jenkinsfile that blocks critically vulnerable images.
  • Re-run the Jenkins pipeline and verify the security gate works.
Scan your Docker image for vulnerabilities

The image you pushed to Docker Hub in the last step is built on python:3.11-slim. That base image ships with system-level packages, and some of those packages have known CVEs. Let's find out exactly what you shipped.

  • In your SSH session on the EC2 instance, scan the local image you built earlier by running:
trivy image attendance-tracker:v1

Trivy downloads its vulnerability database on the first run, then scans the image layer by layer. After a moment, you will see a summary line showing total CVEs by severity (UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) followed by a detailed table.

Each row in the table shows a library name, the CVE ID, its severity level, the installed version, and the fixed version.

What does this output mean?

Every row is a publicly disclosed vulnerability living inside your container image. The CRITICAL and HIGH entries are the ones that matter most. These are the exact vulnerabilities that were pushed to Docker Hub in the previous step, visible to anyone who pulls your image.

This is the gap in your pipeline. Without a scanning stage, every build ships blind.

Seeing a download error?

Trivy needs to pull its vulnerability database on the first scan. If you see a timeout or network error, wait a moment and try the command again. The EC2 instance needs outbound internet access on port 443.

Help me debug a Trivy database download error on my EC2 instance.

Add the Trivy Scan stage to your Jenkinsfile

Now that you have seen the vulnerabilities firsthand, it is time to automate this check. You will add a Trivy stage to your Jenkinsfile that runs between Build and Push. If Trivy finds any CRITICAL vulnerabilities, the pipeline fails and the image never reaches Docker Hub.

  • Open your Jenkinsfile in your repository.
  • Find the closing braces of the Build Docker Image stage:
        stage('Build Docker Image') {
            steps {
                sh "docker build -t ${IMAGE_NAME}:${BUILD_NUMBER} ."
                sh "docker tag ${IMAGE_NAME}:${BUILD_NUMBER} ${IMAGE_NAME}:latest"
            }
        }
  • Add the following Trivy Scan stage directly after the Build Docker Image stage and before the Push to Docker Hub stage:
        stage('Trivy Scan') {
            steps {
                sh "trivy image --exit-code 1 --severity CRITICAL --format table ${IMAGE_NAME}:${BUILD_NUMBER}"
            }
        }

What does this stage do?

The --exit-code 1 flag tells Trivy to return a non-zero exit code if vulnerabilities of the specified severity are found. Jenkins interprets any non-zero exit as a stage failure, which stops the pipeline.

The --severity CRITICAL flag filters the scan to only fail on CRITICAL vulnerabilities. HIGH, MEDIUM, and LOW issues are still reported in the table output but do not block the build.

The --format table flag ensures the CVE table appears in the Jenkins console log for human review.

  • Commit and push your updated Jenkinsfile to GitHub by running:
git add Jenkinsfile
git commit -m "Add Trivy security scan stage"
git push origin main

Push rejected or authentication error?

Make sure you are using a GitHub personal access token (not your password) for HTTPS authentication. If you set up SSH keys, confirm your remote URL uses the SSH format.

Help me fix a git push authentication failure.

✔️ Awesome, I've got everything!

Great. Double check you have saved your Jenkinsfile and pushed it to GitHub before moving on.

ⓧ I'd like to double check the full code

Your Jenkinsfile should now contain four stages in this order: Checkout, Build Docker Image, Trivy Scan, Push to Docker Hub. Scroll through and confirm each is present. Here is the Trivy Scan stage you just added:

        stage('Trivy Scan') {
            steps {
                sh "trivy image --exit-code 1 --severity CRITICAL --format table ${IMAGE_NAME}:${BUILD_NUMBER}"
            }
        }

It sits between the Build Docker Image stage and the Push to Docker Hub stage. The rest of the file remains unchanged from the previous step.

Re-run the pipeline and verify the security gate

Your Jenkinsfile now includes the scan. Time to trigger the pipeline and see Trivy in action inside Jenkins.

  • Switch back to the Jenkins UI in your browser.
  • Open your pipeline job.
  • Click Build Now in the left sidebar.

Before you check the result, think about what you expect to happen. Do you think the pipeline will pass or fail? Remember: the scan only blocks on CRITICAL severity.

  • Click the build number (e.g. #2) in the build history.
  • Click Console Output in the left sidebar.

Scroll through the output. You will see the Trivy scan table in the console log showing detected CVEs with their severity levels. If no CRITICAL vulnerabilities are found, the pipeline passes all stages and pushes your image to Docker Hub. If CRITICAL vulnerabilities are present, the pipeline stops at the Trivy Scan stage and never reaches Push.

What should I see?

In the console log, look for the Trivy output table between the Build and Push stages. You will see library names, CVE IDs, and severity columns. The python:3.11-slim base image typically contains LOW, MEDIUM, and HIGH CVEs from Debian system packages, but may not have CRITICAL ones depending on patch timing.

If the pipeline passes: your image was clean enough to ship. The security gate did its job by proving the image was reviewed. If the pipeline fails: Trivy found a CRITICAL CVE and blocked the push. Either way, you now have visibility into what is inside your images before they reach production.

Pipeline failing at the Trivy Scan stage?

If the build fails because Trivy found CRITICAL vulnerabilities, that is the security gate working as designed. Your image contains a known critical issue. In a production workflow you would update the base image or patch the package.

If the failure is not from CVEs but from a "command not found" error, confirm Trivy is installed on the EC2 instance by running trivy --version in your SSH session. Also confirm the jenkins user can access the Trivy binary.

Help me debug why Trivy is failing in my Jenkins pipeline.

Your pipeline now has a security gate. Every image is scanned before it can reach Docker Hub, and critically vulnerable images are blocked automatically. Next up, you will deploy your scanned, trusted image to a live Kubernetes cluster using ArgoCD and GitOps.

Deploy to EKS with ArgoCD GitOps

Your CI pipeline produces tested, scanned Docker images and pushes them to Docker Hub. That covers Continuous Integration. Now you need the other half: Continuous Deployment.

In this step, you will set up ArgoCD to watch your Kubernetes manifests in GitHub and automatically deploy changes to an EKS cluster. The result is a full CI/CD loop where a single git push flows from build to live deployment without manual kubectl commands.

In this step, get ready to:
  • Install eksctl and create an EKS cluster with Kubernetes manifests for your app.
  • Install ArgoCD on the cluster and access its dashboard.
  • Create an ArgoCD Application that auto-deploys your attendance tracker from GitHub.
Install eksctl and create the EKS cluster

Your EKS cluster is where your attendance tracker will run in production. You need eksctl (the official CLI for Amazon EKS) and kubectl (to interact with Kubernetes) on your EC2 instance.

  • In your SSH session on the EC2 instance, install kubectl by running these commands:
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
  • Verify the install by running:
kubectl version --client

You should see a version string confirming kubectl is installed.

  • Install eksctl by running these commands:
ARCH=amd64
PLATFORM=$(uname -s)_$ARCH
curl -sLO "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_$PLATFORM.tar.gz"
tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp
sudo install -m 0755 /tmp/eksctl /usr/local/bin

What do these commands do?

The first set downloads the kubectl binary from the official Kubernetes release and installs it to /usr/local/bin.

The eksctl commands download the latest release for your platform, extract it, and install the binary. This gives you the CLI needed to create and manage EKS clusters.

  • Confirm eksctl installed correctly by running:
eksctl version

You should see a version number printed.

Command not found?

Make sure you ran each line of the eksctl install block. If /usr/local/bin is not in your PATH, try restarting your shell session with exec bash.

Help me fix my PATH for eksctl

  • Create your EKS cluster by running this command:
eksctl create cluster --name cicd-cluster --region us-east-1 --node-type t3.medium --nodes 2

What does this command do?

This creates a managed Kubernetes cluster named cicd-cluster in the us-east-1 region with two t3.medium worker nodes. It also automatically configures your kubectl to connect to the new cluster.

This takes 15-20 minutes. While the cluster is being created, you will prepare your Kubernetes manifests in the next commands.

Cluster creation takes 15-20 minutes

Do not close your terminal. Let the command run. While it is working, open a new SSH session to the same EC2 instance to continue with the next instructions.

  • Open a second SSH session to your EC2 instance.
  • Navigate to your attendance tracker repository directory.
  • Create the k8s/ directory for your Kubernetes manifests by running:
mkdir k8s
  • Create the deployment manifest by running:
cat > k8s/deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: attendance-tracker
  labels:
    app: attendance-tracker
spec:
  replicas: 2
  selector:
    matchLabels:
      app: attendance-tracker
  template:
    metadata:
      labels:
        app: attendance-tracker
    spec:
      containers:
        - name: attendance-tracker
          image: your-dockerhub-username/attendance-tracker:latest
          ports:
            - containerPort: 5000
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "200m"
EOF

What does this manifest do?

This Deployment tells Kubernetes to run 2 replicas of your attendance tracker container. Each pod gets resource limits so it does not consume too much memory or CPU on your nodes.

Replace your-dockerhub-username in the image field with your actual Docker Hub username, matching what you used in your Jenkinsfile.

  • Create the service manifest by running:
cat > k8s/service.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
  name: attendance-tracker-service
spec:
  type: LoadBalancer
  selector:
    app: attendance-tracker
  ports:
    - protocol: TCP
      port: 80
      targetPort: 5000
EOF

What does this service do?

A LoadBalancer Service creates an AWS Elastic Load Balancer that routes external traffic on port 80 to your pods on port 5000. This gives you a public URL to access your attendance tracker.

  • Verify both files exist by running:
ls k8s/

You should see deployment.yaml and service.yaml listed.

  • Commit and push these manifests to GitHub by running:
git add k8s/
git commit -m "Add Kubernetes deployment manifests"
git push origin main
  • Switch back to your first SSH session where eksctl create cluster is running.
  • Wait for the command to complete. You will see a message confirming the cluster is ready.
  • Verify your cluster is accessible by running:
kubectl get nodes

You should see two nodes listed with status Ready.

Cluster creation failed?

If eksctl reports an error, check that your AWS CLI is configured with credentials that have permissions to create EKS clusters, EC2 instances, and VPCs. Also confirm you are using us-east-1 as the region.

Help me troubleshoot EKS cluster creation

Install ArgoCD on the EKS cluster

With the cluster running, you can now install ArgoCD. It runs inside the cluster and watches your Git repository for changes to your Kubernetes manifests.

  • Create the ArgoCD namespace and install ArgoCD by running these commands:
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

What does this install?

This pulls the official ArgoCD manifests from the stable branch and deploys all ArgoCD components (API server, repo server, controller) into a dedicated argocd namespace on your cluster.

  • Wait for all ArgoCD pods to be ready by running:
kubectl get pods -n argocd

Wait until all pods show Running status. This may take 1-2 minutes.

  • Install the ArgoCD CLI by running:
curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
rm argocd-linux-amd64
  • Expose the ArgoCD server so you can access its UI. Change the service type to LoadBalancer by running:
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
  • Get the ArgoCD server external URL by running:
kubectl get svc argocd-server -n argocd

Look at the EXTERNAL-IP column. It may take a minute to appear. Once it shows a hostname ending in .elb.amazonaws.com, note it down.

  • Retrieve the ArgoCD admin password by running:
argocd admin initial-password -n argocd

Copy the password that is displayed. You will use it to log in.

  • Open the ArgoCD UI by navigating to https:// followed by the EXTERNAL-IP in your browser.
  • Accept the self-signed certificate warning.
  • Log in with username admin and the password you retrieved.

You should see the ArgoCD dashboard with no applications listed yet.

EXTERNAL-IP still showing pending?

It can take 2-3 minutes for AWS to provision the load balancer. Re-run kubectl get svc argocd-server -n argocd until the hostname appears. If it stays pending for more than 5 minutes, your EKS nodes may not have the correct IAM permissions to create load balancers.

Help me fix the pending load balancer

Create an ArgoCD Application and deploy to EKS

The final piece connects ArgoCD to your GitHub repository. Once the Application is created with auto-sync enabled, ArgoCD will pull your manifests from the k8s/ directory and deploy them to the cluster automatically.

  • Log into ArgoCD from the CLI by running:
argocd login $(kubectl get svc argocd-server -n argocd -o jsonpath='{.status.loadBalancer.ingress[0].hostname}') --username admin --password $(argocd admin initial-password -n argocd | head -1) --insecure
  • Create the ArgoCD Application pointing to your GitHub repository by running:
argocd app create attendance-tracker \
  --repo https://github.com/your-username/attendance-tracker.git \
  --path k8s \
  --dest-server https://kubernetes.default.svc \
  --dest-namespace default \
  --sync-policy automated

What does this command do?

This creates an ArgoCD Application named attendance-tracker that watches the k8s directory in your GitHub repo. The --dest-server https://kubernetes.default.svc tells ArgoCD to deploy to the same cluster it is running on.

The --sync-policy automated flag means ArgoCD will automatically apply any changes you push to the k8s/ directory in GitHub. No manual intervention needed.

  • Replace your-username in the --repo URL with your actual GitHub username.
  • Check the application sync status by running:
argocd app get attendance-tracker

You should see the status transition to Synced and Healthy within a minute as ArgoCD pulls and applies your manifests.

  • Confirm your pods are running by running:
kubectl get pods

You should see two attendance-tracker pods with status Running.

  • Get the public URL for your attendance tracker by running:
kubectl get svc attendance-tracker-service

Copy the EXTERNAL-IP value from the output. It may take 1-2 minutes to appear.

Before you open this URL, what do you expect to see? Think about what container image is deployed and what port the service routes to.

  • Open the EXTERNAL-IP in your browser (on port 80, so just paste the hostname directly).

You should see your attendance tracker dashboard running live on Kubernetes, deployed entirely through ArgoCD.

  • Return to the ArgoCD UI in your browser. You should see the attendance-tracker application with status Synced and Healthy.

Why ArgoCD instead of kubectl apply?

You could deploy manually with kubectl apply, but that requires a human to run the command every time. ArgoCD watches Git and deploys automatically. If someone pushes a bad manifest, you can see the drift in the ArgoCD UI and roll back with one click.

This is the GitOps pattern: Git is the single source of truth for what runs in your cluster.

Application stuck on OutOfSync or Degraded?

Check that the image field in k8s/deployment.yaml matches your actual Docker Hub username and that the image exists on Docker Hub. Run argocd app sync attendance-tracker to force a re-sync.

Help me debug my ArgoCD deployment

Your full CI/CD pipeline is now operational. Code goes into GitHub, Jenkins builds and scans the image, pushes it to Docker Hub, and ArgoCD deploys it to your EKS cluster. Next up, the Secret Mission turns this into a fully automatic flow triggered on every push.

Secret mission

Automate Triggers with GitHub Webhooks

Right now your pipeline only runs when you click Build Now. A production CI/CD pipeline fires automatically the moment code is pushed. Can you wire up a GitHub webhook so that every push triggers the full build, scan, push, and deploy flow without any manual intervention?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses paid AWS resources that accumulate charges at approximately $0.23/hr ($5.50/day) while running.

Cost warning

Your EKS cluster costs $0.10/hr for the control plane plus ~$0.08/hr for the two t3.medium worker nodes. Your Jenkins EC2 instance adds another ~$0.05/hr. If you leave everything running overnight, you could accumulate $5+ in charges.

If you are done with this project, delete your resources now to stop all charges.

Resources you used:

  • EKS cluster cicd-cluster (control plane + 2x t3.medium nodes).
  • EC2 instance for Jenkins (t2.medium, 30GB EBS storage).
  • Security group with ports 22, 8080, and 443 open.
  • Docker Hub images (free tier, no ongoing cost).
  • GitHub webhook (no cost).

Keep everything running

No action needed. Choose this if you are still actively building on your pipeline or plan to continue within the next few hours.

  • Your EKS cluster, EC2 instance, and all services remain active.
  • Charges continue at ~$0.23/hr while resources are running.
  • Your Jenkins pipeline, ArgoCD deployment, and GitHub webhook all stay functional.

Pause - I'll come back to this later

Shut down what you can to reduce costs while keeping your project recoverable.

  • EKS cannot be paused. Delete it with the command in the Delete tab to stop cluster charges. You can recreate it later with the same eksctl create cluster command from Step 5.
  • Stop (do not terminate) your Jenkins EC2 instance from the AWS Console. Go to EC2 > Instances, select your instance, then click Instance state > Stop instance.
  • EBS storage charges still apply at ~$0.10/GB/month. Your 30GB volume costs approximately $3/month while stopped.
  • Your Docker Hub images, GitHub webhook, and Jenkins configuration on disk are preserved.

Delete - I don't want to use this again

Remove all project resources and stop all charges. This is permanent.

  • Delete the EKS cluster by running this command on your EC2 instance (this takes 10-15 minutes):
eksctl delete cluster --name cicd-cluster --region us-east-1
  • After the cluster deletion completes, terminate the Jenkins EC2 instance. In the AWS Console, go to EC2 > Instances, select your Jenkins instance, then click Instance state > Terminate instance.
  • Delete the security group. In the AWS Console, go to EC2 > Security Groups, select the security group you created for this project, then click Actions > Delete security groups. You may need to wait a few minutes after EC2 termination before the security group can be deleted.
  • Remove Docker Hub images. Go to hub.docker.com, navigate to your attendance-tracker repository, click Settings, then click Delete repository.
  • Remove the GitHub webhook. In your GitHub repository, go to Settings > Webhooks, find the Jenkins webhook, and click Delete.

That's a Wrap!

That's a Wrap!

Nice work! You've built a production-grade CI/CD pipeline on AWS that takes code from a git push all the way to a live Kubernetes deployment, with automated security scanning at every step.

Here's what you accomplished:

  • Created a Jenkins pipeline-as-code that automatically builds Docker images from your Flask attendance tracker and pushes them to Docker Hub with unique build tags.
  • Integrated Trivy security scanning as a pipeline gate that catches critical CVEs and blocks vulnerable images from ever reaching your container registry.
  • Deployed an EKS cluster with ArgoCD for GitOps-based continuous deployment, where pushing updated Kubernetes manifests to GitHub triggers automatic deployment to your live cluster.
  • Secret Mission: Configured GitHub webhooks for fully automated pipeline triggers on every push, removing the need to manually click "Build Now" in Jenkins.

Ready to quiz yourself?