[EKS] EKS환경에서 karpenter를 사용해보자! (feat. 페더레이션 계정 TroubleShooting)

vinca·2024년 3월 9일
0
post-thumbnail

Introduction

karpenter 내용의 실습까지 이전글에 첨부하게 될 경우, 너무 길어질 듯하여 karpenter의 실습 부분은 이 글에 따로 정리한다.✍🏻

먼저 짧게 다시, karpenter에 대해서 복기한 뒤, 실습을 수행해 보도록 하자.

karpenter란?

karpenter란 AWS사에서 개발한 쿠버네티스 클러스터 상의 CA를 수행하는 오픈소스 프로젝트이다.
AWS가 개발한 만큼 2024년3월 현재까지도 EKS 환경만을 지원한다.

이러한 카펜터는 CA와 비교하여 다음과 같은 특징이 있다.

빨라요! ⚡

기존 ASG의 오토스케일링 플로우를 따라가지 않으므로, 훨씬 빠르고 민첩하게 노드를 프로비저닝할 수 있다. 즉, ASG가 아닌 AWS Fleet API를 이용한다.⚡
심지어 파드를 노드에 배치할 때도 직접 Node-Binding을 하므로 scheduler 동작하지 않아 더 빠르다.

관리할 것도 없고, 동기화 문제도 안생겨요! 👍🏻

ASG의 관리부담 또한 줄고, "노드""EC2 인스턴스"간에 발생하는 관리주체가 달라 발생하는 동기화 문제 또한 해결할 수 있다.💱

저렴하고 낭비가 없어요! 💸

또한 노드의 크기(인스턴스의 종류)를 CA는 노드그룹에 종속되어 직접 노드 사이즈별로 로직을 짜줘야하는 반면, Karpenter는 적합하고, 저렴한 인스턴스를 자동으로 찾아 프로비저닝 한다는 특징이 있다.💸👌🏻

karpenter 환경 구성

karpenter 실습 환경 구성파일을 통해 CloudFormation 스택을 생성하고 진행한다.

Bastion으로 생성된 EC2에 접속 후 다음 과정을 진행해준다.

환경 변수 설정

// 환경 변수 정보 확인
export | egrep 'ACCOUNT|AWS_|CLUSTER' | egrep -v 'SECRET|KEY'

// 환경 변수 설정
export KARPENTER_VERSION=v0.30.0
export TEMPOUT=$(mktemp)

echo $KARPENTER_VERSION; echo $CLUSTER_NAME; echo $AWS_DEFAULT_REGION; echo $AWS_ACCOUNT_ID $TEMPOUT

Karpenter 관련 IAM, EC2 Instance Profile 생성

karpenter 또한 IAM 정책 및 역할 그리고, IRSA가 필요하다. 당연하게도, AWS의 리소스에 fleet API를 통해서 직접 접근하는 것이므로 필수적이다.

// CloudFormation 스택으로 IAM Policy, Role, EC2 Instance Profile 생성
// 약 3분 정도 소요
curl -fsSL https://raw.githubusercontent.com/aws/karpenter/"${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}"

두개의 역할이 생성되는데 첫번째 KarpenterNodeRole-myeks2는 Karpenter가 생성하는 각 노드에게 주어지는 역할이고, 두번째 myeks2-karpenter는 내 EKS 클러스터가 AWS 리소스에 접근하기 위한 역할이다(IRSA).

Amazon EKS 클러스터 생성

메타데이터 영역에서 태그를 설정해 줘야한다.

ClusterConfig 영역에 karepnter 태그를 붙여줘야 각 클러스터의 리소스에 태그가 부착되고, karpenter가 클러스터를 식별하고 대상을 찾아 확장할 수 있다.

노드 그룹은 m5.large로 최초 2대의 인스턴스를 사용했다.

// EKS 클러스터 생성 : myeks2 생성
// 약 19분 정도 소요
eksctl create cluster -f - <<EOF
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: ${CLUSTER_NAME}
  region: ${AWS_DEFAULT_REGION}
  version: "1.26"
  tags:
    karpenter.sh/discovery: ${CLUSTER_NAME}

iam:
  withOIDC: true
  serviceAccounts:
  - metadata:
      name: karpenter
      namespace: karpenter
    roleName: ${CLUSTER_NAME}-karpenter
    attachPolicyARNs:
    - arn:aws:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}
    roleOnly: true

