Skip to main content

🚀 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.

flowchart TD A[AWS deployment options] --> B[Compute services] A --> C[Container services] A --> D[Serverless services] A --> E[Platform services] B --> B1[EC2] B --> B2[Lightsail] C --> C1[ECS + Fargate] C --> C2[EKS] C --> C3[App Runner] D --> D1[Lambda] D --> D2[Fargate] E --> E1[Elastic Beanstalk] E --> E2[Amplify]

📖 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).

graph LR A[Code repo] --> B[CI/CD pipeline] B --> C[Build artifact / AMI] C --> D[Auto Scaling Group of EC2] D --> E[Load Balancer] E --> F[Route 53 DNS] F --> G[Users]

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).

graph LR A[Application code] --> B[Elastic Beanstalk] B --> C[Auto Scaling Group] B --> D[Load Balancer] C --> E[EC2 instances]
# 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.

graph TD A[ECR registry] --> B[ECS cluster] A --> C[EKS cluster] B --> D[ECS service] --> E[Tasks / containers] C --> F[Deployments] --> G[Pods / containers] H[Load Balancer] --> D H --> F

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.

graph TD A[Code repo] --> B[CI/CD pipeline] B --> C[AWS Lambda] C --> D[API Gateway] D --> E[CloudFront] E --> F[Users] G[S3 static site] --> E H[DynamoDB] <--> C

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.

Control versus operational effort across AWS strategies A diagonal axis: EC2 offers the most control and effort, then ECS/EKS, then Elastic Beanstalk, then Lambda with the least of both. More control / more you operate → Managed for you → Lambda Beanstalk ECS / EKS EC2
Figure 1 — The core trade-off: EC2 gives you the most control but the most to operate; Lambda flips both. Pick the highest point on this line your app can live at.

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
  • Versionedgit blame tells 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.

graph LR A[Push to main] --> B[Run tests] B --> C[Build Docker image] C --> D[Push to ECR] D --> E[Deploy to ECS] E --> F[Smoke test / rollback on fail]

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

  1. Choose an AWS service for each component.
  2. Sketch the architecture as a simple diagram (boxes + arrows).
  3. Pick a deployment strategy and justify it.
  4. 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
ComponentAWS serviceWhy
FrontendS3 + CloudFrontCheap, globally cached static hosting
API backendECS FargateContainerized, autoscaling, no servers to patch
DatabaseRDS for PostgreSQL (Multi-AZ)Managed, highly available
Image processingS3 event → LambdaRuns only when an image is uploaded
AuthAmazon CognitoManaged 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

🚀 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.