Skip to main content

Jenkins CI pipelines for Flask applications: from setup to automation

A comprehensive guide to setting up and configuring Jenkins CI pipelines for Flask applications, integrating pytest tools, and automating your testing workflow.

Introduction

Jenkins has remained one of the most powerful and flexible continuous integration servers despite the emergence of many cloud-based alternatives. For Flask application development, Jenkins offers distinct advantages: complete control over the execution environment, unlimited build minutes, and extensive customisation options through its plugin ecosystem.

This guide focuses on setting up Jenkins specifically for Flask applications and integrating the pytest ecosystem we discussed in our previous articles. We'll cover everything from initial Jenkins configuration to creating sophisticated pipelines that automate testing, quality checks, and deployment processes.

By the end of this article, you'll understand how to:

  • Set up a Jenkins server optimised for Python and Flask projects
  • Configure Jenkins pipelines using both Declarative and Scripted syntax
  • Integrate pytest, pytest-flask, pytest-cov and other testing tools
  • Visualise test results and coverage reports
  • Automate quality checks and deployment processes
  • Optimise Jenkins for better performance with Flask applications

Setting up Jenkins for Flask applications

Before diving into pipeline configurations, let's establish a proper Jenkins environment for Flask projects.

Installation and basic setup

The most straightforward way to get started with Jenkins is using Docker:

docker run -d -p 8080:8080 -p 50000:50000 \
  -v jenkins_home:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --name jenkins jenkins/jenkins:lts

After installation, access Jenkins at http://localhost:8080 and follow the setup wizard.

Installing essential plugins

For Flask projects, install these crucial plugins:

  1. Pipeline: Enables pipeline-as-code functionality
  2. Git Integration: For source code management
  3. Docker: For containerised builds
  4. Python: For improved Python support
  5. Cobertura: For code coverage reports
  6. JUnit: For test results visualisation
  7. Blue Ocean: For improved pipeline visualisation

Install these plugins via Manage Jenkins > Manage Plugins > Available.

Creating Python environments

You have two main options for Python environments in Jenkins:

Option 1: System Python with virtualenv

Configure a tool installation in Manage Jenkins > Global Tool Configuration:

// Jenkinsfile example using system Python
pipeline {
    agent any
    tools {
        python 'python3.9'
    }
    stages {
        stage('Setup') {
            steps {
                sh '''
                python -m venv venv
                . venv/bin/activate
                pip install -r requirements.txt
                pip install -r requirements-dev.txt
                '''
            }
        }
    }
}

Option 2: Docker-based Python (recommended)

Use Docker containers for consistent environments:

// Jenkinsfile example using Docker
pipeline {
    agent {
        docker {
            image 'python:3.9-slim'
        }
    }
    stages {
        stage('Setup') {
            steps {
                sh 'pip install -r requirements.txt -r requirements-dev.txt'
            }
        }
    }
}

Setting up credentials

Store sensitive information like deployment keys in Jenkins credentials:

  1. Navigate to Manage Jenkins > Manage Credentials

  2. Add credentials for:

    • Git repositories
    • Deployment servers
    • Docker registries
    • API keys

Creating Jenkins pipelines for Flask applications

Jenkins pipelines can be defined using either Declarative or Scripted syntax. Declarative is generally easier for beginners, while Scripted offers more flexibility.

Basic pipeline structure

Here's a complete Declarative pipeline for a Flask application:

pipeline {
    agent {
        docker {
            image 'python:3.9-slim'
        }
    }

    stages {
        stage('Setup') {
            steps {
                sh 'pip install -r requirements.txt -r requirements-dev.txt'
            }
        }

        stage('Linting') {
            steps {
                sh 'flake8 myapp tests'
                sh 'black --check myapp tests'
            }
        }

        stage('Test') {
            steps {
                sh 'pytest --junitxml=test-results.xml --cov=myapp --cov-report=xml'
            }
            post {
                always {
                    junit 'test-results.xml'
                    cobertura coberturaReportFile: 'coverage.xml'
                }
            }
        }

        stage('Build') {
            steps {
                sh 'python setup.py sdist bdist_wheel'
                archiveArtifacts artifacts: 'dist/*', fingerprint: true
            }
        }
    }

    post {
        always {
            cleanWs()
        }
    }
}

Save this as Jenkinsfile in your repository root.

Integrating pytest tools with Jenkins

Let's see how to integrate the pytest tools we discussed in previous articles.

pytest-flask integration

