infra
Platform

모듈 맵

[Network] Docker & Kubernetes 컨테이너 네트워크 실전 디버깅

0 / 35 완료

펼치기
0 / 35 완료0%

Networking · 34 / 35

[Network] Docker & Kubernetes 컨테이너 네트워크 실전 디버깅

docker network inspect, kubectl exec, port-forward로 컨테이너 간 통신 장애를 5분 안에 원인 특정하는 체계적 방법론

🚨INCIDENT ALERT
HIGH

컨테이너 안에서는 API가 정상인데 호스트에서는 8080 접속이 실패합니다. Docker 네트워크, 포트 바인딩, iptables NAT, Kubernetes Service가 한꺼번에 얽혀 있어 어디서 끊겼는지 보이지 않습니다.

컨테이너 네트워크 장애는 레이어를 나눠 보면 패턴이 있습니다. 컨테이너 내부, 호스트 포트, 서비스 규칙을 차례로 확인해야 합니다.

컨테이너 네트워크 디버깅 — Docker & Kubernetes

docker-compose up을 실행했는데 API 서버에서 DB로 연결이 안 됩니다. 두 컨테이너 모두 실행 중이고, 포트도 맞고, 코드도 어제까지 잘 됐는데 — 갑자기 connection refused입니다. 서버 네트워크라면 pingnc로 빠르게 진단하겠지만, 컨테이너 안에는 도구가 없고 어디서부터 봐야 할지 막막합니다. 컨테이너 네트워크는 가상 브릿지, 내장 DNS, 네임스페이스 격리 등 여러 추상화 계층이 쌓여있어 베어메탈 장애와는 다른 접근이 필요합니다. 이 챕터에서는 Docker와 Kubernetes 양쪽에서 통신 장애가 발생했을 때 5분 안에 원인을 특정하는 체계적인 방법을 배웁니다.


학습 목표

이번 챕터에서 배울 것
  • 1Docker bridge 네트워크 구조와 docker network inspect, ip addr로 현재 상태 파악하기
  • 2컨테이너 간 통신 / 호스트→컨테이너 / 컨테이너→외부 각 경로의 차이 이해하기
  • 3K8s ClusterIP → NodePort → LoadBalancer 타입별 네트워크 경로 추적하기
  • 4kubectl exec, kubectl port-forward로 Pod 내부를 직접 점검하는 방법
  • 5docker-compose 네트워크 이름 충돌과 --network 누락 시나리오 진단하기
  • 6dial tcp: lookup xxx on 127.0.0.11:53: no such host 실제 에러 원인 분석 및 해결
실습 환경 준비
Docker 실행 확인
docker info | grep 'Server Version'
테스트용 네트워크 생성
docker network create debug-net
busybox 이미지 준비 (네트워크 진단용)
docker pull busybox:latest
kubectl 컨텍스트 확인 (K8s 실습 시)
kubectl config current-context
실습 디렉터리 생성
mkdir -p /tmp/container-net-debug && cd /tmp/container-net-debug

1. Docker bridge 네트워크 구조

💡개념

Docker 네트워크는 어떻게 생겼는가

컨테이너가 시작될 때 Docker는 각 컨테이너를 **가상 네트워크 인터페이스(veth pair)**로 호스트의 bridge에 연결합니다. 이 bridge가 같은 네트워크 안의 컨테이너들을 연결하는 가상 스위치 역할을 합니다. 이름으로 서로를 찾을 수 있는 건 Docker가 제공하는 내장 DNS 서버(127.0.0.11) 덕분입니다 — 사용자 정의 네트워크에서만 작동합니다.

Docker 네트워크는 어떻게 생겼는가

호스트 OS
├── docker0 (기본 브릿지, 172.17.0.1/16)
│   ├── veth0a3f → [컨테이너 A: 172.17.0.2]  ← 이름 DNS 없음
│   └── veth1b2c → [컨테이너 B: 172.17.0.3]  ← 이름 DNS 없음
│
└── br-abc123 (사용자 정의 네트워크, 172.20.0.1/16)
    ├── veth2d4e → [컨테이너 C: 172.20.0.2]  ← "api"로 DNS 해석 가능
    └── veth3e5f → [컨테이너 D: 172.20.0.3]  ← "db"로 DNS 해석 가능

