본문으로 건너뛰기

험난한 Kubernetes 전환기 (2) - Amazon EKS와 Karpenter

· 약 18분
Austin Lee
DevOps Engineer @ Allganize

작년부터 시작한 Kubernetes 전환 작업이 조금씩 진행되고 있습니다.
이번에는 Azure Kubernetes Service에 이어 Amazon EKS 환경을 구성하며 겪었던 이슈들을 정리해 보았습니다.

2번 타자, AWS

AWS는 사내 주 클라우드 환경으로 사용하고 있습니다. 이미 기존에도 많은 서비스가 AWS 기반이었고, 아무래도 AWS 환경이 많은 개발자에게 익숙하기 때문에 Azure보다는 비교적 수월하게 진행할 수 있을 것이라 기대했습니다.

다만 앞에서 진행한 Azure 환경이 더 우선순위가 높았고, AWS에서는 4월부터 Infra Team EKS 클러스터를 시작으로 Kubernetes 환경을 구축하기 시작했습니다.

Infra Team EKS

Infra Team EKS 클러스터는 다음과 같은 목적을 가지고 구성되었습니다.

  1. 역할 분리를 명확하게 하고 싶었습니다.
  2. 팀에서 사용하는 도구들은 특성상 거의 내부에서만 사용되고 외부에는 제한적으로만 노출되기 때문에, 분리해 두는 게 좋다고 생각했습니다.
  3. 파편화되어 있던 인프라 팀의 관리 범위를 줄이고 싶기도 했습니다.

여기서 배운 점들을 활용하여 이후 EKS 클러스터를 더 쉽게 구성하고 싶었기 때문에, 최대한 많은 것을 조사하고 테스트하는 과정이 있었습니다.

AWS 환경으로 넘어오며 달라진 점이 있다면, Azure 진영에서는 클러스터 구성을 할 때 Terraform 코드를 직접 작성하거나 모듈도 제한적으로 사용했지만, AWS는 모듈이 잘 정리되어 있어 활용하기가 매우 좋았습니다. 특히 terraform-aws-eks 모듈의 Karpenter 예제1를 변형하여 사용했는데, 약간의 수정만으로 초기 환경을 쉽게 구성할 수 있었습니다.

다만 Karpenter는 추가 작업이 필요했는데 이는 나중에 설명하겠습니다.

VPC 구성하기

EKS 클러스터를 구성할 때 가장 신경 썼던 부분은 네트워크 구성이었습니다.
우선 VPC 설정이 필요했는데, 이는 VPC 모듈을 활용하여 구성했습니다. VPC 모듈은 잘 사용하면 네트워크 관련 설정을 상당히 압축할 수 있고, 옵션도 매우 많기 때문에 README를 중심으로 자세하게 확인해 보시는 것을 추천합니다.

EKS에서 사용할 Public, Private 서브넷을 모두 정의하고, 비용 효율성을 위해 Single NAT 구성을 채택했습니다. 또한 이건 Karpenter 예제에도 있는 내용인데, AWS Load Balancer Controller와 Karpenter를 위한 서브넷 태그 설정이 필요합니다.

public_subnet_tags = {
"kubernetes.io/role/elb" = 1
}

private_subnet_tags = {
"kubernetes.io/role/internal-elb" = 1
"karpenter.sh/discovery" = var.name
}

특별한 경우가 아니라면 변경될 일이 없겠지만 중요한 부분입니다. 이 부분이 없으면 Load balancer나 Karpenter 노드가 배포되며 자동으로 서브넷을 찾아 연결하지 못하는 문제가 발생합니다.

트래픽 흐름 구성

저희는 기본적으로 2가지 시나리오를 구상했습니다.

  1. 외부 트래픽: AWS Load Balancer Controller로 생성된 ALB(Application Load Balancer) 사용
    • 외부 트래픽 > ALB > Ingress > Service > Pod
  2. 내부 트래픽: NGINX Ingress Controller + NLB(Network Load Balancer) 사용
    • 내부 트래픽 > NLB > NGINX Ingress > Service > Pod

