AWS — Cloud Deployment
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.
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.
| 1 | # Build your static site |
| 2 | npm run build |
| 3 | |
| 4 | # Create an S3 bucket |
| 5 | aws s3 mb s3://my-app-production --region us-east-1 |
| 6 | |
| 7 | # Enable static website hosting |
| 8 | aws s3 website s3://my-app-production \ |
| 9 | --index-document index.html \ |
| 10 | --error-document 404.html |
| 11 | |
| 12 | # Sync build output to S3 |
| 13 | aws s3 sync ./out s3://my-app-production --delete |
| 1 | // CDK infrastructure for S3 + CloudFront |
| 2 | import * as cdk from "aws-cdk-lib"; |
| 3 | import * as s3 from "aws-cdk-lib/aws-s3"; |
| 4 | import * as cloudfront from "aws-cdk-lib/aws-cloudfront"; |
| 5 | import * as s3deploy from "aws-cdk-lib/aws-s3-deployment"; |
| 6 | import * as origins from "aws-cdk-lib/aws-cloudfront-origins"; |
| 7 | |
| 8 | export 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
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.
| 1 | # Launch an EC2 instance |
| 2 | aws 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 |
| 11 | ssh -i my-key.pem ec2-user@<public-ip> |
| 12 | |
| 13 | # Install Node.js on Amazon Linux 2023 |
| 14 | sudo dnf install -y nodejs20 |
| 15 | |
| 16 | # Clone and start your app |
| 17 | git clone https://github.com/user/app.git |
| 18 | cd app && npm ci && npm run build && npm start |
| 1 | # User data script for auto-setup on instance launch |
| 2 | #!/bin/bash |
| 3 | dnf update -y |
| 4 | dnf install -y nodejs20 git nginx |
| 5 | |
| 6 | # Clone application |
| 7 | cd /home/ec2-user |
| 8 | git clone https://github.com/user/app.git |
| 9 | cd app |
| 10 | npm ci |
| 11 | npm run build |
| 12 | |
| 13 | # Configure nginx as reverse proxy |
| 14 | cat > /etc/nginx/conf.d/app.conf << 'EOF' |
| 15 | server { |
| 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 | } |
| 28 | EOF |
| 29 | |
| 30 | systemctl enable nginx |
| 31 | systemctl start nginx |
| 32 | |
| 33 | # Start app with systemd |
| 34 | cat > /etc/systemd/system/app.service << 'EOF' |
| 35 | [Unit] |
| 36 | Description=My App |
| 37 | After=network.target |
| 38 | |
| 39 | [Service] |
| 40 | User=ec2-user |
| 41 | WorkingDirectory=/home/ec2-user/app |
| 42 | ExecStart=/usr/bin/node dist/server.js |
| 43 | Restart=always |
| 44 | Environment=NODE_ENV=production |
| 45 | |
| 46 | [Install] |
| 47 | WantedBy=multi-user.target |
| 48 | EOF |
| 49 | |
| 50 | systemctl enable app |
| 51 | systemctl start app |
best practice
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.
| 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 | } |
| 1 | # Create ECR repository and push image |
| 2 | aws ecr create-repository --repository-name my-app |
| 3 | aws ecr get-login-password | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com |
| 4 | docker build -t my-app . |
| 5 | docker tag my-app:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:latest |
| 6 | docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:latest |
| 7 | |
| 8 | # Register task definition |
| 9 | aws ecs register-task-definition --cli-input-json file://task-definition.json |
| 10 | |
| 11 | # Create ECS cluster |
| 12 | aws ecs create-cluster --cluster-name my-app-cluster |
| 13 | |
| 14 | # Create Fargate service |
| 15 | aws 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
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.
| 1 | // Lambda function handler |
| 2 | import { APIGatewayProxyHandler } from "aws-lambda"; |
| 3 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; |
| 4 | import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb"; |
| 5 | |
| 6 | const client = new DynamoDBClient({}); |
| 7 | const docClient = DynamoDBDocumentClient.from(client); |
| 8 | |
| 9 | export 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 | }; |
| 1 | // CDK Lambda + API Gateway setup |
| 2 | import * as cdk from "aws-cdk-lib"; |
| 3 | import * as lambda from "aws-cdk-lib/aws-lambda"; |
| 4 | import * as apigateway from "aws-cdk-lib/aws-apigateway"; |
| 5 | import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; |
| 6 | |
| 7 | export 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
AWS managed services handle patching, backups, scaling, and monitoring automatically. Prefer managed services over self-managed alternatives unless you have specific requirements.
| Service | Use Case | Pricing |
|---|---|---|
| RDS | Managed PostgreSQL, MySQL, Aurora | Per instance-hour + storage |
| DynamoDB | Serverless NoSQL, single-digit ms latency | Per request + storage |
| ElastiCache | Managed Redis or Memcached | Per node-hour |
| S3 | Object storage, static assets, backups | Per GB stored + requests |
| CloudFront | Global CDN, edge caching, SSL termination | Per GB transferred |
| SQS | Message queuing, decoupled architectures | Per million requests |
CloudWatch provides metrics, logs, and alarms for all AWS services. Combined with X-Ray for distributed tracing, you get full visibility into application behavior.
| 1 | // CDK CloudWatch alarm setup |
| 2 | import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch"; |
| 3 | import * as sns from "aws-cdk-lib/aws-sns"; |
| 4 | |
| 5 | const alarmTopic = new sns.Topic(this, "AlarmTopic", { |
| 6 | displayName: "Production Alarms", |
| 7 | }); |
| 8 | |
| 9 | // Alarm for 5xx errors |
| 10 | new 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 |
| 21 | new 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
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
Blueprint — Engineering Documentation · Section ID: DEP-AWS-01 · Revision: 1.0