Skip to content

Instantly share code, notes, and snippets.

@filipeandre
Last active November 21, 2024 21:22
Show Gist options
  • Save filipeandre/544934d8273a2197139d834f806b7a1d to your computer and use it in GitHub Desktop.
Save filipeandre/544934d8273a2197139d834f806b7a1d to your computer and use it in GitHub Desktop.
Testing managed ec2 auto scaling based on ecs service desired count using CDK
import {DockerImageAsset} from 'aws-cdk-lib/aws-ecr-assets';
import {FoundationApp, FoundationStack, FoundationStackProps} from "@devops/cdk";
import * as cdk from 'aws-cdk-lib';
import * as autoscaling from 'aws-cdk-lib/aws-autoscaling';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
export class AutoScalingTestStack extends FoundationStack {
constructor(scope: FoundationApp, id: string, props?: FoundationStackProps) {
super(scope, id, props);
// -----------------------------------------------------------------
// VPC
// -----------------------------------------------------------------
const vpc = new ec2.Vpc(this, 'Vpc', {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{ name: 'public', subnetType: ec2.SubnetType.PUBLIC },
{ name: 'private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }
],
});
vpc.addGatewayEndpoint('S3GatewayVpcEndpoint', {
service: ec2.GatewayVpcEndpointAwsService.S3
});
vpc.addInterfaceEndpoint('EcrDockerVpcEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER
});
vpc.addInterfaceEndpoint('EcrVpcEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.ECR
});
vpc.addInterfaceEndpoint('CloudWatchLogsVpcEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS
});
// -----------------------------------------------------------------
// ECS Cluster
// -----------------------------------------------------------------
const cluster = new ecs.Cluster(this, 'Cluster', { vpc, enableFargateCapacityProviders: false });
const ecsSG = new ec2.SecurityGroup(this, 'SecurityGroupEcsEc2', {
vpc: vpc,
allowAllOutbound: true,
});
// Security group for the load balancer
const loadBalancerSG = new ec2.SecurityGroup(this, 'LoadBalancerSG', {
vpc: vpc,
allowAllOutbound: true,
});
// Security group rule to allow traffic from the load balancer to ECS instances on port 80
ecsSG.addIngressRule(
loadBalancerSG,
cdk.aws_ec2.Port.tcp(80),
'Allow HTTP traffic from load balancer only'
);
// -----------------------------------------------------------------
// Ec2 Template
// -----------------------------------------------------------------
// Role for the EC2 instances
const instanceRole = new iam.Role(this, 'InstanceRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly'),
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')
],
});
const userData = ec2.UserData.forLinux();
userData.addCommands(
`#!/bin/bash`,
`cat <<'EOF' >> /etc/ecs/ecs.config`,
`ECS_CLUSTER=${cluster.clusterName}`,
`ECS_WARM_POOLS_CHECK=true`,
`EOF`
);
// Launch Template with Hibernation enabled
const launchTemplate = new ec2.LaunchTemplate(this, 'LaunchTemplate', {
instanceType: new ec2.InstanceType('t4g.micro'),
machineImage: ecs.EcsOptimizedImage.amazonLinux2023(ecs.AmiHardwareType.ARM),
hibernationConfigured: true, // Enables hibernation support in the ASG
role: instanceRole,
blockDevices: [ // ebs needed for ec2 hibernate
{
deviceName: '/dev/xvda',
volume: ec2.BlockDeviceVolume.ebs(100, {
encrypted: true,
volumeType: ec2.EbsDeviceVolumeType.GP3,
}),
},
],
securityGroup: ecsSG,
userData
});
// disable as it won't allow me to destroy infra
// autoScalingGroup.protectNewInstancesFromScaleIn();
const targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {
vpc,
port: 80,
protocol: elbv2.ApplicationProtocol.HTTP,
targetType: elbv2.TargetType.INSTANCE,
healthCheck: {
path: "/",
protocol: elbv2.Protocol.HTTP,
port: "80"
}
});
// --------------------------------
// ECS TASK
// --------------------------------
// Docker image for the Python service
const pythonImage = new DockerImageAsset(this, 'PythonAppDockerImage', {
directory: './api'
});
// Execution role for ECS tasks
const executionRole = new iam.Role(this, 'ECSExecutionRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')
]
});
// Ecs task role
const taskRole = new iam.Role(this, 'ECSTaskRole2', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com')
});
taskRole.addToPolicy(new iam.PolicyStatement({
actions: [
'ecs:DescribeContainerInstances',
'ecs:ListContainerInstances'
],
resources: ['*'], // lambda permissions
}));
// ECS Task Definition
const taskDefinition = new ecs.Ec2TaskDefinition(this, 'TaskDef', {
executionRole,
taskRole,
});
taskDefinition.addContainer('PythonAppContainer', {
image: ecs.ContainerImage.fromDockerImageAsset(pythonImage),
memoryLimitMiB: 0.9*1000, // 90% of 1000 => t4g micro
cpu: 0.9*2000, // 90% of 4 vcpu => t4g micro
portMappings: [{
hostPort: 80,
containerPort: 80
}]
});
// ------------------------
// EC2 Auto Scaling Group
// ---------------------------
const ec2AutoScalingGroup = new autoscaling.AutoScalingGroup(this, 'ASG', {
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
updatePolicy: autoscaling.UpdatePolicy.rollingUpdate(), // should be a rolling update to avoid service disruption
launchTemplate,
minCapacity: 0,
maxCapacity: 4
});
ec2AutoScalingGroup.node.addDependency(cluster)
// -------------------------------------------------------------------
// Managed scaling and draining capacity for ecs cluster ec2 instances
// -------------------------------------------------------------------
const capacityProvider = new ecs.AsgCapacityProvider(this, 'AsgCapacityProvider', {
autoScalingGroup: ec2AutoScalingGroup,
enableManagedScaling: true, // your cluster will automatically scale instances based on the load your tasks put on the cluster.
enableManagedDraining: true, // your service workloads stop safely and are rescheduled to non-terminating instances
enableManagedTerminationProtection: false, // https://github.com/aws/aws-cdk/issues/14732#issuecomment-855054581
});
cluster.addAsgCapacityProvider(capacityProvider); // https://github.com/aws/aws-cdk/issues/19275
// --------------------------------
// Application Load Balancer
// --------------------------------
const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LoadBalancer', {
vpc,
internetFacing: true,
securityGroup: loadBalancerSG
});
loadBalancer.addListener('Listener', {
port: 80,
defaultAction: elbv2.ListenerAction.forward([targetGroup]),
});
// --------------------------------
// Warm Pool
// --------------------------------
ec2AutoScalingGroup.addWarmPool({
minSize: 1,
poolState: autoscaling.PoolState.HIBERNATED,
reuseOnScaleIn: false, // should not reuse on scaleIn for ECS
})
ec2AutoScalingGroup.attachToApplicationTargetGroup(targetGroup)
// --------------------------------
// ECS Service
// --------------------------------
const service = new ecs.Ec2Service(this, 'Service', {
cluster,
taskDefinition,
capacityProviderStrategies: [
{
capacityProvider: capacityProvider.capacityProviderName,
weight: 1,
},
],
circuitBreaker: {
enable: true,
rollback: true
},
});
service.node.addDependency(capacityProvider)
// -----------------------
// Service Auto Scaling
// -----------------------
const serviceAutoScaling = service.autoScaleTaskCount({
minCapacity: 2,
maxCapacity: 4,
});
// -------------------------------
// Service Auto Scaling
// scalingSteps + EXACT_CAPACITY
// -------------------------------
// Define CPU step scaling policy
serviceAutoScaling.scaleOnMetric('CpuStepScaling', {
metric: service.metricCpuUtilization(),
scalingSteps: [
{ upper: 10, change: 2 }, // Scale to 2 instances if CPU < 10%
{ lower: 30, upper: 60, change: 3 }, // Scale to 3 instances if CPU between 30% and 60%
{ lower: 60, change: 4 }, // Scale to 4 instances if CPU > 60%
],
adjustmentType: autoscaling.AdjustmentType.EXACT_CAPACITY,
cooldown: cdk.Duration.seconds(120),
});
// Define Memory step scaling policy
serviceAutoScaling.scaleOnMetric('MemoryStepScaling', {
metric: service.metricMemoryUtilization(),
scalingSteps: [
{ upper: 20, change: 2 }, // Scale to 2 instances if Memory < 20%
{ lower: 40, upper: 60, change: 3 }, // Scale to 3 instances if Memory between 40% and 60%
{ lower: 60, change: 4 }, // Scale to 4 instances if Memory > 60%
],
adjustmentType: autoscaling.AdjustmentType.EXACT_CAPACITY,
cooldown: cdk.Duration.seconds(120),
});
// Define Request count step scaling policy
serviceAutoScaling.scaleOnMetric('RequestStepScaling', {
metric: targetGroup.metric('RequestCountPerTarget', {
statistic: 'Sum',
period: cdk.Duration.minutes(1),
}),
scalingSteps: [
{ upper: 25, change: 2 }, // Scale to 2 instances if requests/target < 25
{ lower: 50, upper: 100, change: 3 }, // Scale to 3 instances if requests/target between 50 and 100
{ lower: 100, change: 4 }, // Scale to 4 instances if requests/target > 100
],
adjustmentType: autoscaling.AdjustmentType.EXACT_CAPACITY,
cooldown: cdk.Duration.seconds(120),
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment