aws

AWS hosting and deployment patterns covering compute (EC2, ECS, Lambda), networking (ALB, CloudFront, Route 53, API Gateway), databases (RDS, ElastiCache), infrastructure-as-code (CDK), IAM, monitorin

SKILL.md

GitHub

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