DevOps.dev

Devops.dev is a community of DevOps enthusiasts sharing insight, stories, and the latest…

Follow publication

Blue-Green Deployment with Jenkins, AWS ECR, and AWS EC2 Using Django

Jaecom
DevOps.dev
Published in
8 min read4 days ago
Image generated using DALL·E

Blue-Green deployment is a DevOps strategy that minimizes downtime and risk during software releases. It involves two identical production environments: one active (Blue) and one standby (Green). While users interact with the active environment, you update and test the standby environment. Once it’s verified, you can seamlessly switch traffic to the newly updated environment, thus achieving zero-downtime deployments.

In this article, we will create a Jenkins pipeline that:

  1. Builds and pushes a Dockerized Django application to AWS ECR (Elastic Container Registry).
  2. Deploys these Docker images to AWS EC2 instances.
  3. Uses a Blue-Green approach to keep downtime minimal.

By the end, you’ll have a basic CI/CD pipeline and a repeatable method for releasing Django application updates without interrupting your users.

1. Prerequisites

1.1 Install Jenkins Plugins

Make sure you have the following Jenkins plugins installed beforehand:

1.2 Jenkins Credentials

Configure Jenkins Credentials in Manage Jenkins → Manage Credentials. Here is an overview:

After you’ve set up the credentials, in your Jenkins Console, you should see the following:

1.3 Load Balancer

This guide assumes a load balancer is already set up, directing traffic to both of your EC2 instances. If you don’t yet have a load balancer, you can still deploy to multiple servers, but you’ll need a different mechanism to switch traffic seamlessly.

2. Pipeline Overview

2.1 Create a Jenkinsfile with Defined Deployments

First, define your EC2 targets in a list called deployments. Each entry includes:

  • title: A descriptive name (used in the pipeline UI).
  • name: The Docker container name for running on that server.
  • credentialsId: The Jenkins credential ID containing the SSH key.
  • host: The public DNS or IP of the EC2 instance.
  • user: The SSH username (e.g., ubuntu on Ubuntu).
  • port: The external port on the EC2 instance mapped to the Django container’s port (commonly 8000).
def deployments = [
[
"title": "Server 1",
"name": 'project-server-1',
"credentialsId": 'project-ec2-ssh',
"host": '<ec2 url>',
"user": 'ubuntu',
"port": '8000'
],
[
"title": "Server 2",
"name": 'project-server-2',
"credentialsId": 'project-ec2-ssh',
"host": '<ec2 url>',
"user": 'ubuntu',
"port": '8000'
]
]

2.2 Define a Pipeline with Environment Variables

Next, create your pipeline and set environment variables for AWS:

def deployments = [
//defined deployments from above
]

pipeline {
agent any

environment {
AWS_REGION = '<ap-northeast-2>'
ECR_REGISTRY = '<your ecr registry>'
ECR_REPO = '<ecr repo name>'
}

//... add the stages as you continue this guide
}

Where:

  • AWS_REGION: Your AWS region (e.g., ap-northeast-2).
  • ECR_REGISTRY: The URI for your ECR registry (e.g., 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com).
  • ECR_REPO: The name of your ECR repository (e.g., my-django-app).

2.3 Retrieve Git Commit Message

In the first stage, get the latest commit message to use as a Docker tag. Docker tags are limited to 128 characters, so you’ll truncate accordingly:

stage('Get Git Commit Message') {
steps {
script {
// Get the commit message, sanitize it, and save it as a new environment variable
COMMIT_MESSAGE = sh(script: "git log -1 --pretty=%B | tr -d '\n' | tr ' ' '-' | tr -cd '[:alnum:]._-'", returnStdout: true).trim()
env.COMMIT_TAG = COMMIT_MESSAGE.take(128) // Docker tags are limited to 128 characters
echo "${env.COMMIT_TAG}"
}
}
}

2.4 Stage: Load Env file

Load your Django (or other application) .env variables from Jenkins Credentials:

stage('Load Env File from Jenkins Credentials') {
steps {
withCredentials([file(credentialsId: 'project-server-env', variable: 'ENV_FILE')]) {
// Copy the uploaded .env file to the project
sh '''
if [ -f .env ]; then
rm .env
fi
'''
sh 'cp $ENV_FILE .env'
}
}
}
  • campable-server-env: The Jenkins “Secret file” credential ID that holds your application’s .env.

