使用 GHCR 自定义镜像构建 Sandbox 模板

更新时间:
复制 MD 格式

云沙箱支持直接从容器镜像仓库拉取镜像来构建 Sandbox 模板。当业务需要预置特定语言运行时、系统依赖、私有 SDK 或数据科学工具链时,把这些内容打包成一个容器镜像,再从镜像构建模板,比在运行时逐条安装依赖更可控、更可复现。

本文以 GitHub Container Registry(GHCR) 上的私有镜像为例,说明如何通过 OpenAPI CreateTemplate 在中国香港地域(cn-hongkong)从 ghcr.io/<owner>/<image>:<tag> 构建第二代(micro)运行时模板、创建 Sandbox 并校验运行结果。GHCR 属于需要鉴权的私有仓库,创建模板时需要携带拉取凭据;GHCR 是海外仓库,构建地域需要能够访问 ghcr.io,本文选择中国香港地域。

业务场景

  • 团队把统一的 Python/Node 运行时和常用依赖打包成镜像,多个 Agent 共用同一基础模板。

  • 分析类任务需要预装 pandas、numpy、绘图库等重依赖,希望构建期一次装好,而不是每次运行时安装。

  • 私有 SDK 或内部工具只发布到 GHCR 私有仓库,不能公开拉取。

  • 需要把镜像版本与模板版本对齐,做到构建可追溯、可回滚。

