Building an End-to-End CI/CD Pipeline for Python Applications

A comprehensive guide to setting up a complete CI/CD pipeline for Python applications using Jenkins and Docker.

Building an End-to-End CI/CD Pipeline for Python Applications

Table of Contents

Introduction

In this tutorial, we will create a comprehensive Continuous Integration and Continuous Deployment (CI/CD) pipeline for a Python application. A well-designed CI/CD pipeline automates the build, test, and deployment processes, enabling developers to deliver code changes more frequently and reliably. We’ll use Jenkins for orchestration, Docker for containerization, and various tools to ensure our Python application meets quality and security standards.

Code Repository

The sample code for this project is available at Python Demo App GitHub Repository.

Prerequisites

Before starting this project, make sure you have the following prerequisites:

  1. Jenkins: Ensure Jenkins is installed and running on a server.
  2. Python: Python 3.8 or higher should be installed (typically included by default in Ubuntu).
  3. Docker: Docker should be installed for containerization.
  4. EC2 Instance: AWS EC2 instance of type t2.large (or equivalent) to ensure sufficient resources.

Project Customization Notes

Before we begin, make note of these customizations you might need to make:

  1. Update the Flask version in requirements.txt to >=2.2.2 for security reasons
  2. Modify the Dockerfile to use your own GitHub repository link
  3. Change the image registry in the Makefile to docker.io
  4. Update the image repository name in the Makefile to your own Docker Hub username/repository

Setting Up Jenkins

Let’s start by setting up Jenkins on an EC2 instance:

  1. SSH into the EC2 instance and first install Java:
sudo apt update -y
sudo apt install openjdk-11-jdk -y
java -version
  1. Download and install Jenkins:
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
sudo apt-get update
sudo apt-get install jenkins

Don’t forget to open port 8080 in your security group to access Jenkins.

  1. Access Jenkins in your browser using http://<your-ec2-ip>:8080

  2. Retrieve the initial admin password:

sudo cat /var/lib/jenkins/secrets/initialAdminPassword
  1. Install the suggested plugins when prompted.

  2. Create an admin user with your credentials.

Installing Required Jenkins Plugins

You need to install the following plugins:

  1. Docker Pipeline: For Docker integration
  2. Pipeline: For creating pipeline jobs
  3. Git Integration: For source code management
  4. SonarQube Scanner: For code quality analysis

To install these plugins:

  1. Go to Manage Jenkins → Manage Plugins → Available
  2. Search for each plugin and install it
  3. Restart Jenkins after installation

Setting Up Docker

Install Docker on the Jenkins server:

sudo apt-get update
sudo apt-get install docker.io -y
sudo systemctl start docker
sudo docker run hello-world
sudo systemctl enable docker
docker --version
sudo usermod -a -G docker jenkins
sudo usermod -a -G docker $(whoami)
newgrp docker
sudo systemctl restart jenkins

Setting Up SonarQube

Install SonarQube using Docker:

docker run -d --name sonarqube -p 9000:9000 sonarqube:lts-community

Access SonarQube at http://<your-ec2-ip>:9000 with the default login credentials:

  • Username: admin
  • Password: admin

Generate a token for Jenkins integration:

  1. Log in to SonarQube
  2. Go to Administration → Security → Users
  3. Click on the update token option for the admin user
  4. Generate a token named “jenkins”
  5. Save the token for later use

Configure SonarQube in Jenkins:

  1. Go to Manage Jenkins → Configure System
  2. Find the SonarQube servers section
  3. Add a SonarQube installation:
    • Name: “SonarQube”
    • Server URL: http://:9000
    • Server authentication token: Add your token as a credential

Installing Required Tools

Before we can create our CI/CD pipeline, we need to ensure all the necessary tools are available on our Jenkins server. Let’s create a preliminary pipeline to install these tools:

  1. Go to Dashboard → New Item → Enter “tools-installation” as the name → Select Pipeline → OK
  2. Under “Pipeline”, select “Pipeline script” and use the following script:
pipeline {
    agent any
    stages {
        stage('Install Python Tools') {
            steps {
                sh 'sudo apt-get update -y'
                sh 'sudo apt-get install python3 -y'
                sh 'sudo apt-get install python3-pip -y'
                sh 'sudo apt-get install python3-venv -y'
            }
        }
        stage('Verify Docker') {
            steps {
                sh 'docker --version'
            }
        }
        stage('Install Make') {
            steps {
                sh 'sudo apt-get install make -y'
                sh 'sudo apt-get install make-guile -y'
            }
        }
    }
}
  1. Run this pipeline to install all necessary tools.

Creating the CI/CD Pipeline

