从集群内部触发 GKE Agent Sandbox 快照

在集群中使用 Google Kubernetes Engine (GKE) 快照上的 Agent Sandbox,以保留沙盒化环境的状态。这种方法可提供一致的调试、加速开发迭代,并有助于快速恢复复杂的设置。在本教程中,您将通过运行一个客户端应用来部署和测试此功能,该应用会以程序化方式在集群内创建、暂停和恢复沙盒化环境。

如需详细了解如何创建 Pod 快照,请参阅从 Pod 快照恢复。

费用

GKE 上的 Agent Sandbox 是一款开源软件,您可以免费使用。GKE 价格适用于您创建的资源,包括 Agent Sandbox 控制器使用的计算资源。

准备工作

  1. 在 Google Cloud 控制台的项目选择器页面上,选择或创建 Google Cloud 项目。

    选择或创建项目所需的角色

    • 选择项目:选择项目不需要特定的 IAM 角色,您可以选择已获授角色的任何项目。
    • 创建项目:如需创建项目,您需要拥有 Project Creator 角色 (roles/resourcemanager.projectCreator),该角色包含 resourcemanager.projects.create 权限。了解如何授予角色。

    转到“项目选择器”

  2. 验证是否已为您的 Google Cloud 项目启用结算功能。

  3. 启用 Artifact Registry API、Kubernetes Engine API、Cloud Build API(如果尚未启用)。

    启用 API 所需的角色

    如需启用 API,您需要拥有 serviceusage.services.enable 权限。如果您创建了项目,则可能已经通过 Owner 角色 (roles/owner) 拥有此权限。否则,您可以通过 Service Usage Admin 角色 (roles/serviceusage.serviceUsageAdmin) 获得此权限。了解如何授予角色。

    启用 API

  4. 在 Google Cloud 控制台中,激活 Cloud Shell。

    激活 Cloud Shell

  5. 验证您是否拥有完成本教程所需的权限。

所需的角色

如需获得创建和管理沙盒所需的权限,请让您的管理员为您授予项目的 Kubernetes Engine Admin (roles/container.admin) IAM 角色。如需详细了解如何授予角色,请参阅管理对项目、文件夹和组织的访问权限。

您也可以通过自定义角色或其他预定义角色来获取所需的权限。

限制

Pod 快照需要 gVisor,不适用于 microVM 沙盒。

在区域级集群中,不同可用区中的节点可能具有不同的 CPU 微架构。由于快照会捕获 CPU 状态,因此在缺少 CPU 功能的节点上恢复快照会失败(例如,出现错误 OCI runtime restore failed: incompatible FeatureSet)。

为避免此问题,请为您的环境使用适当的配置:

  • 生产环境:为在整个集群中保持高可用性,请勿将工作负载固定到特定可用区。而是通过指定满足最低要求的 CPU 平台,帮助确保所有可用区中的 CPU 功能一致。如需了解详情,请参阅选择满足最低要求的 CPU 平台。
  • 测试:为简化设置并避免初始 CPU 不匹配错误,您可以在 SandboxTemplate 清单中使用 nodeSelector 字段将 Pod 固定到特定可用区,例如 us-central1-a。本教程中的示例使用此测试配置。

定义环境变量

为了简化您在本教程中运行的命令,您可以在 Cloud Shell 中设置环境变量。在 Cloud Shell 中,运行以下命令来定义以下有用的环境变量:

export PROJECT_ID=$(gcloud config get project)
export PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")
export CLUSTER_NAME="test-snapshot"
export LOCATION="us-central1"
export ZONE="us-central1-a"
export BUCKET_LOCATION="us"
export MACHINE_TYPE="n2-standard-2"
export REPOSITORY_NAME="agent-sandbox"
export BUCKET_NAME="${PROJECT_ID}_snapshots"
export CLOUDBUILD_BUCKET_NAME="${PROJECT_ID}_cloudbuild"

