DevOps · 35 Days · Week 3 Final Day 15 — Continuous Deployment
1 / 22
Week 3 · Day 15 · Final Day

Continuous Deployment

Deploy to staging automatically on every merge. Gate production with a human approval. Blue/green, rolling, canary strategies. Rollback in 30 seconds. Week 3 capstone — a production-grade CD pipeline from scratch.

⏱ Duration 60 min
📖 Theory 25 min
🔧 Lab 30 min
❓ Quiz 5 min
Week 3 Capstone — CI/CD Complete Full pipeline: test → build → stage → prod
Session Overview

What we cover today

01
Deployment Environments
Dev → Staging → Production. What each environment is for. Why staging should mirror production exactly.
02
Deployment Strategies
Recreate, Rolling, Blue/Green, Canary. Trade-offs: downtime vs complexity vs rollback speed.
03
GitHub Environments + Approval Gates
Configure environments with required reviewers. Production deployment pauses until approved.
04
Rollback Strategy
Docker: redeploy previous SHA. K8s: kubectl rollout undo. Feature flags. DB backward-compatibility.
05
Full CD Workflow — deploy.yml
test → docker build → deploy staging (auto) → approval gate → deploy production → Slack notify.
06
Week 3 Complete Pipeline
All 5 workflow files together — ci.yml through deploy.yml. A production-grade pipeline from scratch.
07
🔧 Lab — Full CD Pipeline
GitHub Environments setup → deploy.yml → staging auto → prod approval → rollback demo.
Part 1 of 4

Deployment Environments — the promotion path

DEV local / sandbox no guardrails git push + CI pass STAGING production mirror auto-deploy on merge QA verify ✋ approval PRODUCTION live traffic manual gate DR / Failover (some teams)
Dev
  • Developer local machine or shared sandbox
  • No guardrails — experiment freely
  • May use mocked services
  • Not all features need to work together
Staging (Pre-prod)
  • Mirror of production — same config, scale, data shape
  • Auto-deploys on every merge to main
  • Where QA and stakeholders verify
  • "Staging should be boring" — if staging differs from prod, your tests prove nothing
Production
  • Live traffic — real users, real money
  • Changes must be safe, fast, reversible
  • Manual approval gate before deploy
  • Deployments tracked and audited
  • Always have a rollback plan ready
Part 2 of 4

Deployment Strategies — how you deploy matters

Recreate — simplest, has downtime

Stop all old instances → start new. Simple but causes downtime.

v1 ●●●● → STOP → v2 ●●●●
          ↑ downtime here

Rollback: redeploy v1. Use for: dev/staging, batch jobs, where downtime is acceptable.

Rolling — gradual, no downtime

Replace instances one by one. Mix of v1 + v2 briefly running.

v1●●●● → v1●●●v2 → v1●●v2v2
→ v1v2v2v2 → v2v2v2v2

Rollback: slow (roll back instance by instance). Use for: most production deployments. K8s default.

Blue/Green — instant rollback ⭐

Two identical environments. Switch traffic instantly.

BLUE (v1) ← traffic   GREEN (v2) staging
      Deploy v2 to green, test it
BLUE (v1) idle         GREEN (v2) ← traffic
      → Rollback: flip LB back to blue

Rollback: instant — flip load balancer. Cost: 2× infrastructure. Use for: critical services.

Canary — validate before full rollout

Route small % of traffic to new version. Monitor. Increase gradually.

5% → v2,  95% → v1  (watch metrics)
20% → v2, 80% → v1  (still good?)
100% → v2           (full rollout)

Use for: high-risk releases, new features. Netflix, Google, Amazon use this.

Part 3 of 4

GitHub Environments — approval gates built into CI/CD

deploy.yml — with approval gate
name: CD Pipeline

on:
  push:
    branches: [ main ]

