|$ curl https://forge-ai.dev/api/markdown?path=docs/deploy/aws
$cat docs/aws-—-cloud-deployment.md
updated Recently·55 min read·published

AWS — Cloud Deployment

CloudAWSIntermediate to Advanced🎯Free Tools
Introduction

Amazon Web Services provides a comprehensive suite of cloud services for deploying web applications at any scale. From simple static sites to distributed microservices, AWS offers managed services that eliminate infrastructure management while providing enterprise-grade reliability.

This guide covers the most practical deployment paths: EC2 for full control, ECS Fargate for containerized apps, Lambda for serverless, and S3 + CloudFront for static sites.

Static Sites — S3 + CloudFront

The simplest and cheapest way to deploy a static site (Next.js static export, React SPA, HTML/CSS/JS) is S3 for hosting and CloudFront for global CDN distribution.

terminal
Bash
1# Build your static site
2npm run build
3
4# Create an S3 bucket
5aws s3 mb s3://my-app-production --region us-east-1
6
7# Enable static website hosting
8aws s3 website s3://my-app-production \
9 --index-document index.html \
10 --error-document 404.html
11
12# Sync build output to S3
13aws s3 sync ./out s3://my-app-production --delete
lib/static-site-stack.ts
TypeScript
1// CDK infrastructure for S3 + CloudFront
2import * as cdk from "aws-cdk-lib";
3import * as s3 from "aws-cdk-lib/aws-s3";
4import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
5import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";
6import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
7
8export class StaticSiteStack extends cdk.Stack {
9 constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
10 super(scope, id, props);
11
12 // S3 bucket for static files
13 const bucket = new s3.Bucket(this, "SiteBucket", {
14 bucketName: "my-app-production",
15 removalPolicy: cdk.RemovalPolicy.DESTROY,
16 autoDeleteObjects: true,
17 blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
18 });
19
20 // CloudFront distribution
21 const distribution = new cloudfront.Distribution(this, "Distribution", {
22 defaultBehavior: {
23 origin: new origins.S3Origin(bucket),
24 viewerProtocolPolicy:
25 cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
26 cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
27 },
28 defaultRootObject: "index.html",
29 errorResponses: [
30 {
31 httpStatus: 404,
32 responsePagePath: "/404.html",
33 responseHttpStatus: 404,
34 },
35 {
36 httpStatus: 403,
37 responsePagePath: "/index.html",
38 responseHttpStatus: 200,
39 },
40 ],
41 });
42
43 // Deploy files to S3
44 new s3deploy.BucketDeployment(this, "Deploy", {
45 sources: [s3deploy.Source.asset("./out")],
46 destinationBucket: bucket,
47 distribution,
48 distributionPaths: ["/*"],
49 });
50
51 new cdk.CfnOutput(this, "DistributionId", {
52 value: distribution.distributionId,
53 });
54
55 new cdk.CfnOutput(this, "SiteUrl", {
56 value: "https://" + distribution.distributionDomainName,
57 });
58 }
59}

info

Use CloudFront error responses to handle client-side routing. Map 403 and 404 errors to index.html so your SPA router handles all routes correctly.
EC2 — Virtual Machines

EC2 gives you full control over virtual machines. Use it when you need custom runtimes, background workers, or cannot containerize your application. Best paired with an Application Load Balancer and Auto Scaling Group.

terminal
Bash
1# Launch an EC2 instance
2aws ec2 run-instances \
3 --image-id ami-0c55b159cbfafe1f0 \
4 --instance-type t3.medium \
5 --key-name my-key \
6 --security-group-ids sg-0123456789 \
7 --subnet-id subnet-0123456789 \
8 --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=my-app}]'
9
10# SSH into the instance
11ssh -i my-key.pem ec2-user@<public-ip>
12
13# Install Node.js on Amazon Linux 2023
14sudo dnf install -y nodejs20
15
16# Clone and start your app
17git clone https://github.com/user/app.git
18cd app && npm ci && npm run build && npm start
user-data.sh
Bash
1# User data script for auto-setup on instance launch
2#!/bin/bash
3dnf update -y
4dnf install -y nodejs20 git nginx
5
6# Clone application
7cd /home/ec2-user
8git clone https://github.com/user/app.git
9cd app
10npm ci
11npm run build
12
13# Configure nginx as reverse proxy
14cat > /etc/nginx/conf.d/app.conf << 'EOF'
15server {
16 listen 80;
17 server_name myapp.example.com;
18
19 location / {
20 proxy_pass http://localhost:3000;
21 proxy_http_version 1.1;
22 proxy_set_header Upgrade $http_upgrade;
23 proxy_set_header Connection "upgrade";
24 proxy_set_header Host $host;
25 proxy_set_header X-Real-IP $remote_addr;
26 }
27}
28EOF
29
30systemctl enable nginx
31systemctl start nginx
32
33# Start app with systemd
34cat > /etc/systemd/system/app.service << 'EOF'
35[Unit]
36Description=My App
37After=network.target
38
39[Service]
40User=ec2-user
41WorkingDirectory=/home/ec2-user/app
42ExecStart=/usr/bin/node dist/server.js
43Restart=always
44Environment=NODE_ENV=production
45
46[Install]
47WantedBy=multi-user.target
48EOF
49
50systemctl enable app
51systemctl start app

