--- kind: PROJECT_DECISION slug: fix-guest-addresses-with-a-dhcp-reservation title: 게스트 주소는 libvirt DHCP 예약으로 고정한다 — 바꿀 수 없어서가 아니라 되돌리기가 가장 비싸서 topic: lab-environment-build topicName: 실험대 환경 구성 project: virtualization status: 게시 전 decisionStatus: ADOPTED source: - final/document.md#247-dhcp-예약-ip-dhcp-host-과-mac-52-54-00 - final/document.md#201-네트워크-dhcp-예약의-실제-동작 - final/document.md#245-libvirt-default-네트워크와-virbr0 - final/document.md#246-dnsmasq-libvirt-내장-dhcp-dns - final/document.md#248---live---config sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6 --- # 게스트 주소는 libvirt DHCP 예약으로 고정한다 — 바꿀 수 없어서가 아니라 되돌리기가 가장 비싸서 게스트 IP 를 libvirt 의 DHCP 예약으로 묶고 예약을 넣은 다음에 게스트를 만든다. 주소가 바뀌면 k3s 가 설정 파일과 인증서에 구워 둔 IP 부터 어긋나, 고치는 값이 한 곳이 아니라 재발급이나 재설치가 된다. ## 근거 - **cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다** 이 예약을 실제로 넣는 명령이 그 절차에 있고, DHCP 예약이 `virt-install` 보다 먼저여야 한다는 순서도 거기서 지킨다. - **k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다** 고정한 `192.168.122.11` 과 `192.168.122.12` 를 그대로 받아 쓰는 절차다. `--node-ip` 와 `--tls-san` 과 agent 의 `K3S_URL` 이 그 주소를 담는다. - **엣지 게스트에 nginx 를 세우고 호스트 DNAT 으로 밖에서 들어오는 길을 연다** nginx 의 `upstream` 이 뒤쪽 게스트 주소를 적는 곳이다. 주소가 바뀌면 reload 전까지 502 가 이어진다. - **호스트에 직접 깔지 않고 게스트 VM 두 대로 간다 — 관측자가 실험 대상과 함께 죽으면 안 된다** 이 결정이 서 있는 바닥이다. 게스트를 반복해 죽이는 실험이 전제라서 주소 고정이 필요해졌다. - **도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다** `net-dumpxml` 의 예약과 `net-dhcp-leases` 의 리스가 서로 다른 것을 말한다는 것을 그 기준이 일반화한다. - **L7 이 두 겹인 이유와, 진입점 하나를 이중화하려 할 때 끝나지 않는 재귀** nginx 가 뒤쪽 노드를 주소로 가리키는 구조를 그 글이 설명한다. 여기서 고정한 주소가 그 upstream 에 적힌다. ## 결정문 게스트의 IP 는 libvirt `default` 네트워크의 DHCP 예약(`ip-dhcp-host`)으로 고정한다. 게스트 안에서 static IP 를 잡지 않고, MAC 은 QEMU/KVM 에 할당된 `52:54:00` 대역을 쓴다. 예약을 먼저 넣고 그다음에 `virt-install` 로 게스트를 만든다. 2026-09-10 에 실제로 돌린 결과가 그렇게 나왔다. `virsh net-update default add ip-dhcp-host ... --live --config` 이 `Updated network default persistent config and live state` 를 냈고, 예약을 먼저 넣고 만든 게스트가 첫 부팅에서 바로 `192.168.122.10` 을 받았다. ## 판단 이유 §247 이 인과 순서를 직접 못박아 두었다. upstream 에 IP 를 박으려고 예약을 거는 것이 아니다. 고정 주소가 필요한 이유가 여럿이고 그것을 충족하는 수단이 DHCP 예약이며, 그렇게 얻은 주소를 upstream 에도 적는 순서다. 고정이 필요한 이유를 §247 이 중요도 순으로 넷 든다. k3s 가 IP 를 설정 파일과 인증서에 굽는다 : `--node-ip` 와 `--tls-san`, agent 의 `K3S_URL=https://192.168.122.11:6443`, kubeconfig 의 `server:` 필드가 전부 IP 를 담는다. server 노드의 IP 가 바뀌면 agent 가 클러스터에 합류하지 못한다. API 서버 인증서의 SAN 도 어긋나서 재발급이나 재설치가 필요해진다. 되돌리기가 가장 비싼 항목이다 nginx 는 upstream 주소를 기동 시점에 한 번만 해석한다 : 오픈소스판은 `upstream` 블록의 이름을 설정 로드 때 풀고 런타임에 다시 조회하지 않는다. 그래서 뒤쪽 IP 가 바뀌면 reload 전까지 계속 502 다. 다시 조회하게 하려면 `resolver` 와 변수 조합을 쓰거나 상용판이 필요하다 VM 을 반복해서 죽이는 것이 실험 그 자체다 : `virsh destroy` 로 노드 상실을 재현하는데 되살릴 때마다 주소가 달라지면 실험이 성립하지 않는다 장애 주입 규칙이 주소 기반이다 : 「kc-lab-2 로 가는 7800 을 막아라」에서 IP 가 어긋나면 조용히 엉뚱한 것을 막는다. 실패가 드러나지 않아 특히 위험하다 수단은 셋을 견줬고 §247 이 표로 적었다. 게스트 안에서 static IP 를 설정하면 cloud-init 이 복잡해지고 libvirt 는 그 사실을 모르므로 주소 설정이 두 곳으로 흩어진다. upstream 에 호스트명을 쓰면 libvirt 의 dnsmasq 가 이름을 풀어 주기는 한다. 다만 호스트의 리졸버가 `virbr0` 를 바라봐야 하고, 기동 시 한 번만 해석한다는 둘째 문제는 이 방법으로 없어지지 않는다. DHCP 예약을 고른 까닭은 주소 관리가 libvirt 한 곳에 모이기 때문이다. 그 한 곳이 libvirt 가 네트워크마다 하나씩 띄우는 dnsmasq 이고, 예약은 네트워크 정의 XML 의 `` 요소에 들어간다. 게스트는 평범한 DHCP 클라이언트로 두면 되고, 게스트 쪽 설정 파일은 아무것도 손대지 않는다. ## 영향 값이 맞으면 조용히 되고 틀리면 더 조용히 틀린다. 그래서 치른 비용이 셋 다 「오류가 안 나는 실패」다. 순서가 결과를 바꾼다 : 예약을 넣고 나서 `virt-install` 해야 한다. 게스트를 먼저 만들면 동적 대역(`192.168.122.2`부터 `192.168.122.254`)에서 아무 주소나 받고, 예약이 먹으려면 리스 만료를 기다리거나 재부팅해야 한다 MAC 이 한 글자만 달라도 조용히 무시된다 : 예약의 `mac` 과 `virt-install --network` 에 준 `mac=` 이 정확히 같아야 한다. 다르면 오류 메시지 없이 동적 범위에서 아무 주소나 받고, 증상은 「왜 IP 가 다르지」로만 나타난다 플래그 둘을 다 줘야 한다 : `--live` 만 주면 재부팅에 사라지고 `--config` 만 주면 지금 반영이 안 된다. 성공 판정은 출력에 `persistent config` 와 `live state` 두 마디가 다 나오는 것이고, 한쪽만 나온 것을 성공으로 읽으면 「재부팅하니 IP 가 바뀐다」가 된다 예약을 조회하는 두 명령도 서로 다른 것을 말한다. `net-dumpxml` 이 보여 주는 예약은 dnsmasq 에게 준 의도이고, `net-dhcp-leases` 가 보여 주는 리스는 실제로 나간 기록이다. 둘이 다를 수 있으므로 예약을 넣었다는 것만으로 게스트가 그 주소를 받았다고 읽지 않는다. 동적 범위가 예약 주소를 품고 있는 것은 이대로 두었다. 현재 범위 `192.168.122.2`부터 `192.168.122.254` 안에 `192.168.122.11` 과 `192.168.122.12` 가 들어간다. 그래도 dnsmasq 는 정적으로 예약된 주소를 다른 클라이언트에게 내주지 않아 정상 동작한다. 더 방어적으로 가려면 범위를 `192.168.122.100`부터 `192.168.122.254` 로 좁혀 예약 대역과 나눈다. 네트워크가 비활성일 때는 `--live` 를 쓸 수 없다. 그때는 `--config` 만 주고 네트워크를 시작한다.