jobs:

  # ── Stage 1: Run tests ──────────────────
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci && npm test

  # ── Stage 2: Deploy to staging (AUTO) ───
  deploy-staging:
    needs: test
    runs-on: ubuntu-latest
    environment: staging   # links to GitHub Environment
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to staging
        run: |
          echo "🚀 Deploying \${{ github.sha }} to STAGING"
          # docker pull ghcr.io/... && docker run ...
          # or: kubectl set image deployment/app app=ghcr.io/...:SHA
          # or: helm upgrade app chart/ --set image.tag=SHA

  # ── Stage 3: Production (MANUAL GATE) ───
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production   # requires approval ← configured in GitHub
      url: https://myapp.example.com
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: |
          echo "🌐 Deploying \${{ github.sha }} to PRODUCTION"

  # ── Stage 4: Notify team ────────────────
  notify:
    needs: [ deploy-production ]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Slack notification
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: |
            { "text": "Deploy \${{ job.status }} — \${{ github.sha }}" }
        env:
          SLACK_WEBHOOK_URL: \${{ secrets.SLACK_WEBHOOK_URL }}
Setting up GitHub Environments
  1. Repo → Settings → Environments
  2. Click New environment → name: staging
  3. Click New environment → name: production
  4. Click productionRequired reviewers
  5. Add yourself (or a team) as required reviewer
  6. Save protection rules

When the deploy-production job runs, GitHub pauses and sends a notification to reviewers. They must click Approve before the deploy proceeds.

What the approval looks like
Workflow run → deploy-production pending approval → email/Slack to reviewers → reviewer clicks "Review deployments" on GitHub → selects environment → clicks Approve and deploy → pipeline continues. The entire team can see who approved what and when.
Part 4 of 4

Rollback Strategy — define it before you deploy

Rollback options
# ── Option 1: Redeploy previous SHA (Docker) ───
# Every image is tagged with its commit SHA
# Rollback = deploy the previous SHA tag
PREVIOUS_SHA=$(git log --format="%H" -n 2 | tail -1)
docker pull ghcr.io/user/myapp:sha-${PREVIOUS_SHA:0:7}
docker run -p 3000:3000 ghcr.io/user/myapp:sha-${PREVIOUS_SHA:0:7}

# ── Option 2: Kubernetes rollout undo ──────────
kubectl rollout history deployment/myapp
# REVISION  CHANGE-CAUSE
# 1         sha-a3f7c2d
# 2         sha-b4e8d3e  ← current, broken
kubectl rollout undo deployment/myapp
# → rolls back to revision 1 instantly
kubectl rollout undo deployment/myapp --to-revision=1

# ── Option 3: Feature flag (no redeploy) ───────
# Feature flags: toggle feature off in LaunchDarkly/Unleash
# Bug is in new feature → turn flag off → bug hidden
# Fix, test, turn flag back on → zero downtime

# ── Option 4: Git revert + redeploy ────────────
git revert HEAD                     # creates revert commit
git push origin main
# → triggers CI/CD → new deploy with code reverted
# slower: requires full CI run

# ── Option 5: Database rollback (careful!) ─────
# RULE: always make backward-compatible schema changes
# ADD nullable column first (both old + new app work)
# REMOVE column only after all instances are updated
# NEVER remove a column the old app still reads!
Rollback comparison
MethodSpeedDowntime
Blue/Green flipInstantZero
K8s rollout undo<30 secZero
Feature flag offInstantZero
Redeploy previous SHA5-10 minBrief
Git revert + CI10-20 minLonger
⚠ Database rollback is dangerous
Code rolls back in seconds. Schema changes are hard to revert. Never remove or rename a column in the same deployment as the code that stops using it. First deploy: add nullable column. Second deploy: app uses new column. Third deploy: remove old column.
💡 Define rollback BEFORE deploying
What's your rollback plan? "We'll figure it out if something breaks" is not a plan. Define it in the runbook: which command, who runs it, what's the health check to confirm it worked.
Week 3 Capstone Lab

🔧 Full CD Pipeline

GitHub Environments → deploy.yml → staging auto-deploy → production approval gate → rollback demo