best practice

Use Systems Manager Session Manager instead of SSH for production access. It provides auditable, IAM-controlled access without managing SSH keys or opening port 22 in your security group.
ECS Fargate — Serverless Containers

ECS Fargate runs containers without managing servers. You define task definitions (like Docker Compose files), and AWS handles provisioning, scaling, and patching the underlying infrastructure.

task-definition.json
JSON
1{
2 "family": "my-app",
3 "networkMode": "awsvpc",
4 "requiresCompatibilities": ["FARGATE"],
5 "cpu": "512",
6 "memory": "1024",
7 "executionRoleArn": "arn:aws:iam::role/ecsTaskExecutionRole",
8 "containerDefinitions": [
9 {
10 "name": "app",
11 "image": "123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:latest",
12 "portMappings": [
13 {
14 "containerPort": 3000,
15 "protocol": "tcp"
16 }
17 ],
18 "environment": [
19 { "name": "NODE_ENV", "value": "production" }
20 ],
21 "secrets": [
22 {
23 "name": "DATABASE_URL",
24 "valueFrom": "arn:aws:ssm:us-east-1:123456789:parameter/my-app/db-url"
25 }
26 ],
27 "logConfiguration": {
28 "logDriver": "awslogs",
29 "options": {
30 "awslogs-group": "/ecs/my-app",
31 "awslogs-region": "us-east-1",
32 "awslogs-stream-prefix": "ecs"
33 }
34 },
35 "healthCheck": {
36 "command": ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"],
37 "interval": 30,
38 "timeout": 5,
39 "retries": 3
40 }
41 }
42 ]
43}
terminal
Bash
1# Create ECR repository and push image
2aws ecr create-repository --repository-name my-app
3aws ecr get-login-password | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com
4docker build -t my-app .
5docker tag my-app:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:latest
6docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:latest
7
8# Register task definition
9aws ecs register-task-definition --cli-input-json file://task-definition.json
10
11# Create ECS cluster
12aws ecs create-cluster --cluster-name my-app-cluster
13
14# Create Fargate service
15aws ecs create-service \
16 --cluster my-app-cluster \
17 --service-name my-app \
18 --task-definition my-app \
19 --desired-count 2 \
20 --launch-type FARGATE \
21 --network-configuration "awsvpcConfiguration={subnets=[subnet-xxx],securityGroups=[sg-xxx],assignPublicIp=ENABLED}" \
22 --load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:...,containerName=app,containerPort=3000"

best practice

Store secrets in SSM Parameter Store or Secrets Manager, never in environment variables. ECS can inject them at runtime, and they are encrypted at rest and in transit.
Lambda — Serverless Functions

Lambda runs code without provisioning servers. Pay only for compute time consumed — no charge when your code isn't running. Ideal for APIs, background jobs, and event-driven architectures.