핵심 차이: 기본 docker0 브릿지는 내장 DNS를 제공하지 않습니다. docker network create로 만든 사용자 정의 네트워크에서만 컨테이너 이름으로 통신할 수 있습니다.

네트워크 현황 파악 — 첫 번째 진단 명령

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/networking/part6/exam_34 && cd /tmp/networking/part6/exam_34

# 현재 Docker 네트워크 목록
docker network ls
# NETWORK ID     NAME          DRIVER    SCOPE
# a1b2c3d4e5f6   bridge        bridge    local   ← 기본(docker0)
# b2c3d4e5f6a7   debug-net     bridge    local   ← 사용자 정의
# c3d4e5f6a7b8   host          host      local
# d4e5f6a7b8c9   none          null      local

# 특정 네트워크 상세 조회 — 핵심 진단 명령
docker network inspect debug-net
🔍실행 후 확인할 것
  • Network 이름컨테이너가 같은 사용자 정의 네트워크에 붙어 있는지 확인합니다
  • PORTS 매핑호스트 포트와 컨테이너 포트가 기대한 방향으로 연결됐는지 봅니다
  • NAT 규칙Docker가 만든 iptables 규칙이 포트 포워딩을 처리하는지 확인합니다

docker network inspect 출력에서 봐야 할 세 가지:

JSON
{
  "Name": "debug-net",
  "IPAM": {
    "Config": [{ "Subnet": "172.20.0.0/16", "Gateway": "172.20.0.1" }]
  },
  "Containers": {
    "abc123...": {
      "Name": "api",          // ← 1. 이 컨테이너가 이 네트워크에 있는가?
      "IPv4Address": "172.20.0.2/16"
    },
    "def456...": {
      "Name": "db",           // ← 2. 함께 통신해야 할 컨테이너가 같은 항목에 있는가?
      "IPv4Address": "172.20.0.3/16"   // ← 3. IP는 같은 서브넷인가?
    }
  }
}

컨테이너 내부에서 인터페이스 확인

Docker
# 컨테이너 안에서 IP 확인 (ip 명령 있을 경우)
docker exec api ip addr show eth0
# 2: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
#     inet 172.20.0.2/16 brd 172.20.255.255 scope global eth0

# ip 명령이 없는 최소 이미지라면
docker exec api cat /etc/hosts
# 127.0.0.1    localhost
# 172.20.0.2   abc123def456 api   ← 자기 자신 항목

# DNS 해석 확인
docker exec api cat /etc/resolv.conf
# nameserver 127.0.0.11   ← Docker 내장 DNS
# options ndots:0

# 실제 DNS 해석 테스트
docker exec api nslookup db 2>/dev/null || \
  docker exec api getent hosts db
# 172.20.0.3    db          ← db 컨테이너 IP로 정상 해석

2. 실습: 세 가지 통신 경로 직접 확인

컨테이너 통신에는 세 가지 경로가 있고, 장애 증상이 다릅니다. 경로를 정확히 이해해야 어느 계층에서 막혔는지 빠르게 특정할 수 있습니다.

로컬 터미널
# 실습 환경 준비
cd /tmp/container-net-debug

# 사용자 정의 네트워크 생성
docker network create debug-net --subnet=172.20.0.0/16

# 컨테이너 A: 간단한 HTTP 서버 (포트 8080 노출)
docker run -d --name server \
  --network debug-net \
  -p 8080:80 \
  nginx:alpine

# 컨테이너 B: 클라이언트 역할 (busybox)
docker run -d --name client \
  --network debug-net \
  busybox sleep 3600

경로 1: 컨테이너 → 컨테이너 (같은 네트워크)

Docker
# client 컨테이너 안에서 server 컨테이너로 요청
docker exec client wget -qO- http://server/
# <!DOCTYPE html>  ← nginx 응답 — 컨테이너 이름 DNS 해석 + 직접 통신 성공

