コンテンツにスキップ

Karpenter@ハードウェアリソース管理

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。


01. Karpenterの仕組み

アーキテクチャ

Karpenterは、karpenterコントローラーから構成される。

karpenter_architecture


karpenterコントローラー

▼ karpenterコントローラーとは

karpenterコントローラーは、Karpenterのcustom-controllerとして、カスタムリソースを作成/変更する。

また、カスタムリソースの設定値に応じて、API (例:起動テンプレート、EC2フリート) をコールし、AWSリソース (例:起動テンプレート、EC2) をプロビジョニングする。

この時、AWS Load Balancerコントローラーも使用していると、Clusterのサブネット内にEC2 Nodeが増えたことを検知し、ターゲットグループにこれを登録してくれる。

なお、NodePool配下のEC2 Nodeは起動テンプレートから作成するが、起動テンプレート自体はEC2 Nodeの作成後に削除するようになっている。

karpenter_controller

▼ Podのバインド

Karpenterは、新しいNodeにPodをバインドし、kube-schedulerがNodeにPodをスケジューリングさせることを待つ。

kube-schedulerの代わりに、KarpenterがNodeを選定しているため、Podのスケジューリングが早い。

一方で、cluster-autoscalerであれば、Podをバインドしない。

cluster-autoscalerのNodeのスケールアウト後に、kube-schedulerがNode選定処理に基づいてPodをNodeにバインドするため、スケジューリングまでに時間がかかる。


disruption-controller

記入中...


termination-controller

EC2ワーカーNodeの削除命令を検知し、EC2ワーカーNodeのGraceful Shutdownから削除までを行う。

EC2ワーカーNodeは、デフォルトではNodeのGraceful Shutdownを実施しない。

そのため、kubelet-config.jsonファイル (KubeletConfiguration)の--shutdown-grace-periodオプションを使用する必要がある。

一方で、Karpenterを使用すると、termination-controllerがGraceful Shutdownを実施してくれる。


02. スケーリング

スケーリングの仕組み

Karpenterは、起動テンプレートを作成した上でAWS EC2フリートAPIをコールし、EC2 Nodeをスケールアウト/スケールアップする。

また反対に、EC2 Nodeをスケールイン/スケールダウンする。

Karpenterはバージョニングされてない独立した起動テンプレートを作成する。

そのため、残骸として残らないように、その都度起動テンプレートを削除する。

...

2023-11-30T08:28:56.735Z    INFO    controller.provisioner  found provisionable pod(s)  {...}
2023-11-30T08:28:56.735Z    INFO    controller.provisioner  computed new nodeclaim(s) to fit pod(s) {...}
2023-11-30T08:28:56.748Z    INFO    controller.provisioner  created nodeclaim   {...}

# 起動テンプレート作成
2023-11-30T08:28:56.957Z    DEBUG   controller.nodeclaim.lifecycle  created launch template {...}
2023-11-30T08:28:57.121Z    DEBUG   controller.nodeclaim.lifecycle  created launch template {...}
2023-11-30T08:28:57.297Z    DEBUG   controller.nodeclaim.lifecycle  created launch template {...}

# EC2 Node作成
2023-11-30T08:28:59.211Z    INFO    controller.nodeclaim.lifecycle  launched nodeclaim  {...}
2023-11-30T08:29:14.009Z    DEBUG   controller.disruption   discovered subnets  {...}
2023-11-30T08:29:33.910Z    DEBUG   controller.nodeclaim.lifecycle  registered nodeclaim    {...}
2023-11-30T08:29:46.264Z    INFO    controller.nodeclaim.lifecycle  initialized nodeclaim   {...}
2023-11-30T08:30:15.505Z    DEBUG   controller.disruption   discovered subnets  {...}

# 起動テンプレート削除
2023-11-30T08:32:58.872Z    DEBUG   controller  deleted launch template {...}
2023-11-30T08:32:59.027Z    DEBUG   controller  deleted launch template {...}
2023-11-30T08:32:59.299Z    DEBUG   controller  deleted launch template {...}

