EnJinnier

AWS EKS 및 카펜터를 이용한 워크로드 분리하기 본문

프로젝트

AWS EKS 및 카펜터를 이용한 워크로드 분리하기

공학도진니 2025. 5. 19. 23:15

졸업 프로젝트로 어플을 개발하고 있는데 문득 우리 서비스에 사용자가 많아진다면? 라는 생각이 들었다.

네트워크 부하뿐 아니라 서비스를 안정적으로 운용하기 위해 쿠버네티스(AWS EKS)를 이용하여

1. 파드위에서 어플을 실행하고,

2. 카펜터를 설치하여 어플의 cpu 사용이 급증하더라도 노드 스케일링을 자동화 시키며,

3. taints 옵션을 사용하여 기본 시스템이 올라가는 노드그룹(system), 카펜터만 올라가는 노드그룹(addon), 어플이 올라가는 노드그룹(app)을 엄격하게 워크로드를 분리시켜보기로 하였다.

 

(VPC, RDS 등은 이미 구성이 되어있음을 전제로 하며 이 문서에서는 쿠버네티스 설정 위주로 다룰 예정이다.)

 

1. 이미지 저장소(ECR) 구성

컨테이너 이미지 저장소로 AWS ECR을 사용한다. 이 레포지토리는 KMS로 암호화하며 보안을 위해 푸시할때 스캔이 되도록 했고, 또한 같은 태그의 이미지가 업로드 되지 않도록 하였다.

2. 클러스터 구성

본격적으로 eks 클러스터를 생성한다. 버전은 1.32로 해주었으며(나중에 카펜터 버전과 호환성을 맞추어야 한다) 외부에서 접속할 수 없도록 엔드포인트 액세스를 프라이빗으로 해주었다. 앞으로의 클러스터 설정은 모두 콘솔 또는 연결된 bastion server에서 할 예정이다.

 

1. bastion server와 클러스터 연결하기- 보안그룹 설정: 클러스터의 보안그룹에 소스가 bastion ec2의 보안그룹인 인바운드룰을 추가해야함- iam 액세스 추가: 보안주체: bastion-role, 다음과 같은 정책 추가 AmazonEKSAdminPolicy, AmazonEKSClusterAdminPolicy
- ec2서버에서 kubeconfig 구성하기

aws eks update-kubeconfig --region ap-northeast-2 --name ws-eks-cluster
>> Added new context arn:aws:eks:ap-northeast-2:491085395789:cluster/ws-eks-cluster to /home/ec2-user/.kube/config

 

위 세가지 조건을 모두 충족해야만 ec2와 클러스터가 연결되고 ec2에서 클러스터의 서비스에 접근하거나 수정할 수 있다.

 

2. 네임스페이스 생성

#네임스페이스 생성하기
kubectl create namespace <my-namespace-name>
kubectl create namespace app
kubectl create namespace karpenter
-> namespace/app created
#네임스페이스 조회하기
kubectl get namespaces
NAME              STATUS   AGE
karpenter               Active   2m1s

 

각 워크노드를 분리하기 위해 네임스페이스를 생성해준다.

3. 노드 그룹 생성 

  • addon 노드그룹 생성
    • 이름: addon
    • 노드 IAM 역할: ws-eks-node-role
    • 인스턴스 유형: t3.xlarge
    • 원하는크기:2 / 최소크기:2 / 최대크기: 3
    • 노드 라벨: key=nodegroup/value=addon
    • taints: key=node-role.kubernetes.io/ws-addon, effect=NoSchedule
  • system 노드 그룹 생성
    • 이름: system
    • 노드 IAM 역할: ws-eks-node-role
    • 인스턴스 유형: t3.medium(기본값)
    • 원하는크기:2 / 최소크기:2 / 최대크기: 3
    • 노드 라벨: key=nodegroup/value=system
  • 네임 스페이스: app (app 관련 모든 리소스는 이 네임스페이스에 생성)

 

4. Karpenter 구성

EKS 클러스터의 노드 관리를 위해 Karpenter를 설치한다.

카펜터는 addon 노드 그룹 및 karpenter 네임스페이스 위에서만 배포되어야 하고,

또한 카펜터가 생성하는 모든 노드는 app 네임스페이스를 가지도록 taints를 설정해야 하며

어플을 수행하는 모든 노드는 카펜터에 의해서만 생성되고 관리되어야 하는 것이 나의 의도였다.

  • 기본환경 세팅하기(카펜터 공식 가이드의 클라우드 포메이션 이용)
export KARPENTER_VERSION="1.2.3"
export TEMPOUT="$(mktemp)"
export CLUSTER_NAME="ws-eks-cluster"

curl -fsSL https://raw.githubusercontent.com/aws/karpenter-provider-aws/v"${KARPENTER_VERSION}"/website/content/en/preview/getting-started/getting-started-with-karpenter/cloudformation.yaml  > "${TEMPOUT}" \
&& aws cloudformation deploy \
  --stack-name "Karpenter-${CLUSTER_NAME}" \
  --template-file "${TEMPOUT}" \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides "ClusterName=${CLUSTER_NAME}"

 

  • oidc 공급자 생성 및 연결하기
