19.2 다중 언어 기반 Zenoh 컴포넌트의 컨테이너화(Containerization)
Zenoh가 오케스트레이션하는 네트워크 인프라는 단일 언어로 작성된 단순한 모놀리스(Monolith)가 아니다. 이는 각 생태계의 장점을 극대화하기 위해 설계된 거대한 다국적 군대와 같다. 중앙 데이터 라우터와 고성능 백엔드 플러그인은 완벽한 메모리 안정성과 실행 속도를 보장하는 Rust로 작성되어 있다. 반면, 하드웨어 제어와 밀접히 맞닿아 있는 로봇 운영체제(ROS2) 브릿지나 초경량 에지 노드는 C/C++ 로 구동된다. 또한, 인공지능(AI) 추론 모듈은 텐서플로우(TensorFlow)나 파이토치(PyTorch)를 품고 있는 Python 환경에서 실행되며, 사용자 관제를 위한 대시보드와 비동기 백엔드는 쾌적한 이벤트 루프를 가진 TypeScript (Node.js) 환경에서 작동한다.
이처럼 각양각색의 언어와 생태계 의존성(Dependencies)을 가진 애플리케이션들을 각 서버나 에지 디바이스의 호스트(Host) 운영체제에 직접 apt-get install 혹은 pip install 방식으로 설치하려 시도한다면, 여러분의 시스템은 일주일도 채 지나지 않아 ’의존성 지옥(Dependency Hell)’에 빠져 완벽하게 붕괴할 것이다. 우분투 20.04와 22.04의 C 표준 라이브러리(glibc) 버전에 따른 런타임 충돌, 혹은 Python 모듈 버전의 파편화는 새벽 3시에 삐삐를 울리게 만드는 주범이다.
1. 컨테이너화(Containerization)의 사명
이 단원에서는 어떠한 언어로 개발되었든 간에, 모든 Zenoh 기반의 애플리케이션을 OS 환경과 100% 격리된 “도커(Docker) 컨테이너“라는 똑같이 생긴 규격화된 네모난 박스(Artifact)로 포장하는 방법론을 제시한다.
이 박스는 호스트 환경이 우분투(Ubuntu)이든, 알파인(Alpine)이든, 라즈베리 파이 OS(Raspbian)이든 묻지도 따지지도 않고 코드를 실행시키는 유일무이한 표준 규격이다. 단순히 Dockerfile을 작성하는 것을 넘어, 에지 환경의 디스크 크기와 보안성을 고려하여 불필요한 쓰레기를 극단적으로 걷어내는 이미지 경량화 전략, 그리고 서로 다른 CPU 아키텍처(x86_64, ARM64)를 한 번에 빌드해 내는 다중 아키텍처 빌드(Multi-arch Build) 기술까지 깊숙이 해부한다.
graph LR
subgraph "다국적 소스코드 (Multi-language Source)"
Rust[Zenoh Router (Rust)]
C[Edge Sensor (C/C++)]
Py[AI ML Model (Python)]
TS[Dashboard (TypeScript)]
end
subgraph "Docker Container Build Pipeline"
Builder[Multi-stage Build & Strip]
end
subgraph "규격화된 OCI 컨테이너 박스"
Box1[Distroless Image<br/>30MB]
Box2[Alpine Image<br/>15MB]
Box3[Slim Python<br/>300MB]
Box4[Node Alpine<br/>100MB]
end
Rust --> Builder
C --> Builder
Py --> Builder
TS --> Builder
Builder --> Box1
Builder --> Box2
Builder --> Box3
Builder --> Box4
우리는 이제 컨테이너(Container)라는 마법의 망토를 활용하여, 복잡한 소프트웨어 환경을 단일한 바이너리 덩어리로 래핑(Wrapping)하는 완벽한 표준화 작업에 착수한다. 이 과정을 체화한 엔지니어는 더 이상 대상 기기의 리눅스 배포판 버전을 묻지 않게 될 것이다.
2. Zenoh 라우터(Router) Docker 이미지 경량화 및 최적화 빌드
공식적으로 이클립스 재단에서 제공하는 eclipse/zenoh 도커 이미지를 사용하는 것도 좋은 방법이다. 그러나 만약 여러분의 에지 로봇에 탑재된 플래시 디스크 용량이 4GB, 8GB에 불과하다면 범용성을 위해 우분투(Ubuntu) 전체를 말아 넣은 수백 메가바이트(MB) 크기의 베이스 이미지를 탑재하는 것은 엄청난 사치이자 치명적인 병목이다.
에지와 배포 파이프라인 구간의 네트워크 대역폭을 아끼기 위해서는 100MB짜리 컨테이너를 10MB로 극단적으로 다이어트(Diet)시켜야만 한다.
2.1 궁극의 다이어트를 위한 멀티-스테이지 빌드 (Multi-stage Build)
컨테이너 경량화의 제1 원칙은 “빌드 환경“과 “실행 환경“을 처절하게 분리하는 데 있다. Rust 코드를 컴파일하기 위해서는 수 기가바이트(GB)에 달하는 툴체인(Cargo, LLVM, 빌드 의존성 포함)이 필요하지만, 정작 빌드가 끝난 후 컨테이너 안에서 실제로 구동되는 것은 딸랑 수십 메가바이트짜리 우분투용 zenohd 쇳덩어리(Binary) 하나뿐이다.
멀티-스테이지 빌드는 이 쇳덩어리만 쏙 빼내어 새로운 빈 깡통에 담는 마술이다. 다음의 Dockerfile 런북을 면밀히 분석하라.
## -------------------------------------------------------------------
## Stage 1: Builder (무거운 컴파일 전용 컨테이너 구역)
## -------------------------------------------------------------------
FROM rust:1.75-bookworm as builder
## 소스 코드가 위치할 작업 공간 생성
WORKDIR /usr/src/zenoh
## 로컬의 소스코드를 빌더 내부로 복사
COPY . .
## Release 모드로 최적화된 바이너리 추출 (시간 소요)
RUN cargo build --release --bin zenohd
## -------------------------------------------------------------------
## Stage 2: 런타임 이미지 (궁극의 디스트롤리스)
## -------------------------------------------------------------------
## 베이스로 데비안/우분투가 아닌 구글의 distroless(컨테이너 이미지) 이미지를 사용한다.
FROM gcr.io/distroless/cc-debian12:latest
## Stage 1에서 생성된 순수한 바이너리 1개만 Stage 2로 훔쳐온다.
COPY --from=builder /usr/src/zenoh/target/release/zenohd /usr/local/bin/zenohd
## 디스트롤리스 기반이므로 포트를 명시
EXPOSE 7447/tcp
EXPOSE 7447/udp
## 실행 가능한 엔진 스크립트를 지정 (bash 없이 바이너리 직접 실행)
ENTRYPOINT ["/usr/local/bin/zenohd"]
2.2 디스트롤리스(Distroless) 아키텍처가 선사하는 무결성과 보안성
두 번째 스테이지의 베이스 이미지로 사용된 gcr.io/distroless/cc-debian12 는 구글(Google)에서 배포하는 프로덕션 전용 이미지다. 이 이미지 내부에는 apt, yum 등 패키지 매니저는 고사하고, 심지어 ls, grep, bash, sh 같은 기본적인 리눅스 셸(Shell) 명령어조차 아예 존재하지 않는다. 오직 C 런타임 환경(glibc) 기초만 들어있다.
이로 인해 두 가지의 압도적인 아키텍처적 승리를 쟁취하게 된다.
- 극적인 용량 감소:
ubuntu베이스라인 이미지가 기본적으로 80MB를 잡아먹는 데 반해, 디스트롤리스 이미지는 20MB 남짓이다. 최종 산출물의 압축 페이로드는 30MB 안팎으로 극도로 감소하여 드론이나 로봇으로 OTA 배포를 쏠 때 통신 비용을 비약적으로 줄여준다. - 해킹 불가능(Un-hackable) 방벽 구축: 만에 하나 제로데이 취약점이나 어플리케이션 버그(CVE) 등으로 인해 악의적인 해커가 Zenoh 포트의 빈틈을 뚫고 라우터 컨테이너 내부로 친입(Reverse Shell)하는 데 성공했다 하더라도, 컨테이너 내부에서 할 수 있는 것이 아무것도 없다. 터미널 명령어를 입력할 셸(
sh또는bash)이 물리적으로 지워져 있으므로 횡적 이동(Lateral Movement)이나 후속 해킹 스크립트 다운로드가 원천 봉쇄된다.
크기를 줄이는 행위 자체가 곧 클라우드 네이티브 보안(Security)의 성벽을 쌓는 완벽한 일치(Alignment)를 이루어낸 것이다.
3. 다중 아키텍처(x86_64, ARM64)를 위한 Cross-compilation 및 Docker Buildx 활용
여러분이 근무하는 회사의 클라우드 서버는 이마존 EC2나 구글 클라우드를 이용하며 대개 Intel이나 AMD의 x86_64 CPU로 동작한다. 하지만 현장에 배치될 사물인터넷 게이트웨이 기기, 드론, 그리고 자율주행 모바일 로봇(예: Raspberry Pi, NVIDIA Jetson Nano, NXP i.MX 시리즈)들은 발열과 전력 소모를 줄이기 위해 스마트폰과 같은 뼈대를 가진 ARM64 (aarch64) 칩셋을 쓴다.
당신의 쾌적한 윈도우나 맥북 리눅스 터미널(x86_64)에서 평소 하던 대로 docker build -t zenoh-robot:v1 . 명령어를 쳐서 도커 이미지를 구운 뒤 현장에 있는 로봇으로 이미지를 전송해 보라. 컨테이너를 실행하는 순간, 로봇은 지체 없이 심장마비를 일으키며 다음과 같은 치명적인 콘솔 에러를 내뱉고 멈춰버릴 것이다.
standard_init_linux.go:211: exec user process caused "exec format error"
이 에러 문구는 대상 하드웨어의 CPU 기계어 세트(Instruction Set) 형식이 도커에 포장된 바이너리 파일과 일치하지 않아 프로세서를 구동할 수 없다는 완벽한 죽음의 선고(Death Sentence)다. 이를 타파하기 위해, 명령 한 번으로 모든 세상의 하드웨어 언어를 지배하는 도커 빌드엑스(Docker Buildx) 병기 사용법을 살펴본다.
3.1 QEMU 에뮬레이션과 Docker Buildx 엔진의 가동
과거에는 x86 서버 한 대, ARM 서버 한 대를 각각 구매해서 Git Action을 연결하여 CI(Continuous Integration)를 따로따로 운영해야 했다. 이는 파이프라인의 엄청난 비용과 관리를 요구했다. 하지만 현시대에는 QEMU(하드웨어 에뮬레이터) 를 얹어, 논리적으로 x86 머신 안에서 수십 개의 가상 CPU 아키텍처 코딩을 실시간으로 번역해 낼 수 있다.
1. QEMU와 멀티-아키텍처 지원 권한 활성화
여러분의 리눅스 CI 빌드용 서버에 접속하여 먼저 전역적인 QEMU 바이너리를 시스템 커널 깊숙이 활성화하라.
## 리눅스 커널에 binfmt_misc 레지스트리를 등록해 ARM 등의 에뮬레이션을 허용함
docker run --privileged --rm tonistiigi/binfmt --install all
2. 새로운 세계수(Yggdrasil) 빌더 노드 생성
기본 docker build 명령어 대신 컨테이너 세상을 다중 병렬 처리하는 새로운 컨테이너형 빌더 인스턴스를 창조하고 교체해야 한다.
## 새로운 다중 빌더 엔진 생성 (이름: multi-arch-builder)
docker buildx create --name multi-arch-builder --use
## 빌더의 상태와 지원하는 플랫폼 리스트 확인
docker buildx inspect --bootstrap
위 명령어를 치면 지원 플랫폼에 linux/amd64, linux/arm64, linux/riscv 등이 촘촘하게 뜨는 것을 눈으로 직접 확인하라.
3.2 도커 매니페스트 리스트(Manifest List)로의 동시 타격 배포
이제 준비는 끝났다. 기존의 복잡했던 쉘 스크립트는 폐기하라. 다음 단 한 줄의 명령어로 클라우드 백엔드 라우터(x86)와 드론 내부의 엔드포인트 프로그램(ARM64)을 완벽하게 동시에 컴파일하고 압축하여 원격 레지스트리로 승천시킨다.
## linux/amd64와 linux/arm64 두 가지 타겟을 동시에 깎아낸 뒤,
## 원격의 도커 허브(또는 사설 레지스트리)로 통합된 태그(v2.0)로 밀어 올린다.
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t my-company-registry.io/zenoh-robot-app:v2.0 \
--push \
.
3.2.1 마법의 동기화 (Docker Manifest)
이 명령어의 가장 아름다운 부분은 도커 허브 레지스트리에 저장될 때 나타난다.
레지스트리에는 v2.0 이라는 라벨(태그)이 단 한 개만 생성된다(Manifest List).
이후:
- AWS에 떠 있는 K8s 시스템이
pull zenoh-robot-app:v2.0을 요청하면, OCI 허브는 알아서 K8s의 하드웨어를 감지하고 “아, 넌 x86이구나“라며 AMD64용 디스크 이미지를 내어준다. - 산속의 광산 갱도에 있는 라즈베리 파이 로봇이 똑같이
pull zenoh-robot-app:v2.0을 요청하면, 허브는 자비를 베풀어 “아, 넌 ARM 계열이네“라며 ARM64 용 디스크 이미지를 조용히 내려준다.
결과적으로, 인프라 배포 설정 파일(YAML 등)이나 Ansible 스크립트에는 절대로 아키텍처를 구분하는 if-else 분기 처리를 넣을 필요가 없게 되었다. 단 하나의 통일된 태그를 지배함(Single Entity Rule)으로써, 우리는 거대 이기종(Heterogeneous) 하드웨어 로봇 함대를 단 하나의 선언으로 통제할 수 있게 된 것이다.
4. Rust 및 C 기반 Zenoh 클라이언트 애플리케이션의 컨테이너 빌드 전략
로봇 공학과 스마트 팩토리의 정밀한 심장부에서 구동되는 Zenoh 클라이언트 디바이스 노드는 파이썬(Python) 같은 스크립트 언어의 여유를 허락하지 않는다. 1 밀리초(ms) 단위의 응답 라우팅과 하드웨어의 미세 전류 제어를 위해 우리는 Rust와 C/C++ 이라는 날 선 검을 빼어 든다.
그러나 이 컴파일(Compile) 언어들로 작성된 애플리케이션을 도커 컨테이너로 밀어 넣을 때는 치명적인 장애물에 부딪힌다. 바로 동적 공유 라이브러리(Dynamic Shared Library, .so)라는 보이지 않는 종속성의 사슬이다.
4.1 동적 링킹(Dynamic Linking)의 저주
개발자의 최신 우분투 PC에서 cargo build 나 gcc를 통해 빌드된 실행 파일은 겉보기엔 파일 하나인 것처럼 속이지만, 실행하는 순간 시스템 안을 뒤져 libc.so.6, libssl.so, libpthread.so 같은 운영체제 핵심 파일에 강제로 의존(Dependency)하려 든다.
가벼운 배포를 위해 로봇에 속이 텅 빈 거대한 컨테이너(예: alpine이나 scratch)를 띄우고 그곳에 실행 파일을 살포시 집어넣어 보아라.
## 실행 시도 시 마주하게 되는 암울한 에러 메시지
/usr/local/bin/zenoh-client: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory
운영체제를 극한으로 다이어트(Diet) 시킨 도커 컨테이너 안에는 그러한 라이브러리가 존재하지 않아 애플리케이션이 즉사하는 “지옥(DLL Hell)“에 봉착하는 것이다.
4.2 [인스펙션] 정적 링크(Static Link)를 통한 무결점 수호 전술
외부 라이브러리에 기대어 사는 삶을 포기하고, 애플리케이션 바이너리 단 하나에 구동에 필요한 모든 어셈블리어를 압축해서 쑤셔 넣는 순수 정적 링킹(Pure Static Linking) 마법을 전개해야 한다.
4.2.1 Rust 컴파일러의 MUSL 정적 빌드 병기화
Rust를 통해 Zenoh 노드를 짰다면, 표준 glibc 기반 빌드를 미련 없이 버려라. 경량 임베디드 리눅스의 표준 C 라이브러리인 무슬(MUSL)을 타겟으로 컴파일을 명령해야 한다.
- 방법:
x86_64-unknown-linux-musl또는aarch64-unknown-linux-musl이라는 완벽한 자급자족형(Standalone) 빌드 타겟 체인(Target Chain)을 도입하라.
## Rust 빌드 스테이지
FROM rust:alpine as builder
## Alpine 환경에서 컴파일하기 위한 사전 작업
RUN apk add --no-cache musl-dev
WORKDIR /app
COPY . .
## glibc가 아닌 musl 의존성만을 타겟팅하여 영원히 묶어버리는 정적 링킹 발동
RUN cargo build --release --target x86_64-unknown-linux-musl
## 런타임 스테이지 (생명체조차 없는 완전한 진공 우주인 scratch 사용)
FROM scratch
LABEL maintainer="zenoh-engineer"
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/zenoh-app /zenoh-app
## 어떠한 OS 커널 파일이 없어도 이 쇳덩어리 스스로 생존한다
ENTRYPOINT ["/zenoh-app"]
이렇게 깎여 나온 바이너리는 OS에 특정 버전의 라이브러리가 깔려 있든 말든, 심지어 OS 자체가 아예 존재하지 않는 0 byte의 빈(scratch) 컨테이너 내부에서도 완벽하게 홀로 호흡하며 스레드를 작동시키는 무적의 방탄조끼를 입게 된다.
4.2.2 C 컴파일러(GCC/Clang)의 무자비한 -static 플래그
만약 Zenoh-C API를 C/C++로 바인딩하여 레거시 장비를 연동하는 백엔드 데몬을 만들었다면, gcc 구동 시 모든 다이나믹 링크 경로를 차단해야 한다.
- 방법:
CMake나 Makefile 내부의 컴파일러 옵션 최말단에-static플래그를 강제로 삽입하라. 이는 링커(Linker)에게 “외부libzenohc.so동적 라이브러리 파일들을 연결하지 말고, 컴파일 시점에libzenohc.a형태의 정적 라이브러리로 찾아내어 실행 파일 내부에 전부 때려 박아라“라는 준엄한 명령이다.
## 컴파일 커맨드 예시
gcc -o io_sensor_bridge src/main.c -I/opt/zenoh/include -L/opt/zenoh/lib -lzenohc -static
이렇게 생성된 io_sensor_bridge 바이너리는 파일 크기가 3MB에서 15MB 정도로 덩치가 급격히 불어나게 되지만, 그 대가로 도커 컨테이너 내부로 배포될 때 종속성(Dependency) 제로라는 어마어마한 배포 신뢰성을 회의실 책상 위에 올려놓게 된다. “내 컴퓨터에선 되는데 로봇에 넣으니 안 켜져요“라는 막내 개발자의 비명은 즉각 사라질 것이다. 확고한 최소한의 격리만이 극한의 신뢰성을 창조한다.
5. TypeScript 기반 웹-Node.js Zenoh 클라이언트의 컨테이너 구성
Node.js 환경 위에서 동작하는 TypeScript 기반 Zenoh 클라이언트(예: 관제 대시보드 백엔드, 웹소켓 릴레이 서버)는 앞에서 설명한 Rust나 C 언어와는 컨테이너화 전략이 완전히 다르다.
이들은 컴파일된 단일 바이너리(Binary)를 생산해 내는 것이 아니라, 자바스크립트 소스 코드 그 자체와 이를 읽고 즉석에서 실행해 주는 거대한 가상 머신(V8 런타임 엔진), 그리고 node_modules라는 비대한 종속성 폴더를 모두 컨테이너 안에 함께 끌어안고 다녀야 하는 숙명을 지닌다. 따라서 자칫 잘못 빌드하면 컨테이너 사이즈가 1GB를 훌쩍 넘어가 버리는 참사를 겪게 된다.
5.1 종속성 계층(Layer)의 물리적 방어선 구축 전술
도커(Docker) 빌드 엔진은 Dockerfile의 각 줄을 실행할 때마다 하나의 레이어(캐시)를 만들어 저장한다. 코드를 한 줄 고칠 때마다 npm install이 처음부터 다시 돌아서 빌드가 10분씩 걸리는 끔찍한 상황을 막으려면, 도커 레이어 캐싱 원리를 악용한 순서 배치가 절대적으로 필요하다.
FROM node:20-alpine AS builder
WORKDIR /app
## 1. 소스 코드가 바뀌어도 패키지 정보가 동일하다면 캐시를 사용하도록
## package.json 계열의 파일만을 먼저 독립적으로 복사한다.
COPY package.json package-lock.json ./
## 2. 깨끗하고 확정적(deterministic)인 설치를 위해 npm install 대신 npm ci를 사용한다.
## 오직 프로덕션(production) 구동에 필요한 패키지만 설치하여 node_modules의 덩치를 줄인다.
RUN npm ci --only=production
## 3. 그 이후에야 자주 변동하는 소스 코드(src)를 복사하여 빌드한다.
COPY src/ ./src/
COPY tsconfig.json ./
RUN npm run build
## 4. 최종 런타임 컨테이너 분리
FROM node:20-alpine
WORKDIR /app
## 빌드된 js 파일과 꼭 필요한 node_modules 만을 복사해 온다.
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
## 컨테이너 보호를 위해 root 계정이 아닌 node 전용 유저로 권한을 강등시킨다.
USER node
EXPOSE 8000
ENTRYPOINT ["node", "dist/main.js"]
5.2 PID 1의 저주와 Tini/PM2 기반 권력 장악
Node.js 애플리케이션을 도커 안에 띄울 때 가장 흔히 저지르는 치명적 실수는 ENTRYPOINT ["npm", "start"]로 애플리케이션을 시작하는 것이다.
이 방식으로 컨테이너를 구동하면, 리눅스 커널의 최상위 부모 프로세스(PID 1) 권한을 Node.js 본체가 아닌 npm 셸 스크립트가 먹어치우게 된다. 이것이 왜 재앙인가?
서버를 종료하거나 K8s가 파드를 다른 워커 노드로 옮기기 위해 SIGTERM(종료 신호)을 보내면, 이 신호를 npm이 먹어버리고 실제 자식 프로세스인 Node.js 로 전달하지 않는다. 결과적으로 Node.js 앱이 쥐고 있던 데이터베이스 커넥션이나 Zenoh 라우터와의 세션(zenoh.Session)이 우아하게 종료(Graceful Shutdown)되지 못하고 K8s에 의해 SIGKILL로 강제 참수당하게 되며, 네트워크망에는 흔적 세션이 좀비처럼 남아 라우터의 메모리릭(Memory Leak)을 유발하게 된다.
5.2.1 해결책: 진입점(Init) 프로세스 위임
반드시 Node.js 프로세스 앞에 유닉스 시그널을 적절히 하위로 전파해 주는 작고 가벼운 Init 도구(tini)나 프로세스 매니저(pm2-runtime)를 배치해야만 운영 체제와의 정상적인 타협이 성사된다.
## 알파인 리눅스에 tini를 설치
RUN apk add --no-cache tini
## PID 1의 왕좌를 tini에게 주고, node 프로세스를 tini의 자식으로 등록한다.
ENTRYPOINT ["/sbin/tini", "--", "node", "dist/main.js"]
이렇게 구성된 TypeScript 컨테이너만이 시스템 종료(Terminate) 명령을 접수했을 때 session.close()를 온전히 호출하고 장렬히 전사할 자격을 얻게 된다.
6. ROS2-Zenoh 브릿지의 컨테이너화 및 호스트 네트워크 바인딩 기법
로봇의 성능을 온전히 활용하기 위해 로컬 호스트(Host)에는 ROS2 기반 센서 드라이버가 돌고 있고, 외부 관제탑과의 통신을 위해 zenoh-bridge-dds를 도커(Docker) 컨테이너로 띄운다고 가정해 보자.
만약 이 브릿지 컨테이너를 평범한 방식으로 띄우게 된다면, 브릿지에 연결된 외부 클라우드와의 Zenoh 연결은 문제없이 이루어지겠지만, 정작 방문 너머에 있는 로봇 본체(Host OS)의 ROS2 노드(센서들)와는 통신이 완전히 단절되는 심각한 장애를 겪게 된다.
ROS2의 기반 통신 미들웨어인 DDS(Data Distribution Service)는 본질적으로 로컬 네트워크의 지형 공간에 극도로 민감한 멀티캐스트(Multicast) 및 공유 메모리(Shared Memory) 프로토콜이기 때문이다. 도커가 자체적으로 치고 있는 가상 방화벽(NAT)과 가상 랜카드(docker0) 성벽이 DDS 통신의 맥을 1차적으로 끊어버린다.
6.1 [인스펙션] 도커 네트워크 성벽 파괴 전술
ROS2 통신은 닫힌 방을 극도로 혐오한다. 컨테이너의 보안이라는 우아함을 포기하고, 로봇 하드웨어의 생살을 맞대는 극단적인 융합 모드로 진입해야 한다.
6.1.1 Host Network Mode 전면 개방
DDS 프로토콜이 브릿지의 컨테이너 내부로 온전히 흘러들어오게 만들려면, 도커 컨테이너를 실행할 때 네트워크 망 자체를 하나로 합쳐야 한다.
## --network host 옵션으로 도커의 가상망을 완전히 찢어버리고
## 로봇 본체의 랜카드(eth0, wlan0)와 브릿지를 직결시킨다.
docker run -d \
--name zenoh-bridge \
--network host \
eclipse/zenoh-bridge-dds:latest \
-e tcp/클라우드_라우터_IP:7447
이 옵션을 활성화해야만 DDS 프로토콜의 UDP 브로드캐스트 패킷이 도커의 NAT 방어벽에 막히지 않고 브릿지 컨테이너 귀에 도달하게 되며, 마침내 K8s나 AWS에 떠 있는 Zenoh 라우터 쪽으로 ROS 토픽(/base/cmd_vel, /camera/image_raw)을 밀어 올릴 수 있게 된다.
6.1.2 IPC (지식 메모리 공유) 및 네트워크 버퍼 바인딩
만약 로봇의 자율주행 3D 라이다(LiDAR) 센서처럼 초당 수백 메가바이트(MB)의 포인트 클라우드(Point Cloud) 데이터가 발생하는 상황이라면, 이 거대한 데이터를 이더넷 네트워크 계층(UDP)을 통해 컨테이너 안팎으로 복사(Copy)하는 행위 자체가 K8s 워커 노드 또는 로봇의 CPU를 불타오르게 만든다.
이를 타파하기 위해 최신 DDS 미들웨어(예: Eclipse CycloneDDS 의 Zero Copy 플래그, eProsima FastDDS 의 Shared Memory transport)는 네트워크 스택을 무시하고 직접 RAM 공간 메모리에 데이터를 적어버리는 IPC(Inter-Process Communication) 통신을 주무기로 사용한다.
이 메모리 통신 기법이 도커 컨테이너 경계를 관통하려면 다음의 두 가지 권한을 마저 해제해 주어야 한다.
docker run -d \
--network host \
--ipc host \ # 호스트 OS의 /dev/shm (Shared Memory) 공간을 컨테이너와 동일하게 공유
--pid host \ # 포인터 참조를 위해 호스트와 컨테이너가 동일한 프로세스 ID 체계를 공유
eclipse/zenoh-bridge-dds:latest
--ipc=host와 --pid=host 플래그까지 컨테이너에 부여함으로써, 우리는 도커 안의 브릿지와 바깥의 센서 프로세서가 완전히 동일한 물리 메모리 자원(RAM 공간)을 공유하고 쳐다보게 만들 수 있다. 도커 컨테이너가 가지는 프로세스 격리라는 신성한 원칙을 부수어 버렸지만, 그 대가로 무지막지한 ROS2 고대역폭 센서 데이터를 지연 시간(Latency) 0에 가깝게 빨아들여 Zenoh 고속도로에 즉각 태워버리는 야만의 효율을 성취해 낼 수 있다.
7. 안전한 프라이빗 컨테이너 이미지 레지스트리 구축 및 권한 관리
여러분의 훌륭한 엔지니어들이 1년간 피땀 흘려 짜낸 자율주행 AI 알고리즘과 군집 로봇 제어 코드를 도커(Docker) 컨테이너 이미지로 완벽하게 빚어내었다. 하지만 배포의 편의성을 이유로, 이 기업 자산의 결정체를 누구나 무료로 접근할 수 있는 퍼블릭(Public) Docker Hub 저장소에 업로드(docker push my-company/robot-ai:v1)하는 패착을 저지른다면?
당신의 도커 이미지는 전 세계에 스트리밍되며 경쟁사의 엔지니어들은 명령어 한 줄 만으로 여러분의 수억 원짜리 알고리즘과 인증서 로직이 담긴 바이너리를 완벽히 복제하여 훔쳐가게(Pull) 된다.
7.1 [Runbook] 폐쇄형 무기고(Private Registry) 건설 전술
우리가 만든 모든 컨테이너 산출물과 AI 맵퍼 이미지는 엄격하게 자물쇠가 채워진 우리들만의 비밀 창고(Private Registry)에 은닉(Stealth)되어야 한다.
7.1.1 프라이빗 레지스트리의 물리적 건설 (Harbor / AWS ECR)
클라우드 데이터센터 깊숙한 곳이나 외부망 접근이 제한된 인트라넷 환경에, 역할 기반 접근 제어(RBAC) 체계를 가진 오픈소스 기반 프로젝트인 Harbor 엔진을 띄우거나, 관리형 서비스인 아마존의 AWS ECR(Elastic Container Registry)를 구축한다.
이후 19.2절에서 다룬 빌드 파이프라인(CI)의 최종 목적지 태그(Tag)를 퍼블릭 공간이 아닌 내부 프라이빗 레지스트리의 주소로 매핑하여 푸시(Push)한다.
## 산출물을 퍼블릭이 아닌 프라이빗 ECR/Harbor 저장소로 전송
docker tag zenoh-ai-client:v1 private-registry.mycompany.io/robotics/zenoh-ai:v1
docker push private-registry.mycompany.io/robotics/zenoh-ai:v1
7.1.2 수만 대 군단(Edge Fleet)의 진입 암호(ImagePullSecrets) 탈취 기법
인증 장막을 쳐 두었기에, 이제 전 세계를 누비는 로봇과 드론들이 이 무기고에서 업데이트 이미지를 당겨가(Pull) 가기 위해서는 반드시 출입문 비밀번호를 제시해야 한다. 이를 배포 파이프라인의 언어로 ImagePullSecrets라 명명한다.
Kubernetes (K3s, MicroK8s) 클러스터 제어 배포의 경우:
배포 스크립트에 컨테이너 비밀 토큰 자격 증명을 Base64 로 암호화하여 네임스페이스(Namespace)마다 심어두어야만 라우터가 배포를 인가한다.
## K8s 시크릿 객체 생성 명령
kubectl create secret docker-registry harbor-login \
--docker-server=private-registry.mycompany.io \
--docker-username=robot_fleet_a \
--docker-password=슈퍼시크릿접근토큰값 \
--docker-email=admin@mycompany.io
## 파드(로봇 앱) 선언부에 해당 토큰을 달아 하달함 (매니페스트 일부)
spec:
containers:
- name: zenoh-ai
image: private-registry.mycompany.io/robotics/zenoh-ai:v1
imagePullSecrets:
- name: harbor-login # 이 열쇠를 가지고 가야만 이미지를 꺼내올 수 있음
순수 Docker CLI 를 구동하는 원시적인 Edge 의 경우:
로컬 쉘에서 docker run을 실행하기 전, 배포 오토메이션 스크립트의 제일 첫 줄에 무조건 docker login -u [발급된_로봇_ID] -p [토큰] 커맨드를 박아 넣어 런타임 호스트 레벨에서의 인증을 취득해야 한다.
7.1.3 컴포넌트 유출 방어와 폐기(Revocation) 체계
절대로 단일한 ‘마스터 패스워드’ 1개로 10,000대의 로봇 모두에게 토큰을 발급하는 아마추어적인 실수를 범하지 마라.
지역이나 군집(Fleet) 스쿼드별로 각각 다른 인증 토큰을 수백 개 단위로 쪼개어 다중 발급(Multi-Issuing) 체계를 갖추어야 한다.
만약 전장(필드)에서 드론 1대를 탈취당하거나 해킹 의심 징후가 포착되었을 때, 전체 Ин프라의 컨테이너 무기고 비밀번호를 일괄 롤링(재설정)해야 하는 군견병 급 참사를 피해야 하기 때문이다. 사고 인지 즉시, 특정 로봇 그룹에 할당된 1개의 전용 토큰 권한만을 단독 폐기(Revoke) 함으로써, 나머지 9,999대의 정상 로봇들의 배포 라이프사이클에 아무런 영향을 주지 않고 바이러스(유출점)를 완벽히 도려내는 보안 파이프라인을 완성하라.