iamIdentityMappings:
- arn: "arn:aws:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}"
  username: system:node:{{EC2PrivateDNSName}}
  groups:
  - system:bootstrappers
  - system:nodes

managedNodeGroups:
- instanceType: m5.large
  amiFamily: AmazonLinux2
  name: ${CLUSTER_NAME}-ng
  desiredCapacity: 2
  minSize: 1
  maxSize: 10
  iam:
    withAddonPolicies:
      externalDNS: true
EOF

Default Namespace로 적용

// Default Namespace로 위치 변경
kubectl ns default

EKS 클러스터 확인

// EKS 클러스터 배포 확인
eksctl get cluster
eksctl get nodegroup --cluster $CLUSTER_NAME
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
kubectl describe cm -n kube-system aws-auth

신규 터미널 - EKS Node Viewer 설치

// EKS Node Viewer 설치 :2분 이상 소요
// EKS 클러스터 생성 완료 후 작업
go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@v0.5.0

// EKS Node Viewer 접속
cd ~/go/bin && ./eks-node-viewer

ExternalDNS 설치

MyDomain=<자신의 도메인>
MyDnsHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnsHostedZoneId

curl -s -O https://raw.githubusercontent.com/cloudneta/cnaeblab/master/_data/externaldns.yaml
MyDomain=$MyDomain MyDnsHostedZoneId=$MyDnsHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -

kube-ops-view 설치

helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set env.TZ="Asia/Seoul" --namespace kube-system
kubectl patch svc -n kube-system kube-ops-view -p '{"spec":{"type":"LoadBalancer"}}'

kubectl annotate service kube-ops-view -n kube-system "external-dns.alpha.kubernetes.io/hostname=kubeopsview.$MyDomain"
echo -e "Kube Ops View URL = http://kubeopsview.$MyDomain:8080/#scale=1.5"

Karpenter 설치

Karpenter 환경 구성

// Karpenter 설치를 위한 환경 변수 설정 및 확인
export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.endpoint" --output text)"

export KARPENTER_IAM_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-karpenter"

echo $CLUSTER_ENDPOINT; echo $KARPENTER_IAM_ROLE_ARN

EC2 Spot fleet의 server-linked-role 확인

// EC2 Spot Fleet 사용을 위한 service-linked-role 생성 확인 (이미 생성됐다는 에러가 나와야 정상)
// An error occurred (InvalidInput) when calling the CreateServiceLinkedRole operation...

aws iam create-service-linked-role --aws-service-name spot.amazonaws.com || true

Warning: 만약 에러 메시지가 아닌 새롭게 생성하는 메시지가 나온다면 처음부터 다시 설치해야 합니다. 천천히 단계별로 과정을 진행해 주세요.

public.ecr.aws logout

// docker logout
docker logout public.ecr.aws

// helm registry logout
helm registry logout public.ecr.aws

Karpenter 설치

// karpenter 설치
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter --version ${KARPENTER_VERSION} --namespace karpenter --create-namespace \
  --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=${KARPENTER_IAM_ROLE_ARN} \
  --set settings.aws.clusterName=${CLUSTER_NAME} \
  --set settings.aws.defaultInstanceProfile=KarpenterNodeInstanceProfile-${CLUSTER_NAME} \
  --set settings.aws.interruptionQueueName=${CLUSTER_NAME} \
  --set controller.resources.requests.cpu=1 \
  --set controller.resources.requests.memory=1Gi \
  --set controller.resources.limits.cpu=1 \
  --set controller.resources.limits.memory=1Gi \
  --wait

두대의 파드가 각각의 노드에 배치되고, 1개의 ClusterIP 서비스가 생성된다.

더 상세한 정보는 krew를 설치하고 krew 명령어를 통해 확인할 수 있다.

Karpenter 설치 확인

// karpenter 설치 확인
kubectl get-all -n karpenter
kubectl get all -n karpenter
kubectl get cm -n karpenter karpenter-global-settings -o jsonpath={.data} | jq
kubectl get crd | grep karpenter

krew로 설치한 get-all 명령어를 통해서 확인해 보도록 하자.

다음과 같이 다양한 리소스가 설치되어 있다.

Krew? 👨‍👩‍👧‍👦
Krew는 Kubernetes 클러스터를 관리하는 데 도움이 되는 플러그인 매니저로, Kubernetes의 kubectl 명령어를 확장하여 다양한 추가 기능을 제공할 수 있도록 해준다.

