0. 이 글을 쓰는 이유
쿠버네티스 내부 통신이 아닌 경우 ALB + 쿠버네티스 조합에서 하나의 ALB로 여러 서비스까지 라우팅 되는 과정을 정리해 보기 위함
1. 환경
- AWS ALB
- EKS
그림 및 최종 정리본은 본 글을 맡긴 후 정리된 AI 생성물입니다.
2. ALB ENI ip 찾기
pod에서는 요청을 보낸다.
요청 URL : https://service-a.example.com/api/orders
이후 CoreDNS까지 나가게 되는데 일단 FQDN(Fully Qualified Domain Name)(QNAME으로 service-a.example.com A를 받게 된다)으로 core-dns에 들어가고 나면 거기서는 앞에 example.com에 대해 모르는 도메인이라고 판단한다.
CoreDNS도 모르는 도메인이기에 CoreDNS의 /etc/resolv.conf에 적힌 nameserver (상위 리졸버, VPC DNS Resolver)로 포워딩을 하게 된다.
(쉽게 생각하면 CoreDNS에서 모르니까 더 위로 책임 전가를 한다고 생각하자)
이후 VPC의 DNS Resolver (VPC CIDR의 .2 (+2) 주소, 쉽게 생각하면 AWS에서 미리 할당해 준 DNS 서버)로 보내고 거기서 리졸버가 탐색을 하게 된다. 여기에 엮인 Private Hosted Zone이 현재 VPC에 연결이 되어있는지를 먼저 본다.
이제 여기서 여기서 2가지 경우로 나뉜다. (여전히 QNAME은 service-a.example.com A)
private zone에 걸려있거나 그 어느 zone에도 걸리지 않은 경우.
private zone에 속하는 경우
- - 거기서 바로 꺼냄.
private zone에 속하지 않음.
- - 재귀 탐색 시작
DNS 해석기(Route53)에서는 Alias 레코드를 보고 매칭되는 것을 찾는다. (사전에 Route53에 alias로 service-a, service-b 등이 모두 등록되어 있다고 가정하자.)
이 과정에서 동일한 ALB의 IP들 (10.x.x.A, 10.x.x.B)로 해석됨 -> 같은 ALB를 가리키는 별칭이기 때문
재귀 리졸브 과정 (여전히 FQDN으로 동작함. service-a.example.com A)
- roote name server에게 FQDN을 물어봄 -> 몰라서 .com 담당자에게 넘김
- .com 담당자(TLD 서버)에게 FQDN을 물어봄 -> 몰라서 example.com 담당자에게 넘김
- example.com를 담당하고 있는 authoritative server에게 FQDN을 물어봄 -> 정답을 알고 있고 service-a의 ALB ENI ip를 넘겨줌


이제 pod가 요청해야 하는 ip를 받았으면 그 ip로 TCP 443 연결을 시작한다.
여기서 절대 혼동하면 안되는게 받은 요청대상의 ip는 ALB의 ENI ip이다.

3. pod to ALB, ALB to pod
이제 pod -> core-dns -> vpc를 이용해 요청대상의 ALB ip를 받았고 이제 ALB와 통신을 할 차례.
pod의 TCP요청이 ALB로 들어감. pod에서는 ALB로 ClientHello를 보냄.
ALB에서는 pod에서 온 SNI(Servcer Name Indication)을 보고 인증서를 찾기 시작하고 해당 인증서와 함께 ServerHello와 인증서를 보내게 됨.
pod에서는 검증 및 키 교환을 하게 되고(HTTPS 의 그것) 이제 Finished 패킷을 서로 교환하게 되면 터널이 완성됨
이제 /api/orders의 요청이 나간다.
pod -> ALB로 나가면서 HTTP Client에서는 요청 url을 보고 헤더에 url을 넣어주고 ALB에서는 load balancer controller를 통해 들어온 target group 목록을 보고 여기에 매칭되는 target group을 찾고 현재 해당 target group에서 동작하는 pod들 중 healthy상태의 pod들 중 하나의 ip를 찾게 되고 거기로 연결하게 된다. 이후 응답은 이렇게 요청한 것의 반대로 진행하게 된다.
여기서 하나 걸리는건 이건 target-type이 ip 모드일 때 이렇게 된다는 것이다. instance모드일 때는 달라진다.
(참고로 이 모드는 Ingress의 alb.ingress.kubernetes.io/target-type 로 정한다)
instance모드일 때는 pod의 ip가 아닌 node의 ip를 알려주게 된다. (NodeIP:NodePort) 이렇게 되면 요청은 ALB에서 kube-proxy를 통하게 되고 여기서 kube-proxy에서는 해당 NodePort를 보고 어느 service로의 요청인지 판단 후 해당 서비스 pod들 중 적당한 pod를 골라 해당 pod로 전달하게 된다.
여기서 중요한게 하나 있다. 고른 pod가 다른 노드에 있으면 출발지 주소도 노드 자신으로 바꿔서(SNAT) 보낸다. 그래야 응답이 자기한테 돌아오기 때문이다.

