Introduction
Continuous Integration and Continuous Deployment (CI/CD) are fundamental practices in modern software development. Automating build, test, and deployment processes reduces errors, accelerates time-to-market, and improves code quality. However, choosing the right CI/CD platform is criticalโthe wrong choice can lead to slow pipelines, high maintenance costs, and operational headaches.
This comprehensive guide compares three major CI/CD platformsโGitHub Actions, Jenkins, and GitLab CI/CDโwith practical examples and real-world deployment strategies.
Core Concepts & Terminology
Continuous Integration (CI)
Automatically building and testing code changes on every commit to catch issues early.
Continuous Deployment (CD)
Automatically deploying tested code to production without manual intervention.
Continuous Delivery
Automatically preparing code for production but requiring manual approval for deployment.
Pipeline
Sequence of automated steps (build, test, deploy) triggered by code changes.
Workflow
Automated process triggered by events (push, pull request, schedule).
Job
Individual task within a pipeline (e.g., run tests, build Docker image).
Stage
Logical grouping of jobs that run in sequence or parallel.
Artifact
Output from a job (e.g., compiled binary, test reports, Docker image).
Secret
Sensitive data (API keys, credentials) stored securely and injected at runtime.
Webhook
Mechanism for triggering pipelines based on repository events.
Trigger
Event that initiates a pipeline (push, pull request, schedule, manual).
Rollback
Reverting to a previous version if deployment fails.
CI/CD Platform Comparison
Feature Comparison Matrix
| Feature | GitHub Actions | Jenkins | GitLab CI/CD |
|---|---|---|---|
| Pricing | Free (public), $21/month (private) | Free (self-hosted) | Free (SaaS), $228/year (premium) |
| Hosting | Cloud only | Self-hosted or cloud | Cloud or self-hosted |
| Setup Complexity | Very easy | Complex | Easy |
| Learning Curve | Gentle | Steep | Moderate |
| Scalability | Excellent | Excellent | Excellent |
| Ecosystem | 10,000+ actions | 1,000+ plugins | 500+ integrations |
| Container Support | Native | Via plugins | Native |
| Kubernetes Support | Excellent | Excellent | Excellent |
| Cost (100 jobs/month) | $0-21 | $0 (self-hosted) | $0-228 |
| Best For | GitHub projects | Enterprise | GitLab projects |
GitHub Actions
Architecture Overview
GitHub Repository
โโโ .github/workflows/
โ โโโ ci.yml
โ โโโ deploy.yml
โ โโโ scheduled-tasks.yml
โโโ Code
Workflow Triggers:
- push
- pull_request
- schedule
- workflow_dispatch
- repository_dispatch
Execution:
GitHub-hosted runners (Ubuntu, Windows, macOS)
or Self-hosted runners
Basic Workflow Example
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run linting
run: |
flake8 src/
black --check src/
- name: Run tests
run: |
pytest tests/ --cov=src --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} .
docker tag myapp:${{ github.sha }} myapp:latest
- name: Push to registry
if: github.ref == 'refs/heads/main'
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker push myapp:${{ github.sha }}
docker push myapp:latest
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to production
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
ssh -i ~/.ssh/deploy_key ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} \
"cd /app && docker pull myapp:${{ github.sha }} && docker-compose up -d"
Advanced Workflow with Matrix
name: Multi-Version Testing
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', '3.10', '3.11']
exclude:
- os: macos-latest
python-version: '3.9'
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: pytest tests/
Deployment to Kubernetes
name: Deploy to Kubernetes
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and push Docker image
run: |
docker build -t myregistry.azurecr.io/myapp:${{ github.sha }} .
docker login -u ${{ secrets.REGISTRY_USERNAME }} -p ${{ secrets.REGISTRY_PASSWORD }} myregistry.azurecr.io
docker push myregistry.azurecr.io/myapp:${{ github.sha }}
- name: Deploy to Kubernetes
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
kubectl set image deployment/myapp \
myapp=myregistry.azurecr.io/myapp:${{ github.sha }} \
-n production
kubectl rollout status deployment/myapp -n production
Jenkins
Architecture Overview
Jenkins Master
โโโ Job Configuration
โโโ Pipeline Definition
โโโ Credential Management
Jenkins Agents
โโโ Agent 1 (Linux)
โโโ Agent 2 (Windows)
โโโ Agent 3 (macOS)
Plugins:
- Pipeline
- Docker
- Kubernetes
- Git
- GitHub
- Slack
Declarative Pipeline Example
pipeline {
agent any
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 1, unit: 'HOURS')
timestamps()
}
environment {
DOCKER_REGISTRY = 'myregistry.azurecr.io'
DOCKER_IMAGE = "${DOCKER_REGISTRY}/myapp"
DOCKER_TAG = "${BUILD_NUMBER}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
script {
sh '''
python -m pip install --upgrade pip
pip install -r requirements.txt
'''
}
}
}
stage('Lint') {
steps {
script {
sh '''
flake8 src/
black --check src/
'''
}
}
}
stage('Test') {
steps {
script {
sh '''
pytest tests/ --cov=src --cov-report=xml
'''
}
}
}
stage('Build Docker Image') {
steps {
script {
sh '''
docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} .
docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest
'''
}
}
}
stage('Push to Registry') {
when {
branch 'main'
}
steps {
script {
withCredentials([usernamePassword(credentialsId: 'docker-registry', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh '''
echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin ${DOCKER_REGISTRY}
docker push ${DOCKER_IMAGE}:${DOCKER_TAG}
docker push ${DOCKER_IMAGE}:latest
'''
}
}
}
}
stage('Deploy to Kubernetes') {
when {
branch 'main'
}
steps {
script {
withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
sh '''
kubectl set image deployment/myapp \
myapp=${DOCKER_IMAGE}:${DOCKER_TAG} \
-n production
kubectl rollout status deployment/myapp -n production
'''
}
}
}
}
}
post {
always {
junit 'test-results.xml'
publishHTML([
reportDir: 'htmlcov',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
failure {
emailext(
subject: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Build failed. Check console output at ${env.BUILD_URL}",
to: "${env.CHANGE_AUTHOR_EMAIL}"
)
}
success {
slackSend(
color: 'good',
message: "Build Successful: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}
Scripted Pipeline Example
node {
try {
stage('Checkout') {
checkout scm
}
stage('Build') {
sh 'python -m pip install -r requirements.txt'
}
stage('Test') {
sh 'pytest tests/'
}
stage('Build Docker') {
sh 'docker build -t myapp:${BUILD_NUMBER} .'
}
if (env.BRANCH_NAME == 'main') {
stage('Deploy') {
sh 'docker push myapp:${BUILD_NUMBER}'
sh 'kubectl set image deployment/myapp myapp=myapp:${BUILD_NUMBER} -n production'
}
}
}
catch (Exception e) {
currentBuild.result = 'FAILURE'
throw e
}
finally {
junit 'test-results.xml'
}
}
Kubernetes Plugin Configuration
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
serviceAccountName: jenkins
containers:
- name: docker
image: docker:latest
command:
- cat
tty: true
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
- name: kubectl
image: bitnami/kubectl:latest
command:
- cat
tty: true
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
'''
}
}
stages {
stage('Build') {
steps {
container('docker') {
sh 'docker build -t myapp:${BUILD_NUMBER} .'
}
}
}
stage('Deploy') {
steps {
container('kubectl') {
sh 'kubectl set image deployment/myapp myapp=myapp:${BUILD_NUMBER} -n production'
}
}
}
}
}
GitLab CI/CD
Architecture Overview
GitLab Repository
โโโ .gitlab-ci.yml
โโโ Code
GitLab Runner
โโโ Docker executor
โโโ Shell executor
โโโ Kubernetes executor
โโโ Machine executor
Pipeline Stages:
- build
- test
- deploy
Basic Pipeline Example
stages:
- build
- test
- deploy
variables:
DOCKER_REGISTRY: myregistry.azurecr.io
DOCKER_IMAGE: $DOCKER_REGISTRY/myapp
build:
stage: build
image: python:3.11
script:
- pip install -r requirements.txt
- python setup.py build
artifacts:
paths:
- build/
expire_in: 1 day
test:
stage: test
image: python:3.11
script:
- pip install -r requirements.txt
- pip install pytest pytest-cov
- pytest tests/ --cov=src --cov-report=xml
coverage: '/TOTAL.*\s+(\d+%)$/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
paths:
- htmlcov/
expire_in: 30 days
lint:
stage: test
image: python:3.11
script:
- pip install flake8 black
- flake8 src/
- black --check src/
docker_build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA .
- docker tag $DOCKER_IMAGE:$CI_COMMIT_SHA $DOCKER_IMAGE:latest
only:
- main
docker_push:
stage: build
image: docker:latest
services:
- docker:dind
script:
- echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin $DOCKER_REGISTRY
- docker push $DOCKER_IMAGE:$CI_COMMIT_SHA
- docker push $DOCKER_IMAGE:latest
only:
- main
deploy_production:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config use-context production
- kubectl set image deployment/myapp myapp=$DOCKER_IMAGE:$CI_COMMIT_SHA -n production
- kubectl rollout status deployment/myapp -n production
only:
- main
environment:
name: production
kubernetes:
namespace: production
Advanced Pipeline with Environments
stages:
- build
- test
- deploy_staging
- deploy_production
variables:
DOCKER_REGISTRY: myregistry.azurecr.io
DOCKER_IMAGE: $DOCKER_REGISTRY/myapp
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA .
- docker tag $DOCKER_IMAGE:$CI_COMMIT_SHA $DOCKER_IMAGE:latest
artifacts:
paths:
- docker-image.tar
expire_in: 1 day
test:
stage: test
image: python:3.11
script:
- pip install -r requirements.txt
- pytest tests/ --cov=src
coverage: '/TOTAL.*\s+(\d+%)$/'
deploy_staging:
stage: deploy_staging
image: bitnami/kubectl:latest
script:
- kubectl config use-context staging
- kubectl set image deployment/myapp myapp=$DOCKER_IMAGE:$CI_COMMIT_SHA -n staging
- kubectl rollout status deployment/myapp -n staging
environment:
name: staging
kubernetes:
namespace: staging
url: https://staging.example.com
only:
- develop
deploy_production:
stage: deploy_production
image: bitnami/kubectl:latest
script:
- kubectl config use-context production
- kubectl set image deployment/myapp myapp=$DOCKER_IMAGE:$CI_COMMIT_SHA -n production
- kubectl rollout status deployment/myapp -n production
environment:
name: production
kubernetes:
namespace: production
url: https://example.com
only:
- main
when: manual # Require manual approval
Real-World Pipeline Comparison
Scenario: Microservices Deployment
GitHub Actions Implementation
name: Deploy Microservices
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
strategy:
matrix:
service: [api, web, worker]
steps:
- uses: actions/checkout@v3
- name: Build ${{ matrix.service }}
run: |
cd services/${{ matrix.service }}
docker build -t myregistry.azurecr.io/${{ matrix.service }}:${{ github.sha }} .
- name: Push to registry
run: |
echo ${{ secrets.REGISTRY_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USERNAME }} myregistry.azurecr.io
docker push myregistry.azurecr.io/${{ matrix.service }}:${{ github.sha }}
- name: Deploy to Kubernetes
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
kubectl set image deployment/${{ matrix.service }} \
${{ matrix.service }}=myregistry.azurecr.io/${{ matrix.service }}:${{ github.sha }} \
-n production
Jenkins Implementation
pipeline {
agent any
parameters {
choice(name: 'SERVICE', choices: ['api', 'web', 'worker'], description: 'Service to deploy')
}
stages {
stage('Build') {
steps {
dir("services/${params.SERVICE}") {
sh 'docker build -t myregistry.azurecr.io/${SERVICE}:${BUILD_NUMBER} .'
}
}
}
stage('Push') {
steps {
withCredentials([usernamePassword(credentialsId: 'docker-registry', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
sh '''
echo $PASS | docker login -u $USER myregistry.azurecr.io
docker push myregistry.azurecr.io/${SERVICE}:${BUILD_NUMBER}
'''
}
}
}
stage('Deploy') {
steps {
withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
sh '''
kubectl set image deployment/${SERVICE} \
${SERVICE}=myregistry.azurecr.io/${SERVICE}:${BUILD_NUMBER} \
-n production
'''
}
}
}
}
}
GitLab CI/CD Implementation
stages:
- build
- deploy
variables:
DOCKER_REGISTRY: myregistry.azurecr.io
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- cd services/$SERVICE
- docker build -t $DOCKER_REGISTRY/$SERVICE:$CI_COMMIT_SHA .
- echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin $DOCKER_REGISTRY
- docker push $DOCKER_REGISTRY/$SERVICE:$CI_COMMIT_SHA
deploy:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/$SERVICE $SERVICE=$DOCKER_REGISTRY/$SERVICE:$CI_COMMIT_SHA -n production
only:
- main
Best Practices & Common Pitfalls
Best Practices
- Keep Pipelines Fast: Optimize build times, parallelize jobs
- Fail Fast: Run quick checks (lint, unit tests) before slow tests
- Artifact Management: Clean up old artifacts to save storage
- Secret Management: Never commit secrets, use secure storage
- Idempotent Deployments: Deployments should be safe to repeat
- Rollback Strategy: Always have a way to revert deployments
- Monitoring: Monitor pipeline performance and failures
- Documentation: Document pipeline configuration and processes
- Testing: Comprehensive testing before production deployment
- Gradual Rollout: Use canary or blue-green deployments
Common Pitfalls
- Slow Pipelines: Inefficient builds, unnecessary steps
- Flaky Tests: Tests that fail intermittently
- Secrets in Logs: Accidentally exposing sensitive data
- No Rollback Plan: Unable to revert failed deployments
- Inadequate Testing: Deploying untested code
- Manual Approvals: Too many manual steps slow down deployment
- Poor Monitoring: Can’t detect deployment issues
- Artifact Bloat: Old artifacts consuming storage
- Tight Coupling: Pipeline tightly coupled to infrastructure
- Lack of Documentation: Team can’t maintain pipeline
External Resources
GitHub Actions
Jenkins
GitLab CI/CD
Learning Resources
Conclusion
Choosing the right CI/CD platform depends on your specific needs, existing infrastructure, and team expertise. GitHub Actions excels for GitHub-hosted projects with its simplicity and tight integration. Jenkins provides maximum flexibility and control for complex enterprise environments. GitLab CI/CD offers a balanced approach with strong Kubernetes support.
Regardless of platform choice, focus on building fast, reliable pipelines that enable rapid, safe deployments. Invest in proper testing, monitoring, and rollback strategies to ensure production stability.
Start with a simple pipeline, gradually add complexity, and continuously optimize based on real-world metrics and feedback.
Comments