EKS aws-node ipamd 행(hang) — 파드 ContainerCreating 멈춤

Kubernetes5분조회

비즈니스 요구사항에 부하 테스트가 있어서, 짧은 시간에 파드가 대량으로 생성·삭제되길 반복하는 식으로 부하를 줬습니다. 그러다 노드가 가득 차자 Karpenter 가 새 노드를 붙였는데, 그 노드에 잡힌 파드가 하나도 뜨지 못하고 전부 ContainerCreating 에서 멈췄습니다.

Karpenter 는 노드가 모자라면 자동으로 늘려 주는 오토스케일러입니다.

Warning  FailedCreatePodSandBox  kubelet  Failed to create pod sandbox:
plugin type="aws-cni" name="aws-cni" failed (add):
add cmd: failed to assign an IP address to container

Running인데 멈춰 있던 ipamd

FailedCreatePodSandBoxfailed to assign an IP address 라고 찍혀 있으니, IP 를 나눠주는 쪽을 먼저 봤습니다. 새로 뜬 노드에서만 나는 증상이라 더 그랬습니다. EKS 에서 파드에 IP 를 주는 건 aws-node(VPC CNI)의 ipamd 입니다. 이게 안 돌면 그 노드의 모든 파드가 IP 를 못 받습니다.

그런데 파드 상태는 멀쩡해 보였습니다.

kubectl get po -n kube-system -o wide | grep <문제-노>
# aws-node: 2/2 Running

2/2 Running 이지만 로그를 보니 초기화하다 멈춰 있었습니다.

level=info msg="Starting IPAM daemon..."
level=info msg="Checking for IPAM connectivity..."
level=info msg="Successfully copied CNI plugin binary and config file."
# 이후 로그 없음

파드가 Running 이어도 그 안의 프로세스가 응답을 멈출 수 있습니다. 헬스체크가 프로세스의 실제 동작까지 보지는 않으니, 2/2 Running 만 보고 정상이라고 판단하면 안 됩니다. 로그가 특정 지점에서 끊겼는지를 봐야 합니다.

첫 조치 — 재시작, 그다음 노드 교체

프로세스가 멈춘 거면 다시 띄우면 풀릴 수 있습니다. DaemonSet 이 관리하니 파드를 지우면 같은 노드에 새로 뜹니다.

kubectl delete po -n kube-system <aws-node-pod>

새 파드도 똑같은 지점에서 hang 했습니다. 두 번 다 같은 자리에서 멈춘다는 건 파드가 아니라 노드 쪽 문제라는 뜻이었습니다. 부하 테스트 중이라 이 노드 하나에 오래 매달릴 상황도 아니었고, 그래서 노드를 갈기로 했습니다.

노드를 지우면 그 위의 파드가 전부 쫓겨나니, 무엇이 떠 있고 다른 노드에 복제본이 있는지부터 봅니다.

kubectl get po -A -o wide | grep <문제-노>

다른 노드에 Running 복제본이 있는 워크로드는 노드를 지워도 영향이 없습니다. 단일 파드라도 어차피 IP 를 못 받아 서비스 불가 상태면 잃을 게 없습니다. 확인하고 나서 지웠습니다.

Karpenter 가 관리하는 노드라 do-not-disrupt 어노테이션이 있으면 먼저 떼야 합니다.

kubectl annotate node <문제-노> karpenter.sh/do-not-disrupt-
kubectl delete node <문제-노>

Karpenter 가 대체 노드를 30초 남짓에 띄웠고, 파드가 새 노드에서 IP 를 받아 Running 이 됐습니다.

진짜 원인 — 서브넷 IP 고갈

노드 교체로 서비스는 돌아왔지만, 그건 응급 처치였습니다. “노드를 갈았더니 됐다” 로 끝내면 다음에 또 당합니다. 왜 새 노드마저 IP 를 못 줬는지가 진짜 질문이었고, 답은 그 노드가 속한 서브넷의 사설 IP 가 고갈된 것이었습니다.

확인은 어렵지 않아서, VPC 콘솔에서 그 서브넷을 열면 가용 IPv4 주소 수가 바로 보이고 CLI 로도 한 줄이면 됩니다.

aws ec2 describe-subnets --subnet-ids <subnet-id> \
  --query 'Subnets[].AvailableIpAddressCount'
# [ 3 ]   ← 거의 바닥
서브넷 ID 상태 가용 IPv4 주소 가용 영역
subnet-0ab3f9c1 Available 3 ap-northeast-2a

‘가용 IPv4 주소’ 가 이렇게 몇 개 안 남으면, 그 서브넷에 새로 뜬 노드는 파드에 줄 IP 를 확보하지 못합니다.

이번 부하 테스트는 파드를 짧은 주기로 대량 생성·삭제(churn)하는 형태였습니다. 그 churn 이 서브넷 가용 IP 를 바닥까지 끌어다 썼고, 그 상태에서 뜬 새 노드는 ipamd 가 ENI 에 붙일 IP 자체를 확보하지 못했습니다. 로그가 초기화 도중 멈춘 것처럼 보인 것도 이 맥락에서 읽힙니다 — 프로세스가 죽은 게 아니라 줄 IP 가 없어 나아가질 못한 겁니다.

여기에 하나가 더 겹치는데, 파드를 지워도 그 IP 가 즉시 풀로 돌아오지 않습니다. VPC CNI 에 IP 쿨다운(IP_COOLDOWN_PERIOD, 기본 30초)이 있어서 회수된 IP 를 잠깐 붙잡아 두기 때문입니다. churn 이 그보다 빠르면 반납이 소비를 못 따라가지 못해서 가용 IP 가 0 근처에 머뭅니다.

“그럼 쿨다운을 0 으로 두면 되지 않나” 싶지만, 그게 위험합니다. 쿨다운은 방금 놓아준 IP 에 남아 있던 라우팅·연결 흔적이 정리될 시간을 버는 장치입니다. 0 으로 만들면 반납되자마자 다른 파드에 재할당돼서, 이전 파드로 가야 할 인플라이트 트래픽이 엉뚱한 새 파드로 갈 수 있습니다. 회수를 빠르게 하겠다고 무작정 0 으로 내리는 게 답이 아닌 이유입니다.

그래서 근본 대응은 회수 속도를 쥐어짜는 게 아니라, 애초에 노드가 안 쓰는 IP 를 덜 쥐게 하는 쪽입니다. WARM_IP_TARGET/MINIMUM_IP_TARGET 을 조이고, 필요하면 서브넷을 넓히거나 세컨더리 CIDR 을 붙입니다. 이 이야기는 따로 정리했습니다 — EKS 서브넷 IP 고갈 — 파드 churn과 WARM_IP_TARGET.

2/2 Running 은 컨테이너가 살아 있다는 뜻이지 그 안의 프로세스가 일을 하고 있다는 뜻이 아닙니다. 로그가 특정 지점에서 끊겨 있으면 상태가 초록불이어도 그 프로세스부터 봅니다.

그리고 파드를 재생성해도 같은 자리에서 멈추면 파드가 아니라 노드나 그 바깥을 의심합니다. 이번에는 서브넷 IP 가 바닥나 있었고, 노드를 갈아 끼운 건 그때까지 버티는 응급 처치였습니다.

참고

  1. 불러오는 중