CDKでECS(EC2)上にLocust masterとworkerのServiceをデプロイしCloud Mapで名前解決させる
aws以前、EKS上にLocustをインストールしたが、使わないときにクラスタを落としていたりすると起動を待つ必要があったり、K8sのバージョンアップに追従する必要があったりと少し不便な所があった。
CDKでEKSクラスタの作成からHelm ChartでのLocustのインストールまでを一気に行う - sambaiz-net
そこで今回はECS上にLocust masterとworkerのServiceをデプロイする。
Dockerfile
次のようなDockerfileを用意する。
$ cat Dockerfile
FROM locustio/locust
COPY locustfile.py /mnt/locust/
Stack
クラスタの作成については以前のものを使い回す。
CDKでALBとECS(EC2)クラスタを作成し、ecs-cliでDocker Composeの構成をデプロイする - sambaiz-net
locustfileを更新する度にデプロイすることになるので、ALBのヘルスチェックの間隔と回数および登録解除のタイムアウト時間を減らして高速化している。また、locustに認証の仕組みがないためIPアドレスでアクセス制限をしているが、ApplicationListenerのopenがデフォルトのtrueのままだとAllow from anyone
の設定が追加されフルオープンになってしまうのでfalseにしている。
import * as cdk from '@aws-cdk/core'
import * as ecs from '@aws-cdk/aws-ecs'
import * as ec2 from '@aws-cdk/aws-ec2'
import * as iam from '@aws-cdk/aws-iam'
import * as autoscaling from '@aws-cdk/aws-autoscaling'
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'
import * as logs from '@aws-cdk/aws-logs'
export class LocustECSStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const cluster = this.createECSCluster(2)
const logGroup = this.createLogGroup()
const locustMaster = this.createLocustMasterService(cluster, logGroup)
this.createALB(cluster.vpc, locustMaster)
this.createLocustWorkerService(cluster, logGroup, 1).node.addDependency(locustMaster)
}
createECSCluster(capacity: number) {
const cluster = new ecs.Cluster(this, `ECSCluster`, {
clusterName: 'locust-ecs',
defaultCloudMapNamespace: {
name: 'locust-ecs.local'
}
})
const securityGroup = new ec2.SecurityGroup(this, `EC2SecurityGroup`, {
vpc: cluster.vpc,
})
securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.allTcp())
const autoScalingGroup = new autoscaling.AutoScalingGroup(this, `AutoScalingGroup`, {
vpc: cluster.vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
minCapacity: capacity,
maxCapacity: capacity,
securityGroup,
})
cluster.addAsgCapacityProvider(new ecs.AsgCapacityProvider(this, `AsgCapacityProvider`, {
autoScalingGroup
}))
autoScalingGroup.role.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'))
return cluster
}
createLogGroup() {
return new logs.LogGroup(this, 'LogGroup', {
logGroupName: 'locust-ecs',
retention: logs.RetentionDays.TWO_WEEKS
})
}
createLocustMasterService(cluster: ecs.ICluster, logGroup: logs.ILogGroup) {
const taskDefinition = new ecs.Ec2TaskDefinition(this, 'LocustMasterTaskDefinition', {
networkMode: ecs.NetworkMode.AWS_VPC,
})
taskDefinition.addContainer('LocustMasterContainer', {
containerName: "locust",
image: ecs.ContainerImage.fromAsset("./lib"),
memoryLimitMiB: 256,
cpu: 128,
command: ["-f", "/mnt/locust/locustfile.py", "--master"],
logging: new ecs.AwsLogDriver({
logGroup,
streamPrefix: "locust-master"
})
}).addPortMappings({
containerPort: 8089,
}, {
containerPort: 5557,
}, {
containerPort: 5558,
})
const securityGroup = new ec2.SecurityGroup(this, `LocustMasterServiceSecurityGroup`, {
vpc: cluster.vpc,
})
securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.allTcp())
return new ecs.Ec2Service(this, 'LocustMasterService', {
serviceName: "locust-master",
cluster,
taskDefinition,
cloudMapOptions: {
name: "master",
},
securityGroups: [securityGroup]
})
}
createLocustWorkerService(cluster: ecs.ICluster, logGroup: logs.ILogGroup, desiredCount: number) {
const taskDefinition = new ecs.Ec2TaskDefinition(this, 'LocustWorkerTaskDefinition')
taskDefinition.addContainer('LocustWorkerContainer', {
containerName: "locust",
image: ecs.ContainerImage.fromAsset("./lib"),
memoryLimitMiB: 256,
cpu: 128,
command: ["-f", "/mnt/locust/locustfile.py", "--worker", "--master-host", "master.locust-ecs.local"],
logging: new ecs.AwsLogDriver({
logGroup,
streamPrefix: "locust-worker"
}),
}).addUlimits({
name: UlimitName.FSIZE,
hardLimit: 65535,
softLimit: 65535
})
return new ecs.Ec2Service(this, 'LocustWorkerService', {
serviceName: "locust-worker",
cluster,
taskDefinition,
desiredCount,
})
}
createALB(vpc: ec2.IVpc, service: ecs.Ec2Service) {
const securityGroup = new ec2.SecurityGroup(this, 'ALBSecurityGroup', {
vpc: vpc,
})
securityGroup.addIngressRule(ec2.Peer.ipv4("xxx.xxx.xxx.xxx/32"), ec2.Port.tcp(80))
const alb = new elbv2.ApplicationLoadBalancer(this, 'ALB', {
loadBalancerName: "locust-ecs",
vpc: vpc,
internetFacing: true,
securityGroup
})
const target = new elbv2.ApplicationTargetGroup(this, 'ALBTargetGroup', {
targetType: elbv2.TargetType.IP,
vpc: vpc,
protocol: elbv2.ApplicationProtocol.HTTP,
port: 80,
targets: [service.loadBalancerTarget({
containerName: 'locust',
containerPort: 8089
})],
healthCheck: {
interval: cdk.Duration.seconds(6),
healthyThresholdCount: 2
},
deregistrationDelay: cdk.Duration.seconds(5)
})
new elbv2.ApplicationListener(this, 'ALBListener', {
loadBalancer: alb,
defaultTargetGroups: [target],
protocol: elbv2.ApplicationProtocol.HTTP,
port: 80,
open: false
})
return alb
}
}
Cloud Map
workerは--master-host
としてLBのDNS名やインスタンスのIPアドレスではなくmaster.locust-ecs.local
を渡している。
これはCloud MapによってRoute53に登録される動的なレコードで、
Taskのnetwork modeがawsvpcの場合、
Aレコードとして、EC2インスタンスに追加で紐づけられるTask専用のENIのIPアドレスが返される。
Taskが動いていてzmq.error.Again: Resource temporarily unavailable
などのエラーでWorkerが登録されない場合は、ENIのSGで弾かれていないか確認する。