<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>스터디 기록장</title>
    <link>https://cocopam.tistory.com/</link>
    <description>인프라 엔지니어의 끄적끄적</description>
    <language>ko</language>
    <pubDate>Sun, 26 Jul 2026 02:21:49 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>꼬꼬팜</managingEditor>
    <image>
      <title>스터디 기록장</title>
      <url>https://tistory1.daumcdn.net/tistory/4417439/attach/afe47779780043c4a4421f1bd730fbc9</url>
      <link>https://cocopam.tistory.com</link>
    </image>
    <item>
      <title>on-prem cluster에 Cloud 노드 조인하기</title>
      <link>https://cocopam.tistory.com/59</link>
      <description>&lt;h2&gt;배경&lt;/h2&gt;
&lt;p&gt;기존에 집에 PC 두 대로 on-premise 환경에서 k8s 클러스터를 구축해두었다.&lt;/p&gt;
&lt;p&gt;추가로 리소스가 필요한 경우가 종종 있었는데, 그때마다 서버를 구매하기엔 부담돼서 public cloud 환경의 server를 필요할 때마다 증설할 수 있는 방법을 찾아보기로 했다.&lt;/p&gt;
&lt;h2&gt;Cloud로 전부 안 옮기나?&lt;/h2&gt;
&lt;p&gt;사실 모든 리소스를 public cloud로 옮기는 게 가장 편하긴 하다. 전력, HW fault 같은 이슈 관리를 cloud에게 맡기면 편하다.&lt;/p&gt;
&lt;p&gt;다만 그렇기에 기존 인프라 리소스를 모두 대체하기엔 비용이 너무 비싸다.&lt;/p&gt;
&lt;p&gt;기존 home infra&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Storage -&amp;gt; block storage, object storage&lt;/li&gt;
&lt;li&gt;PC 2ea -&amp;gt; Server instance&lt;/li&gt;
&lt;li&gt;Public IP -&amp;gt; Elastic IP&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;그래서 기존 인프라를 유지한 상태로, scale-out이 용이한 방향으로 구성을 진행한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;요구사항&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;사설망 통신&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;내부 환경에서는 172 대역의 사설 대역을 사용하고, gateway 겸 SNAT를 통과해 외부와 통신함&lt;/p&gt;
&lt;p&gt;NCP 서버가 워커로 붙으려면 이 사설 대역과 양방향 통신이 되어야 하는데, 외부에서 먼저 들어올 경로가 없으니 IPsec 같은 터널이 필요함&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;기존 K8s Cluster와 통합&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;클러스터에 이미 올라가 있는 아래 컴포넌트들과 새 노드가 문제없이 맞물려야 함&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Internal DNS&lt;/li&gt;
&lt;li&gt;Storage&lt;/li&gt;
&lt;li&gt;cilium CNI&lt;/li&gt;
&lt;li&gt;monitoring stack&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;구성 방안&lt;/h2&gt;
&lt;p&gt;일단 가능한 방향부터 생각해봤다.&lt;/p&gt;
&lt;h3&gt;NCP 관리형 IPsec VPN&lt;/h3&gt;
&lt;p&gt;NCP는 VPC용 관리형 IPsec VPN Gateway 상품을 제공한다. 다만 온프레미스 데이터센터급 연동을 상정한 구성이라, 집 쪽에도 고정 공인 IP를 가진 IPsec 피어 장비가 있어야 한다. (해당 내용은 AWS와 같은 외부 클라우드 서비드와 동일)&lt;/p&gt;
&lt;p&gt;공인 IP는 고정이지만, 지금 공유기는 포트포워딩만 해주는 수준이라 IPsec 피어 역할을 할 전용 게이트웨이가 없다. 게이트웨이 이중화 같은 기능도 워커 노드 하나 붙이는 용도로는 과했다. 그래서 기각.&lt;/p&gt;
&lt;h3&gt;기존 WireGuard(wg-easy) 재사용 시도&lt;/h3&gt;
&lt;p&gt;집에는 이미 &lt;code&gt;soyo&lt;/code&gt;에서 [[Cloudflare Tunnel을 활용한 k8s Ingress 대체|외부 접속용]] wg-easy 컨테이너가 떠있었다. 여기에 NCP 서버를 피어로 추가하면 될 줄 알았는데, 확인해보니 불가능했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec wg-easy iptables -t nat -S POSTROUTING
# -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;wg-easy는 브리지 네트워크 안에서 도는 컨테이너라 VPN 클라이언트 트래픽을 전부 &lt;strong&gt;MASQUERADE(SNAT)&lt;/strong&gt; 함. 개인이 노트북으로 집에 들어올 땐 문제없지만, k8s 노드 간 통신은 불가능하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;soyo → NCP 방향(&lt;code&gt;kubectl exec&lt;/code&gt;, kubelet 10250) 라우팅이 없음(NAT는 단방향)&lt;/li&gt;
&lt;li&gt;NCP → 클러스터 방향도 SNAT 때문에 kubelet/Cilium이 보는 소스 IP가 실제 노드 IP와 달라짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;컨테이너를 개조해서 우회할 수도 있었지만, 재시작마다 규칙이 날아가고 업데이트에 깨지기 쉬워서 포기했다. wg-easy는 개인 VPN 용도로 그대로 두고, 클러스터 전용 네이티브 WireGuard 인터페이스를 하나 더 올리기로 했다.&lt;/p&gt;
&lt;h3&gt;WireGuard 터널 구성&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    subgraph Home[&amp;quot;집 사설망&amp;quot;]
        direction TB
        LAN[&amp;quot;172.16.0.0/16&amp;lt;br/&amp;gt;soyo 등&amp;quot;]
        W1SUB[&amp;quot;192.168.50.0/24&amp;lt;br/&amp;gt;k8s-worker-1&amp;quot;]
    end

    subgraph Tunnel[&amp;quot;WireGuard 터널&amp;lt;br/&amp;gt;192.168.250.0/24&amp;quot;]
        direction LR
        wg1[&amp;quot;wg1&amp;lt;br/&amp;gt;192.168.250.1&amp;quot;]
        wg0[&amp;quot;wg0&amp;lt;br/&amp;gt;192.168.250.2&amp;quot;]
    end

    subgraph NCP[&amp;quot;NCP VPC&amp;quot;]
        NCPSUB[&amp;quot;10.0.90.0/24&amp;lt;br/&amp;gt;k8s-ncp-worker&amp;quot;]
    end

    LAN --- wg1
    W1SUB --- wg1
    wg1 --&amp;gt;|&amp;quot;outbound&amp;lt;br/&amp;gt;203.0.113.10:51820, keepalive 25s&amp;quot;| wg0
    wg0 -.-&amp;gt;|응답 트래픽| wg1
    wg0 --- NCPSUB&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NCP 서버는 공인 IP를 갖고 있어서 리스너로 두고, soyo가 클라이언트로 접속하는 방향으로 잡았다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;공유기 포트포워딩을 새로 열 필요 없음 (soyo가 outbound로 붙으니까)&lt;/li&gt;
&lt;li&gt;집 공인 IP가 유동이어도 &lt;code&gt;PersistentKeepalive&lt;/code&gt;로 알아서 재연결됨&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wg1&lt;/code&gt;이 soyo 호스트 네임스페이스에 그대로 있어서 홈 LAN ↔ NCP 노드가 NAT 없이 라우팅됨&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# NCP: /etc/wireguard/wg0.conf
[Interface]
Address = 192.168.250.2/24
PrivateKey = &amp;lt;ncp-private-key&amp;gt;
ListenPort = 51820

[Peer]
PublicKey = &amp;lt;soyo-public-key&amp;gt;
AllowedIPs = 192.168.250.1/32, 172.16.0.0/16, 192.168.50.0/24&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# soyo: /etc/wireguard/wg1.conf
[Interface]
Address = 192.168.250.1/24
PrivateKey = &amp;lt;soyo-private-key&amp;gt;

[Peer]
PublicKey = &amp;lt;ncp-public-key&amp;gt;
Endpoint = 203.0.113.10:51820
AllowedIPs = 192.168.250.0/24
PersistentKeepalive = 25&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NCP 콘솔의 ACG(방화벽)는 집 공인 IP에서만 &lt;code&gt;UDP/51820&lt;/code&gt;, &lt;code&gt;TCP/22&lt;/code&gt;를 열어뒀다.&lt;/p&gt;
&lt;h4&gt;Pod 네트워크 (Cilium CNI)&lt;/h4&gt;
&lt;p&gt;여기에 Cilium CNI까지 얹으면 파드 대역은 이렇게 나뉜다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    subgraph Pool[&amp;quot;Cilium cluster-pool 10.0.0.0/8&amp;quot;]
        direction LR
        subgraph NodeSoyo[&amp;quot;soyo&amp;quot;]
            PodSoyo[&amp;quot;Pod CIDR&amp;lt;br/&amp;gt;10.0.0.0/24&amp;quot;]
        end
        subgraph NodeW1[&amp;quot;k8s-worker-1&amp;quot;]
            PodW1[&amp;quot;Pod CIDR&amp;lt;br/&amp;gt;10.0.1.0/24&amp;quot;]
        end
        subgraph NodeNCP[&amp;quot;k8s-ncp-worker&amp;quot;]
            PodNCP[&amp;quot;Pod CIDR&amp;lt;br/&amp;gt;10.0.2.0/24&amp;quot;]
        end
    end

    PodSoyo &amp;lt;--&amp;gt;|&amp;quot;VXLAN&amp;lt;br/&amp;gt;(홈 LAN 직결)&amp;quot;| PodW1
    PodSoyo &amp;lt;--&amp;gt;|&amp;quot;VXLAN over WireGuard&amp;lt;br/&amp;gt;MTU 1370&amp;quot;| PodNCP&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WireGuard 터널은 물리 대역(172/192/10.0.90)만 이어주고, 그 위에서 Cilium이 파드 대역(10.0.0.0/8)을 한 번 더 VXLAN으로 캡슐화해서 올리는 구조다.&lt;/p&gt;
&lt;h3&gt;kubeadm join&lt;/h3&gt;
&lt;p&gt;터널이 붙은 뒤엔 평범한 kubeadm 워커 조인. 다만 API 서버 엔드포인트는 WireGuard IP가 아니라 원래 advertise 주소(&lt;code&gt;172.16.1.10:6443&lt;/code&gt;)를 그대로 써야 kubeadm 인증서 SAN 검증에 안 걸린다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# soyo에서 토큰 발급
kubeadm token create --print-join-command

# NCP 노드에서 실행, kubelet node-ip는 WireGuard IP로 고정
echo &amp;#39;KUBELET_EXTRA_ARGS=&amp;quot;--node-ip=192.168.250.2&amp;quot;&amp;#39; &amp;gt; /etc/default/kubelet
kubeadm join 172.16.1.10:6443 --token ... --discovery-token-ca-cert-hash ...&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Terraform으로 IaC화&lt;/h3&gt;
&lt;p&gt;NCP 서버를 매번 콘솔에서 만들다 보니, 다음번 스케일아웃 때는 코드로 관리하려고 했다. NCP는 공식 Terraform 프로바이더(&lt;code&gt;NaverCloudPlatform/ncloud&lt;/code&gt;)를 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &amp;quot;ncloud_subnet&amp;quot; &amp;quot;k8s_worker&amp;quot; {
  vpc_no         = data.ncloud_vpc.main.vpc_no
  subnet         = var.subnet_cidr   # 10.0.90.0/24
  zone           = &amp;quot;KR-1&amp;quot;
  network_acl_no = data.ncloud_vpc.main.default_network_acl_no
  subnet_type    = &amp;quot;PUBLIC&amp;quot;
}

resource &amp;quot;ncloud_access_control_group_rule&amp;quot; &amp;quot;k8s_worker&amp;quot; {
  access_control_group_no = ncloud_access_control_group.k8s_worker.id

  inbound {
    protocol   = &amp;quot;UDP&amp;quot;
    ip_block   = &amp;quot;${var.home_public_ip}/32&amp;quot;
    port_range = &amp;quot;51820&amp;quot;
  }
  # ...
}

