Karpenter への期待
この記事は、Showcase Gig Advent Calendar 2021 11日目の記事です。
AWS re:Invent 2021 で発表のあった open-source Kubernetes cluster autoscaling project である Karpenter の紹介となります。
また、一部 HCL や yaml で書かれたコードが記載されていますが、動作を補償するものではないので、あしからず。
Showcase Gig SRE チームの 内海 (Chiguhagu) です。
主に O:der Platform (こちらの詳細は 2日目の記事 にて)の新規開発に携わっています。
「会社で Advent Calendar やるんだぁ。すごいなぁ、じゃっ、頑張って!」と静観を決め込もうとしたら、巻き込まれました。
「はい、書きます...」
前書き
突然ですが、みなさんは Managed Kubernetes Cluster のオートスケール、どうやって実現していますか?
そうですよね、やはり有名どころでは Cluster Autoscaler が挙げられますね。
弊社でも前述の Cluster Autoscaler を使用し、クラスタのスケーリングをコントロールしています。
これはクラスタに対し Pod を作成したものの「リソース不足で Pod がスケジュールされない!」といった際、
自動で Node をスケールアウトしてくれる Addon となっています。(もちろん、逆のスケールインも勝手に実行してくれます。)
Karpenter の登場
ところが先日、 AWS re:Invent にて Karpenter という同じくクラスタのスケーリングをコントロールしてくれる Addon が GA しました。
「わざわざ Cluster Autoscaler があるのに発表されたという事は、何かしら優位性があるに違いない...」
と、ドキュメントを読もうとしたところ、開発者の方が比較動画をアップロードしていたので、そちらを先行して視聴。
ザックリと以下のようなことを言っているようでした。
ワークロードに適したインスタンスを追加、スケジューリングしてくれる。
必要に応じて、Node のサイズ、個数を最適化(さらには、Podの再スケジュールまで)してくれる。
うん、楽しそう。ドキュメント読んで、実際に試してみよう!
ちなみに、こちら が公式のドキュメント
だそうです。Just-in-time って表現が気になりますね。
Karpenter を試してみよう (準備編)
ちゃんと 実行編 もあります。
公式のドキュメントに Getting Started があったので、そちらを見つつ Karpenter を調理していきます。
ドキュメント上では cloudformation や eksctl コマンドを使用し環境の初期構築をしています。
が、私自身、上記のサービスやツールを使用することを禁じられた家系の出身のため、今回はそれらを Terraform と manifest に置き換えて、環境を構築していきます。
ここは自己満なところがあるので、読み飛ばしてかまいません。
EKS Cluster の準備
大部分は割愛します。
OIDC 周りの設定は、主題からそれてしまいますが、書いておきます。
あとあと使うので。
data "tls_certificate" "main" {
url = aws_eks_cluster.eks_cluster.identity[0].oidc[0].issuer
}
resource "aws_iam_openid_connect_provider" "main" {
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.main.certificates[0].sha1_fingerprint]
url = aws_eks_cluster.eks_cluster.identity[0].oidc[0].issuer
}
cloudformation の分解
次に、ドキュメントに書かれている cloudformation を分解し、Terraform で書き直します。
ここでは3つのリソースを作っているようです。
Karpenter が作成した EC2 リソースの使用する Instance Profile (KarpenterNodeInstanceProfile)
1 の Profile で使用する IAM Policy
EKS 上に展開された Karpenter が AWS の操作をする際に必要な Policy
はい、これなら簡単ですね。
data "aws_iam_policy_document" "karpenter" {
statement {
actions = [
# Write Operations
"ec2:CreateLaunchTemplate",
"ec2:CreateFleet",
"ec2:RunInstances",
"ec2:CreateTags",
"iam:PassRole",
"ec2:TerminateInstances",
# Read Operations
"ec2:DescribeLaunchTemplates",
"ec2:DescribeInstances",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstanceTypeOfferings",
"ec2:DescribeAvailabilityZones",
"ssm:GetParameter",
]
resources = ["*"]
effect = "Allow"
}
}
resource "aws_iam_policy" "karpenter" {
name = "KarpenterControllerPolicy"
policy = data.aws_iam_policy_document.karpenter.json
}
data "aws_iam_policy_document" "karpenter_node_assume" {
statement {
actions = ["sts:AssumeRole"]
effect = "Allow"
principals {
identifiers = ["ec2.amazonaws.com"]
type = "Service"
}
}
}
resource "aws_iam_role" "karpenter_node" {
name = "KarpenterNodeRole"
assume_role_policy = data.aws_iam_policy_document.karpenter_node_assume.json
}
resource "aws_iam_role_policy_attachment" "karpenter_node_eks_worker_node" {
role = aws_iam_role.karpenter_node.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
}
resource "aws_iam_role_policy_attachment" "karpenter_node_eks_cni" {
role = aws_iam_role.karpenter_node.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
}
resource "aws_iam_role_policy_attachment" "karpenter_node_ec2_container_registry_read_only" {
role = aws_iam_role.karpenter_node.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}
resource "aws_iam_role_policy_attachment" "karpenter_node_ssm_managed_instance_core" {
role = aws_iam_role.karpenter_node.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "karpenter_node" {
name = "KarpenterNodeInstanceProfile"
role = aws_iam_role.karpenter_node.name
}
aws-auth ファイルへの追記
こちらは ↑ のドキュメント節の後半部分ですね。
Karpenter が作成した Node がクラスタに参加できるように Role レベルで許可を出してあげます。
最終的には下記のような manifest がクラスタに適用されれば OK です。
apiVersion: v1
kind: ConfigMap
metadata:
name: aws-auth
namespace: kube-system
data:
mapRoles: |
- rolearn: arn:aws:iam::xxxxxxxxxxxx:role/${EKS_YOUR_WORKER_ROLE}
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:bootstrappers
- system:nodes
- rolearn: arn:aws:iam::xxxxxxxxxxxx:role/KarpenterNodeRole
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:bootstrappers
- system:nodes
今回でいう 2 つ目の role の権限ですね。
OIDC Role の作成と Service Account の作成
ドキュメント的には こちら
AWS の IAM Role リソースと kubernetes クラスタ上の Service Account リソースがお互い依存関係にあるため、ちょっと面倒なところです。
事前にルールを決めておけば、別に困る事はないんですけどね。
こちらが Terraform に変換したもの。
data "aws_iam_policy_document" "karpenter_trust_relationships" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
effect = "Allow"
condition {
test = "StringEquals"
variable = "${replace(aws_iam_openid_connect_provider.main.url, "https://", "")}:sub"
values = ["system:serviceaccount:karpenter:karpenter"]
}
principals {
identifiers = [aws_iam_openid_connect_provider.main.arn]
type = "Federated"
}
}
}
resource "aws_iam_role" "karpenter" {
name = "OidcKarpenterRole"
assume_role_policy = data.aws_iam_policy_document.karpenter_trust_relationships.json
}
resource "aws_iam_role_policy_attachment" "karpenter" {
role = aws_iam_role.karpenter.name
policy_arn = aws_iam_policy.karpenter.arn
}
EKS クラスタを作成するところで書いた OIDC を有効にするリソースの情報を使用しています。
aws_iam_openid_connect_provider.main.url
aws_iam_openid_connect_provider.main.arn
そして、今度は Service Account のマニフェスト。namespace と service account 名はドキュメントから拝借しちゃいましょ。
apiVersion: v1
kind: Namespace
metadata:
name: karpenter
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: karpenter
namespace: karpenter
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::xxxxxxxxxxxx:role/OidcKarpenterRole
spot インスタンスの有効化
割愛!すでにやってあったので!
Karpenter のインストール
普段は Helm とか使わないのですが、禁じられているわけではないので、今回は使います。
Service Account の準備は終わっているので、ドキュメント 通りにコマンドを実行し、マニフェストを適用しましょう。
そうして、できあがったリソースがこちらです。
% k get po -n karpenter
NAME READY STATUS RESTARTS AGE
karpenter-controller-59f6dcdfdb-dkqsq 1/1 Running 0 30s
karpenter-webhook-6c8bdff8bf-nmv4l 1/1 Running 0 30s
生まれたての Pod です。かわいいですね。
ちなみに Node はこんな感じ、Karpenter とデフォルトで展開される Addon 達が動いています。
% k get no
NAME STATUS ROLES AGE VERSION
ip-172-31-43-248.ap-northeast-1.compute.internal Ready <none> 2m v1.21.5-eks-bc4871b
Karpenter のログレベルの変更
ドキュメント ではログレベルの変更の仕方も書いてありました。
変更前に kubectl describe cm で Config Map の中身をのぞいて見ましょう!
Data
====
zap-logger-config:
----
{
"level": "info",
"development": false,
"disableStacktrace": true,
"disableCaller": true,
"sampling": {
"initial": 100,
"thereafter": 100
},
"outputPaths": ["stdout"],
"errorOutputPaths": ["stderr"],
"encoding": "console",
"encoderConfig": {
"timeKey": "time",
"levelKey": "level",
"nameKey": "logger",
"callerKey": "caller",
"messageKey": "message",
"stacktraceKey": "stacktrace",
"levelEncoder": "capital",
"timeEncoder": "iso8601"
}
}
へぇ、zap 使ってんだ。
(zap = Go 言語のメジャーな構造化ログライブラリ。弊社ではまだ logrus が優勢です。)
debug に変更することで詳細のログが確認可能となります。
Karpenter の Custom Resource を準備
ドキュメントでは ここ 。
spec.provider.instanceProfile パラメータだけ、先程 Terraform で作成したものに置き換えるのを忘れないでください。
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: default
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
limits:
resources:
cpu: 1000
provider:
instanceProfile: KarpenterNodeInstanceProfile
ttlSecondsAfterEmpty: 30
こちらを適用すれば、環境構築は完了です。
Karpenter を試してみよう (実行編)
ここまでで、最低限 Karpenter へ入門できる環境の構築が完了しました。
あとは 残りのドキュメント通り に進めて、最後に terraform destroy するだけです。
deployment リソースの適用
サンプルとして何もしない Pod を管理する Deployment リソースを追加します。
そして、replica 数を 0 -> 5 に変更。 手順は ドキュメント通り ですので割愛します。
コア数を 1 消費する Pod ですので、ドキュメント通りに準備していれば、リソース不足になるはず。
クラスタへの適用後、Node を確認すると...
% k get no
NAME STATUS ROLES AGE VERSION
ip-172-31-23-133.ap-northeast-1.compute.internal Unknown <none> 20s
ip-172-31-43-248.ap-northeast-1.compute.internal Ready <none> 38m v1.21.5-eks-bc4871b
えっ、誰? 知らない人(Node)がいるよぉ。
先程、 kubectl get node で確認した時には存在していない Node が新規作成されていました。
Pod のログも見てみましょう。
% kubectl logs -f -n karpenter $(kubectl get pods -n karpenter -l karpenter=controller -o name)
2021-12-09T09:33:24.688Z INFO Successfully created the logger.
2021-12-09T09:33:24.688Z INFO Logging level set to: info
{"level":"info","ts":1639042404.797704,"logger":"fallback","caller":"injection/injection.go:61","msg":"Starting informers..."}
2021-12-09T09:33:24.820Z INFO controller starting metrics server {"commit": "6984094", "path": "/metrics"}
I1209 09:33:24.819887 1 leaderelection.go:243] attempting to acquire leader lease karpenter/karpenter-leader-election...
I1209 09:33:24.861045 1 leaderelection.go:253] successfully acquired lease karpenter/karpenter-leader-election
2021-12-09T09:33:24.920Z INFO controller.controller.provisioning Starting EventSource {"commit": "6984094", "reconciler group": "karpenter.sh", "reconciler kind": "Provisioner", "source": "kind source: /, Kind="}
2021-12-09T09:33:24.920Z INFO controller.controller.provisioning Starting Controller {"commit": "6984094", "reconciler group": "karpenter.sh", "reconciler kind": "Provisioner"}
2021-12-09T09:33:24.921Z INFO controller.controller.termination Starting EventSource {"commit": "6984094", "reconciler group": "", "reconciler kind": "Node", "source": "kind source: /, Kind="}
2021-12-09T09:33:24.921Z INFO controller.controller.termination Starting Controller {"commit": "6984094", "reconciler group": "", "reconciler kind": "Node"}
2021-12-09T09:33:24.921Z INFO controller.controller.node Starting EventSource {"commit": "6984094", "reconciler group": "", "reconciler kind": "Node", "source": "kind source: /, Kind="}
2021-12-09T09:33:24.921Z INFO controller.controller.node Starting EventSource {"commit": "6984094", "reconciler group": "", "reconciler kind": "Node", "source": "kind source: /, Kind="}
2021-12-09T09:33:24.921Z INFO controller.controller.node Starting EventSource {"commit": "6984094", "reconciler group": "", "reconciler kind": "Node", "source": "kind source: /, Kind="}
2021-12-09T09:33:24.921Z INFO controller.controller.node Starting Controller {"commit": "6984094", "reconciler group": "", "reconciler kind": "Node"}
2021-12-09T09:33:24.921Z INFO controller.controller.metrics Starting EventSource {"commit": "6984094", "reconciler group": "karpenter.sh", "reconciler kind": "Provisioner", "source": "kind source: /, Kind="}
2021-12-09T09:33:24.921Z INFO controller.controller.metrics Starting Controller {"commit": "6984094", "reconciler group": "karpenter.sh", "reconciler kind": "Provisioner"}
2021-12-09T09:33:24.922Z INFO controller.controller.counter Starting EventSource {"commit": "6984094", "reconciler group": "karpenter.sh", "reconciler kind": "Provisioner", "source": "kind source: /, Kind="}
2021-12-09T09:33:24.922Z INFO controller.controller.counter Starting EventSource {"commit": "6984094", "reconciler group": "karpenter.sh", "reconciler kind": "Provisioner", "source": "kind source: /, Kind="}
2021-12-09T09:33:24.922Z INFO controller.controller.counter Starting Controller {"commit": "6984094", "reconciler group": "karpenter.sh", "reconciler kind": "Provisioner"}
2021-12-09T09:33:25.041Z INFO controller.controller.metrics Starting workers {"commit": "6984094", "reconciler group": "karpenter.sh", "reconciler kind": "Provisioner", "worker count": 10}
2021-12-09T09:33:25.041Z INFO controller.controller.termination Starting workers {"commit": "6984094", "reconciler group": "", "reconciler kind": "Node", "worker count": 10}
2021-12-09T09:33:25.041Z INFO controller.controller.node Starting workers {"commit": "6984094", "reconciler group": "", "reconciler kind": "Node", "worker count": 10}
2021-12-09T09:33:25.041Z INFO controller.controller.counter Starting workers {"commit": "6984094", "reconciler group": "karpenter.sh", "reconciler kind": "Provisioner", "worker count": 10}
2021-12-09T09:33:25.041Z INFO controller.controller.provisioning Starting workers {"commit": "6984094", "reconciler group": "karpenter.sh", "reconciler kind": "Provisioner", "worker count": 10}
2021-12-09T09:38:09.855Z INFO controller.provisioning Starting provisioner {"commit": "6984094", "provisioner": "default"}
2021-12-09T09:38:09.855Z INFO controller.provisioning Waiting for unschedulable pods {"commit": "6984094", "provisioner": "default"}
2021-12-09T09:41:18.608Z INFO controller.provisioning Batched 5 pods in 1.076234859s {"commit": "6984094", "provisioner": "default"}
2021-12-09T09:41:18.728Z INFO controller.provisioning Computed packing of 1 node(s) for 5 pod(s) with instance type option(s) [c1.xlarge c3.2xlarge c4.2xlarge c5d.2xlarge c5.2xlarge c5a.2xlarge c5n.2xlarge m3.2xlarge t3.2xlarge m4.2xlarge m5.2xlarge m5zn.2xlarge t3a.2xlarge m5d.2xlarge m5a.2xlarge m6i.2xlarge m5n.2xlarge m5dn.2xlarge m5ad.2xlarge c3.4xlarge] {"commit": "6984094", "provisioner": "default"}
2021-12-09T09:41:21.608Z INFO controller.provisioning Launched instance: i-0f7a1b39577ba24d5, hostname: ip-172-31-23-133.ap-northeast-1.compute.internal, type: t3a.2xlarge, zone: ap-northeast-1d, capacityType: spot {"commit": "6984094", "provisioner": "default"}
2021-12-09T09:41:21.639Z INFO controller.provisioning Bound 5 pod(s) to node ip-172-31-23-133.ap-northeast-1.compute.internal {"commit": "6984094", "provisioner": "default"}
2021-12-09T09:41:21.640Z INFO controller.provisioning Waiting for unschedulable pods {"commit": "6984094", "provisioner": "default"}
ふむふむ、最後のこのあたりでスケジュールされていない Pod を確認し、新規で Node (EC2) インスタンスを構築しているみたいですね。
2021-12-09T09:38:09.855Z INFO controller.provisioning Waiting for unschedulable pods {"commit": "6984094", "provisioner": "default"}
2021-12-09T09:41:18.608Z INFO controller.provisioning Batched 5 pods in 1.076234859s {"commit": "6984094", "provisioner": "default"}
2021-12-09T09:41:18.728Z INFO controller.provisioning Computed packing of 1 node(s) for 5 pod(s) with instance type option(s) [c1.xlarge c3.2xlarge c4.2xlarge c5d.2xlarge c5.2xlarge c5a.2xlarge c5n.2xlarge m3.2xlarge t3.2xlarge m4.2xlarge m5.2xlarge m5zn.2xlarge t3a.2xlarge m5d.2xlarge m5a.2xlarge m6i.2xlarge m5n.2xlarge m5dn.2xlarge m5ad.2xlarge c3.4xlarge] {"commit": "6984094", "provisioner": "default"}
2021-12-09T09:41:21.608Z INFO controller.provisioning Launched instance: i-0f7a1b39577ba24d5, hostname: ip-172-31-23-133.ap-northeast-1.compute.internal, type: t3a.2xlarge, zone: ap-northeast-1d, capacityType: spot {"commit": "6984094", "provisioner": "default"}
AWS Management Console から確認したところ、Managed Node 外でインスタンスが作成されたようでした。
deployment リソースの削除
次に deployment を削除してみたところ、Cluster Autoscaler と同様に 不要な Node を判別して削除してくれました。
当時のログを下記に抜粋しました。
2021-12-09T09:45:51.738Z INFO controller.node Added TTL to empty node {"commit": "6984094", "node": "ip-172-31-23-133.ap-northeast-1.compute.internal"}
2021-12-09T09:46:21.757Z INFO controller.node Triggering termination after 30s for empty node {"commit": "6984094", "node": "ip-172-31-23-133.ap-northeast-1.compute.internal"}
2021-12-09T09:46:21.787Z INFO controller.termination Cordoned node {"commit": "6984094", "node": "ip-172-31-23-133.ap-northeast-1.compute.internal"}
2021-12-09T09:46:21.951Z INFO controller.termination Deleted node{"commit": "6984094", "node": "ip-172-31-23-133.ap-northeast-1.compute.internal"}
しっかりと Cordoned してから Node の削除をしているようです。
お掃除
最後はしっかりとリソースの削除をしておきましょう。
EKS は Master 部分の時間課金 + Node リソースの課金が発生します。
今回は Terraform でリソースを作成したので terraform destroy で終わりです。
入門して感じた事
動画だけではイメージが湧かなかったので、公式ドキュメントにのっとって(?)Karpenter へ入門してみました。
入門することで EKS 環境における Cluster Autoscaler との差分について見えてきました気がします。
0 -> 1 のスケールが必要な Node の準備が manifest で完結する。
EKS 環境で Cluster Autoscaler を使用する場合、AGS (Auto Scaling Group) が必要となります。
通常の 1 以上の インスタンス数からのスケールを行うだけならば、Cluster Autoscaler 側の設定だけで済むのですが、 0 -> 1 へのスケールを行うためには AGS 、つまりは AWS リソース側への追加設定が必要になります。
Terraform + Managed Node Group の場合はさらに面倒になります。 Terraform リソース間に依存関係ができてしまい。適用する順番をコントロールする必要が出てきてしまうのです。
このように管理に手間が増えてしまうのが難点だったのですが、 今回の入門のユースケースのように、マニフェスト1つで 0 -> 1 スケーリングをコントロールできるのは便利だと思いました。
同期的な Pod のスケジューリング
Cluster Autoscaler を使用した場合、「Node のスケールアウト」と「Pod のスケジューリング」は非同期で行われます。
(前者は Cluster Autoscaler に、後者は kube-scheduler により行われる。)
しかし、Karpenter ではスケーリングだけではなく、スケジュールされなかった Pod のスケジュールまで行ってくれるようです。
これにより、スケールアウト時に発生する非同期処理のオーバーヘッドを削減し、より柔軟なスケールが実現できそうです。
(といっても通常のオートスケールではギリギリ間に合わなかったこともないので、恩恵を感じづらい気もしています。)
冒頭でも取り上げた、
というのは、この機能を指しているんですね。
Node Group を使用したスケーリングからの開放
最初に Karpenter をプロビジョニングするためのコンピューティングリソースは必要となりますが、それ以降のスケールアウトでは、適切なサイズで常に最適化を行いながらリソースの調整してくれます。
これは従来の管理に比べ、より無駄なリソースの使用を抑え、インフラコストの削減が期待できます。
気を付けること
ただし、Cluster Autoscaler を使用する場合に比べ、Node のコントロールがマニフェスト側に寄ってしまう点は気を付けなければならないと思っています。
単一のチームでソフトウェア開発からリソースの調整、インフラコストの管理までを行っている場合は大きく問題にならないはずですが、
そうでない場合は、Audit や Policy をしっかり効かせないと、予想だにしないリソース増強などが行われ、コスト管理者の頭を悩ませることになりそうです。
まとめ
GA したと言っても v0.5 の OSS です。
実際に本番環境で使用するには、まだまだ知見やレポートが足りないため、しばらくは検証しつつ、様子見しようと思っています。
ただし、↑で上げたようなメリットは無視できないため、いつでも Cluster Autoscaler から乗り換えられるように準備を進めておこうとも思っています。
(個人的には EKS Addons のパーティメンバーに入ってくれると、クラスタのバージョンアップ作業がさらに楽になってうれしいなぁ。CRD があるからキビしいかなぁ。)
以上、Showcase Gig Advent Calendar 2021 11日目 の記事でした!
明日もお楽しみに!
この記事が気に入ったらサポートをしてみませんか?