Azure 환경을 구성할 때 외부 로드 밸런서는 L7 로드 밸런서를 사용해야 한다는 점2을 배웠기 때문에 ALB를 사용하였고, 내부 트래픽은 간편하게 NLB + NGINX Ingress Controller를 사용할 수 있도록 했습니다.

이외에 사내 VPN을 내부 통신에 연동하는 작업도 있었지만 이 글의 범위를 벗어나기 때문에 생략하겠습니다.

Karpenter 개선하기

기본적인 구성을 하고, 테스트 앱을 배포해 보면서 네트워크 테스트와 기본적인 Karpenter 동작을 확인했습니다.
이대로 문제가 없다고 생각했지만, 본격적으로 실제 서비스 배포 과정에서 부족한 부분도 있었고, 시행착오도 있었습니다. 그리고 그 중심에는 예제 코드대로만 구성했던 Karpenter가 있었습니다.

Karpenter란?

앞에서 몇 번 언급하긴 했지만, 여기서부터 제대로 정리해 두려 합니다.
Karpenter는 Kubernetes 클러스터의 노드를 동적으로 프로비저닝하는 오토스케일러입니다. 기존의 Cluster Autoscaler와 달리, Karpenter의 노드 관리 방식은 더 효율적이고 유연합니다.

  1. Karpenter는 지속적으로 Pod의 상태를 관찰하여 스케줄링된 현 상태와 스케줄링이 되지 않는 리소스를 파악합니다. 그리고 그 데이터를 바탕으로 조건 내의 최적의 VM을 선정하여 프로비저닝합니다. 그래서 최대한 버리는 리소스를 줄이고, Spot 인스턴스를 활용하는 등 비용 면에서도 이점을 가집니다.
  2. 또한 Karpenter는 EC2 API를 직접 호출하여 인스턴스를 프로비저닝하기 때문에 속도도 매우 빠릅니다. 실제로 일반적인 상황에서 Karpenter가 필요한 인스턴스를 판단하고 프로비저닝하는데 1분이 걸리지 않았습니다. Azure에서 사용했던 Cluster Autoscaler가 3~4분 정도의 시간이 걸렸던 것을 감안하면 매우 빠르다고 할 수 있습니다.

저희 회사는 현재도 클라우드 비용으로 인한 지출이 많아 비용에 매우 민감했고, EKS를 쓴다면 Karpenter는 꼭 사용해야 한다는 팀원들의 긍정적인 의견도 있었습니다.

Karpenter의 동작 방식

Karpenter 구조

자세한 구조는 더 복잡하겠지만, Karpenter의 구조는 다음과 같이 생각할 수 있습니다.

  • 노드를 조율하는 Karpenter controller가 있고,
  • Karpenter controller가 정의된 EC2NodeClass와 NodePool을 바탕으로 Self-managed 노드 풀을 정의하고 관리합니다.

이러한 Karpenter의 동작 방식을 처음에는 이해하지 못했습니다. 특히 오해했던 내용은 크게 2가지였습니다.

  • Karpenter controller가 배치되는 곳은 관리 노드 풀이라는 것, 그래서 시스템에서 관리하고, Auto-scaling도 Managed 노드 풀의 설정을 따라간다는 점
  • Karpenter가 생성하는 노드는 별도의 NodePool과 EC2NodeClass 리소스 정의를 참조하고, 관리 노드 풀과는 관련이 없다는 점
    • 참고로 이렇게 Karpenter 노드 풀이 Self-managed 형태로 관리되는 것은 의도된 것이고, System에서 관리되지 않는 것이 정상입니다.
    • 하지만 예제의 리소스는 일반적인 케이스로 정의되어 있기 때문에 최적화와 수정 작업이 필요했습니다.