Now we’re ready to create our main CI/CD pipeline for the Python application:

  1. Go to Dashboard → New Item → Enter “python-app-pipeline” as the name → Select Pipeline → OK
  2. Under “General”, check “Discard old builds” and set “Max # of builds to keep” to 2
  3. Under “Pipeline”, select “Pipeline script” and use the following script:
pipeline {
    agent any

    environment {
        DOCKER_HUB_CREDS = credentials('dockerhub')
    }

    stages {
        stage('Checkout Code') {
            steps {
                git url: 'https://github.com/benc-uk/python-demoapp.git', branch: 'main'

                // Optional: Update the Flask version for security
                sh '''
                sed -i 's/Flask==2.0.1/Flask>=2.2.2/g' requirements.txt
                cat requirements.txt
                '''
            }
        }

        stage('Setup Python Environment') {
            steps {
                sh '''
                python3 -m venv venv
                . venv/bin/activate
                pip install -r requirements.txt
                pip install pytest pytest-cov flake8
                '''
            }
        }

        stage('Code Quality Analysis') {
            steps {
                sh '''
                . venv/bin/activate
                flake8 --max-line-length=120 --exclude=venv .
                '''
            }
        }

        stage('Run Tests') {
            steps {
                sh '''
                . venv/bin/activate
                pytest --cov=. --cov-report=xml
                '''
            }
        }

        stage('Build Docker Image') {
            steps {
                sh 'make image'
                sh 'docker images'
            }
        }

        stage('Security Scan') {
            steps {
                // Optional: Install Trivy if not already installed
                sh '''
                if ! command -v trivy &> /dev/null; then
                    sudo apt-get install wget apt-transport-https gnupg lsb-release -y
                    wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
                    echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
                    sudo apt-get update
                    sudo apt-get install trivy -y
                fi
                '''

                // Scan the Docker image
                sh 'trivy image --severity HIGH,CRITICAL --no-progress your-dockerhub-username/python-demoapp:latest'
            }
        }

        stage('Push Image to Docker Hub') {
            steps {
                script {
                    withCredentials([string(credentialsId: 'dockerhub', variable: 'DOCKER_HUB_TOKEN')]) {
                        sh '''
                        echo $DOCKER_HUB_TOKEN | docker login -u your-dockerhub-username --password-stdin
                        make push
                        '''
                    }
                }
            }
        }

        stage('Deploy Container') {
            steps {
                sh '''
                docker stop python-demoapp || true
                docker rm python-demoapp || true
                docker run -d --name python-demoapp -p 5000:5000 your-dockerhub-username/python-demoapp:latest
                '''
            }
        }
    }

    post {
        always {
            // Clean up workspace
            cleanWs()
        }
        success {
            echo 'Pipeline completed successfully!'
        }
        failure {
            echo 'Pipeline failed!'
        }
    }
}

Make sure to replace your-dockerhub-username with your actual Docker Hub username.

Adding Docker Hub Credentials

Before running the pipeline, add Docker Hub credentials to Jenkins:

  1. Go to Manage Jenkins → Manage Credentials → Global
  2. Click on Add Credentials
  3. Choose Username with password
  4. Enter your Docker Hub username and password
  5. Set ID as “dockerhub” and description as “Docker Hub”
  6. Click OK

Understanding the Python CI/CD Pipeline

Let’s break down the key components of our pipeline:

1. Checkout Code

Fetches the latest code from the GitHub repository and optionally updates the Flask version for security.

2. Setup Python Environment

Creates a virtual environment and installs all required dependencies, plus testing tools.

3. Code Quality Analysis

Uses flake8 to check the code against Python style guide recommendations.

4. Run Tests

Executes pytest with coverage reporting to ensure code quality and functionality.

5. Build Docker Image

Uses Make to build a Docker image of the application.

6. Security Scan

Uses Trivy to scan the Docker image for security vulnerabilities.

7. Push Image to Docker Hub

Pushes the Docker image to Docker Hub for distribution.

8. Deploy Container

Deploys the application as a Docker container on the Jenkins server.

Customizing the Makefile

The Python Demo App repository includes a Makefile to simplify Docker operations. You’ll need to customize it for your own use:

REGISTRY := docker.io
GROUP := your-dockerhub-username
IMAGE := python-demoapp

# Version, update as needed
VERSION := 1.0.0

# Default tag format: registry/group/image:version
TAG := $(REGISTRY)/$(GROUP)/$(IMAGE):$(VERSION)
LATEST := $(REGISTRY)/$(GROUP)/$(IMAGE):latest

.PHONY: help image push

help: ## Show help
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

image: ## Build the container image
	docker build --file Dockerfile --tag $(TAG) --tag $(LATEST) .

push: ## Push the image to registry
	docker push $(TAG)
	docker push $(LATEST)

Replace your-dockerhub-username with your Docker Hub username.