2.5 Stage: Build Docker Image

Build the Docker image for your Django app, then push it to ECR:

stage('Build Docker Image') {
steps {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'project-aws-ecr-cred']]) {
sh '''
aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}
'''

sh '''
docker buildx build --platform linux/amd64 -t ${ECR_REPO} .
docker tag ${ECR_REPO}:latest ${ECR_REGISTRY}/${ECR_REPO}:latest
docker tag ${ECR_REPO}:latest ${ECR_REGISTRY}/${ECR_REPO}:${COMMIT_TAG}
'''

sh '''
docker push ${ECR_REGISTRY}/${ECR_REPO}:latest
docker push ${ECR_REGISTRY}/${ECR_REPO}:${COMMIT_TAG}
'''
}
}
}

2.6 Stage: SSH into EC2

Finally, deploy the image to each EC2 instance. For each server in deployments, Jenkins will:

  1. SSH into the server using the specified credentials.
  2. Log in to ECR and pull the latest Docker image.
  3. Tag the pulled image locally.
  4. Stop and remove any existing container with the same name.
  5. Run the new container on the desired port.
  6. Prune old Docker images to save disk space.
stage('SSH into EC2 and Pull Image') {
steps {
script {
deployments.each { server ->
stage("Deploy to ${server.title}") {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'project-aws-ecr-cred']]) {
sshagent(credentials: [server.credentialsId]) {
sh """
ssh -o StrictHostKeyChecking=no ${server.user}@${server.host} "
aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}
docker pull ${ECR_REGISTRY}/${ECR_REPO}:latest
docker tag ${ECR_REGISTRY}/${ECR_REPO}:latest ${server.name}
docker stop ${server.name} || true
docker rm ${server.name} || true
docker run -d -p ${server.port}:8000 --name ${server.name} --network campable-network ${server.name}
docker image prune -f
"
"""
}
}
}
}
}
}
}

2.7 Full Pipeline

Here’s the complete Jenkinsfile, combining all the stages:

def deployments = [
[
"title": "Server 1",
"name": 'project-server-1',
"credentialsId": 'project-ec2-ssh',
"host": '<ec2 url>',
"user": 'ubuntu',
"port": '8000'
],
[
"title": "Server 2",
"name": 'project-server-2',
"credentialsId": 'project-ec2-ssh',
"host": '<ec2 url>',
"user": 'ubuntu',
"port": '8000'
]
]

pipeline {
agent any

environment {
AWS_REGION = '<ap-northeast-2>'
ECR_REGISTRY = '<your ecr registry>'
ECR_REPO = '<ecr repo name>'
}

stages {

stage('Get Git Commit Message') {
steps {
script {
// Get the commit message, sanitize it, and save it as a new environment variable
COMMIT_MESSAGE = sh(script: "git log -1 --pretty=%B | tr -d '\n' | tr ' ' '-' | tr -cd '[:alnum:]._-'", returnStdout: true).trim()
env.COMMIT_TAG = COMMIT_MESSAGE.take(128) // Docker tags are limited to 128 characters
echo "${env.COMMIT_TAG}"
}
}
}

stage('Load Env File from Jenkins Credentials') {
steps {
withCredentials([file(credentialsId: 'project-server-env', variable: 'ENV_FILE')]) {
// Copy the uploaded .env file to the project
sh '''
if [ -f .env ]; then
rm .env
fi
'''

sh 'cp $ENV_FILE .env'
}
}
}

stage('Build Docker Image') {
steps {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'project-aws-ecr-cred']]) {
sh '''
aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}
'''


sh '''
docker buildx build --platform linux/amd64 -t ${ECR_REPO} .
docker tag ${ECR_REPO}:latest ${ECR_REGISTRY}/${ECR_REPO}:latest
docker tag ${ECR_REPO}:latest ${ECR_REGISTRY}/${ECR_REPO}:${COMMIT_TAG}
'''


sh '''
docker push ${ECR_REGISTRY}/${ECR_REPO}:latest
docker push ${ECR_REGISTRY}/${ECR_REPO}:${COMMIT_TAG}
'''

}
}
}

