Skip to content

AWS LBC, Ingress, ExternalDNS, CoreDNS

이전 EKS Networking 문서는 EKS 클러스터 자체의 네트워킹 구조(VPC CNI, ENI, IP 할당, Service 기초)를 다뤘다. 이 문서는 그 위에서 동작하는 AWS 전용 컨트롤러들을 다룬다.

AWS Load Balancer Controller, ExternalDNS, CoreDNS는 모두 클러스터 외부의 AWS 서비스(NLB/ALB, Route 53)를 Kubernetes 리소스와 연동시켜주는 레이어다. 네트워킹 기초와 섞이면 너무 길어지기도 하고, 이 컨트롤러들은 각각 IAM 권한(IRSA), Helm 배포, CRD 등 별도의 설치 절차가 있어서 분리하는 게 낫다고 판단했다.


Terraform Code와 함께 보는 AWS LBC / Ingress / ExternalDNS / CoreDNS

05 문서와 같은 방식으로, 실습에서 AWS CLI나 eksctl로 진행했던 부분을 Terraform으로 표현하면 어떻게 되는지를 중심으로 정리했다.

AWS Load Balancer Controller (LBC)

LBC가 필요한 이유

05 문서에서 LoadBalancer 타입 Service를 만들면 로드밸런서가 자동으로 생긴다고 했는데, 그 기본 동작은 Cloud Controller Manager(CCM)이 처리한다. CCM은 NLB를 인스턴스 모드로만 만든다.

인스턴스 모드: 외부 -> NLB -> 노드IP:NodePort -> iptables -> Pod

Pod IP 모드로 만들려면 CCM 대신 AWS Load Balancer Controller가 필요하다.

Pod IP 모드: 외부 -> NLB -> Pod IP 직접

LBC는 Service/Ingress 리소스를 감시하다가 annotation에 따라 NLB 또는 ALB를 프로비저닝한다. VPC CNI가 Pod에 VPC IP를 직접 부여하기 때문에 이 방식이 가능하다.

IRSA - LBC가 AWS를 제어하는 방법

LBC는 NLB/ALB를 직접 생성하고 관리해야 하므로 IAM 권한이 필요하다. EKS Pod가 IAM을 사용하는 권장 방법은 IRSA(IAM Roles for Service Accounts)다.

흐름: LBC Pod -> K8s ServiceAccount -> IAM Role (OIDC 신뢰) -> AWS API

실습에서는 eksctl create iamserviceaccount로 처리했는데, Terraform으로는 아래처럼 쓸 수 있다.

# lbc.tofu (신규 파일로 분리하거나 eks.tofu 하단에 추가)

# eks.tofu에 이미 enable_irsa = true 가 설정되어 있어서 OIDC provider는 준비된 상태다

module "lbc_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 6.4"

  role_name                              = "aws-load-balancer-controller"
  attach_load_balancer_controller_policy = true  # LBC용 IAM 정책 자동 생성/부착

  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:aws-load-balancer-controller"]
    }
  }
}

attach_load_balancer_controller_policy = true를 쓰면 IAM 정책 json을 직접 작성하지 않아도 된다. 내부적으로 공식 IAM 정책과 동일한 내용이 적용된다.

Helm으로 LBC 설치 - Terraform 예시

실습에서는 helm install 명령어로 직접 설치했는데, Terraform helm_release 리소스로 관리할 수 있다.

resource "helm_release" "aws_load_balancer_controller" {
  name       = "aws-load-balancer-controller"
  repository = "https://aws.github.io/eks-charts"
  chart      = "aws-load-balancer-controller"
  namespace  = "kube-system"
  version    = "3.1.0"

  set {
    name  = "clusterName"
    value = var.cluster_base_name
  }
  set {
    name  = "serviceAccount.create"
    value = "false"
  }
  set {
    name  = "serviceAccount.name"
    value = "aws-load-balancer-controller"
  }
  # 아래 두 값이 없으면 LBC가 VPC ID를 IMDS에서 가져오려다 실패한다
  set {
    name  = "region"
    value = var.target_region
  }
  set {
    name  = "vpcId"
    value = module.vpc.vpc_id
  }
}

NLB Service

LBC가 설치되면 Service에 annotation을 붙여서 NLB 동작 방식을 제어한다.

# echo-service-nlb.yaml (kubectl apply로 배포)
apiVersion: v1
kind: Service
metadata:
  name: svc-nlb-ip-type
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip          # Pod IP 모드
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing       # 외부 노출
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
    service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: deregistration_delay.timeout_seconds=60
spec:
  allocateLoadBalancerNodePorts: false  # Pod IP 모드에서는 NodePort 불필요
  type: LoadBalancer

allocateLoadBalancerNodePorts: false는 K8s 1.22 이상 버전 에서 지원하는 옵션이다. Pod IP 모드에서는 NLB가 NodePort를 거치지 않으므로 불필요한 NodePort 할당을 막는다.

NLB 인스턴스 모드 vs Pod IP 모드