stage('Test') {
    steps {
        sh '''
        pytest --junitxml=test-results.xml \
               --cov=myapp \
               --cov-report=xml \
               --cov-report=html
        '''
    }
    post {
        always {
            junit 'test-results.xml'
            cobertura coberturaReportFile: 'coverage.xml'
            publishHTML target: [
                allowMissing: false,
                alwaysLinkToLastBuild: true,
                keepAll: true,
                reportDir: 'htmlcov',
                reportFiles: 'index.html',
                reportName: 'Coverage Report'
            ]
        }
    }
}

pytest-cov for code coverage

Jenkins can visualise coverage reports with the Cobertura plugin:

stage('Coverage') {
    steps {
        sh 'pytest --cov=myapp --cov-report=xml --cov-fail-under=85'
    }
    post {
        always {
            cobertura coberturaReportFile: 'coverage.xml',
                      conditionalCoverageTargets: '70, 0, 0',
                      lineCoverageTargets: '85, 0, 0',
                      methodCoverageTargets: '80, 0, 0',
                      failNoReports: true,
                      failUnhealthy: true,
                      failUnstable: false
        }
    }
}

pytest-mock integration

stage('Unit Tests') {
    steps {
        sh 'pytest tests/unit --junitxml=unit-test-results.xml'
    }
    post {
        always {
            junit 'unit-test-results.xml'
        }
    }
}

Handling flaky tests with pytest-rerunfailures

stage('Integration Tests') {
    steps {
        sh 'pytest tests/integration --reruns 3 --reruns-delay 1 --junitxml=integration-test-results.xml'
    }
    post {
        always {
            junit 'integration-test-results.xml'
        }
    }
}

Parallel testing strategies

Speed up your pipeline with parallel testing:

stage('Test') {
    parallel {
        stage('Unit Tests') {
            steps {
                sh 'pytest tests/unit --junitxml=unit-test-results.xml'
            }
            post {
                always {
                    junit 'unit-test-results.xml'
                }
            }
        }
        stage('Integration Tests') {
            steps {
                sh 'pytest tests/integration --junitxml=integration-test-results.xml'
            }
            post {
                always {
                    junit 'integration-test-results.xml'
                }
            }
        }
        stage('Functional Tests') {
            steps {
                sh 'pytest tests/functional --junitxml=functional-test-results.xml'
            }
            post {
                always {
                    junit 'functional-test-results.xml'
                }
            }
        }
    }
}

Advanced Jenkins pipeline techniques

Multi-branch pipelines

Multi-branch pipelines automatically discover branches and pull requests in your repository:

  1. In Jenkins, select New Item
  2. Choose Multibranch Pipeline
  3. Configure the branch source (e.g., GitHub)
  4. Set the build configuration path to Jenkinsfile

This will create pipelines for all branches that contain a Jenkinsfile.

Environment-specific pipelines

Configure different behaviours based on the branch:

pipeline {
    agent {
        docker {
            image 'python:3.9-slim'
        }
    }

    environment {
        FLASK_ENV = "${env.BRANCH_NAME == 'main' ? 'production' : 'development'}"
    }

    stages {
        stage('Test') {
            steps {
                sh 'pytest'
            }
        }

        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                echo 'Deploying to staging...'
                // Deployment steps here
            }
        }

        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            steps {
                echo 'Deploying to production...'
                // Production deployment steps here
            }
        }
    }
}

Shared libraries

For organisations with multiple Flask projects, create reusable pipeline components:

  1. Create a Git repository for shared Jenkins libraries
  2. In Manage Jenkins > Configure System, add the repository under Global Pipeline Libraries
  3. Create helper functions in your library:
// vars/flaskPipeline.groovy
def runTests(Map config = [:]) {
    def testPath = config.testPath ?: 'tests'
    def coverageThreshold = config.coverageThreshold ?: 80

    sh """
    pytest ${testPath} \
        --junitxml=test-results.xml \
        --cov=myapp \
        --cov-report=xml \
        --cov-fail-under=${coverageThreshold}
    """

    junit 'test-results.xml'
    cobertura coberturaReportFile: 'coverage.xml'
}
  1. Use the library in your Jenkinsfile:
@Library('my-jenkins-library') _

pipeline {
    agent {
        docker {
            image 'python:3.9-slim'
        }
    }

    stages {
        stage('Setup') {
            steps {
                sh 'pip install -r requirements.txt -r requirements-dev.txt'
            }
        }

        stage('Test') {
            steps {
                flaskPipeline.runTests(
                    testPath: 'tests/unit',
                    coverageThreshold: 85
                )
            }
        }
    }
}

Flask-specific pipeline configurations

Testing with different database backends

