AWS Hosting
AWS provides the broadest set of cloud services. This skill covers the most common hosting patterns for web applications, APIs, and background services, with infrastructure-as-code via AWS CDK.
Architecture Decision Tree
What are you deploying?
├─ Static site (HTML/CSS/JS) ──────────> S3 + CloudFront
├─ Server-rendered app (Next.js, etc.) ─> ECS Fargate or Lambda
├─ REST/GraphQL API ───────────────────> Lambda + API Gateway OR ECS Fargate + ALB
├─ Long-running background workers ────> ECS Fargate (always-on) or EC2
├─ Event-driven functions ─────────────> Lambda (triggered by SQS, S3, EventBridge)
├─ Containerized microservices ────────> ECS Fargate + ALB + Service Connect
└─ Full control over the VM ───────────> EC2 (last resort)
EC2 — Virtual Machines
Use EC2 only when you need full OS-level control, GPU instances, or specific hardware. For most web apps, prefer ECS Fargate or Lambda.
// CDK: EC2 instance
import * as ec2 from 'aws-cdk-lib/aws-ec2';
const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2 });
const instance = new ec2.Instance(this, 'WebServer', {
vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
machineImage: ec2.MachineImage.latestAmazonLinux2023(),
keyPair: ec2.KeyPair.fromKeyPairName(this, 'KeyPair', 'my-key'),
});
instance.connections.allowFromAnyIpv4(ec2.Port.tcp(80));
instance.connections.allowFromAnyIpv4(ec2.Port.tcp(443));
ECS Fargate — Serverless Containers
The standard choice for containerized web applications. No servers to manage, auto-scaling built in.
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
const cluster = new ecs.Cluster(this, 'Cluster', { vpc });
// Web service with ALB (most common pattern)
const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'WebApp', {
cluster,
cpu: 512,
memoryLimitMiB: 1024,
desiredCount: 2,
taskImageOptions: {
image: ecs.ContainerImage.fromAsset('./'), // Build from Dockerfile in root
containerPort: 3000,
environment: {
NODE_ENV: 'production',
DATABASE_URL: databaseUrl,
},
},
publicLoadBalancer: true,
});
// Auto-scaling
const scaling = service.service.autoScaleTaskCount({ maxCapacity: 10 });
scaling.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
});
scaling.scaleOnMemoryUtilization('MemoryScaling', {
targetUtilizationPercent: 80,
});
ECS with custom task definition
const taskDef = new ecs.FargateTaskDefinition(this, 'TaskDef', {
cpu: 1024,
memoryLimitMiB: 2048,
});
taskDef.addContainer('app', {
image: ecs.ContainerImage.fromEcrRepository(repo, 'latest'),
portMappings: [{ containerPort: 3000 }],
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'app' }),
healthCheck: {
command: ['CMD-SHELL', 'curl -f http://localhost:3000/health || exit 1'],
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
retries: 3,
},
});
Lambda — Serverless Functions
Best for event-driven workloads, APIs with variable traffic, and cost-sensitive projects.
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
const handler = new lambdaNode.NodejsFunction(this, 'ApiHandler', {
runtime: lambda.Runtime.NODEJS_20_X,
entry: 'src/lambda/handler.ts',
handler: 'handler',
memorySize: 512,
timeout: cdk.Duration.seconds(30),
environment: {
DATABASE_URL: databaseUrl,
},
bundling: {
minify: true,
sourceMap: true,
},
});
Lambda handler pattern
// src/lambda/handler.ts
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const body = JSON.parse(event.body ?? '{}');
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'OK', data: body }),
};
};
Lambda limitations
| Constraint | Limit |
|---|---|
| Max execution time | 15 minutes |
| Max memory | 10,240 MB |
| Max payload (sync) | 6 MB |
| Max payload (async) | 256 KB |
| Cold start | 100ms-2s (depends on runtime/size) |
| Concurrent executions | 1,000 (default, can increase) |
API Gateway
HTTP API (v2) — Recommended
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
const httpApi = new apigwv2.HttpApi(this, 'Api', {
corsPreflight: {
allowOrigins: ['https://myapp.com'],
allowMethods: [apigwv2.CorsHttpMethod.GET, apigwv2.CorsHttpMethod.POST],
allowHeaders: ['Content-Type', 'Authorization'],
},
});
httpApi.addRoutes({
path: '/users/{id}',
methods: [apigwv2.HttpMethod.GET],
integration: new integrations.HttpLambdaIntegration('GetUser', handler),
});
REST API (v1) — When you need request validation, API keys, or usage plans
import * as apigw from 'aws-cdk-lib/aws-apigateway';
const api = new apigw.RestApi(this, 'RestApi', {
restApiName: 'MyService',
deployOptions: { stageName: 'prod' },
});
const users = api.root.addResource('users');
users.addMethod('GET', new apigw.LambdaIntegration(handler));
CloudFront CDN
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as s3 from 'aws-cdk-lib/aws-s3';
const siteBucket = new s3.Bucket(this, 'SiteBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
const distribution = new cloudfront.Distribution(this, 'CDN', {
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(siteBucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
},
defaultRootObject: 'index.html',
errorResponses: [
{
httpStatus: 404,
responsePagePath: '/index.html', // SPA fallback
responseHttpStatus: 200,
},
],
});
Route 53 DNS
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
const zone = route53.HostedZone.fromLookup(this, 'Zone', {
domainName: 'example.com',
});
new route53.ARecord(this, 'SiteAlias', {
zone,
recordName: 'app', // app.example.com
target: route53.RecordTarget.fromAlias(
new targets.CloudFrontTarget(distribution)
),
});
RDS — Relational Database
import * as rds from 'aws-cdk-lib/aws-rds';
const database = new rds.DatabaseInstance(this, 'Database', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_16_4,
}),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
databaseName: 'myapp',
credentials: rds.Credentials.fromGeneratedSecret('dbadmin'),
multiAz: false, // true for production
allocatedStorage: 20,
maxAllocatedStorage: 100, // Auto-scaling storage
backupRetention: cdk.Duration.days(7),
deletionProtection: true, // Prevent accidental deletion
});
// Allow ECS tasks to connect
database.connections.allowFrom(service.service, ec2.Port.tcp(5432));
ElastiCache — Redis
import * as elasticache from 'aws-cdk-lib/aws-elasticache';
const subnetGroup = new elasticache.CfnSubnetGroup(this, 'RedisSubnets', {
description: 'Subnets for Redis',
subnetIds: vpc.privateSubnets.map(s => s.subnetId),
});
const redis = new elasticache.CfnCacheCluster(this, 'Redis', {
cacheNodeType: 'cache.t3.micro',
engine: 'redis',
numCacheNodes: 1,
cacheSubnetGroupName: subnetGroup.ref,
vpcSecurityGroupIds: [redisSecurityGroup.securityGroupId],
});
IAM Best Practices
import * as iam from 'aws-cdk-lib/aws-iam';
// Principle of least privilege — grant only what is needed
handler.addToRolePolicy(new iam.PolicyStatement({
actions: ['s3:GetObject', 's3:PutObject'],
resources: [`${bucket.bucketArn}/uploads/*`], // Scoped to prefix
}));
// Never use wildcards for actions in production
// BAD: actions: ['s3:*']
// GOOD: actions: ['s3:GetObject', 's3:PutObject']
// Use managed policies for common patterns
taskDef.taskRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess')
);
IAM Anti-Patterns
| Anti-Pattern | Risk | Correct Approach |
|---|---|---|
Action: '*' |
Full account access | List specific actions |
Resource: '*' |
Applies to all resources | Scope to specific ARNs |
| Long-lived access keys | Key rotation burden, leak risk | Use IAM roles (EC2, ECS, Lambda get them automatically) |
| Root account for anything | Unrestricted, unauditable | Create IAM users/roles, enable MFA on root |
| Sharing credentials between services | Blast radius, no audit trail | One role per service |
AWS CDK — Infrastructure as Code
pnpm add -D aws-cdk-lib constructs
npx cdk init app --language typescript
CDK Project Structure
infra/
bin/app.ts # Entry point — instantiates stacks
lib/
network-stack.ts # VPC, subnets, security groups
compute-stack.ts # ECS, Lambda
data-stack.ts # RDS, ElastiCache, S3
cdn-stack.ts # CloudFront, Route 53
CDK Commands
npx cdk synth # Generate CloudFormation template
npx cdk diff # Preview changes
npx cdk deploy # Deploy all stacks
npx cdk deploy ComputeStack # Deploy specific stack
npx cdk destroy # Tear down
CDK Stack Pattern
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class ComputeStack extends cdk.Stack {
public readonly serviceUrl: string;
constructor(scope: Construct, id: string, props: cdk.StackProps & {
vpc: ec2.IVpc;
databaseUrl: string;
}) {
super(scope, id, props);
// Resources defined here
// Export values for cross-stack references
this.serviceUrl = service.loadBalancer.loadBalancerDnsName;
}
}
CloudWatch Monitoring
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as actions from 'aws-cdk-lib/aws-cloudwatch-actions';
import * as sns from 'aws-cdk-lib/aws-sns';
const alarmTopic = new sns.Topic(this, 'AlarmTopic');
// ECS CPU alarm
new cloudwatch.Alarm(this, 'HighCpu', {
metric: service.service.metricCpuUtilization(),
threshold: 80,
evaluationPeriods: 3,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
}).addAlarmAction(new actions.SnsAction(alarmTopic));
// Lambda error rate alarm
new cloudwatch.Alarm(this, 'LambdaErrors', {
metric: handler.metricErrors({ period: cdk.Duration.minutes(5) }),
threshold: 5,
evaluationPeriods: 1,
}).addAlarmAction(new actions.SnsAction(alarmTopic));
// Custom dashboard
const dashboard = new cloudwatch.Dashboard(this, 'AppDashboard');
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'ECS CPU & Memory',
left: [service.service.metricCpuUtilization()],
right: [service.service.metricMemoryUtilization()],
}),
);
Cost Optimization
| Strategy | Savings | How |
|---|---|---|
| Right-size instances | 30-50% | Use CloudWatch metrics to identify over-provisioned resources |
| Spot instances (ECS) | Up to 90% on compute | Use Fargate Spot for fault-tolerant workloads |
| Reserved instances | 30-60% | Commit to 1-3 year terms for steady-state workloads |
| S3 lifecycle rules | Variable | Move infrequent data to IA/Glacier automatically |
| Lambda right-sizing | 10-40% | Use AWS Lambda Power Tuning to find optimal memory |
| NAT Gateway alternatives | $30+/mo | Use VPC endpoints for S3/DynamoDB, reduce NAT traffic |
| CloudFront caching | Variable | Reduce origin requests with proper cache policies |
| Scheduled scaling | Variable | Scale down non-prod environments nights/weekends |
Fargate Spot for cost savings
const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'App', {
cluster,
capacityProviderStrategies: [
{ capacityProvider: 'FARGATE_SPOT', weight: 2 },
{ capacityProvider: 'FARGATE', weight: 1 }, // Fallback to on-demand
],
taskImageOptions: { image: ecs.ContainerImage.fromAsset('./') },
});
Common Architecture Patterns
Static Site
Route 53 -> CloudFront -> S3 (static files)
API + Database
Route 53 -> CloudFront -> API Gateway -> Lambda -> RDS
-> ElastiCache (caching)
Containerized Web App
Route 53 -> CloudFront -> ALB -> ECS Fargate (2+ tasks)
|
RDS (private subnet)
Event-Driven Processing
S3 upload -> EventBridge -> Lambda -> DynamoDB
SQS queue -> Lambda -> External API
Schedule -> EventBridge -> Lambda -> SNS notification
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Public subnets for databases | Direct internet exposure | Private subnets, access only via application layer |
| No health checks on ALB targets | Traffic routed to dead instances | Configure /health endpoint, set healthy thresholds |
| Lambda with VPC when not needed | Cold start penalty (seconds) | Only put Lambda in VPC if it needs private resources |
| Hardcoding region/account IDs | Breaks multi-env deploys | Use cdk.Stack.of(this).region and env variables |
| One giant CloudFormation stack | Slow deploys, blast radius | Split into network, compute, data, CDN stacks |
| Not enabling deletion protection | Accidental cdk destroy kills DB |
Set deletionProtection: true on RDS, removalPolicy: RETAIN |
| Storing secrets in environment variables | Visible in console, logs | Use AWS Secrets Manager or SSM Parameter Store |