...

そのため、Nodeグループは不要 (グループレス) であり、Karpenterで指定した条件のNodeをまとめてスケーリングできる。

Karpenterを使用しない場合、クラウドプロバイダーのNode数は固定である。


スケーリングパラメーター

▼ スケーリングパラメーターとは

Karpenterは、様々な情報に基づいて、Nodeをスケーリングするか否かを決定する。

鳥の群れの動きをモデリングしたBoidsアルゴリズムに似たような方法で、スケーリング対象のNodeを選定する。

▼ Podのスケジューリングの可否

kube-schedulerから情報を取得し、新しいPodをNode上にスケジューリングできない状態 (Pending状態) を検知し、Nodeのスケジューリングを検討する。

新しいPodをスケジューリングできなくなる理由としては、Nodeの上限数超過やハードウェアリソース不足がある。

▼ コスト

より低コストになるようにEC2 Nodeを統合する。

以下のような条件の場合に、統合を発動する。

  • EC2 NodeにPodがおらず、これを削除できる
  • Podが特定のEC2 Nodeに偏っており、Podが少ないNodeから多いNodeに再スケジューリングさせても、Podを問題なく動かせる。
  • 現在のインスタンスタイプより低いインスタンスタイプにしても、問題なくPodを動かせる。

反対に、以下のような条件の場合には統合しない。

  • コントローラーが不明なPodがいる
  • Podの退避を拒否するPodDisruptionBudget (例:disruptionsAllowed=0) があり、統合を実行してしまうとPodDisruptionBudgetに違反する。
  • Podのmetadata.annotationsキー配下にkarpenter.sh/do-not-evictキーがある。
  • PodにAffinityがあり、統合を実行してしまうとAffinityに違反する。


削除対象のNode選定

▼ Finalizer

Nodeの削除はKarpenterが管理する。

Karpenter外から削除操作 (例:kubectl deleteコマンド) があったとして、Karpenterがこれを検知し、Nodeを削除する。

▼ Expiration

記入中...

▼ Consolidation

記入中...

▼ Drift

記入中...

▼ Interruption

記入中...


AWSリソースとの連携

▼ 起動テンプレート

Karpenterのkarpenterコントローラーは、起動テンプレートを作成した上で、EC2フリートAPIからEC2 Nodeを作成する。

執筆時点 (2023/11/04) 時点では、karpenterコントローラーは自身以外 (例:Terraform、など) で作成した起動テンプレートを参照できない。

不都合があって廃止した経緯がある。

▼ EC2フリート

▼ マネージドNodeグループ (有無に関係ない)

Karpenterは、マネージドNodeグループの有無に関係なく、Nodeをスケーリングできる。

マネージドNodeグループは静的キャパシティであり、KarpenterはマネージドNodeグループ配下のEC2のEC2フリートAPIを動的にコールする。

ただし、マネージドNodeグループで管理するNodeをKarpenterに置き換えるために、マネージドNodeグループ管理下のNodeを意図的にスケールインさせ、KarpenterにNodeをプロビジョニングさせる必要がある。


Karpenterとcluster-autoscaler

▼ Karpenterのいいところ

AWSの場合のみ、cluster-autoscalerの代わりにKarpenterを使用できる。

Karpenterでは、作成されるNodeのスペックを事前に指定する必要がなく、またリソース効率も良い。

そのため、必要なスペックの上限がわかっている場合はもちろん、上限を決めきれないような要件 (例:負荷が激しく変化するようなシステム) でも合っている。

karpenter_vs_cluster-autoscaler

▼ cluster-autoscalerのいいところ

cluster-autoscalerはクラウドプロバイダーによらずに使用できるが、Karpenterは執筆時点 (2023/02/26) では、AWS上でしか使用できない。

そのため、クラウドプロバイダーの自動スケーリング (例:AWS EC2AutoScaling) に関するAPIをコールすることになり、その機能が自動スケーリングに関するAPIに依存する。

