🚀 AWS Deployment Strategies
AWS is less a single product than a toolbox with hundreds of drawers. The skill isn't memorizing every service — it's recognizing which of four deployment shapes fits your app, then wiring in Infrastructure as Code, CI/CD, secrets, and monitoring around it.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Distinguish AWS's compute, container, serverless, and platform service families
- Compare four deployment strategies — EC2, Elastic Beanstalk, ECS/EKS, and Lambda — and when each fits
- Define infrastructure with Terraform / CloudFormation / CDK (Infrastructure as Code)
- Automate releases with a CI/CD pipeline and manage secrets safely
- Add monitoring, logging, and tracing with CloudWatch and X-Ray
Estimated Time: 40–50 minutes • Difficulty: Intermediate
Hands-on: Architect an AWS deployment for a React + Node.js + database app.
In This Lesson
The AWS Deployment Toolbox
AWS gives you many ways to run code. They cluster into four families, and picking the right family is 80% of the decision. The rest is plumbing.
📖 The four families at a glance
Compute (EC2, Lightsail): raw virtual servers. Maximum control, maximum responsibility.
Containers (ECS, EKS, App Runner): run Docker images with orchestration. Consistent environments, efficient packing.
Serverless (Lambda, Fargate): run code or containers with no servers to manage; pay per use.
Platform (Elastic Beanstalk, Amplify): push code, AWS provisions the infrastructure behind it.
💡 A rule of thumb: start as high up the stack as your app allows. Reach for Elastic Beanstalk or App Runner before EC2, and Lambda before a cluster — drop to lower-level services only when a real requirement forces you there. Every layer of control you take on is a layer you now have to operate.
Four Deployment Strategies
Strategy 1 — EC2 (maximum control)
Deploy directly onto virtual machines. You own the OS, the runtime, and the scaling policy. Best for legacy apps, unusual OS needs, or specialized instances (GPU for ML).
Typical building blocks: an Auto Scaling Group that adds/removes instances by demand, an Application Load Balancer spreading traffic, and a launch template that bootstraps each instance.
# Register two freshly launched instances behind a target group
aws elbv2 register-targets \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/web/73e2d6bc \
--targets Id=i-0abc123 Id=i-0def456
# Roll a new version by refreshing the Auto Scaling Group's instances
aws autoscaling start-instance-refresh --auto-scaling-group-name web-asg
Use when: you need full OS control, specific instance types, or you're lifting-and-shifting a legacy app not yet containerized.
Strategy 2 — Elastic Beanstalk (PaaS simplicity)
Beanstalk provisions and manages the EC2 instances, load balancer, and auto-scaling for you. You just push code in a supported runtime (Node.js, Python, Java, .NET, PHP, Ruby, Go).
# Initialize, create an environment, deploy, set env vars
eb init -p node.js my-app --region us-east-1
eb create production-env
eb deploy
eb setenv NODE_ENV=production DB_HOST=mydb.abc123.us-east-1.rds.amazonaws.com
Use when: you want a standard web app running fast without managing infrastructure, and your stack is on Beanstalk's supported-platform list.
Strategy 3 — Containers with ECS/EKS
Package the app as a Docker image, push it to ECR (Elastic Container Registry), and let ECS or EKS run and scale it. ECS is AWS-native and simpler; EKS is managed Kubernetes for teams that want portability and the K8s ecosystem. Pairing either with Fargate removes server management entirely.
An ECS deploy is: build, push to ECR, then force a new deployment.
# Build, authenticate, tag, and push the image to ECR
docker build -t myapp:latest .
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
docker tag myapp:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
# Trigger a rolling deployment of the new image
aws ecs update-service --cluster my-cluster --service my-service --force-new-deployment
On EKS the same image is described by a standard Kubernetes manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
Use when: you have microservices or a containerized app, need identical dev/prod environments, or want efficient resource packing. Prefer ECS + Fargate for AWS-native simplicity; EKS when you need Kubernetes portability.
Strategy 4 — Serverless with Lambda
Break the backend into functions that run on demand behind API Gateway. No servers, automatic scaling, and you pay per invocation — ideal for spiky or unpredictable traffic.
The AWS SAM (Serverless Application Model) template keeps the whole stack in version control:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
HelloFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs20.x
CodeUri: ./
Events:
Api:
Type: Api
Properties:
Path: /hello
Method: get
# Build and deploy the serverless stack
sam build
sam deploy --guided
Use when: the workload is event-driven, traffic is variable, or you want to minimize operational overhead and idle cost.
Infrastructure as Code
Clicking through the AWS console to build infrastructure is fine to learn, but it doesn't scale and it can't be reviewed, versioned, or reproduced. Infrastructure as Code (IaC) declares your infrastructure in text files you commit to Git — so a whole environment can be rebuilt from scratch, identically, on command.
✅ Why IaC matters
- Reproducible — spin up an identical staging environment from the same file
- Reviewable — infrastructure changes go through pull requests like code
- Versioned —
git blametells you who opened that security group and why - Disaster-proof — rebuild after a region failure instead of remembering clicks
Terraform is the multi-cloud standard. The same tool provisions AWS, Azure, and GCP with the same workflow:
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
vpc_security_group_ids = [aws_security_group.web.id]
tags = { Name = "web-server" }
}
resource "aws_security_group" "web" {
name = "web-sg"
description = "Allow HTTP"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
terraform init # download the AWS provider plugin
terraform plan # preview exactly what will change
terraform apply # create/update the infrastructure
AWS CDK lets you define the same resources in a real programming language — handy when you want loops, conditionals, and IDE autocompletion:
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import { Construct } from 'constructs';
export class MyEcsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, 'MyVpc', { maxAzs: 2 });
const cluster = new ecs.Cluster(this, 'MyCluster', { vpc });
const taskDef = new ecs.FargateTaskDefinition(this, 'TaskDef');
taskDef.addContainer('app', {
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
memoryLimitMiB: 512,
cpu: 256,
portMappings: [{ containerPort: 80 }],
});
new ecs.FargateService(this, 'Service', { cluster, taskDefinition: taskDef, desiredCount: 2 });
}
}
CloudFormation is the underlying AWS-native YAML/JSON engine that CDK compiles down to. Terraform and CDK are usually friendlier day-to-day, but you'll see raw CloudFormation in AWS docs and older projects.
CI/CD Pipelines
A CI/CD pipeline turns "someone deploys manually and prays" into "every merge to main is tested and shipped automatically." You can build this with AWS-native tools (CodePipeline + CodeBuild + CodeDeploy) or, increasingly common, with GitHub Actions deploying into AWS.
A modern GitHub Actions workflow that builds an image and deploys it to ECS looks like this:
name: Deploy to ECS
on:
push:
branches: [main]
permissions:
id-token: write # for OIDC auth to AWS (no long-lived keys)
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: us-east-1
- name: Log in to Amazon ECR
id: ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push image
env:
REGISTRY: ${{ steps.ecr.outputs.registry }}
run: |
docker build -t "$REGISTRY/myapp:$GITHUB_SHA" .
docker push "$REGISTRY/myapp:$GITHUB_SHA"
- name: Deploy to ECS
run: |
aws ecs update-service --cluster my-cluster \
--service my-service --force-new-deployment
⚠️ Don't store AWS keys in CI
Long-lived AWS_ACCESS_KEY_ID secrets in a CI system are a leak waiting to happen. Prefer OIDC federation (as above): GitHub proves its identity to AWS and receives short-lived credentials scoped to one IAM role. Nothing secret is ever stored.
Secrets & Configuration
Never hard-code database passwords or API keys. AWS provides two managed stores for them, injected into your app at runtime.
SSM Parameter Store — good for general config and free-tier secrets:
# Store an encrypted parameter
aws ssm put-parameter \
--name "/myapp/prod/database-url" \
--value "postgres://user:pass@db.example.com:5432/mydb" \
--type SecureString
# Retrieve it (decrypted) at deploy time
aws ssm get-parameter --name "/myapp/prod/database-url" --with-decryption
Secrets Manager — adds automatic rotation, ideal for database credentials:
# Create a rotating secret
aws secretsmanager create-secret \
--name "myapp/db-credentials" \
--secret-string '{"username":"admin","password":"REPLACE_ME"}'
# Fetch it from your app or pipeline
aws secretsmanager get-secret-value --secret-id "myapp/db-credentials"
💡 Least privilege, always. Give each service an IAM role that can read only the secrets it needs. The Lambda that reads the database URL should not be able to read the payment API key. Scope narrowly and you contain the blast radius if any one component is compromised.
Monitoring & Tracing
You can't fix what you can't see. Wire in observability before you need it — during an incident is the worst time to discover you have no logs.
CloudWatch collects metrics, logs, and alarms. This alarm pages you when CPU stays hot:
aws cloudwatch put-metric-alarm \
--alarm-name "HighCPU" \
--namespace AWS/EC2 \
--metric-name CPUUtilization \
--dimensions "Name=InstanceId,Value=i-0abc123" \
--statistic Average --period 300 --evaluation-periods 2 \
--threshold 70 --comparison-operator GreaterThanThreshold \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts
X-Ray traces a request across services, so you can see which hop is slow in a distributed system:
const AWSXRay = require('aws-xray-sdk');
const express = require('express');
const app = express();
// Begin a trace segment for every incoming request
app.use(AWSXRay.express.openSegment('MyApp'));
app.get('/', (req, res) => {
res.send('Hello World!');
});
// Close the segment so the trace is sent to X-Ray
app.use(AWSXRay.express.closeSegment());
app.listen(3000);
✅ The AWS deployment checklist
- Security: least-privilege IAM roles, no hard-coded keys, encryption at rest & in transit
- Reliability: deploy across multiple Availability Zones, health checks, automated backups
- Cost: right-size instances, use auto-scaling, set budget alarms in Cost Explorer
- Operations: IaC for everything, CI/CD for every release, centralized logging, runbooks
Hands-on Exercise
🏋️ Architect an AWS Deployment
Scenario: plan the AWS deployment for a new web app:
- React frontend
- Node.js API backend
- PostgreSQL database
- Image upload + processing
- User authentication
- Expected traffic: ~10,000 users/month, growing
Your tasks
- Choose an AWS service for each component.
- Sketch the architecture as a simple diagram (boxes + arrows).
- Pick a deployment strategy and justify it.
- Outline the CI/CD pipeline and one monitoring alarm.
💡 Hint
A React app is static files — serve it from S3 behind CloudFront, not a running server. That leaves only the Node.js API and image processing needing compute. Ask: is traffic steady (containers) or spiky (serverless)? Either is defensible if you can explain the trade-off.
✅ Example solution
| Component | AWS service | Why |
|---|---|---|
| Frontend | S3 + CloudFront | Cheap, globally cached static hosting |
| API backend | ECS Fargate | Containerized, autoscaling, no servers to patch |
| Database | RDS for PostgreSQL (Multi-AZ) | Managed, highly available |
| Image processing | S3 event → Lambda | Runs only when an image is uploaded |
| Auth | Amazon Cognito | Managed user pools + tokens |
Strategy: hybrid — static frontend on S3/CloudFront, containerized API on Fargate, serverless image processing. Pipeline: GitHub Actions with OIDC → test → build image → push to ECR → update-service. Alarm: CloudWatch on ECS service CPU > 70% for 5 minutes, wired to an SNS topic. This gives room to grow without over-provisioning early.
🎯 Quick Quiz
Question 1: Which strategy is the best fit for an event-driven workload with spiky, unpredictable traffic where you want to minimize idle cost?
Question 2: What is the main benefit of defining your infrastructure with Terraform instead of clicking through the AWS console?
Question 3: Why should a CI/CD pipeline use OIDC federation instead of storing an AWS_ACCESS_KEY_ID secret?
Summary & Quiz
🎉 Key Takeaways
- AWS deployment clusters into four families: compute, containers, serverless, platform.
- Match the strategy to the app — EC2 for control, Beanstalk for simple web apps, ECS/EKS for containers, Lambda for event-driven spiky loads.
- Infrastructure as Code (Terraform / CDK / CloudFormation) makes environments reproducible and reviewable.
- CI/CD automates test → build → deploy; use OIDC, never stored keys.
- Store secrets in SSM / Secrets Manager and add CloudWatch + X-Ray observability from day one.
📚 Further Reading
- AWS Well-Architected Framework
- Amazon ECS Developer Guide
- Terraform — AWS Get Started
- GitHub Actions — AWS OIDC credentials
🚀 What's Next?
AWS gives you power at the cost of complexity. Next we swing to the other end of the spectrum: Deploying to Heroku Platform, where a single git push ships your app.
🎉 Solid work!
You can now choose an AWS strategy and wire it up with IaC, CI/CD, and monitoring. On to the simpler path.