This project demonstrates a complete CI/CD pipeline implementation for a simple Flask weather application. The pipeline includes containerization, automated builds, and deployment using various DevOps tools and practices.
The project implements a complete DevOps pipeline with the following components:
- GitHub for source code management
- Jenkins for continuous integration and deployment
- Docker for containerization
- Vagrant for local development environments
- Ansible for configuration management and automated deployment
Before you begin, ensure you have the following installed:
- Ubuntu Server 20.04 LTS VM (Main Jenkins Server)
- Git
- GitHub Account
- Jenkins (2.x or later)
- Docker and Docker Hub Account
- Vagrant (2.x or later)
- VirtualBox (6.x or later)
- Ansible (2.9 or later)
- SSH key pair
-
Initial Git Configuration: Set your global username and email for Git commits:
git config --global user.name "Your Name" git config --global user.email "your-email@example.com"
-
Generate SSH Key Pair:
ssh-keygen -t ed25519 -C "your-email@example.com"
-
Start SSH Agent and Add Key:
eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519
-
Copy Public Key:
cat ~/.ssh/id_ed25519.pub
-
Add SSH Key to GitHub:
- Go to GitHub Settings → SSH and GPG keys
- Click "New SSH key"
- Paste your public key
- Give it a descriptive title
-
Test GitHub SSH Connection:
ssh -T git@github.com
You should see a message like:
Hi USERNAME! You've successfully authenticated, but GitHub does not provide shell access.
-
On GitHub:
- Click "+" → "New repository"
- Select "Private"
-
Clone and Set Up Repository:
git clone git@github.com:yourusername/RepositoryName.git cd RepositoryName
-
Add Project Files:
# Add your project files git add . git commit -m "Initial commit: Add Weather App project files" git push origin main
-
Verify Repository Setup:
git status git remote -v
git clone https://github.com/[your-username]/WeatherApp_CICD.git
cd WeatherApp_CICD
-
Navigate to Vagrant directory:
cd Vagrant
-
Start Vagrant machines:
vagrant up
This will create two Ubuntu machines with:
- 600MB RAM each
- 2 CPU cores each
- Ubuntu 22.04 LTS
Adjust Vagrantfile Configuration:
- Open the
Vagrantfile
in a text editor:nano Vagrantfile
- Set the network configuration by adding the following lines under the
Vagrant.configure
block:config.vm.network "private_network", ip: "192.168.33.10" config.vm.network "private_network", ip: "192.168.33.11"
- To provision a custom SSH key, add the following line:
config.ssh.private_key_path = "~/.ssh/id_ed25519"
- Save and exit the editor (for nano, use
CTRL + X
, thenY
, thenEnter
).
-
Verify machines are running:
vagrant status
You should see the status of both machines as 'running'.
-
SSH into the first machine:
vagrant ssh m01
This command will log you into the first Vagrant machine.
-
Display SSH configuration:
vagrant ssh-config
This will show you the SSH configuration and confirm that your SSH key has been added correctly.
-
Note down the IP addresses of both machines for Ansible inventory.
-
Navigate to the Project Directory:
cd ~/WeatherApp_CICD/app
-
Create a Python Virtual Environment:
python3 -m venv venv
-
Activate the Virtual Environment:
source venv/bin/activate
You should see the environment name in your terminal prompt, indicating that the virtual environment is active.
-
Install Required Packages: Make sure you have a
requirements.txt
file in your project directory. If it doesn't exist, create one with the necessary dependencies for your app. Then, install the packages:pip install -r requirements.txt
-
Modify the App with the Correct Weather API Key: Open the
app.py
file and update theAPI_KEY
variable with your actual weather API key:API_KEY = 'your_actual_weather_api_key'
-
Run the App to Test:
python app.py
The app should start running, and you can access it at
http://localhost:5001
. -
Verify the Application is Working: Open your web browser and navigate to:
http://localhost:5001
You should see the application interface.
-
Set up Docker's apt repository:
# Add Docker's official GPG key sudo apt-get update sudo apt-get install ca-certificates curl 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 repository to Apt sources echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
-
Install Docker packages:
sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
-
Add your user to the docker group:
sudo groupadd docker sudo usermod -aG docker $USER newgrp docker
-
Verify Docker installation:
docker --version docker run hello-world
-
Create a Dockerfile: Create a file named
Dockerfile
in your project root:FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app/ . ENV FLASK_APP=app.py ENV FLASK_ENV=development EXPOSE 5001 CMD ["python", "app.py"]
-
Build the Docker image:
docker build -t weather-app:latest .
-
Run the container:
docker run -d -p 5001:5001 --name weather-app weather-app:latest
-
Test the containerized application:
- Open your browser and navigate to
http://localhost:5001
- Verify that the weather app is working correctly
- Open your browser and navigate to
-
Basic Docker commands for management:
# Stop the container docker stop weather-app # Start the container docker start weather-app # Remove the container docker rm weather-app # List running containers docker ps # List all containers (including stopped) docker ps -a # View container logs docker logs weather-app
-
Create a Docker Compose file: Create a file named
docker-compose.yml
in your project root:version: '3.8' services: weather_app: build: ./app image: abdullahabaza/weather_app:latest ports: - "5001:5001" environment: - FLASK_APP=app.py - FLASK_ENV=production - PYTHONUNBUFFERED=1 restart: always healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5001/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s networks: default: name: weather-network driver: bridge
-
Key Components Explained:
build: ./app
: Builds from the Dockerfile in the app directoryimage: abdullahabaza/weather_app:latest
: Docker Hub image referenceenvironment
: Production-ready environment variableshealthcheck
: Container health monitoringrestart: always
: Ensures container automatically restarts
-
Build and Run with Docker Compose:
# Build and start containers docker-compose up -d # Check container status and health docker-compose ps # View logs docker-compose logs -f weather_app
-
Managing the Container:
# Stop the container docker-compose down # Rebuild and restart docker-compose up -d --build # Login to Docker Hub docker login -u yourusername # When prompted for password, use the access token # Push to Docker Hub docker-compose push weather_app
-
Push to Docker Hub:
a. Create Docker Hub Access Token:
- Log in to Docker Hub
- Go to Account Settings → Security
- Click "New Access Token"
- Give it a descriptive title (e.g., "weather-app-token")
- Choose appropriate permissions (read/write)
- Copy the token immediately (you won't see it again!)
b. Login to Docker Hub:
# Login using your access token docker login -u yourusername # When prompted for password, use the access token
c. Push the Image:
# Push to Docker Hub docker-compose push weather_app
-
Install Java and Jenkins:
# Update and install Java sudo apt update sudo apt install fontconfig openjdk-17-jre # Verify Java installation java -version # Add Jenkins repository key sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \ https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key # Add Jenkins repository 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 # Install Jenkins sudo apt-get update sudo apt-get install jenkins # Start Jenkins service sudo systemctl enable jenkins sudo systemctl start jenkins sudo systemctl status jenkins
-
Configure Jenkins for Docker:
# Add Jenkins user to Docker group sudo usermod -aG docker jenkins # Restart Jenkins service sudo service jenkins restart
-
Access Jenkins Web Interface:
- Open your browser and navigate to
http://localhost:8080
- For cloud installations (AWS/Azure/GCP), use the VM's public IP address
- Ensure port 8080 is open in your firewall/security group
- Open your browser and navigate to
-
Initial Jenkins Setup:
- Locate the initial admin password:
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
- Enter the admin password in the Jenkins web interface
- Install suggested plugins
- Locate the initial admin password:
-
Install Required Jenkins Plugins:
- Git plugin
- Pipeline plugin
- Docker plugin
- Docker Pipeline plugin
- Ansible plugin
- Credentials Binding plugin
-
Install Ansible:
# Update package list sudo apt update # Install dependencies sudo apt install software-properties-common # Add Ansible repository sudo apt-add-repository --yes --update ppa:ansible/ansible # Install Ansible sudo apt install ansible # Verify installation ansible --version
-
Configure Tools in Jenkins:
- Go to "Manage Jenkins" → "Global Tool Configuration"
- Configure Git:
- Name:
git
- Path:
/usr/bin/git
- Name:
- Configure Ansible:
- Name:
ansible
- Path:
/usr/bin/ansible
- Name:
- Save changes
-
Generate GitHub Access Token:
- Log in to GitHub
- Go to Settings → Developer settings → Personal access tokens → Tokens (classic)
- Click "Generate new token (classic)"
- Give it a descriptive name (e.g., "jenkins-weather-app")
- Select scopes:
repo
(Full control of private repositories)admin:repo_hook
(Full control of repository hooks)
- Click "Generate token"
- Copy the token immediately (you won't see it again!)
-
Add GitHub Credentials in Jenkins:
- Go to "Manage Jenkins" → "Manage Credentials"
- Click on "System" → "Global credentials" → "Add Credentials"
- Kind: Username with password
- Scope: Global
- Username: Your GitHub username
- Password: Your GitHub access token
- ID:
github_cred
- Description: "GitHub Access Token"
- Click "Create"
-
Add Docker Hub Credentials in Jenkins:
- Go to "Manage Jenkins" → "Manage Credentials"
- Click on "System" → "Global credentials" → "Add Credentials"
- Kind: Username with password
- Scope: Global
- Username: Your Docker Hub username
- Password: Your Docker Hub access token
- ID:
docker_hub_cred
- Description: "Docker Hub Access Token"
- Click "Create"
Before setting up the Jenkins pipeline, push your changes to GitHub:
# Add all changes
git add .
# Commit changes
git commit -m "Add Docker Files , Jenkins configuration and pipeline setup"
# Push to main branch
git push origin main
-
Create New Pipeline:
- Click "New Item"
- Enter name: "weather-app-pipeline"
- Select "Pipeline"
- Click "OK"
-
Configure Pipeline:
- In Pipeline section, select "Pipeline script from SCM"
- SCM: Git
- Repository URL: Your GitHub repository URL
- Credentials: Select github_cred
- Branch Specifier: */main
- Script Path: Jenkinsfile
- Save
-
Create Jenkinsfile: Create a
Jenkinsfile
in your project root with the following content:pipeline { agent any environment { DOCKERHUB_CREDENTIALS = credentials('docker_hub_cred') } stages { stage('Checkout SCM') { steps { git branch: 'main', credentialsId: 'github_cred', url: 'https://github.com/[your-username]/WeatherApp_CICD.git' } } stage('Build Docker Image') { steps { dir('app') { sh 'docker compose build' } } } stage('Push to Docker Hub') { steps { withCredentials([usernamePassword(credentialsId: 'dockerhub_cred', usernameVariable: 'DOCKER_USERNAME', passwordVariable: 'DOCKER_PASSWORD')]) { sh 'echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin' sh 'docker push ${DOCKER_IMAGE}' sh 'docker logout' } } } } }
-
Test Pipeline:
- Click "Build Now" to start the pipeline
- Monitor the build progress in the Console Output
- Verify that each stage completes successfully
Note: These SSH key configuration steps are ONLY needed in specific scenarios:
- If you're using Vagrant machines provisioned with custom SSH keys
- If you're deploying to cloud servers without a common SSH key
You DON'T need these steps if:
- You're using Vagrant machines with their default SSH setup (Vagrant generates unique private keys for each machine in
~/.vagrant.d/machines/<machine_name>/virtualbox/private_key
)- You're deploying to cloud servers that already have a common SSH key configured
In these cases, you can use the existing keys for Ansible authentication.
-
Generate New SSH Key:
# Generate ED25519 SSH key ssh-keygen -ted25519 -C "Webservers-ssh Key-key generated by AbdullahAbaza" # When prompted, save as: id_ed25519_webservers_key
-
Move Keys to SSH Directory:
mv id_ed25519_webservers_key id_ed25519_webservers_key.pub ./.ssh/
-
Set Correct Permissions for Existing Keys:
# Set proper permissions for Vagrant machine keys chmod 600 ~/.ssh/machines/m01/virtualbox/private_key chmod 600 ~/.ssh/machines/m02/virtualbox/private_key
-
Copy New Key to Target Machines:
# Copy to first machine cat ~/.ssh/id_ed25519_webservers_key.pub | ssh -i ~/.ssh/machines/m01/virtualbox/private_key vagrant@192.168.73.2 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" # Copy to second machine cat ~/.ssh/id_ed25519_webservers_key.pub | ssh -i ~/.ssh/machines/m02/virtualbox/private_key vagrant@192.168.73.3 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
If you're deploying to cloud servers, ensure you have:
- A single SSH key added to all target servers
- The private key accessible to Jenkins
- The key configured in Jenkins credentials for Ansible to use
-
Update Inventory File (
Ansible/inventory
):[webservers] machine1 ansible_host=<vagrant-machine-1-ip> ansible_user=vagrant machine2 ansible_host=<vagrant-machine-2-ip> ansible_user=vagrant [all:vars] ansible_ssh_private_key_file=/path/to/private/key
-
Verify Ansible Connection:
ansible all -m ping -i Ansible/inventory
The Jenkins pipeline executes the following stages:
-
Checkout:
- Pulls code from GitHub repository
-
Build Docker Image:
- Builds Docker image
- Tags image with build number
-
Push to Docker Hub:
- Authenticates with Docker Hub
- Pushes image to Docker Hub repository
-
Deploy:
- Runs Ansible playbook
- Installs Docker on target machines
- Pulls latest image
- Runs containers
-
Check Container Status:
# On each Vagrant machine docker ps
-
Access Application:
- Machine 1: http://[vagrant-machine-1-ip]:5001
- Machine 2: http://[vagrant-machine-2-ip]:5001
-
Jenkins Pipeline Fails:
- Check Jenkins console output
- Verify credentials are correctly configured
- Ensure Docker Hub repository exists
-
Ansible Deployment Fails:
- Check SSH connectivity to Vagrant machines
- Verify inventory file configuration
- Check Ansible logs in Jenkins
-
Container Issues:
- Check Docker logs on target machines
- Verify Docker Hub permissions
- Ensure ports are not in use
WeatherApp_CICD/
├── app/ # Flask application
├── Ansible/ # Ansible configurations
│ ├── inventory # Target hosts inventory
│ └── playbook.yml # Deployment playbook
├── Vagrant/
│ └── Vagrantfile # Vagrant configuration
├── Jenkinsfile # Jenkins pipeline definition
├── docker-compose.yaml # Docker Compose configuration
└── README.md
Abdullah Abaza