Skip to content

EKS 스케일링과 모니터링

AWS SSM Session Manager

AWS SSM Session Manager는 웹 터미널처럼 동작하는 원격 접속 도구다. SSH 키나 별도의 bastion 서버 없이 EC2 인스턴스에 접속할 수 있다.

# SSM 관리 대상 인스턴스 목록 조회
aws ssm describe-instance-information \
--query "InstanceInformationList[*].{InstanceId:InstanceId, Status:PingStatus, OS:PlatformName}" \
--output text

# session-manager-plugin 을 통한 인스턴스 접속
aws ssm start-session --target <INSTANCE_ID>
  • IAM 사용자가 Web Shell 이나 AWS CLI 로 접근할 때 별도 인증 없이 접속된다. IAM 자격증명이 이미 되어 있기 때문이다.
  • Systems Manager 설정으로 세션 활동 로깅이 가능하다.
  • 접속하는 대상 노드에 대해서만 Port Forwarding 이 가능하다.

노드에 직접 SSH로 접속하는 대신 SSM을 사용하는 이유는, SSH 포트(22번)를 열지 않아도 되어 보안이 더 강하고, 접속 이력이 CloudWatch나 S3에 자동으로 기록되기 때문이다.

이번 실습에서는 워커노드가 private 서브넷에 배치되므로 직접 SSH 접근이 불가능하다. 때문에 노드 그룹 IAM Role에 AmazonSSMManagedInstanceCore 정책을 부여해서 SSM으로만 접근하도록 구성했다.

