클러스터 내부에서 에이전트 샌드박스 스냅샷 트리거

이 튜토리얼에서는 Google Kubernetes Engine (GKE) 클러스터 내에서 에이전트 샌드박스 스냅샷 기능을 배포하고 테스트하는 방법을 보여줍니다. 클러스터 내에서 클라이언트 애플리케이션을 실행하여 프로그래매틱 방식으로 샌드박스 환경을 만들고, 일시중지하고, 재개하는 방법을 알아봅니다.

포드 스냅샷을 만드는 방법에 대한 자세한 내용은 포드 스냅샷에서 복원을 참고하세요.

비용

에이전트 샌드박스는 GKE에서 추가 비용 없이 제공됩니다. 생성하는 리소스에는 GKE 가격 책정이 적용됩니다.

시작하기 전에

  1. Cloud de Confiance 콘솔의 프로젝트 선택기 페이지에서 Cloud de Confiance 프로젝트를 선택하거나 만듭니다.

    프로젝트를 선택하거나 만드는 데 필요한 역할

    • 프로젝트 선택: 프로젝트를 선택하는 데는 특정 IAM 역할이 필요하지 않습니다. 역할이 부여된 프로젝트를 선택하면 됩니다.
    • 프로젝트 만들기: 프로젝트를 만들려면 resourcemanager.projects.create 권한이 포함된 프로젝트 생성자 역할(roles/resourcemanager.projectCreator)이 필요합니다. 역할 부여 방법 알아보기

    프로젝트 선택기로 이동

  2. Cloud de Confiance 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

  3. Artifact Registry, Kubernetes Engine API를 사용 설정합니다.

    API 사용 설정에 필요한 역할

    API를 사용 설정하려면 serviceusage.services.enable 권한이 포함된 서비스 사용량 관리자 IAM 역할(roles/serviceusage.serviceUsageAdmin)이 필요합니다. 역할 부여 방법 알아보기

    API 사용 설정

  4. Cloud de Confiance 콘솔에서 Cloud Shell을 활성화합니다.

    Cloud Shell 활성화

  5. 이 튜토리얼을 완료하는 데 필요한 권한이 있는지 확인합니다.

필요한 역할

샌드박스를 만들고 관리하는 데 필요한 권한을 얻으려면 관리자에게 프로젝트에 대한 Kubernetes Engine 관리자 (roles/container.admin) IAM 역할을 부여해 달라고 요청하세요. 역할 부여에 대한 자세한 내용은 프로젝트, 폴더, 조직에 대한 액세스 관리를 참조하세요.

커스텀 역할이나 다른 사전 정의된 역할을 통해 필요한 권한을 얻을 수도 있습니다.

제한사항

리전 클러스터에서 서로 다른 영역의 노드는 CPU 마이크로아키텍처가 다를 수 있습니다. 스냅샷은 CPU 상태를 캡처하므로 CPU 기능이 누락된 노드에서 스냅샷을 복원하면 실패합니다 (예: OCI runtime restore failed: incompatible FeatureSet 오류).

이 문제를 방지하려면 환경에 적합한 구성을 사용하세요.

  • 프로덕션: 클러스터 전체에서 고가용성을 유지하려면 워크로드를 특정 영역에 고정하지 마세요. 대신 최소 CPU 플랫폼을 지정하여 모든 영역에서 CPU 기능 일관성을 유지하세요. 자세한 내용은 최소 CPU 플랫폼 선택을 참고하세요.
  • 테스트: 설정을 간소화하고 초기 CPU 불일치 오류를 방지하려면 SandboxTemplate 매니페스트에서 nodeSelector 필드를 사용하여 포드를 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 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: 현재 Cloud de Confiance by S3NS 프로젝트의 ID입니다. 이 변수를 정의하면 모든 리소스가 올바른 프로젝트에서 생성됩니다.
  • PROJECT_NUMBER: 현재 Cloud de Confiance by S3NS 프로젝트의 프로젝트 번호입니다.
  • CLUSTER_NAME: GKE 클러스터의 이름입니다(예: test-snapshot).
  • LOCATION: GKE 클러스터 및 Artifact Registry 저장소가 있는 Cloud de Confiance by S3NS 리전입니다(예: us-central1).
  • BUCKET_LOCATION: Cloud Storage 버킷의 위치(예: us)
  • BUCKET_NAME: 스냅샷에 사용되는 Cloud Storage 버킷의 이름입니다.
  • CLOUDBUILD_BUCKET_NAME: Cloud Build 로그에 사용되는 Cloud Storage 버킷의 이름입니다.
  • MACHINE_TYPE: 클러스터 노드에 사용할 머신 유형입니다(예: e2-standard-8).
  • REPOSITORY_NAME: Artifact Registry 저장소의 이름입니다(예: agent-sandbox).