앱을 배포하는 과정에서 Karpenter 노드가 볼륨 설정과 Auto-scaling이 원활하게 작동하지 않는다는 것을 발견했을 때, 처음에는 Managed 노드 풀의 설정을 변경하면 해결될 것이라 생각해서 혼선이 있었습니다. 자세한 조사를 하고 나서야 고쳐야 할 부분을 알 수 있었고, 문제를 해결하기 위해 여러 가지 설정을 변경했습니다.

gp3 볼륨 사용하기

gp3 볼륨은 차세대 EBS 볼륨으로, 기존 gp2 볼륨에 비해 IOPS가 높고 비용도 더 저렴합니다. 그래서 보통은 gp3 볼륨을 사용하는 것이 좋은데, EKS는 별도 설정을 하지 않을 경우 기본적으로 gp2 볼륨을 사용합니다.
따라서 gp3 볼륨을 명시적으로 사용하도록 설정이 필요했습니다.

위에서 설명한 것처럼 Karpenter 노드에 gp3 볼륨을 사용하도록 설정하려면 해당 노드가 참고하는 EC2NodeClass 객체를 수정해야 합니다.
예를 들어, 다음과 같이 설정해야 합니다.

blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3
iops: 3000
throughput: 125
encrypted: true
deleteOnTermination: true
- deviceName: /dev/xvdb
ebs:
volumeSize: 20Gi
volumeType: gp3
iops: 3000
throughput: 125
encrypted: true
deleteOnTermination: true

여기서 /dev/xvda, /dev/xvdb는 각각 파일 시스템 파티션을 의미하는데,

  • /dev/xvda는 루트 파일 볼륨,
  • /dev/xvdb는 데이터 파일 볼륨을 의미합니다.

이 볼륨은 적절하게 설정해 주어야 하는데, 보통은 루트 파일 볼륨인 dev/xvda를 사용하지만 Bottlerocket AMI처럼 볼륨을 분리하여 사용하는 경우 Docker 이미지를 가져오는 작업 등에 데이터 파일 볼륨인 dev/xvdb를 사용할 수 있습니다.3

참고로 Managed 노드 풀에 gp3 볼륨을 설정하는 방법은 다음과 같습니다.

block_device_mappings = {
xvda = {
device_name = "/dev/xvda"
ebs = {
volume_size = 10
volume_type = "gp3"
iops = 3000
throughput = 125
encrypted = true
delete_on_termination = true
}
}
xvdb = {
device_name = "/dev/xvdb"
ebs = {
volume_size = 20
volume_type = "gp3"
iops = 3000
throughput = 125
encrypted = true
delete_on_termination = true
}
}
}

IAM Role 설정하기

Karpenter가 제대로 작동하기 위해서는 여러 IAM Role 설정이 필요합니다.

  1. EBS CSI 드라이버에 IRSA Role을 부여해야 합니다.
    • 이 Role이 없으면 Karpenter 노드 생성, 또는 노드가 사라지고 잔여 볼륨을 정리할 때 문제가 생깁니다.
    • 실제로 EBS 볼륨을 사용하는 PVC가 Karpenter 노드가 없어진 뒤에도 회수되지 않는 문제가 있었습니다.
    • Role은 모듈을 사용하여 간단히 생성할 수 있습니다.4
module "ebs_csi_irsa_role" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"

role_name = "${var.name}-ebs-csi-role"
attach_ebs_csi_policy = true

oidc_providers = {
ex = {
provider_arn = module.eks.oidc_provider_arn
namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"]
}
}

tags = var.tags
}
  1. Karpenter controller에 IRSA Role을 부여해야 합니다.
    • 이 Role은 Karpenter가 EC2 인스턴스나 Launch template 등을 생성하고 실행할 수 있도록 해 줍니다.
    • Karpenter 모듈에서 enable_irsatrue로 설정하고 관련 설정을 추가하면 됩니다.
  2. Karpenter node IAM role에 EBS CSI 드라이버 정책을 추가해야 합니다.
    • 이를 통해 Karpenter 노드에서 EBS 볼륨을 생성하고 사용할 수 있습니다.
