[AWS EKS] AWS Load Balancer Controller 트러블슈팅 실습 가이드

본 글은 AWS EKS WorkshopTroubleshooting > ALB Controller 모듈을 기반으로 작성되었습니다. 환경 구성부터 3가지 트러블슈팅 시나리오까지 전체 실습 과정을 다룹니다. 서종호(가시다)님의 AWS EKS Workshop Study(AEWS) 5주차 학습 내용을 기반으로 합니다.

1. 실습 개요

EKS 환경에서 AWS Load Balancer Controller를 통해 ALB를 프로비저닝할 때 실무에서 자주 마주치는 문제들을 의도적으로 재현하고, 이를 단계별로 진단·해결하는 실습이다.

실습에서 다루는 3가지 트러블슈팅 시나리오는 다음과 같다.

# 시나리오 핵심 원인
1 ALB가 생성되지 않음 Public Subnet에 kubernetes.io/role/elb 태그 누락
2 IAM 권한 오류로 ALB 생성 실패 LB Controller의 IAM Role에 필요한 정책 미부착
3 ALB는 생성되었으나 503 오류 Ingress의 Service 이름 오류 + Service Selector 불일치

2. 환경 구성

EKS Workshop은 크게 두 가지 방식으로 환경을 구성할 수 있다.

  • AWS 이벤트 참가자: 제공된 계정을 통해 바로 시작
  • 개인 AWS 계정 사용자: CloudFormation → IDE 생성 → eksctl로 클러스터 구축

여기서는 개인 계정에서 진행하는 방법을 기준으로 설명한다.

2.1. IDE 환경 생성 (CloudFormation)

아래 리전별 Quick-create 링크를 통해 IDE(VS Code Server가 설치된 EC2)를 생성한다.

⚠️ IDE EC2 인스턴스에는 IAM Role 생성 등 광범위한 IAM 권한이 부여된다. 생성 전에 CloudFormation 템플릿의 IAM 권한을 검토하자.

진행 순서:

  1. 링크 클릭 후 하단의 IAM 리소스 생성 승인 체크박스를 선택
  2. Create stack 클릭 (약 5분 소요)
  3. 스택 생성 완료 후 Outputs 탭에서 IdeUrlIdePasswordSecret 확인
  1. IdePasswordSecret URL로 이동하여 Retrieve 버튼 클릭 → 비밀번호 복사
  2. IdeUrl로 접속하여 비밀번호 입력 → IDE 진입

2.2. EKS 클러스터 생성 (eksctl)

IDE 터미널에서 아래 명령어를 실행한다.

export EKS_CLUSTER_NAME=eks-workshop

curl -fsSL https://raw.githubusercontent.com/aws-samples/eks-workshop-v2/stable/cluster/eksctl/cluster.yaml | \
  envsubst | eksctl create cluster -f -

클러스터 생성에는 약 20분이 소요된다. 생성되는 주요 리소스는 다음과 같다.

  • 3개 AZ에 걸친 VPC (CIDR: 10.42.0.0/16)
  • IAM OIDC Provider
  • Managed Node Group (m5.large × 3대)
  • VPC CNI (Prefix Delegation 활성화)

참고로 eksctl이 사용하는 클러스터 설정 파일의 주요 내용은 아래와 같다.

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: ${EKS_CLUSTER_NAME}
  region: ${AWS_REGION}
  version: "1.33"
iam:
  withOIDC: true
vpc:
  cidr: 10.42.0.0/16
  clusterEndpoints:
    privateAccess: true
    publicAccess: true
managedNodeGroups:
  - name: default
    desiredCapacity: 3
    minSize: 3
    maxSize: 6
    instanceType: m5.large
    privateNetworking: true
addons:
  - name: vpc-cni
    configurationValues: '{"env":{"ENABLE_PREFIX_DELEGATION":"true", ...}}'

2.3. 트러블슈팅 랩 환경 준비

클러스터가 준비되면 아래 명령어로 ALB 트러블슈팅 랩 환경을 배포한다.

prepare-environment troubleshooting/alb

이 스크립트는 다음 리소스를 배포한다.

  • 샘플 UI 애플리케이션 (Deployment + Service)
  • Ingress 리소스 (ALB 사용)
  • AWS Load Balancer Controller (의도적인 오류 포함)
  • IAM Role/Policy (의도적인 권한 누락 포함)
💡 prepare-environment 실행 후 몇 분 기다려야 모든 리소스가 배포 완료된다.

3. 실습 환경 확인

랩 환경이 준비되면, 먼저 Service와 Ingress가 정상적으로 생성되었는지 확인한다.

kubectl get svc -n ui
NAME   TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
ui     ClusterIP   172.20.224.112   <none>        80/TCP    12d
kubectl get ingress -n ui
NAME   CLASS   HOSTS   ADDRESS   PORTS   AGE
ui     alb     *                 80      11m

ADDRESS 필드가 비어 있다. 정상적이라면 ALB DNS 이름이 표시되어야 한다.

ALB가 실제로 생성되지 않았는지 AWS CLI로도 확인해보자.

aws elbv2 describe-load-balancers \
  --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-ui-ui`) == `true`]'
[]

ALB가 생성되지 않은 것이 확인되었다. 이제 원인을 파악해보자.


4. 시나리오 1: ALB가 생성되지 않는 문제 — Subnet 태그 누락

4.1. 현상 확인

Pod 자체는 정상 동작 중이다.

kubectl get pod -n ui
NAME                  READY   STATUS    RESTARTS   AGE
ui-68495c748c-jkh2z   1/1     Running   0          85s

Ingress를 describe하여 이벤트를 확인한다.

kubectl describe ingress/ui -n ui

핵심 에러: kubernetes.io/role/elb 태그가 설정된 서브넷을 찾을 수 없다는 것이다.

4.2. 원인 분석

AWS Load Balancer Controller는 ALB를 생성할 때 Subnet Auto Discovery 메커니즘을 사용한다.

  • Internet-facing ALB: kubernetes.io/role/elb=1 태그가 있는 Public Subnet 필요
  • Internal ALB: kubernetes.io/role/internal-elb=1 태그가 있는 Private Subnet 필요

현재 환경에서는 Public Subnet에 해당 태그가 누락되어 있다.

4.3. 해결 과정

Step 1: 클러스터의 서브넷 목록 확인

aws ec2 describe-subnets \
  --filters "Name=tag:alpha.eksctl.io/cluster-name,Values=${EKS_CLUSTER_NAME}" \
  --query 'Subnets[].SubnetId[]'
[
  "subnet-xxxxxxxxxxxxxxxxx",
  "subnet-xxxxxxxxxxxxxxxxx",
  "subnet-xxxxxxxxxxxxxxxxx",
  "subnet-xxxxxxxxxxxxxxxxx",
  "subnet-xxxxxxxxxxxxxxxxx",
  "subnet-xxxxxxxxxxxxxxxxx"
]

Step 2: Public Subnet 식별

각 서브넷의 라우팅 테이블을 확인하여 IGW(igw-xxx)로의 0.0.0.0/0 경로가 있는 서브넷이 Public Subnet이다.

for subnet_id in $(aws ec2 describe-subnets \
  --filters "Name=tag:alpha.eksctl.io/cluster-name,Values=${EKS_CLUSTER_NAME}" \
  --query 'Subnets[].SubnetId[]' --output text); do
  echo "Subnet: ${subnet_id}"
  aws ec2 describe-route-tables \
    --filters "Name=association.subnet-id,Values=${subnet_id}" \
    --query 'RouteTables[].Routes[].[DestinationCidrBlock,GatewayId]' --output text
done
Subnet: subnet-xxxxxxxxxxxxxxxxx
10.42.0.0/16    local
0.0.0.0/0       igw-xxxxxxxxxxxxxxxxx    ← Public Subnet (IGW 경로 존재)

Subnet: subnet-yyyyyyyyyyyyyyyyy
10.42.0.0/16    local
0.0.0.0/0       nat-xxxxxxxxxxxxxxxxx    ← Private Subnet (NAT Gateway)
...

Step 3: 현재 ELB 태그 상태 확인

aws ec2 describe-subnets \
  --filters 'Name=tag:kubernetes.io/role/elb,Values=1' \
  --query 'Subnets[].SubnetId'
[]

태그가 설정된 서브넷이 하나도 없는 것이 확인되었다.

Step 4: Public Subnet에 태그 추가

aws ec2 create-tags \
  --resources $PUBLIC_SUBNET_1 $PUBLIC_SUBNET_2 $PUBLIC_SUBNET_3 \
  --tags 'Key="kubernetes.io/role/elb",Value=1'

Step 5: 태그 적용 확인

aws ec2 describe-subnets \
  --filters 'Name=tag:kubernetes.io/role/elb,Values=1' \
  --query 'Subnets[].SubnetId'
[
  "subnet-xxxxxxxxxxxxxxxxx",
  "subnet-xxxxxxxxxxxxxxxxx",
  "subnet-xxxxxxxxxxxxxxxxx"
]

Step 6: Load Balancer Controller 재시작

kubectl -n kube-system rollout restart deploy aws-load-balancer-controller
deployment.apps/aws-load-balancer-controller restarted

Step 7: 결과 확인

kubectl describe ingress/ui -n ui

서브넷 문제는 해결되었지만, 이번에는 IAM 권한 오류가 발생했다. 다음 시나리오로 넘어가자.


5. 시나리오 2: IAM 권한 부족으로 ALB 생성 실패

5.1. 현상 확인

Ingress 이벤트에서 AccessDenied 에러가 발생한다.

Warning  FailedDeployModel  68s  ingress  Failed deploy model due to AccessDenied:
         User: arn:aws:sts::xxxxxxxxxxxx:assumed-role/alb-controller-xxx/...
         is not authorized to perform: elasticloadbalancing:CreateLoadBalancer
         status code: 403

5.2. 원인 분석

AWS Load Balancer Controller는 IRSA(IAM Roles for Service Accounts) 를 통해 AWS API를 호출한다. 컨트롤러의 ServiceAccount에 연결된 IAM Role에 필요한 권한이 부족한 상태이다.

ServiceAccount를 확인해보자.

kubectl get serviceaccounts -n kube-system \
  -l app.kubernetes.io/name=aws-load-balancer-controller -o yaml
apiVersion: v1
items:
  - apiVersion: v1
    kind: ServiceAccount
    metadata:
      annotations:
        eks.amazonaws.com/role-arn: arn:aws:iam::xxxxxxxxxxxx:role/alb-controller-xxx
      name: aws-load-balancer-controller-sa
      namespace: kube-system

eks.amazonaws.com/role-arn 어노테이션에 지정된 IAM Role에 elasticloadbalancing:CreateLoadBalancer 등의 권한이 없는 것이다.

컨트롤러 로그에서도 동일한 에러를 확인할 수 있다.

kubectl logs -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller
{"level":"error","ts":"...","msg":"Reconciler error","controller":"ingress",
 "error":"AccessDenied: ... is not authorized to perform:
 elasticloadbalancing:CreateLoadBalancer ..."}

5.3. 해결 과정

Step 1: 올바른 IAM Policy 부착

워크샵에서는 올바른 정책이 미리 생성되어 있으므로, 환경 변수를 사용하여 교체한다.

# 올바른 정책 부착
aws iam attach-role-policy \
  --role-name ${LOAD_BALANCER_CONTROLLER_ROLE_NAME} \
  --policy-arn ${LOAD_BALANCER_CONTROLLER_POLICY_ARN_FIX}

Step 2: 잘못된 IAM Policy 제거

# 잘못된 정책 분리
aws iam detach-role-policy \
  --role-name ${LOAD_BALANCER_CONTROLLER_ROLE_NAME} \
  --policy-arn ${LOAD_BALANCER_CONTROLLER_POLICY_ARN_ISSUE}
💡 실무에서는 공식 문서에서 제공하는 IAM Policy JSON을 기반으로 정책을 구성해야 한다.

Step 3: Load Balancer Controller 재시작

kubectl -n kube-system rollout restart deploy aws-load-balancer-controller
deployment.apps/aws-load-balancer-controller restarted
kubectl -n kube-system wait --for=condition=available \
  deployment/aws-load-balancer-controller

Step 4: ALB 생성 확인

ALB 생성에는 몇 분이 소요될 수 있다. Ingress의 ADDRESS 필드를 확인한다.

kubectl get ingress -n ui ui \
  -o jsonpath="{.status.loadBalancer.ingress[*].hostname}{'\n'}"
k8s-ui-ui-5ddc3ba496-1208241872.us-west-2.elb.amazonaws.com

ALB가 성공적으로 생성되었다! 하지만 해당 URL로 접속하면...

"Backend service does not exist" 에러가 발생한다. 다음 시나리오에서 이 문제를 해결해보자.


6. 시나리오 3: Service Endpoint 미등록 — Selector 불일치

6.1. 현상 확인

ALB는 생성되었으나 브라우저에서 접속하면 503 에러 또는 "Backend service does not exist" 메시지가 표시된다.

6.2. 원인 분석 — Ingress Backend Service 이름 불일치

Ingress 설정을 확인해보자.

kubectl get ingress/ui -n ui -o yaml
spec:
  ingressClassName: alb
  rules:
  - http:
      paths:
      - backend:
          service:
            name: service-ui    # ← 문제: 존재하지 않는 Service 이름
            port:
              number: 80
        path: /
        pathType: Prefix

실제 Service 이름은 ui인데, Ingress에서는 service-ui를 참조하고 있다.

Ingress 수정

kubectl apply -k ~/environment/eks-workshop/modules/troubleshooting/alb/creating-alb/fix_ingress

수정된 Ingress 설정:

spec:
  ingressClassName: alb
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: ui          # ← 올바른 Service 이름
            port:
              number: 80

하지만 Ingress를 수정한 후에도 여전히 503 에러가 발생한다.

6.3. 원인 분석 — Service Selector 불일치

Service의 Endpoint를 확인해보자.

kubectl -n ui get endpoints ui
NAME   ENDPOINTS   AGE
ui     <none>      13d

Endpoint가 비어 있다. Service가 어떤 Pod도 선택하지 못하고 있다는 의미이다.

Deployment의 Pod 라벨과 Service의 Selector를 비교해보자.

Deployment의 Pod 라벨:

kubectl -n ui get deploy/ui -o yaml
spec:
  template:
    metadata:
      labels:
        app.kubernetes.io/component: service
        app.kubernetes.io/instance: ui
        app.kubernetes.io/name: ui            # ← Pod 라벨

Service의 Selector:

kubectl -n ui get svc ui -o yaml
spec:
  selector:
    app.kubernetes.io/component: service
    app.kubernetes.io/instance: ui
    app.kubernetes.io/name: ui-app            # ← Selector (불일치!)
항목 Pod Label Service Selector 일치 여부
app.kubernetes.io/component service service
app.kubernetes.io/instance ui ui
app.kubernetes.io/name ui ui-app

app.kubernetes.io/name의 값이 다르기 때문에 Service가 Pod를 찾지 못하는 것이다.

6.4. 해결 과정

Service Selector를 수정한다.

kubectl apply -k ~/environment/eks-workshop/modules/troubleshooting/alb/creating-alb/fix_ui
💡 수동으로 수정할 수도 있다:

6.5. 최종 확인

Endpoint가 정상적으로 등록되었는지 확인한다.

kubectl -n ui get endpoints ui
NAME   ENDPOINTS        AGE
ui     10.42.x.x:8080   13d

이제 브라우저에서 ALB URL로 접속하면 UI 애플리케이션이 정상적으로 표시된다.


7. 정리

AWS Load Balancer Controller의 동작 원리

  1. Controller: Kubernetes API Server의 Ingress 이벤트를 감시하며, 조건에 맞는 Ingress가 감지되면 대응하는 AWS 리소스를 생성한다.
  2. ALB: 각 Ingress 리소스에 대해 하나의 ALB가 생성된다. internet-facing / internal 스키마를 어노테이션으로 지정한다.
  3. Target Group: Ingress에 정의된 각 Kubernetes Service에 대해 생성된다. IP 모드(target-type: ip)를 사용하면 Pod IP로 직접 등록된다.
  4. Listener: Ingress 어노테이션에 지정된 포트별로 생성되며, 기본은 80/443이다.
  5. Rules: Ingress의 Path 규칙에 따라 트래픽을 적절한 Target Group으로 라우팅한다.

이번 실습에서 배운 트러블슈팅 체크리스트

Subnet 관련

  • Public Subnet → kubernetes.io/role/elb=1 태그 필수
  • Private Subnet → kubernetes.io/role/internal-elb=1 태그 필수
  • 서브넷이 올바른 Route Table에 연결되어 있는지 확인

IAM 관련

  • LB Controller의 ServiceAccount에 IRSA가 올바르게 설정되어 있는지 확인
  • IAM Role에 ELB 관련 권한(elasticloadbalancing:*, ec2:Describe*, iam:CreateServiceLinkedRole 등)이 포함되어 있는지 확인
  • 컨트롤러 로그에서 AccessDenied 에러 확인

Service/Ingress 관련

  • Ingress의 backend.service.name이 실제 Service 이름과 일치하는지 확인
  • Service의 selector가 Pod의 labels와 정확히 일치하는지 확인
  • kubectl get endpoints로 Endpoint 등록 여부 확인

유용한 트러블슈팅 명령어 모음

# Ingress 이벤트 확인 (첫 번째로 확인해야 할 곳)
kubectl describe ingress/<name> -n <namespace>

# LB Controller 로그 확인
kubectl logs -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller

# Service Endpoint 확인
kubectl get endpoints <service-name> -n <namespace>

# Pod 라벨 확인
kubectl get pods -n <namespace> --show-labels

# ServiceAccount의 IAM Role 확인
kubectl get sa -n kube-system <sa-name> -o yaml

# ALB 존재 여부 확인
aws elbv2 describe-load-balancers \
  --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-<namespace>`) == `true`]'

# Subnet 태그 확인
aws ec2 describe-subnets \
  --filters 'Name=tag:kubernetes.io/role/elb,Values=1' \
  --query 'Subnets[].SubnetId'

8. 리소스 정리

실습이 끝나면 비용 발생을 방지하기 위해 리소스를 정리한다.

# 1. 랩 환경 제거
delete-environment

# 2. EKS 클러스터 삭제
eksctl delete cluster $EKS_CLUSTER_NAME --wait

# 3. CloudFormation 스택(IDE) 삭제
# AWS Console → CloudFormation → eks-workshop-ide 스택 삭제

참고 자료