以下是这些环境变量的说明:

  • PROJECT_ID:当前 Google Cloud 项目的 ID。本文档中的命令使用此 ID 来命名资源和授予 IAM 权限。
  • PROJECT_NUMBER:当前 Google Cloud 项目的项目编号。
  • CLUSTER_NAME:GKE 集群的名称,例如 test-snapshot。
  • LOCATION:Artifact Registry 仓库的 Google Cloud 区域(例如 us-central1)。如果您创建 Autopilot 集群,该集群也会使用此区域。
  • ZONE:集群所在的可用区。如果您创建 Standard 集群,请将其设置为 LOCATION 区域中的可用区(例如 us-central1-a)。您稍后在本教程中创建的沙盒模板会在 us-central1-a 中运行沙盒。如果您使用其他可用区,请同时更改模板中的可用区。
  • BUCKET_LOCATION:Cloud Storage 存储桶的位置,例如 us。
  • BUCKET_NAME:用于快照的 Cloud Storage 存储桶的名称,例如 PROJECT_ID_snapshots。存储桶名称必须是全局唯一的。
  • CLOUDBUILD_BUCKET_NAME:用于 Cloud Build 日志的 Cloud Storage 存储桶的名称。此值是默认的 Cloud Build 存储桶名称,因此请勿更改。
  • MACHINE_TYPE:用于集群节点和 gVisor 节点池的机器类型,例如 n2-standard-2。Pod 快照不支持 E2 机器类型。
  • REPOSITORY_NAME:Artifact Registry 代码库的名称,例如 agent-sandbox。

配置步骤概览

如需在集群内启用和测试 GKE 环境中 Agent Sandbox 的 Pod 快照,您需要执行几个配置步骤。为了理解这些步骤,您不妨先了解一下整个工作流中涉及的组件。

关键组件

本教程使用以下两个 Python 应用来测试快照过程:

  • 客户端应用:在集群中的标准 Pod 中运行的 Python 脚本。此应用负责管理沙盒生命周期:它会以编程方式创建沙盒、暂停沙盒以触发快照、恢复沙盒,并验证状态是否已保留。在本教程中,您将创建一个名为 agent-sandbox-client-sa 的 Kubernetes 服务账号,并向其授予 RBAC 权限,以便客户端应用 Pod 可以使用 Kubernetes API 管理沙盒自定义资源和快照触发器对象。
  • 沙盒化应用:一个 Python 脚本,每秒递增并输出一个计数器。此应用在隔离的沙盒环境中安全运行,以生成客户端应用可以验证的变化状态。在本教程中,您将创建一个名为 snapshot-sa 的专用 Kubernetes 服务账号,并配置 Workload Identity 以授权沙盒化 Pod 安全地读取和写入 Cloud Storage 中的快照对象。

配置和测试流程

下表总结了设置环境和运行测试所需的步骤:

  1. 创建集群:创建启用了 Pod 快照的 Autopilot 或 Standard 集群。
  2. 创建 Artifact Registry 制品库:创建一个 Docker 制品库,用于存储客户端应用的容器映像。
  3. 安装 Agent Sandbox:在集群上安装核心 agent-sandbox 组件和扩展程序。
  4. 配置存储和权限:创建 Cloud Storage 存储桶并配置 Workload Identity 权限,以便安全地保存快照。
  5. 配置 Pod 快照:创建并应用快照存储配置、快照政策、沙盒模板和温池。
  6. 构建客户端应用:为客户端应用构建容器映像,并将其推送到 Artifact Registry 代码库。
  7. 运行测试:部署客户端应用 Pod,该 Pod 会创建沙盒、暂停沙盒以捕获快照、恢复沙盒,并验证计数器的状态是否已成功恢复。

创建集群

创建启用了 Pod 快照的新 GKE 集群。为实现完整的功能兼容性,请指定快速发布渠道。

Autopilot

创建具有所需功能的 Autopilot 集群:

gcloud container clusters create-auto ${CLUSTER_NAME} \
   --enable-pod-snapshots \
   --release-channel=rapid \
   --location=${LOCATION}

标准

创建具有所需功能的 Standard 集群。为降低费用,以下命令会创建一个可用区级集群,该集群在一个可用区中运行其控制平面和节点:

gcloud container clusters create ${CLUSTER_NAME} \
   --enable-pod-snapshots \
   --release-channel=rapid \
   --machine-type=${MACHINE_TYPE} \
   --workload-pool=${PROJECT_ID}.svc.id.goog \
   --workload-metadata=GKE_METADATA \
   --num-nodes=1 \
   --location=${ZONE}

创建启用了 gVisor 的节点池:

