EKSにおけるAutoScalingパターン

EKSにおけるAutoScalingパターン

December 3, 2019
Kubernetes
kubernetes, autoscaling, EKS

Amazon EKS Advent Calendar 2019の3日目です

最近EKSにおけるAutoScalingについて仕事でいろいろ試して、知見がだいぶたまったのでそれをまとめてみようと思います。 (といっても商用環境でこれを適用したというわけでなく、検証した程度なので商用環境で耐えれる内容ではないかもしれません..)

k8sのAutoScalingについて

最初にk8sにおけるAutoScalingについて触れておこうと思います。 k8sの世界でスケールさせる必要があるリソースはなんでしょうか?

PodとNodeですね。 Podはk8sの世界に置いてデプロイできるアトミックなリソースですし PodはNodeの上に配置されるため、Pod数が増加してきたときに配置に耐えれるようにスケールできる必要があります。

これらのスケール方法ですが Podであれば、単純に新しいPodを配置したりDeployment,ReplicaSetリソースのreplica数を変更してあげればスケールできます。 Nodeであれば、新しいインスタンスを立ち上げてクラスタにジョインさせてあげればいいですね。 ただこれだと、 単なるスケールでありオートスケーリングとは呼べないので、次にk8sが提供しているオートスケーリングのための機能を紹介します。

Podのオートスケーリング手法

Podのオートスケーリングのためにk8sは2つの機能を提供しています。

HPA(Horizontal Pod AutoScaler)

https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/

水平スケーリングです。Podの数を増やすスケーリングになります。 Deploymentリソースなどと組み合わせることで、Podの負荷に応じて動的にreplica数を増減してくれます。

VPA(Vertical Pod AutoScalier)

垂直オートスケーリングです。 podに割り当てるcpuやmemoryを負荷に合わせて動的にスケールしてくれます。 2019/12時点でまだbetaであり、また自分自身ほぼ使ったことがないので(昔軽く動作確認したくらい)、今回のパターンには載っていません。

Nodeのオートスケーリング手法

Cluster AutoScalerというのがk8sから提供されています。 全てのpodが動作できるように自動的にノード台数を調整してくれます。(リソース不足でpodがPendingになる場合は増やして、余分なノードが立っているときは減らしてくれます) EC2 AtuoScalingに対してAPIを叩いて、調整してくれるような仕組みになっています。 AWSの他に、GCP, Azureでも動作します。


k8sのオートスケーリング基礎知識としてはこのあたりになります。 以降は実際に自分が試してみた手法を3つ紹介していきます

  • Podの負荷量に合わせてスケーリング(HPA+MetricsServer+ClusterAutoScaler)
  • 外部メトリクスを利用してのスケーリング(HPA+CloudWatch+ClusterAutoScaler)
  • 閉域でのスケーリング(HPA+CloudWatch Alarm)

Podの負荷量に合わせてスケーリング(HPA+MetricsServer+ClusterAutoScaler)

おそらく一番一般的なオートスケーリングになるのではないかと思います。 スケール対象となるDeployment/PodのCPUやMemory利用率を監視して、閾値を超えたらPod数を増やしてあげます。 さらにNodeにPodが配置できなくなったらClusterAutoScalerでNodeを増やしてあげます。 EKS-architecture.001.png

実際に動かして、動作を確認してみます。

EKSクラスタの構築

検証なので、お手軽にeksctlを使って構築します。 下記のような構成になります。

EKS-architecture.004.png

eksctlで、インターネットリーチャブルなEKSクラスタを立ち上げます。 立ち上げ後のkubectl操作はローカルPCから実行します。(以降のコマンドは全てローカルPC上で実行となります。

eksctlでクラスタ構築

macであればbrewでインストールできます https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/getting-started-eksctl.html

$ brew tap weaveworks/tap
$ brew install weaveworks/tap/eksctl
$ eksctl version
[]  version.Info{BuiltAt:"", GitCommit:"", GitTag:"0.10.2"}

awsの設定をした後に

$ aws configure

以下のコマンドでクラスタを構築できます

$ eksctl create cluster --name <cluster-name> \
  --version 1.14 \
  --nodegroup-name <worker-group-name> \
  --node-type t3.small \
  --nodes 1 \
  --nodes-min 1 \
  --nodes-max 5 \
  --managed \
  --asg-access

--managedをつけると、先月発表されたEKS Managed Worker Groupが使われます --asg-accessをつけるとClusterAutoScaler用にWorkerのIAMにautoscalingへのアクセス権限が付与されます。

大体10分ほど待つと構築されます。 完了したら確認します。

$ aws eks update-kubeconfig --name <clustername>
$ kubectl get no
NAME                                               STATUS   ROLES    AGE   VERSION
ip-192-168-4-177.ap-northeast-1.compute.internal   Ready    <none>   77s   v1.14.7-eks-1861c5
ip-192-168-63-13.ap-northeast-1.compute.internal   Ready    <none>   74s   v1.14.7-eks-1861c5

Cluster AutoScalerの準備

https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/README.md

こちらのマニフェストを落として、以下を変更した後にapplyします

          command:
            - ./cluster-autoscaler
            - --v=4
            - --stderrthreshold=info
            - --cloud-provider=aws
            - --skip-nodes-with-local-storage=false
-            - --nodes=1:10:k8s-worker-asg-1
+            - --nodes=<minsize>:<maxsize>:<autoscaling group name>

minsizeはautoscalingの最小数、maxsizeはautoscalingの最大数を指定します。 <autoscaling group name>には、以下のコマンドで対象のasgのAutoScalingGroupNameを確認して指定してください

$ aws autoscaling describe-auto-scaling-groups

ログを確認して、特にエラーが出ていなければおkです。

$ kubectl get po -n kube-system | grep auto
cluster-autoscaler-7848897864-9jwn2   1/1     Running   0          3m5s

$ kubectl -n kube-system logs cluster-autoscaler-7848897864-9jwn2

HPAの設定

公式の手順にそって進めていきます。

まず、PodのCPU利用率などを回収するためのmetric-serverを作成します。

$ git clone https://github.com/kubernetes-sigs/metrics-server.git
$ kubectl apply -f metrics-server/deploy/1.8+/

次にapacheのdeploymentとserviceを作成します

apiVersion: apps/v1 
kind: Deployment
metadata:
  name: php-apache
spec:
  selector:
    matchLabels:
      app: php-apache
  replicas: 1 
  template:
    metadata:
      labels:
        app: php-apache
    spec:
      containers:
      - name: php-apache
        image: k8s.gcr.io/hpa-example
        resources:
          requests:
            cpu: 200m
          limits:
            cpu: 500m
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: php-apache
spec:
  selector:
    app: php-apache
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

metric serverを使ったhpaの場合は、resourcesを指定しないと動作しないのでここだけは注意しましょう 次にこのdeploymentに対してhpaを作成します

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache-hpa
spec:
  maxReplicas: 50
  minReplicas: 1
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  targetCPUUtilizationPercentage: 50

CPU利用率が50%になるように、1~50の間でpod数を増減してくれます。

この2つをdeployしたら確認します

$ kubectl get hpa
NAME             REFERENCE               TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
php-apache-hpa   Deployment/php-apache   0%/50%    1         50        1          2m11s

これで準備が完了しました。

負荷をかける

apacheに負荷をかけていきます。以下のload-generator podをいくつかたてます。

$ kubectl run --generator=run-pod/v1 -it --rm load-generator --image=busybox /bin/sh
// プロンプトが表示されたら以下のコマンドを叩く
while true; do wget -q -O- http://php-apache.default.svc.cluster.local; done

結果

$ kubectl get hpa -w                                 365ms  Mon Dec  2 17:27:18 2019
NAME             REFERENCE               TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
php-apache-hpa   Deployment/php-apache   0%/50%    1         50        1          9m24s
php-apache-hpa   Deployment/php-apache   249%/50%   1         50        1          10m
php-apache-hpa   Deployment/php-apache   249%/50%   1         50        4          10m
php-apache-hpa   Deployment/php-apache   249%/50%   1         50        5          10m
php-apache-hpa   Deployment/php-apache   62%/50%    1         50        5          11m
php-apache-hpa   Deployment/php-apache   71%/50%    1         50        5          12m

負荷が高まってPod数が増えていってます。 さらに以下でpod数をcloudwatchに集めて、増減を確認しました。

#!/bin/bash
while true
do
  POD_NUM=$(kubectl get deployment php-apache -ojson | jq .status.availableReplicas)
  aws cloudwatch put-metric-data --metric-name sqs-app-pop-pod-nums --namespace "eks-sample" --value $POD_NUM 
  sleep 5
done

image.png

負荷をかけるとPod数とNode数が増加して, 負荷を止めるとPod数とNode数が縮小することが確認できました。

外部メトリクスを利用してのスケーリング(HPA+CloudWatch+ClusterAutoScaler)

1つ前の手法では、外部からリクエストが送られてくるようなPod(例えばWebアプリのような)だとうまくスケールさせることができます。 しかし、Pod自体が外部にリクエストしにいくような場合(例えばQueueにデータを取りにいって処理するようなアプリ)だとうまくスケールできない可能性があります。

いわゆるPull型とPush型の問題で、後者の場合はCPU利用率やメモリ利用率が上がりきらない可能性があるためです。 例えば毎秒30件のデータをQueueから取得できるアプリがあったとして、そのキューに毎秒100件データが挿入される場合も、毎秒30件データが挿入される場合も、アプリのCPU利用率などは変わらないことが想像できます。

この場合は外部メトリクスをスケール判断に利用する必要があるかと思います。 幸いにもHPAにはカスタムメトリックという外部メトリクスでスケール判断を行う機能があるので、これを利用してオートスケールするのを確認してみます。


例として、シンプルにSQSからデータを取得するアプリを用意して、それをスケールさせてみようと思います。 外部メトリクスとしては、SQSを使うので収集が楽なCloudWatchを利用します。 CPU利用率などをみる必要がなくなったので、このhpaはMetrics Serverがなくても動作します。 またClusterAutoScalerについては、1つ前の手法と変わりありません。 EKS-architecture.002.png

SQSとやりとりするアプリを用意

SQSでキューを1つ用意したら、そこにPushやPopを行うプログラムを組みます

import boto3
import uuid
import os
import time
 
queue_name = os.environ['QUEUE_NAME']
action_type = os.environ['ACTION_TYPE']
sqs = boto3.resource('sqs', endpoint_url='https://sqs.ap-northeast-1.amazonaws.com', region_name="ap-northeast-1")
queue = sqs.get_queue_by_name(QueueName=queue_name)

if action_type == 'push':
  while True:
    msg_list = [{'Id' : '{}'.format(uuid.uuid4()), 'MessageBody' : 'msg_{}'.format(uuid.uuid4())} for i in range(10)]
    response = queue.send_messages(Entries=msg_list)

if action_type == 'pop':
  while True:
      msg_list = queue.receive_messages(MaxNumberOfMessages=10)
      if msg_list:
          for message in msg_list:
              print(message.body)
              message.delete()
      else:
          print('queue is empty!!')
          time.sleep(5)

これをdockerでbuildしておきます。以下にあげてます https://hub.docker.com/repository/docker/esaka/sqs-poppush

次にこれをk8s上にdeployしたいのですが、現在のWorkerのIAMロールにはSQSとやりとりするための権限がないので 以下のIAMポリシーを作成して、Workerロールにアタッチしておきます

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sqs:DeleteMessage",
                "sqs:GetQueueUrl",
                "sqs:DeleteMessageBatch",
                "sqs:SendMessageBatch",
                "sqs:ReceiveMessage",
                "sqs:SendMessage"
            ],
            "Resource": "arn:aws:sqs:ap-northeast-1:<accountID>:<queueName>"
        }
    ]
}