前置条件

  • 已开通云沙箱,已创建 Team 并获取 Team ID。

  • 调用 OpenAPI 的阿里云账号或 RAM 用户已配置模板 API 权限(fcsandbox:CreateTemplate、fcsandbox:GetTemplate、fcsandbox:DeleteTemplate),参见构建和管理模板的权限配置。

  • 已获取目标地域的 E2B_API_KEY(用于创建 Sandbox),参见 E2B SDK 接入参数说明。

  • 已在 GHCR 上准备好镜像,且账号与目标地域已开通第二代运行时白名单(未开通请联系技术支持)。第二代运行时仅支持 Ubuntu/Debian 系镜像,且需保留 systemd、openssh-server、sudo、chrony、fuse3、iptables 等系统组件,完整要求见构建和管理模板。满足要求的最小 Dockerfile 示例:

    FROM debian:12
    RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        systemd systemd-sysv openssh-server sudo chrony fuse3 iptables \
        python3 curl ca-certificates \
        && rm -rf /var/lib/apt/lists/*
    CMD ["/sbin/init"]
  • 已创建用于拉取镜像的 GitHub 凭据(见下节 镜像仓库凭据 )。

  • 安装云沙箱 OpenAPI SDK 和 E2B SDK:

pip install alibabacloud_fcsandbox20260509 e2b==2.31.0

镜像仓库凭据

从 GHCR 私有仓库拉取镜像需要携带用户名和密码。这里的凭据是 GitHub 的 Personal Access Token (classic),不是账号登录密码:

  • 用户名:填 GitHub 用户名(镜像所属的 owner)。

  • 密码:填创建的 Personal Access Token (classic) 值(形如 ghp_xxxxxxxx)。

Token 权限要求:Token 只需勾选 read:packages 权限。此外 Token 所属账号必须对该私有包有读取权限;镜像属于开启了 SSO 的组织时,还需在 Token 页面为该组织完成授权。缺少这些权限会导致构建阶段拉取镜像失败(鉴权 401 / 403,可见 GitHub 相关文档)。

创建路径:GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token (classic),在 Select scopes 中勾选 read:packages。

凭据通过 CreateTemplate 请求体的 runtime_config.sandbox_config.registry_config.auth_config 传递:

字段

含义

取值

user_name

镜像仓库用户名

GitHub 用户名

password

镜像仓库密码

Personal Access Token (classic)

凭据属于敏感信息,建议通过环境变量或密钥管理服务注入,不要硬编码进代码或提交进代码仓库。

推荐流程

建议把“自定义镜像 → 模板 → Sandbox”拆成四步:

  1. 调用 CreateTemplate 创建模板,sandbox_config.image 指定 GHCR 镜像地址、generation 指定为 2(第二代 micro 运行时)、registry_config.auth_config 传入仓库凭据,拿到 template_id。

  2. 轮询 GetTemplate 直到构建状态进入终态(ready / error)。

  3. 用 template_id 创建 Sandbox,等待数据面就绪后执行命令做冒烟校验。

  4. 校验通过后销毁 Sandbox,避免资源长期占用;模板确认不再使用时调用 DeleteTemplate 删除。

其中模板构建是一次性动作:同一镜像构建出的 template_id 可以被后续多次 Sandbox.create 复用,生产上通常把构建和运行拆成两个阶段,而不是每次运行都重新构建。

示例代码

以下示例从 GHCR 私有镜像 ghcr.io/<owner>/<image>:<tag> 构建第二代运行时模板,创建 Sandbox,执行命令打印校验标记,最后销毁 Sandbox。阿里云 AK、GitHub 凭据、E2B_API_KEY 均从环境变量读取。

重要

cpu 单位为核,memory_size、disk_size 单位均为 MB;三项均可缺省,缺省时使用默认规格 2 核 / 2048 MB / 15360 MB。规格不足会导致模板构建或 Sandbox 启动失败:内存不足时镜像内进程被 OOM 终止,CPU 不足会拖慢构建与冷启动,磁盘不足会导致文件写入失败。第二代运行时(generation 为 2)配置的磁盘大小包含镜像自身占用,实际可写空间 = disk_size - 镜像大小,需按镜像解压后体积,加上工作负载的峰值内存与磁盘写入量取值并留出余量。

import os
import time

from alibabacloud_fcsandbox20260509 import models
from alibabacloud_fcsandbox20260509.client import Client as FCSandboxClient
from alibabacloud_tea_openapi import models as open_api_models
from e2b import Sandbox

REGION = "cn-hongkong"
POP_ENDPOINT = f"fcsandbox.{REGION}.aliyuncs.com"
API_URL = f"https://api.{REGION}.e2b.fc.aliyuncs.com"
DOMAIN = f"{REGION}.e2b.fc.aliyuncs.com"
SOURCE_IMAGE = "ghcr.io/<owner>/<image>:<tag>"
SANDBOX_MARKER = "sandbox-ok"
READY_TIMEOUT_SECONDS = 90
READY_INTERVAL_SECONDS = 3


def required_env(name: str) -> str:
    value = os.environ.get(name, "").strip()
    if not value:
        raise RuntimeError(f"缺少环境变量: {name}")
    return value


def main() -> int:
    team_id = required_env("FCSANDBOX_TEAM_ID")
    pop_client = FCSandboxClient(
        open_api_models.Config(
            access_key_id=required_env("ALIBABA_CLOUD_ACCESS_KEY_ID"),
            access_key_secret=required_env("ALIBABA_CLOUD_ACCESS_KEY_SECRET"),
            region_id=REGION,
            endpoint=POP_ENDPOINT,
        )
    )

    # 1. 创建模板:第二代 micro 运行时 + GHCR 私有镜像 + 拉取凭据,创建后立即开始首次构建
    response = pop_client.create_template(
        models.CreateTemplateRequest(
            body=models.CreateTemplateInput(
                name=f"ghcr-demo-{int(time.time())}",
                team_id=team_id,
                runtime_config=models.CreateTemplateRuntimeConfig(
                    cpu=2,
                    memory_size=2048,
                    disk_size=15360,
                    sandbox_config=models.CreateTemplateSandboxConfig(
                        image=SOURCE_IMAGE,
                        generation=2,
                        registry_config=models.CreateTemplateRegistryConfig(
                            auth_config=models.CreateTemplateRegistryAuthConfig(
                                user_name=required_env("GHCR_USERNAME"),
                                password=required_env("GHCR_PASSWORD"),
                            ),
                        ),
                    ),
                ),
            )
        )
    )
    template_id = response.body.template_id
    print(f"template_id={template_id}")

    # 2. 轮询 GetTemplate 直到构建进入终态
    deadline = time.time() + 900
    while True:
        got = pop_client.get_template(template_id, models.GetTemplateRequest(team_id=team_id))
        state = got.body.status.state
        print(f"state={state}")
        if state == "ready":
            break
        if state == "error":
            reason = got.body.status.reason.message if got.body.status.reason else ""
            raise RuntimeError(f"构建失败: {reason}")
        if time.time() > deadline:
            raise RuntimeError("构建超时")
        time.sleep(10)

    # 3. 用 template_id 创建 Sandbox
    sandbox = Sandbox.create(
        template=template_id,
        timeout=900,
        api_key=required_env("E2B_API_KEY"),
        api_url=API_URL,
        domain=DOMAIN,
    )
    print(f"sandbox_id={sandbox.sandbox_id}")

    try:
        # 等待数据面就绪并执行冒烟校验
        last_error = None
        deadline = time.monotonic() + READY_TIMEOUT_SECONDS
        while True:
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                raise RuntimeError(f"数据面等待超时: {last_error}") from last_error
            try:
                result = sandbox.commands.run(f"echo {SANDBOX_MARKER}", timeout=min(60, remaining))
                break
            except Exception as err:
                last_error = err
                time.sleep(min(READY_INTERVAL_SECONDS, max(deadline - time.monotonic(), 0)))
        if result.exit_code != 0 or SANDBOX_MARKER not in result.stdout:
            raise RuntimeError(f"校验失败: exit={result.exit_code}, stdout={result.stdout}")
        print(f"stdout={result.stdout.strip()}")
    finally:
        # 4. 销毁 Sandbox
        sandbox.kill()

    # 模板可长期复用;确认不再使用时删除模板清理资源
    # pop_client.delete_template(template_id, models.DeleteTemplateRequest(team_id=team_id))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

运行方式(凭据放在环境变量中):

export ALIBABA_CLOUD_ACCESS_KEY_ID="<your-access-key-id>"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="<your-access-key-secret>"
export FCSANDBOX_TEAM_ID="<your-team-id>"
export GHCR_USERNAME="<github-username>"
export GHCR_PASSWORD="ghp_xxxxxxxxxxxxxxxxxxxx"   # Personal Access Token (classic)
export E2B_API_KEY="<your-e2b-api-key>"
python3 build_from_ghcr.py

正常输出会依次打印 template_id 、构建状态、sandbox_id ,以及校验标记 sandbox-ok ;脚本最后会销毁 Sandbox。

上线建议

  • 凭据管理:GitHub Personal Access Token (classic)、阿里云 AK 和 E2B_API_KEY 通过环境变量或密钥管理服务注入,不要写进代码或提交进 Git;Token 遵循最小权限原则,仅勾选 read:packages,并定期轮转。

  • 构建与运行分离:模板构建是一次性动作,构建产出的 template_id 应持久化保存并复用;不要在每次创建 Sandbox 前重复构建。

  • 镜像版本化:镜像使用明确的 tag(避免只用 latest),做到模板与镜像版本对齐、可回溯、可回滚。

  • 资源规格 :cpu (核数)、memory_size (MB)、disk_size 按镜像内工作负载评估,缺省时使用默认规格。

  • 及时销毁:任务结束后调用 sandbox.kill()(放在 finally 中),避免异常路径下 Sandbox 长期占用;模板确认不再使用时调用 DeleteTemplate 删除。

相关功能