gcloud container node-pools create gvisor-pool \
   --cluster ${CLUSTER_NAME} \
   --machine-type=${MACHINE_TYPE} \
   --num-nodes=1 \
   --location=${ZONE} \
   --project=${PROJECT_ID} \
   --sandbox type=gvisor

创建 Artifact Registry 仓库

在 Artifact Registry 中创建一个 Docker 仓库,用于存储客户端应用(创建和管理沙盒的应用)的容器映像:

gcloud artifacts repositories create ${REPOSITORY_NAME} \
   --repository-format=docker \
   --location=${LOCATION} \
   --description="Docker repository for Agent Sandbox"

安装 Agent Sandbox

在集群上安装 Agent Sandbox 核心组件和扩展程序。本教程使用 1.0.6 版:

# Install the core agent-sandbox components and the extensions (e.g., Warm Pools, Claims)
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v1.0.6/sandbox-with-extensions.yaml

如需详细了解如何安装 Agent Sandbox,请参阅在 GKE 上安装 Agent Sandbox。

配置存储空间和权限

配置 Cloud Storage 存储桶以存储 Pod 快照,并向 snapshot-sa 服务账号和 GKE 服务代理授予所需的 Workload Identity 权限。这样,您的沙盒化工作负载就可以安全地保存和检索快照对象:

  1. 创建新的 Cloud Storage 存储桶:

    gcloud storage buckets create "gs://${BUCKET_NAME}" \
        --uniform-bucket-level-access \
        --enable-hierarchical-namespace \
        --soft-delete-duration=0d \
        --location="${BUCKET_LOCATION}"
    
  2. 在 default 命名空间中创建 Kubernetes 服务账号。沙盒化应用(Python 计数器脚本)使用此身份向外部 API 进行身份验证,并安全地访问存储在 Cloud Storage 对象存储区中的快照对象:

    kubectl create serviceaccount "snapshot-sa" \
        --namespace "default"
    
  3. 使用 Workload Identity 将 storage.bucketViewer 角色绑定到您的服务账号。此角色允许沙盒化工作负载列出存储桶内容并找到特定快照:

    gcloud storage buckets add-iam-policy-binding "gs://${BUCKET_NAME}" \
        --member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/default/sa/snapshot-sa" \
        --role="roles/storage.bucketViewer"
    
  4. 使用 Workload Identity 将 storage.objectUser 角色绑定到您的服务账号。此角色提供读取、保存和删除存储桶中快照二进制对象的权限:

    gcloud storage buckets add-iam-policy-binding "gs://${BUCKET_NAME}" \
        --member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/default/sa/snapshot-sa" \
        --role="roles/storage.objectUser"
    
  5. 向 GKE 服务代理授予管理(创建、列出、读取和删除)存储桶中快照对象的权限:

    gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
        --member="serviceAccount:service-${PROJECT_NUMBER}@container-engine-robot.iam.gserviceaccount.com" \
        --role="roles/storage.objectUser" \
        --condition="expression=resource.name.startsWith(\"projects/_/buckets/${BUCKET_NAME}\"),title=restrict_to_bucket,description=Restricts access to one bucket only"
    

配置 Pod 快照

