in src/autoscaler.ts [76:348]
constructor(scope: cdk.Construct, id: string, props: FargateFastAutoscalerProps) {
super(scope, id);
this.vpc = props.vpc;
this.region = cdk.Stack.of(this).region;
// create a security group that allows all traffic from the same sg
const sg = new ec2.SecurityGroup(this, 'SharedSecurityGroup', {
vpc: this.vpc,
});
sg.connections.allowFrom(sg, ec2.Port.allTraffic());
//sg for HTTP public access
const httpPublicSecurityGroup = new ec2.SecurityGroup(this, 'HttpPublicSecurityGroup', {
allowAllOutbound: true,
securityGroupName: 'HttpPublicSecurityGroup',
vpc: this.vpc,
});
httpPublicSecurityGroup.connections.allowFromAnyIpv4(ec2.Port.tcp(80));
// // Fargate Cluster
const fgCluster = new ecs.Cluster(this, 'fgCluster', {
vpc: this.vpc,
});
// // task iam role
const taskIAMRole = new iam.Role(this, 'fgDemoTaskExecutionRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
taskIAMRole.addToPolicy(new iam.PolicyStatement({
resources: ['*'],
actions: ['xray:PutTraceSegments'],
}));
// ECS task definition
const demoTaskDef = new ecs.FargateTaskDefinition(this, 'cdk-fargate-demo-taskdef', {
cpu: 256,
memoryLimitMiB: 512,
taskRole: taskIAMRole,
});
this.fargateTaskDef = demoTaskDef;
const mainContainer = demoTaskDef.addContainer('main', {
image: ecs.ContainerImage.fromAsset(path.join(__dirname, '../nginx')),
cpu: 0,
logging: new ecs.AwsLogDriver({
streamPrefix: 'echo-http-req',
}),
});
const backendContainer = demoTaskDef.addContainer('backend', props.backendContainer);
// const phpContainer = demoTaskDef.addContainer('backend', {
// image: ecs.ContainerImage.fromAsset('./php', {}),
// cpu: 0,
// logging: new ecs.AwsLogDriver({
// streamPrefix: 'echo-http-req'
// })
// })
// mainContainer.addLink(phpContainer, 'app')
mainContainer.addPortMappings({
containerPort: 80,
});
backendContainer.addPortMappings(...props.backendContainerPortMapping);
// phpContainer.addPortMappings({
// containerPort: 2015
// })
const demoService = new ecs.FargateService(this, 'demo-service', {
cluster: fgCluster,
desiredCount: props.initialTaskNumber ?? 2,
taskDefinition: demoTaskDef,
securityGroup: sg,
});
this.fargateService = demoService;
const externalLB = new elbv2.ApplicationLoadBalancer(this, 'external', {
vpc: this.vpc,
internetFacing: true,
securityGroup: httpPublicSecurityGroup,
});
const externalListener = externalLB.addListener('PublicListener', {
port: 80,
});
const healthCheckDefault = {
port: 'traffic-port',
path: '/',
intervalSecs: 30,
timeoutSeconds: 5,
healthyThresholdCount: 5,
unhealthyThresholdCount: 2,
healthyHttpCodes: '200,301,302',
};
externalListener.addTargets('fg-echo-req', {
port: 80,
protocol: elbv2.ApplicationProtocol.HTTP,
healthCheck: healthCheckDefault,
targets: [demoService],
deregistrationDelay: cdk.Duration.seconds(3),
});
const lambdaRole = new iam.Role(this, 'lambdaRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
lambdaRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonECS_FullAccess'));
lambdaRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ReadOnlyAccess'));
lambdaRole.addToPolicy(new iam.PolicyStatement({
resources: ['*'],
actions: [
'ec2:CreateNetworkInterface',
'ec2:DescribeNetworkInterfaces',
'ec2:DeleteNetworkInterface',
],
}));
lambdaRole.addToPolicy(new iam.PolicyStatement({
resources: ['*'],
actions: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents',
],
}));
// const uniqueId = crypto.createHash('md5').update(this.node.path).digest("hex");
cdk.Stack.of(this).templateOptions.transforms = ['AWS::Serverless-2016-10-31']; // required for AWS::Serverless
const resource = new cdk.CfnResource(this, 'Resource', {
type: 'AWS::Serverless::Application',
properties: {
Location: {
ApplicationId: props.awsCliLayerArn ?? AWSCLI_LAYER_ARN,
SemanticVersion: props.awsCliLayerVersion ?? AWSCLI_LAYER_VERSION,
},
Parameters: {},
},
});
this.layerVersionArn = cdk.Token.asString(resource.getAtt('Outputs.LayerVersionArn'));
const fargateWatcherFunc = new lambda.Function(this, 'fargateWatcherFunc', {
runtime: lambda.Runtime.PROVIDED,
handler: 'main',
code: lambda.Code.fromAsset(path.join(__dirname, '../sam/fargateWatcherFunc/func.d')),
layers: [lambda.LayerVersion.fromLayerVersionArn(this, 'AwsCliLayer', this.layerVersionArn)],
memorySize: 1024,
timeout: cdk.Duration.minutes(1),
role: lambdaRole,
vpc: this.vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE },
securityGroup: sg,
environment: {
cluster: fgCluster.clusterName,
service: demoService.serviceName,
disable_scalein: props.disableScaleIn === false ? 'no' : 'yes',
region: this.region,
},
});
this.fargateWatcherFuncArn = fargateWatcherFunc.functionArn;
// step function
const wait3 = new sfn.Wait(this, 'Wait 3 Seconds', {
// time: sfn.WaitTime.secondsPath('$.wait_time')
time: sfn.WaitTime.duration(cdk.Duration.seconds(3)),
});
const wait60 = new sfn.Wait(this, 'Wait 60 Seconds', {
time: sfn.WaitTime.duration(cdk.Duration.seconds(60)),
});
const getEcsTasks = new sfn.Task(this, 'GetECSTasks', {
task: new sfn_tasks.InvokeFunction(lambda.Function.fromFunctionArn(this, 'getEcsTasks', fargateWatcherFunc.functionArn)),
resultPath: '$.status',
});
const topic = props.snsTopic ?? new sns.Topic(this, `${id}-topic`, {
topicName: `${cdk.Stack.of(this).stackName}-${id}`,
});
const snsScaleOut = new sfn.Task(this, 'SNSScaleOut', {
task: new sfn_tasks.PublishToTopic(topic, {
// message: sfn.TaskInput.fromDataAt('$'),
message: sfn.TaskInput.fromObject({
'Input.$': '$',
}),
subject: 'Fargate Start Scaling Out',
}),
resultPath: '$.taskresult',
});
const svcScaleOut = new sfn.Task(this, 'ServiceScaleOut', {
task: new sfn_tasks.InvokeFunction(lambda.Function.fromFunctionArn(this, 'svcScaleOut', fargateWatcherFunc.functionArn)),
});
const isServiceOverloaded = new sfn.Choice(this, 'IsServiceOverloaded', {
inputPath: '$.status',
});
const isDone = new sfn.Pass(this, 'Done');
const desire2 = new sfn.Pass(this, 'Desire2', {
outputPath: '$',
result: sfn.Result.fromObject({ Desired: 2 }),
});
// const desire5 = new sfn.Pass(this, 'Desire5', {
// outputPath: DISCARD,
// result: sfn.Result.fromObject({Desired: 5})
// })
const desire10 = new sfn.Pass(this, 'Desire10', {
outputPath: '$',
result: sfn.Result.fromObject({ Desired: 10 }),
});
const desire15 = new sfn.Pass(this, 'Desire15', {
outputPath: '$',
result: sfn.Result.fromObject({ Desired: 15 }),
});
const desire20 = new sfn.Pass(this, 'Desire20', {
outputPath: '$',
result: sfn.Result.fromObject({ Desired: 20 }),
});
const chain = sfn.Chain
.start(getEcsTasks)
.next(isServiceOverloaded
.when(sfn.Condition.numberGreaterThanEquals('$.avg', 500), desire20
.next(snsScaleOut
.next(svcScaleOut
.next(wait60
.next(getEcsTasks,
)))))
.when(sfn.Condition.numberGreaterThanEquals('$.avg', 300), desire15
.next(snsScaleOut,
))
.when(sfn.Condition.numberGreaterThanEquals('$.avg', 100), desire10
.next(snsScaleOut,
))
.when(sfn.Condition.numberGreaterThanEquals('$.avg', 50), desire2
.next(snsScaleOut,
))
// .when(sfn.Condition.numberLessThanEquals('$.avg', 10), desire2
// .next(snsScaleOut
// ))
.when(sfn.Condition.numberLessThan('$.avg', 0), isDone)
.otherwise(wait3
.next(getEcsTasks),
));
new sfn.StateMachine(this, 'FargateFastAutoscaler', {
definition: chain,
timeout: cdk.Duration.hours(24),
});
new cdk.CfnOutput(this, 'ClusterARN: ', { value: fgCluster.clusterArn });
new cdk.CfnOutput(this, 'URL: ', { value: 'http://' + externalListener.loadBalancer.loadBalancerDnsName });
new cdk.CfnOutput(this, 'FargateWatcherLambdaArn: ', { value: fargateWatcherFunc.functionArn });
}