1. Teaching Objectives

By the end of this module, students will be able to: - Understand how Docker, Kubernetes, and Terraform work together in a CD pipeline - Design an end-to-end CD pipeline architecture - Implement a complete CD pipeline using GitHub Actions - Deploy applications automatically from code commit to production - Handle infrastructure provisioning and application deployment - Implement verification and rollback strategies - Troubleshoot common CD pipeline issues

2. End-to-End CD Pipeline Architecture

2.1 Pipeline Overview

A complete CD pipeline integrates all the tools we've learned:

Developer commits code
    ↓
GitHub/GitLab triggers CI/CD
    ↓
CI Stage:
  - Run tests
  - Build Docker image
  - Scan for vulnerabilities
  - Push image to ECR
    ↓
CD Stage:
  - Provision infrastructure (Terraform - EKS cluster)
  - Deploy application (Terraform - Kubernetes resources)
  - Run smoke tests
  - Monitor deployment
    ↓
Production running

2.2 Pipeline Components

Source Control: - Git repository (GitHub, GitLab, Bitbucket) - Triggers pipeline on push/merge

CI/CD Platform: - GitHub Actions, GitLab CI, Jenkins, etc. - Orchestrates the pipeline

Container Registry: - Amazon ECR (or Docker Hub, GCR) - Stores Docker images

Infrastructure: - Terraform for provisioning - AWS EKS cluster - VPC, networking, security

Application: - Kubernetes Deployment - Running containers from ECR images

Monitoring: - Health checks - Logs and metrics - Alerting

3. Complete Pipeline Implementation

3.1 Pipeline Structure

Our pipeline has two main stages:

  1. CI Stage: Build and test
  2. CD Stage: Deploy infrastructure and application

3.2 CI Stage (Continuous Integration)

# .github/workflows/ci.yml
name: Continuous Integration

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install -r requirements-dev.txt

      - name: Run tests
        run: |
          pytest tests/ -v --cov=app --cov-report=xml

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage.xml

  build:
    runs-on: ubuntu-latest
    needs: test

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - 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: us-east-1

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build Docker image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: sample-python-app
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest

      - name: Scan image for vulnerabilities
        run: |
          docker scout cves $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG || true

      - name: Push image to ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: sample-python-app
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

3.3 CD Stage (Continuous Deployment)

# .github/workflows/cd.yml
name: Continuous Deployment

on:
  workflow_run:
    workflows: ["Continuous Integration"]
    types:
      - completed
    branches: [main]

