הפעלת Agent Sandbox בתמונות מצב של GKE מתוך אשכול

כדי לשמור את המצב של סביבות ארגז החול, אתם יכולים להשתמש בתמונות מצב של Agent Sandbox ב-Google Kubernetes Engine (GKE) בתוך אשכול. הגישה הזו מאפשרת ניפוי באגים עקבי, מאיצה את החזרה על הפיתוח ומאפשרת שחזור מהיר של הגדרות מורכבות. במדריך הזה תפרסו ותבדקו את היכולת הזו על ידי הפעלת אפליקציית לקוח שיוצרת, משהה וממשיכה באופן פרוגרמטי סביבות של ארגז חול בתוך האשכול.

למידע נוסף על צילום קובצי snapshot של Pods, ראו שחזור מקובץ snapshot של Pod.

עלויות

‫Agent Sandbox ב-GKE היא תוכנה בקוד פתוח שאפשר להשתמש בה ללא עלות נוספת. התמחור של GKE חל על המשאבים שאתם יוצרים, כולל משאבי המחשוב שמשמשים את הבקר של Agent Sandbox.

לפני שמתחילים

  1. בדף לבחירת הפרויקט במסוף Google Cloud , בוחרים פרויקט ב- Google Cloud או יוצרים אותו.

    התפקידים שנדרשים כדי לבחור או ליצור פרויקט

    • בחירת פרויקט: כדי לבחור פרויקט לא נדרש תפקיד IAM ספציפי – אתם יכולים לבחור כל פרויקט שקיבלתם בו תפקיד.
    • יצירת פרויקט: כדי ליצור פרויקט, צריך את התפקיד "יצירת פרויקטים" (roles/resourcemanager.projectCreator), שכולל את ההרשאה resourcemanager.projects.create. איך מקצים תפקידים?

    כניסה לדף לבחירת הפרויקט

  2. מוודאים שהחיוב מופעל בפרויקט ב- Google Cloud .

  3. מפעילים את ממשקי ה-API של Artifact Registry,‏ Kubernetes Engine ו-Cloud Build, אם הם עדיין לא מופעלים.

    התפקידים שנדרשים כדי להפעיל ממשקי API

    כדי להפעיל ממשקי API, צריך את ההרשאה serviceusage.services.enable. אם אתם יצרתם את הפרויקט, סביר להניח שכבר יש לכם את ההרשאה הזו דרך התפקיד "בעלים" (roles/owner). אחרת, אתם יכולים לקבל את ההרשאה הזו דרך התפקיד "אדמין בממשק 'שימוש בשירות'" (roles/serviceusage.serviceUsageAdmin). איך מקצים תפקידים?

    הפעלת ממשקי ה-API

  4. מפעילים את Cloud Shell במסוף Google Cloud .

    הפעלת Cloud Shell

  5. מוודאים שיש את ההרשאות הנדרשות כדי להשלים את המדריך.

התפקידים הנדרשים

כדי לקבל את ההרשאות שנדרשות ליצירה ולניהול של ארגזי חול, צריך לבקש מהאדמין להקצות לכם ב-IAM את התפקיד אדמין של Kubernetes Engine (roles/container.admin) בפרויקט. כדי לקרוא הסבר על מתן תפקידים, ראו איך מנהלים את הגישה ברמת הפרויקט, התיקייה והארגון.

יכול להיות שאפשר לקבל את ההרשאות הנדרשות גם באמצעות תפקידים בהתאמה אישית או תפקידים מוגדרים מראש.

מגבלות

כדי ליצור קובצי snapshot של Pod צריך להשתמש ב-gVisor, והם לא פועלים עם ארגזי חול של microVM.

באשכול אזורי, לצמתים באזורים שונים יכולות להיות מיקרו-ארכיטקטורות שונות של מעבדים. מכיוון שתמונות המצב מתעדות את מצב המעבד (CPU), שחזור של תמונת מצב בצומת שחסרות בו תכונות של המעבד ייכשל (לדוגמה, עם השגיאה OCI runtime restore failed: incompatible FeatureSet).

