Prerequisite
설치하기 전에 미리 설정해야하는 것들이 있다.
Install helm
Karpenter는 helm chart를 통해서 k8s에 설치가 가능하다. helm에 대한 내용은 나중에 포스팅해보도록 하고 간단하게 kubernetes 리소스들을 정해진 template에 맞춰서 배포할 수 있게 해주는 도구라고만 알고 넘어가자.
helm이 일단 먼저 필요하다. Helm Install Docs
AWS 리소스 생성
Karpenter는 EC2 Instance를 생성해서 Node를 추가한다. AWS에 EC2를 생성할 수 있도록 AWS IAM Role을 생성해야한다.
또한 EC2는 VPC subnet에 Provision 되기 때문에 어떤 Subnet에 Node를 Provision할 지 Karpenter가 알아야한다.
IAM Role for Karpenter Node
resource "aws_iam_role" "karpenter" {
name = "KarpenterNodeRole-${aws_eks_cluster.main.name}"
assume_role_policy = data.aws_iam_policy_document.node_group_role.json
}
resource "aws_iam_role_policy_attachment" "karpenter_AmazonEKSWorkerNodePolicy" {
role = aws_iam_role.karpenter.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
}
resource "aws_iam_role_policy_attachment" "karpenter_AmazonEKS_CNI_Policy" {
role = aws_iam_role.karpenter.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
}
resource "aws_iam_role_policy_attachment" "karpenter_AmazonEC2ContainerRegistryReadOnly" {
role = aws_iam_role.karpenter.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}
resource "aws_iam_role_policy_attachment" "karpenter_AmazonSSMManagedInstanceCore" {
role = aws_iam_role.karpenter.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
IAM Role for Karpenter Controller
resource "aws_iam_role" "karpenter_controller" {
name = "KarpenterControllerRole-${aws_eks_cluster.main.name}"
assume_role_policy = data.aws_iam_policy_document.karpenter_controller_role.json
}
resource "aws_iam_role_policy_attachment" "karpenter_controller" {
role = aws_iam_role.karpenter_controller.name
policy_arn = aws_iam_policy.karpenter_controller.arn
}
data "aws_iam_policy_document" "karpenter_controller_role" {
version = "2012-10-17"
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
effect = "Allow"
condition {
test = "StringEquals"
variable = "${replace(aws_iam_openid_connect_provider.eks_oidc.url, "https://", "")}:sub"
values = ["system:serviceaccount:kube-system:karpenter"]
}
condition {
test = "StringEquals"
variable = "${replace(aws_iam_openid_connect_provider.eks_oidc.url, "https://", "")}:aud"
values = ["sts.amazonaws.com"]
}
principals {
identifiers = [aws_iam_openid_connect_provider.eks_oidc.arn]
type = "Federated"
}
}
}
resource "aws_iam_policy" "karpenter_controller" {
name = "KarpenterContollerPolicy-${aws_eks_cluster.main.name}"
policy = data.aws_iam_policy_document.karpenter_controller_policy.json
}
data "aws_iam_policy_document" "karpenter_controller_policy" {
statement {
actions = [
"ssm:GetParameter",
"ec2:DescribeImages",
"ec2:RunInstances",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeLaunchTemplates",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstanceTypeOfferings",
"ec2:DeleteLaunchTemplate",
"ec2:CreateTags",
"ec2:CreateLaunchTemplate",
"ec2:CreateFleet",
"ec2:DescribeSpotPriceHistory",
"pricing:GetProducts"
]
effect = "Allow"
resources = ["*"]
sid = "Karpenter"
}
statement {
actions = ["ec2:TerminateInstances"]
effect = "Allow"
condition {
test = "StringLike"
variable = "ec2:ResourceTag/karpenter.sh/nodepool"
values = ["*"]
}
resources = ["*"]
sid = "ConditionalEC2Termination"
}
statement {
actions = ["iam:PassRole"]
effect = "Allow"
resources = [aws_iam_role.karpenter.arn]
sid = "PassNodeIAMRole"
}
statement {
actions = ["eks:DescribeCluster"]
effect = "Allow"
resources = [aws_eks_cluster.main.arn]
sid = "EKSClusterEndpointLookup"
}
statement {
actions = ["iam:CreateInstanceProfile"]
effect = "Allow"
condition {
test = "StringEquals"
variable = "aws:RequestTag/kubernetes.io/cluster/${aws_eks_cluster.main.name}"
values = ["owned"]
}
condition {
test = "StringEquals"
variable = "aws:RequestTag/topology.kubernetes.io/region"
values = [var.region]
}
condition {
test = "StringLike"
variable = "aws:RequestTag/karpenter.k8s.aws/ec2nodeclass"
values = ["*"]
}
resources = ["*"]
sid = "AllowScopedInstanceProfileCreationActions"
}
statement {
actions = ["iam:TagInstanceProfile"]
effect = "Allow"
condition {
test = "StringEquals"
variable = "aws:ResourceTag/kubernetes.io/cluster/${aws_eks_cluster.main.name}"
values = ["owned"]
}
condition {
test = "StringEquals"
variable = "aws:ResourceTag/topology.kubernetes.io/region"
values = [var.region]
}
condition {
test = "StringEquals"
variable = "aws:RequestTag/kubernetes.io/cluster/${aws_eks_cluster.main.name}"
values = ["owned"]
}
condition {
test = "StringEquals"
variable = "aws:RequestTag/topology.kubernetes.io/region"
values = [var.region]
}
condition {
test = "StringLike"
variable = "aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass"
values = ["*"]
}
condition {
test = "StringLike"
variable = "aws:RequestTag/karpenter.k8s.aws/ec2nodeclass"
values = ["*"]
}
resources = ["*"]
sid = "AllowScopedInstanceProfileTagActions"
}
statement {
actions = [
"iam:AddRoleToInstanceProfile",
"iam:RemoveRoleFromInstanceProfile",
"iam:DeleteInstanceProfile"
]
effect = "Allow"
condition {
test = "StringEquals"
variable = "aws:ResourceTag/kubernetes.io/cluster/${aws_eks_cluster.main.name}"
values = ["owned"]
}
condition {
test = "StringEquals"
variable = "aws:ResourceTag/topology.kubernetes.io/region"
values = [var.region]
}
resources = ["*"]
sid = "AllowScopedInstanceProfileActions"
}
statement {
actions = ["iam:GetInstanceProfile"]
effect = "Allow"
resources = ["*"]
sid = "AllowInstanceProfileReadActions"
}
// for Spot instance
statement {
actions = [
"sqs:ReceiveMessage",
"sqs:GetQueueUrl",
"sqs:DeleteMessage"
]
effect = "Allow"
resources = ["*"]
sid = "AllowInterruptionQueueActions"
}
}
resource "aws_iam_service_linked_role" "spot" {
aws_service_name = "spot.amazonaws.com"
}
SQS for SPOT Instance
Spot Instance를 사용하기 위해서는 SQS를 사용해야한다. Spot Instance는 on-demand Instance와 달리 언제 Interruption이 발생해서 뺏길지 모르는 Instance이다. 이런 Interruption을 Queue로 관리하기 위해 SQS를 필요로한다.
resource "aws_sqs_queue" "karpenter_spot" {
name = "Karpenter-${aws_eks_cluster.main.name}-SpotInterruptionQueue"
message_retention_seconds = 300
sqs_managed_sse_enabled = true
}
Set tag to Subnet & Security Group
AWS에서 리소스를 관리하고 적용하는 방식을 대부분 리소스에 적힌 Tag로 관리한다.
Karpenter도 마찬가지다. Karpenter가 Node를 배포할 수 있는 Subnet 판단을 Subnet에 달린 tag를 보고 판단하고 Node가 사용할 Security Group도 Security Group에 달린 tag를 보고 판단한다.
"karpenter.sh/discovery" = ${EKS_CLUSTER_NAME} 으로 Subent과 Security group에 tag를 추가해야한다.
resource "aws_subnet" "private" {
//.. 중략
tags = {
"kubernetes.io/role/interna-elb" = 1
"karpenter.sh/discovery" = var.name.eks
}
}
// EKS 생성할 때 따로 SG를 추가해주지 않았기에 Node는 EKS Cluster의 SG를 동일하게 사용한다.
// 동적으로 EKS Cluster SG에 karpenter tag를 추가하도록 한다.
resource "null_resource" "add_karpenter_tag" {
count = length(var.vpc.subnet_ids.node)
provisioner "local-exec" {
command = <<EOT
aws ec2 create-tags --region ${var.region} --resources ${aws_eks_cluster.main.vpc_config[0].cluster_security_group_id} --tags Key=karpenter.sh/discovery,Value=${aws_eks_cluster.main.name}
EOT
}
}
Install Karpenter in EKS
필요한 AWS 리소스는 모두 생성했다. 이제 EKS에 Karpenter를 설치해보자.
Update aws-auth ConfigMap
먼저 aws-auth ConfigMap을 변경해줘야한다.
kubectl 명령으로 ConfigMap 수정을 진행한다. ${}로 표시한 값들은 각자 AWS 계정에 맞는 값을 입력해주면 된다.
kubectl edit configmap aws-auth -n kube-system
# Add section in aws-auth configmap
- groups:
- system:bootstrappers
- system:nodes
rolearn: arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}
username: system:node:{{EC2PrivateDNSName}}
Install using helm
Helm chart를 이용해서 Karpenter를 설치한다.
values.yaml
${}로 표시된 values는 각자가 설정한 AWS 리소스를 명시하면 된다.
# Service Account에서 사용할 IAM Role ARN 정의
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: ${KarpenterControllerRole}
# Replica count
replicas: 1
# Karpenter Controller가 Karpenter가 생성한 Node 위에서 실행되지 않도록
# 기본 설정한 default node group 위에서만 실행되도록 affinity를 설정해준다.
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: karpenter.sh/nodepool
operator: DoesNotExist
- key: eks.amazonaws.com/nodegroup
operator: In
values: ["default_node_group"]
# Controller의 리소스를 명시한다
controller:
resources:
requests:
cpu: 50m
memory: 512Mi
limits:
memory: 512Mi
# EKS cluster 이름과 위에서 생성한 SQS queue 이름을 입력한다.
settings:
clusterName: ${EKS_CLUSTER_NAME}
interruptionQueue: Karpenter-${EKS_CLUSTER_NAME}-SpotInterruptionQueue
Helm install
자신의 k8s 버전에 맞는 KARPENTER_VERSION을 사용하자. Compatibility 페이지에서 호환되는 버전을 확인하면 된다.
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter --version ${KARPENTER_VERSION} -n kube-system -f values.yaml
Result
정상적으로 설치가 끝나면 이렇게 Karpenter pod를 확인할 수 있다.
Set Node
Karpenter Controller 설치가 끝났으면 이제 Karpenter가 관리할 Node의 정의를 해줘서 EC2 Instance를 자동으로 Provision하여 Node를 추가하도록 해보자.
Node Class
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2023
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: ${EKS_CLUSTER_NAME}
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: ${EKS_CLUSTER_NAME}
role: "${KARPENTER_NODE_ROLE}"
Node pool
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
metadata:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 720h
terminationGracePeriod: 10m
requirements:
- key: karpenter.k8s.aws/instance-family
operator: In
values:
- t4g
- key: karpenter.k8s.aws/instance-cpu
operator: In
values:
- "2"
- "4"
- key: karpenter.k8s.aws/instance-memory
operator: In
values:
- "4096"
- "8192"
- "16384"
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
- on-demand
- key: topology.kubernetes.io/zone
operator: In
values:
- ap-northeast-2a
- key: kubernetes.io/arch
operator: In
values:
- arm64
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 10m
limit:
cpu: 20
memory: 80Gi
weight:
Result
Node Class와 Node Pool 설정까지 끝나면 Node에 컴퓨팅 자원이 부족할 때 Karpenter가 Node를 새로 추가한다.
Karpenter가 새로 추가한 Node에는 아래처럼 karpenter.k8s.aws/ 로 시작하는 label 들이 붙어있다.
Finished
Karpenter로 Node를 유연하게 Provisioning 되는 것까지 완료했다.
Side Project이다 보니 당연하게 비용을 최저로 나가게 하고 싶은 맘이 컸다. 그래서 node pool은 최대한 spot이 뜨게 하려고 node pool에 추가했고 t class로 node를 띄웠다.
Side Project에서 캐시카우가 점점 늘어나게 되면 고민을 해보겠지만 지금 당장 트래픽이 엄청 많은 것도 아니고 t class로도 문제없이 돌릴 수 있는 상황이기 때문에 최대함 비용이 안나가는 걸 최우선으로 했다.