pipeline {
    agent {
        docker {
            image 'python:3.9-slim'
        }
    }

    stages {
        stage('Test with SQLite') {
            steps {
                sh '''
                export DATABASE_URL="sqlite:///test.db"
                pytest tests/
                '''
            }
        }

        stage('Test with PostgreSQL') {
            agent {
                docker {
                    image 'python:3.9-slim'
                    args '-u root --network jenkins-network'
                }
            }
            steps {
                sh '''
                export DATABASE_URL="postgresql://postgres:postgres@postgres:5432/testdb"
                pytest tests/
                '''
            }
        }
    }
}

This requires setting up a Docker network:

docker network create jenkins-network
docker run -d --name postgres --network jenkins-network -e POSTGRES_PASSWORD=postgres postgres:13

Testing with browser automation

For Flask applications with frontend components:

pipeline {
    agent {
        docker {
            image 'python:3.9-slim'
            args '--network jenkins-network'
        }
    }

    stages {
        stage('Setup') {
            steps {
                sh 'pip install -r requirements.txt -r requirements-dev.txt'
            }
        }

        stage('Unit Tests') {
            steps {
                sh 'pytest tests/unit --junitxml=unit-test-results.xml'
            }
            post {
                always {
                    junit 'unit-test-results.xml'
                }
            }
        }

        stage('Browser Tests') {
            steps {
                sh '''
                export SELENIUM_REMOTE_URL=http://selenium:4444/wd/hub
                pytest tests/browser --junitxml=browser-test-results.xml
                '''
            }
            post {
                always {
                    junit 'browser-test-results.xml'
                }
            }
        }
    }
}

This requires setting up a Selenium container:

docker run -d --name selenium --network jenkins-network selenium/standalone-chrome

Performance optimisation

Caching dependencies

Use the Stash/Unstash functionality to cache dependencies:

pipeline {
    agent {
        docker {
            image 'python:3.9-slim'
        }
    }

    stages {
        stage('Setup') {
            steps {
                sh '''
                pip install -r requirements.txt -r requirements-dev.txt
                mkdir -p .pip-cache
                pip wheel -r requirements.txt -r requirements-dev.txt -w .pip-cache
                '''
                stash includes: '.pip-cache/**', name: 'pip-cache'
            }
        }

        stage('Test') {
            steps {
                unstash 'pip-cache'
                sh '''
                pip install --no-index --find-links=.pip-cache -r requirements.txt -r requirements-dev.txt
                pytest
                '''
            }
        }
    }
}

Using Docker caching

For Docker-based builds, leverage layer caching:

pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                sh 'docker build --cache-from myapp:latest -t myapp:$BUILD_ID .'
            }
        }
    }
}

This requires setting up a proper Dockerfile for your Flask application:

FROM python:3.9-slim

WORKDIR /app

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

COPY . .

ENV FLASK_APP=myapp

CMD ["gunicorn", "myapp:create_app()"]

Best practices for Jenkins pipelines with Flask

1. Fail fast with quick checks

Run fast checks early to provide immediate feedback:

stage('Quick Check') {
    steps {
        sh 'flake8 myapp tests'
        sh 'black --check --diff myapp tests'
        sh 'pytest tests/unit -xvs'
    }
}

2. Implement quality gates

Add quality gates that prevent progression if not met:

stage('Quality Gate') {
    steps {
        sh 'pytest --cov=myapp --cov-fail-under=85'
    }
}

3. Archive artifacts strategically

Only archive artifacts that you'll actually need later:

stage('Build') {
    steps {
        sh 'python setup.py sdist bdist_wheel'
        archiveArtifacts artifacts: 'dist/*.whl', fingerprint: true
    }
}

4. Use declarative pipelines where possible

Declarative pipelines are more maintainable and readable than scripted ones.

5. Document pipeline requirements

Add comments to document dependencies and assumptions:

/*
 * This pipeline requires:
 * - Docker network: jenkins-network
 * - PostgreSQL container: postgres:13
 * - Environment variables in Jenkins credentials:
 *   - DEPLOY_KEY
 *   - PRODUCTION_SERVER
 */

Conclusion

Setting up Jenkins CI pipelines for Flask applications provides a powerful foundation for automating testing, quality checks, and deployment processes. By integrating pytest and its ecosystem of plugins, you can create comprehensive testing workflows that catch issues early and maintain high code quality.

Remember these key points:

  • Jenkins offers flexibility and control for Flask application testing
  • Docker-based agents provide consistent environments
  • Pytest integration enables comprehensive testing
  • Parallel testing and caching optimise pipeline performance
  • Shared libraries allow reuse across multiple Flask projects

With these techniques, you can build a robust CI system that scales with your Flask application development needs, ensuring quality while accelerating your development workflow.

Further reading