stage('SSH into EC2 and Pull Image') {
steps {
script {
deployments.each { server ->
stage("Deploy to ${server.title}") {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'project-aws-ecr-cred']]) {
sshagent(credentials: [server.credentialsId]) {
sh """
ssh -o StrictHostKeyChecking=no ${server.user}@${server.host} "
aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}
docker pull ${ECR_REGISTRY}/${ECR_REPO}:latest
docker tag ${ECR_REGISTRY}/${ECR_REPO}:latest ${server.name}
docker stop ${server.name} || true
docker rm ${server.name} || true
docker run -d -p ${server.port}:8000 --name ${server.name} --network campable-network ${server.name}
docker image prune -f
"
"""

}
}
}
}
}
}
}
}
}

3. Testing

3.1 Testing Blue-Green Deployment

To confirm there’s no downtime during deployment, create a script that sends requests to your application endpoint every second. If your setup is correct, you’ll see continuous HTTP 200 (or your success code) responses throughout the update.

Create a monitor_deployment.sh script:

nano monitor_deployment.sh

Paste in the following (update TARGET_URL to your own health-check or application endpoint):

#!/usr/bin/env bash

# Paste in your Target URL for health checks
TARGET_URL="https://your-api-endpoint.com/health"

SLEEP_INTERVAL=1

echo "Starting deployment monitor..."
echo "Sending requests to: $TARGET_URL"
echo "Interval: $SLEEP_INTERVAL second(s)"
echo "Press Ctrl + C to stop."

while true; do
TIMESTAMP=$(date +"%Y-%m-%d %T")

HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$TARGET_URL")

echo "[${TIMESTAMP}] - HTTP ${HTTP_CODE}"

sleep $SLEEP_INTERVAL
done

Make the script executable:

chmod +x monitor_deployment.sh

Run the script:

./monitor_deployment.sh

You should see something like this:

Starting deployment monitor...
Sending requests to: https://your-api-endpoint.com/health
Interval: 1 second(s)
Press Ctrl + C to stop.
[2025-03-10 10:15:01] - HTTP 200
[2025-03-10 10:15:02] - HTTP 200
[2025-03-10 10:15:03] - HTTP 200
...

3.2 Deploy a new update

With the monitoring script running, push a new update (e.g., run your Jenkins pipeline again).

If your Blue-Green deployment is configured correctly, you should see no downtime in your monitoring script logs (i.e., continuous 200 responses or whichever success code you expect).

3.3 Verify Docker Containers

After the pipeline completes, SSH into each EC2 instance and confirm the new containers are running:

sudo docker ps

A successful run might look like this:

Server 1

//server 1

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7b7s2v42ab3e project-server-1 "gunicorn --bind 0.0…" 1 min ago Up 1 min 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp project-server-1

Server 2

//server 2

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7b5f6e12ab3e project-server-2 "gunicorn --bind 0.0…" 1 min ago Up 1 min 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp project-server-2

If both containers are listed, and your monitoring script showed no failed requests, you’ve successfully performed a Blue-Green deployment with zero downtime!

Conclusion

By following the steps in this guide, you have successfully created a Blue-Green deployment pipeline using Jenkins, Docker, AWS ECR, and EC2. Specifically, you:

  1. Defined Jenkins Environments to maintain consistent AWS configuration (region, ECR registry, and repository).
  2. Retrieved Git Commit Messages to generate meaningful Docker tags.
  3. Loaded Environment Variables (.env) from Jenkins Credentials for application-specific secrets.
  4. Built and Pushed Docker Images to ECR, leveraging AWS credentials for secure access.
  5. Deployed to EC2 Instances by pulling the newest image, stopping/removing old containers, and running the updated containers.
  6. Verified Zero Downtime by continuously monitoring your application endpoint with a simple Bash script.

With this pipeline, you can confidently deploy new versions of your Django application without interrupting user traffic. Feel free to expand on these steps — by adding automated tests, integrating load balancer logic, or enhancing your monitoring approach — to tailor the solution to your specific production requirements.

Happy building!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in DevOps.dev

Devops.dev is a community of DevOps enthusiasts sharing insight, stories, and the latest development in the field.

Write a response