CI/CD Pipeline Explained with Examples
Continuous Integration and Continuous Deployment (CI/CD) pipelines have revolutionized how software teams deliver code to production. In this article, we'll break down what CI/CD pipelines are, why they're essential, and provide practical examples to help you understand how they work in real-world scenarios.
What is a CI/CD Pipeline?
At its core, a CI/CD pipeline is an automated process that takes new code from developers and delivers it to users (or a production environment) quickly and reliably. It's a series of steps that must be passed for new code to be released.
Let's break down the two main components:
CI: Continuous Integration
Concept: Developers frequently merge their code changes into a central repository (like Git).
Goal: To detect integration issues early and often. Instead of everyone working in isolation for weeks and then facing a massive, painful merge, small changes are integrated frequently.
Process:
- Build: The code is compiled (if necessary).
- Test: Automated tests (unit tests, integration tests) are run to ensure the new code works and doesn't break existing functionality.
- Feedback: If the build or tests fail, the team is immediately notified, and they can fix the issue quickly.
CD: Continuous Delivery / Continuous Deployment
Concept: This extends CI by automating the release of the successfully integrated code to various environments.
Goal: To ensure that you can release new changes to your customers quickly, safely, and sustainably.
There are two related terms here:
- Continuous Delivery: The code is automatically built, tested, and prepared for release to production. The final step to deploy to production is usually a manual button push (e.g., after business approval or a specific release window). The software is always releasable.
- Continuous Deployment: This goes one step further. Every change that passes all automated tests is automatically deployed to production. There's no manual intervention.
The "Pipeline" Analogy
Think of it like an assembly line. Code enters one end, and if it passes all quality checks at each stage, it comes out the other end as a deployed application. If it fails at any stage, it's pulled off the line, fixed, and re-enters.
Real-World Analogy: A Pizza Restaurant with an Automated Kitchen
Imagine you're running a very popular pizza restaurant that wants to get pizzas to customers as fast and consistently as possible.
- New Order (Developer Commits Code):
- A customer (developer) places an order for a new type of pizza (writes new code/features).
- Order Verification (Automated Build - CI):
- The order (code) is sent to the automated kitchen system.
- Dough Making (Compile): The system automatically checks if the basic ingredients (code syntax) are correct and can be formed into dough (compiled). If not, the order is rejected (build fails).
- Ingredient Check (Unit Tests - CI): The system checks if each individual topping (code module/function) is fresh and correct (passes unit tests). If a topping is bad, the order is flagged (test fails).
- Pizza Assembly (Integration Tests - CI):
- The system assembles the pizza with all the toppings (integrates different code modules).
- Taste Test (Integration Tests): A robot taster checks if the combination of toppings works well together (do modules interact correctly?). If it tastes bad, the order is flagged (integration test fails).
- Baking (Packaging/Creating an Artifact - CI/CD):
- The assembled pizza is automatically baked in a high-tech oven (code is packaged into a deployable artifact – like a Docker image, .jar file, etc.).
- Quality Control (Deploy to Staging & Acceptance Tests - CD):
- The baked pizza (artifact) is put on a "display counter" that's exactly like the customer's table (staging environment).
- Manager's Check (Automated Acceptance Tests): A robot manager inspects the pizza for appearance, correct toppings, and even does a final taste test (runs automated end-to-end tests simulating user behavior). If it's not perfect, it's rejected.
- Ready for Delivery (Continuous Delivery - CD):
- If the pizza passes the manager's check, it's boxed and ready. A human waiter (operations team/product owner) gives the final "OK" to send it out (manual deployment trigger).
- Automatic Delivery (Continuous Deployment - CD):
- Alternatively, if the restaurant is super confident, once the manager's check is passed, a drone (automated deployment script) immediately takes the pizza to the customer's table (production environment) without waiting for a human waiter.
Benefits of this "Pizza Pipeline":
- Speed: Pizzas (features) get to customers faster.
- Consistency: Every pizza (release) meets a high quality standard because of the automated checks.
- Early Problem Detection: Bad ingredients (bugs) are found quickly, not when the customer complains.
- Efficiency: Chefs (developers) can focus on creating new recipes (features) rather than manually checking every step.
Example: A Simple Web Application Pipeline
Let's say you have a web application and you use Git for version control and Jenkins (a popular CI/CD tool) to automate the pipeline.
Developer Action:
- A developer writes new code for a feature (e.g., a new "Contact Us" form).
- They run local tests on their machine.
- They
git commit
andgit push
their changes to the main branch (or a feature branch that then gets merged into main).
CI Phase (Triggered by Git Push):
Jenkins detects the push and starts the pipeline:
Stage 1: Build
# If it's a Node.js app
npm install
# If it's a Java app
mvn clean install
If this fails, the pipeline stops, and the developer is notified.
Stage 2: Unit & Integration Tests
# For Node.js
npm test
# For Java
mvn test
If any test fails, the pipeline stops, and the developer is notified.
Stage 3: Static Code Analysis
Tools like SonarQube check for code quality, security vulnerabilities, and code smells.
Stage 4: Package
# Build Docker image
docker build -t myapp:${BUILD_NUMBER} .
# Push to registry
docker push myapp:${BUILD_NUMBER}
CD Phase (Continues if CI is successful):
Stage 5: Deploy to Staging
# Deploy to staging environment
kubectl apply -f kubernetes/staging/deployment.yaml
Stage 6: Automated Acceptance Tests
# Run end-to-end tests
cypress run --config baseUrl=https://staging.myapp.com
If these tests fail, the pipeline stops, and the team is notified.
Stage 7: Manual Approval (for Continuous Delivery)
Jenkins pauses the pipeline and waits for a human to approve the release to production.
Stage 8: Deploy to Production
# Deploy to production
kubectl apply -f kubernetes/production/deployment.yaml
Stage 9: Smoke Tests & Monitoring
# Run basic smoke tests
curl -f https://myapp.com/health
# Set up monitoring alerts
datadog-agent monitor set
Real-World Example: GitLab CI/CD Pipeline
Here's a simplified .gitlab-ci.yml
file that defines a CI/CD pipeline for a web application:
stages:
- build
- test
- analyze
- deploy_staging
- test_staging
- deploy_production
build_job:
stage: build
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
unit_test_job:
stage: test
script:
- npm run test:unit
integration_test_job:
stage: test
script:
- npm run test:integration
code_quality:
stage: analyze
script:
- npm run lint
- npm audit
deploy_staging:
stage: deploy_staging
script:
- aws s3 sync dist/ s3://staging-bucket/
environment:
name: staging
url: https://staging.example.com
e2e_tests:
stage: test_staging
script:
- npm run test:e2e -- --baseUrl=https://staging.example.com
deploy_production:
stage: deploy_production
script:
- aws s3 sync dist/ s3://production-bucket/
environment:
name: production
url: https://example.com
when: manual
only:
- main
Real-World Example: GitHub Actions CI/CD Pipeline
Here's a GitHub Actions workflow file (.github/workflows/ci-cd.yml
) that implements a CI/CD pipeline:
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-files
path: ./dist
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
deploy-staging:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: ./dist
- name: Deploy to staging
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- run: aws s3 sync ./dist s3://staging-bucket/
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
if: github.event_name == 'push'
environment:
name: production
url: https://www.example.com
steps:
- uses: actions/checkout@v3
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: ./dist
- name: Deploy to production
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- run: aws s3 sync ./dist s3://production-bucket/
Our Own CI/CD Pipeline at PCJ
At PCJ, we've implemented a robust CI/CD pipeline using GitLab CI/CD to deploy our Laravel and Nuxt.js application. Our pipeline:
- Builds both our Laravel backend and Nuxt.js frontend
- Tests the application with PHPUnit and Jest
- Deploys to our staging environment automatically
- Runs end-to-end tests with Cypress
- Deploys to production after manual approval
This approach has significantly reduced our deployment time from hours to minutes and has virtually eliminated deployment-related issues.
Benefits of CI/CD Pipelines
- Faster Time to Market: Automate manual processes to deliver features quickly.
- Higher Quality Code: Catch bugs early through automated testing.
- Reduced Risk: Small, incremental changes are easier to troubleshoot.
- Improved Developer Productivity: Developers focus on writing code, not deployment tasks.
- Consistent Releases: Standardized process ensures reliable deployments.
- Better Collaboration: Provides visibility into the development and deployment process.
Conclusion
CI/CD pipelines have become an essential part of modern software development. By automating the build, test, and deployment processes, teams can deliver high-quality software faster and more reliably. Whether you're using Jenkins, GitLab CI, GitHub Actions, or another tool, the principles remain the same: automate repetitive tasks, catch issues early, and streamline the path to production.
By implementing a CI/CD pipeline, you're not just improving your technical processes—you're transforming how your entire team collaborates and delivers value to customers.