Last active
November 21, 2024 21:22
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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