module "karpenter" {
create = var.enable_karpenter
source = "terraform-aws-modules/eks/aws//modules/karpenter"

enable_irsa = true
irsa_oidc_provider_arn = module.eks.oidc_provider_arn
irsa_namespace_service_accounts = [
"kube-system:karpenter"
]

node_iam_role_additional_policies = {
AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
AmazonEBSCSIDriverPolicy = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"
}

tags = var.tags
}
  1. 끝으로 Karpenter 서비스 계정에 역할 ARN을 지정해야 합니다.
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/karpenter-controller-role

Spot 인스턴스 사용하기

Karpenter 노드가 Auto-scaling을 하지 못하는 문제를 확인해 보니, Spot 인스턴스를 생성하지 못하고 있었습니다. Karpenter는 Spot 인스턴스를 우선적으로 사용하고, 별도 권한이 없으면 Spot 인스턴스를 생성하지 못하기 때문입니다.

이를 해결하기 위해 두 가지 설정을 추가했습니다:

  1. 서비스 연결 역할로 Spot 인스턴스 권한을 부여했습니다.
resource "aws_iam_service_linked_role" "spot" {
aws_service_name = "spot.amazonaws.com"
}
  1. NodePool 리소스에 karpenter.sh/capacity-type 속성으로 요구사항을 추가했습니다. 아래는 제한 없이 모든 인스턴스 타입을 사용할 수 있도록 설정한 예시입니다.
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand", "reserved"]

마치며

이렇게 모든 설정을 완료한 뒤에는 실제 서비스 배포에도 문제가 없었습니다.
무작정 도입했다가 제대로 사용을 못 할 수도 있었는데, 미리 문제를 발견하고 해결할 수 있어 다행이었다고 생각하네요.

당연히 현재 Karpenter 설정이 완벽한 것은 아닙니다. 몇 가지 설정을 추가한 것 외에는 크게 변경을 거치지 않았기 때문에 운영 환경에서는 인스턴스 타입이나 아키텍처 등을 제한하는 설정을 추가해야 할 것이고, 궁극적으로는 Weighted NodePool 설정을 추가하는 등5 좀 더 최적화된 설정이 필요할 것입니다.

Karpenter를 포함해 EKS를 구성하는 과정에서 그래도 Azure에 비해 참고할 자료가 많아졌지만, 여전히 운영 환경은 다르다는 것을 한 번 더 느끼기도 했습니다.
팀 차원에서 Cloud 환경에 Kubernetes 적용이 처음이라는 점, 그리고 여기에 모두 적지는 않았지만 기존 서비스와 동일한 조건을 가져가기 위해 설정할 부분도 많았기 때문입니다. 그래도 EKS 클러스터 하나가 온전히 구성되었고, 이를 바탕으로 운영 환경 EKS를 구성하는 데는 상대적으로 적은 시간이 걸렸습니다.

이제 새로 배포하는 서비스는 EKS 환경에서 배포를 기본으로 하고, 기존 서비스들도 옮기는 작업을 진행하고 있습니다.
또한 팀 내에서도 Argo CD, Harbor 등 기존에 사용하지 못했던 도구들을 검증하고 적용하여 Cloud Native 환경을 구성하는 것이 최종적인 목표입니다.

다소 속도가 느려질 수는 있지만, 안정적이고 탄력적인 환경을 구성할 수 있도록 여유를 가지고 진행하려 합니다.

그 외 참고 자료

Footnotes

  1. https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/examples/karpenter

  2. https://blog.haulrest.me/blog/250225-01/#azure-load-balancer%EC%99%80-azure-application-gateway

  3. https://awslabs.github.io/data-on-eks/docs/bestpractices/preload-container-images

  4. https://github.com/terraform-aws-modules/terraform-aws-iam/tree/master/examples/iam-role-for-service-accounts-eks

  5. https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools