
구축형 OwlAI를 기업망 호스트에 설치하고 몇 주 뒤, 어제까지 붙던 사내 DB나 추론 서버에 컨테이너에서 닿지 않는 날이 올 수 있습니다. 같은 호스트의 셸에서도 닿지 않습니다. 방화벽 규칙은 그대로이고 서버도 살아 있습니다. 원인은 그 사이에 도커가 새로 만든 브리지 네트워크입니다. 도커가 사내 서버가 있는 대역을 그 네트워크의 서브넷으로 받아 호스트 라우팅 테이블에 경로를 올리면, 그 대역으로 가던 패킷이 전부 브리지로 나갑니다.
도커는 서브넷을 고를 때 호스트 라우팅 테이블에 이미 있는 대역을 피하려 합니다. 그런데 기본 게이트웨이 너머에 있는 사내 대역은 라우팅 테이블에 나타나지 않으니, 도커도 피할 수 없습니다. 설치 도구가 라우팅 테이블만 보고 판정해도 같은 이유로 놓칩니다. 그래서 구축형 OwlAI는 도커 주소 풀을 설치하는 사람이 정하게 하고, 설치 전 점검은 사람이 적어 준 사내 대역과 주소 풀을 대조합니다. 이 글은 충돌이 생기는 과정과 그 두 장치를 다룹니다.
기본 경로로 가던 사내 트래픽이 도커 브리지로 나간다
예를 들어 호스트가 10.20.0.15이고, 사내 서버 192.168.5.20에는 기본 게이트웨이를 거쳐 닿는다고 하겠습니다. 호스트 라우팅 테이블에는 default via 10.20.0.1 dev eth0과 자기 서브넷 경로만 있습니다. 도커가 192.168.0.0/20을 받아 네트워크를 만들면 그 뒤로는 이렇게 나옵니다.
$ ip route get 192.168.5.20
192.168.5.20 dev br-3f2a… src 192.168.0.1
리눅스는 목적지에 맞는 경로가 여럿이면 접두사가 가장 긴 경로를 고릅니다(가장 긴 접두사 일치). 기본 경로는 /0이고 도커 브리지 경로는 /20이라, 사내 서버로 가는 패킷이 물리 인터페이스 대신 브리지로 나갑니다. 그 /20에 드는 주소 4,096개는 이제 컨테이너에서도 호스트에서도 닿지 않습니다.
장애가 이미 났다면 ip route get <사내 서버 IP> 한 줄로 이 문제인지 가립니다. 결과가 dev br-…나 dev docker0이면 도커 네트워크가 그 대역을 차지한 것입니다. docker network inspect로 그 브리지가 어느 네트워크인지 찾고, 주소 풀을 고친 뒤 그 네트워크를 다시 만듭니다. 브리지가 같은 호스트에서 도는 다른 프로젝트의 것일 수 있으니 지우기 전에 주인부터 확인합니다.
도커는 라우팅 테이블에 있는 대역만 피한다
daemon.json에 default-address-pools를 적지 않으면 도커는 기본 풀에서 서브넷을 뗍니다. 도커 공식 문서에 따르면 기본 풀은 다음과 같고, 호스트에서 이미 쓰는 대역은 피하려 합니다.
| 풀 | 한 네트워크의 크기 | 나오는 네트워크 수 |
|---|---|---|
172.17.0.0/16, 172.18.0.0/16, 172.19.0.0/16 |
/16 |
3 |
172.20.0.0/14, 172.24.0.0/14, 172.28.0.0/14 |
/16 |
12 |
192.168.0.0/16 |
/20 |
16 |
172 대역에서 나오는 네트워크는 기본 브리지 docker0까지 합쳐 15개입니다. 그다음 네트워크부터는 192.168.0.0/16에서 나옵니다. 구축형 OwlAI의 운영 compose는 공유 외부 네트워크 하나와, 스크립트 실행·문서 렌더·파일 저장소·DB 드라이버 사이드카마다 하나씩 전용 네트워크 넷을 씁니다. 사이드카가 필요한 네트워크에만 붙게 나눈 결과입니다. 다른 네트워크가 없는 호스트라면 운영 스택을 올린 뒤 172 대역에 남는 자리는 9개입니다. 네트워크를 넷씩 만드는 compose 프로젝트가 셋 더 뜨면, 셋째 프로젝트의 둘째 네트워크부터 192.168 대역에서 나옵니다.
이 흐름 때문에 원인을 짚기 어렵습니다. 설치한 날에는 172 대역에 자리가 있어 아무 일도 없습니다. 몇 주 뒤 누군가 같은 호스트에 다른 스택을 띄운 순간 충돌이 나고, 그 사람이 바꾼 것은 자기 프로젝트의 네트워크뿐입니다. 사내 서버가 끊긴 일과 테스트 스택 하나를 띄운 일을 이어 생각하기는 쉽지 않습니다.
172 대역도 안전하지 않습니다. 기업망은 172.16.0.0/12도 흔히 쓰고, AWS 기본 VPC의 대역 172.31.0.0/16은 도커 기본 풀의 172.28.0.0/14 안에 들어 있습니다.
어느 기본 대역을 골라도 그 대역을 쓰는 고객 사내망이 있다
흔한 접근은 설치 도구가 겹칠 일이 적어 보이는 대역을 기본값으로 정해 두는 것입니다. 설치 담당자는 이 결정을 건너뛸 수 있고, 대부분의 호스트에서는 문제가 없습니다. 그런데 구축형 제품은 고객사마다 사내망이 다르고, 사설 대역 세 개(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) 가운데 어디를 쓰는지 설치 도구는 알 수 없습니다. 기본값과 겹치는 고객사에서는 설치 몇 주 뒤에 장애로 드러납니다.
설치 도구가 호스트 라우팅 테이블을 읽어 빈 대역을 고르는 방법도 있습니다. 입력 없이 돌릴 수 있다는 장점이 있지만, 도커가 놓치는 대역을 이 방법도 똑같이 놓칩니다. 기본 게이트웨이 너머, VPN 너머, 상위 라우터 뒤의 사내 대역은 라우팅 테이블에 없기 때문입니다.
사내망 전체를 아는 것은 그 회사의 네트워크 담당자뿐입니다. 그래서 대역은 사람이 정하고, 도구는 사람이 정한 값을 대조하는 일을 맡습니다.
대역은 설치하는 사람이 정하고, 준비 스크립트는 겹치면 멈춘다
구축형 OwlAI의 호스트 준비 스크립트(Ubuntu용·RHEL 계열용)는 도커 주소 풀을 필수 인자로 받습니다. 값이 없으면 시작하지 않습니다. 적는 값은 사내 라우팅, VPN 경로, 피어링된 VPC 어디에도 쓰이지 않는 대역이어야 합니다.
{
"default-address-pools": [ { "base": "10.250.0.0/16", "size": 24 } ]
}
스크립트는 이 값으로 daemon.json을 씁니다. 이렇게 적으면 도커는 10.250.0.0/16 안에서 /24씩 네트워크를 떼어 주고, 네트워크를 256개까지 만들 수 있습니다. 쓰기 전에 스크립트는 그 대역을 호스트 라우팅 테이블과 대조해 겹치면 멈춥니다. 기존 daemon.json이 원하는 내용과 다르면 덮어쓰기 전에 멈추고, 강제 옵션을 준 때에만 원본을 .bak으로 남기고 바꿉니다. 이 판정은 패키지 설치처럼 호스트를 바꾸는 단계보다 먼저 돌아서, 멈출 때는 호스트에 아무것도 바뀌지 않습니다.
주소 풀을 바꾼 뒤에는 dockerd를 재시작해야 합니다. 재시작하면 그 호스트의 모든 컨테이너가 내려갔다 올라오므로, 스크립트는 설정 파일이 실제로 바뀌었을 때만 재시작합니다. 이미 만든 네트워크는 옛 서브넷을 그대로 쓰므로 다시 만들어야 새 풀이 적용됩니다.
점검은 지금 있는 네트워크와 앞으로 내줄 풀을 따로 본다
설치 전 점검(「설치 전 점검은 호스트를 읽기만 하고, 고칠 명령은 사람에게 넘긴다」)은 배포와 업데이트 때마다 다시 돌고, 네트워크 충돌은 세 항목으로 판정합니다.
| 항목 | 판정 |
|---|---|
network.pools |
지금 있는 컨테이너 네트워크의 서브넷이 호스트 경로나 --corp-cidrs로 준 사내 대역과 겹치면 FAIL |
network.pools_future |
엔진의 주소 풀(명시값 또는 기본 풀)이 --corp-cidrs로 준 사내 대역을 앞으로 내줄 수 있으면 FAIL |
network.aws_vpc |
호스트가 AWS 기본 VPC 대역을 라우팅하고 주소 풀이 그 대역과 겹치면 WARN |
지금 있는 네트워크만 보는 점검은 단순하고 거짓 경보가 없습니다. 하지만 설치한 날에는 충돌할 네트워크가 아직 없으니, 그 점검은 몇 주 뒤의 장애를 통과시킵니다. network.pools_future는 네트워크 대신 풀을 봅니다. 기본 풀을 그대로 둔 호스트에서 --corp-cidrs에 192.168.0.0/16을 적으면, 도커가 아직 192.168 대역에서 서브넷을 하나도 떼어 주지 않았어도 설치 전에 FAIL이 나옵니다.
사내 대역은 --corp-cidrs 옵션으로 받습니다. 라우팅 테이블에 없는 대역을 대조할 방법은 사람이 적어 주는 것뿐이기 때문입니다.
tools/ops/preflight.sh --corp-cidrs 10.0.0.0/8,172.16.0.0/12
network.pools는 여기에 호스트 라우팅 테이블도 함께 대조합니다. 그래서 네트워크를 만든 뒤에 VPN이 붙거나 정적 경로가 생긴 경우도 잡힙니다. 이때 도커와 podman이 만든 경로(docker0, br-*, veth*, podman 브리지)는 대조 대상에서 뺍니다. 빼지 않으면 점검이 자기 네트워크를 사내망으로 보고 FAIL하고, 배포 때마다 도는 점검이 매번 배포를 멈춥니다. podman은 첫 컨테이너가 붙을 때 브리지 경로를 올리므로, 서비스가 하나라도 뜬 호스트라면 이 제외가 없을 때 다음 배포가 멈춥니다.
점검은 엔진마다 주소 풀을 읽는 자리를 따로 봅니다. rootful Docker는 /etc/docker/daemon.json, rootless Docker는 배포 사용자의 ~/.config/docker/daemon.json, podman은 containers.conf의 [network] 아래 default_subnet_pools를 읽습니다. podman은 이 값이 없으면 10.89.0.0/16부터 10.255.0.0/16까지를 기본 풀로 쓰므로, 사내망이 10.0.0.0/8이면 그대로 FAIL이 나옵니다. daemon.json이 있는데 읽을 수 없으면 판정할 수 없음(SKIP)으로 남깁니다. 읽지 못한 파일을 기본 풀로 간주해 판정하면 틀린 PASS나 FAIL이 나오기 때문입니다.
설계에서 고른 것
주소 풀은 기본값 대신 필수 인자로 골랐습니다. 기본값이 있으면 설치가 한 단계 짧아지고 대부분의 호스트에서는 맞습니다. 그 대신 기본값과 겹치는 고객사에서는 장애가 설치 몇 주 뒤에, 원인과 상관없어 보이는 작업 직후에 납니다. 필수 인자의 대가는 설치 담당자가 네트워크 담당자에게 빈 대역을 물어야 한다는 점이고, 그 질문은 설치 날 한 번이면 끝납니다.
사내 대역도 라우팅 테이블에서 추정하는 대신 --corp-cidrs로 받는 쪽을 골랐습니다. 도커가 대역을 피하지 못하는 이유와 점검이 놓치는 이유가 같기 때문입니다. 두 쪽 모두 라우팅 테이블만 보는데, 사내 대역은 대개 라우팅 테이블에 없습니다.
network.pools_future는 도커가 실제로 그 대역까지 내려가 서브넷을 뗄지 예측하는 대신, 풀이 사내 대역을 포함하기만 하면 FAIL하는 쪽을 골랐습니다. 그 시점은 같은 호스트에 네트워크가 몇 개 더 생기느냐에 달려 있고, 설치 전에는 아무도 모릅니다. 그래서 일어날 수 있는 충돌을 모두 설치 날 FAIL로 올리고, 담당자가 그날 주소 풀을 정하게 했습니다.
주소 풀을 바꾼 뒤 기존 네트워크를 다시 만드는 일은 사람에게 맡겼습니다. 도구가 하면 손은 덜 가지만, 서비스가 도는 중에 컨테이너가 내려갑니다. 컨테이너를 내릴 시간은 그 서버를 함께 쓰는 사람이 정해야 하고, 점검을 읽기 전용으로 둔 것(「설치 전 점검은 호스트를 읽기만 하고, 고칠 명령은 사람에게 넘긴다」)과 같은 이유입니다.
한 호스트에 compose 스택 여러 개를 올리는 제품이라면, 도커 주소 풀을 설치 입력으로 받고 사내 대역 목록을 점검의 입력으로 받는 것부터 시작할 만합니다. 같은 호스트에 네트워크가 쌓일수록 기본 풀은 기업망의 사설 대역 쪽으로 내려갑니다.
어떤 문제를 해결하나요?
설치·운영과 품질의 다른 글

엔지니어링 노트 · 설치·운영과 품질
설치 전 점검은 호스트를 읽기만 하고, 고칠 명령은 사람에게 넘긴다
구축형 OwlAI는 고객이 고른 호스트 한 대에 Docker Compose로 올라갑니다. 배포판과 커널, 도커 판, 보안 모듈, 인증서는 고객마다 다릅니다.

엔지니어링 노트 · 설치·운영과 품질
사내 프록시 뒤에서는 NO_PROXY를 대역 대신 이름과 IP로 적는다
사내 프록시로만 밖에 나갈 수 있는 기업망에 구축형 OwlAI를 설치하면, 클라우드 LLM으로 가는 호출은 HTTPSPROXY를 따라 프록시로 나가야 합니다.

엔지니어링 노트 · 설치·운영과 품질
배포와 폐쇄망 적재는 태그 대신 다이제스트로 확인한다
구축형 OwlAI는 고객 호스트에서 컨테이너 이미지 아홉 종으로 뜹니다. 시험 환경에서 통과한 이미지와 고객 호스트에서 뜨는 이미지가 같은 바이트여야 시험 결과가 고객 설치에도 통합니다.
