CDK で EKS クラスタに Karpenter をインストールし柔軟で高速なオートスケールを行う

awskubernetes

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
})

ProvisionerAWSNodeTemplate の設定

プロビジョニングするインスタンスの設定。

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 で確認できる。