# 이름이 안 된다면 IP로 시도해 DNS 문제인지 네트워크 문제인지 분리
SERVER_IP=$(docker inspect server --format '{{.NetworkSettings.Networks.debug-net.IPAddress}}')
echo "server IP: $SERVER_IP"
docker exec client wget -qO- http://$SERVER_IP/
# 이름은 실패하는데 IP가 된다 → DNS 문제 (네트워크 분리 또는 이름 오타)
# IP도 실패한다 → 네트워크 자체 문제 (같은 네트워크에 없음)

경로 2: 호스트 → 컨테이너 (-p 포트 바인딩)

로컬 또는 서버
# 호스트에서 포트 바인딩을 통해 접근
curl -s http://localhost:8080/
# <!DOCTYPE html>  ← 호스트 포트 8080 → 컨테이너 포트 80 → nginx 응답

# 포트 바인딩 현황 확인
docker port server
# 80/tcp -> 0.0.0.0:8080
# 80/tcp -> :::8080

# 호스트 측 iptables NAT 규칙 확인 (포트 포워딩 구현체)
sudo iptables -t nat -L DOCKER --line-numbers | grep 8080
# DNAT  tcp  --  anywhere  anywhere  tcp dpt:8080 to:172.20.0.2:80

# 바인딩은 됐는데 접근이 안 된다면
ss -tlnp | grep 8080
# Docker가 0.0.0.0:8080을 리스닝 중인지 확인

경로 3: 컨테이너 → 외부 인터넷

Docker
# 컨테이너에서 외부로 요청 (NAT masquerade 경로)
docker exec client wget -qO- --timeout=5 http://httpbin.org/ip
# {"origin": "203.0.113.xxx"}  ← 호스트의 공인 IP로 나가는 것 확인

# 외부 통신이 안 된다면 iptables MASQUERADE 규칙 확인
sudo iptables -t nat -L POSTROUTING | grep MASQUERADE
# MASQUERADE  all  --  172.20.0.0/16  !172.20.0.0/16

# DNS 해석 확인 (외부 도메인)
docker exec client nslookup google.com
# Server:    127.0.0.11    ← Docker 내장 DNS
# Non-authoritative answer: google.com → 8.8.8.8 등으로 포워딩

3. K8s Service 타입별 네트워크 경로

💡개념

ClusterIP → NodePort → LoadBalancer 경로 추적

Kubernetes에서 Pod에 접근하는 경로는 Service 타입에 따라 완전히 다릅니다. 장애 상황에서 어느 경로가 끊겼는지 알아야 올바른 도구를 선택할 수 있습니다.

[요청자]           [Service 타입]        [실제 경로]
─────────────────────────────────────────────────────────
클러스터 내 Pod  → ClusterIP             kube-proxy → 대상 Pod
                   (10.96.x.x, 내부만)

외부 사용자      → NodePort              호스트포트(30000-32767)
                   (모든 노드의 포트)     → kube-proxy → 대상 Pod

외부 사용자      → LoadBalancer          클라우드 LB IP
                   (클라우드 프로비전)    → NodePort → kube-proxy → 대상 Pod

ClusterIP — 클러스터 내부 DNS 기반 통신

ClusterIP는 <service-name>.<namespace>.svc.cluster.local로 해석됩니다. kube-dns(CoreDNS)가 이 DNS를 제공하고, kube-proxy가 iptables/IPVS 규칙으로 실제 Pod IP로 라우팅합니다.

Kubernetes
# Service 목록 확인
kubectl get svc -n my-namespace
# NAME         TYPE        CLUSTER-IP      PORT(S)    AGE
# api-svc      ClusterIP   10.96.45.12     8080/TCP   2d
# db-svc       ClusterIP   10.96.78.34     5432/TCP   2d

# ClusterIP가 실제 Pod와 연결되어 있는지 확인
kubectl get endpoints api-svc -n my-namespace
# NAME      ENDPOINTS            AGE
# api-svc   10.244.1.5:8080     2d   ← Pod IP:Port
# ← Endpoints가 <none>이면 selector 불일치 → Pod label 확인 필요

# Service selector와 Pod label 비교
kubectl describe svc api-svc -n my-namespace | grep Selector
# Selector: app=api,version=v1    ← 이 label이 Pod에 있어야 함
kubectl get pods -n my-namespace --show-labels | grep api