Provisioner와 AWSNodeTemplate에 대한 CRD 또한 정의된 것을 확인할 수 있다. 이를 이용해서 다음 챕터에서 바로 정책을 정의하고 생성할 것이다.

Provisioner/AWSNodeTemplate 생성 및 확인

// Provisioner와 AWSNodeTemplate 정책을 정의하고 생성
cat <<EOF | kubectl apply -f -
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
  ttlSecondsAfterEmpty: 30
---
apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
  name: default
spec:
  subnetSelector:
    karpenter.sh/discovery: ${CLUSTER_NAME}
  securityGroupSelector:
    karpenter.sh/discovery: ${CLUSTER_NAME}
EOF

Provisioner와 정책과 AWSNodeTemplate 정책을 정의하고 생성하자.

Provisioner를 정의한 유형으로 values: ["spot"]으로 지정하여, karpenter로 프로비저닝되는 인스턴스를 스팟 인스턴스로 지정하였다.

이렇게 설정만 해두면 프로비저너는 가장 적합한 스펙의 인스턴스를 생성하는 Just-In-Time을 수행한다.👍🏻

또한, ttlSecondsAfterEmpty: 30가 지정되어 있는 것을 확인할 수 있는데, 이는 만약 노드가 비어있고 나서 30초가 흐른다면 해당 노드를 삭제하는 기능(Deprovisioning)이다.🕤

AWSNodeTemplate에는 단지 subnetSelectorsecurityGroupSelector만을 지정하고 있는데 새롭게 생성되는 EC2 인스턴스에 대해서 서브넷과 보안그룹을 지정해 준다.

Provisioner와 AWSNodeTemplate 확인

kubectl get awsnodetemplates,provisioners


Karpenter 실습 🚅

드디어 karpenter의 길고 긴 환경 구성작업이 끝났다.🥳
이제 karpenter를 본격적으로 사용해 보도록 하자.🙆🏻‍♂️

Scale-Out과 Scale-In 상황에 따라서 확인해보자.

Karpenter 동작 확인

디플로이먼트 배포

테스트용 디플로이먼트를 배포하는데, replicas가 0이다.
즉, 디플로이먼트를 배포하고 파드를 생성하지 않는다.

💡karpenter 테스트를 위해 컨테이너의 생성과 삭제에 대한 시간은 거의 무시하기 위해 다음과 같이 진행한다.

pause라는 가벼운 컨테이너를 사용하고, terminationGracePeriodSeconds를 0으로 지정하여 빠르게 종료되도록 한다.

// 디플로이먼트 배포
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 0
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - name: inflate
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
          resources:
            requests:
              cpu: 1
EOF

// 디플로이먼트 확인
kubectl get deploy

Scale-Out 확인

// 테스트용 디플로이먼트 생성
// replicas 수정 및 로그 확인 (replicas 0 -> 5)
kubectl scale deployment inflate --replicas 5

kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller | grep provisioner


// 스팟 인스턴스 확인
kubectl get node --label-columns=eks.amazonaws.com/capacityType,karpenter.sh/capacity-type,node.kubernetes.io/instance-type

Scale-In 확인

// 디플로이먼트 삭제 및 로그 확인 (ttlSecondsAfterEmpty 30)
kubectl delete deployment inflate; date

kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller | grep deprovisioning

이번에는 기존처럼 자동으로 최적화된 인스턴스를 생성하는 것이 아닌, 직접 인스턴스를 지정해서 생성하도록 해보자. ✅

Provisioner 삭제

// 기존 provisioner를 삭제
kubectl delete provisioners default

Provisioner 생성

// 신규 provisioner 생성
cat <<EOF | kubectl apply -f -
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec:
  consolidation:
    enabled: true
  labels:
    type: karpenter
  limits:
    resources:
      cpu: 1000
      memory: 1000Gi
  providerRef:
    name: default
  requirements:
    - key: karpenter.sh/capacity-type
      operator: In
      values:
        - on-demand
    - key: node.kubernetes.io/instance-type
      operator: In
      values:
        - c5.large
        - m5.large
        - m5.xlarge
EOF

디플로이먼트 배포

// 디플로이먼트 배포
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 0
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - name: inflate
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
          resources:
            requests:
              cpu: 1
EOF

// 디플로이먼트 확인
kubectl get deploy

