gvx를 활용한 AI Agent RL Rollouts용 빠르고 격리된 Docker 환경

코딩 에이전트 RL 롤아웃의 보안 및 인프라 병목
SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl, OpenHands와 같은 벤치마크 및 강화학습(RL) 프레임워크 전반에서 소프트웨어 엔지니어링 에이전트를 학습하고 평가하려면 신뢰할 수 없는 LLM 생성 코드를 수천 개의 동시 롤아웃으로 실행해야 합니다. 실제 소프트웨어 엔지니어링 작업에서 에이전트는 단순히 텍스트 파일만 편집하지 않습니다. 에이전트는 docker build를 실행하고, docker compose로 데이터베이스와 마이크로서비스를 기동하며, 통합 테스트 스위트를 실행하고, 컨테이너 로그를 검사합니다.
롤아웃 규모(클러스터당 128개에서 1,024개의 동시 에피소드)에서 기존 컨테이너 인프라는 네 가지 체계적인 보안 및 성능 병목에 부딪힙니다.
- 호스트
dockerd의 잠금 경합 및 상태 누출: 수백 개의 동시 RL 워커가 단일 호스트/var/run/docker.sock을 가리키면, 모든 에이전트가 호스트 데몬에 대한 root 동등 권한을 얻게 됩니다. 또한 이미지 레이어 추출이 직렬화되고,docker0서브넷 할당이 고갈되며, 호스트iptables에서Another app is currently holding the xtables lock오류가 발생하고, 에피소드 간에 고아 컨테이너가 남습니다. - 특권 DinD 컨테이너 탈출 위험: 표준
docker:dind컨테이너를 실행하려면--privileged(CAP_SYS_ADMIN)가 필요하며, 이로 인해 원시 호스트 블록 디바이스, cgroup release agent, 커널 모듈이 신뢰할 수 없는 RL 롤아웃에 노출됩니다. - 하드웨어 가상화(
/dev/kvm) 제약: MicroVM 런타임(Firecracker,QEMU,Kata Containers)은 내부 커널을 깔끔하게 격리하지만/dev/kvm이 필요합니다. 중첩 가상화가 없는 표준 Kubernetes 파드, KVM이 비활성화된 베어메탈 클라우드 인스턴스, 기존gVisor(runsc) 샌드박스에서는 KVM microVM을 실행할 수 없습니다. - 느린 에피소드 리셋(
env.reset()) 지연: RL 에피소드마다 새docker:dind환경을 시작하고 기본 이미지를 pull하거나 압축 해제하는 데 10초에서 60초가 걸리며, 그동안 GPU 학습 워커는 환경 리셋을 기다리며 유휴 상태가 됩니다.
gvx(rungv CLI 및 z.gfire.dev/gvx/pkg/gvx Go SDK)는 보안이 강화된 심층 방어형 롤아웃 런타임을 제공하여 네 가지 병목을 모두 해결합니다. 각 롤아웃 환경은 단일의 비특권 자가 잠금(self-locked) 사용자 공간 프로세스로 실행됩니다. 이 프로세스는 Google의 gVisor Sentry 커널, pkg/hostlockdown, 메모리 내 netstack에 의해 격리되며, 전체 /var/lib/docker 상태는 단일 Copy-on-Write .ext4 파일(ext4lite) 내부에 존재합니다.
신뢰할 수 없는 에이전트 롤아웃을 위한 무권한 보안: 모든
gvx에피소드는 게스트 코드를 실행하기 전에 자신의 호스트 OS 프로세스를 잠급니다(Linux에서는PR_SET_NO_NEW_PRIVS및 BPFseccomp, macOS에서는 Seatbeltdeny default, Windows에서는 Restricted Token 및 Job Object). RL 에이전트가 에피소드 내부에서docker run --privileged나 파괴적인rm -rf /명령을 실행하더라도, 해당 작업은 그 에피소드의 사용자 공간Sentry커널과 임시.ext4볼륨 파일 내부로 엄격하게 제한됩니다.
롤아웃 아키텍처: 병렬 비특권 인스턴스 + Reflink 스냅샷
gvx에서 모든 롤아웃 워커는 고유한 gVisor Sentry 커널, 고유한 사용자 공간 netstack(10.0.2.15/24), 고유한 ext4lite 블록 볼륨(ep-N.ext4)을 가진 일반적인 비특권 OS 프로세스입니다. 동시에 읽기 전용 콘텐츠 주소 지정 erofs OCI 이미지 캐시를 호스트 페이지 캐시에서 공유합니다.

