Horizontal Pod Autoscaling と Cluster Autoscaler を EKSに設定してみた

はじめに

おはようございます、加藤です。CKAを受けようと思っていましたが、勉強する時間もとれず、申し込みが英語でまるで理解できなかったのでチキって辞めました。
というわけで、EKSでHorizontal Pod Autoscaling と Cluster Autoscaler を設定してみました。

説明

Horizontal Pod Autoscaling

Horizontal Pod Autoscaler - Kubernetes

Horizontal Pod Autoscaling(以降、HPA)はCPU負荷などメトリクスに応じてDeploymentに命令を送りReplica数を制御する機能です。
コンテナに与えるリソースは変えずに、同時実行を増減させます。つまり、スケールイン/アウトを行います。

Horizontal Pod Autoscaler で利用可能なメトリクスは下記があります。 1

  • Resource
    • CPU/メモリのリソース
  • Object
    • Kubernetes Objectのメトリクス(例: Ingressのhits/s)
  • Pods
    • Podのメトリクス(例: Podのコネクション数
  • External
    • Kubernetes外のメトリクス(例: LBのQPS、Cloud Pub/Subの溜まっているメッセージ数)

そして、EKSでHPAを使うには、Kubernetes Metrics Serverをインストールする必要があります。

Cluster Autoscaler

Cluster Autoscaler(以降、CA)はWorkerNodesのAutoscalingを行う機能です。この機能は現在NodeのCPU負荷などではなく、Pending状態のPodを検知してスケールアウトを行います。
EKSのWorkerNodesはAmazon EC2 Auto Scaling(以降、EC2 Auto Scaling)で構築しますが、スケーリングポリシーは設定しません。
CAからEC2 Auto Scalingに対して、希望するキャパシティを指定する事でAutoscalingを行います。

やってみた

前提

  • EKS環境が構築されている
  • Helmがインストールされている

Metrics Server

EKSにMetrics Serverをインストールします。

helm install stable/metrics-server --version 2.6.0 --name metrics-server --namespace kube-system

HPA

DeploymentとHorizontalPodAutoscalerのマニフェストファイルを作成してapplyします。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - image: busybox
          name: sample-deployment
          command: ["dd", "if=/dev/zero", "of=/dev/null"] # CPUを100%使うように
          resources:
            requests:
              memory: "64Mi"
              cpu: "250m"
            limits:
              memory: "128Mi"
              cpu: "500m"
---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: sample-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: sample-deployment
  minReplicas: 1
  maxReplicas: 30
  metrics:
    - type: Resource
      resource:
        name: cpu
        targetAverageUtilization: 50
kubectl apply -f sample-hpa.yaml

メトリクスの取得状況を確認します。

kubectl get hpa -w

NAME         REFERENCE                      TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
sample-hpa   Deployment/sample-deployment   <unknown>/50%   1         30        3          47s
sample-hpa   Deployment/sample-deployment   199%/50%   1     30    3     90s

最初は、と表示されますが、しばらく待っているとメトリクスが表示されます。表示されたら、Ctrl+Cで抜けてください。

Pending状態になっているPodが存在するか確認します。

kubectl get po | grep Pending

もし、Nodesに十分なリソースがあり全て起動している場合は、下記の値を増やしてください。

  maxReplicas: 30

WorkerNodesのIAMロール

CAはWorkerNodesの上で動き、CAからEC2 Auto Scalingの制御を行います。つまりWorkerNodesのIAMロールにアクセスする権限を与える必要があります。
下記のIAMポリシーを作成して、アタッチします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "autoscaling:DescribeAutoScalingGroups",
                "autoscaling:DescribeAutoScalingInstances",
                "autoscaling:DescribeTags",
                "autoscaling:SetDesiredCapacity",
                "autoscaling:TerminateInstanceInAutoScalingGroup"
            ],
            "Resource": "*"
        }
    ]
}

なお、autoscaling:DescribeTags--node-group-auto-discoveryでAuto Scaling Groupを自動検出する時以外は本来不要です。

autoscaler/README.md at master · kubernetes/autoscaler

CA

CAのマニフェストファイルを作成します。

apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
  name: cluster-autoscaler
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: cluster-autoscaler
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
rules:
  - apiGroups: [""]
    resources: ["events", "endpoints"]
    verbs: ["create", "patch"]
  - apiGroups: [""]
    resources: ["pods/eviction"]
    verbs: ["create"]
  - apiGroups: [""]
    resources: ["pods/status"]
    verbs: ["update"]
  - apiGroups: [""]
    resources: ["endpoints"]
    resourceNames: ["cluster-autoscaler"]
    verbs: ["get", "update"]
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["watch", "list", "get", "update"]
  - apiGroups: [""]
    resources:
      [
        "pods",
        "services",
        "replicationcontrollers",
        "persistentvolumeclaims",
        "persistentvolumes",
      ]
    verbs: ["watch", "list", "get"]
  - apiGroups: ["extensions"]
    resources: ["replicasets", "daemonsets"]
    verbs: ["watch", "list", "get"]
  - apiGroups: ["policy"]
    resources: ["poddisruptionbudgets"]
    verbs: ["watch", "list"]
  - apiGroups: ["apps"]
    resources: ["statefulsets"]
    verbs: ["watch", "list", "get"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["watch", "list", "get"]

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["create"]
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["cluster-autoscaler-status"]
    verbs: ["delete", "get", "update"]

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: cluster-autoscaler
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-autoscaler
subjects:
  - kind: ServiceAccount
    name: cluster-autoscaler
    namespace: kube-system

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: cluster-autoscaler
subjects:
  - kind: ServiceAccount
    name: cluster-autoscaler
    namespace: kube-system

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    app: cluster-autoscaler
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cluster-autoscaler
  template:
    metadata:
      labels:
        app: cluster-autoscaler
    spec:
      serviceAccountName: cluster-autoscaler
      containers:
        - image: k8s.gcr.io/cluster-autoscaler:v1.2.2
          name: cluster-autoscaler
          resources:
            limits:
              cpu: 100m
              memory: 300Mi
            requests:
              cpu: 100m
              memory: 300Mi
          command:
            - ./cluster-autoscaler
            - --v=4
            - --stderrthreshold=info
            - --cloud-provider=aws
            - --skip-nodes-with-local-storage=false
            - --nodes=<MIN>:<MAX>:<ASG NAME>
          env:
            - name: AWS_REGION
              value: <AWS_REGION>
          volumeMounts:
            - name: ssl-certs
              mountPath: /etc/ssl/certs/ca-certificates.crt
              readOnly: true
          imagePullPolicy: "Always"
      volumes:
        - name: ssl-certs
          hostPath:
            path: "/etc/ssl/certs/ca-bundle.crt"

いくつか変更する必要があるパラメータがあります。

EC2 Auto Scalingの設定を元に設定してください。

            - --nodes=<MIN>:<MAX>:<ASG NAME>

EKSが動いているリージョンを設定します。

          env:
            - name: AWS_REGION
              value: <AWS_REGION>

applyします。

kubectl apply -f sample-ca.yaml

ログでスケールアップの計画を確認します。

kubectl logs -f deployment/cluster-autoscaler -n kube-system | grep "scale-up plan"

I0423 02:23:07.675641       1 scale_up.go:292] Final scale-up plan: [{<ASG NAME> 3->4 (max: 6)}]
I0423 02:24:21.913766       1 scale_up.go:292] Final scale-up plan: [{<ASG NAME> 4->5 (max: 6)}]

動作確認

EC2 Auto Scaling のコンソールで状態を確認してみます。
a user request explicitly set group desired capacity changing the desired capacity from 3 to 4. ユーザーつまりCAからの要求によってdesired capacityが変更されて、Autoscalingが発生しています。

Podの稼働状態を確認してみます。

kubectl get po

NAME                                READY   STATUS    RESTARTS   AGE
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          20m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          22m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          21m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          24m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          22m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          21m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          21m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          22m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          21m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          21m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          21m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          22m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          22m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          20m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          20m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          21m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          21m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          20m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          22m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          20m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          21m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          24m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          22m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          22m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          21m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          21m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          20m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          22m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          20m
sample-deployment-xxxxxxxxx-xxxxx   1/1     Running   0          24m

すべてのSTATUSがRunningです。

HPAは、targetAverageUtilizationで指定したメトリクスに収まる様にAutoscalingします。今回はCPU使用率 50%に設定したので、平均使用率が50%になるまでスケールアウトし続けます。

ですが、今回は100%に張り付く様にコマンドを指定しているので、いくらスケールアウトしても平均使用率は下がりません。 MAXPODSに達してこれ以上スケールアウトが行われなくなりました。

kubectl get hpa

NAME         REFERENCE                      TARGETS    MINPODS   MAXPODS   REPLICAS   AGE
sample-hpa   Deployment/sample-deployment   116%/50%   1         30        30         25m

CAによるスケールインを確認します。立ち上げたコンテナを全て終了させます。

kubectl delete -f sample-hpa.yaml

ログでスケールインの計画を確認します。

kubectl logs -f deployment/cluster-autoscaler -n kube-system | grep "Scale-down: removing"

I0423 03:52:25.073390       1 scale_down.go:594] Scale-down: removing empty node ip-XX-X-XX-XXX.ap-northeast-1.compute.internal
I0423 03:52:25.073425       1 scale_down.go:594] Scale-down: removing empty node ip-XX-X-XX-XXX.ap-northeast-1.compute.internal

あとがき

EKSでAutoscalingってどうするのか調べる必要があり、まとめてみました。
今回は適当ですが、metrics-serverなど、namespace: kube-system で動かすようなシステムリソースは専用のNodeGroup作ったほうが良いなと思いました。
また、スケールイン周りの動きの深いところが理解しきれていないです、CA→EC2 Auto Scalingに希望キャパシティを減らす命令を出す際に、CA側でどうやって削除されるインスタンスを特定しているのかなどがわかっていないです。

調べながらのブログ化だったので間違いもあるかも知れません、ツッコミや追記した方が良い情報があれば、ぜひコメントをお願います!

参考文献

1: Kubernetes 完全ガイド 著者 青山真也 発行所 株式会社インプレス ISBN978-4-295-00480-6 C3005