Verifying the Deployment

After the pipeline completes, verify the deployment by accessing your Python application:

  1. Open a web browser and navigate to http://<your-ec2-ip>:5000
  2. You should see the Python Demo App running

Enhancing Your Python CI/CD Pipeline

Here are some ways to enhance your Python CI/CD pipeline:

1. Add Code Quality Gates with SonarQube

Integrate SonarQube analysis in your pipeline:

stage('SonarQube Analysis') {
    steps {
        withSonarQubeEnv('SonarQube') {
            sh '''
            . venv/bin/activate
            pip install pylint
            pylint --output-format=parseable --reports=no --exit-zero src/ > pylint-report.txt

            sonar-scanner \
              -Dsonar.projectKey=python-demoapp \
              -Dsonar.sources=. \
              -Dsonar.python.coverage.reportPaths=coverage.xml \
              -Dsonar.python.pylint.reportPath=pylint-report.txt
            '''
        }
    }
}

2. Implement Multi-Environment Deployments

Create a more sophisticated deployment strategy for different environments:

stage('Deploy') {
    steps {
        script {
            def deployEnv = 'dev'

            if (env.BRANCH_NAME == 'main') {
                deployEnv = 'prod'
            } else if (env.BRANCH_NAME == 'staging') {
                deployEnv = 'staging'
            }

            sh "make deploy ENV=${deployEnv}"
        }
    }
}

3. Add Security Scanning for Python Dependencies

Integrate safety to check Python dependencies for security issues:

stage('Check Dependencies') {
    steps {
        sh '''
        . venv/bin/activate
        pip install safety
        safety check -r requirements.txt --full-report
        '''
    }
}

4. Implement Automated Rollbacks

Add functionality to roll back to previous versions if deployment fails:

stage('Deploy with Rollback') {
    steps {
        script {
            try {
                sh '''
                docker stop python-demoapp || true
                docker rm python-demoapp || true
                docker run -d --name python-demoapp -p 5000:5000 your-dockerhub-username/python-demoapp:latest
                '''

                // Verify deployment success
                sh '''
                sleep 10
                response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/)
                if [ "$response" != "200" ]; then
                    exit 1
                fi
                '''
            } catch (Exception e) {
                echo 'Deployment failed, rolling back...'
                sh 'docker run -d --name python-demoapp -p 5000:5000 your-dockerhub-username/python-demoapp:previous'
                error 'Deployment failed, rolled back to previous version'
            }
        }
    }
}

Best Practices for Python CI/CD Pipelines

Here are some best practices to follow when building CI/CD pipelines for Python applications:

1. Use Virtual Environments

Always use virtual environments to isolate dependencies and prevent conflicts.

2. Freeze Dependencies

Use pip freeze > requirements.txt to create reproducible builds.

3. Automate Testing

Include unit tests, integration tests, and end-to-end tests in your pipeline.

4. Implement Linting

Use tools like flake8, pylint, or black to ensure code quality and consistency.

5. Scan for Security Issues

Regularly scan dependencies and container images for vulnerabilities.

6. Tag Versions Appropriately

Use semantic versioning for your Docker images and releases.

7. Implement Blue/Green Deployments

For zero-downtime deployments, implement blue/green or canary deployment strategies.

Common Issues and Troubleshooting

1. Permission Issues with Docker

If Jenkins can’t access Docker, ensure the Jenkins user is in the Docker group:

sudo usermod -a -G docker jenkins
sudo systemctl restart jenkins

2. Python Virtual Environment Problems

If you encounter issues with Python virtual environments, try:

chmod +x venv/bin/activate

3. Docker Login Failures

If Docker login fails, verify your credentials and check if you have Docker Hub rate limits:

docker login -u your-dockerhub-username

4. Testing Framework Issues

If pytest fails with import errors, ensure all dependencies are installed in the virtual environment:

. venv/bin/activate
pip install -r requirements.txt
pip install pytest pytest-cov

Conclusion

In this tutorial, we’ve created a comprehensive CI/CD pipeline for a Python application using Jenkins and Docker. We’ve covered:

  1. Setting up Jenkins with necessary plugins
  2. Implementing a multi-stage pipeline for build, test, and deployment
  3. Creating Docker images and pushing them to Docker Hub
  4. Implementing security scanning with Trivy
  5. Deploying the application as a Docker container

This pipeline provides a solid foundation for automating the delivery of Python applications, ensuring code quality, security, and reliability in the process. As your application grows, you can extend this pipeline with additional stages and integrations to meet your specific requirements.

By implementing a CI/CD pipeline for your Python applications, you’ll reduce manual errors, increase deployment frequency, and provide faster feedback to your development team, ultimately leading to better software quality and happier users.

Table of Contents