これを以下のマニフェストで動かして、キューにデータをpushします

apiVersion: apps/v1 
kind: Deployment
metadata:
  name: sqs-app-push
spec:
  selector:
    matchLabels:
      app: sqs-app-push
  replicas: 1
  template:
    metadata:
      labels:
        app: sqs-app-push
    spec:
      containers:
      - name: sqs-app-push
        image: esaka/sqs-poppush:latest
        command: ["python", "sqs-poppush.py"]
        env:
        - name: QUEUE_NAME
          value: eks-sample-queue
        - name: ACTION_TYPE
          value: push
        - name: AWS_DEFAULT_REGION
          value: "ap-northeast-1"

環境変数でPushするQueueを指定してあげてください。eks-sample-queueというところを使うようにしました。

k8s-cloudwatch-adapterの用意

AWSは公式でk8s-cloudwatch-adapterというcloudwatch用のadapterを用意しています。 これを利用することで、hpaのスケール条件にcloudwatchのメトリクスが使えるようになります。

まずこれもIAMの設定が必要です。メトリクスを取得するためのポリシーを作成してWorkerのIAMロールにアタッチします

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cloudwatch:GetMetricData"
            ],
            "Resource": "*"
        }
    ]
}

あとは提供しているマニフェストをapplyします

$ kubectl apply -f https://raw.githubusercontent.com/awslabs/k8s-cloudwatch-adapter/master/deploy/adapter.yaml

