AWS Compute Services Deep Dive: EC2, Lambda, ECS, and EKS (Part 2 of 6)

AWS offers a comprehensive range of compute services from virtual machines to serverless functions. This guide covers EC2, Lambda, ECS, EKS, and Fargate with practical deployment examples using AWS CDK, CloudFormation, and Terraform.

📚

AWS FUNDAMENTALS SERIES

This is Part 2 of a 6-part series covering AWS Cloud Platform for developers.

  • Part 1: Fundamentals – Account Structure, IAM, Regions
  • Part 2 (this article): Compute Services – EC2, Lambda, ECS, EKS
  • Part 3: Storage & Databases – S3, RDS, DynamoDB
  • Part 4: Networking – VPC, Route 53, CloudFront
  • Part 5: Security & Compliance – KMS, WAF, Shield
  • Part 6: DevOps & IaC – CDK, CloudFormation, Terraform

AWS Compute Services Overview

Choosing the right compute service depends on your workload characteristics, operational preferences, and cost optimization goals. AWS provides options ranging from full-control virtual machines to fully-managed serverless functions.

AWS Compute Services Comparison
Figure 1: AWS Compute Services – IaaS to Serverless Spectrum

Amazon EC2 (Elastic Compute Cloud)

EC2 provides resizable virtual servers (instances) in the cloud. You have complete control over the operating system, networking, and storage configuration.

AWS EC2 Multi-AZ Architecture
Figure 2: Production EC2 Architecture with Auto Scaling

Instance Types & Selection

Family Optimized For Use Cases Examples
M (General) Balanced compute/memory Web servers, app servers m7i.large, m7g.xlarge
C (Compute) High CPU performance Batch processing, HPC c7i.2xlarge, c7g.4xlarge
R (Memory) High memory-to-CPU ratio Databases, in-memory caching r7i.4xlarge, r7g.8xlarge
P/G (GPU) GPU acceleration ML training, graphics p5.48xlarge, g5.xlarge
T (Burstable) Variable workloads Dev/test, microservices t3.micro, t4g.small

EC2 with AWS CDK (TypeScript)

// AWS CDK: Production EC2 Auto Scaling Group
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as autoscaling from 'aws-cdk-lib/aws-autoscaling';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';

export class Ec2Stack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC with public and private subnets
    const vpc = new ec2.Vpc(this, 'AppVpc', {
      maxAzs: 3,
      natGateways: 1,
      subnetConfiguration: [
        { cidrMask: 24, name: 'Public', subnetType: ec2.SubnetType.PUBLIC },
        { cidrMask: 24, name: 'Private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      ],
    });

    // Security group
    const webSg = new ec2.SecurityGroup(this, 'WebSg', {
      vpc,
      description: 'Security group for web servers',
      allowAllOutbound: true,
    });
    webSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP');

    // Launch template with user data
    const userData = ec2.UserData.forLinux();
    userData.addCommands(
      'yum update -y',
      'amazon-linux-extras install nginx1 -y',
      'systemctl start nginx',
      'systemctl enable nginx'
    );

    // Auto Scaling Group
    const asg = new autoscaling.AutoScalingGroup(this, 'WebAsg', {
      vpc,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
      machineImage: ec2.MachineImage.latestAmazonLinux2(),
      minCapacity: 2,
      maxCapacity: 10,
      desiredCapacity: 2,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      securityGroup: webSg,
      userData,
      healthCheck: autoscaling.HealthCheck.elb({ grace: cdk.Duration.minutes(5) }),
    });

    // CPU-based scaling
    asg.scaleOnCpuUtilization('CpuScaling', {
      targetUtilizationPercent: 70,
      cooldown: cdk.Duration.minutes(5),
    });

    // Application Load Balancer
    const alb = new elbv2.ApplicationLoadBalancer(this, 'WebAlb', {
      vpc,
      internetFacing: true,
    });

    const listener = alb.addListener('HttpListener', { port: 80 });
    listener.addTargets('WebTargets', {
      port: 80,
      targets: [asg],
      healthCheck: {
        path: '/health',
        interval: cdk.Duration.seconds(30),
        healthyThresholdCount: 2,
        unhealthyThresholdCount: 5,
      },
    });

    new cdk.CfnOutput(this, 'AlbDns', { value: alb.loadBalancerDnsName });
  }
}

EC2 with Terraform

# Terraform: EC2 Auto Scaling with ALB
resource "aws_launch_template" "web" {
  name_prefix   = "web-"
  image_id      = data.aws_ami.amazon_linux_2.id
  instance_type = "t3.medium"

  network_interfaces {
    associate_public_ip_address = false
    security_groups             = [aws_security_group.web.id]
  }

  user_data = base64encode(<<-EOF
    #!/bin/bash
    yum update -y
    amazon-linux-extras install nginx1 -y
    systemctl start nginx
    systemctl enable nginx
  EOF
  )

  iam_instance_profile {
    name = aws_iam_instance_profile.web.name
  }

  monitoring {
    enabled = true
  }

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name        = "web-server"
      Environment = var.environment
    }
  }
}

