Deploy and Monitor a Full-Stack Application

Weekend Project: Putting Theory into Practice

Introduction and Objectives

Throughout this module, we've covered various aspects of DevOps and deployment: cloud platforms, deployment strategies, infrastructure as code, monitoring, and domain configuration. Now it's time to put everything together in a comprehensive weekend project.

In this project, you'll deploy a complete full-stack application to a production environment and set up monitoring to ensure its reliability and performance. This hands-on experience will reinforce the concepts you've learned and prepare you for real-world deployment scenarios.

By the end of this weekend project, you will have:

graph TD A[Local Development] --> B[CI/CD Pipeline] B --> C[Cloud Infrastructure] C --> D[Deployed Application] D --> E[Monitoring & Alerting] C --> F[Frontend Hosting] C --> G[Backend Services] C --> H[Database] C --> I[Networking & Security] E --> J[Performance Metrics] E --> K[Error Tracking] E --> L[Logs Management] E --> M[Uptime Monitoring]

Project Specifications

The Application

For this project, we'll use a simple full-stack "Task Manager" application that allows users to create, read, update, and delete tasks. The application consists of:

You can either use the provided Task Manager application or adapt your own project to follow along.

Application Repository: https://github.com/example/task-manager (placeholder URL)

Deployment Environment

You'll deploy the application to a cloud provider of your choice. The instructions will focus on AWS, but the concepts apply to other providers like Google Cloud Platform or Microsoft Azure.

Monitoring Requirements

Your deployment must include:

Project Approach

We'll follow George Polya's 4-step problem-solving method:

  1. Understand the Problem: Clarify what we're trying to accomplish
  2. Devise a Plan: Create a step-by-step approach
  3. Execute the Plan: Implement our solution
  4. Review/Extend: Evaluate the solution and consider improvements

Step 1: Understand the Problem

Deploying a full-stack application involves several interconnected challenges:

Technical Challenges

Production Considerations

flowchart LR A[Users] --> B[DNS] B --> C[Load Balancer] C --> D[Frontend/CDN] D <--> E[Backend API] E <--> F[Database] G[Monitoring] --> C G --> D G --> E G --> F H[Alerting] --> G

Before proceeding, let's clarify what we want to achieve:

Step 2: Devise a Plan

Let's break down our deployment and monitoring process into manageable steps:

  1. Prepare the application for production
    • Configure environment variables
    • Build the frontend for production
    • Create Dockerfiles for frontend and backend
  2. Set up cloud infrastructure
    • Create a Virtual Private Cloud (VPC) with proper networking
    • Provision compute resources (EC2, ECS, or serverless options)
    • Set up a managed database service
    • Configure load balancing and auto-scaling
  3. Configure domain and HTTPS
    • Register or configure a domain name
    • Set up DNS records
    • Obtain and configure SSL certificates
  4. Deploy the application
    • Create a CI/CD pipeline (using GitHub Actions or similar)
    • Deploy the frontend to a CDN or web hosting service
    • Deploy the backend to a container service or virtual machines
    • Configure the application to use the production database
  5. Implement monitoring and alerting
    • Set up infrastructure monitoring
    • Configure application performance monitoring
    • Implement centralized logging
    • Create monitoring dashboards
    • Set up alerting for critical issues
  6. Test the deployment
    • Verify functionality in the production environment
    • Test monitoring and alerting
    • Perform basic load testing
  7. Document the deployment
    • Create a detailed deployment guide
    • Document monitoring setup and alerts
    • Create runbooks for common issues
gantt title Deployment Timeline dateFormat YYYY-MM-DD axisFormat %d section Infrastructure Set up VPC and networking :a1, 2025-05-10, 1d Provision compute resources :a2, after a1, 1d Set up database service :a3, after a1, 1d Configure load balancer :a4, after a2, 1d section Application Prepare for production :b1, 2025-05-10, 1d Set up CI/CD pipeline :b2, after b1, 1d Deploy frontend :b3, after a4 b2, 1d Deploy backend :b4, after a4 b2, 1d section Domain & Security Configure domain and DNS :c1, after a4, 1d Set up SSL certificates :c2, after c1, 1d section Monitoring Infrastructure monitoring :d1, after b3 b4, 1d Application monitoring :d2, after d1, 1d Centralized logging :d3, after d1, 1d Create dashboards and alerts :d4, after d2 d3, 1d section Testing & Documentation Test deployment :e1, after c2 d4, 1d Create documentation :e2, after e1, 1d