これでカスタムリソースとして、CloudWatchのメトリクスをk8s内で扱えるようになりました。 SQSのキューの長さをメトリクスとして登録しておきます。

apiVersion: metrics.aws/v1alpha1
kind: ExternalMetric
metadata:
  name: eks-sample-queue-length
spec:
  name: eks-sample-queue-length
  resource:
    resource: "deployment"
  queries:
    - id: sqs_queue_length
      metricStat:
        metric:
          namespace: "AWS/SQS"
          metricName: "ApproximateNumberOfMessagesVisible"
          dimensions:
            - name: QueueName
              value: "eks-sample-queue"
        period: 300
        stat: Average
        unit: Count
      returnData: true

hpaの設定

最後にキューをpopするアプリをdeployし、それのhpaを設定します。

apiVersion: apps/v1 
kind: Deployment
metadata:
  name: sqs-app-pop
spec:
  selector:
    matchLabels:
      app: sqs-app-pop
  replicas: 1
  template:
    metadata:
      labels:
        app: sqs-app-pop
    spec:
      containers:
      - name: sqs-app-pop
        image: esaka/sqs-poppush:latest
        command: ["python", "sqs-poppush.py"]
        env:
        - name: QUEUE_NAME
          value: eks-sample-queue
        - name: ACTION_TYPE
          value: pop
        - name: AWS_DEFAULT_REGION
          value: "ap-northeast-1"
kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2beta1
metadata:
  name: sqs-app-pop-scaler
spec:
  scaleTargetRef:
    apiVersion: apps/v1beta1
    kind: Deployment
    name: sqs-app-pop
  minReplicas: 1
  maxReplicas: 40
  metrics:
  - type: External
    external:
      metricName: eks-sample-queue-length # cloudwatch-metrics.ymlのname
      targetValue: 100

動作確認

pushするアプリをreplica数=2で動かしている状態で, popするアプリをreplica数=1で開始してみました。

image.png

最初はpushの方が量多いので、どんどんキューに溜まっていきますが 途中からhpaが始まって、pod数が増えると共にキューの長さの傾きが緩やかになり 最終的には傾きが逆になって、pod数も減っていきました。 ゆっくりですが、pushとpopで均衡が取れるreplica数が変化していく様子が見れます。

閉域でのスケーリング(HPA+CloudWatch Alarm)

仕事でAWS使う場合などは、インターネットとのアクセスが禁止されるというのはよくあると思います。 しかしながら、これまで紹介したパターンは2つともインターネットに繋がるVPCというのが前提となっています。 その理由がec2autoscalingエンドポイントのせいです。

Node数の増減を行っていたClusterAutoScalerはec2autoscalingエンドポイントを叩いて、Node数を増減させます。 EC2 AutoScalingはEC2の機能の一部だからec2エンドポイントでAPI叩けるだろうと思ってしまうのですが、これが厳密に分かれており、また2019/12時点でec2autoscalingにはVPCエンドポイントが提供されていないため閉域で叩くことができません。 (CloudWatchはVPCエンドポイントがありますので、HPAはこれまで通り実行できます)