resource &amp;quot;ncloud_server&amp;quot; &amp;quot;k8s_worker&amp;quot; {
  subnet_no            = ncloud_subnet.k8s_worker.id
  server_image_number  = local.ubuntu_base_image_number  # ubuntu-24.04-base
  server_spec_code     = data.ncloud_server_specs.worker.server_spec_list[0].server_spec_code
  login_key_name       = ncloud_login_key.k8s_worker.key_name
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;VPC/서브넷 CIDR을 데이터소스로 조회해서 겹치지 않는 대역을 계산하고, &lt;code&gt;terraform plan&lt;/code&gt;으로 미리 확인한 뒤 apply하는 흐름이라 다음 서버 증설부터는 콘솔 클릭 없이 진행 가능함.&lt;/p&gt;
&lt;h2&gt;Troubleshooting&lt;/h2&gt;
&lt;h3&gt;서브넷 CIDR이 Cilium 파드 대역과 충돌&lt;/h3&gt;
&lt;p&gt;NCP VPC에 서버를 처음 올릴 때 서브넷을 딱히 신경 쓰지 않고 기존에 파둔 &lt;code&gt;10.0.1.0/24&lt;/code&gt;에 그대로 붙였는데, 여기서 문제가 터졌다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl -n kube-system get cm cilium-config -o yaml | grep cluster-pool
# cluster-pool-ipv4-cidr: 10.0.0.0/8
# cluster-pool-ipv4-mask-size: &amp;quot;24&amp;quot;

kubectl get ciliumnodes -o custom-columns=&amp;#39;NAME:.metadata.name,PODCIDRS:.spec.ipam.podCIDRs&amp;#39;
# k8s-worker-1   [10.0.1.0/24]   &amp;lt;- NCP VPC 서브넷과 완전히 동일&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cilium은 원격 파드 대역으로 가는 라우트를 호스트 라우팅 테이블에 직접 설치함(&lt;code&gt;ip route&lt;/code&gt;에 &lt;code&gt;10.0.1.0/24 via ... dev cilium_host&lt;/code&gt;).&lt;br&gt;이 상태로 NCP 노드를 그 서브넷에 붙이면, NCP 노드 입장에서 자기 VPC 게이트웨이로 가야 할 패킷이 &lt;strong&gt;파드 라우트로 잘못 들어가서 통신 자체가 끊긴다&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;해결은 단순했다. 파드 대역과 절대 겹치지 않는 &lt;code&gt;10.0.90.0/24&lt;/code&gt;로 새 서브넷을 파서 서버를 옮겼다. Cilium &lt;code&gt;cluster-pool&lt;/code&gt;이 &lt;code&gt;/8&lt;/code&gt;을 쓴다면, 클라우드 쪽 사설 서브넷도 그 안에서 뽑히는 경우가 많아서 미리 겹치는지 확인이 필요하다.&lt;/p&gt;
&lt;h3&gt;Cilium CrashLoopBackOff&lt;/h3&gt;
&lt;p&gt;서브넷을 옮기고 조인했더니 Cilium 파드가 계속 &lt;code&gt;CrashLoopBackOff&lt;/code&gt;에 빠졌다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level=error msg=&amp;quot;Unable to contact k8s api-server&amp;quot; ipAddr=https://10.96.0.1:443
error=&amp;quot;dial tcp 10.96.0.1:443: i/o timeout&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;API 서버 서비스 IP(&lt;code&gt;10.96.0.1&lt;/code&gt;)로 가는 트래픽인데 타임아웃이 난다. tcpdump로 까보니 원인은 라우팅 순서였다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NCP 노드에서 서비스 IP로 나가는 패킷은 &lt;strong&gt;DNAT되기 전에&lt;/strong&gt; 라우팅 결정이 먼저 일어나서, 소스 주소가 NCP 노드의 실제 IP(&lt;code&gt;10.0.90.6&lt;/code&gt;)로 찍힌 채 나간다&lt;/li&gt;
&lt;li&gt;이 패킷이 WireGuard 터널을 타고 soyo에 도착하는데, soyo의 &lt;code&gt;wg1&lt;/code&gt; 설정에는 &lt;code&gt;AllowedIPs&lt;/code&gt;에 &lt;code&gt;192.168.250.0/24&lt;/code&gt;만 있어서 &lt;code&gt;10.0.90.6&lt;/code&gt;발 패킷을 거부(drop)하고 있었다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# 수정 전
AllowedIPs = 192.168.250.0/24

# 수정 후: NCP 서브넷 대역을 추가해야 서비스 트래픽이 통과한다
AllowedIPs = 192.168.250.0/24, 10.0.90.0/24&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AllowedIPs&lt;/code&gt;는 출발지 대역이자 목적지 대역으로 동시에 쓰인다. 클러스터 서비스/파드 트래픽처럼 소스 IP가 여러 대역을 오가는 경우, 상대 노드의 전체 서브넷을 열어줘야 한다.&lt;/p&gt;
&lt;h3&gt;MTU&lt;/h3&gt;
&lt;p&gt;Cloud Server들은 대부분 VM과 오버레이 네트워크(VPC) 위에서 동작한다. 여기에 WireGuard 터널을 얹고, 다시 Cilium VXLAN까지 얹히면서(오버레이 위에 오버레이) MTU를 낮춰야 했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WireGuard 기본 MTU: 1420&lt;/li&gt;
&lt;li&gt;VXLAN 헤더 오버헤드: 50바이트&lt;/li&gt;
&lt;li&gt;계산상 여유치: &lt;code&gt;1420 - 50 = 1370&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl -n kube-system patch cm cilium-config -p &amp;#39;{&amp;quot;data&amp;quot;:{&amp;quot;mtu&amp;quot;:&amp;quot;1370&amp;quot;}}&amp;#39;
kubectl -n kube-system rollout restart ds/cilium&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;파드 두 개(on-prem 쪽 / NCP 쪽)를 띄워서 페이로드 크기를 변경해가면서 테스트 진행&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl exec home-nettest -- ping -c 2 -s 1292 -W 2 $NCP_POD_IP   # 0% loss
kubectl exec home-nettest -- ping -c 2 -s 1293 -W 2 $NCP_POD_IP   # 100% loss&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실측 임계값은 payload 1292바이트(IP+ICMP 헤더 28바이트 포함 총 1320바이트). &lt;/p&gt;
&lt;p&gt;설정한 파드 인터페이스 MTU(1370)보다 50바이트 낮은데, 이 차이가 &lt;strong&gt;Cilium VXLAN 오버헤드와 정확히 일치&lt;/strong&gt;한다.&lt;br&gt;파드 간 트래픽이 노드 사이 구간에서 Cilium 자체 VXLAN 캡슐화를 한 번 더 타면서 유효 MTU가 그만큼 줄어든다. TCP는 MSS 협상 덕에 문제없지만, &lt;strong&gt;ICMP나 UDP처럼 협상 없이 나가는 트래픽은 이 값을 넘으면 드롭&lt;/strong&gt;되니 실측 검증이 필요하다.&lt;/p&gt;
&lt;h3&gt;Harbor 레지스트리 TLS&lt;/h3&gt;
&lt;p&gt;사설 Harbor(&lt;code&gt;harbor.home.internal&lt;/code&gt;)는 자체 서명 인증서를 쓰고 있어서, 기존 노드들은 &lt;code&gt;containerd&lt;/code&gt;의 &lt;code&gt;certs.d/hosts.toml&lt;/code&gt;로 &lt;code&gt;skip_verify&lt;/code&gt;를 설정해뒀다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;# soyo (containerd 2.2.1) — 정상 동작
server = &amp;quot;https://harbor.home.internal&amp;quot;
[host.&amp;quot;https://harbor.home.internal&amp;quot;]
  capabilities = [&amp;quot;pull&amp;quot;, &amp;quot;resolve&amp;quot;]
  skip_verify = true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NCP 노드(Ubuntu 24.04 기본 저장소 containerd, 1.7.x)에 똑같이 설정했는데 계속 인증서 오류가 났다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x509: certificate is valid for ...traefik.default, not harbor.home.internal&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;containerd 1.7 계열은 &lt;code&gt;certs.d&lt;/code&gt; 기반 &lt;code&gt;hosts.toml&lt;/code&gt;의 &lt;code&gt;skip_verify&lt;/code&gt;가 &lt;strong&gt;CRI 경로에서 제대로 안 먹는&lt;/strong&gt; 이슈가 있다. 결국 레거시 방식인 &lt;code&gt;config.toml&lt;/code&gt;의 &lt;code&gt;registry.configs&lt;/code&gt; 섹션으로 우회했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[plugins.&amp;quot;io.containerd.grpc.v1.cri&amp;quot;.registry.configs.&amp;quot;harbor.home.internal&amp;quot;.tls]
  insecure_skip_verify = true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 클러스터라도 노드마다 containerd 버전이 다르면 레지스트리 신뢰 설정 방식이 달라질 수 있다. &lt;code&gt;crictl pull&lt;/code&gt;로 실제 이미지를 당겨서 성공까지 확인.&lt;/p&gt;
&lt;h2&gt;노드 격리&lt;/h2&gt;
&lt;p&gt;NCP 노드는 스토리지(NFS)나 MetalLB에 의존하는 워크로드를 &lt;strong&gt;태울 수 없다&lt;/strong&gt;. NFS는 터널을 타고 왕복하면 느리고, MetalLB L2 광고는 애초에 홈 LAN 밖으로 안 나간다. 그래서 기본적으로는 아무것도 스케줄되지 않게 막아뒀다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl taint nodes k8s-ncp-worker node.home-infra/remote=true:NoSchedule
kubectl label nodes k8s-ncp-worker \
  node.home-infra/location=ncp \
  topology.kubernetes.io/zone=ncp-kr1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;모니터링(cadvisor, node-exporter)처럼 모든 노드에 반드시 떠야 하는 DaemonSet에는 toleration을 추가해서 예외를 뒀다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spec:
  template:
    spec:
      tolerations:
        - key: node.home-infra/remote
          operator: Exists
          effect: NoSchedule&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;검증&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get nodes
# k8s-ncp-worker   Ready    &amp;lt;none&amp;gt;          v1.30.14
# k8s-worker-1     Ready    &amp;lt;none&amp;gt;          v1.30.14
# soyo             Ready    control-plane   v1.30.14&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;파드 간 레이턴시: soyo ↔ NCP 파드 왕복 약 5ms (인터넷 경유치고 나쁘지 않다)&lt;/li&gt;
&lt;li&gt;Harbor 이미지 pull: &lt;code&gt;imagePullSecret&lt;/code&gt; + 위 TLS 설정으로 정상 pull, 파드 &lt;code&gt;Running&lt;/code&gt; 확인&lt;/li&gt;
&lt;li&gt;모니터링 커버리지: &lt;code&gt;cadvisor&lt;/code&gt;/&lt;code&gt;node-exporter&lt;/code&gt;가 3개 노드 전부에 떠서 Grafana에서 동일하게 조회됨&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    subgraph Home[&amp;quot;집 (172.16.0.0/16)&amp;quot;]
        soyo[&amp;quot;soyo&amp;lt;br/&amp;gt;control-plane&amp;quot;]
        w1[&amp;quot;k8s-worker-1&amp;quot;]
    end
    subgraph NCP[&amp;quot;NCP VPC (10.0.90.0/24)&amp;quot;]
        w2[&amp;quot;k8s-ncp-worker&amp;lt;br/&amp;gt;taint: remote=true&amp;quot;]
    end
    soyo &amp;lt;--&amp;gt;|Cilium vxlan| w1
    soyo &amp;lt;--&amp;gt;|&amp;quot;WireGuard(wg1↔wg0)&amp;lt;br/&amp;gt;+ Cilium vxlan, MTU 1370&amp;quot;| w2&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;끝&lt;/h2&gt;
&lt;p&gt;관리형 IPsec VPN 상품 없이도, 이미 떠있는 서버 하나 + 네이티브 WireGuard로 하이브리드 워커를 붙일 수 있었다. 다만 &amp;quot;클러스터 확장&amp;quot;보다는 &amp;quot;터널로 이어붙인 원격 연산 노드&amp;quot;에 가깝다. 스토리지, LoadBalancer, 레지스트리 신뢰 관계는 전부 홈 인프라 쪽에 남고, NCP 노드는 taint로 격리된 worker node로 사용하려고한다. &lt;del&gt;이걸로 로컬 LLM agent 돌려야지&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;다음에 노드를 더 늘린다면 Terraform 모듈에 서버 개수만 늘리면 된다.&lt;/p&gt;</description>
      <category>Infra</category>
      <author>꼬꼬팜</author>
      <guid isPermaLink="true">https://cocopam.tistory.com/59</guid>
      <comments>https://cocopam.tistory.com/59#entry59comment</comments>
      <pubDate>Tue, 14 Jul 2026 01:04:15 +0900</pubDate>
    </item>
    <item>
      <title>eBPF &amp;amp; Katran (SW L4) 리뷰</title>
      <link>https://cocopam.tistory.com/58</link>
      <description>&lt;p&gt;이전 포스트에서 정리한 XDP, DSR/IPIP, RSS, Session Table, Maglev가 실제로 어떻게 구현되는지 &lt;strong&gt;Katran&lt;/strong&gt; 코드를 까보면서 확인해보자.&lt;/p&gt;
&lt;h2&gt;Katran Overview&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TB
    subgraph CP[&amp;quot;Control Plane (user space)&amp;quot;]
        direction LR
        GRPC[&amp;quot;`**gRPC server**
        VIP/real 추가·삭제`&amp;quot;] --&amp;gt; LIB[&amp;quot;`**KatranLb (C++)**
        설정 → map 반영
        Maglev ring 계산`&amp;quot;]
    end
    subgraph DP[&amp;quot;Data Plane (kernel, XDP)&amp;quot;]
        BAL[&amp;quot;`**balancer_kern.c**
        패킷당 실행`&amp;quot;]
    end
    MAPS[(&amp;quot;`**BPF Maps**
    vip / ch_rings / reals
    lru / stats`&amp;quot;)]

    LIB --&amp;gt;|&amp;quot;bpf syscall로 write&amp;quot;| MAPS
    MAPS --&amp;gt;|&amp;quot;패킷 처리 중 lookup&amp;quot;| BAL

    style LIB fill:#d1ecf1,stroke:#17a2b8,color:#000
    style BAL fill:#fff3cd,stroke:#ffc107,color:#000
    style MAPS fill:#d4edda,stroke:#28a745,color:#000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Katran은 크게 두 컴포넌트로 이루어진다.&lt;/p&gt;
&lt;h3&gt;Data Plane&lt;/h3&gt;
&lt;p&gt;메인 로드밸런싱 로직을 처리하는 부분이다.&lt;/p&gt;
&lt;p&gt;NIC 드라이버의 XDP hook에 붙어서, 패킷이 커널 네트워크 스택에 올라가기 전에 곧바로 NIC으로 TX 해버린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/txNYw/dJMcabY9LU2/0WAH964i8gW7b8InsxrmY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/txNYw/dJMcabY9LU2/0WAH964i8gW7b8InsxrmY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/txNYw/dJMcabY9LU2/0WAH964i8gW7b8InsxrmY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtxNYw%2FdJMcabY9LU2%2F0WAH964i8gW7b8InsxrmY0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;BPF Map을 확인해 로드밸런싱이 필요한 패킷만 IPIP encap 하고, 나머지는 커널 스택으로 올려보낸다.&lt;/p&gt;
&lt;h3&gt;Control Plane&lt;/h3&gt;
&lt;p&gt;user space 애플리케이션으로, backend 추가/삭제 같은 설정 변경을 담당한다.&lt;/p&gt;
&lt;p&gt;설정이 바뀌면 BPF map을 업데이트해서, data plane이 로드밸런싱할 backend 목록을 바꾼다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;둘은 &lt;strong&gt;BPF Map을 사이에 두고만 만난다&lt;/strong&gt;. control plane은 map을 쓰고, data plane은 map을 읽는다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;BPF Maps??&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;eBPF는 커널 안에서 코드를 실행할 수 있게 해주고, 커널과 user space 양쪽에서 접근 가능한 자료구조인 Map을 제공한다. Katran이 쓰는 map type들은 아래 섹션에서 정리한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Control Plane&lt;/th&gt;
&lt;th&gt;Data Plane&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;위치&lt;/td&gt;
&lt;td&gt;user space (C++)&lt;/td&gt;
&lt;td&gt;kernel (XDP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;언어&lt;/td&gt;
&lt;td&gt;C++ / libbpf&lt;/td&gt;
&lt;td&gt;C → BPF bytecode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빈도&lt;/td&gt;
&lt;td&gt;설정 변경 시&lt;/td&gt;
&lt;td&gt;패킷마다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;td&gt;map 작성, ring 계산, 통계 수집&lt;/td&gt;
&lt;td&gt;lookup, 선택, encap, 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상태&lt;/td&gt;
&lt;td&gt;갖지 않음(전부 map)&lt;/td&gt;
&lt;td&gt;갖지 않음(전부 map)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;무중단 XDP hook 부착&lt;/h2&gt;
&lt;h2&gt;XDP hook&lt;/h2&gt;
&lt;p&gt;진입점은 NIC 드라이버에 attach된 XDP 프로그램 하나다. 반환값으로 패킷의 운명이 정해진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    NIC([&amp;quot;`NIC 수신`&amp;quot;]) --&amp;gt; PROG[&amp;quot;`**xdp balancer**
    balancer_kern.c`&amp;quot;]
    PROG --&amp;gt;|&amp;quot;VIP 아님&amp;quot;| PASS[&amp;quot;`XDP_PASS
    커널 스택으로`&amp;quot;]
    PROG --&amp;gt;|&amp;quot;encap 완료&amp;quot;| TX[&amp;quot;`XDP_TX
    backend로 되돌려 전송`&amp;quot;]
    PROG --&amp;gt;|&amp;quot;비정상&amp;quot;| DROP[&amp;quot;`XDP_DROP`&amp;quot;]

    style PROG fill:#fff3cd,stroke:#ffc107,color:#000
    style TX fill:#d4edda,stroke:#28a745,color:#000
    style PASS fill:#d1ecf1,stroke:#17a2b8,color:#000
    style DROP fill:#f8d7da,stroke:#dc3545,color:#000&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;DSR이므로 응답 경로는 없다. LB가 하는 건 요청을 encap해서 &lt;code&gt;XDP_TX&lt;/code&gt;로 backend에 쏘는 것까지다. 응답은 Real Server가 Client로 직접 보낸다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;Dataplane 패킷 처리 flow&lt;/h2&gt;
&lt;p&gt;배경 1편의 Session Table → Maglev 흐름이 여기서 그대로 코드가 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    START[&amp;quot;`패킷 수신`&amp;quot;] --&amp;gt; PARSE[&amp;quot;`L2/L3/L4 파싱
    IPv4 / IPv6`&amp;quot;]
    PARSE --&amp;gt; VIP{&amp;quot;`dst가 VIP?
    (vip_map lookup)`&amp;quot;}
    VIP --&amp;gt;|no| PASS[&amp;quot;`XDP_PASS`&amp;quot;]
    VIP --&amp;gt;|yes| KEY[&amp;quot;`flow key 생성
    (5-tuple)`&amp;quot;]
    KEY --&amp;gt; LRU{&amp;quot;`Session Table
    lookup (lru)`&amp;quot;}
    LRU --&amp;gt;|hit| REAL[&amp;quot;`기존 real 재사용`&amp;quot;]
    LRU --&amp;gt;|miss| HASH[&amp;quot;`Maglev hash
    → ch_rings 인덱싱`&amp;quot;]
    HASH --&amp;gt; PICK[&amp;quot;`real 선택`&amp;quot;]
    PICK --&amp;gt; STORE[&amp;quot;`lru에 기록`&amp;quot;]
    STORE --&amp;gt; REAL
    REAL --&amp;gt; RLOOK[&amp;quot;`reals lookup
    → real IP`&amp;quot;]
    RLOOK --&amp;gt; ENCAP[&amp;quot;`IPIP/GUE encap
    outer dst = real IP`&amp;quot;]
    ENCAP --&amp;gt; STAT[&amp;quot;`stats / reals_stats
    카운터 증가`&amp;quot;]
    STAT --&amp;gt; TX[&amp;quot;`XDP_TX`&amp;quot;]

    style VIP fill:#fff3cd,stroke:#ffc107,color:#000
    style LRU fill:#fff3cd,stroke:#ffc107,color:#000
    style HASH fill:#d1ecf1,stroke:#17a2b8,color:#000
    style ENCAP fill:#d4edda,stroke:#28a745,color:#000
    style TX fill:#d4edda,stroke:#28a745,color:#000&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;parse → VIP? → 연결 있으면 재사용 / 없으면 Maglev로 선택 → encap → XDP_TX&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;정상 트래픽의 절대다수는 &lt;strong&gt;lru hit&lt;/strong&gt; 경로로 빠진다. Maglev 계산은 새 연결(또는 lru miss)일 때만 탄다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Session Table이 왜 필요할까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;운영 중인 LB에서는 backend 목록이 수시로 바뀐다. (health check fail, scale out 등)&lt;/p&gt;
&lt;p&gt;backend가 바뀌어도 기존 연결의 패킷은 원래 가던 backend로 계속 가야 한다. (TCP처럼 stateful한 통신일수록 더욱)&lt;/p&gt;
&lt;p&gt;물론 뒷단의 Maglev 해시도 5-tuple 기반이라 웬만하면 같은 backend가 나오지만, ring이 다시 계산되면 일부 flow는 다른 backend로 옮겨간다. 그래서 앞단에서 session table을 먼저 확인해 더 확실하게 sticky한 session을 보장한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;eBPF map 종류&lt;/h2&gt;
&lt;p&gt;Katran의 map은 크게 &lt;strong&gt;포워딩용&lt;/strong&gt;과 &lt;strong&gt;통계용&lt;/strong&gt;으로 나뉜다. (실제 &lt;code&gt;balancer_maps.h&lt;/code&gt; 기준)&lt;/p&gt;
&lt;h3&gt;포워딩 (lookup으로 backend를 찾는다)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Map&lt;/th&gt;
&lt;th&gt;타입&lt;/th&gt;
&lt;th&gt;저장 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vip_map&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HASH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VIP → vip 메타(번호, flag)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ch_rings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ARRAY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Maglev ring&lt;/strong&gt;: (vip, hash) → real index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reals&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ARRAY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;real index → real 서버 IP/flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lru_mapping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ARRAY_OF_MAPS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CPU별 Session Table&lt;/strong&gt;(아래에서 자세히)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fallback_cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LRU_HASH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per-CPU lru가 빗나갈 때의 공용 백업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ctl_array&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ARRAY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;mac 등 제어값&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;통계 (전부 &lt;code&gt;PERCPU_ARRAY&lt;/code&gt;)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Map&lt;/th&gt;
&lt;th&gt;타입&lt;/th&gt;
&lt;th&gt;저장 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PERCPU_ARRAY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VIP 단위 패킷/바이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reals_stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PERCPU_ARRAY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;real 단위 패킷/바이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lru_miss_stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PERCPU_ARRAY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lru miss 카운트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;통계가 전부 &lt;code&gt;PERCPU_ARRAY&lt;/code&gt;인 이유&lt;/strong&gt;: CPU마다 자기 복사본에 락 없이 더하기만 하면 된다. 합산은 user space(control plane)가 읽을 때 한 번에 한다. 패킷당 atomic/lock이 없다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    CPU0[&amp;quot;`CPU0 +1`&amp;quot;] --&amp;gt; S0[&amp;quot;`slot[cpu0]`&amp;quot;]
    CPU1[&amp;quot;`CPU1 +1`&amp;quot;] --&amp;gt; S1[&amp;quot;`slot[cpu1]`&amp;quot;]
    CPUN[&amp;quot;`CPUn +1`&amp;quot;] --&amp;gt; SN[&amp;quot;`slot[cpun]`&amp;quot;]
    S0 &amp;amp; S1 &amp;amp; SN --&amp;gt;|&amp;quot;읽을 때 sum&amp;quot;| USER[&amp;quot;`control plane`&amp;quot;]

    style USER fill:#d1ecf1,stroke:#17a2b8,color:#000&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;PER-CPU session table&lt;/h2&gt;
&lt;p&gt;여기가 Katran 설계에서 제일 재밌는 부분이다.&lt;/p&gt;
&lt;p&gt;연결 추적 테이블은 &lt;strong&gt;CPU마다 따로&lt;/strong&gt; 둔다. 그런데 이름만 보면 &lt;code&gt;LRU_PERCPU_HASH&lt;/code&gt;를 쓸 것 같은데, 실제로는 &lt;strong&gt;&lt;code&gt;ARRAY_OF_MAPS&lt;/code&gt; 안에 CPU별 &lt;code&gt;LRU_HASH&lt;/code&gt;를 하나씩&lt;/strong&gt; 넣는 구조다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    PROG[&amp;quot;`XDP 프로그램`&amp;quot;] --&amp;gt; CPUID[&amp;quot;`bpf_get_smp_processor_id()`&amp;quot;]
    CPUID --&amp;gt; OUTER[&amp;quot;`**lru_mapping**
    (ARRAY_OF_MAPS)
    key = cpu id`&amp;quot;]
    OUTER --&amp;gt;|cpu0| L0[&amp;quot;`LRU_HASH (CPU0 전용)`&amp;quot;]
    OUTER --&amp;gt;|cpu1| L1[&amp;quot;`LRU_HASH (CPU1 전용)`&amp;quot;]
    OUTER --&amp;gt;|cpuN| LN[&amp;quot;`LRU_HASH (CPUn 전용)`&amp;quot;]

    style OUTER fill:#fff3cd,stroke:#ffc107,color:#000
    style L0 fill:#d4edda,stroke:#28a745,color:#000
    style L1 fill:#d4edda,stroke:#28a745,color:#000
    style LN fill:#d4edda,stroke:#28a745,color:#000&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;cpu = bpf_get_smp_processor_id();
lru = lookup(lru_mapping, cpu);   // 이 CPU 전용 LRU_HASH
real = lookup(lru, flow_key);     // 그 안에서 연결 조회&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;왜 CPU별로 쪼개나&lt;/h3&gt;
&lt;p&gt;이전 포스트에서 설명한 &lt;strong&gt;RSS&lt;/strong&gt; 때문이다.&lt;/p&gt;
&lt;p&gt;NIC이 5-tuple을 해시해 연결을 CPU에 고정하므로, &lt;strong&gt;한 연결의 패킷은 항상 같은 CPU = 같은 inner LRU&lt;/strong&gt;로 온다. 그래서 CPU끼리 테이블을 공유할 필요가 없다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    F[&amp;quot;`연결 A 패킷들`&amp;quot;] --&amp;gt;|RSS| CPU[&amp;quot;`항상 CPU k`&amp;quot;]
    CPU --&amp;gt; LRUk[&amp;quot;`CPU k의 LRU만 접근
    → 락 경합 0`&amp;quot;]

    style CPU fill:#fff3cd,stroke:#ffc107,color:#000
    style LRUk fill:#d4edda,stroke:#28a745,color:#000&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;왜 &lt;code&gt;LRU_PERCPU_HASH&lt;/code&gt;가 아닌가&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구조&lt;/th&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;th&gt;문제/이점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LRU_PERCPU_HASH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모든 CPU 공유&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CPU마다 별도&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;한 연결의 value가 CPU 수만큼 복제 → 메모리 ×N, 의미상 불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ARRAY_OF_MAPS&lt;/code&gt; + &lt;code&gt;LRU_HASH&lt;/code&gt; (Katran)&lt;/td&gt;
&lt;td&gt;바깥에서 CPU별 분리&lt;/td&gt;
&lt;td&gt;단일 값&lt;/td&gt;
&lt;td&gt;이미 CPU 전용이라 value 복제 불필요, &amp;quot;한 연결 → 한 real&amp;quot;이 그대로 단일 값&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;바깥(&lt;code&gt;ARRAY_OF_MAPS&lt;/code&gt;)에서 이미 &lt;strong&gt;CPU별로 인스턴스가 분리&lt;/strong&gt;돼 있다 → 안쪽까지 per-CPU일 이유가 없다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LRU_PERCPU_HASH&lt;/code&gt;라면 key는 공유하면서 value를 CPU 수만큼 들고 있어, 정작 그 연결을 만지는 CPU는 하나인데 &lt;strong&gt;메모리만 N배&lt;/strong&gt; 쓴다.&lt;/li&gt;
&lt;li&gt;우리가 원하는 값은 &amp;quot;이 연결이 가야 할 real &lt;strong&gt;하나&lt;/strong&gt;&amp;quot;다. 일반 &lt;code&gt;LRU_HASH&lt;/code&gt;의 단일 value가 정확히 그 의미다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;그래도 빗나가면? fallback_cache&lt;/h3&gt;
&lt;p&gt;RSS 가정이 항상 성립하는 건 아니다. 큐 재해시, CPU 수 초과, &lt;code&gt;XDP_REDIRECT&lt;/code&gt;로 CPU가 바뀌는 경우가 있다. 그래서 &lt;strong&gt;공용 &lt;code&gt;fallback_cache (LRU_HASH)&lt;/code&gt;&lt;/strong&gt; 를 하나 더 둔다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    P[&amp;quot;`패킷`&amp;quot;] --&amp;gt; A{&amp;quot;`CPU 전용 lru
    hit?`&amp;quot;}
    A --&amp;gt;|yes| OK[&amp;quot;`그대로 사용`&amp;quot;]
    A --&amp;gt;|no| B{&amp;quot;`fallback_cache
    hit?`&amp;quot;}
    B --&amp;gt;|yes| OK
    B --&amp;gt;|no| C[&amp;quot;`Maglev로 선택
    양쪽에 기록`&amp;quot;]

    style A fill:#fff3cd,stroke:#ffc107,color:#000
    style B fill:#d1ecf1,stroke:#17a2b8,color:#000
    style C fill:#d4edda,stroke:#28a745,color:#000&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;per-CPU 테이블은 &lt;strong&gt;속도(락 없음)&lt;/strong&gt; 를 위해, fallback은 &lt;strong&gt;정확성(연결이 다른 CPU로 가도 유지)&lt;/strong&gt; 을 위해 둔다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;Maglev 해시&lt;/h2&gt;
&lt;p&gt;lru miss일 때 타는 경로다. ring 자체는 &lt;strong&gt;control plane(C++)이 미리 계산&lt;/strong&gt;해서 &lt;code&gt;ch_rings&lt;/code&gt;에 써두고, data plane은 패킷당 &lt;strong&gt;배열 인덱싱 한 번&lt;/strong&gt;만 한다.&lt;/p&gt;
&lt;h3&gt;ring 생성 (control plane)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MaglevHash::generateHashRing&lt;/code&gt;(&lt;code&gt;MaglevHash.cpp&lt;/code&gt;)이 만든다. real마다 MurmurHash3로 &lt;code&gt;offset&lt;/code&gt;/&lt;code&gt;skip&lt;/code&gt;을 뽑아 &lt;strong&gt;자기만의 slot 선호 순서&lt;/strong&gt;를 만들고, 라운드로빈으로 빈 slot을 하나씩 가져간다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// MaglevBase.cpp — real마다 고유한 순회 순서
offset = MurmurHash3(real.hash, seed_a) % ring_size;
skip   = MurmurHash3(real.hash, seed_b) % (ring_size - 1) + 1;
// i번째 선호 slot = (offset + i * skip) % ring_size&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ring_size 7로 줄인 예시 (실제 기본값은 &lt;code&gt;RING_SIZE 65537&lt;/code&gt;, 소수):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;B0: offset=3, skip=4  →  선호 순서: 3, 0, 4, 1, 5, 2, 6
B1: offset=0, skip=2  →  선호 순서: 0, 2, 4, 6, 1, 3, 5
B2: offset=3, skip=1  →  선호 순서: 3, 4, 5, 6, 0, 1, 2

라운드마다 각자 &amp;quot;선호 순서상 아직 빈 slot&amp;quot;을 하나씩 가져간다.

round 1:  B0→3    B1→0    B2→(3 점유라 패스)→4
round 2:  B0→(0,4 점유)→1    B1→2    B2→5
round 3:  B0→(5,2 점유)→6    ... 7칸 다 참 → 종료

slot   :  0   1   2   3   4   5   6
backend:  B1  B0  B1  B0  B2  B2  B0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 B2가 죽어서 빠지면 (남은 B0, B1로 다시 fill):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;slot     :  0   1   2   3   4   5   6
B2 제거 전:  B1  B0  B1  B0  B2  B2  B0
B2 제거 후:  B1  B0  B1  B0  B0  B0  B1
            =   =   =   =   x   x   x

= 유지   x 변경 (B2 몫 2칸 + 덤으로 1칸)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;real이 빠져도 남은 real들의 &lt;code&gt;offset&lt;/code&gt;/&lt;code&gt;skip&lt;/code&gt;은 그대로라 선호 순서가 안 바뀐다. 그래서 빠진 real 몫 위주로만 재배치되고 나머지는 거의 그대로다. 기본값인 65537칸 ring에서는 엉뚱하게 옮겨가는 slot이 대략 1% 수준이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;원 논문과 달리 Katran은 &lt;strong&gt;weight&lt;/strong&gt;도 지원한다. 첫 라운드에서 weight 수만큼 slot을 연달아 가져가는 방식이다(&lt;code&gt;generateHashRing&lt;/code&gt;의 inner loop).&lt;/p&gt;
&lt;h3&gt;ring 조회 (data plane)&lt;/h3&gt;
&lt;p&gt;계산된 ring은 &lt;code&gt;programHashRing&lt;/code&gt;(&lt;code&gt;KatranLb.cpp&lt;/code&gt;)이 &lt;code&gt;key = vip_num * ring_size + pos&lt;/code&gt; 형태로 batch write한다. data plane 쪽은:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// balancer.bpf.c — get_packet_dst()
hash = jhash(src ip, src/dst ports) % RING_SIZE;
key  = RING_SIZE * vip_num + hash;

real_id = *bpf_map_lookup_elem(&amp;amp;ch_rings, &amp;amp;key);  // slot → real id
real    =  bpf_map_lookup_elem(&amp;amp;reals, &amp;amp;real_id); // real id → IP&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;해시 입력에 &lt;strong&gt;dst(VIP)가 없다&lt;/strong&gt;. VIP 구분은 &lt;code&gt;vip_num * RING_SIZE&lt;/code&gt; offset이 대신한다. 즉 &lt;code&gt;ch_rings&lt;/code&gt;는 모든 VIP의 ring을 이어붙인 1차원 &lt;code&gt;ARRAY&lt;/code&gt; 하나다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;IPIP encap&lt;/h2&gt;
&lt;p&gt;real이 정해졌으면 원본 패킷은 건드리지 않고 &lt;strong&gt;앞에 outer IP 헤더만 덧붙여서&lt;/strong&gt; real로 쏜다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;before (client → VIP):
  [ ETH ][ IP src=client, dst=VIP ][ TCP ][ payload ]

after encap (LB → real):
  [ ETH* ][ outer IP src=172.16.x.y, dst=real, proto=IPIP ][ IP src=client, dst=VIP ][ TCP ][ payload ]
            ^ 새로 붙인 20 byte                               ^ 원본 그대로

  ETH*: dst = 다음 홉(라우터) mac (ctl_array에서), src = LB 자신의 mac&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;encap_v4&lt;/code&gt;(&lt;code&gt;pckt_encap.h&lt;/code&gt;)에서 하는 일은 사실 이게 전부다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;bpf_xdp_adjust_head(xdp, -sizeof(struct iphdr));   // 패킷 앞에 20 byte 확보
memcpy(new_eth-&amp;gt;h_dest, cval-&amp;gt;mac, 6);             // 다음 홉 mac
memcpy(new_eth-&amp;gt;h_source, old_eth-&amp;gt;h_dest, 6);     // 원래 받던 mac = LB 자신

ip_src = create_encap_ipv4_src(sport, client_ip);  // outer src 생성 (아래)
create_v4_hdr(iph, tos, ip_src, real-&amp;gt;dst, len, IPPROTO_IPIP);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;outer src가 LB IP가 아니다?&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// encap_helpers.h — RFC1918 172.16/16 + flow에서 유도한 하위 16bit
__u32 ip_suffix = bpf_htons(port) &amp;lt;&amp;lt; 16 ^ src;
return (0xFFFF0000 &amp;amp; ip_suffix) | IPIP_V4_PREFIX;  // 172.16.x.y&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;outer src를 LB IP 하나로 고정하면, real server 입장에서 모든 tunneled 패킷이 같은 (src, dst) 쌍이 된다. IPIP에는 port가 없어서 real의 NIC RSS가 IP로만 해시하는데, 그러면 &lt;strong&gt;전부 한 CPU로 몰린다&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    LB[&amp;quot;`**LB**
    outer src = 172.16.x.y
    (flow마다 다름)`&amp;quot;] --&amp;gt; NIC[&amp;quot;`real의 NIC
    outer 헤더로 RSS`&amp;quot;]
    NIC --&amp;gt; C0[&amp;quot;`CPU0`&amp;quot;]
    NIC --&amp;gt; C1[&amp;quot;`CPU1`&amp;quot;]
    NIC --&amp;gt; CN[&amp;quot;`CPUn`&amp;quot;]

    style LB fill:#fff3cd,stroke:#ffc107,color:#000
    style NIC fill:#6c757d,stroke:#495057,color:#fff&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;client의 (ip, port)를 outer src에 XOR로 심어서, &lt;strong&gt;real server에서도 RSS가 퍼지게&lt;/strong&gt; 만든다. LB에서 통했던 &amp;quot;flow당 CPU 고정&amp;quot;이 real에서도 그대로 성립한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;IPv6도 같은 방식으로 RFC 6666 discard prefix(&lt;code&gt;0100::/64&lt;/code&gt;)에 flow 정보를 심는다. UDP 기반 &lt;strong&gt;GUE encap&lt;/strong&gt;도 옵션(&lt;code&gt;GUE_ENCAP&lt;/code&gt;)으로 지원하는데, 이때는 outer UDP source port가 같은 역할을 한다.&lt;/p&gt;
&lt;p&gt;real server에서는 ipip tunnel device가 outer 헤더를 벗겨내면 dst=VIP인 원본 패킷이 나온다 (VIP는 real의 loopback에 붙어 있다). 응답은 real이 src=VIP로 client에 직접 보낸다. 이게 앞에서 말한 DSR이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    C[&amp;quot;`client`&amp;quot;] --&amp;gt;|&amp;quot;dst=VIP&amp;quot;| LB[&amp;quot;`**katran**
    encap + XDP_TX`&amp;quot;]
    LB --&amp;gt;|&amp;quot;outer dst=real (IPIP)&amp;quot;| R[&amp;quot;`**real server**
    decap 후 처리`&amp;quot;]
    R --&amp;gt;|&amp;quot;src=VIP로 직접 응답&amp;quot;| C

    style LB fill:#fff3cd,stroke:#ffc107,color:#000
    style R fill:#d4edda,stroke:#28a745,color:#000&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/facebookincubator/katran&quot;&gt;facebookincubator/katran&lt;/a&gt; — 소스&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://engineering.fb.com/2018/05/22/open-source/open-sourcing-katran-a-scalable-network-load-balancer/&quot;&gt;Open-sourcing Katran (Meta Engineering)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/maps.html&quot;&gt;The Linux Kernel - BPF map types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://research.google/pubs/maglev-a-fast-and-reliable-software-network-load-balancer/&quot;&gt;Maglev: A Fast and Reliable Software Network Load Balancer (Google, NSDI &amp;#39;16)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>꼬꼬팜</author>
      <guid isPermaLink="true">https://cocopam.tistory.com/58</guid>
      <comments>https://cocopam.tistory.com/58#entry58comment</comments>
      <pubDate>Sat, 4 Jul 2026 15:38:50 +0900</pubDate>
    </item>
    <item>
      <title>SW L4를 위한 배경</title>
      <link>https://cocopam.tistory.com/57</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;L4 Load Balancer를 소프트웨어로 구현하려면 필요한 부분을 정리해보자&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;L7 vs L4 LB&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 차이는 &lt;b&gt;연결을 끊는가, 패킷을 흘려보내는가&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;1050&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bixq66/dJMcagTAhPe/gA9vPvOT10Nna4h818lho1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bixq66/dJMcagTAhPe/gA9vPvOT10Nna4h818lho1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bixq66/dJMcagTAhPe/gA9vPvOT10Nna4h818lho1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbixq66%2FdJMcagTAhPe%2FgA9vPvOT10Nna4h818lho1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;517&quot; height=&quot;1050&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;1050&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 84.5349%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 16.3953%;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 35.7288%;&quot;&gt;L7 LB&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 33.4281%;&quot;&gt;L4 LB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 16.3953%;&quot;&gt;보는 정보&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 35.7288%;&quot;&gt;HTTP header, path, cookie&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 33.4281%;&quot;&gt;IP/Port (5-tuple)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 16.3953%;&quot;&gt;연결&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 35.7288%;&quot;&gt;Client&amp;harr;LB, LB&amp;harr;Backend &lt;b&gt;2개&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 33.4281%;&quot;&gt;Client&amp;harr;Backend &lt;b&gt;1개&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 16.3953%;&quot;&gt;할 수 있는 일&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 35.7288%;&quot;&gt;라우팅, TLS offload, WAF, header rewrite&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 33.4281%;&quot;&gt;분산, 그대로 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 16.3953%;&quot;&gt;비용&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 35.7288%;&quot;&gt;패킷마다 user space overhead&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 33.4281%;&quot;&gt;커널/NIC 레벨에서 처리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 L7(ALB)로 다 처리하면 되는거 아닌가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAS 와 같은 client -&amp;gt; Backend의 분산을 위해선 해당 아키텍처가 맞을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS offload를 결국 처리해주는 지점 + WAF를 붙이기엔 운영상 이점과 MSA구조에선 장점들이 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 HTTPS(or gRPC) 외 통신의 경우엔 어떨까&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DNS anycast (UDP)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;k8s cluster service LB&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우는 프로토콜의 특성과, 성능상의 이점을 위해 L4를 사용할 수 밖에 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HW L4 vs SW L4&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적으로 L4는 전용 어플라이언스(F5, Citrix 등)의 몫이었다. SW L4는 &lt;b&gt;commodity 서버 + 커널 기술&lt;/b&gt;로 같은 일을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 66.3952%; height: 179px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;width: 26.5822%;&quot;&gt;HW L4&lt;/td&gt;
&lt;td style=&quot;width: 32.9137%;&quot;&gt;SW L4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;확장&lt;/td&gt;
&lt;td style=&quot;width: 26.5822%;&quot;&gt;scale-up (장비 교체)&lt;/td&gt;
&lt;td style=&quot;width: 32.9137%;&quot;&gt;scale-out (서버 추가 + ECMP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;비용&lt;/td&gt;
&lt;td style=&quot;width: 26.5822%;&quot;&gt;고가 어플라이언스&lt;/td&gt;
&lt;td style=&quot;width: 32.9137%;&quot;&gt;commodity 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;유연성&lt;/td&gt;
&lt;td style=&quot;width: 26.5822%;&quot;&gt;벤더 펌웨어에 종속&lt;/td&gt;
&lt;td style=&quot;width: 32.9137%;&quot;&gt;코드로 로직 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;한계&lt;/td&gt;
&lt;td style=&quot;width: 26.5822%;&quot;&gt;용량이 박스에 고정&lt;/td&gt;
&lt;td style=&quot;width: 32.9137%;&quot;&gt;NIC/CPU/커널 성능에 의존&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런저런 이유가 있겠지만 &lt;b&gt;가장 큰 문제는 HW L4 비용&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; HW L4의 어플라이언스 (장비 + 라이센스) 값은 굉장히 비싸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게다가 거의 독점 HW L4 인 citrix에서 라이센스를 구독제로 변경하고 및 가격 인상하는 걸 보면, HW l4를 유지하는것이 크나큰 부담이 되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wtit.com/blog/2023/12/11/citrix-netscaler-f5-big-ip-history-migration-features/&quot;&gt;https://wtit.com/blog/2023/12/11/citrix-netscaler-f5-big-ip-history-migration-features/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 운영상의 부담 및 벤더 지원을 받을 수 없다는 단점에도 불구하고 SW L4의 도입을 고려할 수 밖에 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;L3 DSR &amp;amp; IPIP&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L4도 응답까지 다 받으면 병목이 된다. 그래서 응답은 LB를 우회하는 &lt;b&gt;DSR&lt;/b&gt;을 쓴다. 다른 방식의 NAT 방식도 있지만 성능상의 이점을 위해 프로덕션에서 DSR을 많이 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsLLOC/dJMcajvZWDb/4P4OwkbLH0Z5Dk8pVGku20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsLLOC/dJMcajvZWDb/4P4OwkbLH0Z5Dk8pVGku20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsLLOC/dJMcajvZWDb/4P4OwkbLH0Z5Dk8pVGku20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsLLOC%2FdJMcajvZWDb%2F4P4OwkbLH0Z5Dk8pVGku20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;459&quot; height=&quot;900&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 패킷은 원본을 그대로 둔 채 outer IP header만 한 겹 덧씌운다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Before: [IP: Client &amp;rarr; VIP][payload]
After:  [Outer IP: LB &amp;rarr; RIP][IP: Client &amp;rarr; VIP][payload]&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LB는 요청 방향에서만 backend를 고르고, 응답은 Real Server가 Client로 직접 보낸다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 실제 운영에선 Real Server에서 IPIP decap 기능 및 lo 인터페이스의 VIP 주소 등록이 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;eBPF&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SW L4를 user space proxy로 만들면 패킷마다 커널&amp;harr;유저 왕복이 생긴다. eBPF는 그 로직을 &lt;b&gt;커널 안에서&lt;/b&gt; 돌린다. verifier&amp;middot;JIT 같은 내부 동작은 이 글의 범위를 넘으니, 여기서는 핵심만 짚는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기억할 두 가지.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BPF Map&lt;/b&gt;: 커널-유저가 공유하는 key-value 저장소. 상태(VIP, backend, 연결)는 전부 여기 둔다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;O(1) lookup&lt;/b&gt;: iptables의 chain 순회(O(n))와 달리, map lookup으로 backend를 바로 찾는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;XDP hook&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XDP는 패킷이 &lt;b&gt;커널 네트워크 스택에 올라오기 전, NIC 드라이버 레벨&lt;/b&gt;에서 실행된다. L4 LB가 패킷을 만질 수 있는 가장 빠른 지점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biGY0k/dJMcadJdwQg/yEG941lpvhFlN0nI9mlkfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biGY0k/dJMcadJdwQg/yEG941lpvhFlN0nI9mlkfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biGY0k/dJMcadJdwQg/yEG941lpvhFlN0nI9mlkfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiGY0k%2FdJMcadJdwQg%2FyEG941lpvhFlN0nI9mlkfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1378&quot; height=&quot;636&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6279%; height: 93px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 28.2558%;&quot;&gt;Action&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 24.6512%;&quot;&gt;의미&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 28.6046%;&quot;&gt;L4 LB에서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 28.2558%;&quot;&gt;XDP_PASS&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 24.6512%;&quot;&gt;커널 스택으로&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 28.6046%;&quot;&gt;VIP가 아닌 패킷&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 28.2558%;&quot;&gt;XDP_DROP&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 24.6512%;&quot;&gt;즉시 폐기&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 28.6046%;&quot;&gt;DDoS, 비정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 28.2558%;&quot;&gt;XDP_TX&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 24.6512%;&quot;&gt;받은 NIC로 되돌려 전송&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 28.6046%;&quot;&gt;&lt;b&gt;encap 후 backend로&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 28.2558%;&quot;&gt;XDP_REDIRECT&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 24.6512%;&quot;&gt;다른 NIC/CPU로&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 28.6046%;&quot;&gt;멀티 NIC 구성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L4 LB의 빠른 경로는 encap &amp;rarr; XDP_TX다. 커널에 올리지 않고 드라이버에서 backend로 쏜다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RSS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XDP는 CPU별로 병렬 실행된다. 그럼 &lt;b&gt;같은 연결의 패킷이 매번 다른 CPU로 가면&lt;/b&gt; 연결 상태 공유가 골치 아파진다. 이걸 막아주는 게 RSS다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RSS(Receive Side Scaling)&lt;/b&gt; = NIC이 패킷의 5-tuple을 해시해서 &lt;b&gt;RX queue(=CPU)로 분배&lt;/b&gt;한다. 같은 연결은 항상 같은 CPU로 간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyNLKc/dJMcadP0ykM/8SYJjxxpSMH3nKSB1fpCv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyNLKc/dJMcadP0ykM/8SYJjxxpSMH3nKSB1fpCv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyNLKc/dJMcadP0ykM/8SYJjxxpSMH3nKSB1fpCv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyNLKc%2FdJMcadP0ykM%2F8SYJjxxpSMH3nKSB1fpCv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;670&quot; height=&quot;430&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIIbhu/dJMcaccysLu/q3y19EnGBuQBfJKMJcyN1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIIbhu/dJMcaccysLu/q3y19EnGBuQBfJKMJcyN1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIIbhu/dJMcaccysLu/q3y19EnGBuQBfJKMJcyN1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIIbhu%2FdJMcaccysLu%2Fq3y19EnGBuQBfJKMJcyN1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;667&quot; height=&quot;468&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 client(src IP, src Port) -&amp;gt; 같은 CPU가 처리한다. 이후 katran lru_mapping에서 자세히 설명한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Session Table&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L4 LB는 패킷마다 backend를 새로 고르면 안 된다. &lt;b&gt;한 연결은 끝까지 같은 backend&lt;/b&gt;여야 한다. backend 집합이 바뀌어도 기존 연결은 유지되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;1376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRWWnY/dJMcahSp413/T14MlOsKrOFJSKfbP32JD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRWWnY/dJMcahSp413/T14MlOsKrOFJSKfbP32JD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRWWnY/dJMcahSp413/T14MlOsKrOFJSKfbP32JD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRWWnY%2FdJMcahSp413%2FT14MlOsKrOFJSKfbP32JD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;509&quot; height=&quot;633&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;1376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session Table은 보통 LRU 해시다. 연결은 무한히 쌓이므로 오래된 항목을 자동으로 밀어낸다. RSS로 연결이 CPU에 고정되니 이 테이블도 CPU별로 둘 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Maglev hash&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session Table이 miss일 때 backend를 고르는 알고리즘. 단순 modulo 해시는 &lt;b&gt;backend 한 대만 바뀌어도 대부분의 연결이 재배치&lt;/b&gt;되는 문제가 있다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;hash(5-tuple) % N   &amp;rarr;   N이 바뀌면 거의 전부 재매핑 (연결 끊김)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Maglev&lt;/b&gt;는 backend마다 순열을 만들어 크기 M(소수)의 &lt;b&gt;lookup table(ring)&lt;/b&gt; 을 채운다. 패킷은 &lt;code&gt;hash % M&lt;/code&gt;으로 ring을 한 번 인덱싱하면 backend가 나온다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시: backend 한 대가 빠지면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;backend &lt;b&gt;C&lt;/b&gt;가 빠진다고 하자. (&lt;code&gt;A, B, C&lt;/code&gt; &amp;rarr; &lt;code&gt;A, B&lt;/code&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;modulo 방식&lt;/b&gt; &amp;mdash; 나누는 수가 &lt;code&gt;3 &amp;rarr; 2&lt;/code&gt;로 바뀌니 C와 무관한 연결까지 자리가 밀린다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;backend:  A B C  &amp;rarr;  A B          (C 제거,  A=0 B=1 C=2)

hash      0    1    2    3    4    5
%3        A    B    C    A    B    C
%2        A    B    A    B    A    B
          =    =    C    x    x    C
                         └────┴── A&amp;middot;B로 잘 가던 hash 3,4까지 끊긴다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Maglev 방식&lt;/b&gt; &amp;mdash; ring에서 C가 차지하던 칸만 메우고 나머지는 그대로 둔다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;backend:  A B C  &amp;rarr;  A B          (C 제거)

slot      0    1    2    3    4    5    6
before    A    B    C    A    C    B    A
after     A    B    B    A    A    B    A
          =    =    C    =    C    =    =
                    └─────────┴── C의 칸(2,4)만 교체, A&amp;middot;B 연결은 전부 유지

legend:  = 유지   x 멀쩡한 연결이 끊김   C 제거된 backend(불가피)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maglev도 실제론 약간의 추가 이동이 있지만 modulo와 비교할 수 없을 만큼 적다. 이 안정성 덕분에 Session Table이 miss여도 &amp;mdash; 항목이 밀려났거나 다른 노드로 가도 &amp;mdash; 같은 연결은 같은 backend로 다시 향한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;특징&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;균등 분배&lt;/td&gt;
&lt;td&gt;ring 슬롯이 backend에 고르게 채워짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최소 교란&lt;/td&gt;
&lt;td&gt;backend 1대 추가/제거 시 영향받는 연결 최소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빠른 조회&lt;/td&gt;
&lt;td&gt;패킷당 ring 인덱싱 1회 (O(1))&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;session table이 있는데, 굳이 비싼 연산이 필요한 Maglev가 필요할까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 LB 단일 노드면 그렇게 큰 문제는 없다. 하지만 대부분 이중화를 위해 멀티 노드 구성이 필요하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 ring을 모든 LB 노드가 공유하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;어느 노드로 가도 같은 연결은 같은 backend&lt;/b&gt;로 향한다. ECMP scale-out과 Session Table을 동시에 지탱하는 핵심이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 SW L4의 내용이 담긴게 Meta의 오픈소스 프로젝트 &lt;b&gt;Katran&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2편 &lt;i&gt;Katran 코드 리뷰&lt;/i&gt;에서 control plane / data plane 구조와 BPF map 설계, 그리고 &lt;b&gt;per-CPU session table이 왜 그렇게 생겼는지&lt;/b&gt;를 코드로 본다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.usenix.org/conference/nsdi16/technical-sessions/presentation/eisenbud&quot;&gt;Maglev: A Fast and Reliable Software Network Load Balancer (NSDI 2016)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ebpf.io&quot;&gt;eBPF.io&lt;/a&gt; &amp;mdash; eBPF 개요&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/facebookincubator/katran&quot;&gt;facebookincubator/katran&lt;/a&gt; &amp;mdash; XDP 기반 L4 LB (2편에서 다룬다)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Infra</category>
      <author>꼬꼬팜</author>
      <guid isPermaLink="true">https://cocopam.tistory.com/57</guid>
      <comments>https://cocopam.tistory.com/57#entry57comment</comments>
      <pubDate>Sun, 28 Jun 2026 15:58:05 +0900</pubDate>
    </item>
    <item>
      <title>인증서, 분산 시스템 클러스터링</title>
      <link>https://cocopam.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 서버의 인증서 교체 작업에서 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드간 clustering을 맺고 있는 환경에서, clustering을 위한 DB certificates가 만료되어 renew를 해야하는 상황이었다. 인증서 교체 이후 클러스터링이 제대로 맺어지지 않았는데, 이참에 인증서 및 분산 시스템의 클러스터링에 대해서 제대로 정리해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Certificates?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cocopam.tistory.com/51&quot;&gt;https://cocopam.tistory.com/51&lt;/a&gt; 해당 게시글에서도 한번 정리했지만, 인증서의 목적은 딱 하나로 정리할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 통신하려는 서버가 위조되지 않은 정상적인 서버인가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSYEIq/dJMcaftj6KS/PCs9injnLY6LiBaoiuWa3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSYEIq/dJMcaftj6KS/PCs9injnLY6LiBaoiuWa3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSYEIq/dJMcaftj6KS/PCs9injnLY6LiBaoiuWa3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSYEIq%2FdJMcaftj6KS%2FPCs9injnLY6LiBaoiuWa3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;654&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위 그림처럼 인증서는 End-Entity &amp;rarr; Intermediate CA &amp;rarr; Root CA로 이어지는 &lt;b&gt;체인(Chain of Trust)&lt;/b&gt; 구조를 가진다. public한 환경에서 브라우저는 이 체인 최상단의 Root CA가 신뢰 저장소에 내장된 공인 CA일 때만 인증서를 신뢰하도록 기본 보안 정책이 지정되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 인증서는 web(https)환경 외에 다양한 용도로 사용될 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서버간 통신 암호화 및 인증 (mTLS)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;radius 인증&lt;/li&gt;
&lt;li&gt;ssh 통신 암호화 및 인증 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제가 되었던 케이스는 클러스터링 시, 서버간 DB에 접근을 진행하는데, 이 때, 인증서 때문에 문제가 발생했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB connection&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postgres 기준으로, DB 커넥션의 경우 보통 TCP 5432 포트를 사용해서 DB에 원격으로 접근한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DB 서버 TCP connection 수립&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SSL/TLS 세션 수립 (옵션)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;username / password 확인&lt;/li&gt;
&lt;li&gt;DB connection 수립&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB connection 간 데이터 암호화 및 서버 신뢰 확인을 위해, 서버는 자신의 인증서(공개키 포함)를 클라이언트에게 전달한다. 클라이언트는 이 인증서로 &quot;정상적인 서버인가&quot;를 검증하고, 이후 키 교환을 통해 양측이 동일한 세션키를 도출한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히는 인증서가 세션키를 &quot;생성&quot;하는 게 아니다. 인증서로 서버 신원을 검증한 뒤, pre-master secret과 랜덤 값을 키 교환해 세션키를 파생시킨다. (TLS handshake 상세는 &lt;a href=&quot;https://cocopam.tistory.com/51&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SSL 인증서&lt;/a&gt; 참고)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 2번 단계에서 certificates invalid가 발생했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클러스터링 DB certificates&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xfpR3/dJMcadWzb1N/bFGYA0pHe9KZKuw8FBOH31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xfpR3/dJMcadWzb1N/bFGYA0pHe9KZKuw8FBOH31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xfpR3/dJMcadWzb1N/bFGYA0pHe9KZKuw8FBOH31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxfpR3%2FdJMcadWzb1N%2FbFGYA0pHe9KZKuw8FBOH31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1228&quot; height=&quot;326&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 각 노드들은 각각의 DB 인증서를 가지게되는데, 해당 인증서들은 공인 인증서가 아닌, 자체 서명된 사설 인증서를 사용하게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 신뢰 구조가 두 갈래로 나뉜다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;노드마다 독립적인 self-signed 인증서&lt;/b&gt;: 각 노드는 통신하는 모든 상대 노드의 인증서를 자기 신뢰 저장소(postgres라면 &lt;code&gt;sslrootcert&lt;/code&gt;)에 일일이 등록해야 함 &amp;rarr; 노드가 늘수록 관리 부담&amp;uarr;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사설 CA 1개로 각 노드 인증서를 서명&lt;/b&gt;: 모든 노드가 이 사설 CA 하나만 신뢰하면 됨 &amp;rarr; 노드 추가/교체가 훨씬 수월&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규모가 커지면 후자(사설 CA)를 권장한다. 뒤에 나올 etcd도 이 방식이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자체 인증서를 생성할 때, 인증서엔 다음 내용들이 담기게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CA (Certificate Authority)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서를 발급해주는 신뢰 기관&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DigiCert&lt;/li&gt;
&lt;li&gt;Let&amp;rsquo;s Encrypt&lt;/li&gt;
&lt;li&gt;GlobalSign&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서엔 &lt;b&gt;Issuer(발급자)&lt;/b&gt; 필드가 있어서 &quot;누가 이 인증서에 서명했는지&quot;를 담는다. 공인 인증서라면 이 자리에 DigiCert 같은 CA가 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 오해하는 부분인데, &lt;b&gt;자체 서명(self-signed) 인증서라고 해서 이 Issuer가 비어있는 게 아니다.&lt;/b&gt; 위 인증서 체인 그림의 Root 인증서처럼 Issuer(발급자)와 Subject(주체)가 동일하고, 자기 자신의 private key로 서명한다 (&lt;code&gt;Issuer == Subject&lt;/code&gt;).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 차이는 &quot;비어있다&quot;가 아니라 &lt;b&gt;이 인증서를 보증해주는 제3자(공인 CA)가 없다&lt;/b&gt;는 점이다. 그래서 공인 CA처럼 OS/브라우저 신뢰 저장소에 기본 내장되어 있지 않고, 이 인증서(또는 이를 서명한 사설 CA)를 통신 상대방이 &lt;b&gt;명시적으로 신뢰 저장소에 등록&lt;/b&gt;해줘야 한다. 서버간 폐쇄망 통신에선 공인 CA를 거칠 이유가 없으니 이 방식을 주로 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CN (Common Name)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서가 누구의 것인지 확인하는 필드&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;CN=db.company.com
or
CN=10.10.10.11&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 서버의 인증을 담당하는지 나타내는 필드로, 과거엔 여기에 DNS/IP 같은 호스트네임을 담아 서버를 검증했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &lt;b&gt;지금은 호스트네임 검증에 CN을 거의 쓰지 않고 아래 SAN을 사용한다.&lt;/b&gt; SAN이 있으면 libpq&amp;middot;브라우저 모두 CN을 무시하고 SAN만 본다. SAN이 &lt;b&gt;없을 때&lt;/b&gt; 동작이 갈리는데, libpq는 CN으로 fallback하지만 최신 브라우저(Chrome 58+)는 fallback 없이 인증서를 거부한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SAN&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CN은 단 하나의 서버만 등록이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 서버들은 다양한 호스트네임 및 IP을 가질 수 있기에 다양한 필드를 담는 경우가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 최근 인증서에서 서버 검증은 이 SAN 필드를 활용한다. 특히 이번처럼 &lt;b&gt;IP(10.10.10.x)로 노드끼리 붙는 경우, SAN의 &lt;code&gt;iPAddress&lt;/code&gt; 항목이 사실상 필수&lt;/b&gt;다 (IP에 대한 CN 매칭은 클라이언트마다 동작이 일정치 않음). 예를 들어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;CN=db.company.com

SAN:
DNS=db.company.com
DNS=postgres.company.com
DNS=database.company.com
IP=10.10.10.11&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 IP or DNS를 등록해서 사용 가능하다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;openssl req \
  -x509 \
  -newkey rsa:2048 \
  -sha256 \
  -days 365 \
  -nodes \
  -keyout server.key \
  -out server.crt \
  -subj &quot;/CN=db.example.com&quot; \
  -addext &quot;subjectAltName=DNS:db.example.com,IP:10.10.10.11&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-subj&lt;/code&gt;로 CN만 넣고 &lt;code&gt;-addext&lt;/code&gt;로 SAN을 빠뜨리면, 위에서 설명한 호스트네임 검증 단계에서 바로 깨진다. (&lt;code&gt;-addext&lt;/code&gt;는 OpenSSL 1.1.1+ 필요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 인증서의 SAN은 아래로 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;openssl x509 -in server.crt -noout -text | grep -A1 &quot;Subject Alternative Name&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;clustering 시 서버 신뢰 불가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 돌아와서, 각 서버에 SAN에 본인이 등록이 없다면 어떤일이 발생할까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터링은 노드끼리 양방향으로 붙기 때문에, 한쪽만 인증서를 검사하는 일반 HTTPS와 달리 &lt;b&gt;mTLS(mutual TLS)&lt;/b&gt; 를 쓴다. 즉 connection을 거는 쪽(client)과 받는 쪽(server)이 &lt;b&gt;서로의 인증서를 검증&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은, &lt;b&gt;호스트네임/SAN 검증은 그 순간 'server로 동작하는 쪽'의 인증서에 대해서만 일어난다&lt;/b&gt;는 점이다. client는 자기가 접속한 주소(IP)와 server 인증서의 SAN이 일치하는지 확인할 뿐, 반대로 server가 client의 SAN을 호스트네임으로 검증하진 않는다 (client 인증서는 보통 CA 체인 신뢰 여부만 본다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Br6FS/dJMcacKb38U/ShkP48evJKfzKJKWFFonB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Br6FS/dJMcacKb38U/ShkP48evJKfzKJKWFFonB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Br6FS/dJMcacKb38U/ShkP48evJKfzKJKWFFonB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBr6FS%2FdJMcacKb38U%2FShkP48evJKfzKJKWFFonB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1360&quot; height=&quot;396&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 노드는 양방향으로 DB connection을 맺고 DB sync를 진행함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Worker &amp;rarr; Master&lt;/b&gt;: 이때 Master가 server. Master 인증서 SAN에 &lt;code&gt;10.10.10.1&lt;/code&gt;이 있으니 검증 통과 &amp;rarr; 접근 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Master &amp;rarr; Worker&lt;/b&gt;: 이때 Worker가 server. Worker 인증서엔 SAN이 없으니 호스트네임 검증 실패 &amp;rarr; 접근 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;b&gt;한쪽 방향만 깨지는 비대칭 장애&lt;/b&gt;가 나타난다. 양방향 sync가 핵심인 클러스터링에선 한쪽만 끊겨도 결국 클러스터가 정상화되지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;어디까지 검증할까 &amp;mdash; postgres&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 &quot;SAN 검증 실패&quot;가 실제로 connection을 깨뜨리는지는 클라이언트의 &lt;code&gt;sslmode&lt;/code&gt; 설정에 달려있다. postgres는 검증 수준을 단계별로 제공한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EtdJM/dJMcafmBuh0/C9wkKRLk4kUFyOMbBLXbp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EtdJM/dJMcafmBuh0/C9wkKRLk4kUFyOMbBLXbp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EtdJM/dJMcafmBuh0/C9wkKRLk4kUFyOMbBLXbp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEtdJM%2FdJMcafmBuh0%2FC9wkKRLk4kUFyOMbBLXbp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;986&quot; height=&quot;368&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;SAN 불일치는 &lt;code&gt;verify-full&lt;/code&gt;에서만 connection을 깨뜨린다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;verify-ca&lt;/code&gt;까지는 CA 체인만 맞으면 통과하고, &lt;code&gt;require&lt;/code&gt;는 암호화만 하고 신원 검증을 아예 안 한다.&lt;br /&gt;그래서 같은 인증서라도 클러스터링이 &lt;code&gt;sslmode&lt;/code&gt;를 어떻게 잡았느냐에 따라 멀쩡할 수도, 깨질 수도 있다. (앞의 &quot;strict하게 설정&quot;이 바로 이 &lt;code&gt;sslmode&lt;/code&gt; 단계 선택이다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그래서 왜 renew 후에 깨졌나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 상황으로 돌아오면, 인증서를 renew한 뒤 클러스터링이 안 맺어졌다. DB 연결 시, &lt;code&gt;sslmode&lt;/code&gt; 가 &lt;code&gt;vertify-full&lt;/code&gt; 이어서 SAN에 도메인이 비어있어서 문제가 발생했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;verify-full&lt;/code&gt;인 경우&lt;/b&gt; &amp;rarr; 새로 발급한 인증서에 &lt;b&gt;SAN(특히 노드 IP)이 빠졌을 가능성&lt;/b&gt;이 크다. 기존 인증서엔 있었는데 renew 스크립트가 &lt;code&gt;-addext&lt;/code&gt;를 누락하게 되었다.&lt;/li&gt;
&lt;li&gt;위 둘과 무관하게, &lt;b&gt;단순 만료/노드 간 시계 불일치&lt;/b&gt;도 검증을 깨뜨린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유사 케이스 &amp;mdash; k8s etcd&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zFfEe/dJMcab5xoFg/QEs2ggN9Gdnjo1LIegXuYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zFfEe/dJMcab5xoFg/QEs2ggN9Gdnjo1LIegXuYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zFfEe/dJMcab5xoFg/QEs2ggN9Gdnjo1LIegXuYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzFfEe%2FdJMcab5xoFg%2FQEs2ggN9Gdnjo1LIegXuYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;901&quot; height=&quot;461&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;461&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k8s의 etcd도 멤버(서버)끼리 위와 똑같이 mTLS로 통신한다. etcd는 용도별로 인증서를 나눠 쓴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;peer 인증서&lt;/b&gt; (&lt;code&gt;peer.crt&lt;/code&gt;/&lt;code&gt;peer.key&lt;/code&gt;): etcd 멤버 &amp;harr; 멤버 통신용. 멤버끼리 양방향으로 붙으므로 서로의 인증서를 검증함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;server 인증서&lt;/b&gt;: kube-apiserver 같은 client &amp;rarr; etcd 통신용&lt;/li&gt;
&lt;li&gt;모두 보통 클러스터 전용 &lt;b&gt;사설 CA(&lt;code&gt;etcd/ca.crt&lt;/code&gt;)&lt;/b&gt; 로 서명하고, 각 멤버는 이 CA 하나만 신뢰함 (앞서 말한 &quot;사설 CA 1개&quot; 방식)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서도 핵심은 동일하다. &lt;b&gt;각 멤버의 peer 인증서 SAN에 자신의 peer URL(IP/호스트네임)이 들어있어야&lt;/b&gt; 한다. 멤버를 추가하거나 IP가 바뀌었는데 SAN을 갱신하지 않으면, 위 클러스터링 사례와 똑같이 일부 방향의 mTLS 검증이 실패하면서 멤버가 클러스터에 합류하지 못한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# etcd 멤버 기동 시 (요지)
etcd \
  --peer-cert-file=peer.crt \
  --peer-key-file=peer.key \
  --peer-trusted-ca-file=etcd/ca.crt \
  --peer-client-cert-auth=true \   # peer 인증서 검증(mTLS) 활성화
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubeadm으로 띄운 클러스터라면 이 인증서들은 보통 &lt;code&gt;/etc/kubernetes/pki/etcd/&lt;/code&gt; 아래에 있고, &lt;code&gt;kubeadm certs check-expiration&lt;/code&gt;으로 각 인증서 만료일을 한 번에 확인할 수 있다.&lt;/p&gt;</description>
      <category>Infra</category>
      <author>꼬꼬팜</author>
      <guid isPermaLink="true">https://cocopam.tistory.com/56</guid>
      <comments>https://cocopam.tistory.com/56#entry56comment</comments>
      <pubDate>Sun, 7 Jun 2026 17:03:33 +0900</pubDate>
    </item>
    <item>
      <title>회사에서 AI를 어떻게 써야할까</title>
      <link>https://cocopam.tistory.com/55</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 반년간 회사에서 AI TF 업무를 진행하면서 확인했던 인사이트들을 정리해 보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현업에서 AI에 대한 고찰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 팀원분들이 AI를 얼마나 업무에 활용하나 보면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;업무 대부분을 AI에게 맡겨 자동화한다.&lt;/li&gt;
&lt;li&gt;아직까지는 업무에 조언 정도만 받는 정도고, 실질적인 도움은 잘 모르겠다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 갈리는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 작년 말부터 제대로 GPT나 Claude 같은 도구들의 enterprise 계약을 시작하며 본격적으로 업무에 활용한 기간은 길지 않지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI를 어떻게 업무에 활용하는지 고민을 하게 되는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이건 직무마다 AI를 밀도 있게 활용할 수 있는 범위가 다르긴 하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0eGsZ/dJMcadad9Wh/j5Y4VqGugDsK7kGwZQtxeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0eGsZ/dJMcadad9Wh/j5Y4VqGugDsK7kGwZQtxeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0eGsZ/dJMcadad9Wh/j5Y4VqGugDsK7kGwZQtxeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0eGsZ%2FdJMcadad9Wh%2Fj5Y4VqGugDsK7kGwZQtxeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;806&quot; height=&quot;1024&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트는 &lt;b&gt;시스템을 얼마나 쉽게 AI에 통합할 수 있는지&lt;/b&gt;인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 개발자들의 경우 기존 업무 환경 자체가 AI에게 친화적인 환경(Git, Slack, k8s)이고, 대부분의 컨텍스트가 자연어로 처리되며, 조회(GET), 변경(UPDATE) 자체가 명세로 작성되어 있는 환경인 경우 더욱 AI를 도입하기 쉬워지고, LLM 학습 자체가 해당 환경 자체를 학습 데이터로 활용했을 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이 구분도 생각보다 멀지 않게 허물어질 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI의 능동화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 AI 솔루션들이 모델 자체의 발전보다는 LLM에게 sandbox 내에서 subtask 명령을 수행할 권한을 주고, 본인이 도구들을 선택해 수행하게 되었다. 예를 들어 Claude Code, Codex, OpenClaw 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 이게 &lt;b&gt;가장 큰 터닝포인트&lt;/b&gt;인 것 같다. 단순히 외부에서 Agentic AI라는 것을 칭송할 때도 그렇게 큰 도움이 되지 않을 거라고 생각했다. 작년에 LangGraph가 처음 나왔을 때, 직접 코딩으로 해당 workflow를 구현해야 했고, 튜닝도 직접 개발자가 개입할 필요가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이제는 LangGraph는커녕, Skills와 같은 source에 대한 connector도 프롬프트로 생성이 가능해진 단계까지 보고, 이제는 AI의 커버리지가 훨씬 더 늘었다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI Connector에 대한 생각&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼마 전까지 핫했던 게 MCP 서버였다. 수행해야 할 행동들을 서버에 구현해 놓고, LLM이 해당 서버에 tool 호출을 통해 서비스에 접근하게 만든 프로토콜이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근엔 이도 필요 없다고 한 게, 위에서도 언급한 sandbox 형태의 CLI에서 동작 수행을 너무나 잘하기에, 굳이 사전에 행동들이 명시된 MCP 서버도 필요 없다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 나는 아직까지 &lt;b&gt;둘이 상위 호환적인 관계는 아니라고&lt;/b&gt; 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MCP&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Action에 대한 명세가 정확하고 예외적인 상황이 필요 없는 경우&lt;/li&gt;
&lt;li&gt;연결된 서비스들에 대한 연동이 구현되어 있어야 함(Open API, SQL 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SKILL (CLI)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;너무나 다양한 Action의 형태가 존재하는 경우(사용자가 개별로 custom이 필요한 경우)&lt;/li&gt;
&lt;li&gt;sandbox 밖의 context가 필요한 경우는 보안상 위험&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각에 맞게 필요한 connector들을 선택해 AI에 통합시키면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Interface (UX)에 대한 고민&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 가장 큰 고민인 게, 대부분의 사람이 익숙한 게 ChatGPT와 같은 챗봇 형태의 UX이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 해당 UX에 한정되다 보니, 사람이 중간에 개입하는 interaction이 제한될 수밖에 없고, 팀에 대한 공유가 어렵다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고, 웹 환경의 새로운 시스템을 만들자니, 들어가는 리소스를 고려하면 배보다 배꼽이 커지는 경우이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케줄링과 다양한 시스템과 연동이 가능한 n8n과 같은 도구들도 고민하고 있지만, (생각보다 굉장히 빠르게 workflow를 찍어낼 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI agent에 대한 커스텀이 너무나 부족해, 단순한 작업만 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 아직까지 고민이 많은 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI를 업무에 더 밀도 있게 쓰려면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 내가 말하고 싶은 부분은 사내에서 개인이 AI를 더 잘 쓰는 법을 정리하는 게 아니다.&lt;br /&gt;예를 들어, AI에게 어떻게 프롬프트를 해야 더욱 답변이 잘 나오고, Claude보다 Gemini가 어떤 업무를 잘하나? 이런 미시적인 영역이 아니라, &lt;b&gt;업무 시스템 자체를 AI에 통합할 수 있는 거시적인 영역&lt;/b&gt;에 대한 고민이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 사내에서 Confluence 위키, 공용 드라이브를 사용한다고 해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위키들의 정보들을 AI에게 참고시키고, context를 이해시키게 해야 한다고 하자. 그렇다면 해당 환경에서 지양해야 할 것은 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 LLM들은 context size가 200K 토큰 정도이다. 당연히 사내 정보를 모두 넣을 수 없는 용량이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무에 대한 &lt;b&gt;검색 시스템&lt;/b&gt;이 필요할 것이고, LLM에게 ground rule로 해당 검색 시스템에 어느 정보들이 있는지 지시하는 것이다. (여기에 말한 검색은 RAG일 필요는 없다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지나 PPT와 같은 소스들은 자연어보다 훨씬 많은 token을 소비하기에 최대한 자연어로 검색이 가능한 시스템일수록 성능이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;커뮤니케이션 이력들이 투명해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전에 합의한 내용들이 메일과 유선으로만 공유되어, AI에게 접근이 불가한 경우 해당 맥락이 제공이 어렵다면 이는 큰 방해 요소가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회가 가능한 Jira, Slack과 같은 기타 협업 툴을 쓰는 것이 더욱 효율적이다.&lt;/p&gt;</description>
      <category>끄적끄적/회고</category>
      <author>꼬꼬팜</author>
      <guid isPermaLink="true">https://cocopam.tistory.com/55</guid>
      <comments>https://cocopam.tistory.com/55#entry55comment</comments>
      <pubDate>Sat, 21 Mar 2026 17:45:55 +0900</pubDate>
    </item>
    <item>
      <title>k8s CoreDNS, 사설 DNS로 활용하기</title>
      <link>https://cocopam.tistory.com/54</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k8s 서비스를 사설망에서 접근할 때,&lt;br /&gt;매번 MetalLB IP를 직접 확인해서 써야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 Traefik Host 기반 reverse proxy를 쓰고 있어서,&lt;br /&gt;접근하려는 도메인을 서비스마다 수동 매핑해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 늘어날수록 관리 포인트가 계속 늘어났고,&lt;br /&gt;이름 해석/매핑 역할을 CoreDNS로 넘기는 방식으로 정리했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CoreDNS가 하는 일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreDNS는 Kubernetes 기본 DNS 서버다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E6tnx/dJMb99SWaHL/kcqpVqQBjutkofxWtix2Ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E6tnx/dJMb99SWaHL/kcqpVqQBjutkofxWtix2Ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E6tnx/dJMb99SWaHL/kcqpVqQBjutkofxWtix2Ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE6tnx%2FdJMb99SWaHL%2FkcqpVqQBjutkofxWtix2Ik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1170&quot; height=&quot;878&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 디스커버리: &lt;code&gt;service.namespace.svc.cluster.local&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Pod DNS 자동 구성: Pod의 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;에 &lt;code&gt;kube-dns&lt;/code&gt; 주소 주입&lt;/li&gt;
&lt;li&gt;Stateful 워크로드 DNS: headless service 기반 Pod 레코드 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 클러스터 기준 기본 DNS 엔드포인트는 아래다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;kube-system/kube-dns&lt;/code&gt; (ClusterIP: &lt;code&gt;10.96.0.10&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreDNS는 보통 &lt;code&gt;kube-system&lt;/code&gt; 네임스페이스의 Deployment로 동작하고,&lt;br /&gt;ConfigMap(&lt;code&gt;Corefile&lt;/code&gt;)에 정의한 플러그인 체인으로 질의를 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 쓰는 플러그인은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;kubernetes&lt;/code&gt;: cluster.local 레코드 처리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forward&lt;/code&gt;: 외부 도메인을 상위 DNS로 전달&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cache&lt;/code&gt;: 질의 결과 캐시&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CoreDNS 작동 로직&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1615&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czjbWY/dJMcagEyEuu/pXUO6Mohti6AnwQLJdE3h0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czjbWY/dJMcagEyEuu/pXUO6Mohti6AnwQLJdE3h0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czjbWY/dJMcagEyEuu/pXUO6Mohti6AnwQLJdE3h0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczjbWY%2FdJMcagEyEuu%2FpXUO6Mohti6AnwQLJdE3h0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1615&quot; height=&quot;399&quot; data-origin-width=&quot;1615&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Pod 내부 &lt;code&gt;resolv.conf&lt;/code&gt; 기준 질의 로직&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;를 보고 DNS 질의를 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Pod 설정 예시는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;nameserver&lt;/code&gt;: DNS 서버 주소 (&lt;code&gt;kube-dns&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;search&lt;/code&gt;: 짧은 이름 조회 시 붙는 suffix&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ndots&lt;/code&gt;: 절대도메인(FQDN)로 볼지 판단하는 기준&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1291&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQnw42/dJMcafFC7hN/jK9CKluPYYoZ3gdH4tBWz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQnw42/dJMcafFC7hN/jK9CKluPYYoZ3gdH4tBWz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQnw42/dJMcafFC7hN/jK9CKluPYYoZ3gdH4tBWz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQnw42%2FdJMcafFC7hN%2FjK9CKluPYYoZ3gdH4tBWz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1291&quot; height=&quot;730&quot; data-origin-width=&quot;1291&quot; data-origin-height=&quot;730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 외부 인터넷 접속 시에도 DNS 조회는 CoreDNS를 거칠 수 있지만,&lt;br /&gt;실제 HTTP/TCP 트래픽은 조회된 IP로 직접 통신한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Service를 IP 대신 도메인으로 쓸까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &quot;&lt;b&gt;변하는 값은 숨기고, 고정된 이름으로 접근&lt;/b&gt;&quot;하기 위해서다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pod IP는 재시작/재스케줄링 때 자주 바뀜&lt;/li&gt;
&lt;li&gt;Service 이름은 고정이고, 뒤의 Pod 집합만 동적으로 교체됨&lt;/li&gt;
&lt;li&gt;환경(dev/stg/prod) 간 IP 대역이 달라도 이름은 동일하게 유지 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 앱 설정에는 IP 하드코딩보다 DNS 이름을 쓰는 게 운영이 훨씬 쉽다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사설망에 Core DNS 활용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성은 크게 두 층으로 나눴다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클러스터 내부 기본 DNS: &lt;code&gt;kube-dns&lt;/code&gt; (&lt;code&gt;10.96.0.10&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;사설망 클라이언트용 DNS: &lt;code&gt;internal-dns&lt;/code&gt; (&lt;code&gt;172.16.200.53&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 내부용과 외부(사설망)용 역할을 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서비스 네트워크 flow&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2420&quot; data-origin-height=&quot;1744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TlkMt/dJMcaadf80H/0g4TstxkSrIIaQOyBeVXr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TlkMt/dJMcaadf80H/0g4TstxkSrIIaQOyBeVXr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TlkMt/dJMcaadf80H/0g4TstxkSrIIaQOyBeVXr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTlkMt%2FdJMcaadf80H%2F0g4TstxkSrIIaQOyBeVXr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2420&quot; height=&quot;1744&quot; data-origin-width=&quot;2420&quot; data-origin-height=&quot;1744&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 사설망 전용 CoreDNS 노출&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;internal-dns&lt;/code&gt; 네임스페이스에 전용 CoreDNS를 배치하고,&lt;br /&gt;Service를 LoadBalancer로 노출했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Service: &lt;code&gt;internal-dns/internal-dns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Type: &lt;code&gt;LoadBalancer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;LB IP: &lt;code&gt;172.16.200.53&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Port: &lt;code&gt;53/UDP&lt;/code&gt;, &lt;code&gt;53/TCP&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 사설망 Zone(&lt;code&gt;home.internal&lt;/code&gt;) 구성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사설망에서 쓸 이름은 &lt;code&gt;home.internal&lt;/code&gt; 존으로 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;internal-dns-corefile&lt;/code&gt; 핵심 설정:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;home.internal:53 {
  log
  errors
  file /etc/coredns/zones/home.internal.db home.internal
  header {
    response set ra
  }
}

.:53 {
  log
  errors
  health
  ready
  cache 30
  forward . 1.214.68.2 61.41.153.2
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 foward dns 주소는 통신사(유플러스) dns 주소&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;internal-dns-zone&lt;/code&gt;의 주요 레코드:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ORIGIN home.internal.
ns1     IN A 172.16.200.53
traefik IN A 172.16.200.20
hubble  IN A 172.16.200.20
n8n     IN A 172.16.200.20
grafana IN A 172.16.200.20&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 공유기 DHCP에서 DNS 배포&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 클라이언트 수동 설정을 피하려고,&lt;br /&gt;공유기 DHCP Option 6의 DNS를 &lt;code&gt;172.16.200.53&lt;/code&gt;으로 설정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xzJcT/dJMcaivxWQX/qxxRKUoYAhvyaht8fDDtGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xzJcT/dJMcaivxWQX/qxxRKUoYAhvyaht8fDDtGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xzJcT/dJMcaivxWQX/qxxRKUoYAhvyaht8fDDtGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxzJcT%2FdJMcaivxWQX%2FqxxRKUoYAhvyaht8fDDtGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;848&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Primary DNS: &lt;code&gt;172.16.200.53&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Secondary DNS: 필요 시 내부 DNS만 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) MetalLB 대역 정적 라우팅&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 환경의 MetalLB 풀은 아래 두 IP를 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;172.16.200.20/32&lt;/code&gt; (Traefik LB)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;172.16.200.53/32&lt;/code&gt; (internal-dns LB)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbjyvD/dJMcaioPwTR/8jhz7CKQZCLNLgwY28iyN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbjyvD/dJMcaioPwTR/8jhz7CKQZCLNLgwY28iyN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbjyvD/dJMcaioPwTR/8jhz7CKQZCLNLgwY28iyN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbjyvD%2FdJMcaioPwTR%2F8jhz7CKQZCLNLgwY28iyN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;949&quot; height=&quot;272&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 서브넷에서도 접근되도록 공유기에 정적 라우팅을 추가했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대상 대역: &lt;code&gt;172.16.200.0/24&lt;/code&gt; (또는 &lt;code&gt;/32&lt;/code&gt; host route 2개)&lt;/li&gt;
&lt;li&gt;다음 홉: MetalLB를 광고하는 노드(&lt;code&gt;soyo&lt;/code&gt;)의 LAN IP&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;code&gt;L2Advertisement&lt;/code&gt;가 &lt;code&gt;soyo&lt;/code&gt;에 고정되어 있어,&lt;br /&gt;다음 홉도 해당 노드를 기준으로 잡았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) WireGuard VPN에서도 사설 DNS 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 WireGuard로 붙을 때도 내부 도메인을 쓰기 위해,&lt;br /&gt;클라이언트 설정에 DNS를 명시했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[Interface]
Address = &amp;lt;vpn-client-ip&amp;gt;/32
PrivateKey = &amp;lt;client-private-key&amp;gt;
DNS = 172.16.200.53

[Peer]
PublicKey = &amp;lt;server-public-key&amp;gt;
Endpoint = &amp;lt;vpn-endpoint&amp;gt;:51820
AllowedIPs = 172.16.200.0/24
PersistentKeepalive = 25&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심: &lt;code&gt;DNS = 172.16.200.53&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Split tunnel이면 DNS/LB 대역이 &lt;code&gt;AllowedIPs&lt;/code&gt;에 포함되어야 함&lt;/li&gt;
&lt;li&gt;Full tunnel이면 DNS 질의도 자동으로 VPN 경로 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6) 전체 흐름 요약&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;flowchart LR
    A[사설망 or VPN Client] --&amp;gt;|DNS Query: grafana.home.internal| B[internal-dns LB 172.16.200.53]
    B --&amp;gt; C[응답: 172.16.200.20]
    A --&amp;gt;|HTTP/HTTPS| D[Traefik LB 172.16.200.20]
    D --&amp;gt; E[Kubernetes Service/Pod]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면,&lt;br /&gt;DNS는 &lt;code&gt;172.16.200.53&lt;/code&gt;에서 이름만 해석하고,&lt;br /&gt;실제 서비스 트래픽은 &lt;code&gt;172.16.200.20&lt;/code&gt;으로 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7) 검증&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사설망 클라이언트/VPN 클라이언트에서 아래 순서로 확인했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;dig +short @172.16.200.53 grafana.home.internal
dig +short @172.16.200.53 traefik.home.internal
dig +short @172.16.200.53 google.com
nslookup grafana.home.internal
wg show&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 포인트는 다음이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;home.internal&lt;/code&gt; 질의가 &lt;code&gt;172.16.200.20&lt;/code&gt;으로 응답되는지&lt;/li&gt;
&lt;li&gt;외부 도메인도 forward를 통해 정상 조회되는지&lt;/li&gt;
&lt;li&gt;DHCP/WireGuard 경로에서도 동일한 DNS 결과가 나오는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;1451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qByMz/dJMcagxKm0n/SHWLNUOmqJNxkGzFqir590/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qByMz/dJMcagxKm0n/SHWLNUOmqJNxkGzFqir590/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qByMz/dJMcagxKm0n/SHWLNUOmqJNxkGzFqir590/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqByMz%2FdJMcagxKm0n%2FSHWLNUOmqJNxkGzFqir590%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1316&quot; height=&quot;1451&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;1451&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Infra</category>
      <author>꼬꼬팜</author>
      <guid isPermaLink="true">https://cocopam.tistory.com/54</guid>
      <comments>https://cocopam.tistory.com/54#entry54comment</comments>
      <pubDate>Sun, 22 Feb 2026 16:03:32 +0900</pubDate>
    </item>
    <item>
      <title>k8s network (kube-proxy)</title>
      <link>https://cocopam.tistory.com/53</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Why k8s network?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 모노리틱 구조에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 가진 &lt;b&gt;고유 IP 또는 VIP&lt;/b&gt;에 애플리케이션이 직접 바인딩됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;1120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWAN4q/dJMcagEb5wD/KQhckIFL4kkRQFEQM83vH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWAN4q/dJMcagEb5wD/KQhckIFL4kkRQFEQM83vH1/img.png&quot; data-alt=&quot;Monolithic Service Network&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWAN4q/dJMcagEb5wD/KQhckIFL4kkRQFEQM83vH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWAN4q%2FdJMcagEb5wD%2FKQhckIFL4kkRQFEQM83vH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;453&quot; height=&quot;425&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;1120&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Monolithic Service Network&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 클러스터에서는 애플리케이션이 &lt;b&gt;서버 위가 아니라 Pod 단위&lt;/b&gt;로 배치&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1732&quot; data-origin-height=&quot;1358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nBVcg/dJMcahb0oEk/HQ5AF35NI4IDP6QKDNngk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nBVcg/dJMcahb0oEk/HQ5AF35NI4IDP6QKDNngk1/img.png&quot; data-alt=&quot;k8s Service Network&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nBVcg/dJMcahb0oEk/HQ5AF35NI4IDP6QKDNngk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnBVcg%2FdJMcahb0oEk%2FHQ5AF35NI4IDP6QKDNngk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;615&quot; height=&quot;1358&quot; data-origin-width=&quot;1732&quot; data-origin-height=&quot;1358&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;k8s Service Network&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;img src=&quot;https://kubernetes.io/docs/images/kubernetes-cluster-network.svg&quot; alt=&quot;A figure illustrating the different network ranges in a kubernetes cluster&quot; /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pod는 클러스터 내부 &lt;b&gt;Overlay Network&lt;/b&gt; 위에 존재&lt;/li&gt;
&lt;li&gt;Underlay(Baremetal 서버, 스위치, ToR)는 Pod나 컨테이너의 IP/Port를 &lt;b&gt;직접 인지하지 않음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 인프라는 단순히 &amp;ldquo;Pod 네트워크를 전달하는 역할&amp;rdquo;만 수행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 추상화를 가능하게 하는 핵심 컴포넌트&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CNI (Container Network Interface)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;kube-proxy&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;기본 구성(default)&lt;/b&gt; 인 kube-proxy을 기준으로 Kubernetes 네트워크 흐름을 정리&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;k8s 통신 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 클러스터 네트워크에서 발생하는 주요 트래픽&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;container to container&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pod to pod&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pod to service&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;external service&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Container to Container&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Pod 내부 컨테이너 간 통신&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes에서 &lt;b&gt;Pod는 네트워크의 최소 단위&lt;/b&gt;&lt;br /&gt;같은 Pod에 속한 컨테이너들은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일한 &lt;b&gt;Network Namespace&lt;/b&gt; 공유 (Host network와 분리)&lt;/li&gt;
&lt;li&gt;동일한 IP 주소 사용&lt;/li&gt;
&lt;li&gt;동일한 routing table, iptables 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;1156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqHqzI/dJMcadgkTWE/j4bnFkzs74j9bS0aWPHBu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqHqzI/dJMcadgkTWE/j4bnFkzs74j9bS0aWPHBu0/img.png&quot; data-alt=&quot;Contaienr to Container 통신&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqHqzI/dJMcadgkTWE/j4bnFkzs74j9bS0aWPHBu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqHqzI%2FdJMcadgkTWE%2Fj4bnFkzs74j9bS0aWPHBu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;599&quot; height=&quot;1156&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;1156&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Contaienr to Container 통신&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;127.0.0.1(localhost) 또는 Pod IP로 접근 가능&lt;/li&gt;
&lt;li&gt;포트만 다르면 충돌 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해당 방식을 통해 sidecar 패턴이 가능함&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 namespace 공유 -&amp;gt; http polling 가능&lt;/li&gt;
&lt;li&gt;filesystem 공유 -&amp;gt; log file read 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pod to Pod&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 통신을 가능하게 하는 것은 &lt;b&gt;CNI 플러그인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CNI가 수행하는 역할은 다음과 같음&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pod IP 할당&lt;/li&gt;
&lt;li&gt;veth pair 생성&lt;/li&gt;
&lt;li&gt;노드 내/노드 간 라우팅 구성&lt;/li&gt;
&lt;li&gt;Overlay(VXLAN) 또는 Underlay(BGP) 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service, NAT, Load Balancing은 개입 x&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서 자세히 다룰 예정&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pod to Service&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service는 &lt;b&gt;Pod 집합에 대한 추상화 계층&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Service는 &lt;b&gt;가상 IP (ClusterIP)&lt;/b&gt; 를 가짐&lt;/li&gt;
&lt;li&gt;실제로는 네트워크 인터페이스에 존재하지 않음&lt;/li&gt;
&lt;li&gt;ARP 응답도 하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가상 IP를 실제 Pod IP로 변환하는 역할이 &lt;b&gt;kube-proxy &lt;/b&gt;(아래에 작성)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;External Service&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes에서 &amp;ldquo;External Service&amp;rdquo; 통신은 &lt;b&gt;클러스터 기준 방향성&lt;/b&gt;에 따라&lt;br /&gt;&lt;b&gt;Inbound(외부 &amp;rarr; 클러스터)&lt;/b&gt; 와 &lt;b&gt;Outbound(클러스터 &amp;rarr; 외부)&lt;/b&gt; 로 나눌 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Inbound traffic&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 &lt;b&gt;클러스터 내부 Pod(Service)&lt;/b&gt; 로 접근하는 흐름&lt;br /&gt;&amp;rarr; 반드시 &lt;b&gt;Service 또는 Ingress&lt;/b&gt; 를 통해 진입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.&amp;nbsp;NodePort&amp;nbsp;(L4)&lt;/b&gt;&lt;br /&gt;각 Node의 특정 포트를 열어 Service로 트래픽 전달&lt;/p&gt;
&lt;pre id=&quot;code_1767432792458&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Client
  &amp;rarr; NodeIP:NodePort
    &amp;rarr; kube-proxy (DNAT)
      &amp;rarr; Service VIP (ClusterIP)
        &amp;rarr; Pod&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.&amp;nbsp;LoadBalancer&amp;nbsp;(L4)&lt;/b&gt;&lt;br /&gt;Cloud Provider 또는 MetalLB가 &lt;b&gt;외부 IP&lt;/b&gt; 할당&lt;/p&gt;
&lt;pre id=&quot;code_1767432819974&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Client
  &amp;rarr; External LB IP
    &amp;rarr; Node
      &amp;rarr; Service VIP
        &amp;rarr; Pod&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3.&amp;nbsp;Ingress&amp;nbsp;(L7)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ingress&amp;nbsp;Controller를&amp;nbsp;두어&amp;nbsp;Service로&amp;nbsp;프록시&amp;nbsp;시킴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Outbound traffic&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod가 &lt;b&gt;외부 서비스(API, DB, SaaS 등)&lt;/b&gt; 로 나가는 트래픽&lt;br /&gt;&amp;rarr; &lt;b&gt;Service VIP와는 무관&lt;/b&gt;, 순수 &lt;b&gt;egress 흐름&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-proxy 기준 outbound 트래픽 (Cilium 구성시, 달라짐)&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Pod
 &amp;rarr; veth
   &amp;rarr; Node routing
     &amp;rarr; iptables POSTROUTING
       &amp;rarr; SNAT (Pod IP &amp;rarr; Node IP)
         &amp;rarr; External&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kube-proxy 작동&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iptables 기준으로 작성&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kube-proxy는 &lt;b&gt;라우팅 x&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Overlay 네트워크 생성 x&lt;/li&gt;
&lt;li&gt;단순히 &lt;b&gt;L4 NAT 규칙을 설치&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-proxy는 다음을 수행&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b84pvB/dJMcajgx4VE/mReYMokG0PbHQ28VYrLJkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b84pvB/dJMcajgx4VE/mReYMokG0PbHQ28VYrLJkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b84pvB/dJMcajgx4VE/mReYMokG0PbHQ28VYrLJkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb84pvB%2FdJMcajgx4VE%2FmReYMokG0PbHQ28VYrLJkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1220&quot; height=&quot;980&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;980&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Service / Endpoint 정보를 watch&lt;/li&gt;
&lt;li&gt;iptables 규칙 생성&lt;/li&gt;
&lt;li&gt;Service IP로 들어온 트래픽을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DNAT&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;실제 Pod IP로 전달&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Infra</category>
      <author>꼬꼬팜</author>
      <guid isPermaLink="true">https://cocopam.tistory.com/53</guid>
      <comments>https://cocopam.tistory.com/53#entry53comment</comments>
      <pubDate>Sat, 3 Jan 2026 18:38:55 +0900</pubDate>
    </item>
    <item>
      <title>Cloudflare Tunnel을 활용한 k8s Ingress 대체</title>
      <link>https://cocopam.tistory.com/52</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;배경&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스를 하나 추가할때마다 필요한 작업들
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 Domain Name에 해당하는 TLS 인증서 발급&lt;/li&gt;
&lt;li&gt;webserver or ingress에서 해당 서비스 프록시 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS 인증서 자체를 관리하는것도 번거롭고, 서비스별로 프록시를 추가하고 관리하는 것도 상당히 귀찮음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 wildcard 도메인으로 발급받아서 공통으로 쓰는것도 가능하지만, 프록시를 추가하거나, 인증서를 갱신하는 과정은 필요함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloudflare에서 TLS termination도 지원하니까, 아예 TLS 인증 자체를 cloudflare에 넘겨버리고, 도메인 -&amp;gt; pod, service로 매핑만 할 수 있는 방법을 고민&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cloudflare Tunnel&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식적인 설명은 다음과 같음&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloudflare Tunnel provides you with a secure way to connect your resources to Cloudflare without a publicly routable IP address. With Tunnel, you do not send traffic to an external IP &amp;mdash; instead, a lightweight daemon in your infrastructure (cloudflared) creates &lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/#outbound-only-connection&quot;&gt;outbound-only connections&lt;/a&gt; to Cloudflare's global network.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;635&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yh4Tu/btsQRHvIaII/qsku6j2KMwdoZaBxpkc7D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yh4Tu/btsQRHvIaII/qsku6j2KMwdoZaBxpkc7D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yh4Tu/btsQRHvIaII/qsku6j2KMwdoZaBxpkc7D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyh4Tu%2FbtsQRHvIaII%2Fqsku6j2KMwdoZaBxpkc7D0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1192&quot; height=&quot;635&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;635&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k8s의 private network에 cloudflared를 띄워, cloudflare의 global network과 outbound tunnel을 맺음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 맺어진 터널은 외부에서 접근하는 트래픽이 cloudflare를 거쳐 cloudflared가 private 네트워크로 프록시하는데 쓰임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;k8s에 cloudflared 띄우기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: cloudflared
  namespace: edge
spec:
  replicas: 1
  selector:
    matchLabels: { app: cloudflared }
  template:
    metadata:
      labels: { app: cloudflared }
    spec:
      containers:
      - name: cloudflared
        image: cloudflare/cloudflared:latest
        args: [&quot;tunnel&quot;,&quot;--no-autoupdate&quot;,&quot;run&quot;]
        env:
        - name: TUNNEL_TOKEN
          valueFrom:
            secretKeyRef:
              name: cloudflared-token
              key: token
        resources:
          requests: { cpu: &quot;50m&quot;, memory: &quot;64Mi&quot; }
          limits:   { cpu: &quot;500m&quot;, memory: &quot;256Mi&quot; }
        securityContext:
          runAsNonRoot: true
          readOnlyRootFilesystem: true
      restartPolicy: Always&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cloudflare 대시보드에서 도메인 -&amp;gt; 타겟 서비스 설정&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2302&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uBHz6/btsQSsSmH7c/MIfbqCYYxoUm1tlXEZfSnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uBHz6/btsQSsSmH7c/MIfbqCYYxoUm1tlXEZfSnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uBHz6/btsQSsSmH7c/MIfbqCYYxoUm1tlXEZfSnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuBHz6%2FbtsQSsSmH7c%2FMIfbqCYYxoUm1tlXEZfSnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2302&quot; height=&quot;832&quot; data-origin-width=&quot;2302&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;바로 service로 연결해도 되는데, 중간에 traefik을 배치해서 IngressRoute를 통해서 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 배포된 deployment에 해당 내용&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# edge 네임스페이스에 배포
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: n8n
  namespace: edge
spec:
  entryPoints: [&quot;web&quot;]                           # 터널 &amp;rarr; Traefik(HTTP)
  routes:
  - match: Host(`n8n.cocopam.dev`)
    kind: Rule
    services:
    - name: n8n
      namespace: tools                  
      port: 5678
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트래픽 모니터링&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;1250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsnvZr/btsQSvhd4At/qdFj86Yo7pmvb3BxKIDaD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsnvZr/btsQSvhd4At/qdFj86Yo7pmvb3BxKIDaD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsnvZr/btsQSvhd4At/qdFj86Yo7pmvb3BxKIDaD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsnvZr%2FbtsQSvhd4At%2FqdFj86Yo7pmvb3BxKIDaD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1266&quot; height=&quot;1250&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;1250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;cilium hubble을 통해서 트래픽이 흐르는 것을 확인해보면, 외부 Cloud network outbound tunnel이 맺어진 것을 확인할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 cloudflared로 들어온 트래픽을 traefik을 통해 타겟 서비스로 넘겨줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cloudflared가 바라보는 주소는 kube-dns상에 있는 서비스 domain name이라서 해당 dns resolve 과정도 확인&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1796&quot; data-origin-height=&quot;1480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAXnj0/btsQSvVOQ6Z/sxHxWvmY6RNsALjGtTsom0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAXnj0/btsQSvVOQ6Z/sxHxWvmY6RNsALjGtTsom0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAXnj0/btsQSvVOQ6Z/sxHxWvmY6RNsALjGtTsom0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAXnj0%2FbtsQSvVOQ6Z%2FsxHxWvmY6RNsALjGtTsom0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1796&quot; height=&quot;1480&quot; data-origin-width=&quot;1796&quot; data-origin-height=&quot;1480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 외부 트래픽이 ingress를 통해서 들어오는게 아니라 내부의 cloudflared로부터 들어오기때문에 Ingress traffic에 대한 모니터링이 안됌&lt;/p&gt;</description>
      <category>Infra</category>
      <author>꼬꼬팜</author>
      <guid isPermaLink="true">https://cocopam.tistory.com/52</guid>
      <comments>https://cocopam.tistory.com/52#entry52comment</comments>
      <pubDate>Sat, 27 Sep 2025 16:59:04 +0900</pubDate>
    </item>
    <item>
      <title>SSL 인증서</title>
      <link>https://cocopam.tistory.com/51</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Background&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;용어 정리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증 (Authenticate) : 해당 사용자가 누구인지 신원을 확인&lt;/li&gt;
&lt;li&gt;인가 (Authorization) : 해당 사용자가 어떤 권한을 가지는 확인하고 부여&lt;/li&gt;
&lt;li&gt;대칭키 (Symmetric Key) : 암호, 복호에 같은 키를 사용&lt;/li&gt;
&lt;li&gt;비대칭키 (Asymmetric Key) : 암호, 복호에 다른 키를 사용&lt;/li&gt;
&lt;li&gt;Root CA : 신뢰가능한 인증서 발급 기관 (GeoTrust)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인증서는 왜 쓰는건가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 인터넷망을 통해 패킷을 주고받는 과정에서 다음과 같은 문제가 발생할 수 있음&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;L2 스위칭, L3 라우팅 중에 패킷을 훔쳐본다면?&lt;/li&gt;
&lt;li&gt;Dst 주소가 정상적인 목적지가 아니라면?&lt;/li&gt;
&lt;li&gt;중간에 패킷이 변조된다면?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기밀성(Confidentiality), 무결성(Integrity)을 보장하기 위해 통신 프로토콜에 보안 계층을 추가&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSL/TLS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L4 layer에서는 암호화나 보안 기능을 제공하지 않음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L4위에 SSL (Secure Sockets Layer)를 통해서 TCP 세션을 암호화 및 인증을 진행함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(L3, UDP 는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://cocopam.tistory.com/48&quot;&gt;IPSec&lt;/a&gt;을 적용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS는 이전의 암호화 프로토콜 &lt;a href=&quot;https://www.cloudflare.com/learning/ssl/what-is-ssl/&quot;&gt;SSL&lt;/a&gt;에서 발전한 버전으로, 실제로 현재는 TLS가 표준으로 사용되고 있음. 다만 개념적으로 용어를 SSL과 섞어서 사용&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TLS handshake&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2918&quot; data-origin-height=&quot;1667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cy7QtA/btsQAo4iatH/sufxG84Ok7diqWYAE9Ztk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cy7QtA/btsQAo4iatH/sufxG84Ok7diqWYAE9Ztk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cy7QtA/btsQAo4iatH/sufxG84Ok7diqWYAE9Ztk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcy7QtA%2FbtsQAo4iatH%2FsufxG84Ok7diqWYAE9Ztk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2918&quot; height=&quot;1667&quot; data-origin-width=&quot;2918&quot; data-origin-height=&quot;1667&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;TCP 3-way handshake 진행&lt;/li&gt;
&lt;li&gt;ClientHello: 클라이언트가 암호 스위트, TLS 버전, 랜덤 값 전달&lt;/li&gt;
&lt;li&gt;ServerHello: 서버가 암호 스위트 선택, 인증서(공개키 포함) 전달&lt;/li&gt;
&lt;li&gt;클라이언트는 인증서 체인을 Root CA까지 검증 &amp;rarr; 서버 공개키 유효성 확인&lt;/li&gt;
&lt;li&gt;(TLS 1.2) RSA 또는 ECDHE로 pre-master secret 교환&lt;/li&gt;
&lt;li&gt;양측은 pre-master secret + 랜덤 값으로 session key 생성&lt;/li&gt;
&lt;li&gt;session key로 이후 패킷 암호화&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인증서 생성 및 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 서버(웹서버, LB, ..)에 SSL 인증서를 등록하고 이를 활용함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;letsencrypt 와 같은 오픈소스도구를 사용하면 해당 과정을 통해서 인증서를 발급 받음&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;key pair 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;private key / public key&lt;/b&gt; pair 생성&lt;/li&gt;
&lt;li&gt;private key는 서버 내부에만 저장, public key는 인증서&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CSR (Certificate Signing Request) 생성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;public key + 도메인 정보(CN=example.com 등)를 포함한 요청 생성&lt;/li&gt;
&lt;li&gt;이 CSR을 CA(Certificate Authority)에 제출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CA의 서명&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CA는 신청자의 도메인 소유권을 검증(DNS TXT 레코드, 이메일, HTTP 파일 ..)&lt;/li&gt;
&lt;li&gt;검증이 끝나면 CA는 서버의 public key에 &lt;b&gt;자신의 private key로 서명&lt;/b&gt;한 인증서를 발급&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버에 인증서 등록&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 CA가 서명한 인증서(=서버 인증서)와 private key를 함께 보관&lt;/li&gt;
&lt;li&gt;필요하다면 중간 인증서(Intermediate CA) 체인도 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cocopam.tistory.com/43&quot;&gt;https://cocopam.tistory.com/43&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1758007325651&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;certbot wildcard 인증서 갱신&quot; data-og-description=&quot;ACME dns-01Let&amp;rsquo;s Encrypt는 도메인 소유권을 검증하기 위해 여러 인증 방식을 지원그중 dns-01 challenge는 다음 절차를 따름 인증 서버가 무작위 토큰을 발급함Certbot은 토큰과 계정 키를 조합해 CERTBOT_VAL&quot; data-og-host=&quot;cocopam.tistory.com&quot; data-og-source-url=&quot;https://cocopam.tistory.com/43&quot; data-og-url=&quot;https://cocopam.tistory.com/43&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cTLg8Q/hyZJNFivFF/qaKpWoN61AcBNlnXSUfKLK/img.png?width=800&amp;amp;height=490&amp;amp;face=0_0_800_490,https://scrap.kakaocdn.net/dn/WdbAa/hyZJa9Wr7G/gGHHAyAy4KnUw7t570EJt0/img.png?width=800&amp;amp;height=490&amp;amp;face=0_0_800_490,https://scrap.kakaocdn.net/dn/entvhr/hyZJEhhH9h/4Cz4LjuZH5cQqwCqolTPrk/img.png?width=832&amp;amp;height=510&amp;amp;face=0_0_832_510&quot;&gt;&lt;a href=&quot;https://cocopam.tistory.com/43&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cocopam.tistory.com/43&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cTLg8Q/hyZJNFivFF/qaKpWoN61AcBNlnXSUfKLK/img.png?width=800&amp;amp;height=490&amp;amp;face=0_0_800_490,https://scrap.kakaocdn.net/dn/WdbAa/hyZJa9Wr7G/gGHHAyAy4KnUw7t570EJt0/img.png?width=800&amp;amp;height=490&amp;amp;face=0_0_800_490,https://scrap.kakaocdn.net/dn/entvhr/hyZJEhhH9h/4Cz4LjuZH5cQqwCqolTPrk/img.png?width=832&amp;amp;height=510&amp;amp;face=0_0_832_510');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;certbot wildcard 인증서 갱신&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ACME dns-01Let&amp;rsquo;s Encrypt는 도메인 소유권을 검증하기 위해 여러 인증 방식을 지원그중 dns-01 challenge는 다음 절차를 따름 인증 서버가 무작위 토큰을 발급함Certbot은 토큰과 계정 키를 조합해 CERTBOT_VAL&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cocopam.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인증서 계층&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷에서 쓰이는 인증서는 단일 구조가 아니라 계층적으로 신뢰를 이어가는 구조를 가짐. 이를 &lt;b&gt;Chain of Trust&lt;/b&gt;라고 부름.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGLItt/btsQCxljkbU/T5IYE2xLUEAS1m3BaFWy9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGLItt/btsQCxljkbU/T5IYE2xLUEAS1m3BaFWy9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGLItt/btsQCxljkbU/T5IYE2xLUEAS1m3BaFWy9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGLItt%2FbtsQCxljkbU%2FT5IYE2xLUEAS1m3BaFWy9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;654&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Root CA (최상위 인증 기관)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신뢰의 최상단에 위치&lt;/li&gt;
&lt;li&gt;전 세계적으로 공신력 있는 기관들이 운영, 브라우저/운영체제에 기본 내장됨&lt;/li&gt;
&lt;li&gt;보안을 위해 Root CA는 직접 서버 인증서를 발급하지 않고 대부분 Intermediate CA를 통해 위임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Intermediate CA (중간 인증 기관)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Root CA로부터 서명받은 인증서로 서버 인증서를 발급하는 역할&lt;/li&gt;
&lt;li&gt;만약 보안 사고가 발생하더라도 Root CA를 보호하기 위한 중간 단계&lt;/li&gt;
&lt;li&gt;실제 서버가 전송하는 인증서 체인에 포함되어, 클라이언트가 Root CA까지 신뢰를 이어갈 수 있게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Server Certificate (서버 인증서)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 서비스 도메인(CN=example.com)에 발급되는 인증서&lt;/li&gt;
&lt;li&gt;서버 public key + 도메인 정보 포함&lt;/li&gt;
&lt;li&gt;Intermediate CA의 private key로 서명됨&lt;/li&gt;
&lt;li&gt;TLS handshake 과정에서 클라이언트에게 제시되는 인증서가 바로 이 부분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 서버와 TLS 연결을 맺을 때는 다음 순서로 검증이 진행됨&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버가 자신의 &lt;b&gt;서버 인증서&lt;/b&gt;와 함께 &lt;b&gt;Intermediate CA 인증서&lt;/b&gt;를 전송&lt;/li&gt;
&lt;li&gt;클라이언트는 서버 인증서를 확인하고 &amp;rarr; ICA가 서명했는지 검증&lt;/li&gt;
&lt;li&gt;ICA 인증서를 확인하고 &amp;rarr; Root CA가 서명했는지 검증&lt;/li&gt;
&lt;li&gt;Root CA가 로컬(브라우저/OS)에 내장된 신뢰 저장소에 존재하는지 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;client 패킷 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;https 요청을 보낼때, 어떤 과정이 일어나는지 확인&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;TCP handshake&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1323&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baOLK5/btsQBZJegG1/z6w4pcFOFE501d8bElGnu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baOLK5/btsQBZJegG1/z6w4pcFOFE501d8bElGnu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baOLK5/btsQBZJegG1/z6w4pcFOFE501d8bElGnu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaOLK5%2FbtsQBZJegG1%2Fz6w4pcFOFE501d8bElGnu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1323&quot; height=&quot;768&quot; data-origin-width=&quot;1323&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;hello 이후, 서버 인증서 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;991&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v4u8M/btsQBDGsCqe/jz2sJgktEhJqBkUA5Tm2g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v4u8M/btsQBDGsCqe/jz2sJgktEhJqBkUA5Tm2g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v4u8M/btsQBDGsCqe/jz2sJgktEhJqBkUA5Tm2g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv4u8M%2FbtsQBDGsCqe%2Fjz2sJgktEhJqBkUA5Tm2g1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1312&quot; height=&quot;991&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;991&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dV5onx/btsQzaMaxc9/tADoKy0YNd8aMsnGWn1FZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dV5onx/btsQzaMaxc9/tADoKy0YNd8aMsnGWn1FZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dV5onx/btsQzaMaxc9/tADoKy0YNd8aMsnGWn1FZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdV5onx%2FbtsQzaMaxc9%2FtADoKy0YNd8aMsnGWn1FZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;868&quot; height=&quot;167&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;issuer의 정보를 보고 상위 인증서를 찾음&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beqI0r/btsQA7HIFRB/RE07kQvCLmmHADqLkRRVO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beqI0r/btsQA7HIFRB/RE07kQvCLmmHADqLkRRVO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beqI0r/btsQA7HIFRB/RE07kQvCLmmHADqLkRRVO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeqI0r%2FbtsQA7HIFRB%2FRE07kQvCLmmHADqLkRRVO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2122&quot; height=&quot;746&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;브라우저에서는 해당 인증서를 로컬에서 확인 (여기서는 바로 root)&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;client는 Root까지 확인한 이후, 키 교환 방식 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1305&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cldg50/btsQzOa39Gj/3sQYbeFaRRk58tHTpypIbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cldg50/btsQzOa39Gj/3sQYbeFaRRk58tHTpypIbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cldg50/btsQzOa39Gj/3sQYbeFaRRk58tHTpypIbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcldg50%2FbtsQzOa39Gj%2F3sQYbeFaRRk58tHTpypIbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1305&quot; height=&quot;928&quot; data-origin-width=&quot;1305&quot; data-origin-height=&quot;928&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 session key 생성 과정은 생략&lt;/p&gt;</description>
      <category>Infra</category>
      <author>꼬꼬팜</author>
      <guid isPermaLink="true">https://cocopam.tistory.com/51</guid>
      <comments>https://cocopam.tistory.com/51#entry51comment</comments>
      <pubDate>Tue, 16 Sep 2025 16:26:22 +0900</pubDate>
    </item>
    <item>
      <title>Docker로 알아보는 Linux network 가상화</title>
      <link>https://cocopam.tistory.com/50</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;docker engine에서 네트워크를 어떻게 구성하고, 컨테이너에 연결하는지 정리&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제 확인&lt;br /&gt;&lt;br /&gt;Docker는 기본적으로 Linux의 커널 기능을 통해 컨테이너 기술들을 구현. 때문에 Mac, Window에서는 VM위에 docker를 돌리기에, host상에서는 network 인터페이스, 컴퓨팅 리소스가 안보일 수 있음&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 해당 실험은 Ubuntu 환경의 물리 서버에서 진행&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker Network&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 네트워크 드라이버는 다음과 같이 설정할 수 있음&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pxljF/btsQmOCgj8T/ratX7aqSU9LedZtKy3Bri1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pxljF/btsQmOCgj8T/ratX7aqSU9LedZtKy3Bri1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pxljF/btsQmOCgj8T/ratX7aqSU9LedZtKy3Bri1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpxljF%2FbtsQmOCgj8T%2FratX7aqSU9LedZtKy3Bri1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;617&quot; height=&quot;420&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;bridge&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; : 기본 드라이버. 컨테이너 veth &amp;harr; docker0 (브리지) &amp;harr; host NIC. 호스트는 docker0 IP로 게이트웨이 역할&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;host&lt;/b&gt; : 컨테이너가 host 네트워크 stack을 그대로 공유. 별도 네트워크 네임스페이스 없음, 컨테이너가 host NIC를 직접 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;none&lt;/b&gt; : 네트워크 연결 없음. 컨테이너 네임스페이스에 NIC만 있고 라우팅 없음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;overlay&lt;/b&gt; : VXLAN 기반으로 여러 host의 Docker 데몬을 하나의 L2 네트워크처럼 묶음. Docker Swarm/K8s 등에서 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ipvlan&lt;/b&gt; : 컨테이너가 host NIC와 동일한 L2 도메인에서 IP를 직접 받음. bridge 불필요, VLAN 기반으로 L3 라우팅&lt;/li&gt;
&lt;li&gt;&lt;b&gt;macvlan&lt;/b&gt; : 컨테이너가 고유한 MAC 주소를 받아 host NIC를 스위치에 &amp;ldquo;물리 장비 여러 대&amp;rdquo;처럼 보이게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 default인 bridge 네트워크에 대해 정리&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-size: 1.44em; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;Linux Network Interface&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스에서 &lt;b&gt;네트워크 인터페이스(Network Interface)&lt;/b&gt; 는 네트워크 계층에서 데이터를 송수신하기 위해 커널이 제공하는 논리적 장치&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하드웨어/소프트웨어 구현에 따라 물리 인터페이스와 가상 인터페이스로 나뉨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;물리 인터페이스 (Physical NIC)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버나 PC에 장착된 실제 네트워크 카드 (ex. eth0, enp2s0)&lt;/li&gt;
&lt;li&gt;하드웨어 MAC 주소를 가지며 스위치/라우터 같은 물리 네트워크 장비와 직접 연결&lt;/li&gt;
&lt;li&gt;외부 네트워크와 통신하기 위한 기본 경로&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가상 인터페이스 (Virtual NIC)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어적으로 여러 종류의 가상 인터페이스를 제공&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;loopback (lo)&lt;/b&gt;: 자기 자신과 통신하기 위한 인터페이스. 항상 127.0.0.1 로 연결됨.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;bridge (docker0, br-xxxxxx)&lt;/b&gt;: 소프트웨어 스위치 역할. 여러 NIC을 묶어 같은 네트워크 세그먼트에 두는 기능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;veth&lt;/b&gt;: 가상 이더넷 페어(링크). 물리적인 이더넷 연결 가상화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;tun/tap&lt;/b&gt;: 사용자 공간과 커널 네트워크 스택을 연결하는 가상 장치. VPN에서 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;macvlan/ipvlan&lt;/b&gt;: 가상으로 별도의 MAC/IP를 가진 인터페이스를 호스트 NIC 위에 올려서, 컨테이너가 외부 네트워크에 직접 붙은 것처럼 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IP&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 인터페이스는 &lt;b&gt;IP 주소&lt;/b&gt;, &lt;b&gt;netmask&lt;/b&gt;, &lt;b&gt;gateway&lt;/b&gt; 등을 설정 가능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물리 NIC: 외부 네트워크와 직접 연결되므로 ISP나 내부 DHCP 서버에서 IP를 받음&lt;/li&gt;
&lt;li&gt;가상 NIC: Docker나 K8s, VPN 같은 소프트웨어가 IPAM(IP Address Management)을 통해 주소를 할당&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실험&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;services:
  # default bridge(docker0)에 붙는 컨테이너
  web-default:
    image: nginx:alpine
    container_name: web-default
    restart: unless-stopped
    # 로컬에서 바로 보기 쉽게 포트 하나만 매핑
    ports:
      - &quot;18080:80&quot;

  # 사용자 정의 bridge(br-xx)에 붙는 컨테이너 1
  web-bridge-1:
    image: nginx:alpine
    container_name: web-bridge-1
    restart: unless-stopped
    networks:
      br_xx:
        ipv4_address: 172.30.0.10

  # 사용자 정의 bridge(br-xx)에 붙는 컨테이너 2
  web-bridge-2:
    image: nginx:alpine
    container_name: web-bridge-2
    restart: unless-stopped
    networks:
      br_xx:
        ipv4_address: 172.30.0.11
networks:
  br_xx:
    driver: bridge
    ipam:
      config:
        - subnet: 172.30.0.0/16
          gateway: 172.30.0.1

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;네트워크 구성도&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;1386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T9Pu3/btsQpz4yHom/cqc57CzrC6ZFTnxWY0fKvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T9Pu3/btsQpz4yHom/cqc57CzrC6ZFTnxWY0fKvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T9Pu3/btsQpz4yHom/cqc57CzrC6ZFTnxWY0fKvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT9Pu3%2FbtsQpz4yHom%2Fcqc57CzrC6ZFTnxWY0fKvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1990&quot; height=&quot;1386&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;1386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도의 네트워크를 지정하지 않는 컨테이너는 default bridge 네트워크 docker0에 붙여서 연결&lt;/li&gt;
&lt;li&gt;컨테이너간 네트워크 연결은 bridge interface를 통해 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;host의 인터페이스 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;cocopam@soyo:~/docker-network-test$ ifconfig
br-cbbf334f7335: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 1500
        inet 172.30.0.1  netmask 255.255.0.0  broadcast 172.30.255.255
        inet6 fe80::8f8:f4ff:fec7:cb37  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether 0a:f8:f4:c7:cb:37  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

docker0: flags=4099&amp;lt;UP,BROADCAST,MULTICAST&amp;gt;  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::780f:6aff:fe8b:f762  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether 7a:0f:6a:8b:f7:62  txqueuelen 0  (Ethernet)
        RX packets 13349  bytes 10863266 (10.8 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 15979  bytes 2222681 (2.2 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

enp2s0: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 1500
        inet 192.168.0.21  netmask 255.255.255.0  broadcast 192.168.0.255
        inet6 fe80::e251:d8ff:fe19:d971  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether e0:51:d8:19:d9:71  txqueuelen 1000  (Ethernet)
        RX packets 4305101  bytes 3610932183 (3.6 GB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2058422  bytes 377432017 (377.4 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73&amp;lt;UP,LOOPBACK,RUNNING&amp;gt;  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10&amp;lt;host&amp;gt;
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 542525534  bytes 113566300139 (113.5 GB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 542525534  bytes 113566300139 (113.5 GB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth5f81027: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 1500
        inet6 fe80::e805:acff:fe5b:a1a0  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether ea:05:ac:5b:a1:a0  txqueuelen 0  (Ethernet)
        RX packets 3  bytes 126 (126.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 15  bytes 1626 (1.6 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth3931235: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 1500
        inet6 fe80::74b6:a4ff:fe8e:ffe7  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether 76:b6:a4:8e:ff:e7  txqueuelen 0  (Ethernet)
        RX packets 3  bytes 126 (126.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 16  bytes 1580 (1.5 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

vethf3e5e1e: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 1500
        inet6 fe80::3cf6:9bff:fe36:245a  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether 3e:f6:9b:36:24:5a  txqueuelen 0  (Ethernet)
        RX packets 3  bytes 126 (126.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 18  bytes 1752 (1.7 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 &lt;code&gt;br-cbbf334f7335&lt;/code&gt; 브릿지 인터페이스 생성&lt;/li&gt;
&lt;li&gt;veth 가상 이더넷 링크 3개 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;bridge에 링크된 인터페이스&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;cocopam@soyo:~/docker-network-test$ brctl show br-cbbf334f7335
bridge name            bridge id            STP enabled       interfaces
br-cbbf334f7335        8000.0af8f4c7cb37    no               veth3931235
                                                       vethf3e5e1e
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성된 브릿지에 링크된 두 개의 veth 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Host routing table 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;cocopam@soyo:~/docker-network-test$ netstat -nr
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         192.168.0.1     0.0.0.0         UG        0 0          0 enp2s0
172.17.0.0      0.0.0.0         255.255.0.0     U         0 0          0 docker0
172.30.0.0      0.0.0.0         255.255.0.0     U         0 0          0 br-cbbf334f7335
192.168.0.0     0.0.0.0         255.255.255.0   U         0 0          0 enp2s0
192.168.0.1     0.0.0.0         255.255.255.255 UH        0 0          0 enp2s0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;할당한 네트워크이 사설 대역 172.30.0.0/16 라우팅은 생성한 &lt;code&gt;br-cbbf334f733&lt;/code&gt;로 감&lt;/li&gt;
&lt;li&gt;때문에 HOST에서도 bridge를 타고 컨테이너에 접근 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨테이너 내부 인터페이스 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;cocopam@soyo:~/docker-network-test$ docker exec -it web-bridge-1 ifconfig
eth0      Link encap:Ethernet  HWaddr 7E:93:22:8D:92:C5
          inet addr:172.30.0.10  Bcast:172.30.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:18 errors:0 dropped:0 overruns:0 frame:0
          TX packets:3 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1752 (1.7 KiB)  TX bytes:126 (126.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너 내부의 eth0에 172.30.0.10가 할당되어있고, 해당 링크의 MAC이 &lt;code&gt;7E:93:22:8D:92:C5&lt;/code&gt; 인것을 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;host bridge tcp dump&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;cocopam@soyo:~$ docker exec -it web-bridge-1 ping 172.30.0.11
PING 172.30.0.11 (172.30.0.11): 56 data bytes
64 bytes from 172.30.0.11: seq=0 ttl=64 time=0.539 ms
64 bytes from 172.30.0.11: seq=1 ttl=64 time=0.189 ms
64 bytes from 172.30.0.11: seq=2 ttl=64 time=0.183 ms



cocopam@soyo:~/docker-network-test$ sudo tcpdump -i br-cbbf334f7335
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on br-cbbf334f7335, link-type EN10MB (Ethernet), snapshot length 262144 bytes
21:54:33.977616 ARP, Request who-has 172.30.0.11 tell 172.30.0.10, length 28
21:54:33.977694 ARP, Reply 172.30.0.11 is-at 7a:7c:8e:e5:dd:93 (oui Unknown), length 28
21:54:33.977710 IP 172.30.0.10 &amp;gt; 172.30.0.11: ICMP echo request, id 47, seq 0, length 64
21:54:33.977936 IP 172.30.0.11 &amp;gt; 172.30.0.10: ICMP echo reply, id 47, seq 0, length 64
21:54:34.978097 IP 172.30.0.10 &amp;gt; 172.30.0.11: ICMP echo request, id 47, seq 1, length 64
21:54:34.978184 IP 172.30.0.11 &amp;gt; 172.30.0.10: ICMP echo reply, id 47, seq 1, length 64
21:54:35.978355 IP 172.30.0.10 &amp;gt; 172.30.0.11: ICMP echo request, id 47, seq 2, length 64
21:54:35.978439 IP 172.30.0.11 &amp;gt; 172.30.0.10: ICMP echo reply, id 47, seq 2, length 64
21:54:36.978563 IP 172.30.0.10 &amp;gt; 172.30.0.11: ICMP echo request, id 47, seq 3, length 64
21:54:36.978657 IP 172.30.0.11 &amp;gt; 172.30.0.10: ICMP echo reply, id 47, seq 3, length 64
21:54:37.978790 IP 172.30.0.10 &amp;gt; 172.30.0.11: ICMP echo request, id 47, seq 4, length 64
21:54:37.978873 IP 172.30.0.11 &amp;gt; 172.30.0.10: ICMP echo reply, id 47, seq 4, length 64
21:54:38.979002 IP 172.30.0.10 &amp;gt; 172.30.0.11: ICMP echo request, id 47, seq 5, length 64
21:54:38.979089 IP 172.30.0.11 &amp;gt; 172.30.0.10: ICMP echo reply, id 47, seq 5, length 64
21:54:39.337760 ARP, Request who-has 172.30.0.10 tell 172.30.0.11, length 28
21:54:39.337843 ARP, Reply 172.30.0.10 is-at 7e:93:22:8d:92:c5 (oui Unknown)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;web-bridge-1(172.30.0.10) 에서 web-bridge-2(172.30.0.11) ping을 확인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ICMP를 위해서 IP를 MAC으로 변환하는 ARP 패킷 전송&lt;/li&gt;
&lt;li&gt;이후 web-bridge-2에서 eth0 MAC 전달 (arp reply)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;bridge mac table 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;cocopam@soyo:~/docker-network-test$ sudo bridge fdb show | grep br-cbbf334f7335
33:33:00:00:00:01 dev br-cbbf334f7335 self permanent
33:33:00:00:00:02 dev br-cbbf334f7335 self permanent
01:00:5e:00:00:6a dev br-cbbf334f7335 self permanent
33:33:00:00:00:6a dev br-cbbf334f7335 self permanent
01:00:5e:00:00:01 dev br-cbbf334f7335 self permanent
33:33:ff:c7:cb:37 dev br-cbbf334f7335 self permanent
33:33:ff:00:00:00 dev br-cbbf334f7335 self permanent
0a:f8:f4:c7:cb:37 dev br-cbbf334f7335 vlan 1 master br-cbbf334f7335 permanent
0a:f8:f4:c7:cb:37 dev br-cbbf334f7335 master br-cbbf334f7335 permanent
7e:93:22:8d:92:c5 dev vethf3e5e1e master br-cbbf334f7335
3e:f6:9b:36:24:5a dev vethf3e5e1e vlan 1 master br-cbbf334f7335 permanent
3e:f6:9b:36:24:5a dev vethf3e5e1e master br-cbbf334f7335 permanent
7a:7c:8e:e5:dd:93 dev veth3931235 master br-cbbf334f7335
76:b6:a4:8e:ff:e7 dev veth3931235 vlan 1 master br-cbbf334f7335 permanent
76:b6:a4:8e:ff:e7 dev veth3931235 master br-cbbf334f7335 permanent&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;web-bridge-1
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너 내부의 &lt;code&gt;7E:93:22:8D:92:C5&lt;/code&gt; eth0 MAC -&amp;gt; vethf3e5e1e로 포워딩&lt;/li&gt;
&lt;li&gt;veth MAC &lt;code&gt;76:b6:a4:8e:ff:e7&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;마찬가지로 web-bridge-2도 연결된 것 확인 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Ref&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.docker.com/engine/network/&quot;&gt;https://docs.docker.com/engine/network/&lt;/a&gt;&lt;/p&gt;</description>
      <category>Infra</category>
      <author>꼬꼬팜</author>
      <guid isPermaLink="true">https://cocopam.tistory.com/50</guid>
      <comments>https://cocopam.tistory.com/50#entry50comment</comments>
      <pubDate>Sat, 6 Sep 2025 22:24:09 +0900</pubDate>
    </item>
  </channel>
</rss>