Published on

EKS Calico NetworkPolicy 적용 후 통신 끊김 복구

Authors

서론

EKS에 Calico를 붙이고 NetworkPolicy를 적용하는 순간, 잘 돌던 서비스가 한꺼번에 죽는 경험은 꽤 흔합니다. 특히 default deny를 먼저 걸고 예외 규칙을 추가하는 방식으로 롤아웃하면, “어디가 막혔는지” 자체가 보이지 않아 장애가 길어집니다. 이 글은 EKS + Calico NetworkPolicy 적용 직후 통신 끊김을 빠르게 진단하고 복구하기 위한 실전 체크리스트입니다.

핵심은 다음 3가지를 분리해서 보는 것입니다.

  1. Pod ↔ Pod (동일/다른 네임스페이스)
  2. Pod ↔ DNS(CoreDNS) / kube-system 필수 컴포넌트
  3. Pod ↔ 외부(인터넷, AWS API, DB 등) egress / ingress

또한 “통신 끊김”이 실제로는 Service Endpoints가 0이라 라우팅 대상이 없는 문제인 경우도 많습니다. 이 경우는 NetworkPolicy가 아니라 레이블/셀렉터/엔드포인트 문제이므로 먼저 구분해야 합니다. 필요하면 아래 글도 같이 확인하세요: EKS에서 Pod는 뜨는데 Service Endpoints가 0일 때

장애 패턴 5가지(가장 흔한 순)

1) default deny 이후 DNS가 막혀서 “모든 게” 죽음

대부분의 애플리케이션은 외부/내부 호출 전에 DNS를 사용합니다. DNS가 막히면 증상은 다음처럼 나타납니다.

  • curl https://api...Could not resolve host
  • DB 접속 실패(호스트네임 사용)
  • AWS SDK가 STS/S3 엔드포인트를 못 찾음

2) kube-system(특히 CoreDNS)로의 egress를 허용하지 않음

네임스페이스 기준 정책을 만들 때 kube-system을 별도 취급하지 않으면 DNS가 막힙니다.

3) NodeLocal DNSCache/hostNetwork, 혹은 CNI 경로 차이로 규칙이 안 맞음

클러스터에 NodeLocal DNSCache가 있거나, CoreDNS가 특정 IP로 노출되는 구성이라면 podSelector만으로는 매칭이 깨질 수 있습니다.

4) Ingress는 열었는데 return traffic(응답)이 막힘

Kubernetes NetworkPolicy는 기본적으로 stateful하게 동작해 “응답 트래픽은 허용”되는 것이 일반적이지만, Calico의 GlobalNetworkPolicy, 정책 우선순위(order), 혹은 특정 조합에서는 의도치 않게 응답이 드랍되는 케이스가 있습니다.

5) 외부 egress(특히 443) 전부 막힘

이미지 pull, STS, S3, 외부 API 호출이 동시에 실패합니다. 이때는 DNS 문제와 함께 발생하는 경우가 많아 “둘 다” 열어야 정상화됩니다.

0단계: 이게 NetworkPolicy 문제인지 먼저 확정

아래 3가지를 먼저 확인하면 불필요한 삽질을 줄일 수 있습니다.

A. Service Endpoints가 정상인지

kubectl -n <ns> get svc <svc>
kubectl -n <ns> get endpoints <svc> -o wide
kubectl -n <ns> get endpointslices -l kubernetes.io/service-name=<svc>
  • Endpoints/EndpointSlice가 0이면 정책 이전에 라우팅 대상이 없음입니다.
  • 이 경우는 레이블 셀렉터 불일치, readiness 실패, targetPort 불일치 등이 원인입니다.

B. 실제로 NetworkPolicy가 적용되는 네임스페이스인지

Kubernetes NetworkPolicy는 해당 네임스페이스에 정책이 존재할 때만(그리고 CNI가 지원할 때만) 동작합니다.

kubectl get netpol -A
kubectl -n <ns> describe netpol

C. Calico가 정책 엔진으로 동작 중인지

EKS에서 Calico를 “정책 엔진”으로 쓰는지(혹은 AWS VPC CNI + Calico policy only 구성인지)에 따라 디버깅 포인트가 달라집니다.

kubectl -n kube-system get pods -l k8s-app=calico-node
kubectl -n kube-system get pods -l k8s-app=calico-kube-controllers

