The Three Legs of CI/CD
Often confused because they use similar acronyms, but each serves a completely different purpose:
1. Continuous Integration (CI)
Definition: Developers constantly merge code into a shared mainbranch. Each merge automatically runs build, test, and validation steps.
How CI Works
Developer commits to feature-branch
↓
GitHub/GitLab notifies CI system (webhook)
↓
CI system pulls code
↓
Run linting checks (2 sec)
↓
Run unit tests (30 sec) ← If FAILS, STOP here, notify dev
↓
Build artifact (60 sec)
↓
Report results back on GitHub PR
↓
Developer can merge if all green ✅
Goal of CI
✅ Catch bugs EARLY (in minutes, not weeks) ✅ Enable frequent merges (10+ per day without conflicts) ✅ Provide rapid feedback (developer still in context) ✅ Prevent "merge hell" (too many branches diverging) ✅ Enforce code quality standards
Real-World CI Example
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
quality-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run unit tests
run: npm test -- --coverage
- name: Check code coverage
run: |
COVERAGE=$(npm run coverage:report | grep "Statements:" | awk '{print $NF}' | sed 's/%//')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage too low: $COVERAGE%"
exit 1
fi
- name: Build project
run: npm run build
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3CI Pipeline Timeline
10:00:00 - Developer pushes to GitHub
10:00:15 - ESLint runs → PASS ✅
10:00:45 - Unit tests run (45 tests) → PASS ✅
10:01:15 - Coverage check (85%) → PASS ✅
10:02:00 - Build → SUCCESS ✅
10:02:30 - All checks green! PR approved for merge ✅
Total time: 2.5 minutes
When to Use CI Only
- Small teams (fewer than 5 developers)
- Learning projects
- High safety requirements (medical industry)
- Very risk-averse organizations
❌ Not recommended for production releases (someone still must manually deploy)
2. Continuous Delivery (CD - with Manual Gate)
Definition: Every commit that passes CI is automatically built, tested, and packaged into a release artifact. But actual deployment to production requires manual approval.
How Continuous Delivery Works
Commit passes all CI tests ✅
↓
Automatically deploy to staging ✅
↓
Run full integration tests ✅
↓
Run end-to-end tests (real browser, real DB) ✅
↓
Run performance tests ✅
↓
Package as Docker image with version tag ✅
↓
Create release in GitHub/GitLab ✅
↓
👤 WAIT FOR HUMAN APPROVAL 👤
[PM/Manager clicks "Deploy to Production" button]
↓
Automatically deploy to production ✅
↓
Run smoke tests against production ✅
↓
Monitoring & alerts active ✅
Goal of Continuous Delivery
✅ Always have production-ready code in staging ✅ Humans control WHEN to release (business timing) ✅ Safety gate for critical decisions ✅ Traceability (know who deployed what) ✅ Quick rollback capability (seconds) ✅ Balances speed with control
Real-World Continuous Delivery Example
# .github/workflows/cd.yml
name: Continuous Delivery
on:
push:
branches: [main]
tags: ['v*']
jobs:
build-and-test:
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} .
docker build -t myapp:latest .
- name: Push to registry
run: |
docker push myapp:${{ github.sha }}
docker push myapp:latest
deploy-staging:
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to Staging
run: |
kubectl set image deployment/myapp-staging \
myapp=myapp:${{ github.sha }} \
-n staging
- name: Wait for rollout
run: kubectl rollout status deployment/myapp-staging -n staging
- name: Run integration tests
run: npm run test:integration -- --url https://staging.myapp.com
- name: Run E2E tests
run: npm run test:e2e -- --url https://staging.myapp.com
- name: Run performance tests
run: npm run test:performance -- --url https://staging.myapp.com
manual-approval:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
steps:
- run: echo "Awaiting GitHub approval..."
deploy-production:
needs: manual-approval
runs-on: ubuntu-latest
steps:
- name: Deploy to Production
run: |
kubectl set image deployment/myapp \
myapp=myapp:${{ github.sha }} \
-n production
- name: Monitor deployment
run: |
kubectl rollout status deployment/myapp -n production
- name: Health checks
run: |
curl -f https://myapp.com/health || exit 1
- name: Create GitHub Release
run: |
gh release create v${{ github.sha }} \
--notes "Production deployment" \
--draft=false
notify:
needs: [deploy-staging, deploy-production]
runs-on: ubuntu-latest
if: always()
steps:
- name: Send Slack notification
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-d '{"text":"Production deployment complete ✅"}'When to Use Continuous Delivery
✅ Regulated Industries: Banking, insurance, healthcare ✅ Marketing Timing: Coordinated product launch ✅ Business-Critical: Can't afford mistakes ✅ Organizational Policy: Compliance requirements ✅ Most companies - 80% use this model
3. Continuous Deployment (CD - Fully Automatic)
Definition: Every commit that passes all tests is automatically deployed to production. No manual gate, no waiting.
How Continuous Deployment Works
Commit passes all tests ✅
↓
Deploy to production automatically ✅
↓
Run smoke tests against prod ✅
↓
Monitor error rates & latency ✅
↓
If all green: DONE ✅
If errors detected: Auto-rollback 🔄
Goal of Continuous Deployment
⚡ Maximum speed - features ship in minutes ⚡ Fastest feedback loop - real users test immediately ⚡ Smaller changes = lower risk ⚡ Auto-rollback handles failures automatically ⚡ Highest deployment frequency (10+ per day)
Who Uses Continuous Deployment
🎬 Netflix: ~1,000 deployments/day to production 💬 Slack: Multiple deployments/day 🛍️ Etsy: 50+ deployments/day 🌩️ Amazon: 30,000+ internal deployments/day 🔍 Google: 100,000+ deployments/day
Real-World Continuous Deployment Example
jobs:
auto-deploy-production:
runs-on: ubuntu-latest
needs: build-and-test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Deploy to Production
run: |
kubectl set image deployment/myapp \
myapp=myapp:${{ github.sha }} \
-n production
- name: Monitor deployment rollout
run: kubectl rollout status deployment/myapp -n production
- name: Wait 2 minutes for stabilization
run: sleep 120
- name: Check error rate (CloudWatch)
run: |
ERROR_RATE=$(aws cloudwatch get-metric-statistics \
--namespace AWS/ApplicationELB \
--metric-name HTTPCode_Target_5XX_Count \
--start-time $(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 \
--statistics Sum \
--query 'Datapoints[0].Sum' \
--output text)
if [ "$ERROR_RATE" != "None" ] && [ "$ERROR_RATE" -gt 100 ]; then
echo "Error rate too high: $ERROR_RATE 5XX errors in last 5 min"
kubectl rollout undo deployment/myapp -n production
exit 1
fi
- name: Check latency
run: |
LATENCY=$(aws cloudwatch get-metric-statistics \
--namespace AWS/ApplicationELB \
--metric-name TargetResponseTime \
--start-time $(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 \
--statistics Average \
--query 'Datapoints[0].Average' \
--output text)
if (( $(echo "$LATENCY > 2.0" | bc -l) )); then
echo "Latency too high: ${LATENCY}s (max: 2.0s)"
kubectl rollout undo deployment/myapp -n production
exit 1
fi
- name: Deployment successful
if: success()
run: echo "Production deployment successful ✅"
- name: Send Slack notification
if: always()
run: |
STATUS=${{ job.status }}
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-d "{\"text\":\"Production deployment: $STATUS\"}"Requirements for Continuous Deployment
❌ Without these, don't use continuous deployment:
- Automated Testing: 90%+ coverage required
- Monitoring & Alerts: Must detect issues in under 2 minutes
- Auto-Rollback: System must rollback without human intervention
- Feature Flags: Deploy code but feature OFF for users
- Load Tests: Must verify performance before release
- Team Discipline: Code reviews must be tight
- Experience: Team must understand risks
Comparison Matrix
| Aspect | CI Only | Continuous Delivery | Continuous Deployment |
|---|---|---|---|
| Testing | Automated | Automated | Automated + monitoring |
| Deployment | Manual | Manual approval gate | Automatic |
| Decision | Developers | Humans (PM/Release) | Algorithm (metrics) |
| Risk | Medium | Low | Very Low (auto-rollback) |
| Speed | Slow (waiting for merge) | Fast (1-2x/day) | Fastest (10+x/day) |
| Safety | Requires discipline | Requires approval | Requires monitoring |
| Best For | Teams learning CI | Most companies | Tech-forward, high-scale |
| Examples | GitHub teams | Banks, insurance, retail | Netflix, Etsy, Slack, Uber |
| Adoption | 30% | 60% | 10% |
The Deployment Spectrum
No Automation CI Only Continuous Continuous
(Manual) (Test Only) Delivery Deployment
|________________|__________________|_________________|
Risky Growing Balanced Fast
Slow Safe Safe & Fast Requires expertise
Which Should YOU Use?
Use CI Only if:
- ❌ Very small team (fewer than 5 developers)
- ❌ Learning project
- ❌ Hobby/personal project
- ✅ Better to start here and graduate to Continuous Delivery
Use Continuous Delivery if:
- ✅ Medium to large team
- ✅ Business/marketing timing matters
- ✅ Regulated industry (banking, healthcare)
- ✅ Risk-averse organization
- ✅ This is what 60% of companies use
- ✅ RECOMMENDED for most situations
Use Continuous Deployment if:
- ✅ Highly experienced team
- ✅ Fast feedback is critical to business
- ✅ Strong monitoring and alerting in place
- ✅ Feature flags implemented
- ✅ Auto-rollback available
- ✅ Competitive advantage critical
- ✅ Only use if you have all prerequisites
Pro Tip: Feature Flags Enable Safe Deployment
Use feature flags to separate deployment from release:
// Code deployed to production but feature is OFF
if (featureFlags.get('new-checkout-v2')) {
renderNewCheckout(); // Hidden from users
} else {
renderOldCheckout(); // Active for all users
}Timeline:
10:00 - Deploy code with flag OFF
10:05 - Monitor metrics (no changes to users)
15:00 - Enable for 5% of users
15:30 - Monitor error rates (0.0%)
16:00 - Enable for 25% of users
16:30 - Monitor latency (2ms slower)
17:00 - Enable for 100% of users
17:30 - Monitor for issues
18:00 - All green, disable rollback safety
If anything goes wrong, just toggle the flag OFF (instant rollback!).
Decision Tree
Start here
|
├─ Team size < 5 and learning?
│ └─> CI Only
│
├─ Regulated industry or risk-averse?
│ └─> Continuous Delivery (with canary/blue-green)
│
├─ Want speed but need safety?
│ └─> Continuous Delivery (with feature flags)
│
└─ Netflix-scale with expert team?
└─> Continuous Deployment (with all safety checks)