כדי להימנע מהבעיה הזו, צריך להשתמש בהגדרה שמתאימה לסביבה:

  • ייצור: כדי לשמור על זמינות גבוהה באשכול, אל תצמידו עומסי עבודה לאזור ספציפי. במקום זאת, כדי להבטיח עקביות בתכונות של המעבד בכל האזורים, צריך לציין פלטפורמת CPU מינימלית. מידע נוסף זמין במאמר בחירת פלטפורמת CPU מינימלית.
  • בדיקה: כדי לפשט את ההגדרה ולמנוע שגיאות ראשוניות של חוסר התאמה במעבד (CPU), אתם יכולים להשתמש בשדה nodeSelector במניפסט של SandboxTemplate כדי להצמיד את ה-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 . הפקודות במסמך הזה משתמשות במזהה הזה כדי לתת שמות למשאבים ולהעניק הרשאות IAM.
  • ‫PROJECT_NUMBER: מספר הפרויקט הנוכחי ב- Google Cloud .
  • ‫CLUSTER_NAME: השם של אשכול GKE, לדוגמה test-snapshot.
  • ‫LOCATION: האזור של Google Cloud למאגר Artifact Registry (לדוגמה, us-central1). אם יוצרים אשכול Autopilot, האשכול משתמש גם באזור הזה.
  • ‫ZONE: האזור של האשכול. אם יוצרים אשכול Standard, צריך להגדיר אותו לאזור ב-LOCATION (לדוגמה, us-central1-a). תבנית ארגז החול שתיצרו בהמשך המדריך מריצה ארגזי חול ב-us-central1-a. אם אתם משתמשים באזור אחר, שנו את האזור גם בתבנית.
  • ‫BUCKET_LOCATION: המיקום של הקטגוריות ב-Cloud Storage, לדוגמה us.
  • ‫BUCKET_NAME: השם של הקטגוריה של Cloud Storage שמשמשת לשמירת קובצי snapshot, לדוגמה PROJECT_ID_snapshots. שמות הקטגוריות חייבים להיות ייחודיים באופן גלובלי.
  • ‫CLOUDBUILD_BUCKET_NAME: השם של הקטגוריה של Cloud Storage שמשמשת ליומנים של Cloud Build. הערך הזה הוא שם הקטגוריה שמוגדר כברירת מחדל ב-Cloud Build, אז אל תשנו אותו.
  • ‫MACHINE_TYPE: סוג המכונה שבה משתמשים לצומתי האשכול ולמאגר הצמתים של gVisor, לדוגמה n2-standard-2. אי אפשר להשתמש בתמונות מצב של Pod במכונות מסוג E2.
  • ‫REPOSITORY_NAME: השם של מאגר Artifact Registry – לדוגמה, agent-sandbox.

סקירה כללית של שלבי ההגדרה

כדי להפעיל ולבדוק את תמונות המצב של Pod ב-Agent Sandbox בסביבות GKE מתוך האשכול, צריך לבצע כמה שלבי הגדרה. כדי להבין את השלבים האלה, כדאי קודם להבין את הרכיבים שמעורבים בתהליך העבודה הכולל.

הרכיבים העיקריים