resource "aws_autoscaling_group" "web" {
  name                = "web-asg"
  vpc_zone_identifier = aws_subnet.private[*].id
  target_group_arns   = [aws_lb_target_group.web.arn]
  health_check_type   = "ELB"
  min_size            = 2
  max_size            = 10
  desired_capacity    = 2

  launch_template {
    id      = aws_launch_template.web.id
    version = "$Latest"
  }

  instance_refresh {
    strategy = "Rolling"
    preferences {
      min_healthy_percentage = 75
    }
  }
}

resource "aws_autoscaling_policy" "cpu" {
  name                   = "cpu-scale"
  autoscaling_group_name = aws_autoscaling_group.web.name
  policy_type            = "TargetTrackingScaling"

  target_tracking_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ASGAverageCPUUtilization"
    }
    target_value = 70.0
  }
}
⚠️

EC2 LIMITS & CAVEATS

  • Default vCPU limit: 32 On-Demand per region (request increase)
  • EBS volume limit: 5,000 per region
  • Spot instances can be interrupted with 2-minute warning
  • T3/T4g instances have CPU credits - monitor carefully

AWS Lambda (Serverless Functions)

Lambda runs code without provisioning servers. You're charged only for compute time consumed—there's no charge when your code isn't running.

AWS Lambda Serverless Architecture
Figure 3: Lambda Architecture with Event Sources

Lambda with AWS CDK

// AWS CDK: Lambda with API Gateway and DynamoDB
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

export class LambdaStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // DynamoDB table
    const table = new dynamodb.Table(this, 'ItemsTable', {
      partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // Lambda function
    const handler = new lambda.Function(this, 'ApiHandler', {
      runtime: lambda.Runtime.NODEJS_18_X,
      code: lambda.Code.fromAsset('lambda'),
      handler: 'index.handler',
      memorySize: 256,
      timeout: cdk.Duration.seconds(30),
      environment: {
        TABLE_NAME: table.tableName,
      },
      tracing: lambda.Tracing.ACTIVE, // X-Ray tracing
    });

    // Grant permissions
    table.grantReadWriteData(handler);

    // Provisioned Concurrency for low latency
    const version = handler.currentVersion;
    const alias = new lambda.Alias(this, 'ProdAlias', {
      aliasName: 'prod',
      version,
      provisionedConcurrentExecutions: 5,
    });

    // API Gateway
    const api = new apigw.RestApi(this, 'ItemsApi', {
      restApiName: 'Items Service',
      deployOptions: {
        stageName: 'prod',
        throttlingRateLimit: 1000,
        throttlingBurstLimit: 2000,
      },
    });

    const items = api.root.addResource('items');
    items.addMethod('GET', new apigw.LambdaIntegration(alias));
    items.addMethod('POST', new apigw.LambdaIntegration(alias));
    
    const item = items.addResource('{id}');
    item.addMethod('GET', new apigw.LambdaIntegration(alias));
  }
}

Lambda Function Code (Python)

# lambda/handler.py - Lambda function with best practices
import json
import os
import boto3
from aws_lambda_powertools import Logger, Tracer, Metrics
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.utilities.typing import LambdaContext

logger = Logger()
tracer = Tracer()
metrics = Metrics()
app = APIGatewayRestResolver()

# Initialize outside handler for connection reuse
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])

@app.get("/items")
@tracer.capture_method
def get_items():
    response = table.scan(Limit=100)
    return {"items": response.get('Items', [])}

@app.get("/items/")
@tracer.capture_method
def get_item(item_id: str):
    response = table.get_item(Key={'id': item_id})
    if 'Item' not in response:
        return {"error": "Item not found"}, 404
    return response['Item']

@app.post("/items")
@tracer.capture_method
def create_item():
    body = app.current_event.json_body
    table.put_item(Item=body)
    metrics.add_metric(name="ItemsCreated", unit="Count", value=1)
    return {"message": "Created", "id": body.get('id')}, 201

@logger.inject_lambda_context
@tracer.capture_lambda_handler
@metrics.log_metrics(capture_cold_start_metric=True)
def handler(event: dict, context: LambdaContext) -> dict:
    return app.resolve(event, context)
💡

LAMBDA BEST PRACTICES

  • Initialize SDK clients outside the handler for connection reuse
  • Use Provisioned Concurrency for consistent latency
  • Set appropriate memory (CPU scales with memory)
  • Use Lambda Powertools for observability
  • Keep deployment packages small (<50MB unzipped)