创建并应用配置文件,以安装所需的 Kubernetes 自定义资源。这些资源定义了集群如何存储和管理 Pod 快照:

  • PodSnapshotStorageConfig:指定用于存储快照二进制对象的 Cloud Storage 存储桶。
  • PodSnapshotPolicy:定义如何手动触发快照、快照的分组频率及其保留政策。
  • SandboxTemplate:定义用于运行隔离的沙盒化工作负载的底层容器、节点选择器和服务账号。
  • SandboxWarmPool:保留一个根据 SandboxTemplate 资源创建的预热沙盒,供客户端应用随时声明。
  1. 为测试客户端文件创建新目录:

    mkdir test_client
    
  2. 前往新目录:

    cd test_client
    
  3. 创建一个名为 snapshot_storage_config.yaml 的文件。此配置指定集群保存二进制 Pod 快照状态的目标 Cloud Storage 存储桶:

    apiVersion: podsnapshot.gke.io/v1
    kind: PodSnapshotStorageConfig
    metadata:
      name: example-pod-snapshot-storage-config
    spec:
      snapshotStorageConfig:
        gcs:
          bucket: "$BUCKET_NAME"
    
  4. 替换配置文件中的环境变量占位符:

    sed -i "s/\$BUCKET_NAME/${BUCKET_NAME}/g" snapshot_storage_config.yaml
    
  5. 应用存储配置清单:

    kubectl apply -f snapshot_storage_config.yaml
    
  6. 等待存储配置准备就绪:

    kubectl wait --for=condition=Ready podsnapshotstorageconfig/example-pod-snapshot-storage-config --timeout=60s
    
  7. 创建一个名为 snapshot_policy.yaml 的文件。此配置会建立一条保留规则,为沙盒化工作负载最多保留两个快照。触发器类型设置为 manual:这允许客户端应用按需控制快照:

    apiVersion: podsnapshot.gke.io/v1
    kind: PodSnapshotPolicy
    metadata:
      name: example-pod-snapshot-policy
      namespace: default
    spec:
      storageConfigName: example-pod-snapshot-storage-config
      selector:
        matchLabels:
          app: agent-sandbox-workload
      triggerConfig:
        type: manual
        postCheckpoint: resume
      snapshotGroupingRules:
        groupByLabelValue:
          labels: ["agents.x-k8s.io/sandbox-name-hash", "tenant-id", "user-id"]
          groupRetentionPolicy:
            maxSnapshotCountPerGroup: 2
    
  8. 应用快照政策清单:

    kubectl apply -f snapshot_policy.yaml
    
  9. 创建一个名为 python-counter-template.yaml 的文件。此配置定义了沙盒 Pod,并为其分配了 snapshot-sa 服务账号身份。此分配有助于确保沙盒安全运行。在该 Pod 中,沙盒化应用(Python 脚本)会持续将递增计数器输出到容器日志:

    apiVersion: extensions.agents.x-k8s.io/v1beta1
    kind: SandboxTemplate
    metadata:
      name: python-counter-template
      namespace: default
    spec:
      podTemplate:
        metadata:
          labels:
            app: agent-sandbox-workload
        spec:
          serviceAccountName: snapshot-sa
          runtimeClassName: gvisor
          nodeSelector:
            topology.kubernetes.io/zone: us-central1-a # Pin to a zone to avoid CPU mismatch during restore
          containers:
          - name: python-counter
            image: python:3.13-slim
            command: ["python3", "-c"]
            args:
              - |
                import time
                i = 0
                while True:
                  print(f"Count: {i}", flush=True)
                  i += 1
                  time.sleep(1)
    
  10. 应用沙盒模板清单:

    kubectl apply -f python-counter-template.yaml
    
  11. 创建一个名为 python-counter-pool.yaml 的文件。此配置会创建一个温池,其中包含一个根据 python-counter-template 模板创建的预热沙盒,可供客户端应用声明:

    apiVersion: extensions.agents.x-k8s.io/v1beta1
    kind: SandboxWarmPool
    metadata:
      name: python-counter-pool
      namespace: default
    spec:
      replicas: 1
      sandboxTemplateRef:
        name: python-counter-template
    
  12. 应用温池清单:

    kubectl apply -f python-counter-pool.yaml
    

构建客户端应用