במדריך הזה משתמשים בשתי האפליקציות הבאות ב-Python כדי לבדוק את תהליך יצירת קובץ ה-snapshot:

  • אפליקציית לקוח: סקריפט Python שפועל ב-Pod רגיל באשכול. האפליקציה הזו מנהלת את מחזור החיים של ארגז החול: היא יוצרת את ארגז החול באופן פרוגרמטי, משהה אותו כדי להפעיל תמונת מצב, מפעילה מחדש את ארגז החול ומוודאת שהמצב נשמר. במדריך הזה תיצרו חשבון שירות ב-Kubernetes בשם agent-sandbox-client-sa ותתנו לו הרשאות RBAC, כדי ש-Pod של אפליקציית הלקוח יוכל לנהל משאבים בהתאמה אישית של ארגז החול ואובייקטים של טריגרים של תמונות מצב באמצעות Kubernetes API.
  • אפליקציה בארגז חול: סקריפט Python שמגדיל ומדפיס מונה בכל שנייה. האפליקציה הזו פועלת באופן מאובטח בסביבת ארגז החול המבודדת כדי ליצור מצב משתנה שאפליקציית הלקוח יכולה לאמת. במדריך הזה תיצרו חשבון שירות ייעודי של Kubernetes בשם snapshot-sa ותגדירו את Workload Identity כדי לאשר ל-Pod בארגז החול לקרוא ולכתוב באופן מאובטח אובייקטים של תמונות מצב ב-Cloud Storage.

תהליך ההגדרה והבדיקה

ברשימה הבאה מפורטים השלבים שצריך לבצע כדי להגדיר את הסביבה ולהריץ את הבדיקה:

  1. יצירת אשכול: יוצרים אשכול Autopilot או אשכול Standard עם תמונות מצב של Pods.
  2. יצירת מאגר ב-Artifact Registry: יוצרים מאגר Docker כדי לאחסן את קובץ האימג' של הקונטיינר של אפליקציית הלקוח.
  3. התקנת Agent Sandbox: מתקינים את רכיבי הליבה והתוספים של Agent Sandbox באשכול.
  4. הגדרת אחסון והרשאות: יוצרים קטגוריה ב-Cloud Storage ומגדירים הרשאות של Workload Identity כדי לאפשר שמירה מאובטחת של קובצי snapshot.
  5. הגדרת קובצי snapshot של Pod: יוצרים ומחילים את הגדרת האחסון של קובצי ה-snapshot, את מדיניות קובצי ה-snapshot, את תבנית ארגז החול ואת המאגר החם.
  6. פיתוח גרסת build של אפליקציית הלקוח: פיתוח גרסת build של קובץ האימג' של הקונטיינר לאפליקציית הלקוח והעברה שלו בדחיפה למאגר Artifact Registry.
  7. מריצים את הבדיקה: פורסים את Pod של אפליקציית הלקוח, שיוצר את ארגז החול, משהה אותו כדי ליצור קובץ snapshot, ממשיך אותו ומאמת שהמצב של המונה שוחזר בהצלחה.

יצירת אשכול

יוצרים אשכול GKE חדש עם תמונות מצב של Pods. כדי ליהנות מתאימות מלאה של התכונות, צריך לציין את ערוץ ההפצה המהירה.

טייס אוטומטי

יוצרים אשכול 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

יוצרים מאגר Docker ב-Artifact Registry כדי לאחסן את קובץ האימג' של הקונטיינר של אפליקציית הלקוח (האפליקציה שיוצרת את ארגז החול ומנהלת אותו):

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

הגדרת האחסון וההרשאות