Scale-Out 확인

// 테스트용 디플로이먼트 생성
// replicas 수정 및 로그 확인 (replicas 0 -> 12)
kubectl scale deployment inflate --replicas 12

kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller | grep provisioner


// karpenter로 생성한 노드 확인
kubectl get node -l type=karpenter

kubectl get node --label-columns=eks.amazonaws.com/capacityType,karpenter.sh/capacity-type

짠! 다음과 같이 노드가 3대 더 증설된 것을 확인할 수 있고,c5.large, m5.large, m5.xlarge 중 가장 적합한 인스턴스인 m5.large로 생성된 것을 확인할 수 있다.

Consolidation 확인 ⚙️

이러한 Karpenter는 Consolidation을 통해 노드 구성의 최적화를 진행한다.

Consolidation는 통합/강화의 의미로 노드에 구성된 파드 정보를 통해서 노드를 최적의 환경으로 재구성하는 것이다.

그 예로, 파드의 수량을 12 ➡️ 7로 감소시켜 보자.

// replicas 수정 및 로그 확인 (replicas 12 -> 7)
kubectl scale deployment inflate --replicas 7

kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller | grep consolidation -A 3
  • 콘솔리데이션(Consolidation) 결과
    m5.xlarge에서 c5.large로 파드가 감소함에 따라 자동으로 노드의 크기 또한 Consolidation 되었다.

TroubleShoothing 💣

처음 실행을 하니 카펜터가 동작하지 않았고, 계속해서 Pending 상태로 파드가 멈춰있었다. 🤯

로그 분석 📜

이러한 이유를 알기 위해서 karpenter controller의 전체 로그를 찍어보았다.

kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller

처음에는 다음과 같이, 프로비저닝이 대기중이라는 디버깅만 찍혀나왔다.

[root@myeks2-bastion-EC2 ~]# kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller
2024-03-10T04:48:44.877Z        DEBUG   controller      discovered version      {"commit": "637a642", "version": "v0.30.0"}
2024-03-10T04:48:44.877Z        DEBUG   controller      Registering 1 clients   {"commit": "637a642"}
2024-03-10T04:48:44.877Z        DEBUG   controller      Registering 2 informer factories        {"commit": "637a642"}
2024-03-10T04:48:44.877Z        DEBUG   controller      Registering 3 informers {"commit": "637a642"}
2024-03-10T04:48:44.877Z        DEBUG   controller      Registering 5 controllers       {"commit": "637a642"}
2024-03-10T04:48:44.878Z        INFO    controller      Starting server {"commit": "637a642", "path": "/metrics", "kind": "metrics", "addr": "[::]:8000"}
2024-03-10T04:48:44.878Z        INFO    controller      Starting server {"commit": "637a642", "kind": "health probe", "addr": "[::]:8081"}
2024-03-10T04:48:44.979Z        INFO    controller      attempting to acquire leader lease karpenter/karpenter-leader-election...
        {"commit": "637a642"}
2024-03-10T04:58:50.835Z        DEBUG   controller.deprovisioning       waiting on cluster sync {"commit": "637a642"}
2024-03-10T04:58:51.836Z        DEBUG   controller.deprovisioning       waiting on cluster sync {"commit": "637a642"}

시간이 지나자 다음과 같은 오류가 나왔다. 😲