(설치 방식에 따라 라벨은 다를 수 있습니다.)

1단계: 가장 먼저 복구해야 할 최소 허용 규칙(DNS)

장애 복구의 1순위는 DNS입니다. 아래는 default deny가 이미 걸려 있는 네임스페이스에서, 해당 네임스페이스의 Pod들이 CoreDNS로 질의할 수 있게 하는 예시입니다.

> 전제: CoreDNS가 kube-system에 있고, 라벨이 k8s-app: kube-dns(EKS 기본)라고 가정

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: <your-namespace>
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

DNS가 여전히 안 되면 체크할 것

  1. CoreDNS 라벨 확인
kubectl -n kube-system get pods --show-labels | grep -E 'coredns|kube-dns'
  1. CoreDNS Service 확인(ClusterIP)
kubectl -n kube-system get svc kube-dns -o wide
  1. NodeLocal DNSCache 사용 여부
kubectl -n kube-system get ds | grep -i nodelocal
kubectl -n kube-system get svc | grep -i nodelocal

NodeLocal DNSCache를 쓰는 경우, 실제 질의 대상이 169.254.20.10 같은 링크로컬 IP일 수 있어 podSelector 기반 허용이 안 맞습니다. 이때는 CIDR 기반 허용(Calico의 NetworkPolicy 확장 또는 GlobalNetworkPolicy) 혹은 NodeLocal DNSCache Pod로의 트래픽 허용이 필요합니다.

2단계: default deny(ingress/egress) 설계 실수 정리

보통 다음과 같은 형태로 시작합니다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: <your-namespace>
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

이 자체는 맞지만, 여기에 예외를 붙이는 방식이 중요합니다.

권장 접근

  • **Egress는 최소한 DNS + 필수 외부(예: 443)**를 먼저 열고 점진적으로 좁히기
  • Ingress는 “받는 쪽” 기준으로 열되, 서비스 단위로 정책을 쪼개기
  • 네임스페이스 간 통신은 namespaceSelector + podSelector 조합으로 명시하기

3단계: 서비스 간 통신 끊김(동일 NS/다른 NS) 복구 패턴

A. 동일 네임스페이스 내 통신 허용

마이크로서비스가 같은 NS에 있고, 일단 정상화가 급하면 아래처럼 동일 NS 내부 허용을 임시로 넣고 원인을 좁힐 수 있습니다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-within-namespace
  namespace: <your-namespace>
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector: {}

B. 다른 네임스페이스에서 특정 앱만 허용

예: frontend NS의 app=webbackend NS의 app=api로 접근 허용

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: backend
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: frontend
          podSelector:
            matchLabels:
              app: web
      ports:
        - protocol: TCP
          port: 8080

여기서 자주 틀리는 지점:

  • namespaceSelector 라벨: Kubernetes 1.21+에서는 kubernetes.io/metadata.name가 기본으로 붙지만, 구버전/특수 환경에서는 다를 수 있습니다.
  • 실제 트래픽 포트: Service 포트(80)와 Pod targetPort(8080)를 혼동

4단계: 외부 egress(인터넷/AWS API) 끊김 해결

운영에서 제일 많이 부딪히는 건 “외부로 나가야 하는데 다 막힘”입니다. 특히 AWS SDK는 STS, KMS, S3 등 여러 엔드포인트를 호출합니다.

빠른 임시 복구(전체 443 허용)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-https
  namespace: <your-namespace>
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - ports:
        - protocol: TCP
          port: 443

이 규칙은 “어디로든 443은 나가게” 하므로 보안적으로는 느슨합니다. 하지만 장애 복구에는 유용하고, 이후에 목적지 CIDR 혹은 egress gateway/프록시로 좁히는 식으로 개선합니다.

AWS API 호출 실패가 IRSA 문제로 보이는 경우

NetworkPolicy로 egress가 막히면 애플리케이션 로그는 종종 AccessDenied, Unable to retrieve credentials, timeout 등으로 섞여 보입니다. 특히 IMDS/STS 경로가 꼬이면 IRSA 문제처럼 보일 수 있습니다. 아래 글을 함께 보면 “권한”과 “네트워크”를 분리하는 데 도움이 됩니다.