구성 단계 개요

클러스터 내에서 에이전트 샌드박스 환경의 포드 스냅샷을 사용 설정하고 테스트하려면 여러 구성 단계를 실행해야 합니다. 이러한 단계를 이해하려면 먼저 전체 워크플로에 포함된 구성요소를 이해하는 것이 좋습니다.

주요 구성요소

이 튜토리얼에서는 다음 두 Python 애플리케이션을 사용하여 스냅샷 프로세스를 테스트합니다.

  • 클라이언트 애플리케이션: 클러스터의 표준 포드에서 실행되는 Python 스크립트입니다. 이 애플리케이션은 샌드박스 수명 주기를 관리합니다. 샌드박스를 프로그래매틱 방식으로 만들고, 스냅샷을 트리거하기 위해 일시중지하고, 샌드박스를 재개하고, 상태가 유지되었는지 확인합니다. 이 튜토리얼에서는 agent-sandbox-client-sa라는 Kubernetes 서비스 계정을 만들고 클라이언트 애플리케이션 포드가 Kubernetes API를 사용하여 샌드박스 커스텀 리소스와 스냅샷 트리거 객체를 관리할 수 있도록 RBAC 권한을 부여합니다.
  • 샌드박스 처리된 애플리케이션: 매초 카운터를 증가시키고 출력하는 Python 스크립트입니다. 이 애플리케이션은 격리된 샌드박스 환경 내에서 안전하게 실행되어 클라이언트 애플리케이션이 확인할 수 있는 변경 상태를 생성합니다. 이 튜토리얼에서는 snapshot-sa라는 전용 Kubernetes 서비스 계정을 만들고, 샌드박스 처리된 Pod가 Cloud Storage에서 스냅샷 객체를 안전하게 읽고 쓸 수 있도록 Workload Identity를 구성합니다.

구성 및 테스트 프로세스

다음 목록에는 환경을 설정하고 테스트를 실행하기 위해 수행해야 하는 단계가 요약되어 있습니다.

  1. 클러스터 만들기: Pod 스냅샷 및 에이전트 샌드박스 기능이 사용 설정된 Autopilot 또는 Standard 클러스터를 만듭니다.
  2. Artifact Registry 저장소 만들기: 클라이언트 애플리케이션의 컨테이너 이미지를 저장할 Docker 저장소를 만듭니다.
  3. 에이전트 샌드박스 설치: 클러스터에 핵심 agent-sandbox 구성요소와 확장 프로그램을 설치합니다.
  4. 스토리지 및 권한 구성: Cloud Storage 버킷을 만들고 스냅샷을 안전하게 저장할 수 있도록 워크로드 아이덴티티 권한을 구성합니다.
  5. 포드 스냅샷 구성: 스냅샷 스토리지 구성, 스냅샷 정책, 샌드박스 템플릿을 만들고 적용합니다.
  6. 클라이언트 애플리케이션 빌드: 클라이언트 애플리케이션의 컨테이너 이미지를 빌드하고 Artifact Registry 저장소에 푸시합니다.
  7. 테스트 실행: 샌드박스를 만들고, 스냅샷을 캡처하기 위해 일시중지하고, 다시 시작하고, 카운터의 상태가 성공적으로 복원되었는지 확인하는 클라이언트 애플리케이션 포드를 배포합니다.

클러스터 만들기

포드 스냅샷이 사용 설정된 새 GKE 클러스터를 만듭니다. 전체 기능 호환성을 위해서는 빠른 출시 채널을 지정하세요.

Autopilot

필요한 기능이 있는 Autopilot 클러스터를 만듭니다.

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

표준

필수 기능이 있는 Standard 클러스터를 만듭니다.

gcloud beta 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=${LOCATION}

gVisor가 사용 설정된 노드 풀을 만듭니다.

gcloud container node-pools create gvisor-pool \
   --cluster ${CLUSTER_NAME} \
   --num-nodes=1 \
   --location=${LOCATION} \
   --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"

에이전트 샌드박스 설치

클러스터에 에이전트 샌드박스 핵심 구성요소와 확장 프로그램을 설치합니다(v0.4.6 버전을 예로 사용).

# Install the core agent-sandbox components
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v0.4.6/manifest.yaml

# Install the extensions (e.g., Warm Pools, Claims)
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v0.4.6/extensions.yaml