Step 3: Execute the Plan

Part 1: Prepare the Application for Production

Environment Variables

First, ensure your application uses environment variables for configuration instead of hardcoded values. Create a .env.example file in both frontend and backend:

Backend .env.example (located in /backend/.env.example):

# Server configuration
PORT=5000
NODE_ENV=production

# Database configuration
MONGODB_URI=mongodb://username:password@mongodb-host:27017/taskmanager

# JWT Secret for authentication
JWT_SECRET=your_production_jwt_secret

# Logging
LOG_LEVEL=info

# CORS
ALLOWED_ORIGINS=https://yourdomain.com

Frontend .env.example (located in /frontend/.env.example):

# API URL
REACT_APP_API_URL=https://api.yourdomain.com

# Environment
REACT_APP_ENV=production

# Analytics ID (if using Google Analytics)
REACT_APP_GA_ID=UA-XXXXXXXXX-X
Build the Frontend for Production

Create a production build of the React application:

# Navigate to the frontend directory
cd frontend

# Install dependencies
npm install

# Create a production build
npm run build

This will generate optimized static files in the build directory.

Create Dockerfiles

For consistent deployment, containerize both the frontend and backend.

Frontend Dockerfile (located in /frontend/Dockerfile):

# Build stage
FROM node:18-alpine as build

WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm ci

# Copy the rest of the code
COPY . .

# Build the application
RUN npm run build

# Production stage
FROM nginx:alpine

# Copy the build output to replace the default nginx contents
COPY --from=build /app/build /usr/share/nginx/html

# Copy nginx configuration
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf

# Expose port 80
EXPOSE 80

# Start nginx
CMD ["nginx", "-g", "daemon off;"]

Create an Nginx configuration file (located in /frontend/nginx/nginx.conf):

server {
    listen 80;
    
    # React app
    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
    
    # Enable gzip
    gzip on;
    gzip_vary on;
    gzip_min_length 10240;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/javascript application/xml;
    gzip_disable "MSIE [1-6]\.";
}

Backend Dockerfile (located in /backend/Dockerfile):

FROM node:18-alpine

# Set working directory
WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy app source
COPY . .

# Expose the port the app runs on
EXPOSE 5000

# Command to run the application
CMD ["node", "src/server.js"]
Docker Compose for Local Testing

Create a docker-compose.yml file in the root directory to test locally:

version: '3.8'

services:
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    depends_on:
      - backend
    environment:
      - REACT_APP_API_URL=http://localhost:5000

  backend:
    build: ./backend
    ports:
      - "5000:5000"
    depends_on:
      - mongodb
    environment:
      - PORT=5000
      - NODE_ENV=production
      - MONGODB_URI=mongodb://mongodb:27017/taskmanager
      - JWT_SECRET=local_secret_key
      - ALLOWED_ORIGINS=http://localhost

  mongodb:
    image: mongo:5.0
    ports:
      - "27017:27017"
    volumes:
      - mongodb_data:/data/db

volumes:
  mongodb_data:

Test the containerized application locally:

docker-compose up --build

Part 2: Set Up Cloud Infrastructure (AWS Example)

We'll use AWS for this example, but the concepts apply to other cloud providers as well.

Create a Virtual Private Cloud (VPC)

Set up a VPC with public and private subnets:

# Create a VPC with Terraform

# terraform/main.tf
provider "aws" {
  region = "us-east-1"
}

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  version = "3.14.0"

  name = "task-manager-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]

  enable_nat_gateway = true
  enable_vpn_gateway = false

  tags = {
    Environment = "production"
    Project     = "TaskManager"
  }
}
Set Up a Managed Database

Create a MongoDB Atlas cluster or use AWS DocumentDB:

# MongoDB Atlas setup via Terraform
# terraform/mongodb.tf
resource "mongodbatlas_cluster" "task_manager" {
  project_id   = var.mongodb_atlas_project_id
  name         = "task-manager"
  
  provider_name               = "AWS"
  provider_region_name        = "US_EAST_1"
  provider_instance_size_name = "M10"
  mongo_db_major_version      = "5.0"

  backup_enabled              = true
  auto_scaling_disk_gb_enabled = true

  tags {
    key   = "Environment"
    value = "Production"
  }
}

Alternatively, use the MongoDB Atlas web interface to create a cluster:

  1. Sign up or log in to MongoDB Atlas
  2. Create a new project
  3. Build a new cluster (choose AWS as the provider)
  4. Select your preferred region and instance size
  5. Create a database user with a strong password
  6. Configure network access (whitelist your application's IP range)
  7. Get the connection string for your application
Provision Compute Resources

We'll use Amazon ECS (Elastic Container Service) with Fargate for our containerized application:

# terraform/ecs.tf
resource "aws_ecs_cluster" "task_manager" {
  name = "task-manager-cluster"
}

resource "aws_ecs_task_definition" "backend" {
  family                   = "task-manager-backend"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "256"
  memory                   = "512"
  execution_role_arn       = aws_iam_role.ecs_execution_role.arn
  task_role_arn            = aws_iam_role.ecs_task_role.arn

  container_definitions = jsonencode([
    {
      name      = "backend"
      image     = "${aws_ecr_repository.backend.repository_url}:latest"
      essential = true
      
      portMappings = [
        {
          containerPort = 5000
          hostPort      = 5000
          protocol      = "tcp"
        }
      ]
      
      environment = [
        { name = "NODE_ENV", value = "production" },
        { name = "PORT", value = "5000" },
        { name = "MONGODB_URI", value = var.mongodb_uri },
        { name = "JWT_SECRET", value = var.jwt_secret },
        { name = "ALLOWED_ORIGINS", value = "https://${var.domain_name}" }
      ]
      
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = "/ecs/task-manager-backend"
          "awslogs-region"        = "us-east-1"
          "awslogs-stream-prefix" = "ecs"
        }
      }
    }
  ])
}
Set Up a Content Delivery Network (CDN) for the Frontend

Deploy the frontend to Amazon S3 and CloudFront:

# terraform/frontend.tf
resource "aws_s3_bucket" "frontend" {
  bucket = "task-manager-frontend"
}

resource "aws_s3_bucket_website_configuration" "frontend" {
  bucket = aws_s3_bucket.frontend.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "index.html"
  }
}

resource "aws_cloudfront_distribution" "frontend" {
  origin {
    domain_name = aws_s3_bucket.frontend.bucket_regional_domain_name
    origin_id   = "S3-task-manager-frontend"

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.frontend.cloudfront_access_identity_path
    }
  }

  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"

  aliases = ["${var.domain_name}"]

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS"]
    cached_methods   = ["GET", "HEAD", "OPTIONS"]
    target_origin_id = "S3-task-manager-frontend"

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  custom_error_response {
    error_code         = 404
    response_code      = 200
    response_page_path = "/index.html"
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.cert.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
}
Configure Load Balancing

Set up an Application Load Balancer for the backend:

# terraform/alb.tf
resource "aws_lb" "backend" {
  name               = "task-manager-backend"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = module.vpc.public_subnets

  enable_deletion_protection = false

  tags = {
    Environment = "production"
    Project     = "TaskManager"
  }
}

resource "aws_lb_target_group" "backend" {
  name        = "task-manager-backend"
  port        = 5000
  protocol    = "HTTP"
  vpc_id      = module.vpc.vpc_id
  target_type = "ip"

  health_check {
    enabled             = true
    interval            = 30
    path                = "/api/health"
    timeout             = 5
    healthy_threshold   = 3
    unhealthy_threshold = 3
    matcher             = "200"
  }
}

resource "aws_lb_listener" "backend" {
  load_balancer_arn = aws_lb.backend.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = aws_acm_certificate.cert.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.backend.arn
  }
}

Part 3: Configure Domain and HTTPS

Set Up a Domain in Route 53
# terraform/dns.tf
resource "aws_route53_zone" "main" {
  name = var.domain_name
}

resource "aws_route53_record" "frontend" {
  zone_id = aws_route53_zone.main.zone_id
  name    = var.domain_name
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.frontend.domain_name
    zone_id                = aws_cloudfront_distribution.frontend.hosted_zone_id
    evaluate_target_health = false
  }
}

resource "aws_route53_record" "api" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "api.${var.domain_name}"
  type    = "A"

  alias {
    name                   = aws_lb.backend.dns_name
    zone_id                = aws_lb.backend.zone_id
    evaluate_target_health = true
  }
}
Create SSL Certificates
# terraform/certificates.tf
resource "aws_acm_certificate" "cert" {
  domain_name       = var.domain_name
  validation_method = "DNS"
  subject_alternative_names = ["*.${var.domain_name}"]

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_route53_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      type   = dvo.resource_record_type
      record = dvo.resource_record_value
    }
  }

  zone_id = aws_route53_zone.main.zone_id
  name    = each.value.name
  type    = each.value.type
  ttl     = 60
  records = [each.value.record]
}

resource "aws_acm_certificate_validation" "cert" {
  certificate_arn         = aws_acm_certificate.cert.arn
  validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}

Part 4: Deploy the Application

Set Up Container Registries
# terraform/ecr.tf
resource "aws_ecr_repository" "frontend" {
  name = "task-manager-frontend"
}

resource "aws_ecr_repository" "backend" {
  name = "task-manager-backend"
}
Create a CI/CD Pipeline with GitHub Actions

Create a workflow file at .github/workflows/deploy.yml:

name: Deploy Task Manager

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Configure AWS credentials
      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
    
    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1
    
    # Build and push frontend image
    - name: Build and push frontend image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: task-manager-frontend
        IMAGE_TAG: ${{ github.sha }}
      run: |
        cd frontend
        echo "REACT_APP_API_URL=https://api.yourdomain.com" > .env.production
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
    
    # Build and push backend image
    - name: Build and push backend image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: task-manager-backend
        IMAGE_TAG: ${{ github.sha }}
      run: |
        cd backend
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
    
    # Deploy frontend to S3
    - name: Build and deploy frontend to S3
      run: |
        cd frontend
        npm ci
        npm run build
        aws s3 sync build/ s3://task-manager-frontend --delete
    
    # Invalidate CloudFront cache
    - name: Invalidate CloudFront cache
      run: |
        aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"
    
    # Update ECS service
    - name: Update ECS service
      run: |
        aws ecs update-service --cluster task-manager-cluster --service task-manager-backend --force-new-deployment

Part 5: Implement Monitoring and Alerting