5단계: Calico 우선순위/GlobalNetworkPolicy로 인한 의도치 않은 드랍

Kubernetes NetworkPolicy만 쓴다고 생각했는데, 실제로는 Calico의 다음 요소가 개입하는 경우가 있습니다.

  • GlobalNetworkPolicy (클러스터 전체 적용)
  • order(우선순위)
  • preDNAT, applyOnForward 등 고급 옵션

이 경우 “네임스페이스에 allow를 넣었는데도 막힌다”가 발생합니다. 가장 빠른 확인은 Calico CRD 리소스를 조회해 전역 정책이 있는지 보는 것입니다.

kubectl get globalnetworkpolicy.crd.projectcalico.org 2>/dev/null || true
kubectl get globalnetworkpolicy -o wide 2>/dev/null || true
kubectl get felixconfiguration -o yaml 2>/dev/null || true

전역 정책이 있고 deny가 먼저 매칭되면, 네임스페이스 정책을 아무리 추가해도 효과가 없을 수 있습니다. 이때는:

  • 전역 정책에 예외를 추가하거나
  • order를 조정하거나
  • 정책 적용 범위를 selector로 축소

하는 방식으로 해결합니다.

6단계: 디버깅을 “증상 → 계층”으로 쪼개는 커맨드

1) Pod 내부에서 DNS/HTTP 확인

kubectl -n <ns> run -it --rm netshoot \
  --image=nicolaka/netshoot -- bash

# DNS
nslookup kubernetes.default.svc.cluster.local
nslookup <your-service>.<ns>.svc.cluster.local

# Service로 HTTP
curl -sv http://<svc>.<ns>.svc.cluster.local:<port>/health

# Pod IP로 직접
curl -sv http://<pod-ip>:<port>/health
  • Service로는 실패하지만 Pod IP로는 성공: Service/Endpoint/iptables(kube-proxy) 계층 의심
  • 둘 다 실패: NetworkPolicy/Pod readiness/앱 리스닝/포트 불일치 의심

2) 어떤 NetworkPolicy가 선택되는지(셀렉터 검증)

kubectl -n <ns> get pod --show-labels
kubectl -n <ns> get netpol -o yaml

정책의 podSelector가 실제 Pod 라벨과 매칭되는지, namespaceSelector 라벨이 실제 네임스페이스에 존재하는지 반드시 확인합니다.

3) Calico 로그/드랍 확인(가능한 경우)

설치 구성에 따라 다르지만, calico-node(Felix) 로그에서 힌트를 얻을 수 있습니다.

kubectl -n kube-system logs -l k8s-app=calico-node --tail=200

드랍을 더 명확히 보려면 Calico의 flow log/packet capture(예: tcpdump)를 병행합니다.

7단계: 운영에서 안전한 롤아웃 전략

NetworkPolicy는 “한 번에 완벽”이 어렵습니다. 운영에서는 다음 순서를 권장합니다.

  1. 관찰(Observe): 어떤 통신이 필요한지 목록화(서비스 맵, 포트, DNS, 외부 의존성)
  2. Egress부터 최소 허용: DNS(53) + HTTPS(443) 임시 허용으로 장애를 줄임
  3. Ingress를 서비스 단위로 세분화: app=apiapp=web에서만 허용 등
  4. 점진적 제한: 외부 egress를 CIDR/프록시/게이트웨이로 축소
  5. 정책 테스트: staging에서 netshoot 기반의 사전 테스트 스크립트 구축

결론

EKS에서 Calico NetworkPolicy 적용 후 통신이 끊기는 문제는 대부분 “default deny 이후 필수 트래픽(DNS, kube-system, 외부 443)을 먼저 열지 않음”에서 시작합니다. 복구는 어렵지 않지만, DNS → 서비스 엔드포인트 → 네임스페이스/라벨 매칭 → 전역 정책(GlobalNetworkPolicy) 여부 순서로 계층을 나눠 확인해야 시간을 아낄 수 있습니다.

장애가 길어질수록 원인이 네트워크인지(정책) 서비스 디스커버리인지(Endpoints) IAM/IRSA인지가 섞여 보이므로, 위의 최소 허용 정책(DNS/443)로 빠르게 정상화한 뒤 범위를 좁혀가며 재설계하는 접근이 가장 안전합니다.