⏱ 30 minutes
my-devops-app ✓
Environments configured ✓
🔧 Lab — Steps

Build the complete CD pipeline

1
Create GitHub Environments
Repo → Settings → Environments → create staging (no protection) and production (add yourself as required reviewer).
2
Write .github/workflows/deploy.yml
4 jobs: test → deploy-staging → deploy-production (gated) → notify. Use Slide 5 YAML as base. Stage deploy prints the SHA being deployed.
3
Push and watch staging auto-deploy
Merge to main. Actions tab: test → deploy-staging run automatically. Verify both complete. Notice production job shows "waiting for review".
4
Approve the production deployment
Click the pending deploy-production job → "Review deployments" → select production → "Approve and deploy". Watch it complete. ✅ Full CI/CD pipeline working!
5
Simulate a rollback
Push a "broken" change (intentional error). Watch it deploy to staging. Reject the production approval. Then run git revert HEAD && git push to automatically fix it.
🔧 Lab — Complete deploy.yml

Full CD workflow code

.github/workflows/deploy.yml
name: CD — Deploy

on:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci && npm test

  deploy-staging:
    needs: test
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Staging
        run: |
          echo "Deploying \${{ github.sha }} to STAGING"
          echo "Image: ghcr.io/\${{ github.repository }}:sha-\${{ github.sha }}"
          echo "✅ Staging deploy complete"

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.example.com
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Production
        run: |
          echo "Deploying \${{ github.sha }} to PRODUCTION"
          echo "Image: ghcr.io/\${{ github.repository }}:sha-\${{ github.sha }}"
          echo "🌐 Production deploy complete"
      - name: Smoke test
        run: |
          echo "Running smoke tests..."
          echo "✅ App healthy at \${{ env.APP_URL }}"

  notify:
    needs: [ deploy-production ]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Deploy status: \${{ needs.deploy-production.result }}"
          echo "SHA: \${{ github.sha }}"
          echo "Actor: \${{ github.actor }}"
Environment setup (GitHub UI)

Before pushing the workflow, set up environments:

  1. Repo → Settings → Environments
  2. New environment → staging → Save (no protection needed)
  3. New environment → production
  4. ✅ Required reviewers → add yourself
  5. Optional: set Deployment branches → main only
  6. Save protection rules
💡 Real deploy commands (swap echo for these)
kubectl set image deployment/app app=ghcr.io/.../app:sha
helm upgrade app chart/ --set image.tag=sha
docker pull img:sha && docker run img:sha
aws ecs update-service --cluster prod --task-def app:v2
What the reviewer sees
Workflow run → deploy-production waiting → GitHub sends email → reviewer opens the run → "Review deployments" button → selects production → writes optional comment → "Approve and deploy". Full audit trail logged.
Week 3 — Complete

Your full pipeline — 5 workflows, production-grade

Full pipeline flow — git push → prod
──── git push origin feat/x ─────────────────

ci.yml:
  ├─ lint (ESLint, Husky)
  └─ test (matrix Node 18+20)

──── git merge → main ───────────────────────

test.yml:
  └─ jest --coverage (70% gate)

docker.yml:
  └─ docker build + push to GHCR
     ghcr.io/user/app:sha-abc1234
     ghcr.io/user/app:latest

deploy.yml:
  ├─ test (regression check)
  ├─ deploy-staging  ← AUTO (no gate)
  │   └─ "✅ sha-abc1234 → staging"
  ├─ [⏸ WAITING FOR APPROVAL]
  ├─ deploy-production ← GATED
  │   └─ "🌐 sha-abc1234 → production"
  └─ notify
      └─ Slack: "Deploy success: sha-abc1234"

──── Total time: ~8-12 minutes ──────────────
# (excluding approval wait time)
All 5 workflow files
.github/workflows/
├── ci.yml          ← lint + test (on PR)
├── ci-advanced.yml ← matrix tests
├── test.yml        ← Jest coverage gate
├── docker.yml      ← GHCR push on merge
└── deploy.yml      ← staging + prod
Week 3 Capabilities
  • ✅ Lint + tests on every PR
  • ✅ 70% coverage gate enforced
  • ✅ Docker image built + pushed on merge
  • ✅ Auto-deploy to staging on merge
  • ✅ Manual approval → production deploy
  • ✅ Rollback strategy defined