이제 해당 처리 후 응답을 하는 과정에서는 응답 pod에서는 node로 응답을 하게 되고 node에서는 conntrack을 보고 요청 왔던 경로인 ALB로 응답을 보내고 ALB에서는 요청이 왔던 pod로 응답을 보내게 된다.

위 내용들을 모두 정리한 최종 정리
╔══════════════════════════════════════════════════════════════════════════════╗
║ 최종본: 파드의 curl https://service-a.example.com/api/orders ║
║ 한 줄이 밟는 전 과정 (ip / instance 모드 포함) ║
╚══════════════════════════════════════════════════════════════════════════════╝
━━━━━━━ [0단계. DNS — 연결하기 전, 이름을 IP로. 트래픽 경로가 아니라 조회임] ━━━━━━━
호출 파드 10.20.7.41
│ "service-a.example.com A?" ← 항상 전체 이름 + 레코드 타입(A=IPv4)
▼
CoreDNS ── 클러스터 도메인(cluster.local) 아님 → forward . /etc/resolv.conf
│ (도메인 계층 분석 없음 — 전부 알거나 전부 모르거나, 통째로 중계)
▼
VPC 리졸버 10.20.0.2 (VPC CIDR+2)
│ 존 판정(longest match): example.com 프라이빗 존이 이 VPC에 연결됨 → 걸림
│ → 퍼블릭 재귀(루트→TLD→권위) 안 탐. 존 안의 ALIAS 레코드 발견
│ → ALIAS: IP 저장 안 함, ALB 리소스 참조 → 질의 시점에 현재 ENI IP 조회
▼
응답: 10.20.9.130, 10.20.11.52 ← ★ 타깃 그룹/파드 IP가 아니라 "ALB의 ENI"임.
│ service-a/b/c 모두 같은 답 — 서비스 구분
│ 정보는 DNS에서 소실되고, 요청 안에 실려 감
▼
파드가 하나 선택 (10.20.9.130)
━━━━━━━ [1단계. TCP — 연결 ①의 바닥 깔기] ━━━━━━━
호출 파드 10.20.7.41:43712 ──SYN──▶ ALB ENI 10.20.9.130:443
(ephemeral 포트) ◀─SYN-ACK─
──ACK──▶ 3-way 완료. 커넥션 존재 시작
━━━━━━━ [2단계. TLS 핸드셰이크 — 연결 ① 위에 터널 만들기] ━━━━━━━
호출 파드 (TLS 클라이언트) ALB (TLS 서버)
│ │
│─① ClientHello ────────────────────────▶│ SNI: "service-a.example.com"
│ (아직 키 합의 전 = 필연적으로 평문. │ ← 인증서가 아니라 이름표(문자열).
│ TLS버전·암호스위트 목록 동봉) │ 인증서는 전부 서버 쪽에만 있음
│ │
│ │ ALB: 걸려 있는 인증서들의 SAN 대조
│ │ cert-1: SAN=service-a.example.com ✓
│ │ cert-2: SAN=*.other.com
│ │ → cert-1 선택
│◀─② ServerHello + Certificate ──────────│ cert-1 제시 (leaf+intermediate 체인)
│ │
│ 파드: 검증 4종 실행 │
│ ① 체인: leaf→intermediate→내 신뢰저장소의 루트 도달 ✓
│ ② 기간: NotBefore~NotAfter 안 ✓ │
│ ③ 호스트명: 내가 찾아간 이름 service-a.example.com ∈ SAN ✓
│ (ALB 자체 도메인 k8s-xx.elb...로 불렀으면 여기서 사망)
│ ④ 폐기: OCSP ✓ │
│─③ 키 교환 ─────────────────────────────▶│
│◀─④ Finished ──────────── Finished ────▶│
│════════════ TLS 터널 완성 ═════════════│ ← 시작 주체는 파드, 완성은 왕복 끝에
│ │
━━━━━━━ [3단계. HTTP — 터널 안으로 실제 요청 (연결 ① 계속)] ━━━━━━━
│══▶ (암호화됨) │
│ GET /api/orders HTTP/1.1 │
│ Host: service-a.example.com ← SNI와 같은 문자열, 다른 계층·다른 용도
│ ▼
│ ┌─ ALB 내부 판단 (요청은 여기서 소비되고 재생산됨) ──┐
│ │ 1. TLS 종단 — 복호화. 터널은 여기서 끝남 │
│ │ (그래서 Host를 읽을 수 있음) │
│ │ 2. Host 헤더 읽음: "service-a.example.com" │
│ │ 3. 규칙 표 대조 (우선순위 순): │
│ │ IF Host==service-a → forward tg-service-a ✓ │
│ │ IF Host==service-b → forward tg-service-b │
│ │ default → 404 │
│ │ ※ 이 표의 출처: 각 Ingress의 host를 AWS LB │
│ │ Controller가 읽어 등록 (순서=group.order) │
│ │ 4. tg-service-a 명부를 펼침... │
│ └──────────────────────┬─────────────────────────────┘
│ │
│ ★★ 갈림길 — 명부에 "뭐가 적혀 있느냐"가 모드임 ★★
│ │
│ ┌────────────────────────────┴────────────────────────────┐
│ ▼ ▼
━━━━ [4단계-A. ip 모드 — 우리 구성] ━━━━ ━━━━ [4단계-B. instance 모드] ━━━━
명부 tg-service-a: 명부 tg-service-a:
10.20.7.88:8080 (healthy) ← 파드 IP 10.20.5.10:31234 ← 노드IP:NodePort
10.20.8.63:8080 (healthy) (EndpointSlice 10.20.6.20:31234 (파드 위치
의 사본) 정보 없음)
ALB가 파드를 직접 선택 → 10.20.7.88 ALB는 노드까지만 선택 → 10.20.5.10
[연결 ②] ALB ─새 TCP─▶ 파드 10.20.7.88:8080 [연결 ②] ALB ─새 TCP─▶ 노드:31234
│ (보통 평문 HTTP — ①의 터널 연장 아님. │ kube-proxy(iptables)가 수신:
│ TG를 HTTPS로 잡으면 "별도" TLS를 새로 맺음) │ "31234=service-a의 NodePort.
│ GET /api/orders │ Service 명부에서 파드 추첨"
│ Host: service-a.example.com │ ├ 이 노드의 파드 → 전달
│ X-Forwarded-For: 10.20.7.41 │ └ 다른 노드의 파드
│ ← 대상 파드가 보는 출발지는 ALB IP라서, │ → 노드 간 홉 추가
│ 원 클라이언트는 이 헤더로만 전해짐 ▼
│ 이 사이: VPC 라우팅(local)뿐 파드 도착 — 선택 주체가
│ CoreDNS ✕ Service ✕ kube-proxy ✕ kube-proxy로 넘어간 2단 분배
▼
파드 10.20.7.88의 앱 컨테이너 처리
│
━━━━━━━ [5단계. 응답 — 왔던 길을 정확히 거꾸로 (두 모드 공통)] ━━━━━━━
│
파드 ──200 OK──▶ ALB (연결 ②로)
│ ALB가 연결 ①의 터널로 재암호화
호출 파드 ◀══ 200 OK ══ ALB (연결 ①로)
│
▼
응답 수신. 이후 요청은 지름길: DNS는 TTL 내 캐시, 연결 ①은 keep-alive 재사용
(①과 ②의 keep-alive는 서로 다른 연결이라 각자 따로 동작함)
╔══════════════════════════════════════════════════════════════════════════════╗
║ 계층별 결정 요약 — 같은 문자열 "service-a.example.com"의 세 번의 등장 ║
║ DNS(0단계): 어느 ALB로 가나 → ENI IP (여기서 서비스 구분 소실) ║
║ SNI(2단계): 어느 인증서를 받나 → cert-1 (평문, 터널 생기기 전) ║
║ Host(3단계): 어느 타깃 그룹인가 → tg-service-a (터널 안, ALB가 복호화) ║
║ 그리고 어느 파드인가: ip 모드=ALB가, instance 모드=kube-proxy가 결정 ║
╚══════════════════════════════════════════════════════════════════════════════╝
다음 글 : ALB를 통하지 않고 kubernetes안에서 바로 pod to pod통신을 하게 되는 경우