All posts
AWS & Cloud

AWS Cloud Architecture: From EC2 to Serverless - A Solutions Architect's Guide

16 min readby imnb57
AWSCloud ArchitectureEC2LambdaS3RDSSolutions Architect
Share

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:

yaml
# 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 automatically

EC2 Best Practices

bash
# 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 1

Lambda Patterns and Pitfalls

javascript
// 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

python
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

sql
-- 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 analytics

Security Best Practices

json
{
  "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.

Keep reading