# 클러스터의 OIDC 공급자 ID를 판단(검색하고 변수에 저장)
oidc_id=$(aws eks describe-cluster --name $CLUSTER_NAME --query "cluster.identity.oidc.issuer" --output text | cut -d '/' -f 5) 
echo $oidc_id
# IAM OIDC 제공자가 이미있는지 확인(출력이 반환되면 다음단계 건너뛰고 
aws iam list-open-id-connect-providers | grep $oidc_id | cut -d "/" -f4
# 반환되지 않으면 이 명령을 통해 OIDC ID 공급자 생성
eksctl utils associate-iam-oidc-provider --cluster $CLUSTER_NAME --approve

 

  • 카펜터 컨트롤러 Role 생성 
    • 카펜터Role을 생성하기 위해 이 역할에 들어갈 정책을 먼저 생성한다. 아래 코드를 복붙하여 권한으로 지정해주었다.
{
    "Statement": [
        {
            "Action": [
                "ssm:GetParameter",
                "ec2:DescribeImages",
                "ec2:RunInstances",
                "ec2:DescribeSubnets",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeLaunchTemplates",
                "ec2:DescribeInstances",
                "ec2:DescribeInstanceTypes",
                "ec2:DescribeInstanceTypeOfferings",
                "ec2:DescribeAvailabilityZones",
                "ec2:DeleteLaunchTemplate",
                "ec2:CreateTags",
                "ec2:CreateLaunchTemplate",
                "ec2:CreateFleet",
                "ec2:DescribeSpotPriceHistory",
                "pricing:GetProducts"
            ],
            "Effect": "Allow",
            "Resource": "*",
            "Sid": "Karpenter"
        },
        {
            "Action": "ec2:TerminateInstances",
            "Condition": {
                "StringLike": {
                    "ec2:ResourceTag/karpenter.sh/nodepool": "*"
                }
            },
            "Effect": "Allow",
            "Resource": "*",
            "Sid": "ConditionalEC2Termination"
        },
        {
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "arn:aws:iam::${myaccountnumber}:role/ws-eks-node-role",
            "Sid": "PassNodeIAMRole"
        },
        {
            "Effect": "Allow",
            "Action": "eks:DescribeCluster",
            "Resource": "arn:aws:eks:ap-northeast-2:${myaccountnumber}:cluster/${CLUSTER_NAME}",
            "Sid": "EKSClusterEndpointLookup"
        },
        {
            "Sid": "AllowScopedInstanceProfileCreationActions",
            "Effect": "Allow",
            "Resource": "*",
            "Action": [
            "iam:CreateInstanceProfile"
            ],
            "Condition": {
            "StringEquals": {
                "aws:RequestTag/kubernetes.io/cluster/${CLUSTER_NAME}": "owned",
                "aws:RequestTag/topology.kubernetes.io/region": "ap-northeast-2"
            },
            "StringLike": {
                "aws:RequestTag/karpenter.k8s.aws/ec2nodeclass": "*"
            }
            }
        },
        {
            "Sid": "AllowScopedInstanceProfileTagActions",
            "Effect": "Allow",
            "Resource": "*",
            "Action": [
            "iam:TagInstanceProfile"
            ],
            "Condition": {
            "StringEquals": {
                "aws:ResourceTag/kubernetes.io/cluster/${CLUSTER_NAME}": "owned",
                "aws:ResourceTag/topology.kubernetes.io/region": "ap-northeast-2",
                "aws:RequestTag/kubernetes.io/cluster/${CLUSTER_NAME}": "owned",
                "aws:RequestTag/topology.kubernetes.io/region": "ap-northeast-2"
            },
            "StringLike": {
                "aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass": "*",
                "aws:RequestTag/karpenter.k8s.aws/ec2nodeclass": "*"
            }
            }
        },
        {
            "Sid": "AllowScopedInstanceProfileActions",
            "Effect": "Allow",
            "Resource": "*",
            "Action": [
            "iam:AddRoleToInstanceProfile",
            "iam:RemoveRoleFromInstanceProfile",
            "iam:DeleteInstanceProfile"
            ],
            "Condition": {
            "StringEquals": {
                "aws:ResourceTag/kubernetes.io/cluster/${CLUSTER_NAME}": "owned",
                "aws:ResourceTag/topology.kubernetes.io/region": "ap-northeast-2"
            },
            "StringLike": {
                "aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass": "*"
            }
            }
        },
        {
            "Sid": "AllowInstanceProfileReadActions",
            "Effect": "Allow",
            "Resource": "*",
            "Action": "iam:GetInstanceProfile"
        },
        {
  "Sid": "AllowInterruptionQueueActions",
  "Effect": "Allow",
  "Resource": "*",
  "Action": [
    "sqs:DeleteMessage",
    "sqs:GetQueueUrl",
    "sqs:ReceiveMessage"
  ]
}
    ],
    "Version": "2012-10-17"
}

 

