Continuous Integration and Continuous Deployment (CI/CD) have become essential practices in modern software development. In this guide, we’ll explore how to build robust CI/CD pipelines that accelerate your development workflow while maintaining high quality standards.
What is CI/CD?
CI/CD is a methodology that introduces automation into the stages of app development:
- Continuous Integration (CI): Automatically build and test code changes
- Continuous Deployment (CD): Automatically deploy tested code to production
- Continuous Delivery: Keep code in a deployable state at all times
Benefits of CI/CD
Implementing CI/CD provides numerous advantages:
- Faster time to market - Automated processes speed up releases
- Improved code quality - Automated testing catches bugs early
- Reduced risk - Small, frequent changes are easier to troubleshoot
- Better collaboration - Teams can work on features independently
- Increased productivity - Developers focus on code, not deployment
CI/CD Pipeline Stages
A typical CI/CD pipeline includes these stages:
1. Source Stage
Trigger pipeline on code changes:
# GitHub Actions example
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
2. Build Stage
Compile and package your application:
build:
stage: build
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
3. Test Stage
Run automated tests to ensure quality:
test:
stage: test
script:
- npm run test:unit
- npm run test:integration
- npm run lint
coverage: '/Coverage: \d+\.\d+%/'
4. Security Stage
Scan for vulnerabilities:
security:
stage: test
script:
- npm audit
- docker scout cves
- trivy scan --severity HIGH,CRITICAL
5. Deploy Stage
Deploy to target environment:
deploy_production:
stage: deploy
script:
- kubectl apply -f k8s/
- kubectl rollout status deployment/myapp
only:
- main
when: manual
GitHub Actions Example
Complete workflow for a Node.js application:
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '18'
REGISTRY: ghcr.io
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Build application
run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
docker-build:
needs: build-and-test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ github.repository }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
needs: docker-build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- name: Deploy to Staging
run: |
echo "Deploying to staging environment"
# Add your deployment commands here
deploy-production:
needs: docker-build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
steps:
- name: Deploy to Production
run: |
echo "Deploying to production environment"
# Add your deployment commands here
GitLab CI Example
Comprehensive pipeline for a Python application:
stages:
- build
- test
- security
- deploy
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
PYTHON_VERSION: "3.11"
before_script:
- python --version
- pip install -r requirements.txt
build:
stage: build
image: python:$PYTHON_VERSION
script:
- pip install build
- python -m build
artifacts:
paths:
- dist/
expire_in: 1 week
unit-test:
stage: test
image: python:$PYTHON_VERSION
script:
- pip install pytest pytest-cov
- pytest --cov=app tests/
coverage: '/TOTAL.*\s+(\d+%)$/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
lint:
stage: test
image: python:$PYTHON_VERSION
script:
- pip install pylint black flake8
- black --check app/
- flake8 app/
- pylint app/
security-scan:
stage: security
image: python:$PYTHON_VERSION
script:
- pip install safety bandit
- safety check
- bandit -r app/
allow_failure: true
docker-build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
deploy-staging:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config use-context staging
- kubectl set image deployment/myapp myapp=$DOCKER_IMAGE
- kubectl rollout status deployment/myapp
only:
- develop
environment:
name: staging
url: https://staging.example.com
deploy-production:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config use-context production
- kubectl set image deployment/myapp myapp=$DOCKER_IMAGE
- kubectl rollout status deployment/myapp
only:
- main
when: manual
environment:
name: production
url: https://example.com
Best Practices
1. Keep Pipelines Fast
# Use caching
cache:
paths:
- node_modules/
- .npm/
# Run jobs in parallel
test:
parallel:
matrix:
- NODE_VERSION: ['16', '18', '20']
2. Fail Fast
# Run critical checks first
stages:
- validate # Quick syntax checks
- test # Unit tests
- security # Security scans
- build # Build artifacts
- deploy # Deploy if everything passes
3. Use Environment Secrets
# Never hardcode secrets
deploy:
script:
- kubectl create secret generic app-secret
--from-literal=api-key=$API_KEY
variables:
API_KEY:
vault: production/api-key@secret
4. Implement Rollback Strategy
deploy:
script:
- kubectl apply -f k8s/
- kubectl rollout status deployment/myapp
after_script:
- |
if [ $CI_JOB_STATUS == 'failed' ]; then
kubectl rollout undo deployment/myapp
fi
Monitoring and Notifications
Set up alerts for pipeline failures:
notify-failure:
stage: .post
when: on_failure
script:
- |
curl -X POST $SLACK_WEBHOOK_URL \
-H 'Content-Type: application/json' \
-d "{\"text\": \"Pipeline failed: $CI_PIPELINE_URL\"}"
Testing Your Pipeline
Before committing pipeline changes:
- Validate syntax - Use CI/CD linters
- Test locally - Use tools like
actfor GitHub Actions - Use branch pipelines - Test on feature branches
- Monitor resource usage - Optimize job performance
Conclusion
A well-designed CI/CD pipeline is essential for modern software development. Start simple, iterate based on your team’s needs, and gradually add more sophisticated features as your process matures.
Remember: the goal is to make deployments boring and predictable, not exciting and stressful!
Want to practice CI/CD concepts? Try our DevOps Mock Exams to test your knowledge!