Dive into OSS의 두 번째 주제로 이번엔 OpenSandbox를 소개해드리려 합니다.
AI를 활용한 개발을 진행할 때 PR을 올리고 나서 CI에서 테스트가 실패하면 “이 PR에서 실패하는 테스트를 고쳐줘.”라고 AI 에이전트에게 요청하는 경우를 자주 볼 수 있습니다. 에이전트는 사용자의 요청대로 수정하지만 이 코드가 실제로 테스트를 통과할지, 실패했다면 로그를 보고 다시 고칠 수 있을지는 에이전트의 결과를 기다려야만 알 수 있습니다.
에이전트의 결과물 출력 대기시간 없이 소프트웨어 개발 흐름을 자동화하려면 코드를 실행할 환경부터 준비해야 합니다. 이 때 에이전트와 함께 구축하게 되면 특히 컨테이너 및 여러 기초 파이프라인(빌드 산출물 처리 등)을 구축하게 되는데, 저의 경우 구축 및 코딩 작업을 진행하면서 AI가 만들어낸 임시 파일들이 무더기로 쌓여있어서 곤혹을 치른적도 있었기에 작업을 하면서 떠오른 생각을 바탕으로 커뮤니티 여러분께 OpenSandbox을 소개하고자 하는 계기가 되었습니다.
OpenSandbox는 이 과정을 API로 다룹니다. 애플리케이션이 필요한 실행 환경을 만들고, 파일과 명령을 보내고, 결과를 가져와 환경을 종료할 수 있게 해주는 오픈소스입니다.
Docker와 Kubernetes를 실행 기반으로 지원하며, Apache 2.0 라이선스로 공개되어 있습니다. 아래 단락부터는 OpenSandbox가 어떻게 동작하고 활용할 수 있는지 살펴보겠습니다.
1. AI가 테스트 과정을 진행할 때
에이전트가 헬스체크 함수를 수정했다고 가정해보겠습니다. 테스트를 실행했더니 일부 항목이 실패하면 에이전트는 실패 로그를 읽고 조건문을 다시 고친 뒤 같은 테스트를 실행합니다.
이때 로컬에서 테스트를 실행하고 정상으로 확인되어 작업이 끝나면 수정 결과와 테스트 로그는 표시되어야 하지만, 사용한 실행 환경까지 계속 켜두면서 유지할 필요는 없습니다. 따라서 환경을 청소하는 과정이 필요한데, 이제 코드 실행뿐 아니라 테스트 할 서버에 대한 생명주기를 관리하는 것도 하나의 일상 업무로서 자리잡게 되었습니다.
서버의 생명주기 흐름은 기존의 Docker API로도 직접 구현할 수 있습니다. 컨테이너를 만들고 파일을 복사한 뒤 명령을 실행하고 삭제하는 코드를 작성하면 됩니다. 다만 서비스가 늘어나면 파일 전송, 준비 상태 확인, 실행 결과 처리, 만료 관리도 따로따로 구현할 위험이 커집니다.
OpenSandbox는 쉽게 서버를 구성하는 기능을 제공합니다. OpenSandbox를 통해 환경을 구성하면 어떤 코드와 파일을 실행할지 정하고, 공통 API로 실행 환경을 다룰수 있게 됩니다.
따라서 테스트의 통과 기준, 수정 횟수, 결과의 보관 위치는 애플리케이션에서 정하고 OpenSandbox는 테스트 판단에 필요한 코드를 실행할 환경과 API를 제공합니다.
2. OpenSandbox의 구성
실제 사용 방법을 알려드리기 전에 구조를 먼저 보면 사용 흐름도 이해하기 쉽습니다. OpenSandbox 서버는 환경을 관리하고, 샌드박스 안의 execd는 파일과 명령을 처리합니다. 애플리케이션의 SDK가 두 역할에 맞춰 요청을 보냅니다.
환경을 준비하고 정리하는 요청
그림의 위쪽 흐름은 환경 관리 경로입니다. 애플리케이션이 사용할 이미지와 CPU·메모리, 유효 시간을 지정하면 OpenSandbox 서버가 선택한 실행 기반에 환경 생성을 요청합니다. Docker에서는 컨테이너를 만들고, Kubernetes에서는 컨테이너를 실행하는 단위인 Pod를 클러스터에 배치하는 방식입니다.
서버는 샌드박스 조회와 유효 시간 갱신, 종료 요청도 처리합니다. 한 작업에 같은 샌드박스를 유지할지, 실행마다 새 환경을 사용할지는 애플리케이션에서 선택합니다.
파일을 보내고 명령을 실행하는 요청
그림의 아래쪽 흐름은 실제 작업 경로입니다. execd는 샌드박스 안에서 실행되는 서비스로, 파일 읽기·쓰기와 명령 실행을 담당합니다. 애플리케이션이 SDK로 소스 파일을 보내면 샌드박스의 파일시스템에 기록하고, 테스트 명령을 보내면 그 환경 안에서 실행합니다.
작업 공간에는 코드와 테스트 파일, 실행 중인 프로세스와 로그가 놓입니다. 이곳에서 나온 결과를 SDK가 받아 애플리케이션으로 돌려줍니다. 애플리케이션은 로그를 보고 다음 수정을 결정하거나, 작업을 완료하고 환경 종료를 요청합니다.
3. 실패한 테스트 하나를 고치고 확인하기
앞서 말씀 드렸던 테스트 작업에 이어서 결과를 쉽게 확인할 수 있는 작은 예제로 흐름을 살펴보겠습니다. HTTP 상태 코드가 200대일 때만 정상으로 판정해야 하는 헬스체크 함수가 있다고 해봅시다. 조건을 < 500으로 작성했다면 페이지를 찾을 수 없다는 404 응답도 정상으로 처리합니다.
def is_healthy(status_code):
- return status_code < 500
+ return 200 <= status_code < 300
검사할 조건은 세 가지입니다. 200은 정상이어야 하고, 404와 503은 비정상이어야 합니다. test_healthcheck.py에는 다음과 같은 검사를 넣을 수 있습니다.
from healthcheck import is_healthy
assert is_healthy(200)
assert not is_healthy(404)
assert not is_healthy(503)
print("3 checks passed")
수정 전에는 404 검사에서 실패하고, 수정 후에는 마지막 줄까지 실행되어야 합니다. 실제 웹 서버에 접속하지 않고 상태 코드만 입력하므로 네트워크 장애와 함수의 버그를 섞지 않고 볼 수 있습니다.
SDK로 연결한 검증 흐름
이제 같은 샌드박스에서 수정 전후의 코드를 실행해보는 API 흐름을 보겠습니다. 아래는 Python SDK 1.1.0의 핵심 호출을 묶은 함수입니다. 서버 접속 설정인 connection과 수정 전 코드 broken, 수정 후 코드 fixed, 테스트 코드 tests를 문자열로 전달하는 구성을 가정합니다. 서버 준비와 접속 설정은 공식 Python SDK 안내에서 확인해보실 수 있습니다.
from datetime import timedelta
from pathlib import Path
from opensandbox import Sandbox
from opensandbox.models import WriteEntry
from opensandbox.models.execd import RunCommandOpts
async def verify_fix(connection, broken, fixed, tests):
sandbox = await Sandbox.create(
"python:3.12-slim",
connection_config=connection,
resource={"cpu": "1", "memory": "512Mi"},
timeout=timedelta(minutes=5),
)
try:
output = Path("artifacts") / sandbox.id
output.mkdir(parents=True, exist_ok=True)
await sandbox.files.write_files([
WriteEntry(path="/tmp/test_healthcheck.py", data=tests, mode=644),
])
for name, source, expected in (
("before", broken, 1),
("after", fixed, 0),
):
await sandbox.files.write_files([
WriteEntry(path="/tmp/healthcheck.py", data=source, mode=644),
])
log_path = f"/tmp/{name}.log"
result = await sandbox.commands.run(
f"python -B /tmp/test_healthcheck.py > {log_path} 2>&1",
opts=RunCommandOpts(timeout=timedelta(seconds=10)),
)
log = await sandbox.files.read_file(log_path)
(output / f"{name}.log").write_text(log, encoding="utf-8")
print(name, result.exit_code)
if result.exit_code != expected:
raise RuntimeError(f"Unexpected result: {name}; see {output}")
finally:
await sandbox.destroy()
첫 Sandbox.create()는 Python이 들어 있는 이미지로 실행 환경을 요청합니다. 여기서 지정한 1 CPU·512Mi 메모리·5분은 이 작은 예제의 설정값입니다. 실제 저장소의 빌드와 테스트에는 필요한 도구를 포함한 이미지와 적절한 자원을 지정해야 합니다.
write_files()는 먼저 테스트 파일을 보내고, 이어서 수정 전후의 구현을 차례로 보냅니다. /tmp/healthcheck.py는 SDK를 실행하는 개발자 컴퓨터의 경로가 아니라 샌드박스 내부의 경로입니다. 테스트 파일은 한 번만 보내므로 두 구현에 같은 검사를 적용합니다.
commands.run()은 그 안에서 Python을 실행합니다. -B는 실행 중 Python 바이트코드 캐시 파일을 새로 쓰지 않는 옵션입니다. > ... 2>&1은 일반 출력과 오류 출력을 함께 로그에 기록합니다. 실행을 마치면 read_file()로 로그 내용을 가져와 개발자 컴퓨터의 artifacts/<샌드박스 ID>/에 저장합니다.
예상하는 종료 코드는 before에서 1, after에서 0입니다. 첫 실패는 버그가 있는 코드를 확인하기 위한 단계이므로 수정본 실행으로 이어갑니다. 예상과 다른 결과가 나오면 해당 로그를 저장한 뒤 오류를 알립니다. 마지막 destroy()는 성공과 실패에 관계없이 원격 환경 종료를 요청합니다. 여기서 사용한 명령 실행과 파일 API는 Python SDK 안내의 사용 예시에서도 확인하실 수 있습니다.
실제 코딩 에이전트를 붙인다면 before의 실패 로그를 모델에 전달하고, 모델이 고친 구현을 다시 보내는 식으로 확장할 수 있습니다. 수정 횟수의 상한과 통과 기준은 애플리케이션에서 정하고, 테스트 결과를 근거로 다음 단계를 결정합니다.
4. 같은 환경에서 재시도하고, 결과를 남기기
파일을 유지하는 것과 실행을 이어가는 것
같은 샌드박스를 사용하면 코드와 테스트 파일, 설치한 의존성을 이어서 사용할 수 있습니다. 수정할 때마다 저장소를 다시 내려받고 패키지를 설치하지 않아도 된다는 장점이 있습니다. 특히 빌드 준비가 오래 걸리는 저장소라면 반복 작업에서 차이가 생길 수 있습니다.
상태를 유지할 때는 이전 실행의 결과도 함께 남습니다. 첫 테스트가 보고서를 만든 뒤 다음 테스트가 보고서를 갱신하기 전에 실패했다면, 남아 있는 파일을 이번 결과로 잘못 읽을 수 있습니다. 예제에서 before.log와 after.log를 나눈 이유입니다. 실제 서비스에서도 실행 시도마다 로그와 보고서의 경로를 구분하면 원인을 추적하기 편합니다.
환경의 수명과 명령의 제한 시간
코드에서 지정한 5분과 10초는 서로 다른 대상을 제한합니다. 여기에 환경 준비를 기다리는 시간도 별도로 있습니다.
| 설정 | 제한하는 대상 | 사용할 때의 의미 |
|---|---|---|
Sandbox.create(timeout=...) |
샌드박스의 수명 | 작업 환경을 얼마나 유지할지 정합니다. |
Sandbox.create(ready_timeout=...) |
생성 응답 뒤 접속 주소·준비 상태를 확인하는 시간 | 환경을 사용할 수 있을 때까지 기다리는 시간을 정합니다. |
RunCommandOpts(timeout=...) |
명령 한 번의 실행 시간 | 끝나지 않는 테스트나 스크립트의 실행을 제한합니다. |
예를 들어 테스트가 10초 안에 끝나야 하더라도 이미지 다운로드, 파일 전송, 결과 저장까지 10초 안에 끝나는 것은 아닙니다. 사용자에게 작업 완료 시간을 보여주려면 환경 준비부터 결과 보관까지 전체 과정을 살펴야 합니다.
로그를 먼저 가져오고 환경을 종료하기
샌드박스 안의 로그는 작업을 수행하는 동안 유용하지만, 사용자가 내일 다시 확인할 보고서는 서비스 저장소에 남겨야 합니다. 앞의 예제도 로그를 읽어 로컬에 저장한 뒤 환경을 삭제합니다. 서비스에서는 결과 파일이 제대로 보관됐는지 확인한 다음 완료 상태를 표시하는 편이 좋습니다.
Python SDK에서는 close()와 destroy()의 정리 동작이 다릅니다. close()는 로컬 HTTP 연결을 정리하고, destroy()는 원격 샌드박스 종료도 요청합니다. 컨텍스트 관리자인 async with의 종료 처리도 로컬 연결을 닫는 동작이므로, 작업 후 환경까지 없애려면 종료 의도를 명시해야 합니다.
클라이언트가 강제로 종료되면 finally까지 실행되지 못할 수도 있습니다. 이때를 위해 샌드박스의 유효 시간을 지정하고 서버 측 만료 관리도 사용합니다. 애플리케이션의 정상적인 정리 요청과 만료 후 정리가 함께 있어야 중단된 작업이 자원을 계속 차지하는 일을 줄일 수 있습니다.
5. Docker에서 시작하고 Kubernetes로 옮기기
같은 API를 쓸 때 달라지는 운영 준비
Docker 환경에서는 OpenSandbox 서버가 Docker Engine에 연결해 컨테이너를 관리합니다. 개발자 컴퓨터나 단일 서버에서 파일 전송과 명령 실행 흐름을 확인하기에 적합합니다. 서버가 어느 Docker Engine에 접속하는지, SDK와 서버가 샌드박스의 실행 API에 도달할 수 있는지가 기본 확인 사항입니다.
SDK를 실행하는 곳, OpenSandbox 서버가 있는 곳, 컨테이너가 실행되는 곳이 항상 같지는 않습니다. Docker Engine이 별도 머신이나 VM에 있다면 컨테이너 내부 주소가 다른 곳에서 바로 보이지 않을 수 있습니다. 이때는 서버 프록시 사용 여부와 서버가 접근할 주소를 배포 환경에 맞게 설정합니다.
Kubernetes에서는 여기에 클러스터의 자원 배정이 더해집니다. 이미지를 내려받을 권한, Pod가 사용할 저장소, 네트워크 연결, CPU·메모리 할당량을 준비해야 합니다. 파일과 명령을 다루는 애플리케이션 코드를 이어서 사용할 수 있어도, 배포 설정까지 자동으로 같아지는 것은 아닙니다. 자세한 항목은 서버 설정 문서에서 확인할 수 있습니다.
시작 대기 시간이 길다면 이미지와 Pool
테스트 자체는 금방 끝나는데 결과를 받기까지 오래 걸릴 수 있습니다. 이미지가 처음 내려받아지는지, 환경을 만들 때마다 패키지를 설치하는지부터 나누어 보면 좋습니다. 자주 사용하는 도구와 의존성을 이미지에 포함하면 매번 준비하는 일을 줄일 수 있고, 대신 이미지 버전과 갱신을 관리하게 됩니다.
Kubernetes의 Pool은 준비된 Pod를 미리 확보해 요청에 배정하는 기능입니다. 새 Pod를 배치하고 시작하는 대기를 줄이는 데 활용할 수 있습니다. 요청을 기다리는 Pod도 자원을 사용하므로, 준비 시간을 줄이는 효과와 유휴 자원의 비용을 함께 봐야 합니다.
준비된 환경이 모두 사용 중일 때의 대기도 확인해야 합니다. 환경 준비와 테스트 실행 시간을 나누어 기록하고, 이미지 캐시와 동시 작업 수를 맞춰 비교하면 지연이 생기는 곳을 찾기 쉽습니다.
6. 개발자가 활용할 수 있는 방법
코드를 고치고 테스트하는 에이전트
가장 직접적인 활용은 코딩 자동화입니다. 작업할 저장소와 빌드 도구를 준비하고, 에이전트가 구현을 수정한 뒤 테스트 결과를 확인하게 할 수 있습니다. 공식 Claude Code 예제는 에이전트 프로그램 자체를 샌드박스 안에서 실행하는 구성을 보여줍니다.
저장소를 받을 방법, 프로젝트 의존성, 에이전트의 설정과 인증 정보를 함께 준비합니다. 수정한 파일과 테스트 결과를 남기면 사람이 결과를 검토하기도 편합니다.
헬스체크와 설정 검사 스크립트
운영 업무에서도 작은 스크립트를 만들고 확인할 일이 많습니다. 예를 들어 상태 코드 판정, 설정 파일의 필수 항목 검사, 로그에서 특정 오류 찾기처럼 입력과 예상 결과를 정할 수 있는 작업을 먼저 연결할 수 있습니다.
운영 시스템에 바로 접속하기 전에 샘플 설정과 고정된 로그로 결과를 확인하면, 스크립트 자체의 오류를 찾기 쉽습니다. 에이전트가 호출하는 파일 읽기·쓰기와 명령 실행 도구를 OpenSandbox에 연결하는 방식은 Google ADK 예제에서도 볼 수 있습니다.
브라우저로 확인하고 결과를 가져오는 작업
테스트 대상이 웹 화면이라면 브라우저를 실행하고 페이지를 조작한 뒤 스크린샷을 가져올 수 있습니다. 공식 Playwright 예제가 이런 구성을 보여줍니다. 배포한 화면의 버튼과 입력 폼을 확인하거나, 실패한 시점의 화면을 테스트 보고서에 붙이는 작업에 연결해볼 수 있습니다.
브라우저와 관련 도구가 들어 있는 이미지를 준비하면 환경 생성·실행·결과 회수의 흐름을 그대로 적용할 수 있습니다. 작업에 맞는 이미지와 자원을 공통 API로 다루는 것입니다.
실행할 코드에 맞춰 접근 범위 정하기
샌드박스가 어떤 네트워크에 접속할 수 있고 어떤 인증 정보를 사용할지도 함께 정합니다. 테스트에 패키지 다운로드가 필요한지, 사내 API를 조회해야 하는지, 운영 데이터에 쓸 권한까지 필요한지에 따라 구성이 달라집니다.
호스트와의 격리를 더 강화해야 한다면 gVisor나 Kata Containers 같은 실행 방식을 검토할 수 있습니다. 이들은 컨테이너의 시스템 호출을 처리하거나 VM 안에서 실행하는 방식을 통해 격리를 보강합니다. 필요한 런타임의 설치와 선택은 운영자가 준비하며, 설정 방법은 실행 환경의 격리 설정 안내에서 확인할 수 있습니다.
여러 팀에 서비스를 제공할 때는 서로의 환경을 조회하거나 종료할 수 있는지도 확인해야 합니다. OpenSandbox의 멀티테넌트 기능은 API 키를 Kubernetes의 네임스페이스에 연결해 환경 관리 범위를 나눕니다. Docker에는 이 기능이 지원되지 않으므로, 운영할 방식에 맞춰 적용 범위를 확인하는 것이 좋습니다.
7. 실행 결과가 예상과 다를 때 확인할 곳
서버는 응답하는데 명령을 실행하지 못할 때
서버의 /health 응답은 환경 관리 서버에 접속할 수 있다는 확인입니다. 실제 테스트를 실행하려면 이미지 다운로드와 컨테이너 시작을 마치고, 샌드박스 안의 execd까지 접속할 수 있어야 합니다.
먼저 서버 로그에서 이미지나 실행 환경 생성이 실패했는지 봅니다. 환경은 만들어졌는데 준비 확인이 끝나지 않는다면 접속 주소와 프록시 설정을 확인합니다. 그림에서 환경을 만드는 위쪽 경로와 작업을 실행하는 아래쪽 경로를 나누어 보면, 다음에 살펴볼 곳을 좁힐 수 있습니다.
명령의 종료 코드가 예상과 다를 때
앞의 예제는 수정 전의 테스트 실패를 정상적인 검증 단계로 다룹니다. 종료 코드가 0이 아니라고 모두 인프라 장애인 것은 아닙니다. 테스트가 실패한 것인지, 필요한 패키지를 찾지 못한 것인지, 명령 자체를 시작하지 못한 것인지 로그를 함께 읽어야 합니다.
테스트 파일은 샌드박스 안에 있는지, 명령이 기대하는 작업 디렉터리와 파일 경로가 맞는지도 확인합니다. 특히 로컬 파일 경로를 그대로 명령에 넣으면 원격 환경에는 그 파일이 없을 수 있습니다. SDK로 보낸 목적지 경로와 실제 실행 명령을 나란히 보면 이런 실수를 찾기 쉽습니다.
재시도나 동시 실행에서만 문제가 생길 때
한 번의 실행에서는 드러나지 않던 상태가 반복 작업에서 영향을 줄 수 있습니다. 이전 보고서를 다시 읽지 않는지, 두 요청이 같은 샌드박스를 잘못 공유하지 않는지, 남은 프로세스가 포트나 파일을 사용하고 있지는 않은지 확인합니다.
애플리케이션의 작업 ID, 샌드박스 ID, 실행 시도 번호를 함께 기록하면 어떤 환경에서 나온 로그인지 구분하기 좋습니다. 여기에 환경 준비·명령 실행·결과 회수에 걸린 시간을 나누어 남기면, 테스트가 느린 것과 실행할 자리가 늦게 준비된 것을 구분할 수 있습니다.
마치며
OpenSandbox는 환경 생성부터 파일 전송, 코드 실행, 결과 회수와 정리까지 하나의 흐름을 간편하게 정리해줍니다.
물론 기존 CI로 충분히 처리하는 작업이라면 현재 구성을 그대로 사용하는 편이 간단할 수 있습니다. 작업마다 devcontainer를 구성하는 것 보다 더 광범위하게 작업이 이루어지거나, 상태를 유지하며 코드를 반복 실행하는 일이 늘어날 때 OpenSandbox를 살펴보시면 좋겠습니다.
다음에 함께 살펴볼 오픈소스를 알려주세요.
탐구해보고 싶은 오픈소스나 직접 발표하고 싶은 오픈소스가 있다면 organizer@cloudbro.ai로 보내주세요. 프로젝트 링크와 궁금한 점, 또는 발표하고 싶은 내용을 함께 적어주시면 확인 후 Dive into OSS 아티클로 다룰 수 있습니다.