항목 인스턴스 모드 (CCM 기본) Pod IP 모드 (LBC 필요)
트래픽 경로 NLB -> 노드:NodePort -> Pod NLB -> Pod IP 직접
LBC 필요 여부 불필요 필요
externalTrafficPolicy Local 권장 해당 없음
Client IP 유지 Local 설정 시 가능 Proxy Protocol v2 필요

externalTrafficPolicy: Local을 쓰면 NLB가 Pod가 없는 노드는 헬스체크 실패로 자동 제외한다. Pod IP 모드에서는 NLB가 Pod를 직접 타겟으로 등록하므로 이 개념 자체가 불필요하다.


Ingress (ALB)

Service LoadBalancer가 L4(TCP)라면, Ingress는 L7(HTTP/HTTPS)에서 동작한다. URL 경로나 호스트 기반 라우팅이 필요할 때 쓴다.

LBC가 설치되면 IngressClass: alb를 통해 ALB도 자동으로 프로비저닝해준다.

# game-2048 ingress 예시
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: game-2048
  name: ingress-2048
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: service-2048
              port:
                number: 80

ALB target-type을 ip로 설정하면 ALB -> Pod IP로 직접 트래픽이 전달된다. 대상 그룹에 Pod IP가 직접 등록되고, Pod 수가 바뀌면 LBC가 자동으로 대상 그룹을 갱신한다.

NNLB는 L4 (TCP/UDP/TLS)에서 동작하고 HTTP 경로 기반 라우팅을 지원하지 않는다


ExternalDNS

Service나 Ingress를 만들 때 도메인을 annotation으로 지정하면, Route 53 A 레코드를 자동으로 생성/삭제해준다. 로드밸런서 DNS 주소를 매번 직접 Route 53에 등록하는 번거로움을 없애준다.

IRSA - Terraform 예시

module "externaldns_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 6.4"

  role_name                     = "external-dns"
  attach_external_dns_policy    = true  # Route 53 변경 권한 자동 부착

  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:external-dns"]
    }
  }
}

Helm 설치도 helm_release로 관리할 수 있다.

resource "helm_release" "external_dns" {
  name       = "external-dns"
  repository = "https://kubernetes-sigs.github.io/external-dns/"
  chart      = "external-dns"
  namespace  = "kube-system"

  set {
    name  = "serviceAccount.create"
    value = "false"
  }
  set {
    name  = "serviceAccount.name"
    value = "external-dns"
  }
  set {
    name  = "provider"
    value = "aws"
  }
  set {
    name  = "policy"
    value = "sync"
  }
  set {
    name  = "txtOwnerId"
    value = var.cluster_base_name
  }
}

도메인 연동 방법

Service나 Ingress에 annotation 하나만 추가하면 된다.

# 이미 만들어진 Service에 annotation 추가
kubectl annotate service tetris "external-dns.alpha.kubernetes.io/hostname=<my-domain>"

또는 Service manifest에 처음부터 포함시킬 수 있다.

metadata:
  annotations:
    external-dns.alpha.kubernetes.io/hostname: <my-domain>

ExternalDNS가 이 annotation을 감지해서 Route 53에 A 레코드를 만든다. policy: sync로 설정하면 Service가 삭제될 때 A 레코드도 같이 지워진다.

public 도메인이 없으면 이 실습은 진행할 수 없다. Route 53에 호스팅 존이 등록되어 있어야 한다.

CoreDNS

클러스터 내부 DNS를 담당한다. Pod 안에서 서비스명.네임스페이스.svc.cluster.local로 질의하면 CoreDNS가 ClusterIP를 반환한다.

05 문서의 addons 코드에 이미 포함되어 있다.

# eks.tofu (기존 addons 블록)
coredns = {
  most_recent = true
}

기본 설정만으로도 잘 동작하지만, 클러스터가 커지면 두 가지를 챙길 필요가 있다.

topologySpreadConstraints - AZ 분산

CoreDNS 파드 2개가 같은 AZ에 몰리면 AZ 장애 시 DNS 전체가 멈춘다. addon 설정으로 AZ 분산을 강제할 수 있다.

# eks.tofu - coredns addon에 configuration_values 추가
coredns = {
  most_recent = true
  configuration_values = jsonencode({
    topologySpreadConstraints = [{
      maxSkew           = 1
      topologyKey       = "topology.kubernetes.io/zone"
      whenUnsatisfiable = "ScheduleAnyway"
      labelSelector     = { matchLabels = { "k8s-app" = "kube-dns" } }
    }]
  })
}

lameduck - 재시작 중 DNS 실패 방지

CoreDNS 파드가 재시작될 때 갑자기 끊기면 DNS 질의가 실패할 수 있다.

lameduck 5s 옵션은 종료 전 5초 동안종료를 5초간 지연시키면서 /health는 200을 유지하고(liveness 유지), /ready는 실패를 반환하여 K8s가 엔드포인트에서 제거할 시간을 확보한다.

최신 EKS CoreDNS addon은 이 옵션이 기본으로 활성화되어 있다.