[OpenStack] provider 네트워크 VM 직결이 'Binding failed'로 실패할 때 — kolla 컴퓨트 bridge_mappings 점검
kolla-ansible로 배포한 OpenStack(ML2/OVS, VXLAN 테넌트망)에서 "provider 네트워크를 VM에 직결"하려다 막힌 기록이다. 라우터 게이트웨이와 Floating IP는 멀쩡히 동작하는데, 같은 provider 네트워크를 VM의 두 번째 NIC로 붙이는 순간 인스턴스가 ERROR로 떨어졌다. 권한도 IP 풀도 정상이었고, 원인은 kolla의 설정 키 하나였다. 이 글은 증상에서 원인까지의 진단 경로와, 컴퓨트 전 노드에 안전하게 적용한 절차, 그 과정에서 밟은 kolla-ansible 운영 함정을 정리한다.
배경
구성은 흔한 kolla 배포다. 컨트롤 플레인 3대가 kolla 인벤토리의 [control]과 [network] 그룹을 겸하고, 컴퓨트 노드가 십여 대 있다. 외부망은 flat provider 네트워크 하나(physnet1)이며 router:external=True, shared=False다. 테넌트는 각자 라우터를 이 외부망에 물리고, 외부 접속은 Floating IP나 port forwarding으로 처리해 왔다.
그러다 한 프로젝트에서 "VM에 provider 네트워크를 직접 붙여 달라"는 요구가 왔다. HAProxy 같은 진입점 VM이 NAT를 거치지 않고 provider 주소를 그대로 갖는 구성이다. Neutron RBAC로 그 프로젝트에만 access_as_shared를 걸어 포트 생성을 허용했고, 여기까지는 계획대로였다.
openstack network rbac create --type network --action access_as_shared \
--target-project <project-id> public
증상 — 스케줄러가 열 번을 돌고 포기한다
프로젝트 멤버 계정으로 테넌트망 NIC와 provider NIC를 하나씩 단 VM을 만들었다. 결과는 ERROR, fault 메시지는 이랬다.
Exceeded maximum number of retries. Exceeded max scheduling attempts 10
for instance <uuid>. Last exception: Binding failed for port <port-uuid>,
please check neutron logs for more information.
"열 번 시도"가 힌트였다. 스케줄러는 후보 호스트를 바꿔 가며 재시도했고, 어느 호스트에서도 포트 바인딩이 성립하지 않았다. 특정 노드의 문제가 아니라 컴퓨트 노드 전체에 공통된 무엇이 빠져 있다는 뜻이다.
먼저 의심한 것은 권한과 풀이었다. RBAC 없이 같은 요청을 보내면 Neutron이 명확한 403을 준다.
{"NeutronError": {"type": "HTTPForbidden",
"message": "Tenant <project-id> not allowed to create port on this network"}}
그런데 이번에는 포트가 만들어졌고 주소도 풀 안에서 정상 할당됐다. 포트가 생기고 난 뒤의 실패, 즉 바인딩 단계의 실패다. 바인딩은 ML2 메커니즘 드라이버가 "이 호스트에 이 네트워크 타입을 실을 수 있는가"를 판정하는 단계이고, flat/VLAN 네트워크라면 그 호스트의 OVS 에이전트가 해당 physnet을 브리지에 매핑하고 있어야 한다.
진단 — 에이전트 설정을 호스트별로 뽑아 본다
openstack network agent show의 configuration 필드에 각 에이전트의 bridge_mappings가 들어 있다. 전 호스트를 한 번에 훑었다.
openstack network agent list --agent-type open-vswitch -f value -c Host -c ID |
while read host id; do
printf '%s ' "$host"
openstack network agent show "$id" -f json -c configuration |
python3 -c 'import json,sys; c=json.load(sys.stdin)["configuration"]; print(c.get("bridge_mappings"))'
done
결과는 한눈에 갈렸다.
compute-01 {}
compute-02 {}
...
compute-16 {}
network-01 {'physnet1': 'br-ex'}
network-02 {'physnet1': 'br-ex'}
network-03 {'physnet1': 'br-ex'}
physnet1 매핑이 네트워크 노드 3대에만 있고 컴퓨트 노드는 전부 비어 있었다. 이러면 라우터 게이트웨이 포트(네트워크 노드에서 바인딩)는 되고, VM 포트(컴퓨트 노드에서 바인딩)는 안 된다. 증상과 정확히 맞는다.
왜 비어 있는가. kolla-ansible의 neutron 롤 템플릿을 열어 보면 조건이 그대로 적혀 있다.
{# roles/neutron/templates/openvswitch_agent.ini.j2 #}
{% if inventory_hostname in groups["network"]
or (inventory_hostname in groups["compute"] and computes_need_external_bridge | bool) %}
bridge_mappings = {{ neutron_physical_networks.split(',') | zip(neutron_bridge_name.split(',')) | map('join', ':') | join(',') }}
{% endif %}
computes_need_external_bridge는 group_vars/all/neutron.yml에서 파생되는 값이고, DVR을 켜거나 enable_neutron_provider_networks가 참일 때만 참이 된다. 기본값은 false다. 같은 조건이 openvswitch 롤의 post-config에도 걸려 있어서, 이 값이 거짓이면 컴퓨트 노드에는 br-ex 브리지 자체가 만들어지지 않고 외부 인터페이스도 어디에도 편입되지 않는다.
인벤토리의 host_vars에는 컴퓨트마다 neutron_external_interface가 이미 정의돼 있었다. 배선과 설계는 준비돼 있었고 스위치만 꺼져 있던 셈이다. 실제로 그 인터페이스들이 provider 세그먼트에 물려 있는지는 ARP 방청으로 확인했다. IP가 없는 포트라 ping은 못 하지만, 세그먼트의 ARP 트래픽은 들린다.
sudo timeout 25 tcpdump -nni <external-iface> -c 40 arp 2>/dev/null |
grep -oE '([0-9]+\.){3}' | sort | uniq -c
16대 전부에서 provider 서브넷 프리픽스의 ARP만 관측됐다. 다른 대역이 섞여 나오면 그 포트는 엉뚱한 VLAN에 물린 것이므로, 이 확인은 매핑을 켜기 전에 반드시 거쳐야 한다.
해결 — 키 하나와 태그 두 개
수정은 globals에 한 줄이다.
# globals.yml
enable_neutron_provider_networks: "yes"
적용은 reconfigure에 openvswitch와 neutron 태그를 함께 준다. br-ex 생성과 외부 인터페이스 편입은 openvswitch 롤의 post-config가, bridge_mappings 렌더링과 OVS 에이전트 컨테이너 재생성은 neutron 롤이 맡기 때문에 둘 다 필요하다.
# 파일럿 1대
kolla-ansible reconfigure -i <inventory> --tags openvswitch,neutron --limit compute-05
# 확인 후 나머지 전체 (파킹 중인 노드는 제외)
kolla-ansible reconfigure -i <inventory> --tags openvswitch,neutron --limit 'compute:!compute-10'
파일럿 노드부터 갔다. 사전에 잡아 둔 기준선과 대조한 결과는 다음과 같았다.
| 항목 | 적용 전 | 적용 후 |
|---|---|---|
ovs-vsctl list-ports br-ex |
브리지 없음 | <external-iface>, phy-br-ex |
에이전트 bridge_mappings |
{} |
{'physnet1': 'br-ex'} |
neutron_openvswitch_agent 컨테이너 |
유지 | 재생성(started_at 갱신) |
openvswitch_db·openvswitch_vswitchd 컨테이너 |
유지 | 유지(StartedAt 불변) |
| 기존 VM 상태 분포 | 54 ACTIVE / 3 SHUTOFF | 동일 |
OVS 데몬은 재시작되지 않았고, drop_flows_on_start가 기본값(false)이라 에이전트 재생성 중에도 기존 VM의 플로우는 유지됐다. 데이터플레인 표본(다른 프로젝트 VM의 공인 포트, 그리고 배포 명령을 내리던 세션 자체가 컴퓨트 위의 VM이었다)도 전후로 변화가 없었다.
파일럿 노드에서 admin 계정으로 실증 VM을 --host로 강제 배치해 포트를 확인했다.
openstack port show <port-id> -f yaml -c binding_vif_type -c binding_host_id -c status
# binding_host_id: compute-05
# binding_vif_type: ovs
# status: ACTIVE
바인딩이 성립한 것을 확인한 뒤 나머지 16대에 일괄 적용했고, 전 노드가 같은 결과를 냈다. 이후 프로젝트 멤버 계정으로 만든 직결 VM은 첫 시도에 ACTIVE가 됐고, 게스트 안에서 provider 주소가 올라오고 상위 NAT를 거쳐 교외에서도 443으로 닿는 것까지 확인했다.
한 가지 첨언하면, br-ex의 fail-mode는 secure로 보이는 것이 정상이다. kolla는 standalone으로 만들지만 Neutron OVS 에이전트가 물리 브리지를 secure로 재설정한다. 이걸 이상 징후로 오독하지 말 것.
밟은 함정 — kolla-ansible 운영에서 놓치기 쉬운 것들
--limit를 줘도 kolla는 인벤토리 전 호스트에 SSH한다. gather-facts.yml의 두 번째 플레이가 limit 밖 호스트 전부에 fact 수집을 delegate한다(kolla_ansible_delegate_facts_hosts가 groups['all']). fact 캐시 TTL(기본 1800초)이 만료돼 있으면 파킹해 둔 노드나 배포 호스트까지 접촉하고, 그중 한 대라도 unreachable이면 배포는 시작도 못 하고 abort된다. limit 밖 호스트가 변경되지는 않지만, 배포 직전에 ansible -i <inventory> all -m ping으로 전건 접속을 확인하는 것이 --limit 실행의 선행 조건이다.
최근 CLI는 서브커맨드가 옵션보다 앞이다. 습관대로 kolla-ansible -i <inventory> prechecks ...를 치면 이런 에러가 난다.
kolla-ansible: '-i inventory prechecks --tags openvswitch,neutron --limit compute-05' is not a kolla-ansible command.
kolla-ansible prechecks -i <inventory> --tags ... --limit ... 순서가 맞다.
긴 reconfigure는 nohup과 로그 파일로 회수한다. 배포 명령을 내리는 세션이 배포 대상 컴퓨트 위의 VM이라면 세션이 끊길 수 있다. 백그라운드로 띄우되, 기동 직후의 $?는 기동 성공 코드일 뿐 ansible의 종료 코드가 아니다. 종료 코드는 명령 뒤에 붙여 로그로 남긴다.
nohup bash -c 'kolla-ansible reconfigure -i <inventory> --tags openvswitch,neutron --limit compute-05; echo RC=$?' \
> reconfigure.log 2>&1 &
# 완료 후
grep '^RC=' reconfigure.log && grep -E 'failed=[1-9]|unreachable=[1-9]' reconfigure.log
disabled 노드에는 admin의 --host 강제 배치도 안 된다. 파일럿 노드로 "VM이 없고 스케줄러에서 빠져 있는" disabled 노드를 고르면 편할 것 같지만, placement가 그 노드의 리소스 프로바이더에 COMPUTE_STATUS_DISABLED 트레이트를 붙여 두기 때문에 allocation candidate가 0이 되어 NoValidHost가 난다. 파일럿은 enabled이면서 VM이 없는 노드로 고른다.
게스트의 provider 주소는 부팅이 끝난 뒤에 확인한다. provider 서브넷의 DHCP가 꺼져 있어도 cloud-init이 메타데이터의 network_data를 읽어 static 주소와 기본 경로를 올린다. 다만 그 시점은 cloud-init의 network 단계(부팅 후 30초 안팎)라, 생성 직후의 ping 무응답을 "주소가 안 올라온다"로 오독하기 쉽다. 파일럿에서 나도 한 번 그렇게 읽었다.
정리
- 증상: provider 네트워크를 VM에 직결하면
Binding failed for port로 스케줄링 재시도 소진. - 진단:
network agent show의bridge_mappings를 호스트별로 뽑는다. 네트워크 노드에만 매핑이 있으면 이 케이스다. - 원인: kolla
enable_neutron_provider_networks기본값false→ 컴퓨트 그룹에서 br-ex 생성과bridge_mappings렌더링이 생략된다. - 해결: 키를 켜고
reconfigure --tags openvswitch,neutron을 파일럿 1대 → 전체 순으로. OVS 데몬은 재시작되지 않고 기존 VM 플로우는 유지된다. - 선행 확인: 각 컴퓨트의 외부 인터페이스가 provider 세그먼트에 물려 있는지 ARP 방청으로 본다.
--limit배포 전ansible all -m ping. - 롤백: 키 제거 후 같은 태그로 reconfigure. br-ex 브리지와 포트 편입은 kolla가 되돌리지 않으므로 필요하면
ovs-vsctl del-br br-ex를 수동으로.
댓글
아직 댓글이 없습니다. 첫 댓글을 남겨보세요.