이제 이 정책을 카펜터 컨트롤러 롤에 붙여주고, 신뢰정책도 아래와 같이 수정해준다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/${oidc_id}"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "oidc.eks.ap-northeast-2.amazonaws.com/id/${oidc_id}:aud": "sts.amazonaws.com",
                    "oidc.eks.ap-northeast-2.amazonaws.com/id/${oidc_id}:sub": "system:serviceaccount:karpenter:karpenter"
                }
            }
        }
    ]
}

 

  • 태그 관리
    • 카펜터가 무사히 구성들을 찾을 수 있도록
    • 서브넷과 eks보안그룹에 key=karpenter.sh/discovery, value=cluster-name 추가
  • AWS auth 수정
    • aws-auth에 노드롤 추가
kubectl edit configmap aws-auth -n kube-system

    - rolearn: arn:aws:iam::${AWS_ACCOUNT_ID}:role/${karpenternoderolename}
      username: system:node:{{EC2PrivateDNSName}}
      groups:
        - system:bootstrappers
        - system:nodes

 

  • helm 설치 및 구성
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh 
./get_helm.sh 

#설치 및 버전 확인 
helm version

export KARPENTER_VERSION=1.2.3

#helm 레포 추가
helm repo add karpenter https://charts.karpenter.sh/
helm repo update

helm template karpenter oci://public.ecr.aws/karpenter/karpenter --version "${KARPENTER_VERSION}" --namespace "${KARPENTER_NAMESPACE}" \
    --set "settings.clusterName=${CLUSTER_NAME}" \
    --set "settings.interruptionQueue=${CLUSTER_NAME}" \
    --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/ws-karpenter-controller-role" \
    --set controller.resources.requests.cpu=1 \
    --set controller.resources.requests.memory=1Gi \
    --set controller.resources.limits.cpu=1 \
    --set controller.resources.limits.memory=1Gi > karpenter.yaml

 

# CRD 생성 후 배포(버전마다 파일명이 다를 수 있음 1.2.x ver)
kubectl create -f \
    "https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/pkg/apis/crds/karpenter.sh_nodepools.yaml"
kubectl create -f \
    "https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml"
kubectl create -f \
    "https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/pkg/apis/crds/karpenter.sh_nodeclaims.yaml"
kubectl apply -f karpenter.yaml

# 정상설치 확인
kubectl get po -n [namespace 이름]

 

이 코드를 보면 카펜터는 잘 생성됨을 확인할 수 있다.

이제부터 카펜터가 참고하는 조건 문서인 NodePool을 구성할 차례다.

//Provisioner와 AWSNodeTemplate 정책을 정의하고 생성
# NodePool(Karpenter가 EMR Spark 작업용 노드 생성 기준 정의)
# 파일로 저장
cat > nodepool.yaml << 'EOF'
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
  namespace: karpenter
spec:
  template:
    metadata:
      labels:
        nodegroup: emr
        eks.amazonaws.com/compute-type: emr
    spec:
      taints:
        - key: eks.amazonaws.com/compute-type
          value: emr
          effect: NoSchedule
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: kubernetes.io/os
          operator: In
          values: ["linux"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]
        - key: "node.kubernetes.io/instance-family"
          operator: In
          values: ["m5", "m5d", "c5", "c5d", "c4", "r4"]
        - key: "node.kubernetes.io/instance-cpu"
          operator: In
          values: ["4", "8", "16", "32"]
        - key: "topology.kubernetes.io/zone"
          operator: In
          values: ["us-east-1a", "us-east-1b"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      expireAfter: 720h # 30 * 24h = 720h
  limits:
    cpu: 40
    memory: 160Gi
  disruption:
    consolidationPolicy: WhenEmpty
    consolidateAfter: 90s
---
#ec2nodeclass(Karpenter가 노드 생성시 참고할 EC2 설정)
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
  namespace: karpenter
spec:
  role: "ws-eks-node-role" # replace with your cluster name
  amiSelectorTerms:
    - alias: "al2023@latest"
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "ws-eks-cluster" # replace with your cluster name
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "ws-eks-cluster" # replace with your cluster name
  blockDeviceMappings:
  - deviceName: /dev/xvda
    ebs:
      volumeSize: 100Gi
      volumeType: gp3
  tags:
    nodegroup: emr
    eks.amazonaws.com/compute-type: emr
EOF

# 파일 적용
kubectl apply -f nodepool.yaml

 

이렇게 설정해주면 끝!! 
이제 카펜터가 파드위에서 잘 돌아가면서, 카펜터가 새롭게 생성할 노드들도 일치하는 toleration이 붙은 파드들만 실행시킨다.

남은 기본 시스템 파드들은 자동으로 유일하게 taint값을 설정하지않은 system 노드그룹 위에 올라갈 예정이다.

이렇게 워크로드를 분리하여 쿠버네티스를 구성하고 카펜터로 자동화까지 하는 방법을 알아봤다.