# eks.tofu - 노드 그룹 IAM Role에 SSM 정책 추가
iam_role_additional_policies = {
  AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

EKS Addons

EKS 애드온은 클러스터 운영에 필수적인 컴포넌트들을 AWS가 관리형으로 제공하는 방식이다. 직접 Helm으로 설치하는 것과 달리, 버전 관리와 업데이트를 AWS 콘솔이나 CLI로 통일해서 처리할 수 있다.

이번 실습에서는 다음 애드온들을 설치했다.

# eks.tofu
addons = {
  coredns = {
    most_recent = true
  }
  kube-proxy = {
    most_recent = true
  }
  vpc-cni = {
    most_recent    = true
    before_compute = true
  }
  metrics-server = {
    most_recent = true
  }
  external-dns = {
    most_recent = true
  }
}

most_recent = true로 설정하면 해당 Kubernetes 버전에서 지원하는 가장 최신 버전의 애드온이 자동으로 선택된다.

CoreDNS

클러스터 내부의 DNS 서버다. 파드가 서비스 이름으로 통신할 때 이 CoreDNS가 IP로 변환해준다. 예를 들어 파드에서 http://my-service로 요청을 보내면 CoreDNS가 해당 서비스의 ClusterIP를 찾아 연결해준다.

EKS에서는 애드온으로 제공되며, 클러스터 생성 시 자동으로 설치된다. kube-system 네임스페이스에서 coredns Deployment로 실행된다.

External DNS

Kubernetes 서비스나 Ingress에 설정된 호스트명을 Route53 같은 외부 DNS에 자동으로 등록해주는 컴포넌트다. 수동으로 DNS 레코드를 등록하지 않아도 된다.

이번 실습에서는 testing.leopark.me 도메인을 사용하고, Ingress에 호스트명을 선언하면 External DNS가 Route53에 자동으로 A 레코드를 생성한다.

외부 DNS 레코드를 변경하는 권한이 필요하기 때문에 노드 IAM Role에 별도 정책을 부여했다. 실습 편의상 노드 IAM Role에 부여했지만, 프로덕션에서는 IRSA나 Pod Identity를 사용해 파드별로 최소 권한을 부여하는 것이 권장된다.

# iam.tofu
data "aws_iam_policy_document" "external_dns" {
  statement {
    effect = "Allow"
    actions = [
      "route53:ChangeResourceRecordSets",
      "route53:ListResourceRecordSets",
      "route53:ListTagsForResources",
    ]
    resources = ["arn:aws:route53:::hostedzone/*"]
  }

  statement {
    effect    = "Allow"
    actions   = ["route53:ListHostedZones"]
    resources = ["*"]
  }
}

Kube Proxy

각 노드에서 실행되는 네트워크 프록시다. Kubernetes Service의 가상 IP(ClusterIP)로 들어오는 트래픽을 실제 파드로 라우팅하는 iptables 규칙을 관리한다. 모든 노드에서 DaemonSet 형태로 실행된다.

Metrics Server

파드와 노드의 CPU, 메모리 사용량을 수집하는 경량 메트릭 파이프라인이다. kubectl top 명령어와 HPA(Horizontal Pod Autoscaler)가 이 Metrics Server에서 데이터를 가져온다.

kubectl top nodes
kubectl top pods -A

Prometheus와 다른 점은, Metrics Server는 HPA 동작에 필요한 실시간 메트릭만 짧게 보관하고 저장하지 않는다. 장기 저장과 시각화가 필요하면 Prometheus를 별도로 구성해야 한다.

VPC CNI

EKS에서 파드에 VPC IP 주소를 직접 할당해주는 네트워크 플러그인이다. 파드가 VPC 내의 일반 EC2 인스턴스처럼 직접 IP를 가지게 된다.

before_compute = true 설정은 노드가 뜨기 전에 VPC CNI가 먼저 설치되어야 함을 의미한다. 네트워크 플러그인 없이 노드가 먼저 올라오면 파드 네트워킹이 정상 동작하지 않기 때문이다.

참고내용

IMDS

EKS 워커 노드는 EC2 인스턴스이기 때문에 IAM Role(Instance Profile)이 기본으로 내장되어있다. 이러한 노드 위에서 돌아가는 모든 파드는 IMDS(169.254.169.254)를 통해 노드의 IAM Role 임시 자격증명을 획득할 수 있다.

  • curl -s http://169.254.169.254/ -v

이는 별도의 AWS 자격증명을 설정하지 않은 파드라도 노드의 권한을 그대로 사용할 수 있다는 뜻이며, 만약 하나의 파드라도 보안이 뚫리면 공격자가 해당 노드의 IAM Role 권한을 탈취할 수 있는 심각한 보안 위험이 된다. IMDSv2를 적용하면 SSRF 같은 간접 공격은 방어할 수 있지만, 파드 내부에서의 직접 접근까지는 막지 못하므로 근본적인 해결책(IRSA 등)이 필요하다.

이번 실습에서는 파드가 EC2 Instance Profile을 통해 AWS 권한을 얻는 방식을 사용하는데, 이를 위해 hop limit을 2로 설정했다. 기본값(1)이면 파드에서 IMDS에 도달하지 못한다. 파드에서 노드를 거쳐 IMDS까지 2단계가 필요하기 때문이다.

# eks.tofu
metadata_options = {
  http_endpoint               = "enabled"
  http_tokens                 = "required"   # IMDSv2 강제
  http_put_response_hop_limit = 2            # Pod -> Node -> IMDS
}
# Token 요청
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
echo $TOKEN

# Token을 이용한 IMDSv2 사용
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/

Monitoring

Prometheus 란?

Prometheus는 오픈소스 시계열 데이터베이스이자 모니터링 시스템이다. 주기적으로 대상 엔드포인트를 직접 방문해서 메트릭을 가져오는 Pull 방식으로 동작한다.

수집한 메트릭은 내부 TSDB(Time Series Database)에 저장되고, PromQL이라는 쿼리 언어로 조회할 수 있다. Grafana와 연동해서 대시보드를 구성하는 것이 일반적이다.

Kubernetes 환경에서는 ServiceMonitor, PodMonitor 같은 CRD를 사용해서 수집 대상을 선언적으로 관리한다. kube-prometheus-stack을 설치하면 이러한 CRD들과 기본 대시보드가 함께 제공된다.

Grafana 란?

Grafana는 시계열 데이터를 시각화하는 오픈소스 대시보드 도구다. Prometheus, CloudWatch, Elasticsearch 등 다양한 데이터소스를 연결할 수 있다.

대시보드를 JSON 파일로 내보내고 가져올 수 있어서, 이번 실습처럼 Grafana.com에서 커뮤니티 대시보드를 그대로 가져와 쓰는 방식이 많이 사용된다.

이번 실습에서는 Grafana 대시보드를 ConfigMap으로 관리했다. grafana_dashboard: "1" 라벨이 붙은 ConfigMap을 grafana-sc-dashboard 사이드카 컨테이너가 감지해서 자동으로 마운트하는 방식이다.

# dashboards.tofu
resource "kubectl_manifest" "grafana_dashboard_api_server" {
  yaml_body = yamlencode({
    apiVersion = "v1"
    kind       = "ConfigMap"
    metadata = {
      name      = "my-dashboard"
      namespace = "monitoring"
      labels = {
        grafana_dashboard = "1"
      }
    }
    data = {
      "k8s-system-api-server.json" = replace(
        data.http.k8s_api_server_dashboard.response_body,
        "$${DS_PROMETHEUS}",
        "prometheus"
      )
    }
  })
  depends_on = [helm_release.kube_prometheus_stack]
}

대시보드 JSON 안에는 ${DS_PROMETHEUS} 같은 데이터소스 플레이스홀더가 있다. 이를 실제 데이터소스 UID인 prometheus로 치환해야 Grafana에서 정상 로딩된다.

Prometheus-Grafana Stack Helm 배포

kube-prometheus-stack 차트 하나로 Prometheus, Grafana, AlertManager, 각종 exporter가 함께 설치된다.

이번 실습에서는 Ingress를 통해 외부에서 접근할 수 있도록 구성했다. ALB 하나를 여러 Ingress가 공유하도록 group.name: study로 묶었다.

# helm.tofu 중 ingress 설정 부분
ingress = {
  enabled          = true
  ingressClassName = "alb"
  hosts            = ["prometheus.${var.domain_name}"]
  paths            = ["/*"]
  annotations = {
    "alb.ingress.kubernetes.io/group.name"       = "study"
    "alb.ingress.kubernetes.io/load-balancer-name" = "${var.cluster_base_name}-ingress-alb"
    "alb.ingress.kubernetes.io/scheme"           = "internet-facing"
    "alb.ingress.kubernetes.io/ssl-redirect"     = "443"
    "alb.ingress.kubernetes.io/certificate-arn"  = data.aws_acm_certificate.this.arn
  }
}

EKS는 관리형 컨트롤 플레인이라 kube-scheduler, kube-controller-manager, etcd에 직접 접근이 불가능하다. 때문에 해당 컴포넌트들의 기본 수집 설정은 비활성화했다.

# helm.tofu
kubeControllerManager = { enabled = false }
kubeEtcd              = { enabled = false }
kubeScheduler         = { enabled = false }

Control Plane 메트릭 수집

EKS에서는 metrics.eks.amazonaws.com API를 통해 관리형 컨트롤 플레인의 메트릭을 간접적으로 가져올 수 있다.

kubectl get --raw "/apis/metrics.eks.amazonaws.com/v1/ksh/container/metrics"  # scheduler
kubectl get --raw "/apis/metrics.eks.amazonaws.com/v1/kcm/container/metrics"  # controller-manager
kubectl get --raw "/apis/metrics.eks.amazonaws.com/v1/etcd/container/metrics" # etcd

Prometheus의 additionalScrapeConfigs에 이 경로를 등록하면 수집이 가능하다.

# helm.tofu - additionalScrapeConfigs 중 scheduler 수집 설정
{
  job_name              = "ksh-metrics"
  kubernetes_sd_configs = [{ role = "endpoints" }]
  metrics_path          = "/apis/metrics.eks.amazonaws.com/v1/ksh/container/metrics"
  scheme                = "https"
  tls_config = {
    ca_file              = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
    insecure_skip_verify = true
  }
  bearer_token_file = "/var/run/secrets/kubernetes.io/serviceaccount/token"
  relabel_configs = [{
    source_labels = ["__meta_kubernetes_namespace", "__meta_kubernetes_service_name", "__meta_kubernetes_endpoint_port_name"]
    action        = "keep"
    regex         = "default;kubernetes;https"
  }]
}

Prometheus 파드가 이 API를 호출하려면 metrics.eks.amazonaws.com API 그룹에 대한 RBAC 권한이 필요하다. kube-prometheus-stack이 생성하는 기본 ClusterRole에는 이 권한이 없으므로 별도로 추가해야 한다.

# rbac.tofu
resource "kubectl_manifest" "prometheus_eks_metrics_clusterrole" {
  yaml_body = yamlencode({
    apiVersion = "rbac.authorization.k8s.io/v1"
    kind       = "ClusterRole"
    metadata = {
      name = "kube-prometheus-stack-prometheus-eks-metrics"
    }
    rules = [{
      apiGroups = ["metrics.eks.amazonaws.com"]
      resources = ["kcm/metrics", "ksh/metrics", "etcd/metrics"]
      verbs      = ["get"]
    }]
  })
  depends_on = [helm_release.kube_prometheus_stack]
}

기존 ClusterRole을 직접 수정하지 않고 별도 ClusterRole을 만들어 바인딩했다. helm upgrade 시 기존 ClusterRole이 덮어씌워져도 이 권한은 유지된다.

도전과제: etcd 메트릭 수집

etcd도 동일한 패턴으로 수집 가능하다. scrape config에 etcd 경로를 추가하고, ClusterRole의 resources에 etcd/metrics를 포함시키면 된다.

# helm.tofu - etcd scrape config 추가
{
  job_name              = "etcd-metrics"
  kubernetes_sd_configs = [{ role = "endpoints" }]
  metrics_path          = "/apis/metrics.eks.amazonaws.com/v1/etcd/container/metrics"
  scheme                = "https"
  tls_config = {
    ca_file              = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
    insecure_skip_verify = true
  }
  bearer_token_file = "/var/run/secrets/kubernetes.io/serviceaccount/token"
  relabel_configs = [{
    source_labels = ["__meta_kubernetes_namespace", "__meta_kubernetes_service_name", "__meta_kubernetes_endpoint_port_name"]
    action        = "keep"
    regex         = "default;kubernetes;https"
  }]
}

도전과제: Grafana 대시보드 추가

Grafana.com에 공개된 대시보드를 tofu apply 시점에 다운로드해서 ConfigMap으로 배포했다. 기존 api-server 대시보드와 동일한 패턴이다.

# dashboards.tofu - controller manager 대시보드
data "http" "k8s_controller_manager_dashboard" {
  url = "https://grafana.com/api/dashboards/12122/revisions/1/download"
}

resource "kubectl_manifest" "grafana_dashboard_controller_manager" {
  yaml_body = yamlencode({
    apiVersion = "v1"
    kind       = "ConfigMap"
    metadata = {
      name      = "dashboard-controller-manager"
      namespace = "monitoring"
      labels    = { grafana_dashboard = "1" }
    }
    data = {
      "k8s-system-controllermanager.json" = replace(
        data.http.k8s_controller_manager_dashboard.response_body,
        "$${DS_PROMETHEUS}",
        "prometheus"
      )
    }
  })
  depends_on = [helm_release.kube_prometheus_stack]
}

노드 모니터링

k9s

k9s는 터미널 기반의 Kubernetes 클러스터 관리 도구다. kubectl 명령어를 일일이 입력하지 않고 키보드로 탐색하면서 파드, 서비스, 노드 상태를 실시간으로 확인할 수 있다.

  • :pod 입력 후 엔터 : 파드 목록
  • :node : 노드 목록 및 리소스 사용량
  • d : describe 보기
  • l : 로그 보기
  • s : 쉘 접속

노드 뷰에서 CPU, 메모리 사용률을 실시간으로 확인하면서 스케일링 동작을 관찰하기에 유용하다.

SPOT 인스턴스

스팟 인스턴스는 AWS의 여유 EC2 용량을 할인된 가격(최대 90% 저렴)에 사용하는 방식이다. 대신 AWS가 용량이 필요하면 2분 전 통보 후 인스턴스를 회수할 수 있다.

장점은 비용 절감이 크고, 단점은 언제든 중단될 수 있다는 것이다. 따라서 스팟 인스턴스에는 상태를 저장하지 않는 워크로드나 중단돼도 재시작 가능한 배치 작업에 적합하다.

이번 실습에서는 세 번째 노드 그룹을 SPOT으로 구성했다. 여러 인스턴스 타입을 지정해 가용성을 높이는 것이 중요하다.

# eks.tofu - SPOT 노드 그룹
capacity_type  = "SPOT"
instance_types = ["c5a.large", "c6a.large", "t3a.large", "t3a.medium"]

busybox 파드는 SPOT 노드에만 배포되도록 nodeSelector를 사용했다. EKS는 SPOT 노드에 자동으로 eks.amazonaws.com/capacityType: SPOT 라벨을 붙여준다.

# app.tofu
nodeSelector = {
  "eks.amazonaws.com/capacityType" = "SPOT"
}

Taint, Toleration, NodeSelector

세 가지 모두 파드를 특정 노드에 배치하거나 배제하는 메커니즘이다.

NodeSelector는 가장 단순한 방식으로, 특정 라벨이 있는 노드에만 파드를 배치한다.

Taint는 노드 측에서 특정 파드를 거부하는 설정이다. NoSchedule, PreferNoSchedule, NoExecute 세 가지 effect가 있다. NoExecute는 이미 실행 중인 파드도 퇴출시킨다.

Toleration은 파드 측에서 Taint를 허용하겠다고 선언하는 설정이다. Taint와 Toleration이 매칭되어야 해당 노드에 배포될 수 있다.

이번 실습에서 ARM64 노드에는 cpuarch=arm64:NoExecute Taint를 설정했다.

# eks.tofu - ARM64 노드 그룹 Taint 설정
taints = {
  cpuarch = {
    key    = "cpuarch"
    value  = "arm64"
    effect = "NO_EXECUTE"
  }
}

sample-app은 ARM64 노드에 배포하기 위해 NodeSelector와 Toleration을 함께 사용했다. Toleration만 있으면 ARM64 노드에 배포될 수 있지만 그 노드에만 배포되는 것은 보장되지 않는다. NodeSelector를 함께 써야 특정 노드에만 배포된다.

# app.tofu
nodeSelector = { "kubernetes.io/arch" = "arm64" }
tolerations = [{
  key      = "cpuarch"
  operator = "Equal"
  value    = "arm64"
  effect   = "NoExecute"
}]

mario 앱은 x86 이미지를 ARM64 노드에 배포하면 어떤 일이 생기는지 확인하는 실습용이다. exec format error가 발생하는데, 이미지 아키텍처와 노드 아키텍처가 불일치하기 때문이다.

AutoScaling

Application Tuning — 프로세스 튜닝

스케일링 전에 애플리케이션 자체를 먼저 최적화하는 것이 가장 비용 효율적인 방법이다.

  • JVM 힙 메모리, 스레드 풀, 커넥션 풀 등 런타임 파라미터 조정
  • 불필요한 리소스 요청(Request/Limit) 낭비 제거
  • 병목 지점 프로파일링 후 코드/설정 개선

VPA — Vertical Pod Autoscaler

파드의 CPU/Memory 크기를 자동으로 조정하는 방식이다.

항목 내용
동작 방식 과거 리소스 사용량을 분석해 Request/Limit 자동 추천 및 적용
기존 문제 리소스 변경 시 파드 재시작 필요 (서비스 순단 발생)
개선 사항 In-Place Resize (K8s v1.27+) 로 재시작 없이 리소스 조정 가능
적합한 워크로드 트래픽 패턴이 일정하고 수직 확장이 효율적인 앱

HPA와 동시에 사용 시 CPU 기준 충돌이 발생할 수 있다. Memory 기준으로만 VPA를 적용하는 것이 권장된다.

HPA — Horizontal Pod Autoscaler

파드 수를 늘리거나 줄이는 수평 확장 방식이다. 일반적으로 시스템을 확장할 때는 수직보다 수평 스케일링을 권장한다.

CPU / Memory 사용률 기반으로 파드 수를 자동으로 조정한다. 목표 사용률을 20%로 설정한 경우, 전체 파드의 평균 CPU 사용률이 20%를 초과하면 복제본을 추가하고, 20% 이하로 내려가면 복제본을 제거한다. 잦은 스케일링 이탈을 방지하기 위해 cooldown 대기 시간이 적용된다.

HPA 사용 시 주의사항은 다음과 같다.

  • Metrics Server가 반드시 설치되어 있어야 CPU/Memory 메트릭을 가져올 수 있다.
  • 파드에 resources.requests가 설정되어 있지 않으면 HPA가 동작하지 않는다.
  • 스케일 인 시 갑작스러운 파드 종료로 요청이 끊길 수 있어 terminationGracePeriodSeconds 설정이 중요하다.
  • KEDA와 달리 HPA는 scale-to-zero를 지원하지 않으므로 최소 1개의 파드는 항상 유지된다.

KEDA — Kubernetes Event-Driven Autoscaling

HPA를 이벤트 기반으로 확장한 오픈소스 프로젝트다. CPU/Memory 외에도 다양한 외부 소스를 기반으로 스케일링할 수 있다.

  • SQS / Kafka / RabbitMQ 메시지 큐 길이
  • Prometheus 커스텀 메트릭
  • Cron 스케줄 기반
  • CloudWatch, Datadog 등 외부 메트릭

Scale-to-Zero를 지원해서 트래픽이 없을 때 파드를 완전히 0개로 줄일 수 있다.

이번 실습에서는 Cron 트리거를 사용해서 15분 주기로 스케일 아웃/인을 반복하도록 ScaledObject를 구성했다.

# app.tofu
resource "kubectl_manifest" "keda_scaledobject_cron" {
  yaml_body = yamlencode({
    apiVersion = "keda.sh/v1alpha1"
    kind       = "ScaledObject"
    metadata = {
      name      = "php-apache-cron-scaled"
      namespace = "keda"
    }
    spec = {
      minReplicaCount = 0
      maxReplicaCount = 2
      pollingInterval = 30
      cooldownPeriod  = 300
      scaleTargetRef = {
        apiVersion = "apps/v1"
        kind       = "Deployment"
        name       = "php-apache"
      }
      triggers = [{
        type = "cron"
        metadata = {
          timezone        = "Europe/London"
          start           = "00,15,30,45 * * * *"
          end             = "05,20,35,50 * * * *"
          desiredReplicas = "1"
        }
      }]
    }
  })
}

KEDA는 내부적으로 HPA를 생성하고 관리한다. ScaledObject를 만들면 KEDA가 자동으로 대응하는 HPA 오브젝트를 생성한다.

KEDA Metrics Server는 CPU/Memory 담당인 기존 metrics-server와 별개로, 외부 이벤트 소스의 메트릭을 external.metrics.k8s.io API로 노출한다.

kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq

Prometheus와 연동해서 KEDA 동작을 모니터링하려면 ServiceMonitor를 활성화해야 한다.

# helm.tofu - KEDA Prometheus 연동
prometheus = {
  metricServer = {
    enabled        = true
    serviceMonitor = { enabled = true }
  }
  operator = {
    enabled        = true
    serviceMonitor = { enabled = true }
  }
}

CA / CAS — Cluster Autoscaler

노드(서버) 자체를 동적으로 추가/삭제하는 방식이다.

  • 파드가 Pending 상태일 때 (리소스 부족) 노드를 자동으로 추가한다.
  • 노드 사용률이 낮을 때 노드를 자동으로 반납(삭제)한다.
  • 동작하려면 퍼블릭 클라우드의 Auto Scaling 그룹이 필요하다.
  • 노드 추가에 수 분이 소요되어 반응 속도가 느리다.

Cluster Autoscaler가 Auto Scaling Group을 조정할 수 있도록 IAM 권한이 필요하다.

# iam.tofu
data "aws_iam_policy_document" "cas_autoscaler" {
  statement {
    effect = "Allow"
    actions = [
      "autoscaling:DescribeAutoScalingGroups",
      "autoscaling:SetDesiredCapacity",
      "autoscaling:TerminateInstanceInAutoScalingGroup",
    ]
    resources = ["*"]
  }
}

Karpenter

CA보다 빠르고 유연한 노드 프로비저닝 도구다. Cluster Autoscaler는 미리 정의된 노드 그룹(ASG) 안에서만 노드를 추가하지만, Karpenter는 EC2 API를 직접 호출해서 파드 요구사항에 맞는 인스턴스 타입을 그때그때 선택한다. 그래서 CA보다 프로비저닝 속도가 빠르고, 불필요하게 큰 인스턴스를 뜨게 하는 낭비도 줄어든다.

IAM 설정 구조

Karpenter가 동작하려면 일반 노드 그룹보다 복잡한 IAM 설정이 필요하다. 실습에서는 CloudFormation이 이 부분을 자동으로 처리했는데, 실제로는 다음 다섯 가지 정책이 생성된다.

  • KarpenterControllerNodeLifecyclePolicy: EC2 인스턴스 생성, 종료, 태그 관리
  • KarpenterControllerIAMIntegrationPolicy: 노드에 IAM 역할 부여
  • KarpenterControllerEKSIntegrationPolicy: EKS 클러스터 정보 조회
  • KarpenterControllerInterruptionPolicy: SQS 큐를 통한 스팟 중단 이벤트 수신
  • KarpenterControllerResourceDiscoveryPolicy: 서브넷, 보안그룹, AMI 조회

Karpenter 컨트롤러 파드는 이 권한들을 Pod Identity(또는 IRSA)로 받아서 사용하고, 노드 자체는 별도의 KarpenterNodeRole을 통해 클러스터에 등록된다.

서브넷과 보안그룹을 Karpenter가 자동으로 찾을 수 있도록 karpenter.sh/discovery 태그를 붙여두는 것이 핵심이다.

NodePool과 EC2NodeClass

Karpenter의 핵심 설정은 두 개의 CRD로 이루어진다.

NodePool은 어떤 종류의 노드를 허용할지 선언하는 곳이다. 아키텍처, OS, 용량 유형(온디맨드/스팟), 인스턴스 카테고리와 세대를 조건으로 지정한다. 단일 NodePool이 다양한 파드 요구사항을 모두 처리할 수 있다. Managed Node Group처럼 인스턴스 타입을 미리 하나씩 나열하지 않아도 된다.

spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["2"]
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m

EC2NodeClass는 노드를 실제로 띄울 때 참조하는 AWS 리소스 정보다. IAM 역할, AMI, 서브넷, 보안그룹을 태그 기반으로 자동 탐색한다.

스케일 아웃 동작

CPU 1개를 요청하는 inflate 파드를 5개로 늘렸을 때 Karpenter가 어떤 선택을 했는지 로그로 확인할 수 있었다.

{
  "message": "launched nodeclaim",
  "instance-type": "c5a.2xlarge",
  "capacity-type": "on-demand",
  "allocatable": {
    "cpu": "7910m",
    "memory": "14162Mi"
  }
}

파드 5개가 각각 CPU 1개를 요청하니 최소 5코어가 필요했고, Karpenter는 c5a.2xlarge(8코어)를 선택해서 하나의 노드에 5개 파드를 모두 배치했다. 미리 인스턴스 타입을 지정하지 않아도 파드 요구사항을 계산해서 가장 적합한 타입을 스스로 결정한다.

스케일 인과 통합(Consolidation)

Deployment를 삭제하면 Karpenter의 Disruption 컨트롤러가 동작한다. 바로 노드를 종료하는 것이 아니라 단계적으로 처리한다.

1단계로 노드에 karpenter.sh/disrupted: NoSchedule Taint를 붙여 새 파드가 스케줄되지 않도록 막는다. 2단계로 기존 파드를 다른 노드로 이동시키거나 종료 대기한다. 3단계로 노드와 NodeClaim을 삭제한다.

{"message": "disrupting nodeclaim(s) via delete, terminating 1 nodes (1 pods) ... reason: underutilized"}
{"message": "tainted node", "taint.Key": "karpenter.sh/disrupted", "taint.Effect": "NoSchedule"}
{"message": "deleted node"}
{"message": "deleted nodeclaim"}

consolidationPolicy: WhenEmptyOrUnderutilized로 설정하면 파드가 완전히 없는 노드뿐 아니라 사용률이 낮은 노드도 정리 대상이 된다. 이 동작이 CA와 비교했을 때 Karpenter의 비용 절감 효과가 더 큰 이유다.

CA와 비교

Cluster Autoscaler는 Pending 파드가 생겼을 때 어느 노드 그룹의 용량을 늘릴지 판단하고 ASG의 desired count를 올린다. 그러면 ASG가 EC2를 띄우고 노드가 클러스터에 합류하기까지 수 분이 걸린다.

Karpenter는 EC2 API를 직접 호출하므로 노드 합류까지 소요 시간이 짧다. 또한 파드 여러 개를 한꺼번에 분석해서 가장 효율적으로 bin-packing할 수 있는 인스턴스 타입을 선택하기 때문에 불필요한 자원 낭비가 적다.

Fargate

노드라는 개념 자체가 없어지는 방식이다. EC2 인스턴스를 직접 관리하지 않고, 파드 단위로 컨테이너를 실행하면 AWS가 알아서 격리된 환경에 배치한다.

노드 패치, AMI 업데이트, 용량 계획 같은 운영 부담이 완전히 사라진다는 것이 가장 큰 장점이다. 각 파드가 완전히 격리된 마이크로 VM 위에서 실행되기 때문에 멀티 테넌트 환경에서 보안 요건이 까다로운 워크로드에도 잘 맞는다.

반면 단점도 뚜렷하다. DaemonSet을 실행할 수 없고, 노드에 직접 접근하는 방식의 모니터링 에이전트나 스토리지 플러그인이 동작하지 않는다. 또한 EC2 대비 비용이 높은 편이라 항상 실행되는 서비스보다는 배치 작업이나 CI/CD 실행기처럼 간헐적으로 뜨고 내려가는 워크로드에 어울린다.

EKS에서 Fargate를 사용하려면 Fargate Profile을 만들어서 어떤 네임스페이스 또는 라벨의 파드를 Fargate로 보낼지 선언해야 한다. 나머지 파드는 기존 EC2 노드에서 그대로 실행된다. EC2와 Fargate를 혼용하는 구성도 가능하다.

EKS Auto Mode

2024년 말에 출시된 기능으로, EKS가 컴퓨팅, 스토리지, 네트워킹을 통합해서 자동으로 관리하는 방식이다. 내부적으로 AWS가 관리하는 Karpenter 기반으로 동작하지만, 사용자가 Karpenter 버전이나 설정을 직접 제어하지 못하며, NodePool이나 EC2NodeClass를 직접 작성하지 않아도 된다.

기존에 관리자가 직접 해야 했던 것들, 예를 들어 노드 그룹 생성, AMI 선택, 보안 패치 적용, 용량 계획 등을 AWS가 대신 처리한다. EKS Managed Node Group보다 한 단계 더 추상화된 셈이다.

EC2 인스턴스 위에서 돌기 때문에 Fargate와 달리 DaemonSet, 로컬 스토리지, GPU 워크로드도 지원한다. 운영 부담을 최소화하면서도 EC2의 유연성은 유지하고 싶을 때 선택하기 좋은 옵션이다.

아직 지원하지 않는 기능들이 있어 기존 클러스터를 그대로 Auto Mode로 전환하기 어려운 경우도 있다.