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:
- Deployed a full-stack application (React frontend, Node.js backend, and database) to the cloud
- Configured proper networking, security, and scaling
- Set up a custom domain with HTTPS
- Implemented comprehensive monitoring and alerting
- Created documentation for your deployment process
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:
- Frontend: React application created with Create React App
- Backend: RESTful API built with Node.js and Express
- Database: MongoDB for data storage
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:
- Infrastructure monitoring (CPU, memory, disk usage)
- Application performance monitoring (response times, error rates)
- Centralized logging
- Uptime monitoring with alerts
- Custom dashboard for key metrics
Project Approach
We'll follow George Polya's 4-step problem-solving method:
- Understand the Problem: Clarify what we're trying to accomplish
- Devise a Plan: Create a step-by-step approach
- Execute the Plan: Implement our solution
- Review/Extend: Evaluate the solution and consider improvements
Step 1: Understand the Problem
Deploying a full-stack application involves several interconnected challenges:
Technical Challenges
- Provisioning appropriate infrastructure for each component
- Ensuring secure communication between frontend, backend, and database
- Setting up networking and security correctly
- Configuring a domain name and SSL certificates
- Implementing comprehensive monitoring
Production Considerations
- Reliability: The application should be resilient to failures
- Performance: The application should respond quickly to user requests
- Security: User data and application resources must be protected
- Scalability: The application should handle increasing load
- Maintainability: The deployment should be well-documented and reproducible
Before proceeding, let's clarify what we want to achieve:
- Deploy a working application that users can access via a custom domain
- Ensure the application is secure, reliable, and performs well
- Set up comprehensive monitoring to detect and alert on issues
- Document the deployment process for future reference
Step 2: Devise a Plan
Let's break down our deployment and monitoring process into manageable steps:
- Prepare the application for production
- Configure environment variables
- Build the frontend for production
- Create Dockerfiles for frontend and backend
- 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
- Configure domain and HTTPS
- Register or configure a domain name
- Set up DNS records
- Obtain and configure SSL certificates
- 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
- Implement monitoring and alerting
- Set up infrastructure monitoring
- Configure application performance monitoring
- Implement centralized logging
- Create monitoring dashboards
- Set up alerting for critical issues
- Test the deployment
- Verify functionality in the production environment
- Test monitoring and alerting
- Perform basic load testing
- Document the deployment
- Create a detailed deployment guide
- Document monitoring setup and alerts
- Create runbooks for common issues
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:
- Sign up or log in to MongoDB Atlas
- Create a new project
- Build a new cluster (choose AWS as the provider)
- Select your preferred region and instance size
- Create a database user with a strong password
- Configure network access (whitelist your application's IP range)
- 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
- Navigate to your domain (e.g.,
https://yourdomain.com) - Test user registration and login functionality
- Create, read, update, and delete tasks
- 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:
- Generate some errors in the application (e.g., try to access a non-existent resource)
- Check that the errors are logged in CloudWatch Logs
- Verify that metrics are being collected in CloudWatch
- 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
- Architecture diagram
- List of all AWS resources
- Network configuration
- Security groups and access control
- Cost estimates
Deployment Guide
- Prerequisites
- Step-by-step deployment instructions
- Environment variables and configuration
- CI/CD pipeline setup
- Rollback procedures
Operations Guide
- Monitoring dashboards and their interpretation
- Alert descriptions and response procedures
- Common troubleshooting scenarios
- Backup and restore procedures
- Scaling guidelines
Evaluate the Solution
Assess your deployment against these criteria:
Functionality
- All features work in the production environment
- Authentication and authorization function correctly
- Data is properly persisted and retrieved
- Error handling is robust
Performance
- Response times are acceptable (typically < 500ms)
- Application handles load effectively
- Static assets are efficiently delivered via CDN
- Database queries are optimized
Security
- All traffic is encrypted (HTTPS)
- Secrets are properly managed (not hardcoded)
- Network security is properly configured
- Authentication is secure
- Input validation prevents common attacks
Reliability
- Monitoring covers all critical components
- Alerts are set up for potential issues
- Logs provide visibility into application behavior
- Infrastructure is resilient to failures
Possible Extensions
Consider these improvements for your deployment:
Infrastructure Improvements
- Multi-region deployment for better global performance
- Auto-scaling based on load
- Disaster recovery planning
- Infrastructure as code for more components
Security Enhancements
- Web Application Firewall (WAF)
- DDoS protection
- Security scanning in the CI/CD pipeline
- Regular security audits
Monitoring Enhancements
- Custom metrics for business KPIs
- Improved logging with structured data
- Advanced alerting with anomaly detection
- User experience monitoring
DevOps Improvements
- Automated testing in the CI/CD pipeline
- Canary deployments
- Feature flags for gradual rollouts
- Chaos engineering for resilience testing
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
- Understanding of cloud infrastructure components and their configuration
- Experience with containerization and orchestration
- Knowledge of monitoring and alerting best practices
- Skills in secure application deployment
- Ability to document deployment processes and procedures
Submission Requirements
Submit the following as your project deliverables:
- GitHub repository with your application code, Dockerfiles, and IaC scripts
- Architecture diagram showing your deployed application components
- Deployment documentation (README.md in your repository)
- Screenshot of your monitoring dashboard
- 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:
- Kubernetes for more advanced container orchestration
- Service mesh technologies like Istio or Linkerd
- GitOps workflows with tools like Flux or ArgoCD
- Advanced monitoring with Prometheus and Grafana
- Infrastructure as Code with more complex Terraform modules
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
- AWS ECS Documentation
- AWS CloudWatch Documentation
- Terraform Documentation
- Docker Documentation
- GitHub Actions Documentation
Books
- "The DevOps Handbook" by Gene Kim, Jez Humble, Patrick Debois, and John Willis
- "Infrastructure as Code" by Kief Morris
- "Site Reliability Engineering: How Google Runs Production Systems" by Niall Richard Murphy, Betsy Beyer, Chris Jones, and Jennifer Petoff
- "Cloud Native DevOps with Kubernetes" by John Arundel and Justin Domingus
- "Terraform: Up & Running" by Yevgeniy Brikman
Online Courses and Tutorials
- AWS Developer Learning Path
- Terraform Beginner to Advanced
- Docker and Kubernetes: The Complete Guide
- A Cloud Guru Courses
- Pluralsight AWS Operations Path