EKS Managed Worker Groupが導入されたことで、EKSのエンドポイントを使ってノード数の増減ができるようになったのですが、ClusterAutoScalerがまだ対応していないようですし、そもそもEKSのエンドポイント(eks.ap-northeast-1.amazonaws.com)もまだVPC Endpointが提供されていないため、閉域での利用は難しいです。

現状では、以下のように CloudWatch Alarmを利用するのが解決策の一つかと思います。

EKS-architecture.003.png

図に書いたように、CloudWatch AlarmからEC2 AutoScalingまでがAWS内のネットワークを通ってリクエストが行われるので, 閉域でもEC2 AutoScalingが実行できます。

これも実際に試しみてみます。

閉域のEKSクラスタを構築

InternetGatewayをなくして、Imageのpullも外部のDocker HubではなくECRを利用するように変更します。 検証なのでkubectlを打つのは手抜きでローカルのままです。。

https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/cluster-endpoint.html エンドポイントのパブリックアクセス, エンドポイントのプライベートアクセス共に有効の状態です。 厳密にやるなら、エンドポイントのパブリックアクセス=無効, エンドポイントのプライベートアクセス=有効にして、EKSのVPC内にkubectlを打つためのインスタンスを用意してあげて、そこに踏み台経由でアクセスして操作することになるかと思います。

EKS-architecture.005.png

作成方法なのですが、これまで通りeksctlそのままだと作れないので、以下の4つの手順に分けて構築します。

  1. ネットワーク周りのリソース作成
  2. EKSクラスタを構築
  3. EKSクラスタの設定をアップデートして、エンドポイントのプライベートアクセスを有効にする
  4. ワーカーグループを作成する

ネットワーク周りのリソース作成

CloudFormationのtemplateを用意したので、これで作成してください

VPCとprivate subnet2つ、それから以下のVPC Endpointを作成します

  • ecr.api
  • ecr.dkr
  • ec2
  • monitoring
  • s3
  • sqs

sqs, monitoringは今回の検証用で使うためでEKSの動作に必須ではありません。 ecr.api, ecr.dkr, ec2, s3はEKSを閉域で動かすのに必須になります。 (s3は不要かと思ったのですが、ecrからpullするときにs3に認証情報っぽい?のを取りにいってるようだったので付与してます)

EKSクラスタを構築

クラスタはeksctlで構築します。(IAMの設定など一括でやってくれるので) 1つ前の手順で作成したsubnet2つのsubnet-idを指定して以下を実行します

$ eksctl create cluster --name <cluster-name> \
    --vpc-private-subnets <private-subentid1>,<private-subnetid2> \
    --without-nodegroup 

--vpc-private-subnetsを指定することで、既存のネットワークを利用でき --without-nodegroupを指定することで、ワーカーグループを作成せずにクラスタだけ構築できます。

これも10分ほど待てば作られます。

EKSクラスタの設定をアップデートして、エンドポイントのプライベートアクセスを有効にする

https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/cluster-endpoint.html 一つ前のステップでこれも設定できればいいのですが、現在eksctlではサポートしていないようなので、クラスタ作成後にプライベートアクセスを有効にしてやります。

以下のコマンドで実行します。

$ aws eks update-cluster-config --name <cluster-name> --resources-vpc-config endpointPublicAccess=true,endpointPrivateAccess=true

ワーカーグループを作成する

最後にworker groupを作成します。 eksctlで作りたいのですが、現在eksctlを使って、manage worker groupかつ、private subnetのみのnodegroupは作成負荷のようなので、前のパターンで作成されていた、CFnテンプレートを参考にして、CloudFormationで作成します。

以下にテンプレートを用意しておきました。 https://github.com/esakat/advent-calender-eks/blob/master/CFn/private-managed-worker.yml

これで作成後にnodeがちゃんとReadyになればおkです。

$ kubectl get no
NAME                                             STATUS   ROLES    AGE   VERSION
ip-10-192-0-79.ap-northeast-1.compute.internal   Ready    <none>   21m   v1.14.7-eks-1861c5