为客户端应用创建容器映像,并将其上传到 Artifact Registry。

  1. 创建一个名为 Dockerfile.client 的文件。此文件定义了客户端应用的 Python 运行时环境和依赖项:

    FROM python:3.13-slim
    
    WORKDIR /app
    
    RUN pip install "k8s-agent-sandbox[tracing]==1.0.6"
    
    # Copy test script
    COPY client_test.py /app/client_test.py
    
    CMD ["python", "/app/client_test.py"]
    
  2. 创建一个名为 client_test.py 的文件。此脚本会管理沙盒生命周期,并验证在创建快照后状态是否成功恢复:

    import time
    import logging
    import re
    from kubernetes import config, client
    from k8s_agent_sandbox.gke_extensions.snapshots import PodSnapshotSandboxClient
    
    logging.basicConfig(level=logging.INFO)
    
    def get_last_count(pod_name, namespace):
        v1 = client.CoreV1Api()
        try:
            logs = v1.read_namespaced_pod_log(name=pod_name, namespace=namespace)
            counts = re.findall(r"Count: (\d+)", logs)
            if counts:
                return int(counts[-1])
            return None
        except Exception as e:
            logging.error(f"Failed to read logs for pod {pod_name}: {e}")
            return None
    
    def get_current_count(sandbox):
        # The sandbox's Pod has the same name as the Sandbox resource.
        pod_name = sandbox.get_pod_name()
        if not pod_name:
            logging.error(f"Could not determine pod name for sandbox {sandbox.sandbox_id}")
            return None
        return get_last_count(pod_name, sandbox.namespace)
    
    def suspend_sandbox(sandbox):
        logging.info("Pausing sandbox (using snapshots)...")
        try:
            suspend_resp = sandbox.suspend(snapshot_before_suspend=True)
            if suspend_resp.success:
                logging.info("Sandbox paused successfully.")
                if suspend_resp.snapshot_response:
                    logging.info(f"Snapshot created: {suspend_resp.snapshot_response.snapshot_uid}")
                return suspend_resp
            else:
                logging.error(f"Failed to pause: {suspend_resp.error_reason}")
                exit(1)
        except Exception as e:
            logging.error(f"Failed to pause sandbox: {e}")
            exit(1)
    
    def resume_sandbox(sandbox):
        logging.info("Resuming sandbox (using snapshots)...")
        try:
            resume_resp = sandbox.resume()
            if resume_resp.success:
                logging.info("Sandbox resumed successfully.")
                if resume_resp.restored_from_snapshot:
                    logging.info(f"Restored from snapshot: {resume_resp.snapshot_uid}")
                return resume_resp
            else:
                logging.error(f"Failed to resume: {resume_resp.error_reason}")
                exit(1)
        except Exception as e:
            logging.error(f"Failed to resume sandbox: {e}")
            exit(1)
    
    def verify_continuity(count_before, count_after):
        if count_before is not None and count_after is not None:
            logging.info(f"Verification: Count before={count_before}, Count after={count_after}")
            if count_after >= count_before:
                logging.info("SUCCESS: Sandbox resumed from where it left off (or later).")
            else:
                logging.error("FAIL: Sandbox counter reset or went backwards!")
        else:
            logging.warning("Could not verify counter continuity.")
    
    def main():
        try:
            config.load_incluster_config()
        except config.ConfigException:
            config.load_kube_config()
    
        client_reg = PodSnapshotSandboxClient()
    
        logging.info("Creating sandbox...")
        sandbox = client_reg.create_sandbox(warmpool="python-counter-pool", namespace="default")
        logging.info(f"Sandbox created with ID: {sandbox.sandbox_id}")
    
        logging.info("Waiting for sandbox to run...")
        time.sleep(10)
    
        count_before = get_current_count(sandbox)
        logging.info(f"Count before suspend: {count_before}")
    
        suspend_sandbox(sandbox)
    
        logging.info("Waiting 10 seconds...")
        time.sleep(10)
    
        resume_sandbox(sandbox)
    
        logging.info("Waiting for sandbox to be ready again...")
        time.sleep(10)
    
        count_after = get_current_count(sandbox)
        logging.info(f"Count after resume: {count_after}")
    
        verify_continuity(count_before, count_after)
    
        logging.info("Snapshot test completed successfully.")
    
    if __name__ == "__main__":
        main()
    
  3. 构建客户端容器映像并将其上传到 Artifact Registry。如果您的环境(例如 Cloud Shell)安装了 Docker,则可以使用 Docker 在本地构建映像。如果您在没有 Docker 的环境中工作,则可以使用 Cloud Build 远程构建和推送映像:

    Docker

    1. 为 Artifact Registry 配置 Docker 身份验证:

      gcloud auth configure-docker "${LOCATION}-docker.pkg.dev"
      
    2. 在本地构建并推送客户端容器映像:

      docker build -t "${LOCATION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY_NAME}/sandbox-client:latest" -f Dockerfile.client .
      docker push "${LOCATION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY_NAME}/sandbox-client:latest"
      

    Cloud Build

    1. 创建一个名为 cloudbuild.yaml 的文件:

      steps:
      - name: 'gcr.io/cloud-builders/docker'
        args: ['build', '-t', '$LOCATION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY_NAME/sandbox-client:latest', '-f', 'Dockerfile.client', '.']
      images:
      - '$LOCATION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY_NAME/sandbox-client:latest'
      
    2. 替换配置文件中的环境变量占位符:

      sed -i "s/\$REPOSITORY_NAME/${REPOSITORY_NAME}/g" cloudbuild.yaml
      sed -i "s/\$LOCATION/${LOCATION}/g" cloudbuild.yaml
      sed -i "s/\$PROJECT_ID/${PROJECT_ID}/g" cloudbuild.yaml
      
    3. 为 Cloud Build 日志创建 Cloud Storage 存储桶。 如果您的项目之前使用过 Cloud Build,则此存储桶可能已存在,并且该命令会失败,并显示一条错误消息,指出您已拥有该存储桶。您可以放心地忽略此错误:

      gcloud storage buckets create "gs://${CLOUDBUILD_BUCKET_NAME}" \
          --location="${BUCKET_LOCATION}"
      
    4. 向 Cloud Build 服务账号授予必要的权限。

      以下命令使用 Compute Engine 默认服务账号 ($PROJECT_NUMBER-compute@developer.gserviceaccount.com),这是大多数项目的默认账号。如果您的项目使用旧版 Cloud Build 服务账号,请将以下命令中的成员邮箱替换为 $PROJECT_NUMBER@cloudbuild.gserviceaccount.com。

      由于您在本教程前面部分向项目添加了条件 IAM 绑定,因此在添加这些新绑定时,应添加 --condition=None 标志。这可以防止 gcloud 暂停以请求交互式确认:

      gcloud projects add-iam-policy-binding ${PROJECT_ID} \
         --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
         --role="roles/artifactregistry.writer" \
         --condition=None
      
      gcloud projects add-iam-policy-binding ${PROJECT_ID} \
         --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
         --role="roles/logging.logWriter" \
         --condition=None
      
      gcloud storage buckets add-iam-policy-binding "gs://${CLOUDBUILD_BUCKET_NAME}" \
         --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
         --role="roles/storage.objectAdmin"
      
    5. 使用 Cloud Build 运行构建:

      gcloud builds submit --config cloudbuild.yaml
      

