Cloud Deployment Patterns (AWS, GCP, Azure)
Overview
Cloud platforms (AWS, GCP, Azure) offer multiple ways to deploy the same architecture. Choosing patterns based on your constraints-cost, latency, skill, scale-is crucial. This guide covers proven deployment patterns across the three major cloud platforms, with real-world trade-offs.
Caveat: Each platform has competing patterns. Use /pb-preamble thinking (challenge assumptions, surface trade-offs) and /pb-design-rules thinking (especially Simplicity and Parsimony-choose what you actually need, not what’s available).
Question your actual constraints before choosing. Challenge vendor recommendations. The cheapest or most featured pattern isn’t always the right one. Choose based on your requirements, not vendor features.
Resource Hint: sonnet - Cloud deployment pattern reference; platform-specific implementation guidance.
AWS Patterns
Pattern 1: API on EC2 with RDS
When to use: Small-to-medium services, full control needed, existing infrastructure knowledge
How it works:
- Application runs on EC2 instances (managed servers)
- PostgreSQL/MySQL in RDS (managed database)
- Auto Scaling Group scales instances based on CPU/memory
- Application Load Balancer (ALB) distributes traffic
Go/Python Example (Deployment):
# AWS CloudFormation template (simplified)
AWSTemplateFormatVersion: '2010-09-09'
Resources:
# Security group
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP/HTTPS
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
# RDS Database
Database:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceClass: db.t3.micro
Engine: postgres
AllocatedStorage: 20
MasterUsername: admin
MasterUserPassword: !Sub '{{resolve:secretsmanager:db-password::password}}'
VPCSecurityGroups:
- !GetAtt WebSecurityGroup.GroupId
# Launch Configuration
LaunchConfig:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
ImageId: ami-0c55b159cbfafe1f0 # Amazon Linux 2
InstanceType: t3.micro
UserData:
Fn::Base64: |
#!/bin/bash
yum update -y
yum install -y golang
git clone https://github.com/yourorg/app.git /app
cd /app
go build -o app ./cmd/main.go
./app
# Auto Scaling Group
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
LaunchConfigurationName: !Ref LaunchConfig
MinSize: 2
MaxSize: 10
DesiredCapacity: 2
LoadBalancerNames:
- !Ref LoadBalancer
VPCZoneIdentifier:
- subnet-12345678
- subnet-87654321
# Load Balancer
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Type: application
Scheme: internet-facing
Subnets:
- subnet-12345678
- subnet-87654321
Terraform Alternative:
provider "aws" {
region = "us-east-1"
}
# RDS Database
resource "aws_db_instance" "app_db" {
identifier = "app-db"
engine = "postgres"
engine_version = "14"
instance_class = "db.t3.micro"
allocated_storage = 20
username = "admin"
password = random_password.db.result
skip_final_snapshot = true
lifecycle {
ignore_changes = [password]
}
}
# EC2 Instance
resource "aws_instance" "app_server" {
count = 2
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
security_groups = [aws_security_group.app.id]
user_data = base64encode(file("${path.module}/user_data.sh"))
tags = {
Name = "app-server-${count.index + 1}"
}
}
# Application Load Balancer
resource "aws_lb" "app" {
name = "app-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
}
Trade-offs:
- ✅ Full control over infrastructure
- ✅ Cost-effective for steady workloads
- ✅ Familiar to traditional sysadmins
- ❌ Requires managing patches, security
- ❌ Manual scaling not as responsive
- ❌ Overkill for small/bursty workloads
Pattern 2: Containerized Service on ECS
When to use: Consistent deployments, rolling updates, container-based workflows
How it works:
- Application containerized in Docker
- ECS Fargate runs containers (serverless container orchestration)
- RDS for data persistence
- ALB routes traffic
- CloudWatch monitors logs and metrics
Dockerfile:
FROM golang:1.21 AS builder
WORKDIR /build
COPY . .
RUN go build -o app ./cmd/main.go
FROM debian:bookworm-slim
COPY --from=builder /build/app /app
EXPOSE 8080
CMD ["/app"]
AWS CloudFormation (ECS Fargate):
Resources:
ECRRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: app
ImageScanningConfiguration:
ScanOnPush: true
TaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: app-task
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
Cpu: 256
Memory: 512
ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
ContainerDefinitions:
- Name: app
Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/app:latest'
PortMappings:
- ContainerPort: 8080
Environment:
- Name: DATABASE_URL
Value: !Sub 'postgres://user:pass@${Database.Endpoint.Address}:5432/app'
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: ecs
Service:
Type: AWS::ECS::Service
DependsOn: LoadBalancerListener
Properties:
Cluster: !Ref Cluster
TaskDefinition: !Ref TaskDefinition
DesiredCount: 2
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
Subnets: [subnet-12345, subnet-67890]
SecurityGroups: [sg-abc123]
LoadBalancers:
- ContainerName: app
ContainerPort: 8080
TargetGroupArn: !Ref TargetGroup
AutoScaling:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MaxCapacity: 10
MinCapacity: 2
ResourceId: !Sub 'service/${Cluster}/${Service.Name}'
RoleARN: !Sub 'arn:aws:iam::${AWS::AccountId}:role/service-role'
ScalableDimension: ecs:service:DesiredCount
ServiceNamespace: ecs
ScalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: cpu-scaling
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref AutoScaling
TargetTrackingScalingPolicyConfiguration:
TargetValue: 70.0
PredefinedMetricSpecification:
PredefinedMetricType: ECSServiceAverageCPUUtilization
ScaleOutCooldown: 60
ScaleInCooldown: 300
Trade-offs:
- ✅ Consistent deployments (same container everywhere)
- ✅ Easy rolling updates
- ✅ Fargate abstracts infrastructure
- ❌ Docker knowledge required
- ❌ Less control than EC2
- ❌ Startup time longer than serverless
Pattern 3: API Gateway + Lambda (Serverless)
When to use: Event-driven, variable load, minimal operations, cost-conscious
How it works:
- API Gateway exposes HTTP endpoint
- Lambda functions execute on-demand
- DynamoDB for ultra-high throughput data
- Pay only for compute used
Go Lambda Example:
package main
import (
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
func HandleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// Get user ID from path
userID := request.PathParameters["id"]
// Query DynamoDB
item, err := getUser(userID)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: 500,
Body: "Error retrieving user",
}, nil
}
return events.APIGatewayProxyResponse{
StatusCode: 200,
Body: item.String(),
}, nil
}
func main() {
lambda.Start(HandleRequest)
}
CloudFormation:
Resources:
ApiRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: dynamodb-access
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Query
Resource: !GetAtt UsersTable.Arn
GetUserFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: get-user
Runtime: go1.x
Handler: bootstrap
Code:
S3Bucket: deployment-bucket
S3Key: lambda.zip
Role: !GetAtt ApiRole.Arn
Environment:
Variables:
TABLE_NAME: !Ref UsersTable
ApiGateway:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: user-api
ProtocolType: HTTP
ApiRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref ApiGateway
RouteKey: 'GET /users/{id}'
Target: !Sub 'integrations/${GetUserIntegration}'
GetUserIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref ApiGateway
IntegrationType: AWS_PROXY
IntegrationUri: !Sub 'arn:aws:apigatewayv2:${AWS::Region}:lambda:path/2015-03-31/functions/${GetUserFunction}/invocations'
PayloadFormatVersion: '2.0'
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
Trade-offs:
- ✅ No infrastructure management
- ✅ Cost-effective for bursty load
- ✅ Automatic scaling
- ❌ Cold start latency (500ms+)
- ❌ Limited execution time (15 minutes)
- ❌ Harder to debug and test
GCP Patterns
Pattern 1: Cloud Run (Containers)
When to use: Containerized services, stateless workloads, simple to manage
How it works:
- Push container to Container Registry
- Cloud Run deploys and manages
- Auto-scales based on requests
- Traffic split for canary deployments
- Cloud SQL for databases
Deployment (gcloud CLI):
# Build container
gcloud builds submit --tag gcr.io/PROJECT/app:latest
# Deploy to Cloud Run
gcloud run deploy app \
--image gcr.io/PROJECT/app:latest \
--platform managed \
--region us-central1 \
--memory 512Mi \
--cpu 1 \
--min-instances 1 \
--max-instances 100 \
--allow-unauthenticated \
--set-env-vars DATABASE_URL=cloudsql://... \
--clear-sql-instances
# Canary deployment (10% to new version)
gcloud run services update-traffic app \
--to-revisions app-v1=90,app-v2=10 \
--region us-central1
Terraform:
resource "google_cloud_run_service" "app" {
name = "app"
location = "us-central1"
template {
spec {
containers {
image = "gcr.io/my-project/app:latest"
ports {
container_port = 8080
}
env {
name = "DATABASE_URL"
value = google_sql_database_instance.postgres.connection_name
}
resources {
limits = {
cpu = "1"
memory = "512Mi"
}
}
}
service_account_name = google_service_account.app.email
timeout_seconds = 3600
}
metadata {
annotations = {
"autoscaling.knative.dev/maxScale" = "100"
"autoscaling.knative.dev/minScale" = "1"
}
}
}
traffic {
percent = 100
latest_revision = true
}
}
resource "google_cloud_run_service_iam_member" "public" {
service = google_cloud_run_service.app.name
location = google_cloud_run_service.app.location
role = "roles/run.invoker"
member = "allUsers"
}
Trade-offs:
- ✅ Simple deployment (push container, auto-manages)
- ✅ Easy traffic splitting (canary/blue-green)
- ✅ Pay per request
- ❌ Cold start for idle services
- ❌ Limited to 1 hour execution
- ❌ Not suitable for background jobs
Pattern 2: GKE (Kubernetes)
When to use: Complex microservice architectures, multi-region, advanced networking
How it works:
- Kubernetes cluster manages containers
- Service mesh (Istio) for networking
- Advanced routing, load balancing, retry logic
- StatefulSet for stateful services
Kubernetes Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: app
image: gcr.io/project/app:v1.2
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
selector:
app: api
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: app
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Trade-offs:
- ✅ Powerful multi-region orchestration
- ✅ Advanced networking and routing
- ✅ Service mesh capabilities
- ❌ Steep learning curve
- ❌ Operational overhead
- ❌ Overkill for simple services
Azure Patterns
Pattern 1: App Service (PaaS)
When to use: Simple to moderately complex services, .NET/Node/Python/Go apps
How it works:
- Deploy code or container directly
- App Service handles infrastructure
- Auto-scaling based on metrics
- Azure Database (SQL, PostgreSQL, MySQL)
- Traffic Manager for multi-region
Azure CLI Deployment:
# Create App Service plan
az appservice plan create \
--name myplan \
--resource-group mygroup \
--sku B1 \
--is-linux
# Create App Service
az webapp create \
--resource-group mygroup \
--plan myplan \
--name myapp \
--runtime "go|1.21"
# Deploy from GitHub
az webapp deployment github-actions add \
--repo-url https://github.com/user/app \
--branch main \
--runtime-version 1.21
# Configure environment
az webapp config appsettings set \
--resource-group mygroup \
--name myapp \
--settings DATABASE_URL="Server=mydb..." ENVIRONMENT="production"
# Enable auto-scaling
az monitor autoscale create \
--resource-group mygroup \
--resource myapp \
--resource-type "microsoft.web/serverfarms" \
--min-count 2 \
--max-count 10 \
--count 2
az monitor autoscale rule create \
--resource-group mygroup \
--autoscale-name myappautoscale \
--condition "Percentage CPU > 70 avg 5m" \
--scale out 1
Terraform:
resource "azurerm_app_service_plan" "app" {
name = "app-plan"
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
kind = "Linux"
reserved = true
sku {
tier = "Standard"
size = "S1"
}
}
resource "azurerm_app_service" "app" {
name = "myapp"
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
app_service_plan_id = azurerm_app_service_plan.app.id
site_config {
linux_fx_version = "DOCKER|myregistry.azurecr.io/app:latest"
}
app_settings = {
DATABASE_URL = azurerm_postgresql_server.db.fqdn
ENVIRONMENT = "production"
}
}
resource "azurerm_monitor_autoscale_setting" "app" {
name = "app-autoscale"
resource_group_name = azurerm_resource_group.app.name
location = azurerm_resource_group.app.location
target_resource_id = azurerm_app_service_plan.app.id
profile {
name = "default"
capacity {
default = 2
minimum = 2
maximum = 10
}
rule {
metric_trigger {
metric_name = "CpuPercentage"
metric_resource_id = azurerm_app_service_plan.app.id
time_grain = "PT1M"
statistic = "Average"
time_window = "PT5M"
operator = "GreaterThan"
threshold = 70
}
scale_action {
direction = "Increase"
type = "ChangeCount"
value = 1
cooldown = "PT5M"
}
}
}
}
Trade-offs:
- ✅ Simple to deploy and manage
- ✅ Good integration with .NET ecosystem
- ✅ Built-in auto-scaling
- ❌ Less control than IaaS
- ❌ Vendor lock-in to Azure
- ❌ Cold starts for idle apps
Pattern 2: Azure Container Instances + Functions
When to use: Serverless workloads, event-driven, minimal management
How it works:
- Azure Functions run code on demand
- Timer triggers, HTTP triggers, event triggers
- Auto-scaling per trigger
- Pay per execution
Python Azure Function Example:
import azure.functions as func
import json
from azure.data.tables import TableClient
def main(req: func.HttpRequest) -> func.HttpResponse:
user_id = req.route_params.get('id')
try:
# Query Azure Table Storage
table_client = TableClient.from_connection_string(
conn_str=os.environ['STORAGE_CONNECTION_STRING'],
table_name='Users'
)
entity = table_client.get_entity(partition_key='user', row_key=user_id)
return func.HttpResponse(json.dumps(entity), status_code=200)
except:
return func.HttpResponse("User not found", status_code=404)
Terraform:
resource "azurerm_function_app" "app" {
name = "myapp"
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
app_service_plan_id = azurerm_app_service_plan.consumption.id
storage_account_name = azurerm_storage_account.app.name
storage_account_access_key = azurerm_storage_account.app.primary_access_key
app_settings = {
FUNCTIONS_WORKER_RUNTIME = "python"
APPINSIGHTS_INSTRUMENTATIONKEY = azurerm_application_insights.app.instrumentation_key
}
}
Trade-offs:
- ✅ No infrastructure management
- ✅ Cheap for sporadic workloads
- ✅ Event-driven (timers, queues, HTTP)
- ❌ 10-minute execution limit
- ❌ Cold start latency
- ❌ Vendor lock-in
Cloud Selection Matrix
| Pattern | AWS | GCP | Azure | Best For |
|---|---|---|---|---|
| Simple CRUD API | EC2+RDS | Cloud Run | App Service | Simplicity |
| Serverless Events | Lambda+DynamoDB | Cloud Functions | Functions | Cost-sensitive, bursty |
| Kubernetes Microservices | EKS | GKE | AKS | Complex, multi-region |
| Container Services | ECS Fargate | Cloud Run | Container Instances | Consistency |
| Global CDN | CloudFront | Cloud CDN | Azure CDN | Static/media content |
| Data Warehouse | Redshift | BigQuery | Synapse | Analytics |
| Message Queue | SQS | Pub/Sub | Service Bus | Async processing |
Cost Comparison (Example: API server, 1M requests/month)
| Platform | Compute | Database | Total (monthly) |
|---|---|---|---|
| AWS Lambda | $0.20 | $8 | $8.20 |
| AWS EC2 | $15 | $8 | $23 |
| GCP Cloud Run | $2.50 | $12 | $14.50 |
| Azure Functions | $0.16 | $15 | $15.16 |
Costs vary by region, data transfer, and specific services. Use cloud calculators for accurate estimates.
Anti-Patterns
❌ Lift-and-shift without optimization - Refactor for cloud, not just migrate ❌ Multi-cloud without strategy - Complexity without clear benefit ❌ Ignoring data residency - Some data must stay in specific regions ❌ Not monitoring costs - Cloud spending grows silently ❌ Manual infrastructure - Use Infrastructure as Code (Terraform, CloudFormation) ❌ No disaster recovery - Plan for region failures
When to Use Cloud Patterns
- MVP: Start simple (Lambda/Cloud Functions), add complexity as needed
- High scale: Multi-region architecture with data replication
- Cost-sensitive: Serverless for bursty workloads
- Operations-heavy: Kubernetes for full control
- Simple services: PaaS (App Service, Cloud Run)
Related Commands
/pb-deployment- Deployment strategy selection/pb-patterns-core- Architectural patterns/pb-observability- Cloud monitoring setup/pb-patterns-distributed- Multi-region patterns/pb-zero-stack- $0/month app architecture (static + edge proxy + CI)
Choose cloud patterns based on your constraints: cost, skill, latency, scale. Start simple, evolve with needs.