jobs:
  deploy:
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-latest
    environment: production  # Requires approval if configured

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - 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: us-east-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.6.0

      # Step 1: Ensure EKS cluster exists
      - name: Terraform Init (EKS)
        run: |
          cd infra/eks
          terraform init -input=false

      - name: Terraform Plan (EKS)
        run: |
          cd infra/eks
          terraform plan -out=tfplan-eks \
            -var="cluster_name=${{ vars.CLUSTER_NAME }}" \
            -var="aws_region=us-east-1"

      - name: Terraform Apply (EKS)
        run: |
          cd infra/eks
          terraform apply -auto-approve tfplan-eks

      # Step 2: Get EKS cluster info
      - name: Get EKS cluster endpoint
        id: get-cluster
        run: |
          cd infra/eks
          CLUSTER_NAME=$(terraform output -raw cluster_name)
          echo "cluster_name=$CLUSTER_NAME" >> $GITHUB_OUTPUT

      # Step 3: Update kubeconfig
      - name: Update kubeconfig
        run: |
          aws eks update-kubeconfig \
            --name ${{ steps.get-cluster.outputs.cluster_name }} \
            --region us-east-1

      # Step 4: Deploy application to Kubernetes
      - name: Terraform Init (K8s)
        run: |
          cd infra/k8s
          terraform init -input=false

      - name: Terraform Plan (K8s)
        run: |
          cd infra/k8s
          terraform plan -out=tfplan-k8s \
            -var="cluster_name=${{ steps.get-cluster.outputs.cluster_name }}" \
            -var="ecr_repository_url=${{ secrets.ECR_REGISTRY }}/sample-python-app" \
            -var="image_tag=${{ github.event.workflow_run.head_sha }}" \
            -var="environment=production"

      - name: Terraform Apply (K8s)
        run: |
          cd infra/k8s
          terraform apply -auto-approve tfplan-k8s

      # Step 5: Wait for deployment
      - name: Wait for deployment
        run: |
          kubectl rollout status deployment/sample-python-app -n default --timeout=5m

      # Step 6: Get service URL
      - name: Get service URL
        id: service-url
        run: |
          # Wait for LoadBalancer to be ready
          sleep 30
          SERVICE_URL=$(kubectl get service sample-python-app -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
          if [ -z "$SERVICE_URL" ]; then
            SERVICE_URL=$(kubectl get service sample-python-app -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
          fi
          echo "url=http://$SERVICE_URL" >> $GITHUB_OUTPUT

      # Step 7: Smoke tests
      - name: Run smoke tests
        run: |
          SERVICE_URL=${{ steps.service-url.outputs.url }}
          max_attempts=10
          attempt=0

          while [ $attempt -lt $max_attempts ]; do
            if curl -f "$SERVICE_URL/healthz"; then
              echo "Smoke test passed!"
              exit 0
            fi
            echo "Attempt $((attempt+1)) failed, retrying..."
            sleep 10
            attempt=$((attempt+1))
          done

          echo "Smoke tests failed after $max_attempts attempts"
          exit 1

      # Step 8: Verify deployment
      - name: Verify deployment
        run: |
          kubectl get pods -l app=sample-python-app
          kubectl get service sample-python-app
          kubectl logs -l app=sample-python-app --tail=50

4. Pipeline Flow Explained

4.1 Trigger Events

On Push to Main: - CI runs automatically - If CI succeeds, CD runs automatically - For Continuous Deployment (no approval needed)

On Pull Request: - Only CI runs (no deployment) - Validates code before merge

Manual Trigger: - Can manually trigger CD for rollbacks or re-deployments

4.2 Stage Breakdown

CI Stage: 1. Test: Run unit and integration tests 2. Build: Create Docker image 3. Scan: Security vulnerability scanning 4. Push: Store image in ECR with commit SHA tag

CD Stage: 1. Infrastructure: Ensure EKS cluster exists (Terraform) 2. Configure: Set up kubectl access 3. Deploy: Deploy application to Kubernetes (Terraform) 4. Verify: Wait for pods to be ready 5. Test: Run smoke tests 6. Monitor: Check logs and metrics

4.3 Image Tagging Strategy

# Tag with commit SHA for traceability
IMAGE_TAG: ${{ github.sha }}

# Also tag as 'latest' for convenience
docker tag $IMAGE:$SHA $IMAGE:latest

Benefits: - Every deployment is traceable to a commit - Easy rollback (use previous SHA) - No ambiguity about which version is deployed

5. Environment Management

5.1 Multiple Environments

Deploy to different environments:

# Deploy to staging on develop branch
deploy-staging:
  if: github.ref == 'refs/heads/develop'
  environment: staging
  steps:
    - name: Deploy to staging
      run: |
        terraform apply -var="environment=staging" ...

# Deploy to production on main branch
deploy-production:
  if: github.ref == 'refs/heads/main'
  environment: production
  steps:
    - name: Deploy to production
      run: |
        terraform apply -var="environment=production" ...

5.2 Environment-Specific Configuration

Using Terraform Variables:

# Different node sizes per environment
variable "node_desired_size" {
  default = {
    staging    = 1
    production = 3
  }
}

Using Kubernetes Namespaces:

# Separate namespace per environment
resource "kubernetes_namespace" "app" {
  metadata {
    name = var.environment  # staging, production
  }
}

6. Verification and Monitoring

6.1 Smoke Tests

Quick health checks after deployment:

# scripts/smoke_test.py
import requests
import sys

def test_health_endpoint(url):
    try:
        response = requests.get(f"{url}/healthz", timeout=5)
        response.raise_for_status()
        data = response.json()
        assert data["status"] == "ok"
        print(f"✓ Health check passed: {data}")
        return True
    except Exception as e:
        print(f"✗ Health check failed: {e}")
        return False

if __name__ == "__main__":
    url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8080"
    success = test_health_endpoint(url)
    sys.exit(0 if success else 1)

6.2 Deployment Verification

# Check pod status
kubectl get pods -l app=sample-python-app

# Check deployment status
kubectl rollout status deployment/sample-python-app

# View logs
kubectl logs -l app=sample-python-app --tail=100

# Check service
kubectl get service sample-python-app

# Test endpoints
curl http://<service-url>/healthz
curl -X POST http://<service-url>/greet \
  -H "Content-Type: application/json" \
  -d '{"name": "Student"}'

6.3 Monitoring Integration

CloudWatch Logs:

# Send logs to CloudWatch
- name: Setup CloudWatch logging
  run: |
    kubectl create configmap aws-logging \
      --from-literal=cluster-name=${{ steps.get-cluster.outputs.cluster_name }}

Metrics: - Use Prometheus for Kubernetes metrics - CloudWatch Container Insights for AWS metrics - Application metrics via application code

7. Rollback Strategies

7.1 Automatic Rollback

Kubernetes can automatically rollback failed deployments:

# In deployment spec
spec:
  progressDeadlineSeconds: 600  # 10 minutes
  revisionHistoryLimit: 10      # Keep 10 old revisions

If new pods don't become ready, Kubernetes can rollback automatically.

7.2 Manual Rollback

Using Terraform:

# Rollback to previous image tag
cd infra/k8s
terraform apply \
  -var="image_tag=previous-commit-sha" \
  -auto-approve

Using kubectl:

# Rollback deployment
kubectl rollout undo deployment/sample-python-app

# Rollback to specific revision
kubectl rollout undo deployment/sample-python-app --to-revision=2

7.3 Rollback Pipeline

Create a workflow for rollbacks:

# .github/workflows/rollback.yml
name: Rollback

on:
  workflow_dispatch:
    inputs:
      image_tag:
        description: 'Image tag to rollback to'
        required: true

jobs:
  rollback:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - 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: us-east-1

      - name: Update kubeconfig
        run: |
          aws eks update-kubeconfig --name my-cluster --region us-east-1

      - name: Rollback deployment
        run: |
          cd infra/k8s
          terraform apply \
            -var="image_tag=${{ github.event.inputs.image_tag }}" \
            -auto-approve

8. Security Best Practices

8.1 Secrets Management

GitHub Secrets: - Store AWS credentials as secrets - Never commit secrets to code - Use least privilege IAM roles

Kubernetes Secrets:

# Create secrets via Terraform
resource "kubernetes_secret" "app" {
  metadata {
    name = "app-secrets"
  }
  data = {
    api_key = base64encode(var.api_key)
  }
}

8.2 IAM Roles

For GitHub Actions: - Use IAM roles with minimal permissions - Separate roles for different environments - Use OIDC for authentication (better than access keys)

For EKS: - Service accounts with IAM roles (IRSA) - Node groups with appropriate permissions - Cluster with least privilege access

8.3 Image Security

# Scan images for vulnerabilities
- name: Scan image
  run: |
    docker scout cves $IMAGE:$TAG
    # Fail pipeline on high/critical vulnerabilities

9. Cost Optimization

9.1 Infrastructure Costs

EKS: - Control plane: ~$73/month - Worker nodes: Pay for EC2 instances - NAT Gateways: ~$32/month each (use one for cost savings)

Optimization: - Use smaller instance types for staging - Auto-scale node groups - Use Fargate for variable workloads

9.2 Pipeline Costs

GitHub Actions: - Free for public repos - 2000 minutes/month for private repos - Use self-hosted runners for cost savings

10. Troubleshooting Common Issues

Issue 1: Pipeline Fails at Terraform Apply

# Check Terraform state
cd infra/eks
terraform show

# Check for state lock
terraform force-unlock <lock-id>

# Verify AWS credentials
aws sts get-caller-identity

Issue 2: Image Pull Errors

# Verify ECR authentication
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin <registry>

# Check image exists
aws ecr describe-images --repository-name sample-python-app

Issue 3: Pods Not Starting

# Check pod events
kubectl describe pod <pod-name>

# Check logs
kubectl logs <pod-name>

# Check node resources
kubectl top nodes

Issue 4: Service Not Accessible

# Check service endpoints
kubectl get endpoints sample-python-app

# Check service configuration
kubectl get service sample-python-app -o yaml

# Test from within cluster
kubectl run -it --rm debug --image=busybox --restart=Never -- \
  wget -O- http://sample-python-app/healthz

11. Advanced Patterns

11.1 Blue/Green Deployment

Deploy new version alongside old, then switch:

# Deploy "green" version
resource "kubernetes_deployment" "app_green" {
  metadata {
    name = "sample-python-app-green"
  }
  # ... new version configuration
}

# Switch service to green
resource "kubernetes_service" "app" {
  selector = {
    app  = "sample-python-app"
    version = "green"  # Switch to green
  }
}

11.2 Canary Deployment

Gradually roll out to subset of users:

# Main deployment (90% traffic)
resource "kubernetes_deployment" "app_main" {
  replicas = 9
  # ... current version
}

# Canary deployment (10% traffic)
resource "kubernetes_deployment" "app_canary" {
  replicas = 1
  # ... new version
}

11.3 Feature Flags

Use ConfigMaps for feature flags:

resource "kubernetes_config_map" "features" {
  data = {
    new_feature_enabled = "true"
  }
}

12. Key Takeaways

  • ✅ CD pipeline integrates Docker, Kubernetes, and Terraform
  • ✅ CI builds and tests, CD deploys to production
  • ✅ Infrastructure and application are deployed separately
  • ✅ Verification and monitoring are critical
  • ✅ Rollback strategies must be planned
  • ✅ Security best practices are essential
  • ✅ Cost optimization matters for production

13. Next Steps

Now that you understand the complete pipeline: - Lab: Practical Exercise - Hands-on implementation - Experiment with different deployment strategies - Set up monitoring and alerting - Practice rollback procedures

14. Instructor Notes

  • Walk through the pipeline step-by-step
  • Show real-time execution of a deployment
  • Demonstrate rollback procedures
  • Discuss trade-offs of different strategies
  • Emphasize the importance of testing in staging first
  • Show cost implications of infrastructure choices
  • Practice troubleshooting common issues