단일 파일 ext4lite 볼륨이 RL 리셋 경제성을 바꾸는 이유
gvx의 ext4lite 파일 시스템은 JBD2 저널링 ext4 볼륨 전체(Docker의 overlayfs 레이어 디렉터리, trusted.overlay.* 확장 속성, whiteout 디바이스 노드, 이미지 메타데이터 포함)를 하나의 일반 호스트 파일 안에 저장합니다. 따라서 에피소드 수명 주기 관리가 단일 파일 작업으로 바뀝니다.
cp --reflink=always를 통한 100밀리초env.reset():- 사전에 pull된 기본 이미지(
python:3.11,postgres:16,redis:7, 또는 작업별SWE-bench기본 이미지)를 포함하는golden-docker.ext4파일을 한 번 빌드합니다. - 각 RL 에피소드를 시작할 때
cp --reflink=always golden-docker.ext4 ep-0042.ext4를 실행합니다(XFS, Btrfs, ZFS 또는 macOS APFS에서). 이 명령은 기저 디스크 블록을 복사하지 않고 100밀리초 미만에 독립적인 Copy-on-Write 복제본을 만듭니다. - 해당 에피소드 동안 더럽혀진(dirty) 4 KiB 블록만 추가 디스크 공간을 사용합니다.
- 사전에 pull된 기본 이미지(
- 잔여물 없는 종료(
env.close()):rungv프로세스를 종료하고(Go에서는sb.Close()),ep-0042.ext4의 링크를 해제하면 약 10밀리초 안에 에피소드의 프로세스, 네트워크 소켓, 마운트, 스토리지를 100% 회수합니다. 호스트 커널의 마운트 테이블에 마운트되거나 호스트 cgroup에 등록되는 항목은 전혀 없습니다.
단계별 워크플로: Golden .ext4 스냅샷 및 빠른 에피소드 리셋
각 롤아웃 워커 노드에 rungv를 설치하는 방법은 두 가지입니다. SLSA 인증을 받은 사전 빌드 바이너리를 GitHub Releases에서 내려받는 방법(gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml --deny-self-hosted-runners), 또는 CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest를 실행하는 방법입니다.
1단계: Golden docker:dind 볼륨을 한 번만 생성
1mkdir -p /srv/rl-snapshots /srv/rl-episodes
2
3# 1. golden-docker.ext4를 백엔드로 사용하는 seed docker:dind 인스턴스 시작
4rungv run \
5 -p unix:/srv/rl-snapshots/seed.sock:2375 \
6 --mount type=volume,src=/srv/rl-snapshots/golden-docker.ext4,dst=/var/lib/docker \
7 -e DOCKER_TLS_CERTDIR= \
8 docker:dind --tls=false &
9SEED_PID=$!
10
11# 2. golden-docker.ext4 내부에 기본 이미지와 빌드 캐시를 미리 채움
12export DOCKER_HOST="unix:///srv/rl-snapshots/seed.sock"
13until docker info >/dev/null 2>&1; do sleep 0.5; done
14
15docker pull python:3.11-slim
16docker pull redis:7-alpine
17docker pull postgres:16-alpine
18
19# 3. ext4lite가 이미지를 플러시하고 압축할 수 있도록 seed 인스턴스를 정상 종료
20kill -INT $SEED_PID
21wait $SEED_PID
2단계: 100밀리초 안에 병렬 RL 에피소드 생성
각 병렬 롤아웃 워커는 reflink를 통해 golden-docker.ext4를 복제하고, 모든 이미지가 이미 /var/lib/docker에 존재하는 격리된 docker:dind 인스턴스를 부팅합니다.
1EPISODE_ID="ep-0042"
2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
3
4# 즉시 수행되는 Copy-on-Write 복제 (XFS/Btrfs/APFS에서 약 100 ms)
5cp --reflink=always \
6 /srv/rl-snapshots/golden-docker.ext4 \
7 "/srv/rl-episodes/${EPISODE_ID}.ext4"
8
9# 4 GiB RAM과 CPU 2개를 갖춘 격리된 비특권 에피소드 시작
10rungv run \
11 -m 4g --cpus 2 \
12 -p "unix:${SOCK}:2375" \
13 --mount "type=volume,src=/srv/rl-episodes/${EPISODE_ID}.ext4,dst=/var/lib/docker" \
14 -e DOCKER_TLS_CERTDIR= \
15 docker:dind --tls=false &
16EP_PID=$!
17
18export DOCKER_HOST="unix://${SOCK}"
19until docker info >/dev/null 2>&1; do sleep 0.2; done
20
21# 롤아웃 워커 내부에서는 네트워크 pull 없이 이미지를 즉시 사용할 수 있음
22docker images
23
24# 보상 계산 후 에피소드 종료 (약 10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
z.gfire.dev/gvx/pkg/gvx Go SDK를 사용한 프로그래밍 방식 롤아웃 풀
RL 환경 서버 또는 평가 하네스가 Go로 작성되었거나 gRPC를 통해 Go 롤아웃 데몬을 호출한다면, z.gfire.dev/gvx/pkg/gvx를 통해 병렬 컨테이너 에피소드를 직접 관리할 수 있습니다.
1package main
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "log"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "time"
12
13 "z.gfire.dev/gvx/pkg/gvx"
14)
15
16func runEpisode(ctx context.Context, goldenExt4, scratchDir string, epID int) error {
17 epExt4 := filepath.Join(scratchDir, fmt.Sprintf("ep-%04d.ext4", epID))
18 defer os.Remove(epExt4)
19
20 // 1. golden ext4lite 볼륨을 reflink로 복제
21 cpCmd := exec.CommandContext(ctx, "cp", "--reflink=always", goldenExt4, epExt4)
22 if err := cpCmd.Run(); err != nil {
23 return fmt.Errorf("clone golden volume: %w", err)
24 }
25
26 // 2. 에피소드별 벽시계 시간 제한 적용
27 epCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
28 defer cancel()
29
30 var stdout, stderr bytes.Buffer
31 sb, err := gvx.New(epCtx, gvx.Config{
32 Platform: gvx.PlatformAuto,
33 NetworkMode: gvx.NetModeNAT,
34 NetRaw: true,
35 AllowPacketSocketWrite: true,
36 AllowSUID: true,
37 OCIImage: "docker:dind",
38 Entrypoint: []string{"sh", "-c"},
39 EntrypointSet: true,
40 Cmd: []string{
41 "dockerd-entrypoint.sh --tls=false >/var/log/dockerd.log 2>&1 & " +
42 "until docker info >/dev/null 2>&1; do sleep 0.2; done && " +
43 "docker run --rm python:3.11-slim python -c 'print(6 * 7)'",
44 },
45 Envv: []string{"DOCKER_TLS_CERTDIR="},
46 Mounts: []gvx.Mount{
47 {
48 Kind: gvx.MountVolume,
49 ImagePath: epExt4,
50 GuestPath: "/var/lib/docker",
51 },
52 },
53 Resources: gvx.Resources{
54 MemoryBytes: 4 << 30, // 4 GiB 상한
55 CPUMillis: 2000, // CPU 2.0개
56 },
57 Stdout: &stdout,
58 Stderr: &stderr,
59 })
60 if err != nil {
61 return fmt.Errorf("init episode %d: %w", epID, err)
62 }
63 defer sb.Close()
64
65 exitCode, err := sb.Run(epCtx)
66 if err != nil {
67 return fmt.Errorf("run episode %d: %w", epID, err)
68 }
69
70 log.Printf("episode %d finished: exit=%d stdout=%q", epID, exitCode, stdout.String())
71 return nil
72}
73
74func main() {
75 ctx := context.Background()
76 if err := runEpisode(ctx, "/srv/rl-snapshots/golden-docker.ext4", "/srv/rl-episodes", 1); err != nil {
77 log.Fatal(err)
78 }
79}
에이전트 RL 샌드박스 런타임 비교
| 지표 / 요구 사항 | 공유 호스트 dockerd | 특권 docker:dind | Firecracker / KVM MicroVM | 표준 gVisor (runsc) | gvx (rungv / pkg/gvx) |
|---|---|---|---|---|---|
호스트 root 또는 --privileged 필요 | 예 (docker.sock = root) | 예 (--privileged) | /dev/kvm + TAP 설정 필요 | 예 (또는 userns + mount) | 아니요 (단일 비특권 프로세스) |
비특권 K8s Pod / 기존 runsc 내부에서 동작 | 아니요 | 아니요 | 아니요 (/dev/kvm 없음) | 실패 (중첩 seccomp에서 EBUSY) | 예 (자동 futex 루프 대체 경로) |
내부 docker build 및 docker compose 지원 | 호스트 데몬 공유 | 예 | 예 | 호스트 DinD 튜닝 없이는 실패 | 예 (ext4lite 위의 overlayfs) |
| 에피소드별 네트워크 격리 | 공유 docker0 / xtables 잠금 | 별도 네임스페이스 | 별도 VM NIC | 별도 netns | 프로세스 내 격리된 netstack |
웜 에피소드 리셋 지연 시간 (env.reset()) | 5초에서 30초 (컨테이너/볼륨 정리) | 10초에서 60초 (콜드 데몬) | 약 150 ms (VM 스냅샷) | 5초에서 20초 | 약 100 ms (cp --reflink=always .ext4) |
| 크로스 플랫폼 로컬 개발 (Windows / macOS / Linux) | Linux VM 필요 | Linux VM 필요 | Linux KVM만 지원 | Linux만 지원 | Windows, macOS, Linux에서 네이티브 실행 |
이 시리즈의 추가 자료
- gvx: 하나의 비특권 프로세스에서 Linux 컨테이너, Docker, Kubernetes 실행하기: Windows(
whpx,systrap), macOS(hvf), Linux(systrap)에 걸친gvx의 완전한 아키텍처 설명입니다. - 루트 또는 사용자 네임스페이스 없이 Docker, docker build, docker compose 실행하기:
sudo,unshare(CLONE_NEWUSER),mount(2),/dev/fuse가 차단되었을 때를 위한 3단계 빠른 부트스트랩 및 문제 해결 가이드입니다. - gvx로 gVisor 및 중첩 샌드박스 내부에서 Docker-in-Docker 실행하기:
docker.io와 중첩runsc가gVisor(runsc) 내부에서 실패하는 이유와,gvx가EBUSY발생 시_RUN_SYSCALL_LOOPfutex 루프로 대체하는 방식에 대한 심층 설명입니다. - gvx로 AI 에이전트 RL 롤아웃을 위한 빠른 격리 Docker 환경 구축하기 (현재 글)