RHOCP OVN-Kubernetes 딥다이브: 소켓, DB, 가상 리소스, 패킷 흐름
OpenShift OVN-Kubernetes를 Pod 생성부터 NBDB/SBDB, ovn-controller, OVS bridge, Geneve, Service, NetworkPolicy, Egress까지 내부 리소스 단위로 추적하는 운영 분석 글입니다.
RHOCP에서 OVN-Kubernetes를 본다는 건 `Pod끼리 ping이 된다` 수준에서 끝나면 부족합니다. 실제 장애는 Pod annotation, CNI socket, NBDB, SBDB, `ovn-controller`, OVS `br-int`, Geneve tunnel, Service load balancer, NetworkPolicy ACL, egress gateway 중간 어딘가에서 갈립니다.
이 글은 OpenShift OVN-Kubernetes를 운영 관점에서 끝까지 따라가는 정리입니다. 기준은 최근 OpenShift 4.x OVN-Kubernetes 구조이고, 명령은 실제 클러스터에서 읽기 위주로 확인할 수 있게 적었습니다. 버전에 따라 컨테이너 이름이나 socket path가 조금 다를 수 있으니, 먼저 현재 클러스터의 `openshift-ovn-kubernetes` 네임스페이스를 확인한 뒤 맞춰서 실행하는 방식으로 보면 됩니다.
1. 전체 그림
OVN-Kubernetes는 Kubernetes 리소스를 OVN 논리 네트워크로 번역하고, 그 논리 네트워크를 각 노드의 Open vSwitch datapath로 내립니다. 핵심 흐름은 아래처럼 잡으면 됩니다.
- Kubernetes API에 Pod, Service, NetworkPolicy, Egress 리소스가 생깁니다.
- OVN-Kubernetes control-plane이 API를 watch하고 OVN Northbound DB에 논리 리소스를 씁니다.
- `ovn-northd`가 Northbound DB를 Southbound DB의 logical flow와 port binding으로 컴파일합니다.
- 각 노드의 `ovn-controller`가 Southbound DB를 읽고 자신이 담당할 port와 flow만 로컬 OVS에 반영합니다.
- OVS `br-int`와 kernel datapath가 실제 Pod packet을 처리합니다.
- 노드 간 트래픽은 Geneve overlay로 상대 노드까지 갑니다.
운영자는 이 흐름을 API, NBDB, SBDB, OVS, Linux socket, packet capture 순서로 쪼개서 봐야 합니다. 한 번에 전부 보려고 하면 오히려 놓칩니다.
2. 클러스터 기본 인벤토리
먼저 OpenShift가 어떤 네트워크 설정으로 떠 있는지 확인합니다. `Network.operator`와 `Network.config`를 같이 봐야 실제 plugin, clusterNetwork, serviceNetwork, defaultNetwork 상태가 보입니다.
oc get clusterversion
oc get network.operator cluster -o yaml
oc get network.config cluster -o yaml
oc -n openshift-ovn-kubernetes get all -o wide
oc -n openshift-ovn-kubernetes get pods -o wide
oc -n openshift-ovn-kubernetes get cm,secret,svc,ep,endpointslice
oc get nodes -o wide
oc get nodes -o json | jq -r '.items[] |
{
node:.metadata.name,
internalIP: (.status.addresses[]? | select(.type=="InternalIP") |.address),
podCIDR:.spec.podCIDR,
podCIDRs:.spec.podCIDRs,
ovnAnnotations: (.metadata.annotations // {} | with_entries(select(.key|test("ovn|k8s.ovn"))))
}'3. 노드의 ovnkube-node Pod 잡기
OVN-Kubernetes 장애 분석은 특정 노드 기준으로 봐야 합니다. 문제 Pod가 떠 있는 노드의 `ovnkube-node` Pod를 잡아두면 이후 명령이 단순해집니다.
NODE=worker-0.example.com
OVN_NS=openshift-ovn-kubernetes
OVN_NODE_POD=$(oc -n ${OVN_NS} get pod -l app=ovnkube-node \
--field-selector spec.nodeName=${NODE} \
-o jsonpath='{.items[0].metadata.name}')
echo "node=${NODE}"
echo "ovnkube-node pod=${OVN_NODE_POD}"
oc -n ${OVN_NS} get pod ${OVN_NODE_POD} -o json | jq -r '.spec.containers[].name,
"----",.status.containerStatuses[]? | [.name,.ready,.restartCount] | @tsv'4. 소켓 관점에서 보는 구성 요소
OVN-Kubernetes는 API만 보는 시스템이 아닙니다. 로컬 Unix socket, OVSDB socket, OVN DB 연결, Geneve UDP socket이 같이 움직입니다. 소켓을 보면 어느 프로세스가 어느 계층을 잡고 있는지 바로 보입니다.
# 노드 host namespace 기준으로 소켓과 리스닝 포트를 본다.
oc debug node/${NODE} -- chroot /host bash -c '
set -e
echo "## UDP/TCP listeners"
ss -lntup | egrep "ovn|ovs|6081|6641|6642|9641|9642|910|10256" || true
echo "## Unix sockets"
ss -xlpn | egrep "ovn|ovs|openvswitch|cni" || true
echo "## runtime files"
find /run /var/run -maxdepth 4 \(-iname "*ovn*" -o -iname "*ovs*" -o -iname "*openvswitch*" -o -iname "*.sock" \) \
-printf "%p %y\n" 2>/dev/null | sort | sed -n "1,180p"
'
# ovnkube-node 컨테이너 안에서도 확인한다. 버전별 컨테이너 이름은 먼저 get pod -o json으로 확인한다.
oc -n ${OVN_NS} exec ${OVN_NODE_POD} -c ovn-controller -- bash -c '
ss -xlpn | egrep "ovn|ovs|openvswitch" || true
ls -al /var/run/ovn /var/run/openvswitch 2>/dev/null || true
'대표 소켓/채널
CRI-O/container runtime -> CNI plugin
- Pod sandbox netns를 만든 뒤 CNI ADD를 실행한다.
- OVN-Kubernetes CNI는 local ovn-kubernetes daemon 쪽 Unix socket을 통해 Pod port 생성 흐름에 붙는다.
ovn-kubernetes control-plane -> NBDB
- Kubernetes API의 Pod, Node, Service, NetworkPolicy, Egress 리소스를 OVN Northbound DB 객체로 변환한다.
ovn-northd -> NBDB/SBDB
- NBDB의 논리 리소스를 SBDB의 logical flow, port binding, multicast group 등으로 컴파일한다.
ovn-controller -> SBDB
- 자신이 담당할 chassis의 port binding과 logical flow를 읽는다.
ovn-controller -> local OVSDB/ovs-vswitchd
- br-int, Interface, OpenFlow rule을 로컬 OVS에 밀어 넣는다.
OVS kernel datapath -> underlay NIC
- 같은 노드면 veth/bridge/datapath 내부에서 끝난다.
- 다른 노드면 Geneve UDP 6081로 캡슐화되어 상대 노드로 간다.소켓 경로는 버전과 컨테이너 분리에 따라 다를 수 있습니다. 그래서 특정 파일명만 외우기보다 `/run`, `/var/run`, 컨테이너 내부 `/var/run/ovn`, `/var/run/openvswitch`를 같이 뒤지는 방식이 안전합니다. Geneve는 일반적으로 UDP 6081을 봅니다.
5. 테스트 Pod 준비
이제 실제 Pod 두 개를 서로 다른 노드에 올려서 흐름을 봅니다. 같은 노드 통신과 다른 노드 통신은 중간 경로가 다릅니다. 여기서는 다른 노드 흐름을 기본으로 잡습니다.
oc new-project ovn-lab
NODE_A=worker-0.example.com
NODE_B=worker-1.example.com
cat <<EOF | oc apply -f -
apiVersion: v1
kind: Pod
metadata:
name: src
namespace: ovn-lab
labels:
app: src
spec:
nodeName: ${NODE_A}
containers:
- name: net
image: nicolaka/netshoot:latest
command: ["sleep","1d"]
---
apiVersion: v1
kind: Pod
metadata:
name: dst
namespace: ovn-lab
labels:
app: dst
spec:
nodeName: ${NODE_B}
containers:
- name: net
image: nicolaka/netshoot:latest
command: ["sleep","1d"]
EOF
oc -n ovn-lab get pods -o wide
oc -n ovn-lab get pod src dst -o json | jq -r '.items[] | {
name:.metadata.name,
node:.spec.nodeName,
podIP:.status.podIP,
annotations: (.metadata.annotations // {} | with_entries(select(.key|test("ovn|k8s.ovn"))))
}'6. Pod network namespace와 veth
Pod 안에서 보이는 `eth0`는 혼자 존재하지 않습니다. 반대쪽 끝은 host network namespace에 있고, OVN-Kubernetes가 OVS `br-int` 쪽으로 연결합니다. 먼저 Pod netns와 peer ifindex를 확인합니다.
# source Pod가 떠 있는 노드에서 sandbox PID와 Pod eth0를 확인한다.
SRC_POD=src
SRC_NS=ovn-lab
NODE_A=$(oc -n ${SRC_NS} get pod ${SRC_POD} -o jsonpath='{.spec.nodeName}')
SRC_IP=$(oc -n ${SRC_NS} get pod ${SRC_POD} -o jsonpath='{.status.podIP}')
DST_IP=$(oc -n ${SRC_NS} get pod dst -o jsonpath='{.status.podIP}')
oc debug node/${NODE_A} -- chroot /host bash -c "
set -e
SANDBOX_ID=\$(crictl pods --namespace ${SRC_NS} --name ${SRC_POD} -q | head -n 1)
echo sandbox=\${SANDBOX_ID}
PID=\$(crictl inspectp -o json \${SANDBOX_ID} | jq -r '.info.pid')
echo pid=\${PID}
echo '## Pod netns address'
nsenter -t \${PID} -n ip -br addr
echo '## Pod netns route'
nsenter -t \${PID} -n ip route
echo '## Pod netns neigh'
nsenter -t \${PID} -n ip neigh || true
echo '## Pod eth0 peer ifindex'
PEER=\$(nsenter -t \${PID} -n ethtool -S eth0 | awk '/peer_ifindex/ {print \$2}')
echo peer_ifindex=\${PEER}
ip -o link | awk -F': ' -v idx=\${PEER} '\$1 == idx {print}'
"이 단계에서 `eth0`, default route, neighbor, peer ifindex가 잡히면 CNI ADD가 적어도 Pod netns까지는 처리한 겁니다. Pod가 `ContainerCreating`에 머물면 이 전 단계에서 CRI/CNI 실패가 났을 가능성이 큽니다.
7. OVS 가상 리소스: br-int, br-ex, Interface, Port
OVN-Kubernetes 노드의 실제 datapath는 Open vSwitch에 내려옵니다. `br-int`는 OVN logical pipeline이 현실로 내려오는 핵심 bridge입니다. 환경에 따라 `br-ex`, management port, patch/interface 구성이 다르게 보일 수 있습니다.
oc debug node/${NODE_A} -- chroot /host bash -c '
set -e
echo "## OVS summary"
ovs-vsctl show
echo "## Open_vSwitch external IDs"
ovs-vsctl list Open_vSwitch
echo "## Bridges"
ovs-vsctl --columns=name,datapath_type,fail_mode,ports,external_ids list Bridge
echo "## Ports"
ovs-vsctl --columns=name,interfaces,external_ids list Port | sed -n "1,220p"
echo "## Interfaces"
ovs-vsctl --columns=name,type,ofport,options,external_ids,status list Interface | sed -n "1,260p"
echo "## br-int ports"
ovs-ofctl -O OpenFlow13 show br-int
ovs-ofctl -O OpenFlow13 dump-ports-desc br-int | sed -n "1,160p"
'`Interface.external_ids`에는 Kubernetes Pod, namespace, sandbox, logical port와 연결되는 힌트가 들어갑니다. Pod annotation과 OVS Interface external_ids를 같이 보면 `Kubernetes Pod -> OVN logical port -> OVS port` 매핑이 잡힙니다.
8. OVN Northbound DB: 의도된 논리 네트워크
NBDB는 운영자가 기대한 논리 네트워크 상태에 가깝습니다. Node는 logical switch/gateway router로, Pod는 logical switch port로, Service는 load balancer로, NetworkPolicy는 ACL/port group/address set으로 나타납니다.
# control-plane 성격의 ovnkube-node Pod 하나를 잡는다.
OVN_CP_POD=$(oc -n ${OVN_NS} get pod -l app=ovnkube-node -o jsonpath='{.items[0].metadata.name}')
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- ovn-nbctl show
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- bash -c '
echo "## Logical switches"
ovn-nbctl --format=table --columns=name,external_ids list Logical_Switch
echo "## Logical switch ports"
ovn-nbctl --format=table --columns=name,type,addresses,options,external_ids list Logical_Switch_Port | sed -n "1,220p"
echo "## Logical routers"
ovn-nbctl --format=table --columns=name,ports,nat,policies,external_ids list Logical_Router
echo "## NAT"
ovn-nbctl --format=table --columns=type,logical_ip,external_ip,external_ids list NAT
echo "## Load balancers"
ovn-nbctl --format=table --columns=name,protocol,vips,external_ids list Load_Balancer
echo "## ACLs"
ovn-nbctl --format=table --columns=priority,direction,match,action,external_ids list ACL | sed -n "1,220p"
echo "## Port groups"
ovn-nbctl --format=table --columns=name,ports,acls,external_ids list Port_Group
echo "## Address sets"
ovn-nbctl --format=table --columns=name,addresses,external_ids list Address_Set | sed -n "1,180p"
'9. OVN Southbound DB: 노드로 내려갈 실행 계획
SBDB는 `ovn-northd`가 NBDB를 컴파일한 결과입니다. 여기에는 chassis, encapsulation, port binding, datapath binding, logical flow가 들어갑니다. 특정 Pod가 어느 노드의 어느 chassis에 binding됐는지를 볼 때 SBDB가 중요합니다.
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c sbdb -- ovn-sbctl show
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c sbdb -- bash -c '
echo "## Chassis"
ovn-sbctl --format=table --columns=name,hostname,encaps,external_ids list Chassis
echo "## Encap"
ovn-sbctl --format=table --columns=chassis_name,type,ip,options list Encap
echo "## Port bindings"
ovn-sbctl --format=table --columns=logical_port,type,chassis,mac,datapath,tunnel_key,external_ids list Port_Binding | sed -n "1,260p"
echo "## Datapath bindings"
ovn-sbctl --format=table --columns=tunnel_key,external_ids list Datapath_Binding | sed -n "1,200p"
echo "## Logical flows"
ovn-sbctl lflow-list | sed -n "1,260p"
'10. Kubernetes 리소스와 OVN/OVS 리소스 매핑
이 매핑을 외워두면 장애 지점이 빨리 줄어듭니다. Kubernetes에서는 정상으로 보여도 NBDB에 없을 수 있고, NBDB에는 있는데 SBDB port binding이 꼬일 수 있고, SBDB에는 있는데 노드 OVS에 반영이 안 될 수 있습니다.
Kubernetes object -> OVN NBDB -> OVN SBDB -> OVS
Node
-> Logical_Switch, gateway router, chassis metadata
-> Chassis, Encap, Datapath_Binding
-> br-int, br-ex, geneve interface, ovn-k8s-mp0 계열 인터페이스
Pod
-> Logical_Switch_Port
-> Port_Binding
-> OVS Interface/Port, OpenFlow rule, kernel datapath entry
Service
-> Load_Balancer, VIP, backend endpoint
-> logical flow로 컴파일
-> br-int OpenFlow/eBPF가 아니라 OVN logical pipeline 기반 처리
NetworkPolicy
-> ACL, Address_Set, Port_Group
-> logical flow
-> OVS flow로 내려가서 실제 허용/차단
EgressIP/EgressFirewall
-> NAT, Logical_Router_Policy, ACL
-> gateway node와 chassis assignment
-> br-ex/underlay 방향으로 실제 송신11. Pod 생성이 실제 네트워크로 내려가는 흐름
Pod 하나가 뜰 때 내부에서는 API, scheduler, kubelet, CRI-O, CNI, OVN DB, OVS가 모두 지나갑니다. 흐름을 순서대로 쓰면 아래와 같습니다.
Pod 생성 흐름
1. API server에 Pod 생성
2. scheduler가 Node 결정
3. kubelet이 CRI-O에 sandbox 생성 요청
4. CRI-O가 Pod network namespace 생성
5. CNI ADD 실행
6. OVN-Kubernetes CNI daemon이 Pod의 logical switch port와 OVS port를 연결
7. NBDB에 Logical_Switch_Port 반영
8. ovn-northd가 SBDB Port_Binding/Logical_Flow로 컴파일
9. 해당 Node의 ovn-controller가 SBDB를 읽고 br-int flow를 구성
10. Pod eth0 <-> host veth/OVS port <-> br-int 경로 완성12. 다른 노드 Pod 통신 흐름
다른 노드의 Pod로 가는 패킷은 단순한 Linux route만으로 설명되지 않습니다. OVN logical pipeline과 OVS flow를 지나고, 노드 간에는 Geneve overlay로 감싸져 이동합니다.
다른 노드 Pod 통신 흐름
src container socket
-> Pod src eth0
-> host 쪽 veth/OVS interface
-> br-int
-> OVN logical switch ingress pipeline
-> OVN logical router pipeline
-> remote chassis Port_Binding 확인
-> br-int의 Geneve tunnel output
-> underlay NIC
-> UDP 6081 Geneve packet
-> remote node underlay NIC
-> remote br-int Geneve decapsulation
-> remote logical switch egress pipeline
-> dst Pod OVS interface/host veth
-> dst Pod eth0
-> dst container socketSRC_IP=$(oc -n ovn-lab get pod src -o jsonpath='{.status.podIP}')
DST_IP=$(oc -n ovn-lab get pod dst -o jsonpath='{.status.podIP}')
oc -n ovn-lab exec src -- ping -c 3 ${DST_IP}
oc -n ovn-lab exec src -- curl -m 3 http://${DST_IP}:8080 || true
oc debug node/${NODE_A} -- chroot /host bash -c "
set -e
echo '## route to destination Pod from host'
ip route get ${DST_IP} || true
echo '## br-int OpenFlow count'
ovs-ofctl -O OpenFlow13 dump-flows br-int | wc -l
echo '## flows with destination Pod IP'
ovs-ofctl -O OpenFlow13 dump-flows br-int | grep '${DST_IP}' | sed -n '1,80p' || true
echo '## tunnel interface'
ip -d link | egrep -A3 'genev|br-int|br-ex|ovn-k8s-mp0' || true
"13. tcpdump를 거는 위치
캡처는 위치를 잘못 잡으면 결론이 틀어집니다. Pod IP가 보이는 지점과 Geneve outer packet이 보이는 지점을 나눠야 합니다.
# terminal 1: source Pod에서 트래픽 발생
oc -n ovn-lab exec src -- ping -c 10 ${DST_IP}
# terminal 2: source node의 br-int 근처
oc debug node/${NODE_A} -- chroot /host bash -c "
tcpdump -ni any host ${SRC_IP} or host ${DST_IP} -c 80
"
# terminal 3: underlay NIC에서 Geneve 확인
NODE_B_IP=$(oc get node ${NODE_B} -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}')
oc debug node/${NODE_A} -- chroot /host bash -c "
IFACE=\$(ip route get ${NODE_B_IP} | awk '{for(i=1;i<=NF;i++) if(\$i=="dev") print \$(i+1); exit}')
echo underlay_if=\${IFACE}
tcpdump -ni \${IFACE} udp port 6081 -c 40
"`any`에서 Pod IP가 보이는데 underlay NIC에서 UDP 6081이 안 보이면 overlay 출력 전 단계에서 막힌 겁니다. underlay에서 6081은 보이는데 상대 Pod에 도달하지 않으면 상대 노드의 Geneve decapsulation, SBDB binding, OVS flow, NetworkPolicy 쪽을 봐야 합니다.
14. ovn-trace로 논리 파이프라인 보기
`tcpdump`가 실제 packet을 보는 도구라면 `ovn-trace`는 OVN 논리 파이프라인을 보는 도구입니다. inport, MAC, IP 값을 맞춰 넣으면 logical switch/router pipeline에서 어떤 ACL, load balancer, route를 타는지 확인할 수 있습니다.
# Logical_Switch_Port 이름을 먼저 찾는다.
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- bash -c "
ovn-nbctl --format=table --columns=name,addresses,external_ids list Logical_Switch_Port \
| egrep '${SRC_IP}|${DST_IP}|ovn-lab|src|dst' || true
"
# ovn-trace는 논리 파이프라인을 보는 도구다. inport와 datapath 이름은 환경에서 확인한 값으로 바꾼다.
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- bash -c '
ovn-nbctl --format=csv --data=bare --no-headings --columns=name list Logical_Switch | sed -n "1,20p"
'
# 예시 형식. 실제 inport/datapath 이름은 위 출력에 맞춰 넣는다.
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- ovn-trace <logical_switch_name> \
'inport=="<src_logical_port>" && eth.src==<src_mac> && eth.dst==<dst_mac> && ip4.src==<src_pod_ip> && ip4.dst==<dst_pod_ip> && ip.ttl==64'15. Service는 OVN Load_Balancer로 본다
OpenShift OVN-Kubernetes에서 Service는 단순 iptables 규칙만 찾으면 놓칠 수 있습니다. Service ClusterIP와 backend endpoint는 OVN load balancer와 logical flow에 반영됩니다.
oc -n ovn-lab create deployment web --image=nginx:1.27
oc -n ovn-lab scale deployment web --replicas=2
oc -n ovn-lab expose deployment web --port=80
oc -n ovn-lab get pod,svc,endpointslice -o wide
SVC_IP=$(oc -n ovn-lab get svc web -o jsonpath='{.spec.clusterIP}')
oc -n ovn-lab exec src -- curl -I --max-time 3 http://${SVC_IP}
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- bash -c "
ovn-nbctl --format=table --columns=name,protocol,vips,external_ids list Load_Balancer | grep -A3 -B3 '${SVC_IP}' || true
"
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c sbdb -- bash -c "
ovn-sbctl lflow-list | grep '${SVC_IP}' | sed -n '1,120p' || true
"`PodIP -> PodIP`는 되는데 `ClusterIP -> Pod`가 안 되면 CNI port나 Geneve만 볼 게 아닙니다. Service object, EndpointSlice, NBDB Load_Balancer, SBDB logical flow를 이어서 봐야 합니다.
16. NetworkPolicy는 ACL, Address_Set, Port_Group으로 내려간다
NetworkPolicy는 `iptables -S`만 보고 판단하면 안 됩니다. OVN-Kubernetes에서는 policy가 NBDB의 ACL, Address_Set, Port_Group으로 표현되고, 이후 logical flow와 OVS flow로 내려갑니다.
cat <<'EOF' | oc apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-src
namespace: ovn-lab
spec:
podSelector:
matchLabels:
app: web
ingress:
- from:
- podSelector:
matchLabels:
app: src
ports:
- protocol: TCP
port: 80
policyTypes:
- Ingress
EOF
oc -n ovn-lab get networkpolicy allow-from-src -o yaml
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- bash -c '
echo "## ACLs related to ovn-lab"
ovn-nbctl --format=table --columns=priority,direction,match,action,external_ids list ACL \
| egrep -i "ovn-lab|networkpolicy|allow-from-src|web|src" -A2 -B2 || true
echo "## Port groups"
ovn-nbctl --format=table --columns=name,ports,acls,external_ids list Port_Group \
| egrep -i "ovn-lab|allow-from-src|web|src" -A4 -B2 || true
'policy 장애는 대부분 selector 해석, namespace/pod label, address set 갱신, ACL match 방향에서 갈립니다. `oc describe networkpolicy`만 보지 말고 NBDB ACL의 match 문자열까지 같이 봐야 합니다.
17. EgressIP, EgressFirewall, br-ex 경로
클러스터 밖으로 나가는 트래픽은 Pod 간 통신과 다르게 gateway 경로가 중요합니다. 플랫폼, gateway mode, EgressIP 사용 여부에 따라 `br-ex`, gateway router, NAT, logical router policy를 봐야 합니다.
Egress 흐름
Pod
-> br-int logical switch
-> cluster logical router
-> gateway router 또는 local gateway path
-> br-ex
-> underlay/default gateway
EgressIP를 쓰면 특정 egress IP가 특정 node/chassis에 배정된다.
OVN NBDB에서는 NAT, logical router policy, external_ids 쪽에서 흔적을 찾는다.
EgressFirewall은 namespace 단위 outbound ACL로 보는 편이 빠르다.# 클러스터에 EgressIP/EgressFirewall을 쓰는 경우만 확인한다.
oc get egressip -A 2>/dev/null || true
oc get egressfirewall -A 2>/dev/null || true
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- bash -c '
echo "## NAT"
ovn-nbctl --format=table --columns=type,logical_ip,external_ip,logical_port,external_ids list NAT
echo "## Logical router policy"
ovn-nbctl --format=table --columns=priority,match,action,nexthops,external_ids list Logical_Router_Policy
echo "## Egress firewall ACL candidates"
ovn-nbctl --format=table --columns=priority,direction,match,action,external_ids list ACL \
| egrep -i "egress|egressfirewall|egressip" -A2 -B2 || true
'
oc debug node/${NODE_A} -- chroot /host bash -c '
ip -br addr
ip route
ovs-vsctl show
'18. 현장형 egress 이슈: namespace 방화벽과 source IP 확인
실제 운영에서 egress 문제는 `외부로 안 나간다` 한 줄로 오지 않습니다. namespace에 default deny를 걸고, Route/Ingress와 EgressFirewall까지 같이 쓰는 순간 source IP가 어디로 보이는지부터 확인해야 합니다. 특히 Route를 타고 들어오는 요청은 정책 작성자가 예상한 ingress Pod IP가 아니라 router node, service network, hostNetwork 경로의 주소로 관측될 수 있습니다.
현장형 egress 이슈에서 자주 나온 패턴
1. namespace default deny를 걸었다.
2. Pod끼리 같은 namespace 통신은 허용했다.
3. Route/Ingress로 들어오는 트래픽을 허용하려고 openshift-ingress namespace selector를 넣었다.
4. 실제 Pod 로그를 보니 source가 ingress Pod IP가 아니라 router node/service network 대역으로 찍혔다.
5. 그래서 NetworkPolicy는 namespaceSelector만으로 끝나지 않고, 관측된 source 대역 ipBlock도 같이 검증해야 했다.
6. EgressFirewall은 default deny 뒤에 DNS, service/router network, 필요한 외부 CIDR만 순서대로 Allow해야 했다.
7. 외부로 나가는 source IP가 기대한 IP와 다르면 EgressIP, NAT, node gateway, br-ex route, logical router policy를 같이 봐야 했다.아래 실습은 원래 장애 기록에서 썼던 방식만 일반화한 것입니다. 실제 도메인, 기관명, IP는 빼고, `net-a`, `net-b`, `ncat`, `curl`, NetworkPolicy, EgressFirewall만 남겼습니다. 핵심은 정책을 먼저 믿는 게 아니라 Pod 서버 로그와 tcpdump로 실제 source를 확인한 뒤 정책을 맞추는 것입니다.
# 예시 대역을 사용한다. 실제 환경에서는 service/router/node/egress 대역으로 바꾼다.
NS=egress-lab
ROUTER_NODE_CIDR=192.0.2.0/24
SERVICE_CIDR=172.30.0.0/16
DNS_IP=172.30.0.10
ALLOWED_EXTERNAL_CIDR=198.51.100.0/24
oc new-project ${NS}
cat <<'EOF' | oc apply -f -
apiVersion: v1
kind: Pod
metadata:
name: net-a
namespace: egress-lab
labels:
app: net-a
spec:
hostUsers: false
containers:
- name: net-a
image: image-registry.openshift-image-registry.svc:5000/openshift/network-tools:latest
command: ["sleep","infinity"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
---
apiVersion: v1
kind: Pod
metadata:
name: net-b
namespace: egress-lab
labels:
app: net-b
spec:
hostUsers: false
containers:
- name: net-b
image: image-registry.openshift-image-registry.svc:5000/openshift/network-tools:latest
command: ["sleep","infinity"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
---
apiVersion: v1
kind: Service
metadata:
name: net-b-svc
namespace: egress-lab
spec:
selector:
app: net-b
ports:
- name: http
port: 8080
targetPort: 8080
EOF
oc -n ${NS} exec net-b -- sh -c '
pkill ncat || true
nohup sh -c "
while true; do
ncat -l 8080 --keep-open --verbose -c '''echo -e "HTTP/1.1 200 OK
ok"''' ;
done
" >/tmp/ncat8080.log 2>&1 &
'
oc -n ${NS} exec net-b -- tail -n 20 /tmp/ncat8080.log || true
oc -n ${NS} exec net-a -- curl -sS --max-time 2 http://net-b-svc:8080default deny를 넣은 뒤에는 허용 정책을 작게 추가합니다. 같은 namespace 내부 통신, router에서 들어오는 트래픽, DNS, service network, 승인된 외부 CIDR을 분리합니다. `namespaceSelector`만으로 충분하지 않은 환경이면 관측된 router/node source 대역을 `ipBlock`으로 따로 검증합니다.
cat <<EOF | oc apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress-egress
namespace: ${NS}
spec:
podSelector: {}
policyTypes: ["Ingress","Egress"]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-same-namespace
namespace: ${NS}
spec:
podSelector: {}
policyTypes: ["Ingress","Egress"]
ingress:
- from:
- podSelector: {}
egress:
- to:
- podSelector: {}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-router-observed-source
namespace: ${NS}
spec:
podSelector: {}
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: openshift-ingress
- from:
- ipBlock:
cidr: ${ROUTER_NODE_CIDR}
ports:
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-for-dns-service-router-and-approved-external
namespace: ${NS}
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- ipBlock:
cidr: ${SERVICE_CIDR}
- to:
- ipBlock:
cidr: ${ROUTER_NODE_CIDR}
- to:
- ipBlock:
cidr: ${ALLOWED_EXTERNAL_CIDR}
- to:
- ipBlock:
cidr: ${DNS_IP}/32
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
EOFEgressFirewall은 namespace outbound의 마지막 문지기처럼 다룹니다. Allow 목록을 먼저 두고 마지막에 `0.0.0.0/0` Deny를 두면 의도가 명확합니다. DNS를 막아놓고 외부 통신 장애로 착각하는 경우가 많으니 DNS 테스트를 반드시 같이 넣습니다.
cat <<EOF | oc apply -f -
apiVersion: k8s.ovn.org/v1
kind: EgressFirewall
metadata:
name: default
namespace: ${NS}
spec:
egress:
- type: Allow
to:
cidrSelector: ${SERVICE_CIDR}
- type: Allow
to:
cidrSelector: ${ROUTER_NODE_CIDR}
- type: Allow
to:
cidrSelector: ${ALLOWED_EXTERNAL_CIDR}
- type: Deny
to:
cidrSelector: 0.0.0.0/0
EOF
oc -n ${NS} get netpol,egressfirewall
oc -n ${NS} exec net-a -- nslookup kubernetes.default.svc.cluster.local ${DNS_IP} || true
oc -n ${NS} exec net-a -- curl -v --max-time 3 http://net-b-svc:8080
oc -n ${NS} exec net-a -- curl -v --max-time 3 http://198.51.100.10 || true
oc -n ${NS} exec net-a -- curl -v --max-time 3 http://203.0.113.10 || true여기서도 결론은 단순합니다. route hostname이 안 되면 DNS, Route, router, Service, endpoint를 나눠 봅니다. 직접 Service는 되는데 Route만 안 되면 ingress/router source와 NetworkPolicy를 봅니다. 외부 CIDR만 안 되면 EgressFirewall, EgressIP, NAT, `br-ex` route를 봅니다.
# 1. 실제 source가 무엇으로 보이는지 Pod 서버 로그에서 확인한다.
oc -n ${NS} exec net-b -- tail -n 80 /tmp/ncat8080.log
# 2. Route/Ingress 경유 시 router Pod와 node 위치를 본다.
oc -n openshift-ingress get pod -o wide
oc -n openshift-ingress-operator get ingresscontroller -o yaml
# 3. NetworkPolicy가 OVN ACL로 내려갔는지 본다.
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- bash -c "
ovn-nbctl --format=table --columns=priority,direction,match,action,external_ids list ACL \
| egrep -i '${NS}|egressFirewall|networkpolicy|${ROUTER_NODE_CIDR}|${ALLOWED_EXTERNAL_CIDR}' -A3 -B2 || true
"
# 4. EgressIP/NAT source IP가 기대와 다르면 NAT와 logical router policy를 먼저 본다.
oc get egressip -A 2>/dev/null || true
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- bash -c '
ovn-nbctl --format=table --columns=type,logical_ip,external_ip,logical_port,external_ids list NAT
ovn-nbctl --format=table --columns=priority,match,action,nexthops,external_ids list Logical_Router_Policy
'
# 5. 노드에서는 br-ex와 underlay route를 같이 본다.
oc debug node/${NODE_A} -- chroot /host bash -c '
ip -br addr
ip route
ip rule
ovs-vsctl show
tcpdump -ni any host 198.51.100.10 -c 40
'예전 NAT 장애 기록에서 반복적으로 보인 교훈도 같습니다. source IP는 사람이 기대한 이름이 아니라 실제 egress interface, route table, NAT rule이 결정합니다. 어떤 환경에서는 특정 IP disable, NAT 해제, 방화벽 rule 순서, gateway failover 같은 작업 뒤에 route table이나 source NAT가 바뀌면서 `외부는 되는데 내부 hairpin은 안 됨`, `같은 서비스인데 source IP가 달라짐` 같은 증상이 나옵니다. OVN-Kubernetes에서도 같은 감각으로 NBDB `NAT`, `Logical_Router_Policy`, `ACL`, 노드 `br-ex`, `ip route`, `ip rule`을 같이 봐야 합니다.
19. DB 일관성 확인
OVN-Kubernetes에서 제일 헷갈리는 장애는 API에는 있는데 DB에는 없거나, NBDB에는 있는데 SBDB/노드에 반영되지 않은 상태입니다. 이때는 DB 계층별로 끊어서 봅니다.
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- bash -c '
echo "## NBDB connection and schema"
ovn-nbctl get-connection
ovn-nbctl show | sed -n "1,160p"
'
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c sbdb -- bash -c '
echo "## SBDB connection and chassis"
ovn-sbctl get-connection
ovn-sbctl show | sed -n "1,180p"
'
oc -n ${OVN_NS} logs ${OVN_CP_POD} -c northd --tail=160
oc -n ${OVN_NS} logs ${OVN_NODE_POD} -c ovn-controller --tail=160
oc -n ${OVN_NS} logs ${OVN_NODE_POD} -c ovnkube-controller --tail=160 2>/dev/null || true
oc -n ${OVN_NS} logs ${OVN_NODE_POD} -c ovnkube-node --tail=160 2>/dev/null || trueNBDB에 logical port가 없으면 ovnkube control-plane이 API 이벤트를 제대로 처리했는지 봅니다. NBDB에는 있는데 SBDB Port_Binding이 없으면 `ovn-northd` 쪽을 봅니다. SBDB에는 있는데 노드 OVS에 포트나 flow가 없으면 해당 노드의 `ovn-controller`와 OVSDB 연결을 봅니다.
20. OVS/OVN 내부 명령
OVS는 `ovs-vsctl show`만으로 끝나지 않습니다. bridge, interface, OpenFlow, datapath port까지 같이 봐야 합니다. OVN 쪽은 `ovn-controller`의 local 상태를 appctl로 확인할 수 있습니다.
oc debug node/${NODE_A} -- chroot /host bash -c '
echo "## ovs-vswitchd commands"
ovs-appctl -t ovs-vswitchd list-commands | egrep "ofproto|bridge|dpif|coverage|memory" | sed -n "1,120p"
echo "## bridge dump-flows"
ovs-ofctl -O OpenFlow13 dump-flows br-int | sed -n "1,120p"
echo "## datapath ports"
ovs-appctl -t ovs-vswitchd dpif/show | sed -n "1,180p"
'
# OVN 컨테이너 안의 ovn-controller ctl socket은 버전마다 경로가 다를 수 있다.
oc -n ${OVN_NS} exec ${OVN_NODE_POD} -c ovn-controller -- bash -c '
find /var/run/ovn -maxdepth 2 -type s -o -type f 2>/dev/null | sort
ovn-appctl -t ovn-controller.ctl connection-status 2>/dev/null || true
ovn-appctl -t ovn-controller.ctl ct-zone-list 2>/dev/null || true
ovn-appctl -t ovn-controller.ctl meter-table-list 2>/dev/null || true
'21. 장애 지점별 빠른 분기
| 증상 | 먼저 볼 계층 | 확인 리소스 |
|---|---|---|
| Pod가 ContainerCreating | CRI/CNI | kubelet log, CRI sandbox, CNI socket, Pod event |
| Pod IP는 있는데 통신 불가 | Pod netns/OVS | eth0, peer veth, OVS Interface, br-int flow |
| 같은 노드는 되고 다른 노드는 안 됨 | Overlay/Chassis | SBDB Chassis, Encap, UDP 6081, tunnel flow |
| PodIP는 되고 ServiceIP가 안 됨 | Service LB | EndpointSlice, NBDB Load_Balancer, SBDB logical flow |
| 특정 namespace만 막힘 | Policy | NetworkPolicy, ACL, Address_Set, Port_Group |
| 외부 통신만 이상함 | Egress/Gateway | br-ex, NAT, Logical_Router_Policy, EgressIP assignment |
| 노드별로 다르게 동작 | ovn-controller/OVS | Port_Binding chassis, local OVSDB, br-int flow |
# 1. Operator와 DaemonSet 상태
oc get co network
oc -n openshift-network-operator get deploy,pod -o wide
oc -n openshift-ovn-kubernetes get ds,deploy,pod -o wide
# 2. 노드별 ovnkube-node 재시작/ready 상태
oc -n ${OVN_NS} get pods -l app=ovnkube-node -o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[*].restartCount'
# 3. Pod annotation과 logical port 매칭
oc -n ovn-lab get pod src -o json | jq '.metadata.annotations,.status.podIP,.spec.nodeName'
# 4. NBDB/SBDB에 리소스가 내려왔는지
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c nbdb -- ovn-nbctl --format=table --columns=name,external_ids list Logical_Switch_Port | grep ovn-lab || true
oc -n ${OVN_NS} exec ${OVN_CP_POD} -c sbdb -- ovn-sbctl --format=table --columns=logical_port,chassis,external_ids list Port_Binding | grep ovn-lab || true
# 5. OVS에 실제 포트와 flow가 있는지
oc debug node/${NODE_A} -- chroot /host bash -c '
ovs-vsctl --columns=name,ofport,external_ids list Interface | grep -A8 -B2 ovn-lab || true
ovs-ofctl -O OpenFlow13 dump-flows br-int | wc -l
'
# 6. 다른 노드로 가는 Geneve가 보이는지
oc debug node/${NODE_A} -- chroot /host bash -c 'ss -lunp | grep 6081 || true'22. 정리
OVN-Kubernetes는 Kubernetes CNI 하나로만 보면 너무 얕게 보입니다. 실제로는 Kubernetes API, CNI daemon socket, OVN NBDB/SBDB, `ovn-northd`, `ovn-controller`, OVSDB, `br-int`, `br-ex`, Geneve tunnel, logical load balancer, ACL, NAT가 한 줄로 이어진 시스템입니다.
장애 분석은 항상 계층을 나눠야 합니다. API에 리소스가 있는지, NBDB에 논리 객체가 있는지, SBDB에 실행 계획이 있는지, 해당 chassis에 binding됐는지, 노드 OVS에 port와 flow가 있는지, underlay에서 Geneve가 오가는지 순서대로 확인하면 막연한 네트워크 장애가 훨씬 좁아집니다.
실습 정리
oc delete project ovn-lab
# 운영 클러스터에서 아래 항목은 삭제하지 말고 읽기 전용 확인만 한다.
oc -n openshift-ovn-kubernetes get pods -o wide
oc get network.operator cluster -o yaml | sed -n '1,120p'