Week 4 Preview — Docker Fundamentals
Week 4 goes deeper on containers: Docker architecture, namespaces, cgroups, container lifecycle, Docker Compose, networking. Building the foundation for Week 5: Kubernetes.
Knowledge Check

Quiz Time

3 questions · 5 minutes · deployment strategies, canary, backward-compatible schema

Week 3 final knowledge check →
QUESTION 1 OF 3
Which deployment strategy allows the fastest rollback with zero downtime?
A
Recreate — stop old, start new
B
Rolling — replace instances gradually
C
Blue/Green — run two environments, flip the load balancer
D
Canary — route small % of traffic to new version
QUESTION 2 OF 3
What is a Canary deployment?
A
Deploying only at midnight to minimise user impact
B
Routing a small percentage of traffic to the new version while most traffic stays on the old — monitoring before full rollout
C
Deploying to a separate datacenter for disaster recovery
D
A type of blue/green deployment using container registries
QUESTION 3 OF 3
What does backward-compatible schema change mean in the context of deployments?
A
Database changes that are encrypted for security
B
Database changes that both the old AND new version of the app can handle — enabling safe rolling deployments
C
Schema changes that require no downtime to apply
D
Read-only database changes that don't affect production
Week 3 — Complete! 🎉

What you learned this week

🚦
Environments
Dev → Staging → Prod. Staging mirrors prod. Approval gates protect production.
🔄
Strategies
Recreate, Rolling, Blue/Green, Canary. Blue/Green = fastest rollback.
Approval Gate
GitHub Environments. Required reviewers. Full audit trail on who approved when.
Rollback
Define before deploying. SHA tags enable instant rollback. DB changes: always backward-compatible.
🎉 Week 3 Complete — CI/CD Mastered
5 days, full CI/CD pipeline:
Day 11: CI/CD concepts · Day 12: Pipelines deep dive
Day 13: Testing + coverage · Day 14: Docker + GHCR
Day 15: Full CD + approval gates

You now have a production-grade pipeline — the same pattern used by DP World, Netflix, and thousands of engineering teams.
Week 4 — Docker Fundamentals
Day 16: Docker Architecture
Namespaces, cgroups, Union FS. Images vs containers. containerd. What Docker actually does at the kernel level.

namespaces cgroups containerd Docker Compose
📌 Reference

Deployment strategies — decision guide