Set Up CloudWatch for Infrastructure Monitoring
# terraform/monitoring.tf
resource "aws_cloudwatch_dashboard" "main" {
  dashboard_name = "TaskManager-Dashboard"
  
  dashboard_body = jsonencode({
    widgets = [
      {
        type   = "metric"
        x      = 0
        y      = 0
        width  = 12
        height = 6
        
        properties = {
          metrics = [
            ["AWS/ECS", "CPUUtilization", "ServiceName", "task-manager-backend", "ClusterName", "task-manager-cluster", { "stat": "Average" }]
          ]
          view    = "timeSeries"
          region  = "us-east-1"
          title   = "Backend CPU Utilization"
          period  = 300
        }
      },
      {
        type   = "metric"
        x      = 12
        y      = 0
        width  = 12
        height = 6
        
        properties = {
          metrics = [
            ["AWS/ECS", "MemoryUtilization", "ServiceName", "task-manager-backend", "ClusterName", "task-manager-cluster", { "stat": "Average" }]
          ]
          view    = "timeSeries"
          region  = "us-east-1"
          title   = "Backend Memory Utilization"
          period  = 300
        }
      },
      {
        type   = "metric"
        x      = 0
        y      = 6
        width  = 12
        height = 6
        
        properties = {
          metrics = [
            ["AWS/ApplicationELB", "RequestCount", "LoadBalancer", "${aws_lb.backend.arn_suffix}", { "stat": "Sum" }]
          ]
          view    = "timeSeries"
          region  = "us-east-1"
          title   = "Backend Request Count"
          period  = 300
        }
      },
      {
        type   = "metric"
        x      = 12
        y      = 6
        width  = 12
        height = 6
        
        properties = {
          metrics = [
            ["AWS/ApplicationELB", "TargetResponseTime", "LoadBalancer", "${aws_lb.backend.arn_suffix}", { "stat": "Average" }]
          ]
          view    = "timeSeries"
          region  = "us-east-1"
          title   = "Backend Response Time"
          period  = 300
        }
      }
    ]
  })
}
Set Up CloudWatch Alarms
# Add to terraform/monitoring.tf
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
  alarm_name          = "task-manager-backend-cpu-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = "300"
  statistic           = "Average"
  threshold           = "80"
  alarm_description   = "This metric monitors ECS CPU utilization"
  
  dimensions = {
    ClusterName = aws_ecs_cluster.task_manager.name
    ServiceName = aws_ecs_service.backend.name
  }
  
  alarm_actions = [aws_sns_topic.alerts.arn]
}

resource "aws_cloudwatch_metric_alarm" "response_time_high" {
  alarm_name          = "task-manager-backend-response-time-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "TargetResponseTime"
  namespace           = "AWS/ApplicationELB"
  period              = "300"
  statistic           = "Average"
  threshold           = "1"
  alarm_description   = "This metric monitors API response time"
  
  dimensions = {
    LoadBalancer = aws_lb.backend.arn_suffix
  }
  
  alarm_actions = [aws_sns_topic.alerts.arn]
}

resource "aws_sns_topic" "alerts" {
  name = "task-manager-alerts"
}
Set Up Centralized Logging with CloudWatch Logs
# Add to terraform/monitoring.tf
resource "aws_cloudwatch_log_group" "backend" {
  name              = "/ecs/task-manager-backend"
  retention_in_days = 30
}
Application Monitoring with AWS X-Ray

Update the backend application to use AWS X-Ray for tracing:

// backend/src/server.js
const AWSXRay = require('aws-xray-sdk');
const express = require('express');

// Configure X-Ray
AWSXRay.config([AWSXRay.plugins.EC2Plugin, AWSXRay.plugins.ECSPlugin]);
const app = AWSXRay.express.openSegment('TaskManagerBackend');

// ... rest of your Express app setup

// Close X-Ray segment
app.use(AWSXRay.express.closeSegment());

Update the backend Dockerfile to include the X-Ray daemon:

# Update backend/Dockerfile
FROM node:18-alpine

# Install AWS X-Ray Daemon
RUN apk --no-cache add curl && \
    curl -o /tmp/xray-daemon.zip https://s3.us-east-2.amazonaws.com/aws-xray-assets.us-east-2/xray-daemon/aws-xray-daemon-linux-3.x.zip && \
    unzip /tmp/xray-daemon.zip -d /tmp && \
    cp /tmp/xray /usr/local/bin/ && \
    rm -rf /tmp/xray-daemon.zip /tmp/xray && \
    apk del curl

# Set working directory
WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy app source
COPY . .

# Add X-Ray to package.json
RUN npm install aws-xray-sdk --save

# Expose the port the app runs on
EXPOSE 5000

# Start X-Ray daemon and application
CMD ["/bin/sh", "-c", "/usr/local/bin/xray -b 0.0.0.0:2000 & node src/server.js"]
Set Up External Monitoring with AWS Synthetic Canary
# Add to terraform/monitoring.tf
resource "aws_synthetics_canary" "website" {
  name                 = "task-manager-website"
  artifact_s3_location = "s3://task-manager-monitoring/canary-artifacts/"
  execution_role_arn   = aws_iam_role.canary_role.arn
  handler              = "index.handler"
  zip_file             = "canaries/website-check.zip"
  runtime_version      = "syn-nodejs-puppeteer-3.4"
  
  schedule {
    expression = "rate(5 minutes)"
  }
  
  run_config {
    timeout_in_seconds = 60
    memory_in_mb       = 1024
    active_tracing     = true
  }
}

