Prerequisites
- Basic understanding of programming concepts π
- Python installation (3.8+) π
- VS Code or preferred IDE π»
What you'll learn
- Understand CI/CD pipeline fundamentals π―
- Apply automated deployment in real projects ποΈ
- Debug common deployment issues π
- Write clean, automated deployment scripts β¨
π― Introduction
Welcome to this exciting tutorial on CI/CD Pipelines and Automated Deployment! π In this guide, weβll explore how to build robust deployment pipelines that take your Python applications from code to production automatically.
Youβll discover how CI/CD can transform your development workflow. Whether youβre deploying web applications π, microservices π₯οΈ, or Python packages π¦, understanding automated deployment is essential for modern software development.
By the end of this tutorial, youβll feel confident building and managing CI/CD pipelines for your Python projects! Letβs dive in! πββοΈ
π Understanding CI/CD Pipelines
π€ What is CI/CD?
CI/CD is like an assembly line for your code π. Think of it as a conveyor belt that takes your raw code, tests it, packages it, and delivers it to production automatically - just like how a factory turns raw materials into finished products!
In Python terms, CI/CD means:
- β¨ Continuous Integration (CI): Automatically testing and merging code changes
- π Continuous Deployment (CD): Automatically deploying tested code to production
- π‘οΈ Quality Gates: Automated checks that ensure only good code gets deployed
π‘ Why Use CI/CD?
Hereβs why developers love CI/CD:
- Faster Releases β‘: Deploy multiple times per day instead of weekly
- Fewer Bugs π: Catch issues before they reach production
- Team Confidence πͺ: Everyone can deploy without fear
- Time Savings β°: Automate repetitive deployment tasks
Real-world example: Imagine deploying an e-commerce site π. With CI/CD, every code change is automatically tested, and if all tests pass, itβs deployed to production within minutes!
π§ Basic CI/CD Setup
π Simple GitHub Actions Example
Letβs start with a friendly example using GitHub Actions:
# π Hello, CI/CD! (.github/workflows/deploy.yml)
name: Python CI/CD Pipeline π
on:
push:
branches: [ main ] # π― Trigger on main branch
jobs:
test-and-deploy:
runs-on: ubuntu-latest
steps:
# π₯ Check out the code
- uses: actions/checkout@v3
# π Set up Python
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
# π¦ Install dependencies
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest flake8 # π§ͺ Testing tools
# π Run linting
- name: Lint with flake8
run: |
flake8 . --count --select=E9,F63,F7,F82 --show-source
# π§ͺ Run tests
- name: Test with pytest
run: |
pytest tests/ -v
# π Deploy (if tests pass)
- name: Deploy to production
if: success()
run: |
echo "π Deploying to production!"
# Your deployment script here
π‘ Explanation: This pipeline runs every time you push to main. It sets up Python, installs dependencies, runs tests, and deploys if everything passes!
π― Common CI/CD Patterns
Here are patterns youβll use daily:
# ποΈ Pattern 1: Test configuration (pytest.ini)
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
addopts = "-v --cov=app --cov-report=html"
# π¨ Pattern 2: Environment-specific configs
import os
class Config:
"""π§ Base configuration"""
DEBUG = False
TESTING = False
class DevelopmentConfig(Config):
"""π» Development environment"""
DEBUG = True
DATABASE_URL = "sqlite:///dev.db"
class ProductionConfig(Config):
"""π Production environment"""
DATABASE_URL = os.environ.get('DATABASE_URL')
SECRET_KEY = os.environ.get('SECRET_KEY')
# π Pattern 3: Deployment script
def deploy_app():
"""π Deploy application to server"""
print("π¦ Building application...")
build_app()
print("π§ͺ Running final tests...")
if not run_tests():
print("β Tests failed! Aborting deployment.")
return False
print("π Deploying to server...")
upload_to_server()
print("β
Deployment successful! π")
return True
π‘ Practical Examples
π Example 1: Flask App CI/CD Pipeline
Letβs build a complete CI/CD pipeline for a Flask application:
# π― app.py - Simple Flask app
from flask import Flask, jsonify
import os
app = Flask(__name__)
@app.route('/')
def home():
"""π Home endpoint"""
return jsonify({
'message': 'Welcome to our CI/CD example! π',
'version': os.environ.get('APP_VERSION', '1.0.0'),
'environment': os.environ.get('ENVIRONMENT', 'development')
})
@app.route('/health')
def health():
"""π Health check endpoint"""
return jsonify({'status': 'healthy', 'emoji': 'πͺ'}), 200
# π§ͺ tests/test_app.py
import pytest
from app import app
@pytest.fixture
def client():
"""π§ Test client fixture"""
app.config['TESTING'] = True
with app.test_client() as client:
yield client
def test_home_endpoint(client):
"""π Test home endpoint"""
response = client.get('/')
assert response.status_code == 200
data = response.get_json()
assert 'message' in data
assert 'π' in data['message']
def test_health_endpoint(client):
"""π Test health endpoint"""
response = client.get('/health')
assert response.status_code == 200
assert response.get_json()['status'] == 'healthy'
# π .github/workflows/flask-deploy.yml
name: Flask App CI/CD π
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python π
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Cache dependencies π¦
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
- name: Install dependencies π₯
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run tests with coverage π§ͺ
run: |
pytest tests/ --cov=app --cov-report=xml
- name: Upload coverage π
uses: codecov/codecov-action@v3
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to Heroku π
uses: akhileshns/[email protected]
with:
heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
heroku_app_name: "my-flask-app"
heroku_email: "[email protected]"
- name: Smoke test deployment π₯
run: |
sleep 30 # Wait for deployment
curl https://my-flask-app.herokuapp.com/health
π― Try it yourself: Add a database migration step to the pipeline!
π¦ Example 2: Python Package Publishing Pipeline
Letβs automate package publishing to PyPI:
# π¦ setup.py - Package configuration
from setuptools import setup, find_packages
setup(
name="awesome-toolkit",
version="1.0.0",
author="Python Developer",
author_email="[email protected]",
description="An awesome Python toolkit π οΈ",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
packages=find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires=">=3.6",
install_requires=[
"requests>=2.25.0",
"click>=7.0",
],
)
# π§ͺ tests/test_package.py
import pytest
from awesome_toolkit import core
def test_version():
"""π Test package version"""
assert hasattr(core, '__version__')
assert core.__version__ == '1.0.0'
def test_main_functionality():
"""π― Test core functionality"""
result = core.process_data([1, 2, 3])
assert result == {'sum': 6, 'count': 3, 'emoji': 'β¨'}
# π¦ .github/workflows/publish.yml
name: Publish Package π¦
on:
release:
types: [published]
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python π
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install build tools π οΈ
run: |
pip install --upgrade pip
pip install build twine
- name: Run tests π§ͺ
run: |
pip install pytest
pytest tests/
- name: Build package π¦
run: python -m build
- name: Check package π
run: twine check dist/*
- name: Publish to Test PyPI π§ͺ
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
run: |
twine upload --repository testpypi dist/*
- name: Test installation π₯
run: |
pip install --index-url https://test.pypi.org/simple/ awesome-toolkit
python -c "import awesome_toolkit; print('β
Package works!')"
- name: Publish to PyPI π
if: success()
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
twine upload dist/*
echo "π Package published successfully!"
π Advanced Concepts
π§ββοΈ Advanced Topic 1: Multi-Stage Deployments
When youβre ready to level up, implement sophisticated deployment strategies:
# π― deployment/pipeline.py
import os
from typing import Dict, List, Callable
class DeploymentPipeline:
"""π Advanced deployment pipeline"""
def __init__(self):
self.stages: List[Dict] = []
self.rollback_stack: List[Callable] = []
def add_stage(self, name: str, action: Callable, rollback: Callable):
"""β Add deployment stage"""
self.stages.append({
'name': name,
'action': action,
'rollback': rollback,
'status': 'pending',
'emoji': 'β³'
})
async def execute(self):
"""π Execute all stages"""
for stage in self.stages:
print(f"\n{stage['emoji']} Executing: {stage['name']}")
try:
# π― Execute stage action
await stage['action']()
stage['status'] = 'success'
stage['emoji'] = 'β
'
self.rollback_stack.append(stage['rollback'])
except Exception as e:
# β Stage failed
stage['status'] = 'failed'
stage['emoji'] = 'β'
print(f"π₯ Stage failed: {e}")
# π Rollback all completed stages
await self.rollback()
raise
async def rollback(self):
"""π Rollback deployment"""
print("\nπ Rolling back deployment...")
while self.rollback_stack:
rollback_fn = self.rollback_stack.pop()
await rollback_fn()
# ποΈ Usage example
async def deploy_with_stages():
pipeline = DeploymentPipeline()
# π Add pre-flight checks
pipeline.add_stage(
"Pre-flight checks",
action=lambda: run_health_checks(),
rollback=lambda: print("π No rollback needed")
)
# π³ Deploy containers
pipeline.add_stage(
"Deploy containers",
action=lambda: deploy_docker_containers(),
rollback=lambda: remove_docker_containers()
)
# π Switch traffic
pipeline.add_stage(
"Switch traffic",
action=lambda: update_load_balancer(),
rollback=lambda: restore_load_balancer()
)
await pipeline.execute()
ποΈ Advanced Topic 2: Blue-Green Deployments
For zero-downtime deployments:
# π blue_green_deploy.py
class BlueGreenDeployment:
"""ππ Blue-Green deployment strategy"""
def __init__(self, load_balancer, health_check_url):
self.load_balancer = load_balancer
self.health_check_url = health_check_url
self.environments = {
'blue': {'status': 'active', 'emoji': 'π'},
'green': {'status': 'inactive', 'emoji': 'π'}
}
def get_active_env(self):
"""π― Get currently active environment"""
for env, data in self.environments.items():
if data['status'] == 'active':
return env
def get_inactive_env(self):
"""π€ Get inactive environment"""
for env, data in self.environments.items():
if data['status'] == 'inactive':
return env
async def deploy(self, new_version):
"""π Deploy new version"""
inactive = self.get_inactive_env()
active = self.get_active_env()
print(f"{self.environments[inactive]['emoji']} Deploying to {inactive} environment...")
# π¦ Deploy to inactive environment
await self.deploy_to_environment(inactive, new_version)
# π§ͺ Run health checks
if await self.health_check(inactive):
print(f"β
Health checks passed for {inactive}!")
# π Switch traffic
await self.switch_traffic(inactive)
# π Update statuses
self.environments[inactive]['status'] = 'active'
self.environments[active]['status'] = 'inactive'
print(f"π Successfully switched to {inactive} environment!")
else:
print(f"β Health checks failed for {inactive}")
raise Exception("Deployment failed health checks")
β οΈ Common Pitfalls and Solutions
π± Pitfall 1: Secrets in Code
# β Wrong way - exposing secrets!
DATABASE_URL = "postgresql://user:[email protected]/mydb" # π°
API_KEY = "sk-1234567890abcdef" # π₯ Never do this!
# β
Correct way - use environment variables!
import os
from dotenv import load_dotenv
load_dotenv() # π₯ Load from .env file
DATABASE_URL = os.environ.get('DATABASE_URL') # π Safe!
API_KEY = os.environ.get('API_KEY') # π‘οΈ Secure!
if not DATABASE_URL or not API_KEY:
print("β οΈ Missing required environment variables!")
sys.exit(1)
π€― Pitfall 2: No Rollback Strategy
# β Dangerous - no way to rollback!
def deploy_without_safety():
update_database() # What if this fails halfway? π±
deploy_new_code()
update_config()
# β
Safe - with rollback capability!
class SafeDeployment:
def __init__(self):
self.rollback_actions = []
def deploy_with_rollback(self):
try:
# πΈ Take database snapshot
snapshot_id = create_db_snapshot()
self.rollback_actions.append(
lambda: restore_db_snapshot(snapshot_id)
)
# π Deploy new code
old_version = get_current_version()
deploy_new_code()
self.rollback_actions.append(
lambda: deploy_specific_version(old_version)
)
print("β
Deployment successful!")
except Exception as e:
print(f"β Deployment failed: {e}")
self.rollback()
raise
def rollback(self):
"""π Execute rollback actions"""
print("π Rolling back...")
for action in reversed(self.rollback_actions):
action()
print("β
Rollback complete!")
π οΈ Best Practices
- π Security First: Never commit secrets, use environment variables
- π§ͺ Test Everything: Automated tests are your safety net
- π Monitor Deployments: Add logging and metrics to track success
- π Plan for Rollbacks: Always have a way to undo changes
- π¦ Use Feature Flags: Deploy code without activating features
π§ͺ Hands-On Exercise
π― Challenge: Build a Complete CI/CD Pipeline
Create a CI/CD pipeline for a Python web API:
π Requirements:
- β Automated testing on every commit
- π Code quality checks (linting, formatting)
- π Test coverage reporting
- π Automatic deployment to staging
- π― Manual approval for production
- π¦ Docker containerization
- π Rollback capability
π Bonus Points:
- Add database migrations
- Implement blue-green deployment
- Set up monitoring and alerts
π‘ Solution
π Click to see solution
# π― Complete CI/CD pipeline!
name: Complete Python CI/CD π
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
PYTHON_VERSION: '3.9'
DOCKER_IMAGE: myapp/api
jobs:
# π§ͺ Testing Stage
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python π
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache dependencies π¦
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
- name: Install dependencies π₯
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Lint with flake8 π
run: |
flake8 . --count --statistics
- name: Format check with black π¨
run: |
black --check .
- name: Type check with mypy π
run: |
mypy src/
- name: Run tests with coverage π§ͺ
run: |
pytest tests/ --cov=src --cov-report=xml --cov-report=html
- name: Upload coverage π
uses: codecov/codecov-action@v3
- name: Security scan π
run: |
pip install safety
safety check
# π³ Build Stage
build:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx π³
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub π
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image π¦
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
${{ env.DOCKER_IMAGE }}:${{ github.sha }}
${{ env.DOCKER_IMAGE }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
# π Deploy to Staging
deploy-staging:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- uses: actions/checkout@v3
- name: Deploy to staging π
run: |
# Run database migrations
python manage.py migrate --no-input
# Deploy using kubectl
kubectl set image deployment/api-staging \
api=${{ env.DOCKER_IMAGE }}:${{ github.sha }}
# Wait for rollout
kubectl rollout status deployment/api-staging
- name: Run smoke tests π₯
run: |
sleep 30
python scripts/smoke_tests.py --env staging
# π― Deploy to Production
deploy-production:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v3
- name: Create deployment π
uses: actions/github-script@v6
with:
script: |
const deployment = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha,
environment: 'production',
required_contexts: [],
auto_merge: false
});
- name: Blue-Green deployment ππ
run: |
# Deploy to green environment
./scripts/deploy.py --env green --version ${{ github.sha }}
# Run health checks
./scripts/health_check.py --env green
# Switch traffic
./scripts/switch_traffic.py --to green
# Mark blue as inactive
./scripts/update_env_status.py --env blue --status inactive
- name: Post-deployment validation π
run: |
python scripts/validate_deployment.py --env production
echo "π Deployment successful!"
# π Python deployment script example
# scripts/deploy.py
import argparse
import subprocess
import sys
def deploy(env: str, version: str):
"""π Deploy application"""
print(f"π Deploying version {version} to {env}...")
# Update Kubernetes deployment
cmd = [
"kubectl", "set", "image",
f"deployment/api-{env}",
f"api=myapp/api:{version}",
f"--namespace={env}"
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"β Deployment failed: {result.stderr}")
sys.exit(1)
print("β
Deployment completed successfully!")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--env", required=True)
parser.add_argument("--version", required=True)
args = parser.parse_args()
deploy(args.env, args.version)
π Key Takeaways
Youβve learned so much! Hereβs what you can now do:
- β Build CI/CD pipelines with confidence πͺ
- β Automate deployments from code to production π
- β Implement safety measures like rollbacks π‘οΈ
- β Use advanced strategies like blue-green deployments π―
- β Secure your pipelines with proper secret management π
Remember: CI/CD is about building confidence in your deployments. Start simple and add complexity as needed! π€
π€ Next Steps
Congratulations! π Youβve mastered CI/CD pipelines and automated deployment!
Hereβs what to do next:
- π» Set up a simple CI/CD pipeline for your project
- ποΈ Experiment with different deployment strategies
- π Explore advanced topics like GitOps and infrastructure as code
- π Share your CI/CD experiences with the community!
Remember: Every deployment expert started with their first pipeline. Keep building, keep deploying, and most importantly, keep automating! π
Happy deploying! ππβ¨