Strategy Downtime Rollback Cost Best for
RecreateYesRedeploy oldDev/staging, batch jobs
RollingNoSlowMost services — K8s default
Blue/GreenNoInstantCritical services, financial
CanaryNoFast~1.1×High-risk releases, new features
Feature FlagNoInstantFeature rollouts, A/B testing
💡 Kubernetes rollout commands
kubectl rollout status deployment/myapp
kubectl rollout history deployment/myapp
kubectl rollout undo deployment/myapp
kubectl rollout undo deployment/myapp --to-revision=2
Deployment checklist
Before every production deploy:
✅ Tests passing + coverage gate
✅ Staging deploy verified
✅ Rollback plan defined
✅ Team notified (who's on-call?)
✅ DB migration backward-compatible
✅ Monitoring dashboards open
Week 3 Complete Review

5 days — CI/CD mastery achieved

Day Topic Key Skills File
11CI/CD ConceptsCI vs CD vs Deployment, pipeline anatomy, Jenkins vs GHAci.yml ✅
12Pipeline Deep Diveneeds:, matrix, secrets, cache, withCredentialsci-advanced.yml ✅
13Testing in CIJest, coverage threshold, quality gate fail+fixtest.yml ✅
14Artifacts & DockerMulti-stage Dockerfile, GHCR push, npm auditdocker.yml ✅
15 ✅Continuous DeploymentEnvironments, approval gates, strategies, rollbackdeploy.yml ✅
The pipeline you built — in production terms
DP World, Netflix, Spotify, GitHub itself — all run CI/CD pipelines structurally identical to what you built this week. The tools may differ (Argo CD vs GitHub Actions, ECR vs GHCR) but the pattern is the same: commit → test → build → gate → deploy.
Week 4 Preview — Docker Deep Dive
  • Day 16 — Docker architecture (namespaces, cgroups)
  • Day 17 — Dockerfile advanced + multi-arch builds
  • Day 18 — Docker Compose (multi-container)
  • Day 19 — Docker networking + volumes
  • Day 20 — Container security scanning
📌 Reference

GitHub Environments — complete configuration

Advanced environment configuration
# Environment with all protection rules
jobs:
  deploy-prod:
    environment:
      name: production
      url: https://app.example.com
    # ↑ URL shown in GitHub UI after deploy
    # ↑ required reviewers set in GitHub Settings

# Environment-specific secrets
# (different from repository secrets)
# production DB_URL ≠ staging DB_URL
    env:
      DB_URL: \${{ secrets.DB_URL }}
      # → uses the DB_URL from 'production' environment
      #    not from the repo-level secrets

# Limit which branches can deploy to production
# Configured in: Settings → Environments → production
# → Deployment branches: main only
# → Prevents feature branches deploying to prod!

# Timeout the approval gate
timeout-minutes: 1440  # 24 hours max wait

# Concurrency — prevent simultaneous deploys
concurrency:
  group: production
  cancel-in-progress: false
  # → queue, don't cancel running deploys
Environment protection rules

Configure in Settings → Environments → [name]:

  • Required reviewers — who can approve deploys (max 6)
  • Wait timer — delay N minutes before running (even without reviewers)
  • Deployment branches — which branches can deploy here (e.g. main only)
  • Environment secrets — separate from repo secrets, only available for that environment
  • Environment variables — non-secret config per environment
💡 Separate secrets per environment
production DB_URL ≠ staging DB_URL. Configure environment-level secrets so staging code can never accidentally hit the production database, even if someone misconfigures the pipeline.
⚠ concurrency: cancel-in-progress: false
For deployments: always use cancel-in-progress: false. Never cancel a running production deployment — it can leave the system in a partial state. Queue it instead.
📌 Notifications

Deploy notifications — keep the team informed

Slack deploy notification
notify:
  needs: [ deploy-production ]
  if: always()    # notify even on failure
  runs-on: ubuntu-latest
  steps:
    - name: Notify Slack — success
      if: needs.deploy-production.result == 'success'
      uses: slackapi/slack-github-action@v1.26.0
      with:
        payload: |
          {
            "text": "✅ Deployed to production",
            "blocks": [
              {
                "type": "section",
                "text": {
                  "type": "mrkdwn",
                  "text": "✅ *Production deploy successful*\n*SHA:* \`\${{ github.sha }}\`\n*By:* \${{ github.actor }}\n*Repo:* \${{ github.repository }}"
                }
              }
            ]
          }
      env:
        SLACK_WEBHOOK_URL: \${{ secrets.SLACK_WEBHOOK_URL }}

    - name: Notify Slack — failure
      if: needs.deploy-production.result == 'failure'
      uses: slackapi/slack-github-action@v1.26.0
      with:
        payload: |
          { "text": "❌ Production deploy FAILED — \${{ github.sha }}" }
      env:
        SLACK_WEBHOOK_URL: \${{ secrets.SLACK_WEBHOOK_URL }}
Setting up Slack webhooks
  1. Create a Slack App at api.slack.com/apps
  2. Enable Incoming Webhooks
  3. Add webhook to workspace → select channel
  4. Copy the webhook URL
  5. GitHub → Settings → Secrets → add SLACK_WEBHOOK_URL
Other notification options
  • Teams: microsoft/teams-deployments@v1
  • Email: built-in GitHub email notifications for environment approvals
  • PagerDuty: pagerduty/pagerduty-send-event-action@v2
  • GitHub deployment status: shows in repo Deployments tab automatically
📌 Troubleshooting

Common CD issues & fixes

Problem Cause Fix
deploy-production never starts (no gate UI)Environment not created or no reviewer setSettings → Environments → create production → add Required reviewers.
"Environment not found" errorEnvironment name in YAML doesn't match GitHubName is case-sensitive. environment: production must match the environment name exactly.
deploy-staging skipped (yellow ○)test job failed or if: condition not metFix the failing test job first. Check if: expressions.
Approval notification not receivedGitHub notification settingsGitHub → Settings → Notifications → Actions → enable environment deployment notifications.
Deployment branch restriction blockingBranch not allowed for this environmentSettings → Environments → production → Deployment branches → add branch pattern.
Slack notification failsInvalid or expired webhook URLRegenerate webhook in Slack API. Update the GitHub secret.
Production deploys cancel each otherMissing concurrency settingAdd concurrency: group: production, cancel-in-progress: false.
Staging uses prod secretsUsing repo-level secrets instead of env-levelMove environment-specific secrets to the environment (not repo-level) in GitHub Settings.
📌 Complete Reference

Week 3 pipeline files — what each does

ci.yml — triggers on PR + push
# Runs on: PR + push to main/feat/**
# Jobs: lint, test (matrix Node 18+20)
# Gate: ESLint errors block merge
# Purpose: quality gate on every PR
test.yml — coverage gate
# Runs on: push to main + feat/**
# Jobs: jest --coverage
# Gate: 70% coverage threshold
# Purpose: no code without tests
docker.yml — image build
# Runs on: push to main + git tags
# Jobs: npm audit + docker build + push
# Output: ghcr.io/user/app:sha-xxx
# Purpose: deployable artifact
deploy.yml — staging + prod
# Runs on: push to main
# Jobs: test → staging → (gate) → prod → notify
# Gate: production requires approval
# Purpose: full CD
Why 5 separate files (not 1 big file)?
Each workflow is independently triggered, independently visible in the Actions tab, and independently debuggable. One big workflow = one failing job blocks visibility of all others. Separation of concerns applies to pipelines too.
Trigger order on a normal merge
  1. PR opens: ci.yml runs (lint + test)
  2. PR merge to main:
    • test.yml runs (coverage gate)
    • docker.yml runs (image push)
    • deploy.yml runs (staging → prod)
  3. Git tag push:
    • docker.yml runs (semver-tagged image)
Week 3 Complete 🎉

Action items & Week 4 preview

Week 3 Checklist
  • ci.yml — lint + test on PRs
  • test.yml — Jest + 70% coverage gate
  • docker.yml — image in GHCR
  • deploy.yml — staging auto + prod gated
  • ✅ GitHub Environments configured
  • ✅ Production deployment approved at least once
  • ✅ Rollback demo (reject + git revert)
  • ✅ Commit: ci: add full cd pipeline
Weekend Challenge
Trigger the full pipeline end-to-end:
1. Create a feature branch
2. Make a code change + test
3. PR → CI passes
4. Merge → staging auto-deploys
5. Approve production → deploys
6. Check GHCR for the new image

Full loop in <15 minutes.
Week 4 — Docker Fundamentals (Days 16–20)
  • Day 16 — Docker architecture: namespaces, cgroups, Union FS
  • Day 17 — Advanced Dockerfiles: build args, multi-arch, distroless
  • Day 18 — Docker Compose: multi-container apps, networking
  • Day 19 — Container networking and volumes in depth
  • Day 20 — Container security: scanning, signing, rootless
Week 5 and beyond — Kubernetes

Week 4 Docker mastery feeds directly into Week 5: Kubernetes. What you built in Week 3 (Docker images, CD pipeline) connects to what you'll deploy in Week 5 (AKS, Helm, kubectl).

The full 35-day arc: Git → CI/CD → Docker → Kubernetes → Observability → Security → Capstone