⚠️

LAMBDA LIMITS & RESTRICTIONS

Execution timeout 15 minutes maximum
Memory 128 MB – 10,240 MB (10 GB)
Ephemeral storage (/tmp) 512 MB – 10,240 MB
Deployment package 50 MB zipped, 250 MB unzipped
Container image 10 GB maximum
Concurrent executions 1,000 per region (soft limit)
Burst concurrency 500-3,000 depending on region
Environment variables 4 KB total size
Layers 5 layers, 250 MB total unzipped
Payload size (sync) 6 MB request/response
Payload size (async) 256 KB

Note: CPU power scales linearly with memory. At 1,769 MB, you get 1 full vCPU. For CPU-intensive tasks, increase memory even if you don't need the RAM.

Container Services: ECS & EKS

AWS offers two container orchestration services: ECS (AWS-native) and EKS (managed Kubernetes). Both can run on EC2 instances or Fargate (serverless containers).

AWS Container Services Comparison
Figure 4: ECS vs EKS - Container Service Comparison

ECS Fargate with AWS CDK

// AWS CDK: ECS Fargate Service with ALB
import * as cdk from 'aws-cdk-lib';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as ecr from 'aws-cdk-lib/aws-ecr';

export class EcsStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ECS Cluster
    const cluster = new ecs.Cluster(this, 'AppCluster', {
      clusterName: 'app-cluster',
      containerInsights: true,
    });

    // Fargate Service with ALB (L3 construct)
    const service = new ecs_patterns.ApplicationLoadBalancedFargateService(
      this, 'AppService', {
        cluster,
        cpu: 512,
        memoryLimitMiB: 1024,
        desiredCount: 2,
        taskImageOptions: {
          image: ecs.ContainerImage.fromRegistry('nginx:latest'),
          containerPort: 80,
          environment: {
            NODE_ENV: 'production',
          },
        },
        publicLoadBalancer: true,
        circuitBreaker: { rollback: true },
      }
    );

    // Auto Scaling
    const scaling = service.service.autoScaleTaskCount({
      minCapacity: 2,
      maxCapacity: 10,
    });

    scaling.scaleOnCpuUtilization('CpuScaling', {
      targetUtilizationPercent: 70,
      scaleInCooldown: cdk.Duration.minutes(5),
      scaleOutCooldown: cdk.Duration.minutes(2),
    });

    scaling.scaleOnRequestCount('RequestScaling', {
      requestsPerTarget: 1000,
      targetGroup: service.targetGroup,
    });
  }
}

EKS with Terraform

# Terraform: EKS Cluster with managed node groups
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 19.0"

  cluster_name    = "app-cluster"
  cluster_version = "1.28"

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  cluster_endpoint_public_access = true

  # Enable IRSA (IAM Roles for Service Accounts)
  enable_irsa = true

  eks_managed_node_groups = {
    general = {
      desired_size = 2
      min_size     = 2
      max_size     = 10

      instance_types = ["m6i.large", "m5.large"]
      capacity_type  = "ON_DEMAND"

      labels = {
        role = "general"
      }
    }

    spot = {
      desired_size = 2
      min_size     = 0
      max_size     = 20

      instance_types = ["m6i.large", "m5.large", "m5a.large"]
      capacity_type  = "SPOT"

      labels = {
        role = "spot-workers"
      }

      taints = [{
        key    = "spot"
        value  = "true"
        effect = "NO_SCHEDULE"
      }]
    }
  }

  # Cluster addons
  cluster_addons = {
    coredns = {
      most_recent = true
    }
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent = true
    }
  }
}

# Karpenter for advanced autoscaling (optional)
module "karpenter" {
  source  = "terraform-aws-modules/eks/aws//modules/karpenter"
  version = "~> 19.0"

  cluster_name = module.eks.cluster_name

  irsa_oidc_provider_arn = module.eks.oidc_provider_arn
}

Key Takeaways

  • Right-size EC2 - Use Compute Optimizer recommendations; Graviton (ARM) saves 20-40%
  • Lambda for event-driven - Ideal for APIs, data processing, scheduled tasks <15 min
  • ECS for simplicity - AWS-native, easier than K8s for most use cases
  • EKS for Kubernetes - When you need K8s ecosystem or multi-cloud portability
  • Fargate for serverless containers - No node management, pay per task
  • Use Spot instances - Up to 90% savings for fault-tolerant workloads

Conclusion

AWS compute services provide options for every workload type. EC2 offers maximum control, Lambda eliminates all infrastructure management, and container services (ECS/EKS) provide the middle ground. In Part 3, we'll explore AWS storage and database services including S3, RDS, DynamoDB, and more.

References


Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.