Karpenter は AWS によって開発されている Kubernetes クラスタのノードを増減させる OSS。 標準の Cluster Autoscaler と比べて Auto Scaling Group を介さない柔軟で高速なプロビジョニングを行うことができる。 現状は EKS にしか対応していないが、他のクラウドについても対応できるようになっているらしい。
今回は CDK で EKS クラスタに Karpenter をインストールし replica 数を増減したときにクラスタがオートスケールすることを確認する。 全体のコードは GitHub にある。
Karpenter が用いる AWS リソースの作成
Karpenter が用いる Role や、スポットインスタンスの中断といった Event を受け取る SQS などのリソースを作成する CFn のテンプレートが 提供されているので、これを NestedStack として含める。
class KapenterResourcesStack extends cdk.NestedStack {
constructor(scope: Construct, id: string, props?: cdk.StackProps & {karpenterVersion: string, clusterName: string}) {
super(scope, id, props);
new cfninc.CfnInclude(this, 'KarpenterResources', {
templateFile: `karpenter_${props?.karpenterVersion}.yaml`,
parameters: {
"ClusterName": props?.clusterName
}
});
}
}
NodeRole の紐付け
上のテンプレートで作られた NodeRole をノードに紐づけるため ConfigMap aws-auth を更新する。
awsAuth.addRoleMapping(nodeRole, {
groups: ["system:bootstrappers", "system:nodes"],
username: "system:node:{{EC2PrivateDNSName}}"
})
ControllerRole の作成
この後 Helm Chart の方で作られる ServiceAccount karpenter が OIDC による認証で Assume できる IAM Role を作成する。 eksctl create cluster で渡される ClusterConfig の iam の部分に相当する。
const controllerRole = new iam.Role(this, 'KarpenterControllerRole', {
roleName: `${cluster.clusterName}-karpenter`,
path: "/",
assumedBy: new iam.WebIdentityPrincipal(
cluster.openIdConnectProvider.openIdConnectProviderArn,
{
// delay resolution to deployment-time to use tokens in object keys
"StringEquals": new cdk.CfnJson(this, 'KarpenterControllerRoleStringEquals', { value: {
[`${cluster.clusterOpenIdConnectIssuer}:aud`]: "sts.amazonaws.com",
[`${cluster.clusterOpenIdConnectIssuer}:sub`]: "system:serviceaccount:karpenter:karpenter"
}})
}
),
managedPolicies: [controllerPolicy]
})
Karpenter のインストール
Helm Chart で Karpenter をインストールする。 ControllerRole の Principal が間違っていて AssumeRole できなかったりすると更新に失敗してロールバックしてしまうので、 最初は wait: false で様子を見てみると良い。
cluster.addHelmChart('karpenter', {
chart: 'karpenter',
repository: 'oci://public.ecr.aws/karpenter/karpenter',
version: karpenterVersion,
namespace: 'karpenter',
createNamespace: true,
values: {
serviceAccount: {
name: 'karpenter', // added
annotations: {
"eks.amazonaws.com/role-arn": controllerRole.roleArn,
}
},
settings: {
aws: {
clusterName: cluster.clusterName,
defaultInstanceProfile: instanceProfileName,
interruptionQueueName: interruptionQueueName
}
},
controller: {
resources: {
requests: {
cpu: 1,
memory: "1Gi"
},
limits: {
cpu: 1,
memory: "1Gi"
}
}
}
},
wait: true
})
Provisioner と AWSNodeTemplate の設定
プロビジョニングするインスタンスの設定。
cluster.addManifest('DefaultProvisionerAndNodeTemplate', {
"apiVersion": "karpenter.sh/v1alpha5",
"kind": "Provisioner",
"metadata": {
"name": "default"
},
"spec": {
"requirements": [{
"key": "karpenter.sh/capacity-type",
"operator": "In",
"values": ["spot"]
}],
"limits": {
"resources": {
"cpu": 1000
},
},
"providerRef": {
"name": "default"
},
"consolidation": {
"enabled": true
}
}
}, {
"apiVersion": "karpenter.k8s.aws/v1alpha1",
"kind": "AWSNodeTemplate",
"metadata": {
"name": "default"
},
"spec": {
"subnetSelector": {
"Name": cluster.vpc.publicSubnets.join(",")
},
"securityGroupSelector": {
"aws-ids": cluster.clusterSecurityGroup.securityGroupId
}
}
})
動作確認
deployment の replica 数を増やすと Provisioner の requirements 通りスポットインスタンスが立ち上がり、削除するとシャットダウンした。
$ kubectl scale deployment inflate --replicas 5
$ kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller
...
2023-10-13T11:23:54.724Z INFO controller.machine.lifecycle launched machine {"commit": "322822a", "machine": "default-hbtqm", "provisioner": "default", "provider-id": "aws:///us-east-1a/i-04ae829209702f2cb", "instance-type": "c6i.2xlarge", "zone": "us-east-1a", "capacity-type": "spot", "allocatable": {"cpu":"7910m","ephemeral-storage":"17Gi","memory":"14162Mi","pods":"58"}}
2023-10-13T11:24:15.299Z DEBUG controller.machine.lifecycle registered machine {"commit": "322822a", "machine": "default-hbtqm", "provisioner": "default", "provider-id": "aws:///us-east-1a/i-04ae829209702f2cb", "node": "ip-10-18-7-7.ec2.internal"}
2023-10-13T11:24:22.399Z DEBUG controller.machine.lifecycle initialized machine {"commit": "322822a", "machine": "default-hbtqm", "provisioner": "default", "provider-id": "aws:///us-east-1a/i-04ae829209702f2cb", "node": "ip-10-18-7-7.ec2.internal"}
2023-10-13T11:24:15.299Z DEBUG controller.machine.lifecycle registered machine {"commit": "322822a", "machine": "default-hbtqm", "provisioner": "default", "provider-id": "aws:///us-east-1a/i-04ae829209702f2cb", "node": "ip-10-18-7-7.ec2.internal"}
2023-10-13T11:24:22.399Z DEBUG controller.machine.lifecycle initialized machine {"commit": "322822a", "machine": "default-hbtqm", "provisioner": "default", "provider-id": "aws:///us-east-1a/i-04ae829209702f2cb", "node": "ip-10-18-7-7.ec2.internal"}
2023-10-13T11:27:22.101Z DEBUG controller.awsnodetemplate discovered subnets {"commit": "322822a", "awsnodetemplate": "default", "subnets": ["subnet-0c890304d3ebc77a8 (us-east-1b)", "subnet-0e42f4251d016c74b (us-east-1a)"]}
ノードには次のような Label が付けられ、nodeSelector でこれらを指定することで条件に合うノードに Pod をスケジュールできる。
karpenter.k8s.aws/instance-category=r
karpenter.k8s.aws/instance-cpu=16
karpenter.k8s.aws/instance-encryption-in-transit-supported=false
karpenter.k8s.aws/instance-family=r6gd
karpenter.k8s.aws/instance-generation=6
...
karpenter.sh/do-not-disrupt: “true” annotation を付けることで、 実行中の Pod が Consolidation などによって evict されて他のノードに移動することを防ぐことができる。 Consolidation や Evict は kubectl get event で確認できる。