스토리지 및 권한 구성

포드 스냅샷을 저장할 Cloud Storage 버킷을 구성하고 snapshot-sa 서비스 계정 및 GKE 서비스 에이전트에 필요한 워크로드 아이덴티티 권한을 부여합니다. 이렇게 하면 샌드박스 처리된 워크로드가 스냅샷 객체를 안전하게 저장하고 검색할 수 있습니다.

  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 카운터 스크립트)은 이 ID를 사용하여 외부 API를 인증하고 Cloud Storage에 저장된 스냅샷 객체에 안전하게 액세스합니다.

    kubectl create serviceaccount "snapshot-sa" \
        --namespace "default"
    
  3. 워크로드 아이덴티티를 사용하여 서비스 계정에 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. 워크로드 아이덴티티를 사용하여 서비스 계정에 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 커스텀 리소스를 설치합니다. 이러한 리소스는 클러스터가 포드 스냅샷을 저장하고 관리하는 방법을 정의합니다.

  • PodSnapshotStorageConfig: 스냅샷 바이너리 객체를 저장하도록 지정된 Cloud Storage 버킷을 지정합니다.
  • PodSnapshotPolicy: 스냅샷이 수동으로 트리거되는 방법, 그룹화되는 빈도, 보관 정책을 정의합니다.
  • SandboxTemplate: 격리된 샌드박스 워크로드를 실행하기 위한 기본 컨테이너, 노드 선택기, 서비스 계정을 정의합니다.
  1. test_client/snapshot_storage_config.yaml 파일을 만듭니다. 이 구성은 클러스터가 바이너리 포드 스냅샷 상태를 저장하는 타겟 Cloud Storage 버킷을 지정합니다.

    apiVersion: podsnapshot.gke.io/v1
    kind: PodSnapshotStorageConfig
    metadata:
      name: example-pod-snapshot-storage-config
    spec:
      snapshotStorageConfig:
        gcs:
          bucket: "$BUCKET_NAME"
    
  2. 구성 파일에서 환경 변수 자리표시자를 바꿉니다.

    sed -i "s/\$BUCKET_NAME/$BUCKET_NAME/g" test_client/snapshot_storage_config.yaml
    
  3. 스토리지 구성 매니페스트를 적용합니다.

    kubectl apply -f test_client/snapshot_storage_config.yaml
    
  4. 스토리지 구성이 준비될 때까지 기다립니다.

    kubectl wait --for=condition=Ready podsnapshotstorageconfig/example-pod-snapshot-storage-config --timeout=60s
    
  5. test_client/snapshot_policy.yaml 파일을 만듭니다. 이 구성은 샌드박스 처리된 워크로드의 스냅샷을 최대 2개까지 보관하는 보관 규칙을 설정합니다. 트리거 유형이 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
    
  6. 스냅샷 정책 매니페스트를 적용합니다.

    kubectl apply -f test_client/snapshot_policy.yaml
    
  7. test_client/python-counter-template.yaml 파일을 만듭니다. 이 구성은 샌드박스 포드를 정의하고 snapshot-sa 서비스 계정 ID를 할당합니다. 이 할당은 샌드박스가 안전하게 실행되도록 지원합니다. 이 포드 내에서 샌드박스 애플리케이션(Python 스크립트)은 증가하는 카운터를 컨테이너 로그에 지속적으로 출력합니다.

    apiVersion: extensions.agents.x-k8s.io/v1alpha1
    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)
    
  8. 샌드박스 템플릿 매니페스트를 적용합니다.

    kubectl apply -f test_client/python-counter-template.yaml
    

클라이언트 애플리케이션 빌드