一方でKarpenterは、EC2のグループ (例:AWS EC2フリート) に関するAPIをコールする。

そのため、より柔軟なNode数にスケーリングでき、マネージドNodeグループを介さない分Nodeの起動が早い。

karpenter_vs_cluster-autoscaler


水平/垂直スケーリング

▼ 対応するスケーリング

Karpenterは、現在のハードウェアリソースの使用量に応じて、Nodeを水平/垂直スケーリングする。

なお、Karpeneterでは垂直スケーリングを代わりに『deprovisioning』という。

▼ スケールアウト/スケールアップの場合

例えば、以下のような仕組みで、Nodeの水平/垂直スケーリングのスケールアウト/スケールアップを実行する。

Karpenterは、スケジューリングできないPod (Pending状態) が出現すると、スケールアウト/スケールアップを検討する。

(1)

Podが、Nodeの70%にあたるハードウェアリソースを要求する。

しかし、Nodeが1台では足りない。70 + 70 = 140%になるため、既存のNodeの少なくとも1.4倍のスペックが必要となる。

(2)

新しく決定したスペックで、Nodeを新しく作成する。

(3)

新しく作成したNodeにPodをスケジューリングさせる。また、既存のNodeが不要であれば削除する。

(4)

結果として、1台で2個のPodをスケジューリングさせている。

▼ スケールイン/スケールダウンの場合

Expiration、Drift、Consolidation、の順にNodeを検証し、削除可能なNodeを選ぶ。


03. セットアップ

AWS側

▼ Terraformの公式モジュール (1) の場合

terraform-aws-modules/iam/.../iam-assumable-role-with-oidcを使用する。

Kapenterのセットアップのうち、AWS側で必要なものをまとめる。

ここでは、Terraformの公式モジュールを使用する。

コマンド (例:eksctlコマンド) を使用しても良い。

module "iam_assumable_role_with_oidc_karpenter_controller" {

  source                        = "terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc"

  version                       = "<バージョン>"

  # karpenterコントローラーのPodに紐付けるIAMロール
  create_role                   = true
  role_name                     = "foo-karpenter-controller"

  # AWS EKS ClusterのOIDCプロバイダーURLからhttpsプロトコルを除いたもの
  provider_url                  = replace(module.eks.cluster_oidc_issuer_url, "https://", "")

  # AWS IAMロールに紐付けるIAMポリシー
  role_policy_arns              = aws_iam_policy.karpenter_controller.arn

  # karpenterコントローラーのPodのServiceAccount名
  # ServiceAccountは、Terraformではなく、マニフェストで定義した方が良い
  oidc_fully_qualified_subjects = [
    "system:serviceaccount:karpenter:karpenter"
  ]
}

resource "aws_iam_policy" "karpenter_controller" {
  name   = "foo-karpenter-controller-policy"
  policy = data.aws_iam_policy_document.karpenter_controller_policy.json
}