Create a simple canary script (save as canaries/website-check.js):

const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');

const pageLoadBlueprint = async function () {
    const urls = [
        'https://yourdomain.com',
        'https://yourdomain.com/tasks',
        'https://api.yourdomain.com/api/health'
    ];

    let page = await synthetics.getPage();
    
    for (const url of urls) {
        log.info(`Testing URL: ${url}`);
        
        const response = await page.goto(url, {waitUntil: 'domcontentloaded', timeout: 30000});
        
        if (response.status() !== 200) {
            throw new Error(`Failed to load ${url}: ${response.status()}`);
        }
        
        await synthetics.takeScreenshot('loaded', 'loaded');
        log.info(`Successfully loaded ${url}`);
    }
};

exports.handler = async () => {
    return await pageLoadBlueprint();
};

Zip the canary script:

mkdir -p canaries
cd canaries
zip website-check.zip website-check.js

Part 6: Test the Deployment

Functional Testing
  1. Navigate to your domain (e.g., https://yourdomain.com)
  2. Test user registration and login functionality
  3. Create, read, update, and delete tasks
  4. Verify all features work as expected
Load Testing

Use a tool like Apache JMeter or Artillery to simulate load:

# Create an Artillery test file (load-test.yml)
config:
  target: "https://api.yourdomain.com"
  phases:
    - duration: 60
      arrivalRate: 5
      rampTo: 20
      name: "Warm up phase"
    - duration: 120
      arrivalRate: 20
      name: "Sustained load"
  defaults:
    headers:
      Content-Type: "application/json"

scenarios:
  - name: "Get tasks and create a new one"
    flow:
      - post:
          url: "/api/auth/login"
          json:
            email: "test@example.com"
            password: "password123"
          capture:
            json: "$.token"
            as: "token"
      
      - get:
          url: "/api/tasks"
          headers:
            Authorization: "Bearer {{ token }}"
      
      - post:
          url: "/api/tasks"
          headers:
            Authorization: "Bearer {{ token }}"
          json:
            title: "Load test task"
            description: "Created during load testing"
            dueDate: "2025-06-01"
            priority: "medium"
            status: "todo"

Run the load test:

artillery run load-test.yml
Monitoring Test

Verify that your monitoring and alerting are working:

  1. Generate some errors in the application (e.g., try to access a non-existent resource)
  2. Check that the errors are logged in CloudWatch Logs
  3. Verify that metrics are being collected in CloudWatch
  4. Test an alarm by temporarily modifying its threshold to trigger it

Step 4: Review and Extend

Documentation

Create comprehensive documentation for your deployment. Include at minimum:

Infrastructure Documentation
Deployment Guide
Operations Guide

Evaluate the Solution

Assess your deployment against these criteria:

Functionality
Performance
Security
Reliability

Possible Extensions

Consider these improvements for your deployment:

Infrastructure Improvements
Security Enhancements
Monitoring Enhancements
DevOps Improvements

Common Challenges and Solutions

Challenge: Application Configuration

Problem: Managing environment-specific configuration across environments.

Solution: Use AWS Systems Manager Parameter Store or AWS Secrets Manager to store configuration values. Retrieve them at runtime rather than building them into container images.

// backend/src/config.js
const AWS = require('aws-sdk');
const ssm = new AWS.SSM();

async function loadConfig() {
  const params = {
    Path: '/task-manager/production/',
    WithDecryption: true
  };
  
  const result = await ssm.getParametersByPath(params).promise();
  
  const config = {};
  result.Parameters.forEach(param => {
    const name = param.Name.split('/').pop();
    config[name] = param.Value;
  });
  
  return config;
}

module.exports = { loadConfig };

Challenge: Database Migrations

Problem: Safely applying database schema changes in production.

Solution: Implement a migration system and integrate it into your CI/CD pipeline.

// backend/src/migrations/20250510-initial.js
module.exports = {
  async up(db) {
    await db.createCollection('tasks');
    await db.collection('tasks').createIndex({ userId: 1 });
  },
  
  async down(db) {
    await db.collection('tasks').drop();
  }
};

// backend/src/migrations/migrate.js
const { MongoClient } = require('mongodb');
const fs = require('fs');
const path = require('path');

async function migrate() {
  const client = new MongoClient(process.env.MONGODB_URI);
  
  try {
    await client.connect();
    const db = client.db();
    
    // Create migrations collection if it doesn't exist
    if (!(await db.listCollections({ name: 'migrations' }).toArray()).length) {
      await db.createCollection('migrations');
    }
    
    // Get applied migrations
    const appliedMigrations = await db.collection('migrations')
      .find()
      .sort({ name: 1 })
      .toArray();
    
    const appliedMigrationNames = new Set(appliedMigrations.map(m => m.name));
    
    // Get migration files
    const migrationsDir = path.join(__dirname);
    const migrationFiles = fs.readdirSync(migrationsDir)
      .filter(file => file.endsWith('.js') && file !== 'migrate.js')
      .sort();
    
    // Apply new migrations
    for (const file of migrationFiles) {
      if (!appliedMigrationNames.has(file)) {
        console.log(`Applying migration: ${file}`);
        
        const migration = require(path.join(migrationsDir, file));
        await migration.up(db);
        
        await db.collection('migrations').insertOne({
          name: file,
          appliedAt: new Date()
        });
        
        console.log(`Migration applied: ${file}`);
      }
    }
  } finally {
    await client.close();
  }
}

// Run migrations if this file is executed directly
if (require.main === module) {
  migrate().catch(console.error);
}

module.exports = { migrate };

Challenge: Zero-Downtime Deployments

Problem: Updating the application without disrupting users.

Solution: Use rolling deployments or blue-green deployments.

# AWS CLI command for ECS rolling update
aws ecs update-service \
  --cluster task-manager-cluster \
  --service task-manager-backend \
  --deployment-configuration maximumPercent=200,minimumHealthyPercent=100 \
  --force-new-deployment

Challenge: Monitoring Actionability

Problem: Too many alerts or unclear monitoring data.

Solution: Focus on actionable metrics and implement proper alert thresholds.

# Improved CloudWatch alarm with dimensions and proper thresholds
resource "aws_cloudwatch_metric_alarm" "api_error_rate" {
  alarm_name          = "task-manager-api-error-rate"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "HTTPCode_ELB_5XX_Count"
  namespace           = "AWS/ApplicationELB"
  period              = "60"
  statistic           = "Sum"
  threshold           = "5"
  alarm_description   = "API server error rate is too high"
  treat_missing_data  = "notBreaching"
  
  dimensions = {
    LoadBalancer = aws_lb.backend.arn_suffix
  }
  
  alarm_actions = [aws_sns_topic.alerts.arn]
  ok_actions    = [aws_sns_topic.alerts.arn]
}

Conclusion and Submission

Congratulations! By completing this weekend project, you've applied the DevOps and deployment concepts covered in this module to create a production-ready full-stack application deployment. You've configured infrastructure, set up monitoring, and documented your process.

Learning Outcomes

Submission Requirements

Submit the following as your project deliverables:

  1. GitHub repository with your application code, Dockerfiles, and IaC scripts
  2. Architecture diagram showing your deployed application components
  3. Deployment documentation (README.md in your repository)
  4. Screenshot of your monitoring dashboard
  5. Brief report (1-2 pages) describing your deployment choices, challenges encountered, and lessons learned

Further Learning

To continue building your deployment and DevOps skills, consider exploring:

Remember, the best way to learn is through practice. Each deployment you do will build your skills and confidence in managing production applications.

Additional Resources

Documentation and Guides

Books

Online Courses and Tutorials

Tools