lambda/handler.ts
TypeScript
1// Lambda function handler
2import { APIGatewayProxyHandler } from "aws-lambda";
3import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
4import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
5
6const client = new DynamoDBClient({});
7const docClient = DynamoDBDocumentClient.from(client);
8
9export const handler: APIGatewayProxyHandler = async (event) => {
10 const { id } = event.pathParameters || {};
11
12 try {
13 const result = await docClient.send(
14 new GetCommand({
15 TableName: process.env.TABLE_NAME!,
16 Key: { id },
17 })
18 );
19
20 if (!result.Item) {
21 return {
22 statusCode: 404,
23 headers: { "Content-Type": "application/json" },
24 body: JSON.stringify({ error: "Not found" }),
25 };
26 }
27
28 return {
29 statusCode: 200,
30 headers: {
31 "Content-Type": "application/json",
32 "Cache-Control": "public, max-age=300",
33 },
34 body: JSON.stringify(result.Item),
35 };
36 } catch (error) {
37 console.error("Error:", error);
38 return {
39 statusCode: 500,
40 body: JSON.stringify({ error: "Internal server error" }),
41 };
42 }
43};
lib/api-stack.ts
TypeScript
1// CDK Lambda + API Gateway setup
2import * as cdk from "aws-cdk-lib";
3import * as lambda from "aws-cdk-lib/aws-lambda";
4import * as apigateway from "aws-cdk-lib/aws-apigateway";
5import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
6
7export class ApiStack extends cdk.Stack {
8 constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
9 super(scope, id, props);
10
11 const table = new dynamodb.Table(this, "ItemsTable", {
12 partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
13 billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
14 });
15
16 const apiFunction = new lambda.Function(this, "ApiHandler", {
17 runtime: lambda.Runtime.NODEJS_20_X,
18 handler: "handler.handler",
19 code: lambda.Code.fromAsset("lambda"),
20 environment: {
21 TABLE_NAME: table.tableName,
22 },
23 });
24
25 table.grantReadWriteData(apiFunction);
26
27 const api = new apigateway.RestApi(this, "ItemsApi", {
28 restApiName: "Items Service",
29 deployOptions: { stageName: "prod" },
30 });
31
32 const items = api.root.addResource("items");
33 items.addMethod("GET", new apigateway.LambdaIntegration(apiFunction));
34 items.addMethod("POST", new apigateway.LambdaIntegration(apiFunction));
35
36 const item = items.addResource("{id}");
37 item.addMethod("GET", new apigateway.LambdaIntegration(apiFunction));
38
39 new cdk.CfnOutput(this, "ApiUrl", {
40 value: api.url,
41 });
42 }
43}

info

Lambda cold starts add latency. Use provisioned concurrency for latency-sensitive endpoints, and keep function packages under 50MB for fast deployments.
Managed Services

AWS managed services handle patching, backups, scaling, and monitoring automatically. Prefer managed services over self-managed alternatives unless you have specific requirements.

ServiceUse CasePricing
RDSManaged PostgreSQL, MySQL, AuroraPer instance-hour + storage
DynamoDBServerless NoSQL, single-digit ms latencyPer request + storage
ElastiCacheManaged Redis or MemcachedPer node-hour
S3Object storage, static assets, backupsPer GB stored + requests
CloudFrontGlobal CDN, edge caching, SSL terminationPer GB transferred
SQSMessage queuing, decoupled architecturesPer million requests
Monitoring & Observability

CloudWatch provides metrics, logs, and alarms for all AWS services. Combined with X-Ray for distributed tracing, you get full visibility into application behavior.

lib/monitoring-stack.ts
TypeScript
1// CDK CloudWatch alarm setup
2import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
3import * as sns from "aws-cdk-lib/aws-sns";
4
5const alarmTopic = new sns.Topic(this, "AlarmTopic", {
6 displayName: "Production Alarms",
7});
8
9// Alarm for 5xx errors
10new cloudwatch.Alarm(this, "5xxAlarm", {
11 metric: api.metricServerError({
12 period: cdk.Duration.minutes(1),
13 statistic: "Sum",
14 }),
15 threshold: 10,
16 evaluationPeriods: 3,
17 alarmDescription: "More than 10 server errors in 1 minute",
18});
19
20// Alarm for high latency
21new cloudwatch.Alarm(this, "LatencyAlarm", {
22 metric: api.metricLatency({
23 period: cdk.Duration.minutes(5),
24 statistic: "p99",
25 }),
26 threshold: 3000,
27 evaluationPeriods: 2,
28 alarmDescription: "P99 latency exceeds 3 seconds",
29});

best practice

Set up alarms for every critical metric: error rates, latency percentiles, database connections, and queue depths. Route alerts to Slack or PagerDuty via SNS for immediate response.
Cost Optimization

AWS costs can spiral quickly without governance. These strategies reduce costs significantly without sacrificing performance.

Right-Size Instances

Use CloudWatch metrics to identify over-provisioned resources. Switch from fixed EC2 to Lambda for sporadic workloads, and use Fargate Spot for non-critical containers (70% cost reduction).

Reserved Capacity

Commit to 1-year or 3-year reserved instances for predictable workloads. Savings Plans offer flexibility across instance types and provide up to 72% discount compared to on-demand pricing.

Lifecycle Policies

Automatically transition old data to cheaper storage tiers. Move S3 objects to Glacier after 90 days, and delete old snapshots automatically. Set lifecycle policies on all storage resources.

info

Use the AWS Cost Explorer and Budgets to set spending alerts. Create a monthly budget with notifications at 80% and 100% thresholds to avoid surprise bills.

Blueprint — Engineering Documentation · Section ID: DEP-AWS-01 · Revision: 1.0