# karpenterコントローラーが操作できるEC2 Nodeを最小限にするために、特定のタグのみを持つEC2を指定できるようにする
# EC2NodeClassでユーザー定義のタグを設定し、karpenterコントローラーがEC2を操作できるようにしておく
data "aws_iam_policy_document" "karpenter_controller_policy" {

  statement {
    actions = [
      "pricing:GetProducts",
      "ec2:DescribeSubnets",
      "ec2:DescribeSpotPriceHistory",
      "ec2:DescribeSecurityGroups",
      "ec2:DescribeLaunchTemplates",
      "ec2:DescribeInstances",
      "ec2:DescribeInstanceTypes",
      "ec2:DescribeInstanceTypeOfferings",
      "ec2:DescribeImages",
      "ec2:DescribeAvailabilityZones",
      "ec2:CreateTags",
      "ec2:CreateLaunchTemplate",
      "ec2:CreateFleet"
    ]
    effect = "Allow"
    resources = [
      "*"
    ]
    sid = ""
  }

  statement {
    actions = [
      "ec2:TerminateInstances",
      "ec2:DeleteLaunchTemplate"
    ]
    # 特定のタグを持つEC2しか指定できない
    condition {
      test     = "StringEquals"
      # KarpenterのEC2NodeClassで挿入したEC2のタグを指定する
      variable = "ec2:ResourceTag/karpenter.sh/discovery"
      # 起動テンプレートからEC2 Nodeを作成する
      values = [
        "${module.eks.cluster_name}-karpenter"
      ]
    }
    effect = "Allow"
    resources = [
      "*"
    ]
    sid = ""
  }

  statement {
    actions = [
      "ec2:RunInstances"
    ]
    # 特定のタグを持つEC2しか指定できない
    condition {
      test     = "StringEquals"
      # KarpenterのEC2NodeClassで挿入したEC2のタグを指定する
      variable = "ec2:ResourceTag/karpenter.sh/discovery"
      values = [
        "${module.eks.cluster_name}-karpenter"
      ]
    }
    effect = "Allow"
    # 起動テンプレートからEC2 Nodeを作成する
    resources = [
      "arn:aws:ec2:*:<アカウントID>:launch-template/*"
    ]
    sid = ""
  }

  statement {
    actions = [
      "ec2:RunInstances"
    ]
    effect = "Allow"
    resources = [
      "arn:aws:ec2:*::snapshot/*",
      "arn:aws:ec2:*::image/*",
      "arn:aws:ec2:*:<アカウントID>:volume/*",
      "arn:aws:ec2:*:<アカウントID>:subnet/*",
      "arn:aws:ec2:*:<アカウントID>:spot-instances-request/*",
      "arn:aws:ec2:*:<アカウントID>:security-group/*",
      "arn:aws:ec2:*:<アカウントID>:network-interface/*",
      "arn:aws:ec2:*:<アカウントID>:instance/*"
    ]
    sid = ""
  }

  statement {
    actions = [
      "ssm:GetParameter"
    ]
    effect = "Allow"
    resources = [
      "arn:aws:ssm:*:*:parameter/aws/service/*"
    ]
    sid = ""
  }

  statement {
    actions = [
      "iam:PassRole"
    ]
    effect = "Allow"
    resources = [
      module.eks_managed_node_group.iam_role_arn
    ]
    sid = ""
  }

  statement {
    actions = [
      "eks:DescribeCluster"
    ]
    effect = "Allow"
    resources = [
      module.eks.cluster_arn
    ]
    sid = ""
  }

  statement {
    actions = [
      "iam:TagInstanceProfile",
      "iam:RemoveRoleFromInstanceProfile",
      "iam:GetInstanceProfile",
      "iam:DeleteInstanceProfile",
      "iam:CreateInstanceProfile",
      "iam:AddRoleToInstanceProfile"
    ]
    effect = "Allow"
    resources = [
      "*"
    ]
    sid = ""
  }
}

▼ Terraformの公式モジュール (2) の場合

terraform-aws-modules/eks/.../karpenterを使用する。

module "eks_iam_karpenter_controller" {
  source  = "terraform-aws-modules/eks/aws//modules/karpenter"
  version = "~> 19.18.0"

  cluster_name = module.eks.cluster_name

  create_iam_role = false

  create_instance_profile = false

  enable_karpenter_instance_profile_creation = true

  enable_spot_termination = false

  queue_managed_sse_enabled = false

  irsa_oidc_provider_arn = module.eks.oidc_provider_arn

  irsa_name = "foo-karpenter-controller"

  irsa_use_name_prefix = false

  # karpenterコントローラーのPodのServiceAccount名
  # ServiceAccountは、Terraformではなく、マニフェストで定義した方が良い
  irsa_namespace_service_accounts = [
    "karpenter:karpenter"
  ]

  # 特定のタグを持つEC2しか指定できない
  irsa_tag_key = "karpenter.sh/discovery"

  irsa_tag_values = [
    "${module.eks.cluster_name}-karpenter"
  ]

  iam_role_additional_policies = {
    AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  }

  iam_role_arn = module.eks_managed_node_group.worker_iam_role_arn
}