NodePort — 외부→내부 진입점

Kubernetes
# NodePort 서비스 확인
kubectl get svc frontend-svc
# NAME           TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)          AGE
# frontend-svc   NodePort   10.96.100.1   <none>        3000:31000/TCP   1d
#                                                              ↑
#                                                         노드포트 31000

# 노드 IP 확인
kubectl get nodes -o wide
# NAME     STATUS   INTERNAL-IP    EXTERNAL-IP
# node-1   Ready    192.168.1.10   <none>

# 외부에서 노드포트로 접근
curl http://192.168.1.10:31000/

# 노드포트가 안 된다면 노드의 방화벽 규칙 확인
sudo iptables -t nat -L | grep 31000

LoadBalancer — 클라우드 환경 외부 접근

Kubernetes
# LoadBalancer External IP 확인 (프로비전 완료 전엔 <pending>)
kubectl get svc my-lb-svc
# NAME       TYPE           CLUSTER-IP    EXTERNAL-IP      PORT(S)        AGE
# my-lb-svc  LoadBalancer   10.96.200.1   52.10.20.30      80:31234/TCP   1h

# <pending>이 계속된다면 클라우드 컨트롤러 로그 확인
kubectl describe svc my-lb-svc | grep Events -A 10

4. 실습: kubectl exec와 port-forward로 Pod 내부 점검

Kubernetes
# 실습 환경 준비 (kubectl 접근 가능한 환경 필요)
# 테스트 Pod 생성
kubectl run debug-pod --image=busybox --restart=Never -- sleep 3600
kubectl run api-pod --image=nginx:alpine --restart=Never --port=80
kubectl expose pod api-pod --port=80 --name=api-svc

kubectl exec — Pod 안에서 직접 네트워크 진단

Kubernetes
# Pod 내부 쉘 접속
kubectl exec -it debug-pod -- sh

# Pod 안에서 ClusterIP 서비스로 요청
# (아래는 exec 내부에서 실행)
wget -qO- http://api-svc/           # Service 이름으로 접근
wget -qO- http://api-svc.default/   # 네임스페이스 포함
wget -qO- http://api-svc.default.svc.cluster.local/  # 완전한 FQDN

# DNS 해석 직접 확인
nslookup api-svc
# Server:    10.96.0.10     ← kube-dns(CoreDNS) IP
# Address:   10.96.0.10:53
# Name:      api-svc.default.svc.cluster.local
# Address:   10.96.45.12    ← ClusterIP

# DNS는 되는데 연결이 안 된다면 → kube-proxy iptables 문제 가능성
# exit 후 호스트에서 확인
exit
Kubernetes
# 대상 Pod의 상태 먼저 확인
kubectl get pods -o wide
# NAME       READY   STATUS    IP           NODE
# api-pod    1/1     Running   10.244.1.5   node-1

# Endpoint가 Service에 연결됐는지 확인
kubectl get endpoints api-svc
# NAME      ENDPOINTS          AGE
# api-svc   10.244.1.5:80     5m   ← 정상

# Pod 로그로 요청이 실제로 도달했는지 확인
kubectl logs api-pod -f
# 요청이 도달하면 nginx access log 출력됨

kubectl port-forward — 로컬에서 Pod/Service 직접 접근

위험 명령어

이 명령은 프로세스를 종료해 연결 중인 사용자나 배치 작업을 중단시킬 수 있습니다. PID와 프로세스 이름이 목표 서비스인지 확인한 뒤 실행하세요.

Kubernetes
# Service를 로컬 포트로 포워딩 (외부 IP 없이 테스트 가능)
kubectl port-forward svc/api-svc 8080:80 &
# Forwarding from 127.0.0.1:8080 -> 80
# Forwarding from [::1]:8080 -> 80

# 로컬에서 직접 요청
curl http://localhost:8080/
# nginx 응답 → Service, Pod 모두 정상

# 특정 Pod를 직접 포워딩 (Service 우회, Pod 자체 확인)
kubectl port-forward pod/api-pod 9090:80 &
curl http://localhost:9090/
# 응답 있음 → Pod 자체는 정상 → Service 문제로 범위 좁힘