运行测试

部署客户端应用以创建沙盒、触发快照,并验证内部计数器是否成功从其保存的状态恢复。

  1. 创建一个名为 client_sa.yaml 的文件。此清单定义了 agent-sandbox-client-sa 服务账号及其管理沙盒自定义资源所需的 RBAC 权限:

    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: agent-sandbox-client-sa
      namespace: default
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: agent-sandbox-client-role
      namespace: default
    rules:
    - apiGroups: ["agents.x-k8s.io"]
      resources: ["sandboxes"]
      verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
    - apiGroups: ["extensions.agents.x-k8s.io"]
      resources: ["sandboxclaims"]
      verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
    - apiGroups: ["podsnapshot.gke.io"]
      resources: ["podsnapshotmanualtriggers", "podsnapshots"]
      verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
    - apiGroups: [""]
      resources: ["pods", "pods/log"]
      verbs: ["get", "list", "watch"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: agent-sandbox-client-rolebinding
      namespace: default
    subjects:
    - kind: ServiceAccount
      name: agent-sandbox-client-sa
      namespace: default
    roleRef:
      kind: Role
      name: agent-sandbox-client-role
      apiGroup: rbac.authorization.k8s.io
    
  2. 应用客户端服务账号和 RBAC 清单:

    kubectl apply -f client_sa.yaml
    
  3. 创建一个名为 client_pod.yaml 的文件。此清单使用预构建的容器映像创建客户端应用 Pod:

    apiVersion: v1
    kind: Pod
    metadata:
      name: agent-sandbox-client-pod
      namespace: default
    spec:
      serviceAccountName: agent-sandbox-client-sa
      containers:
      - name: client
        image: $LOCATION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY_NAME/sandbox-client:latest
        imagePullPolicy: Always
      restartPolicy: Never
    
  4. 替换清单中的环境变量占位符:

    sed -i "s/\$REPOSITORY_NAME/${REPOSITORY_NAME}/g" client_pod.yaml
    sed -i "s/\$LOCATION/${LOCATION}/g" client_pod.yaml
    sed -i "s/\$PROJECT_ID/${PROJECT_ID}/g" client_pod.yaml
    
  5. 等待预热池的沙盒准备就绪:

    kubectl wait sandboxwarmpool python-counter-pool \
        --for=jsonpath='{.status.readyReplicas}'=1 \
        --timeout=600s
    

    在下一步中,客户端应用会创建 SandboxClaim 资源。如果应用在池的沙盒准备就绪之前创建声明,则控制器可能会为声明创建新沙盒,而不是从池中分配沙盒。在 Autopilot 集群中,沙盒可能需要几分钟才能准备就绪,因为 GKE 可能需要为其创建节点。

  6. 应用客户端应用 Pod 清单:

    kubectl apply -f client_pod.yaml
    
  7. 流式传输 Pod 日志以验证执行流程:

    kubectl logs -f agent-sandbox-client-pod
    

当测试正常运行时,输出类似于以下内容(此处为便于阅读而进行了缩短):

2026-04-21 23:02:39,030 - INFO - Creating sandbox...
...
2026-04-21 23:02:51,755 - INFO - Count before suspend: 23
2026-04-21 23:02:51,755 - INFO - Pausing sandbox (using snapshots)...
...
2026-04-21 23:03:07,115 - INFO - Resuming sandbox (using snapshots)...
...
2026-04-21 23:03:21,329 - INFO - Count after resume: 38
2026-04-21 23:03:21,329 - INFO - Verification: Count before=23, Count after=38
2026-04-21 23:03:21,329 - INFO - SUCCESS: Sandbox resumed from where it left off (or later).

输出显示,沙盒在暂停和恢复时成功保留了其状态。沙盒暂停期间,计数器会停止递增,因为 Agent Sandbox 控制器会删除沙盒的 Pod。沙盒恢复后,控制器会重新创建 Pod,Pod 会从快照恢复,计数器会继续计数。如果不暂停,计数器在暂停期间会继续递增,计数会明显更高。

清理资源

为避免系统向您的 Google Cloud 账号收取费用,请删除您创建的资源:

  1. 删除 GKE 集群。 此操作还会删除节点池及其中的所有 Kubernetes 服务账号:

    Autopilot

    gcloud container clusters delete ${CLUSTER_NAME} \
        --location="${LOCATION}" --quiet
    

    标准

    gcloud container clusters delete ${CLUSTER_NAME} \
        --location="${ZONE}" --quiet
    
  2. 删除 Artifact Registry 代码库,以移除您为测试映像创建的 Docker 代码库:

    gcloud artifacts repositories delete ${REPOSITORY_NAME} --location="${LOCATION}" --quiet
    
  3. 删除 Cloud Storage 存储桶以及其中的所有快照。 这会自动移除应用于该存储桶的存储桶级 Workload Identity IAM 绑定:

    gcloud storage rm --recursive "gs://${BUCKET_NAME}"
    
  4. 移除 GKE 服务代理的项目级 IAM 绑定:

    gcloud projects remove-iam-policy-binding "${PROJECT_ID}" \
        --member="serviceAccount:service-${PROJECT_NUMBER}@container-engine-robot.iam.gserviceaccount.com" \
        --role="roles/storage.objectUser" \
        --condition="expression=resource.name.startsWith(\"projects/_/buckets/${BUCKET_NAME}\"),title=restrict_to_bucket,description=Restricts access to one bucket only"
    
  5. 如果您使用 Cloud Build 而不是 Docker 来构建和推送容器映像,请清理 Cloud Build 资源。仅删除您为本教程创建的资源,因为项目中的其他工作负载可能依赖于之前存在的资源:

    • 如果 Cloud Build 存储桶在您开始本教程之前不存在,请将其删除:

      gcloud storage rm --recursive "gs://${CLOUDBUILD_BUCKET_NAME}"
      
    • 如果 Compute Engine 默认服务账号在您开始本教程之前没有 roles/artifactregistry.writer 和 roles/logging.logWriter 角色,请将其移除:

      gcloud projects remove-iam-policy-binding ${PROJECT_ID} \
          --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
          --role="roles/artifactregistry.writer" \
          --condition=None
      
      gcloud projects remove-iam-policy-binding ${PROJECT_ID} \
          --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
          --role="roles/logging.logWriter" \
          --condition=None
      

后续步骤