클라이언트 애플리케이션의 컨테이너 이미지를 만들고 이를 Artifact Registry에 업로드합니다.

  1. test_client/Dockerfile.client 파일을 만듭니다. 이 파일은 클라이언트 애플리케이션의 Python 런타임 환경과 종속 항목을 정의합니다.

    FROM python:3.13-slim
    
    WORKDIR /app
    
    RUN pip install "k8s-agent-sandbox[tracing]==0.4.6"
    
    # Copy test script
    COPY client_test.py /app/client_test.py
    
    CMD ["python", "/app/client_test.py"]
    
  2. test_client/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_pod_name(sandbox_id, namespace):
        custom_api = client.CustomObjectsApi()
        try:
            sandbox_cr = custom_api.get_namespaced_custom_object(
                group="agents.x-k8s.io",
                version="v1alpha1",
                namespace=namespace,
                plural="sandboxes",
                name=sandbox_id
            )
            metadata = sandbox_cr.get("metadata", {})
            annotations = metadata.get("annotations", {})
            return annotations.get("agents.x-k8s.io/pod-name")
        except Exception as e:
            logging.error(f"Failed to get sandbox CR: {e}")
            return None
    
    def get_current_count(sandbox_id, namespace="default"):
        pod_name = get_current_pod_name(sandbox_id, namespace)
        if not pod_name:
            logging.error(f"Could not determine pod name for sandbox {sandbox_id}")
            return None
        return get_last_count(pod_name, 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(template="python-counter-template", 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.sandbox_id)
        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.sandbox_id)
        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 test_client/Dockerfile.client test_client
      docker push "${LOCATION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY_NAME}/sandbox-client:latest"
      

    Cloud Build

    1. test_client/cloudbuild.yaml이라는 파일을 만듭니다.

      steps:
      - name: 'gcr.io/cloud-builders/docker'
        args: ['build', '-t', '$LOCATION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY_NAME/sandbox-client:latest', '-f', 'test_client/Dockerfile.client', 'test_client']
      images:
      - '$LOCATION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY_NAME/sandbox-client:latest'
      
    2. 구성 파일에서 환경 변수 자리표시자를 바꿉니다.

      sed -i "s/\$REPOSITORY_NAME/$REPOSITORY_NAME/g" test_client/cloudbuild.yaml
      sed -i "s/\$LOCATION/$LOCATION/g" test_client/cloudbuild.yaml
      sed -i "s/\$PROJECT_ID/$PROJECT_ID/g" test_client/cloudbuild.yaml
      
    3. Cloud Build 서비스 계정에 필요한 권한을 부여합니다.

      gcloud projects add-iam-policy-binding $PROJECT_ID \
         --member="serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
         --role="roles/artifactregistry.writer"
      
      gcloud projects add-iam-policy-binding $PROJECT_ID \
         --member="serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
         --role="roles/logging.logWriter"
      
      gcloud storage buckets add-iam-policy-binding "gs://$CLOUDBUILD_BUCKET_NAME" \
         --member="serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
         --role="roles/storage.objectAdmin"
      
    4. Cloud Build를 사용하여 빌드를 실행합니다.

      gcloud builds submit --config test_client/cloudbuild.yaml
      

테스트 실행

클라이언트 애플리케이션을 배포하여 샌드박스를 만들고, 스냅샷을 트리거하고, 내부 카운터가 저장된 상태에서 성공적으로 재개되는지 확인합니다.

  1. test_client/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 test_client/client_sa.yaml
    
  3. test_client/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" test_client/client_pod.yaml
    sed -i "s/\$LOCATION/$LOCATION/g" test_client/client_pod.yaml
    sed -i "s/\$PROJECT_ID/$PROJECT_ID/g" test_client/client_pod.yaml
    
  5. 클라이언트 애플리케이션 포드 매니페스트를 적용합니다.

    kubectl apply -f test_client/client_pod.yaml
    
  6. 포드 로그를 스트리밍하여 실행 흐름을 확인합니다.

    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).

출력은 샌드박스가 일시 중지되었다가 재개될 때 상태를 성공적으로 유지함을 보여줍니다. 샌드박스가 일시중지 (일시중지되고 0으로 확장됨)되면 카운터가 진행을 멈추고 샌드박스가 복원되면 카운터가 다시 시작됩니다. 정지하지 않으면 정지 기간 동안 카운터가 계속 증가하여 수가 훨씬 높아집니다.

리소스 삭제

Cloud de Confiance by S3NS 계정에 요금이 청구되지 않도록 하려면 생성된 리소스를 삭제하세요.

  1. GKE 클러스터를 삭제합니다. 이렇게 하면 노드 풀과 노드 풀 내의 모든 Kubernetes 서비스 계정도 삭제됩니다.

    gcloud beta container clusters delete test-snapshot --location="${LOCATION}" --quiet
    
  2. 테스트 이미지를 위해 만든 Docker 저장소를 삭제하려면 Artifact Registry 저장소를 삭제하세요.

    gcloud artifacts repositories delete ${REPOSITORY_NAME} --location="${LOCATION}" --quiet
    
  3. Cloud Storage 버킷과 그 안의 모든 스냅샷을 삭제합니다. 이렇게 하면 버킷 수준 워크로드 아이덴티티 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. Docker 대신 Cloud Build를 사용하여 컨테이너 이미지를 빌드하고 푸시한 경우 로그 버킷을 삭제하고 서비스 계정 권한을 삭제합니다.

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

다음 단계