What is a CI/CD pipeline in the context of programming?
A CI/CD pipeline (Continuous Integration / Continuous Delivery or Continuous Deployment) is an automated process that allows developers to quickly and reliably deliver code changes to a working environment (production).
Let’s break down the key concepts:
π§ CI β Continuous Integration
This is a practice where developers frequently make changes to a shared codebase. Each such change automatically:
- Builds
- Tests (unit tests, integration tests)
- Checks for compliance with standards (linting, static analysis)
π CI Goal: Identify errors at the earliest stage, before they break something important or get into a release.
π CD β Continuous Delivery or Continuous Deployment
There are two options here:
β
Continuous Delivery
After successfully passing the CI stage, changes automatically:
- Pass additional tests (e.g., E2E β end-to-end tests)
- Go to a staging (test) server
π But deployment to production still requires manual confirmation. This gives the team control over when exactly users will see the changes.
π€ Continuous Deployment
This is the next step after Continuous Delivery. Here, deployment to production happens fully automatically if all previous pipeline stages (build, all tests) have passed successfully. This is the most advanced level of automation.
- π What does a CI/CD pipeline usually consist of?
- π Popular CI/CD tools:
- π§ Why do we need CI/CD at all?
- π¦ Simple CI/CD examples with GitHub Actions
- π Deploy to popular platforms
- π Advanced CI/CD: Build Docker β Push to GHCR β Staging/Production on GCP Cloud Run
- π Security β it’s important!
π What does a CI/CD pipeline usually consist of?
A typical pipeline includes the following stages:
- Checkout β Cloning the latest version of the code from the repository.
- Build β Building the project (compilation, artifact assembly, Docker images).
- Test β Running various types of tests (unit, integration, E2E).
- Lint/Code Quality β Checking code for style compliance and potential errors using static analyzers.
- Deploy β Deploying the application (to a staging or production server).
- Notify β Sending notifications about the pipeline status to the team (e.g., in Slack, Email).
π Popular CI/CD tools:
- GitHub Actions (our focus today!)
- GitLab CI/CD
- Jenkins
- CircleCI
- Bitbucket Pipelines
- Azure DevOps
- TeamCity
π§ Why do we need CI/CD at all?
- Reduces human factor: Automation eliminates errors associated with manual operations.
- Fast bug detection: Errors are found earlier, making them easier and cheaper to fix.
- Automation of routine tasks: Developers spend less time on building and deploying, and more on code.
- Improved code quality: Constant checks and tests raise the overall quality bar.
- Fast feature delivery to users: New features reach the end user faster and more frequently.
π¦ Simple CI/CD examples with GitHub Actions
Let’s look at basic pipelines for popular technologies. All examples use GitHub Actions and are saved in the .github/workflows/ directory of your project.
π CI/CD for Python (with pytest and flake8)
# .github/workflows/python-ci.yml
name: Python CI
on: [push, pull_request]
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' # Specify your version
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt # Make sure you have requirements.txt
pip install flake8 pytest
- name: Lint with flake8
run: |
# Check code in src and tests folders (adapt to your project)
flake8 src tests
- name: Run tests
run: |
pytest
π CI/CD for Node.js (with npm test and eslint)
# .github/workflows/node-ci.yml
name: Node.js CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x] # Specify your Node.js version
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm install # or npm ci for a more predictable installation
- name: Lint with ESLint
run: npx eslint . # Make sure ESLint is configured in the project
- name: Run tests
run: npm test
π³ CI/CD for Docker (build and push to Docker Hub)
For this example, you will need DOCKER_USERNAME and DOCKER_PASSWORD (or token) secrets in your GitHub repository settings (Settings -> Secrets and variables -> Actions).
# .github/workflows/docker-ci.yml
name: Docker CI/CD
on:
push:
branches: [ main ] # Run only for the main branch
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Log in to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Build Docker image
# Replace myapp with your application name
run: docker build -t ${{ secrets.DOCKER_USERNAME }}/myapp:latest .
- name: Push Docker image
run: docker push ${{ secrets.DOCKER_USERNAME }}/myapp:latest
π Deploy to popular platforms
Now that we have built and tested artifacts (e.g., a Docker image), let’s see how they can be deployed.
π£ Deploy to Heroku
π GitHub Secrets: HEROKU_API_KEY, HEROKU_APP_NAME.
# .github/workflows/deploy-heroku.yml
name: Deploy to Heroku
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Heroku CLI
run: curl https://cli-assets.heroku.com/install.sh | sh
- name: Login to Heroku
env:
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
run: heroku auth:token
- name: Deploy to Heroku
env:
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
run: |
heroku git:remote -a ${{ secrets.HEROKU_APP_NAME }}
git push heroku main -f # Be careful with -f (force push)
If you are deploying a Docker image to Heroku:
# ... (build and login to Docker Hub/GHCR steps from previous examples) ...
# deploy:
# name: Deploy to Heroku
# needs: build # Depends on the image build job
# runs-on: ubuntu-latest
# steps:
# # ...
# - name: Login to Heroku container registry
# run: echo "${{ secrets.HEROKU_API_KEY }}" | docker login --username=_ --password-stdin registry.heroku.com
# - name: Tag image for Heroku
# # Assuming the image is built as ghcr.io/username/repo/myapp:latest
# run: docker tag ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}/myapp:latest registry.heroku.com/${{ secrets.HEROKU_APP_NAME }}/web
# - name: Push image to Heroku
# run: docker push registry.heroku.com/${{ secrets.HEROKU_APP_NAME }}/web
# - name: Release Heroku App
# env:
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
# run: heroku container:release web --app ${{ secrets.HEROKU_APP_NAME }}
π¨ Deploy to AWS (e.g., static to S3)
π GitHub Secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, S3_BUCKET_NAME.
# .github/workflows/deploy-aws-s3.yml
name: Deploy Static Site to AWS S3
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Sync files to S3
# Replace ./public with the path to your static files
run: aws s3 sync ./public s3://${{ secrets.S3_BUCKET_NAME }} --delete
For deployment to AWS Elastic Beanstalk, EB CLI is usually used; the pipeline will be similar, but with eb deploy commands.
π΅ Deploy to Google Cloud Platform (GCP App Engine)
π GitHub Secrets: GCP_CREDENTIALS (service account JSON key), GCP_PROJECT_ID.
# .github/workflows/deploy-gcp-app-engine.yml
name: Deploy to GCP App Engine
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
with:
project_id: ${{ secrets.GCP_PROJECT_ID }}
service_account_key: ${{ secrets.GCP_CREDENTIALS }}
export_default_credentials: true
- name: Deploy to App Engine
# Make sure you have app.yaml in the project root
run: gcloud app deploy --quiet
πͺ Deploy to Render.com
Render often automatically deploys on push to GitHub if the repository is connected. But for a manual trigger (or as part of a more complex pipeline), you can use a Deploy Hook.
π GitHub Secrets: RENDER_DEPLOY_HOOK (URL obtained from Render service settings).
# .github/workflows/deploy-render.yml
name: Trigger Render Deploy
on:
workflow_dispatch: # Manual trigger from GitHub UI
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Trigger Render Deploy Hook
run: curl -X POST ${{ secrets.RENDER_DEPLOY_HOOK }}
π Advanced CI/CD: Build Docker β Push to GHCR β Staging/Production on GCP Cloud Run
And now for the icing on the cake! Let’s build an advanced pipeline:
- Build Docker image.
- Publish image to GitHub Container Registry (ghcr.io).
- Automatic deployment to staging environment on GCP Cloud Run.
- Deployment to production environment on GCP Cloud Run after manual confirmation.
For this, we will need several workflow files.
Required GitHub Secrets:
GCP_PROJECT_ID: Your GCP project ID.GCP_CREDENTIALS: GCP service account JSON key with permissions to deploy to Cloud Run and access GHCR (if needed). UsuallyGITHUB_TOKENis sufficient for GHCR access from Actions.GCP_REGION: Region for Cloud Run (e.g.,europe-west1).
1. Build and publish Docker image to GHCR
# .github/workflows/build.yml
name: Build & Push to GHCR
on:
push:
branches: [main] # Run on push to main
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read # For checkout
packages: write # For push to GHCR
steps:
- uses: actions/checkout@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}/myapp:latest
# You can add tagging by commit SHA for uniqueness:
# tags: |
# ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}/myapp:latest
# ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}/myapp:${{ github.sha }}
github.repository_owner: Repository owner (your username or organization).github.event.repository.name: Repository name.myapp: Your application/image name.
2. Automatic deployment to Staging (GCP Cloud Run)
This workflow will run automatically after build.yml successfully completes.
# .github/workflows/deploy-staging.yml
name: Deploy to GCP Cloud Run (Staging)
on:
workflow_run:
workflows: ["Build & Push to GHCR"] # Build workflow name
types:
- completed
jobs:
deploy-staging:
runs-on: ubuntu-latest
# Run only if the build workflow completed successfully
if: ${{ github.event.workflow_run.conclusion == 'success' }}
# Use GitHub Environments for staging (optional, but good practice)
environment:
name: staging
url: ${{ steps.deploy.outputs.url }} # URL will be available after deployment
steps:
- name: Checkout repository
uses: actions/checkout@v3 # Needed if you use configurations from the repository
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
credentials_json: '${{ secrets.GCP_CREDENTIALS }}'
- name: 'Deploy to Cloud Run (Staging)'
id: deploy
uses: 'google-github-actions/deploy-cloudrun@v2'
with:
service: 'myapp-staging' # Your staging service name in Cloud Run
region: '${{ secrets.GCP_REGION }}'
# Use the image that was pushed in build.yml
image: 'ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}/myapp:latest'
project_id: '${{ secrets.GCP_PROJECT_ID }}'
flags: '--allow-unauthenticated --platform=managed' # Allow unauthenticated access for example
3. Deploy to Production with manual confirmation (GCP Cloud Run)
This workflow is triggered manually via the GitHub Actions UI.
# .github/workflows/deploy-prod.yml
name: Deploy to GCP Cloud Run (Production)
on:
workflow_dispatch: # Allows manual trigger
jobs:
deploy-production:
runs-on: ubuntu-latest
environment:
name: production
url: ${{ steps.deploy.outputs.url }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
credentials_json: '${{ secrets.GCP_CREDENTIALS }}'
- name: 'Deploy to Cloud Run (Production)'
id: deploy
uses: 'google-github-actions/deploy-cloudrun@v2'
with:
service: 'myapp-production' # Your production service name
region: '${{ secrets.GCP_REGION }}'
image: 'ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}/myapp:latest' # Use the same 'latest' image
project_id: '${{ secrets.GCP_PROJECT_ID }}'
flags: '--allow-unauthenticated --platform=managed'
# For production, you can add --no-traffic and then gradually switch traffic
# traffic:
# latest: true
# percent: 100
Important points of this advanced pipeline:
- GitHub Container Registry (ghcr.io): We use it to store Docker images. This is convenient as it is tightly integrated with GitHub Actions.
workflow_run: Allows one workflow (staging deployment) to be triggered upon completion of another (build).workflow_dispatch: Provides the ability to manually trigger a workflow (production deployment), which ensures control.- GitHub Environments: Allow you to configure protection rules for production (e.g., require approval from specific reviewers) and store environment-specific secrets.
- GCP Cloud Run: An excellent serverless option for running containerized applications.
π Security β it’s important!
- Use GitHub Secrets: Never store tokens, passwords, API keys directly in YAML files. Use
Settings -> Secrets and variables -> Actionsin your repository. - Minimum privileges: For service accounts (e.g., GCP), grant only the rights truly necessary to perform CI/CD tasks.
- Isolate environments: Staging and Production should be as isolated as possible. Different projects/accounts in cloud providers are good practice.
- Branch protection: Configure protection for the
main(ormaster) branch so that pushes to it are only possible via Pull Request with mandatory CI checks.