# 백그라운드 포워딩 종료
kill %1 %2 2>/dev/null

5. docker-compose 네트워크 충돌 시나리오

💡개념

--network 누락과 이름 충돌이 일으키는 문제

docker-compose는 프로젝트 이름 기반으로 자동으로 네트워크를 생성합니다. 하지만 여러 프로젝트를 동시에 운영하거나, --network 옵션을 잘못 지정하면 통신이 단절됩니다.

자동 생성 네트워크 이름 규칙

로컬 터미널
# docker-compose.yml이 있는 디렉터리 이름이 프로젝트명이 됨
# /home/user/myapp/docker-compose.yml → 네트워크: myapp_default

# 이름을 명시적으로 지정하지 않으면
docker network ls | grep default
# NETWORK ID     NAME              DRIVER
# a1b2c3d4e5f6   myapp_default     bridge   ← compose가 생성
# b2c3d4e5f6a7   otherapp_default  bridge   ← 다른 프로젝트

문제 케이스 1: --network 누락으로 기본 네트워크에 연결됨

YAML
# 잘못된 docker-compose.yml 예시
version: "3.8"
services:
  api:
    image: myapp:latest
    # network 미지정 → myapp_default 자동 연결

  db:
    image: postgres:14
    networks:
      - external-net   # ← api와 다른 네트워크!

networks:
  external-net:
    external: true
    name: shared-db-net
로컬 터미널
# 위 구성 시 api는 myapp_default에, db는 shared-db-net에 연결됨
docker network inspect myapp_default
# Containers: { api: ... }   ← api만 있고 db 없음

docker network inspect shared-db-net
# Containers: { db: ... }    ← db만 있음

# api에서 db 이름으로 접근 시 DNS 해석 실패
# dial tcp: lookup db on 127.0.0.11:53: no such host

문제 케이스 2: 서브넷 충돌

로컬 터미널
# 두 프로젝트가 같은 서브넷을 사용하는 경우
docker network inspect projectA_default | grep Subnet
# "Subnet": "172.20.0.0/16"

docker network inspect projectB_default | grep Subnet
# "Subnet": "172.20.0.0/16"  ← 충돌!

# 이 경우 projectB의 컨테이너가 projectA의 컨테이너로 라우팅되거나
# 둘 다 통신 이상 발생

# 해결: 명시적 서브넷 지정
# docker-compose.yml networks 섹션에 ipam 설정 추가

문제 케이스 3: docker run --network와 compose 네트워크 혼용

Docker
# compose로 띄운 서비스와 docker run으로 띄운 컨테이너 연결 시도
docker run -d --name monitor \
  --network bridge \   # ← 기본 bridge 지정
  prom/prometheus      # compose 서비스들은 myapp_default에 있음

# monitor에서 compose 서비스 접근 불가
# 해결: compose 네트워크를 명시적으로 지정
docker run -d --name monitor \
  --network myapp_default \   # ← compose 네트워크에 연결
  prom/prometheus

6. 실습: 두 컨테이너 통신 안 될 때 5분 진단 플로우

통신 장애 신고가 들어왔을 때 5분 안에 원인을 특정하는 플로우입니다. 이 순서를 따르면 무작위로 설정을 바꾸지 않고도 원인을 찾을 수 있습니다.

로컬 터미널
# 시나리오: api 컨테이너에서 db 컨테이너로 통신 안 됨
# 진단 대상: api → db

# ── Step 1: 두 컨테이너 실행 중인지 확인 (30초) ──
docker ps --filter name=api --filter name=db
# 두 컨테이너 모두 STATUS가 Up이어야 함
# 한쪽이 Exited라면 docker logs <name>으로 크래시 원인 확인

# ── Step 2: 같은 네트워크에 있는지 확인 (1분) ──
# api가 연결된 네트워크 목록
docker inspect api --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool
# 출력에서 네트워크 이름 확인

# db가 연결된 네트워크 목록
docker inspect db --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool

# 두 컨테이너가 같은 네트워크에 있는지 비교
# 같은 네트워크 이름이 없다면 → 원인 확정: 네트워크 분리
docker network connect <network-name> db   # 또는 api