2024-03-10T04:59:23.584Z        ERROR   controller      Reconciler error        {"commit": "637a642", "controller": "machine.lifecycle", "controllerGroup": "karpenter.sh", "controllerKind": "Machine", "Machine": {"name":"default-ws4pf"}, "namespace": "", "name": "default-ws4pf", "reconcileID": "112747a4-1c68-449e-8bd8-e3bc202806af", "error": "creating machine, creating instance, with fleet error(s), UnauthorizedOperation: You are not authorized to perform this operation. User: arn:aws:sts::...:assumed-role/myeks2-karpenter/1710046124308773194 is not authorized to perform: ec2:RunInstances on resource: arn:aws:ec2:ap-northeast-2:...:instance/* with an explicit deny in a service control policy. Encoded authorization failure message:

여기서 주목해서 봐야할 부분은 바로 여기이다.

You are not authorized to perform this operation. User: arn:aws:sts::009946608368:assumed-role/myeks2-karpenter/1710046124308773194 is not authorized to perform: ec2:RunInstances on resource: arn:aws:ec2:ap-northeast-2:009946608368:instance/* with an explicit deny in a service control policy.

해석하자면...

  1. 너는 권한이 없다.
  2. ec2:Runlnstances를 하기위한 권한이 없다.
    (서울 지역의 전체 인스턴스에 대해서.)
  3. SCP(service control policy)에 의해 제한되었다.

엥? 이게 무슨 말인가?
SCP는 AWS Organization에서 제한하는 사항인데, 나에게 권한이 없다고?

부리나케 내 IAM 계정을 생성해 준 팀원에게 SCP 제한 사항을 걸어둔 게 있는지 물어봤다.

SCP 걸어둔 게 있나요?

❓ 제가 못보는데요?

😭 저런, AWS에서 우리 팀에게 준 계정은 실제 root 계정이 아니라 AWS Organization에서 Administrator 권한을 부여해서 준 페더레이션 사용자 계정이였다.

페더레이션 사용자

🧑🏻‍💼 페더레이션 사용자?
페더레이션 사용자란 AWS Identity and Access Management (IAM)이 아닌 외부의 신원 관리 서비스를 통해 AWS 관리 콘솔에 액세스하는 사용자이다.
AWS SSO(Single Sign-On) 또는 기업의 신원 관리 시스템을 사용하여 AWS 리소스에 액세스 하게된다.

페더레이션 사용자로 로그인 하면 다음과 같은 계정 문구를 확인할 수 있다.

결국 나는 내 AWS 계정을 통해서 AWS SSO로 액세스한 "페더레이션 사용자"로 AdministratorAccess 권한이 할당되어 있지만, 실제로 수행할 수 있는 작업은 SCP에 따라 결정된다. ✅

다시말해, root 계정이 아니기에 팀원과 나 모두 SCP에 접근할 수 있는 권한이 없었다.😭

정리하자면 아래 그림과 같다.

EC2 지금까지는 잘 생성했는데?

ec2:Runlnstances 오류라니? 🤔

다만 EC2를 지금까지 생성하는데는 문제가 없었고, 페더레이션 사용자로부터 IAM 사용자 계정을 생성하여 여러 작업을 수행하는 것 또한 문제가 없었다.

SCP는 AWS Organizations의 모든 계정이나 특정 계정에 적용할 수 있다.
즉, myeks2-karpenter 역할에 적용된 SCP는 해당 역할이 ec2:RunInstances 작업을 수행하는 것을 명시적으로 거부되고 있었다.

  • myeks2-karpenter 역할이 가진 정책

💡 즉, 내 페더레이션 사용자 계정 자체는 SCP의 영향을 받지 않지만, myeks2-karpenter 역할은 해당 SCP의 제약을 받고 있었다.

인 줄 알았는데,,,그냥 계정 자체에 SCP가 걸려있었다.

문의 결과

이틀 뒤 한통의 전화가 걸려왔다. 📞

🧑🏻‍💼 : ■■■인데요. ○○○씨 맞으세요? 지금 한번 되는 지 확인해 보시겠어요?
🧑🏻‍🎓 : 아 잠시만요. 네 지금 되네요!
🧑🏻‍💼 : 네~
🧑🏻‍🎓 : 아 혹시 제가 너무 궁금해서 그러는데 어떻게 되어있던 건가요?
🧑🏻‍💼 : SCP 제한이 AWS organization에 걸려있는 거 아시죠?
🧑🏻‍🎓 : 네 알고있습니다.
🧑🏻‍💼 : 거기서 인스턴스 생성할 때, 너무 높은 인스턴스는 비용이 초과할 수도 있어서 계정 단위로 최대 xlarge타입 까지만 생성되도록 막아뒀어요.

그렇다. 페더레이션 계정 자체에 SCP가 걸려 있었기에 myeks2-karpenter Role 또한 EC2 인스턴스 타입을 xlarge 급만 실행 가능했던 것이였다.

💡 karpenter는 "가장 적절한" 인스턴스 타입을 Just-In-Time으로 증설하므로 , c7i.2xlarge 인스턴스 타입을 요청했지만 거부당한 것이다.

결과 🎈

SCP 제약 조건을 없애주셨고, karpenter는 가장 적절한 인스턴스라 생각하는 c7i.2xlarge 타입을 잘 요청해서 생성할 수 있게 되었다.

profile
붉은 배 오색 딱다구리 개발자 🦃Cloud & DevOps

0개의 댓글