Amazon EKS 上でKarpenter + HPA のスケーリングの動作を確認する

# はじめに

本記事は、[Amazon EKS](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/what-is-eks.html) 上でNode のオートスケーラーである[Karpenter](https://aws.amazon.com/jp/blogs/news/introducing-karpenter-an-open-source-high-performance-kubernetes-cluster-autoscaler/) とPod のオートスケーラーである[Horizontal Pod Autoscaler(HPA)](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) を組み合わせたスケーリングの動作を確認をした際の作業メモです。

主に、次の2点について記載しています。
- 動作確認をした環境の構築手順
- 簡単なスケーリングの動作確認

Karpenter と HPA を導入する際の参考になれば幸いです。
# 環境の構築手順
## 環境の全体構成
動作確認をする環境の全体構成は、下図としてます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/bac4ad8b-b633-b219-ff6a-6164f7e7d6f0.png"　 width=90%>
## 各種コード類
https://github.com/okubo-t/aws-tf-eks-karpenter-test.git

```
tree aws-tf-eks-karpenter-test 
aws-tf-eks-karpenter-test
├── files # マニフェストファイル群
│   ├── test-app.yaml
│   ├── test-hpa.yaml
│   └── test-karpenter.yaml
├── main.tf # VPC,EKSクラスタ,IRSA構築用tfファイル
├── provider.tf
└── versions.tf
```
## 構築ツールの環境
```
$ terraform version
Terraform v1.1.9

$ helm version --short
v3.9.0+g7ceeda6

$ aws --version
aws-cli/2.7.18 Python/3.10.5 Darwin/21.6.0 source/arm64 prompt/off

$ eksctl version
0.100.0

$ kubectl version --short
Client Version: v1.24.3

$ ab -V          
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>

$ k9s version
Version:    0.26.3
Commit:     0893f13b3ca6b563dd0c38fdebaefdb8be594825
Date:       n/a
```
## EKS クラスターの構築
Terraform でVPC, EKS クラスター, [IRSA（IAM Roles for Service Accounts）](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/iam-roles-for-service-accounts.html) を構築します。(参考: [Getting Started with Terraform](https://karpenter.sh/v0.14.0/getting-started/getting-started-with-terraform/))

EKS クラスターの、初期のノードの構成は下記としてます。
- ノード数は、`1`
- インスタンスタイプは、`m5.large(vCPU:2)`

```terraform:main.tf
# VPC は省略

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "18.27.1"

  # EKS CONTROL PLANE 
  cluster_name = local.cluster_name

  # 
  # Kubernetes 1.22
  # https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.22
  #
  cluster_version = "1.22"

  #
  # Amazon EKS クラスターエンドポイントアクセスコントロール
  # https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/cluster-endpoint.html
  #
  cluster_endpoint_private_access = false
  cluster_endpoint_public_access  = true

  #
  # Amazon EKS コントロールプレーンのログ記録
  # https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/control-plane-logs.html 
  #
  cluster_enabled_log_types = ["audit", "api", "authenticator"]

  #
  # Amazon EKS アドオン
  # https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/eks-add-ons.html
  #
  cluster_addons = {

    #
    # CoreDNS アドオンの管理
    # https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/managing-coredns.html
    #
    coredns = {
      resolve_conflicts = "OVERWRITE"
    }

    #
    # kube-proxy アドオンの管理
    # https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/managing-kube-proxy.html
    #
    kube-proxy = {}

    #
    # Amazon VPC CNI Plugin for Kubernetes を使用したAmazon EKS での Pod ネットワーク
    # https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/pod-networking.html
    #
    vpc-cni = {
      resolve_conflicts = "OVERWRITE"
    }
  }

  # EKS CLUSTER VPC AND SUBNETS
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  #
  # マネージド型ノードグループ
  # https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/managed-node-groups.html
  #
  eks_managed_node_groups = {
    karpenter-test = {
      node_group_name = "managed-ondemand"

      instance_types = ["m5.large"]

      min_size     = 1
      max_size     = 1
      desired_size = 1

      create_security_group = false

      iam_role_additional_policies = [
        "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
      ]

      tags = {
        # This will tag the launch template created for use by Karpenter
        "karpenter.sh/discovery" = local.cluster_name
      }
    }
  }

  node_security_group_additional_rules = {
    # Extend node-to-node security group rules. Recommended and required for the Add-ons
    ingress_self_all = {
      description = "Node to node all ports/protocols"
      protocol    = "-1"
      from_port   = 0
      to_port     = 0
      type        = "ingress"
      self        = true
    }
    # Recommended outbound traffic for Node groups
    egress_all = {
      description      = "Node all egress"
      protocol         = "-1"
      from_port        = 0
      to_port          = 0
      type             = "egress"
      cidr_blocks      = ["0.0.0.0/0"]
      ipv6_cidr_blocks = ["::/0"]
    }

    ingress_cluster_to_node_all_traffic = {
      description                   = "Cluster API to Nodegroup all traffic"
      protocol                      = "-1"
      from_port                     = 0
      to_port                       = 0
      type                          = "ingress"
      source_cluster_security_group = true
    }
  }

  node_security_group_tags = {
    # NOTE - if creating multiple security groups with this module, only tag the
    # security group that Karpenter should utilize with the following tag
    # (i.e. - at most, only one security group should have this tag in your account)
    "karpenter.sh/discovery" = local.cluster_name
  }
}

data "aws_eks_cluster" "cluster" {
  name = module.eks.cluster_id
}

data "aws_eks_cluster_auth" "cluster" {
  name = module.eks.cluster_id
}

#
# AWS Load Balancer Controller IRSA
#
module "load_balancer_controller_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "5.3.0"

  role_name = "load-balancer-controller-${local.cluster_name}"

  attach_load_balancer_controller_policy = true

  oidc_providers = {
    ex = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:aws-load-balancer-controller"]
    }
  }
}

resource "kubernetes_service_account" "aws_loadbalancer_controller" {
  metadata {
    name      = "aws-load-balancer-controller"
    namespace = "kube-system"
    annotations = {
      "eks.amazonaws.com/role-arn" = module.load_balancer_controller_irsa.iam_role_arn
    }
  }
}

#
# Karpenter IRSA
#
module "karpenter_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "5.3.0"

  role_name = "karpenter-controller-${local.cluster_name}"

  attach_karpenter_controller_policy = true
  karpenter_tag_key                  = "karpenter.sh/discovery"

  karpenter_controller_cluster_id = module.eks.cluster_id

  karpenter_controller_node_iam_role_arns = [
    module.eks.eks_managed_node_groups[local.cluster_name].iam_role_arn
  ]

  oidc_providers = {
    ex = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["karpenter:karpenter"]
    }
  }
}

resource "kubernetes_namespace" "karpenter" {
  metadata {
    name = "karpenter"
  }
}

resource "kubernetes_service_account" "karpenter" {
  metadata {
    name      = "karpenter"
    namespace = "karpenter"

    annotations = {
      "eks.amazonaws.com/role-arn" = module.karpenter_irsa.iam_role_arn
    }
  }
}

resource "aws_iam_instance_profile" "karpenter" {
  name = "KarpenterNodeInstanceProfile-${local.cluster_name}"
  role = module.eks.eks_managed_node_groups[local.cluster_name].iam_role_name
}
```

Terraform を実行します。
```
$ export AWS_PROFILE= [ YOUR AWS ACCOUNT PROFILE NAME ]

$ terraform init
$ terraform plan
$ terraform apply

結果:
Apply complete! Resources: 60 added, 0 changed, 0 destroyed.
```
## Kubernetes 上の構築
現在のコンテキストを確認します。
```
$ kubectl config current-context
```
### 環境変数の設定
```
# AWS アカウント情報
$ export AWS_PROFILE=[ YOUR AWS ACCOUNT PROFILE NAME ]

# EKS クラスター名
$ export CLUSTER_NAME=karpenter-test
```
### Metrics Server のインストール
参考: [メトリック サーバー](https://artifacthub.io/packages/helm/metrics-server/metrics-server)
```
# リポジトリを追加
$ helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/

# ローカルリポジトリを更新
$ helm repo update

# インストール
$ helm upgrade -n kube-system --install metrics-server metrics-server/metrics-server
```

### AWS Load Balancer Controller のインストール
参考: [AWS Load Balancer Controller アドオンのインストール](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/aws-load-balancer-controller.html)
```
# リポジトリを追加
$ helm repo add eks https://aws.github.io/eks-charts

# ローカルリポジトリを更新
$ helm repo update

# インストール
$ helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system \
  --set clusterName=${CLUSTER_NAME} \
  --set serviceAccount.create=false \
  --set serviceAccount.name=aws-load-balancer-controller \
  --set image.repository=602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/amazon/aws-load-balancer-controller
```

### Karpenter(v0.14.0) のインストール
参考: [Install Karpenter Helm Chart](https://karpenter.sh/v0.14.0/getting-started/getting-started-with-eksctl/#install-karpenter-helm-chart)
```
# リポジトリを追加
$ helm repo add karpenter https://charts.karpenter.sh/

# ローカルリポジトリを更新
$ helm repo update

# インストール
$ helm upgrade --install karpenter karpenter/karpenter --namespace karpenter \
  --version v0.14.0 \
  --set serviceAccount.create=false  \
  --set serviceAccount.name=karpenter \
  --set clusterName=${CLUSTER_NAME} \
  --set clusterEndpoint=$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.endpoint" --output json) \
  --set aws.defaultInstanceProfile=KarpenterNodeInstanceProfile-${CLUSTER_NAME}
```

### リリースの一覧
```
$ helm list -A                     
NAME                            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART                               APP VERSION
aws-load-balancer-controller    kube-system     1               2022-08-16 07:41:03.655152 +0900 JST    deployed        aws-load-balancer-controller-1.4.4  v2.4.3     
karpenter                       karpenter       1               2022-08-16 07:45:10.041976 +0900 JST    deployed        karpenter-0.14.0                    0.14.0     
metrics-server                  kube-system     1               2022-08-16 07:34:30.364095 +0900 JST    deployed        metrics-server-3.8.2                0.6.1 
```
### ALB とPod のデプロイ
Pod のDeployment のパラメータは下記としています。
`image=kennethreitz/httpbin`,`replicas=1`,`resources request=cpu: 100m`,`resources limits=cpu: 200m`
```
$ kubectl apply -f ./files/test-app.yaml 
```
```yaml:test-app.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: test-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-app
  namespace: test-app
spec:
  selector:
    matchLabels:
      app: test-app
  replicas: 1
  template:
    metadata:
      labels:
        app: test-app
    spec:
      containers:
        - name: test-app
          image: kennethreitz/httpbin:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 80
          resources:
            limits:
              cpu: 200m
            requests:
              cpu: 100m
---
apiVersion: v1
kind: Service
metadata:
  name: test-app
  namespace: test-app
spec:
  selector:
    app: test-app
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: NodePort
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-app
  namespace: test-app
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/tags: Name=karpenter-test
    alb.ingress.kubernetes.io/target-type: ip
spec:
  rules:
    - host: "*.amazonaws.com"
      http:
        paths:
          - path: /
            pathType: Exact
            backend:
              service:
                name: test-app
                port:
                  number: 80
```

### Horizontal Pod Autoscaler の設定
Pod のスケールの定義は、下記としてます。
- Pod のスケール範囲は、`minReplicas=1` 〜 `maxReplicas=10` 
- Pod のスケール条件は、`averageUtilization=50%` 
- Pod のスケールダウンの挙動は、60秒あたり30%(`behavior scaleDown policies`)

```
$ kubectl apply -f ./files/test-hpa.yaml 
```

```yaml:test-hpa.yaml
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: test-hpa
  namespace: test-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: test-app
  minReplicas: 1
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 50
  behavior:
    scaleDown:
      policies:
        - type: Percent
          value: 30
          periodSeconds: 60
      stabilizationWindowSeconds: 60
```

### Provisioner の設定
Node のスケールの定義は、下記としてます。(参考: [Karpenter Provisioner](https://karpenter.sh/v0.14.0/provisioner/))
- Node のインスタンスタイプ は、`m5.large`
- Node は、`on-demand`
- Node に、Pod が 30 秒以上ない場合は、終了(`ttlSecondsAfterEmpty=30`)
- Node の有効期限は、900秒(`ttlSecondsUntilExpired=900`)

```
$ kubectl apply -f ./files/test-karpenter.yaml
```

```yaml:test-karpenter.yaml
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: test-karpenter
spec:
  ttlSecondsUntilExpired: 900
  ttlSecondsAfterEmpty: 30
  requirements:
    - key: node.kubernetes.io/instance-type
      operator: In
      values: ["m5.large"]
    - key: "topology.kubernetes.io/zone"
      operator: In
      values: ["ap-northeast-1a", "ap-northeast-1c"]
    - key: "kubernetes.io/arch"
      operator: In
      values: ["amd64"]
    - key: karpenter.sh/capacity-type
      operator: In
      values: ["on-demand"]
  limits:
    resources:
      cpu: "1000"
      memory: 1000Gi
  provider:
    tags:
      karpenter.sh/discovery: karpenter-test
    subnetSelector:
      karpenter.sh/discovery: karpenter-test
    securityGroupSelector:
      karpenter.sh/discovery: karpenter-test
    instanceProfile: KarpenterNodeInstanceProfile-karpenter-test
```

### Kubernetes 上のリソースの環境
これで、環境の構築は完了です。
```
$ kubectl get node,pod,ing,hpa -A

# 結果
NAME                                                 STATUS   ROLES    AGE   VERSION
node/ip-10-0-3-252.ap-northeast-1.compute.internal   Ready    <none>   35h   v1.22.9-eks-810597c

NAMESPACE     NAME                                               READY   STATUS    RESTARTS   AGE
karpenter     pod/karpenter-5b486c8b59-f876z                     2/2     Running   0          35h
kube-system   pod/aws-load-balancer-controller-79f8b7cdb-ktgjh   1/1     Running   0          35h
kube-system   pod/aws-load-balancer-controller-79f8b7cdb-m4gwd   1/1     Running   0          35h
kube-system   pod/aws-node-jkbr5                                 1/1     Running   0          35h
kube-system   pod/coredns-5b6d4bd6f7-cgl2n                       1/1     Running   0          35h
kube-system   pod/coredns-5b6d4bd6f7-r2542                       1/1     Running   0          35h
kube-system   pod/kube-proxy-b2bbd                               1/1     Running   0          35h
kube-system   pod/metrics-server-694d47d564-g5btk                1/1     Running   0          35h
test-app      pod/test-app-c55bc5b88-5h6qt                       1/1     Running   0          9m12s

NAMESPACE   NAME                                 CLASS    HOSTS             ADDRESS                                                                     PORTS   AGE
test-app    ingress.networking.k8s.io/test-app   <none>   *.amazonaws.com   k8s-testapp-testapp-8e64fd9961-700841967.ap-northeast-1.elb.amazonaws.com   80      29h

NAMESPACE   NAME                                           REFERENCE             TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
test-app    horizontalpodautoscaler.autoscaling/test-hpa   Deployment/test-app   1%/50%    1         10        1          20h
```

## スケーリングの動作確認
ALB のDNS名を確認します。(`ADDRESS=k8s-testapp-testapp-xxxxxxxxxx`)
```
$ kubectl get ingress -n test-app
NAME       CLASS    HOSTS             ADDRESS                                                                     PORTS   AGE
test-app   <none>   *.amazonaws.com   k8s-testapp-testapp-8e64fd9961-700841967.ap-northeast-1.elb.amazonaws.com   80      32h
```

URL に対して、疎通確認をします。
```
$ curl -I http://k8s-testapp-testapp-8e64fd9961-700841967.ap-northeast-1.elb.amazonaws.com/
HTTP/1.1 200 OK
```

Apache Bench でURL に対して、負荷をかけます。
```
$ ab -c 50 -t 300 http://k8s-testapp-testapp-8e64fd9961-700841967.ap-northeast-1.elb.amazonaws.com/
```

負荷によって、Horizontal Pod Autoscaler(HPA)によるPod のスケーリングが実行されますが、一部のPod は容量不足により、スケジュールできず、`Pending` の状態となります。
![01_Karpenter_Test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/6dc0711e-3fa1-73b9-9c54-8f8e330e9017.png)

Karpenter は、`Pending`状態のPod を検知し、Node を追加で作成しようとします。
![02_Karpenter_Test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/95f9fd00-5094-9472-b939-0ccd28c9fd89.png)

Node が追加で作成されます。
![03_Karpenter_Test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/14f69e1a-a837-e5f3-2ca9-d9b61da9a861.png)

Karpenter が`Pending` の状態だったPod を追加作成されたNode にバインドします。
![04_Karpenter_Test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/be68bdb0-e7f4-5f82-969d-821fa18db9ec.png)

以上で、Karpenter + HPA のスケーリングの動作を確認することができました。

# クリーンアップ
[重要]ALB を含むテスト用のリソースを削除します
```
$ kubectl delete -f ./files/test-karpenter.yaml
$ kubectl delete -f ./files/test-hpp.yaml
$ kubectl delete -f ./files/test-app.yaml

# 結果
namespace "test-app" deleted
deployment.apps "test-app" deleted
service "test-app" deleted
ingress.networking.k8s.io "test-app" deleted
```

EKS クラスターを削除します。
```
$ terraform destroy --target=module.eks
```

VPC を削除します。(完全削除)
```
$ terraform destroy
```

# さいごに
実際に計測したわけではないのですが、スケーリングは、[Cluster Autoscaler](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/autoscaling.html#cluster-autoscaler) と比べると[Karpenter](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/autoscaling.html#karpenter) の方が速いと感じています。

Karpenter は、Cluster Autoscaler の代替となるのか、もう少し深く動作確認を進めたいと思います。