מגדירים קטגוריה של Cloud Storage לאחסון תמונות מצב של Pod, ונותנים את ההרשאות הנדרשות של Workload Identity לחשבון השירות snapshot-sa ולסוכן השירות של GKE. כך עומסי העבודה בארגז החול יכולים לשמור ולאחזר אובייקטים של קובצי snapshot בצורה מאובטחת:

  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. יוצרים חשבון שירות של Kubernetes במרחב השמות default. האפליקציה בארגז החול (סקריפט המונה של Python) משתמשת בזהות הזו כדי לאמת את ממשקי ה-API החיצוניים ולגשת באופן מאובטח לאובייקטים של תמונות המצב שמאוחסנים ב-Cloud Storage:

    kubectl create serviceaccount "snapshot-sa" \
        --namespace "default"
    
  3. מקשרים את התפקיד storage.bucketViewer לחשבון השירות באמצעות Workload Identity. התפקיד הזה מאפשר לעומס העבודה בארגז החול להציג את תוכן הקטגוריה ולאתר קובצי snapshot ספציפיים:

    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 לחשבון השירות באמצעות Workload Identity. התפקיד הזה נותן הרשאה לקרוא, לשמור ולמחוק אובייקטים בינאריים של קובצי snapshot בקטגוריה:

    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 הרשאות לנהל (ליצור, להציג ברשימה, לקרוא ולמחוק) אובייקטים של קובצי snapshot בתוך הקטגוריה:

    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. המשאבים האלה מגדירים איך האשכול מאחסן ומנהל את קובצי ה-snapshot של ה-Pod:

  • ‫PodSnapshotStorageConfig: מציין את הקטגוריה של Cloud Storage שמיועדת לאחסון אובייקטים בינאריים של קובצי snapshot.
  • ‫PodSnapshotPolicy: מגדיר איך מפעילים תמונות מצב באופן ידני, באיזו תדירות הן מקובצות ואת מדיניות שמירת הנתונים שלהן.
  • ‫SandboxTemplate: מגדיר את הקונטיינר הבסיסי, את בוררי הצמתים ואת חשבונות השירות להרצת עומס העבודה המבודד בארגז החול.
  • ‫SandboxWarmPool: שומרת על ארגז חול שחומם מראש, שנוצר מהמשאב SandboxTemplate, מוכן לתביעה על ידי אפליקציית הלקוח.
  1. יוצרים ספרייה חדשה לקובצי לקוח הבדיקה:

    mkdir test_client
    
  2. עוברים לספרייה החדשה:

    cd test_client
    
  3. יוצרים קובץ בשם snapshot_storage_config.yaml. ההגדרה הזו מציינת את קטגוריית היעד של Cloud Storage שבה האשכול שומר את המצב של קובץ ה-snapshot הבינארי של ה-Pod:

    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. אם Docker מותקן בסביבה שלכם (כמו Cloud Shell), אתם יכולים להשתמש ב-Docker כדי ליצור את קובץ האימג' באופן מקומי. אם אתם עובדים בסביבה בלי Docker, אתם יכולים להשתמש ב-Cloud Build כדי ליצור את קובץ האימג' ולהעביר אותו בדחיפה מרחוק:

    Docker

    1. מגדירים את האימות של Docker ל-Artifact Registry:

      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 Storage ליומנים של Cloud Build. אם כבר השתמשתם ב-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. מריצים את ה-build באמצעות Cloud Build:

      gcloud builds submit --config cloudbuild.yaml
      

הרצת הבדיקה

פורסים את אפליקציית הלקוח כדי ליצור את ארגז החול, מפעילים קובץ snapshot ומוודאים שהמונה הפנימי ממשיך לפעול מהמצב השמור שלו.

  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. אם האפליקציה יוצרת את ה-claim לפני שארגז החול של המאגר מוכן, יכול להיות שהבקר ייצור ארגז חול חדש בשביל ה-claim במקום להקצות את ארגז החול מהמאגר. באשכול 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, שמשחזר מתמונת המצב, והמונה ממשיך. אם לא היינו משעים את הספירה, המונה היה ממשיך להתקדם במהלך תקופת ההשעיה והספירה הייתה גבוהה משמעותית.

מחיקת משאבים

כדי להימנע מצבירת חיובים בחשבון Google Cloud , מוחקים את המשאבים שיצרתם:

  1. מוחקים את אשכול GKE. הפעולה הזו גם מוחקת את מאגר הצמתים ואת כל חשבונות השירות של Kubernetes שבתוכו:

    טייס אוטומטי

    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 ואת כל קובצי ה-snapshot שבתוכה. הפעולה הזו מסירה באופן אוטומטי את קישורי ה-IAM של Workload Identity ברמת הקטגוריה שחלים עליה:

    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
      

המאמרים הבאים