All posts
AWS & Cloud

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

16 min readby imnb
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.

As an AWS Solutions Architect, I've designed dozens of production systems on AWS. This guide distills the lessons learned into practical patterns you can apply immediately. We'll cover compute, storage, databases, networking, and security with real-world examples.

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