# ── Step 3: DNS 해석 테스트 (1분) ──
# api 컨테이너 안에서 db 이름 해석 확인
docker exec api nslookup db 2>/dev/null || \
  docker exec api getent hosts db 2>/dev/null || \
  docker exec api cat /etc/resolv.conf

# DNS 실패 → 127.0.0.11이 nameserver인지 확인
# DNS 성공 → Step 4로

# ── Step 4: 포트 연결 테스트 (1분) ──
# DB_IP 추출 후 포트 직접 테스트
DB_IP=$(docker inspect db --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
echo "db IP: $DB_IP"

# nc가 없으면 busybox 활용
docker run --rm --network $(docker inspect api --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}') \
  busybox sh -c "nc -z -w3 $DB_IP 5432 && echo 'PORT OPEN' || echo 'PORT CLOSED'"

# PORT CLOSED → db 컨테이너 내부에서 프로세스 확인
docker exec db ss -tlnp | grep 5432
# 5432 리스닝 없음 → DB 프로세스 미실행

# ── Step 5: 애플리케이션 레이어 확인 (30초) ──
# 포트는 열려 있지만 인증/설정 문제인 경우
docker logs db --tail=20
docker logs api --tail=20 | grep -i "connect\|error\|refused"
┌─────────────────────────────────────────────────────────┐
│            컨테이너 통신 장애 5분 진단 트리              │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  컨테이너 실행 중?                                       │
│  ├─ NO  → docker logs로 크래시 원인 확인               │
│  └─ YES ↓                                              │
│                                                         │
│  같은 네트워크에 연결됨?                                 │
│  ├─ NO  → docker network connect로 연결                │
│  └─ YES ↓                                              │
│                                                         │
│  DNS 해석 성공? (nslookup/getent)                       │
│  ├─ NO  → 사용자 정의 네트워크인지 확인                 │
│  │         기본 bridge이면 DNS 없음 — 이름 불가         │
│  └─ YES ↓                                              │
│                                                         │
│  포트 열림? (nc -z or busybox)                          │
│  ├─ NO  → 대상 컨테이너에서 ss -tlnp 확인              │
│  │         프로세스 미실행이면 서비스 시작              │
│  └─ YES ↓                                              │
│                                                         │
│  → 애플리케이션 레이어 문제 (인증, 설정, SQL 오류)       │
│    docker logs로 에러 확인                              │
└─────────────────────────────────────────────────────────┘

7. 실제 에러 해부

에러 메시지가 알려주는 것

dial tcp: lookup web on 127.0.0.11:53: no such host
              ↑           ↑              ↑
         요청한 이름   Docker DNS     DNS는 응답했으나
          "web"       서버 주소      이름이 등록 안 됨

127.0.0.11은 Docker 내장 DNS 서버 주소입니다. 이 서버 자체가 죽은 게 아니라, DNS 서버는 정상 응답했지만 "web"이라는 이름이 현재 네트워크에 등록되어 있지 않다는 뜻입니다.

가능한 원인 세 가지

로컬 터미널
# 원인 1: web 컨테이너가 현재 컨테이너와 다른 네트워크에 있음
docker network inspect <current-network> | grep -A5 '"Containers"'
# Containers에 "web" 항목이 없다면 → 이것이 원인

# 확인 후 해결
docker network connect <current-network> web

# 원인 2: docker-compose 서비스 이름과 컨테이너 이름 불일치
# docker-compose.yml
services:
  frontend:          # ← 서비스 이름 (DNS로 사용 가능)
    container_name: my-web-app  # ← 컨테이너 이름 (docker ps에 표시)

# 같은 네트워크라면 "frontend"로 접근해야 함
# "my-web-app"은 컨테이너 이름이지만 docker-compose 네트워크에서
# DNS로 해석되는 건 서비스 이름 "frontend"

# 원인 3: scale-out 후 이름 변경
# docker-compose up --scale web=2 실행 시
# web 컨테이너 이름이 web_1, web_2로 변경됨
# "web"으로는 여전히 접근 가능하지만 일부 환경에서 이름 해석 실패

# 현재 등록된 이름 직접 확인
docker exec <현재-컨테이너> cat /etc/hosts
docker exec <현재-컨테이너> nslookup web

재현 후 해결 실습

로컬 터미널
# 의도적으로 에러 재현
docker network create net-a
docker network create net-b

docker run -d --name web --network net-a nginx:alpine
docker run -d --name api --network net-b busybox sleep 3600

# api에서 web 접근 시도 → 에러 발생
docker exec api wget -qO- http://web/
# wget: bad address 'web'
# → no such host 계열 에러

# 해결: web을 api의 네트워크(net-b)에도 연결
docker network connect net-b web

# 재시도 → 성공
docker exec api wget -qO- http://web/
# <!DOCTYPE html>  ← nginx 응답

# 정리
docker stop web api && docker rm web api
docker network rm net-a net-b

증상

docker-compose up으로 서비스를 올렸는데, 한 컨테이너에서 다른 컨테이너를 서비스 이름으로 부르면 실패합니다.

Docker
# docker-compose.yml
# services:
#   api:
#     ...
#   db:
#     image: postgres:15

# api 컨테이너에서 db에 연결 시도
docker exec -it myproject_api_1 bash
curl http://db:5432   # 실패
ping db               # ping: db: Name or service not known

원인 진단

로컬 터미널
# 컨테이너 이름과 서비스 이름 구분
docker ps --format "table {{.Names}}\t{{.Image}}"
# myproject_api_1     myapi:latest
# myproject_db_1      postgres:15

# 네트워크 확인 — 어떤 네트워크에 붙어 있나
docker inspect myproject_api_1 | grep -A 20 '"Networks"'
# "myproject_default": {  ← 같은 네트워크 확인

# DNS 확인 — 실제 resolv 대상
docker exec myproject_api_1 cat /etc/resolv.conf
# nameserver 127.0.0.11  ← Docker 내부 DNS 서버

# 서비스 이름으로 DNS 쿼리 테스트
docker exec myproject_api_1 nslookup db
# Server: 127.0.0.11
# Address: 127.0.0.11#53
# Name: db
# Address: 172.20.0.3  ← 정상이면 이렇게 나와야 함

해결

로컬 터미널
# 같은 네트워크에 있는지 확인
docker network inspect myproject_default

# 문제: api 컨테이너가 default 네트워크에 없는 경우
# → docker-compose.yml에서 networks 명시 여부 확인

# docker-compose.yml 수정
services:
  api:
    networks:
      - backend
  db:
    networks:
      - backend
networks:
  backend:

# 재시작
docker-compose down && docker-compose up -d

# 서비스 이름으로 연결 재확인
docker exec myproject_api_1 curl http://db:5432

8. 실무 시나리오

💼
실무 맥락
현업 패턴

상황: 월요일 오전, 백엔드 팀에서 슬랙 메시지가 옵니다.

"K8s 스테이징에서 order-service가 payment-service ClusterIP로 요청을 못 보내고 있습니다. 어제 금요일까지는 됐는데 오늘 갑자기 안 됩니다. 아무것도 바꾼 게 없는데요."

개발자들은 애플리케이션 코드만 담당하고, 인프라 담당자는 지금 자리를 비웠습니다. 이런 상황에서 개발자가 직접 할 수 있는 K8s ClusterIP 장애 진단 방법입니다.

1단계: Service와 Endpoint 상태 확인

Kubernetes
# payment-service의 Service와 Endpoint 확인
kubectl get svc payment-service -n staging
# NAME              TYPE        CLUSTER-IP    PORT(S)    AGE
# payment-service   ClusterIP   10.96.45.12   8080/TCP   30d

kubectl get endpoints payment-service -n staging
# NAME              ENDPOINTS   AGE
# payment-service   <none>      30d   ← Endpoints가 비어 있음! 원인 발견

2단계: Endpoint가 비어 있는 이유 추적

Kubernetes
# Service selector 확인
kubectl describe svc payment-service -n staging | grep Selector
# Selector: app=payment,env=staging

# 해당 label을 가진 Pod가 있는지 확인
kubectl get pods -n staging -l app=payment,env=staging
# No resources found   ← Pod가 없거나 label이 다름

# 전체 payment 관련 Pod 확인
kubectl get pods -n staging | grep payment
# payment-service-v2-abc123   0/1   CrashLoopBackOff   15   2h
# ↑ Pod가 있지만 CrashLoopBackOff — 실행 중이 아니라 Endpoint에 없음

3단계: Pod 크래시 원인 확인

Kubernetes
kubectl logs payment-service-v2-abc123 -n staging --previous
# Error: DATABASE_URL environment variable not set
# ← 배포 시 환경변수 누락이 원인

kubectl describe pod payment-service-v2-abc123 -n staging | grep -A5 Environment
# Environment: DATABASE_URL  <set to the key 'database-url' in secret 'payment-secret'>  Optional: false
# 시크릿이 삭제되었거나 이름이 변경된 경우

kubectl get secret payment-secret -n staging
# Error from server (NotFound): secrets "payment-secret" not found
# ← 시크릿이 없음 — 금요일 정리 작업 중 실수로 삭제된 것

결과 보고:

"payment-service ClusterIP의 Endpoint가 비어 있었고, 원인은 payment-service Pod가 CrashLoopBackOff 상태였습니다. Pod 로그에서 DATABASE_URL 환경변수 누락 에러를 확인했고, payment-secret 시크릿이 삭제된 것을 발견했습니다. 시크릿 재생성 후 Pod가 정상 기동되면 Endpoint가 복구되고 ClusterIP 통신이 재개될 것입니다."

kubectl exec와 port-forward만 알아도 개발자가 직접 K8s 네트워크 장애의 원인을 특정할 수 있습니다. 인프라팀 없이도 "어디가 문제인지"는 찾을 수 있어야 빠른 에스컬레이션이 가능합니다.


정리

컨테이너 네트워크 디버깅의 핵심은 어느 계층에서 끊겼는지 순서대로 좁혀나가는 것입니다.

증상확인 명령가장 많은 원인
이름으로 못 찾음docker network inspect다른 네트워크에 있음
IP로는 되는데 이름이 안 됨/etc/resolv.conf 확인기본 bridge 사용 중
ClusterIP 응답 없음kubectl get endpointsEndpoint 비어 있음
Endpoint 비어 있음kubectl get pods -l <selector>Pod 크래시 또는 label 불일치
Pod는 정상인데 Service 응답 없음kubectl port-forward pod/...kube-proxy 또는 NetworkPolicy

도구는 단순합니다. docker network inspect, docker exec, kubectl exec, kubectl port-forward — 이 네 가지만 능숙하게 쓸 수 있으면 대부분의 컨테이너 네트워크 장애를 혼자 진단할 수 있습니다.

다음 모듈에서는 HTTP 연결 타임아웃 디버깅을 다루며, curl로 연결 단계를 분해하고 외부 API 장애와 내 서버 문제를 구분하는 방법을 배웁니다.

지식 확인

퀴즈 — 3문제

Q1

docker-compose로 두 서비스를 띄웠는데 컨테이너 이름으로 통신이 안 됩니다. 가장 먼저 확인해야 할 것은?

Q2

K8s에서 ClusterIP 서비스를 통해 다른 Pod에 접근이 안 될 때, Pod 내부에서 직접 진단하는 명령은?

Q3

dial tcp: lookup web on 127.0.0.11:53: no such host 에러의 가장 가능성 높은 원인은?

0 / 3 답변

🧪 실습으로 확인하기

포트는 열렸다는데 왜 안 되지? — ss/netstat/telnet으로 TCP 진단

초급

"포트 8080 열었는데요?"와 "왜 안 돼요?" 사이의 간극을 메우는 실습. ss로 바인딩 상태를 확인하고, telnet/nc으로 원격 연결을 테스트하고, iptables 방화벽을 진단하고, 바인딩 주소(0.0.0.0 vs 127.0.0.1)까지 수정하는 4단계 TCP 포트 진단 플로우를 완성한다.

35📋 4단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

docker입문 · 30
[Docker] 백엔드 개발자에게 Docker와 컨테이너 가상화가 필수인 이유
Docker 트랙 시작점
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점