AWS Cloud Architecture: From EC2 to Serverless - A Solutions Architect's Guide
Comprehensive guide to AWS architecture patterns covering compute, storage, databases, and serverless. Learn how to design scalable, cost-effective, and secure cloud infrastructure.
AWS architecture is mostly the discipline of choosing the smallest service boundary that can survive the workload. The interesting decisions are not the service names; they are control of failure, data movement, cost, and operational ownership. The patterns below frame those choices across compute, storage, databases, networking, and security.
Compute: Choosing the Right Service
AWS offers multiple compute options. Here's when to use each:
# Decision Tree for Compute Selection
# EC2 (Virtual Machines)
When to use:
- Need full control over OS and runtime
- Long-running processes (24/7 applications)
- Legacy applications that can't be containerized
- Specific instance types needed (GPU, high memory)
Example: Web application with persistent connections
Instance: t3.medium (2 vCPU, 4GB RAM)
Cost: ~$30/month (with Reserved Instance)
# AWS Lambda (Serverless Functions)
When to use:
- Event-driven, short-lived tasks (<15 minutes)
- Unpredictable or spiky traffic patterns
- Want zero server management
- Infrequent execution (pay per invocation)
Example: Image processing on S3 upload
Cost: $0.20 per 1 million requests + compute time
Free tier: 1M requests/month
# ECS/EKS (Container Orchestration)
When to use:
- Microservices architecture
- Need portability across clouds
- Team already uses Docker/Kubernetes
- Want easier scaling than EC2
Example: API microservices cluster
ECS Fargate: Pay per task, no EC2 management
Cost: ~$50/month for 2 tasks (0.5 vCPU, 1GB each)
# Elastic Beanstalk (Platform as a Service)
When to use:
- Quick deployments without infrastructure code
- Standard web applications (Node, Python, Java, etc.)
- Small teams without DevOps expertise
Example: Django/Flask web app
Handles EC2, load balancer, auto-scaling automaticallyEC2 Best Practices
# 1. Use Auto Scaling for resilience and cost optimization
# Auto Scaling Group configuration example
# Launch Template for consistent EC2 configuration
aws ec2 create-launch-template \
--launch-template-name app-server-template \
--version-description "v1" \
--launch-template-data '{
"ImageId": "ami-0c55b159cbfafe1f0",
"InstanceType": "t3.medium",
"SecurityGroupIds": ["sg-0123456789abcdef0"],
"UserData": "IyEvYmluL2Jhc2gKY3VybCAtc1MgaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL215YXBwL2luc3RhbGwuc2ggfCBiYXNo",
"IamInstanceProfile": {
"Arn": "arn:aws:iam::123456789012:instance-profile/app-server-role"
},
"TagSpecifications": [{
"ResourceType": "instance",
"Tags": [
{"Key": "Name", "Value": "AppServer"},
{"Key": "Environment", "Value": "production"}
]
}]
}'
# Create Auto Scaling Group
aws autoscaling create-auto-scaling-group \
--auto-scaling-group-name app-server-asg \
--launch-template "LaunchTemplateName=app-server-template,Version=1" \
--min-size 2 \
--max-size 10 \
--desired-capacity 3 \
--target-group-arns "arn:aws:elasticloadbalancing:region:account:targetgroup/app-tg" \
--health-check-type ELB \
--health-check-grace-period 300 \
--vpc-zone-identifier "subnet-abc123,subnet-def456,subnet-ghi789"
# 2. Use Reserved Instances for cost savings (up to 75% off)
aws ec2 purchase-reserved-instances-offering \
--reserved-instances-offering-id offering-id \
--instance-count 3
# 3. Enable detailed monitoring for better insights
aws ec2 monitor-instances --instance-ids i-1234567890abcdef0
# 4. Use IMDSv2 for better security
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1Lambda Patterns and Pitfalls
// Lambda Best Practices with Node.js
// 1. Initialize outside handler for connection reuse
const AWS = require('aws-sdk');
const s3 = new AWS.S3(); // ✅ Reused across invocations
const dbConnection = createDBConnection(); // ✅ Connection pooling
exports.handler = async (event) => {
// ❌ Don't initialize here - creates new connection every time
// const s3 = new AWS.S3();
try {
// 2. Use async/await for cleaner error handling
const data = await s3.getObject({
Bucket: process.env.BUCKET_NAME,
Key: event.key
}).promise();
// 3. Process data
const result = processData(data.Body);
// 4. Return proper response
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*' // CORS
},
body: JSON.stringify(result)
};
} catch (error) {
// 5. Log errors for CloudWatch
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: error.message })
};
}
};
// 6. Use environment variables for configuration
const CONFIG = {
bucket: process.env.BUCKET_NAME,
table: process.env.DYNAMODB_TABLE,
apiKey: process.env.API_KEY // Use AWS Secrets Manager for sensitive data
};
// 7. Implement retry logic for external APIs
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
// 8. Cold start optimization
// Keep functions warm with scheduled events
// CloudWatch Events rule: rate(5 minutes)
exports.warmUp = async (event) => {
if (event.source === 'aws.events') {
console.log('Warming up...');
return { statusCode: 200 };
}
return exports.handler(event);
};⚠️ Lambda Caveats: 15-minute max execution time, 10GB memory limit, /tmp storage is ephemeral (512MB max), cold starts can add 1-3 seconds latency for first request.
S3 Storage Patterns
import boto3
import json
from datetime import datetime, timedelta
s3 = boto3.client('s3')
# 1. Lifecycle policies for cost optimization
def create_lifecycle_policy(bucket_name):
"""
Automatically transition objects to cheaper storage classes
"""
lifecycle_config = {
'Rules': [
{
'Id': 'Archive old logs',
'Status': 'Enabled',
'Prefix': 'logs/',
'Transitions': [
{
'Days': 30,
'StorageClass': 'STANDARD_IA' # Infrequent Access
},
{
'Days': 90,
'StorageClass': 'GLACIER' # Long-term archive
}
],
'Expiration': {
'Days': 365 # Delete after 1 year
}
},
{
'Id': 'Delete incomplete multipart uploads',
'Status': 'Enabled',
'AbortIncompleteMultipartUpload': {
'DaysAfterInitiation': 7
}
}
]
}
s3.put_bucket_lifecycle_configuration(
Bucket=bucket_name,
LifecycleConfiguration=lifecycle_config
)
# 2. Pre-signed URLs for secure temporary access
def generate_upload_url(bucket, key, expiration=3600):
"""
Allow users to upload directly to S3 without AWS credentials
"""
url = s3.generate_presigned_url(
'put_object',
Params={'Bucket': bucket, 'Key': key},
ExpiresIn=expiration,
HttpMethod='PUT'
)
return url
def generate_download_url(bucket, key, expiration=300):
"""
Share private files temporarily
"""
url = s3.generate_presigned_url(
'get_object',
Params={'Bucket': bucket, 'Key': key},
ExpiresIn=expiration
)
return url
# 3. Enable versioning for data protection
def enable_versioning(bucket_name):
s3.put_bucket_versioning(
Bucket=bucket_name,
VersioningConfiguration={'Status': 'Enabled'}
)
# 4. Server-side encryption
def upload_with_encryption(bucket, key, data):
s3.put_object(
Bucket=bucket,
Key=key,
Body=data,
ServerSideEncryption='AES256', # or 'aws:kms' for KMS encryption
# For KMS:
# SSEKMSKeyId='arn:aws:kms:region:account:key/key-id'
)
# 5. Event notifications for processing
def setup_lambda_trigger(bucket_name, lambda_arn):
"""
Trigger Lambda when files are uploaded
"""
s3.put_bucket_notification_configuration(
Bucket=bucket_name,
NotificationConfiguration={
'LambdaFunctionConfigurations': [
{
'LambdaFunctionArn': lambda_arn,
'Events': ['s3:ObjectCreated:*'],
'Filter': {
'Key': {
'FilterRules': [
{'Name': 'prefix', 'Value': 'uploads/'},
{'Name': 'suffix', 'Value': '.jpg'}
]
}
}
}
]
}
)
# Storage Class Cost Comparison (per GB/month in us-east-1):
# S3 Standard: $0.023
# S3 Intelligent-Tiering: $0.023 (auto-moves between tiers)
# S3 Standard-IA: $0.0125 (minimum 30-day storage)
# S3 One Zone-IA: $0.01 (single AZ, less resilient)
# S3 Glacier: $0.004 (retrieval time: minutes to hours)
# S3 Glacier Deep Archive: $0.00099 (retrieval time: 12 hours)RDS and Database Selection
-- RDS Best Practices
-- 1. Enable automated backups (retention 7-35 days)
-- AWS Console or CLI:
-- aws rds modify-db-instance --db-instance-identifier mydb \
-- --backup-retention-period 7 --preferred-backup-window "03:00-04:00"
-- 2. Use Multi-AZ for high availability (automatic failover)
-- Cost: 2x single AZ, but essential for production
-- Failover time: typically 60-120 seconds
-- 3. Read replicas for read-heavy workloads
CREATE READ REPLICA mydb-read-1
FROM mydb-source
IN REGION us-east-1;
-- Application connection strategy:
-- Write operations → Primary instance
-- Read operations → Read replica (load balance across multiple replicas)
-- 4. Parameter groups for optimization
-- Example MySQL parameters:
SET GLOBAL max_connections = 500;
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 2; -- Log queries >2 seconds
SET GLOBAL innodb_buffer_pool_size = '12G'; -- 70-80% of instance RAM
-- 5. Connection pooling (application side)
-- ❌ Bad: New connection per query
-- connection = mysql.connect(host, user, password, database)
-- connection.execute(query)
-- connection.close()
-- ✅ Good: Connection pool
-- pool = mysql.createPool({
-- host: process.env.DB_HOST,
-- user: process.env.DB_USER,
-- password: process.env.DB_PASSWORD,
-- database: process.env.DB_NAME,
-- connectionLimit: 10,
-- queueLimit: 0
-- });
-- 6. Use RDS Proxy for serverless applications
-- Benefits:
-- - Connection pooling (reduces DB connections)
-- - Automatic failover (no code changes)
-- - IAM authentication support
-- - Enforces SSL/TLS
-- When to use alternatives:
-- DynamoDB: Key-value access, need single-digit ms latency, unpredictable scale
-- Aurora Serverless: Variable workload, want auto-scaling database
-- ElastiCache (Redis/Memcached): Caching layer, session storage, real-time analyticsSecurity Best Practices
{
"security_checklist": {
"iam": {
"principles": [
"Use IAM roles, never hardcode credentials",
"Follow least privilege principle",
"Enable MFA for root and admin users",
"Rotate access keys every 90 days",
"Use AWS Organizations for multi-account setup"
],
"example_policy": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/uploads/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": "203.0.113.0/24"
}
}
}
]
}
},
"network": {
"vpc_design": "Use private subnets for databases and apps, public for load balancers",
"security_groups": "Whitelist specific ports, not 0.0.0.0/0",
"nacls": "Stateless firewall at subnet level",
"flow_logs": "Enable VPC Flow Logs for audit trail"
},
"data_protection": {
"encryption_at_rest": "Enable for S3, EBS, RDS, DynamoDB",
"encryption_in_transit": "Use TLS/SSL for all connections",
"secrets_management": "AWS Secrets Manager or Systems Manager Parameter Store",
"key_management": "AWS KMS for encryption key management"
},
"monitoring": {
"cloudwatch": "Set alarms for CPU, disk, memory, error rates",
"cloudtrail": "Log all API calls for compliance",
"guardduty": "Threat detection service",
"config": "Track configuration changes"
}
}
}Cost Optimization Strategies
- Right-sizing: Use AWS Compute Optimizer recommendations to match instance types to actual usage
- Reserved Instances: Commit to 1 or 3 years for 40-75% savings on predictable workloads
- Savings Plans: Flexible alternative to RIs with similar discounts
- Spot Instances: Use for fault-tolerant workloads (up to 90% off), great for batch processing
- S3 Intelligent-Tiering: Automatically moves data between access tiers based on usage patterns
- Delete unused resources: EBS volumes, snapshots, Elastic IPs, old AMIs
- Use AWS Cost Explorer: Identify spending trends and anomalies
- Tag everything: Enable cost allocation tags for departmental charge-back
- Auto-scaling: Scale down during off-hours with scheduled actions
- CloudFront CDN: Reduce data transfer costs by caching at edge locations
Key Architecture Patterns
- 3-Tier Architecture: Load Balancer → App Servers (private subnet) → Database (private subnet)
- Serverless API: API Gateway → Lambda → DynamoDB (no servers to manage)
- Microservices: ECS/EKS with Service Mesh for inter-service communication
- Event-Driven: S3 → EventBridge → Lambda for decoupled processing
- Static Website: S3 + CloudFront + Route 53 (costs <$1/month)
- High Availability: Multi-AZ deployments with Auto Scaling Groups across 3 AZs
- Disaster Recovery: Pilot Light or Warm Standby in secondary region
- Data Lake: S3 → Glue → Athena for analytics without moving data
These patterns form the foundation of scalable AWS architectures. Always start with the Well-Architected Framework's five pillars: Operational Excellence, Security, Reliability, Performance Efficiency, and Cost Optimization. Design for failure, automate everything, and monitor relentlessly.