またここで、作成されたWorker用のIAM Roleに1つ前のパターンで作成した2つのポリシー(CloudWatchとSQS)をアタッチしておいてください

外部ImageをECRへPushしておく

外部にアクセスできなかったので、利用するDocker Imageを全てECRにあげておく必要があります。 以下のImageを内部にあげておきます。

  • chankh/k8s-cloudwatch-adapter:v0.7.0
  • esaka/sqs-poppush:latest
$ aws ecr create-repository --repository-name chankh/k8s-cloudwatch-adapter
$ aws ecr create-repository --repository-name esaka/sqs-poppush
$ $(aws ecr get-login --no-include-email --region ap-northeast-1)
$ docker pull chankh/k8s-cloudwatch-adapter:v0.7.0
$ docker pull esaka/sqs-poppush:latest
$ docker tag chankh/k8s-cloudwatch-adapter:v0.7.0 <accountId>.dkr.ecr.ap-northeast-1.amazonaws.com/chankh/k8s-cloudwatch-adapter:v0.7.0
$ docker tag esaka/sqs-poppush:latest <accountId>.dkr.ecr.ap-northeast-1.amazonaws.com/esaka/sqs-poppush:latest
$ docker push <accountId>.dkr.ecr.ap-northeast-1.amazonaws.com/chankh/k8s-cloudwatch-adapter:v0.7.0
$ docker push <accountId>.dkr.ecr.ap-northeast-1.amazonaws.com/esaka/sqs-poppush:latest

k8s-cloudwatch-adapterをデプロイしておきます。 manifestを落とし後に

$ wget https://raw.githubusercontent.com/awslabs/k8s-cloudwatch-adapter/master/deploy/adapter.yaml

imageをecrの方にむけてください

      - name: k8s-cloudwatch-adapter
-        image: chankh/k8s-cloudwatch-adapter:v0.7.0
+        image: <accountId>.dkr.ecr.ap-northeast-1.amazonaws.com/chankh/k8s-cloudwatch-adapter:v0.7.0

applyします

$ kubectl apply -f adapter.yaml
$ kubectl get po -n custom-metrics
NAME                                      READY   STATUS    RESTARTS   AGE
k8s-cloudwatch-adapter-687bbc8c86-rp5nb   1/1     Running   0          27s

正常に動作してくれました。

CloudWatch Alarmの設定

以下にCloudFormationのtemplateを用意したので、それを設定してください

https://github.com/esakat/advent-calender-eks/blob/master/CFn/cloudwatch-alarm.yml

分かりづらいので、具体的に何をやっているか書きます。 作成されるリソースとしては以下3つになります。

  • AutoScalingGroupのScaleIn Policy
  • AutoScalingGroupのScaleOut Policy
  • CloudWatch Alarm

ScaleIn/Out Policyについて

AutoScalingのスケーリングポリシーは3つあるんですが、その中で一番単純なシンプルスケーリングを使っています。

image.png

上記のように2つのポリシーが対象のAutoScaling Groupに割り当てられます。 アクションを実行のところに書かれているとおり、スケールアウトが実行されると1台追加、スケールインが実行されると1台削除という挙動になります。 またその後待機はクールタイムを設定しており、一度スケールが行われた後、再度スケール可能になるまでの時間となります。 スケールアウトは早く実行して欲しいので60秒, スケールインはゆっくり実行して欲しいので300秒の設定になっています。

また、これだとスケールインで0台になったり、スケールアウトで99台立ったりする?とか思うかもしれませんが AutoScalingGroupのMin/Maxで指定した範囲内でのスケールになりますのでご安心を

そして、これらはあくまでpolicyなので、次のCloudWatch Alarmからこれらを呼び出すようになっています。

CloudWatch Alarmについて

Alarmについては以下2つ説明した方が良いと思っています。

  • アラームが行うアクション
  • アラームに使うメトリクス

