マネジメントコンソールからはECSタスク用にApplication Auto Scalingで事前定義された以下のメトリクスでのターゲット追跡スケーリングが設定できる。
- ECSServiceAverageCPUUtilization (Average)
- ECSServiceAverageMemoryUtilization (Average)
- ALBRequestCountPerTarget (Sum)
これ以外の任意のメトリクスの場合はCLIでの設定が必要となる。やり方を確認する。
参考リンク
- AWS SQS ベースで ECS タスクをオートスケーリングする
- Scaling based on Amazon SQS
- こちらはApplication Auto ScalingではなくEC2 Auto Scalingだがカスタムメトリクスのput-metric-dataのやり方を参考にした
準備
まずFargateタスクを動かす。
VPCの作成
テンプレートを用意する。
AWSTemplateFormatVersion: "2010-09-09" Parameters: VPCCIDR: Type: String Default: 10.1.0.0/16 PublicSubnet1CIDR: Type: String Default: 10.1.1.0/24 PublicSubnet2CIDR: Type: String Default: 10.1.2.0/24 PrivateSubnet1CIDR: Type: String Default: 10.1.3.0/24 PrivateSubnet2CIDR: Type: String Default: 10.1.4.0/24 Resources: ########## # VPC ########## VPC: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VPCCIDR EnableDnsHostnames: true EnableDnsSupport: true InstanceTenancy: default Tags: - Key: Name Value: !Sub ${AWS::StackName}-VPC InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Sub ${AWS::StackName}-IGW VPCGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway ########## # PublicSubnet1 ########## PublicSubnet1: Type: AWS::EC2::Subnet Properties: CidrBlock: !Ref PublicSubnet1CIDR VpcId: !Ref VPC AvailabilityZone: !Select [0, !GetAZs ""] MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub ${AWS::StackName}-PublicSubnet1 PublicSubnet1RouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${AWS::StackName}-PublicSubnet1 PublicSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicSubnet1RouteTable SubnetId: !Ref PublicSubnet1 PublicSubnet1DefaultRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PublicSubnet1RouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway DependsOn: - InternetGateway NatGateway1EIP: Type: AWS::EC2::EIP Properties: Domain: vpc NatGateway1: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGateway1EIP.AllocationId SubnetId: !Ref PublicSubnet1 Tags: - Key: Name Value: !Sub ${AWS::StackName}-PublicSubnet1 ########## # PublicSubnet2 ########## PublicSubnet2: Type: AWS::EC2::Subnet Properties: CidrBlock: !Ref PublicSubnet2CIDR VpcId: !Ref VPC AvailabilityZone: !Select [1, !GetAZs ""] MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub ${AWS::StackName}-PublicSubnet2 PublicSubnet2RouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${AWS::StackName}-PublicSubnet2 PublicSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicSubnet1RouteTable SubnetId: !Ref PublicSubnet2 PublicSubnet2DefaultRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PublicSubnet2RouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway DependsOn: - InternetGateway NatGateway2EIP: Type: AWS::EC2::EIP Properties: Domain: vpc NatGateway2: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGateway2EIP.AllocationId SubnetId: !Ref PublicSubnet2 Tags: - Key: Name Value: !Sub ${AWS::StackName}-PublicSubnet2 ########## # PrivateSubnet1 ########## PrivateSubnet1: Type: AWS::EC2::Subnet Properties: CidrBlock: !Ref PrivateSubnet1CIDR VpcId: !Ref VPC AvailabilityZone: !Select [0, !GetAZs ""] MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Sub ${AWS::StackName}-PrivateSubnet1 PrivateSubnet1RouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${AWS::StackName}-PrivateSubnet1 PrivateSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateSubnet1RouteTable SubnetId: !Ref PrivateSubnet1 PrivateSubnet1DefaultRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateSubnet1RouteTable DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway1 ########## # PrivateSubnet2 ########## PrivateSubnet2: Type: AWS::EC2::Subnet Properties: CidrBlock: !Ref PrivateSubnet2CIDR VpcId: !Ref VPC AvailabilityZone: !Select [1, !GetAZs ""] MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Sub ${AWS::StackName}-PrivateSubnet2 PrivateSubnet2RouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${AWS::StackName}-PrivateSubnet2 PrivateSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateSubnet2RouteTable SubnetId: !Ref PrivateSubnet2 PrivateSubnet2DefaultRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateSubnet2RouteTable DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway2 Outputs: VPC: Value: !Ref VPC Export: Name: !Sub ${AWS::StackName}-VPC PublicSubnet1: Value: !Ref PublicSubnet1 Export: Name: !Sub ${AWS::StackName}-PublicSubnet1 PublicSubnet2: Value: !Ref PublicSubnet2 Export: Name: !Sub ${AWS::StackName}-PublicSubnet2 PrivateSubnet1: Value: !Ref PrivateSubnet1 Export: Name: !Sub ${AWS::StackName}-PrivateSubnet1 PrivateSubnet2: Value: !Ref PrivateSubnet2 Export: Name: !Sub ${AWS::StackName}-PrivateSubnet2
パラメーターファイルを作成する。
[ { "ParameterKey": "VPCCIDR", "ParameterValue": "10.1.0.0/16" }, { "ParameterKey": "PublicSubnet1CIDR", "ParameterValue": "10.1.1.0/24" }, { "ParameterKey": "PublicSubnet2CIDR", "ParameterValue": "10.1.2.0/24" }, { "ParameterKey": "PrivateSubnet1CIDR", "ParameterValue": "10.1.3.0/24" }, { "ParameterKey": "PrivateSubnet2CIDR", "ParameterValue": "10.1.4.0/24" } ]
スタックを作成する。
aws cloudformation create-stack \ --stack-name MyVPCStack \ --template-body file://vpc.yaml \ --parameters file://vpc.parameter.json
スタック作成の完了を待つ。
aws cloudformation wait stack-create-complete \ --stack-name MyVPCStack
出力を変数に入れておく。
VpcId=$(aws cloudformation describe-stacks --stack-name MyVPCStack | jq -r '.Stacks[].Outputs[] | select( .OutputKey | test("VPC") ) | .OutputValue') PrivateSubnet1Id=$(aws cloudformation describe-stacks --stack-name MyVPCStack | jq -r '.Stacks[].Outputs[] | select( .OutputKey | test("PrivateSubnet1") ) | .OutputValue') PrivateSubnet2Id=$(aws cloudformation describe-stacks --stack-name MyVPCStack | jq -r '.Stacks[].Outputs[] | select( .OutputKey | test("PrivateSubnet2") ) | .OutputValue') PublicSubnet1Id=$(aws cloudformation describe-stacks --stack-name MyVPCStack | jq -r '.Stacks[].Outputs[] | select( .OutputKey | test("PublicSubnet1") ) | .OutputValue') PublicSubnet2Id=$(aws cloudformation describe-stacks --stack-name MyVPCStack | jq -r '.Stacks[].Outputs[] | select( .OutputKey | test("PublicSubnet2") ) | .OutputValue')
セキュリティグループの作成
VPCのデフォルトのセキュリティグループを取得する。
DefaultSecurityGroupId=$(aws ec2 describe-security-groups | jq -r '.SecurityGroups[] | select( ( .VpcId | test("'${VpcId}'") ) and ( .GroupName | test("default") ) ) | .GroupId'); echo ${DefaultSecurityGroupId}
ALB用のセキュリティグループを作成する。
aws ec2 create-security-group --group-name MyALBSecurityGroup --description MyALBSecurityGroup --vpc-id ${VpcId} ALBSecurityGroupId=$(aws ec2 describe-security-groups | jq -r '.SecurityGroups[] | select( .GroupName | test("MyALBSecurityGroup") ) | .GroupId'); echo ${ALBSecurityGroupId}
80番ポートへのアクセスを許可する。
aws ec2 authorize-security-group-ingress --group-id ${ALBSecurityGroupId} --protocol tcp --port 80 --cidr 0.0.0.0/0
ALBの作成
ALBを作成する。
aws elbv2 create-load-balancer --name MyALB \ --subnets ${PublicSubnet1Id} ${PublicSubnet2Id} --security-groups ${ALBSecurityGroupId} ${DefaultSecurityGroupId} LoadBalancerArn=$(aws elbv2 describe-load-balancers --name MyALB | jq -r '.LoadBalancers[].LoadBalancerArn'); echo ${LoadBalancerArn} DNSName=$(aws elbv2 describe-load-balancers --name MyALB | jq -r '.LoadBalancers[].DNSName'); echo ${DNSName}
ターゲットグループを作成する。
aws elbv2 create-target-group --name MyTargetGroup --protocol HTTP --port 80 \ --target-type ip --vpc-id ${VpcId} TargetGroupArn=$(aws elbv2 describe-target-groups --names MyTargetGroup | jq -r '.TargetGroups[].TargetGroupArn'); echo ${TargetGroupArn}
ターゲットグループにリクエストを転送するデフォルトルールを持つリスナーを作成する。
aws elbv2 create-listener --load-balancer-arn ${LoadBalancerArn} \ --protocol HTTP --port 80 \ --default-actions Type=forward,TargetGroupArn=${TargetGroupArn}
ECSクラスターの作成
Fargateクラスターを作成する。
aws ecs create-cluster --cluster-name fargate-cluster --settings "name=containerInsights,value=enabled"
タスク定義の登録
タスク定義のjsonを作成する。
cat <<EOF > task-definition.json { "family": "echoserver", "taskRoleArn": "", "executionRoleArn": "ecsTaskExecutionRole", "networkMode": "awsvpc", "containerDefinitions": [ { "name": "echoserver", "image": "k8s.gcr.io/echoserver:1.4", "portMappings": [ { "containerPort": 8080, "protocol": "tcp" } ], "essential": true, "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/echoserver", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "ecs" } } } ], "requiresCompatibilities": ["FARGATE"], "cpu": "256", "memory": "512" } EOF
タスク定義を登録する。
aws ecs register-task-definition --cli-input-json file://task-definition.json
タスクが使用するロググループを作成する。
aws logs create-log-group --log-group-name "/ecs/echoserver"
サービスの作成
サービスのjsonを作成する。
cat <<EOF > service.json { "cluster": "fargate-cluster", "serviceName": "echoserver", "taskDefinition": "echoserver", "loadBalancers": [ { "targetGroupArn": "${TargetGroupArn}", "containerName": "echoserver", "containerPort": 8080 } ], "desiredCount": 1, "launchType": "FARGATE", "networkConfiguration": { "awsvpcConfiguration": { "subnets": ["${PrivateSubnet1Id}", "${PrivateSubnet2Id}"], "securityGroups": ["${DefaultSecurityGroupId}"], "assignPublicIp": "DISABLED" } } } EOF
サービスを作成する。
aws ecs create-service --cli-input-json file://service.json
稼働確認
アクセスしてみる。
$ curl -s http://${DNSName}/ CLIENT VALUES: client_address=10.1.1.95 command=GET real path=/ query=nil request_version=1.1 request_uri=http://myalb-211320250.ap-northeast-1.elb.amazonaws.com:8080/ SERVER VALUES: server_version=nginx: 1.10.0 - lua: 10001 HEADERS RECEIVED: accept=*/* host=myalb-211320250.ap-northeast-1.elb.amazonaws.com user-agent=curl/7.54.0 x-amzn-trace-id=Root=1-5f9c2630-1ad009bd47fca22b24e2d8a3 x-forwarded-for=27.0.3.145 x-forwarded-port=80 x-forwarded-proto=http BODY: -no body in request-
オートスケーリングの設定
スケーラブルターゲットの作成
ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account) cat <<EOF > scalable-target.json { "ServiceNamespace": "ecs", "ResourceId": "service/fargate-cluster/echoserver", "ScalableDimension": "ecs:service:DesiredCount", "MinCapacity": 1, "MaxCapacity": 10, "RoleARN": "arn:aws-cn:iam::${ACCOUNT_ID}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService" } EOF
スケーラブルターゲットを登録する。
aws application-autoscaling register-scalable-target --cli-input-json file://scalable-target.json
スケーリングポリシーの作成
cat <<EOF > scaling-policy.json { "TargetValue": 10, "CustomizedMetricSpecification": { "MetricName": "MyMetricName", "Namespace": "MyNamespace", "Dimensions": [ { "Name": "MyMetricDimensionName", "Value": "MyMetricDimensionValue" } ], "Statistic": "Average" }, "ScaleOutCooldown": 60, "ScaleInCooldown": 60, "DisableScaleIn": false } EOF
スケーリングポリシーを作成する。
aws application-autoscaling put-scaling-policy \ --policy-name MyMetricTargetTrackingPolicy \ --service-namespace ecs \ --resource-id service/fargate-cluster/echoserver \ --scalable-dimension ecs:service:DesiredCount \ --policy-type TargetTrackingScaling \ --target-tracking-scaling-policy-configuration file://scaling-policy.json
マネジメントコンソールで設定を確認する。詳細は表示されないようだ。
以下のようなアラームが自動的に設定される。
動作確認
カスタムメトリクスの送信
今回は適当なカスタムメトリクスを固定で送信する。
以下のスクリプトを作成。
#!/bin/bash while true do echo "Put custom metrics..." aws cloudwatch put-metric-data --metric-name MyMetricName --namespace MyNamespace \ --unit None --value 20 --dimensions MyMetricDimensionName=MyMetricDimensionValue echo "Sleep..." sleep 10 done
これを実行しておく。
メトリクスは少し遅れて(最大2分?)CloudWatchで見れるようになる。メトリクスは20を固定で出力しており、ターゲットは10なので、1分毎のデータ3つ見れるようになると、アラームが発報してスケールアウトが行われる。タスクを追加してもメトリクスは20のまま変わらないので、タスクの最大数までスケールアウトが行われる。
スケールアウトのクールダウン時間を60秒に設定したので、60秒後に次のスケールがアウトが行われるのかと思ったが、次のタスク追加まではもう少し時間がかかっているようだ。また追加されるのは1つづつとは限らないようで、ロジックは不明だがいい感じにやろうとしてくれるっぽい。