まずアラームが行うアクションですが、上記で説明したAutoScalingGroupのScaleInとScaleOutを呼び出しています。 ここはCloudFormationのテンプレートをみるとわかるのですが、以下のようにアクションを設定しています。

      AlarmActions:
        - !Ref ASGScaleOutPolicy
      OKActions:
        - !Ref ASGScaleInPolicy

名前の通りなんですが、アラーム時はAlarmActionsで指定したScaleOutが, OK時はOKActionsで指定したScaleInが呼ばれます。 これでAutoScalingGroupのインスタンス台数を増減できます。

次にアラームに使うメトリクスですが、HPAでやってるようにキューの長さだけでもいいのですが それだと、PushとPopで均衡が取れててキューの長さが0の時もスケールインしようとしてしまいます。 ので、インスタンスのCPU利用率も取得して、これらを組み合わせた算術式メトリクスというのを利用しています。

image.png

上が実際にアラームで使っているメトリクスなのですが、e1, m1, m2という3つのメトリクスを用意しています。 m1はキューの長さを集めていて、m2は対象AutoScalingGroupのインスタンスの平均CPU利用率を集めています。 それでこれらをOR条件のように、どちらかがアラームをあげていればアラームという状況にしたいです。 そのためにm1,m2のMAXとしてe1を作っています。 具体的には以下のような算術式にしています。

MAX([(m1/1000),(m2/40)])

最初にm1とm2をそれぞれの閾値で割っています(キューの長さは1000より大きいとアラーム, CPU利用率が40%より大きいとアラーム) これによって2つの閾値が揃って1になります(閾値で割っているので、閾値を超えると1以上になりますし、閾値を下回ると1以下になります) あとはこれのMAXをとることで、どちらかが閾値を超えていればe1 >= 1になります。

あとはe1 >= 1をアラーム条件に設定すれば、どちらかがアラームの場合にアラームをあげるという設定ができます。

結果確認

パターン2の時と同じような試験を試しました。 pushするアプリをreplica数=1で動かしている状態で, popするアプリをreplica数=1で開始してみました。

image.png

いい感じにスケールアウト/インするのが確認できました。 (途中でpush側のpodが乗っているインスタンスが削除されて、そのあとpush側がPendingになったので後半はただのスケールインになってしまった…)

余談1

閉域でもできるという利点の他に、このパターンではスケール時間の短縮を実現できる可能性があります。 前2つのパターンで使っていたClusterAutoScalerでは、HPAでreplica数を増やして、Nodeに配置できなくなったというのを検知してノードを増やすため段階的な処理になり、スケールに時間がかかってしまいます。

このパターンではClusterAutoScalerを利用せず、CloudWatch Alarmを利用するため、NodeとPodのオートスケーリングが非同期的になり また金さえつめばCloudWatchも秒単位Alarmあげれるので、スケールにかかる時間を大幅に減らせると思います。

そのため投機的実行を行いたい場合もこの方法を検討した方が良いかもしれません。

余談2

今回のシングルテナントような、単一のアプリがクラスタを占有する構成であれば、利用するメトリクスはそこまで悩まなくて良いと思うのですが、マルチテナントのように、様々な種類のアプリが稼働するクラスタでは利用するメトリクスを変える必要があるかと思います。 それこそ、ClusterAutoScalerが内部でやってるようにPodのリソース利用率の合計からNode数を割り出すのが良いかと。 ただ、自分で触ってもらえればわかると思うのですが CloudWatch Alarmの発火条件の設定はあまり複雑な設定ができないので, 別の箇所でメトリクスの計算を行って、CloudWatchへカスタムメトリクスとして登録してあげるという手段もありかもです。

最後に

EKSにおけるオートスケーリングパターンを3つ紹介しました。

EKSというかコンテナ周り、どんどん機能が追加されて便利になってきてますね。 (Managed WorkerノードもRe:inventで出るかなと思ってたのですが、kubeconで出たので焦って調べました(今回の記事に関わってくる内容なので))

https://github.com/aws/containers-roadmap/projects/1 あたりをみると、今後追加されそうな機能とか見れて楽しいです。

個人的にはEKS on Fargateがくることを期待してます。 最近Coming Soonになったので、今回のRe:Inventで発表されるのでは?と期待してます