Membuat pipeline OS image kustom secara terprogram

Anda dapat mengonfigurasi dan mengirimkan pipeline Image Builder secara terprogram menggunakan gcloud CLI, Terraform, atau library klien seperti Go. Dengan mengonfigurasi pipeline secara terprogram, Anda dapat menentukan setelan infrastruktur, image OS dasar, tindakan penyesuaian, dan pengujian validasi dalam file konfigurasi deklaratif.

Sebelum memulai

Peran yang diperlukan

Untuk mendapatkan izin yang Anda perlukan untuk membuat dan mengirimkan pipeline penyesuaian gambar secara terprogram, minta administrator untuk memberi Anda peran IAM berikut pada project Anda:

Untuk mengetahui informasi selengkapnya tentang pemberian peran, lihat Mengelola akses ke project, folder, dan organisasi.

Anda mungkin juga bisa mendapatkan izin yang diperlukan melalui peran khusus atau peran bawaan lainnya.

Membuat file konfigurasi

Untuk mengonfigurasi pipeline menggunakan gcloud CLI atau Terraform, buat dua file konfigurasi:

  • imagebuilder.yaml: menentukan resep penyesuaian untuk image, termasuk OS image dasar, setelan infrastruktur worker VM, detail output image target, dan langkah-langkah penyesuaian berurutan seperti menjalankan skrip shell, mentransfer file, atau melakukan reboot.
  • cloudbuild.yaml: mengorkestrasi langkah-langkah proses build di Cloud Build, termasuk penguraian, validasi, build, pengujian, dan publikasi OS image kustom.

Jika Anda mengirimkan build menggunakan Go, Anda hanya perlu membuat file imagebuilder.yaml dan menguploadnya ke bucket Cloud Storage, karena contoh Go menentukan langkah-langkah orkestrasi Cloud Build dalam kode.

Membuat file konfigurasi image

Untuk menentukan konfigurasi image, buat file bernama imagebuilder.yaml di direktori lokal Anda. Untuk mengetahui daftar lengkap semua kolom skema dan tindakan penyesuaian yang didukung, lihat Skema resep penyesuaian dan Tindakan penyesuaian yang didukung.

Contoh file imagebuilder.yaml berikut mengonfigurasi pipeline yang membangun image Ubuntu 22.04 LTS yang disesuaikan menggunakan worker VM e2-standard-4 di region dan zona yang Anda tentukan, serta melakukan update paket sistem.

apiVersion: imagebuilder.gcp.com/v1
kind: OSImageCustomization
metadata:
  name: customized-ubuntu-baseline
  description: "Ubuntu 22.04 LTS custom OS baseline image"
infrastructureConfig:
  machineType: e2-standard-4
  zone: ZONE
  debug: false
source:
  imageFamily: projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts
destinations:
  - diskImage:
      name: custom-ubuntu-v1
      family: custom-ubuntu-family
      project: PROJECT_ID
      storageLocations:
        - REGION
spec:
  config:
    skipSystemTests: false
  steps:
    -   name: "System Package Update"
      action: Shell
      inputs:
        command: "apt-get update -y && apt-get upgrade -y"

Ganti nilai placeholder berikut:

  • PROJECT_ID: project ID Anda.
  • REGION: lokasi penyimpanan image target, misalnya, us-east1 atau europe-west1. Pastikan Anda memenuhi persyaratan regional dan zona berikut:
    • Image Builder hanya didukung di region tempat Cloud Build tersedia.
    • Worker VM ZONE harus berada dalam REGION yang Anda tentukan.
    • Untuk meminimalkan latensi jaringan dan mencegah biaya egress lintas region, pastikan zona VM worker, bucket staging Cloud Storage, repositori Artifact Registry, dan lokasi penyimpanan image target berada di region yang sama.
  • ZONE: zona yang terletak dalam REGION yang Anda tentukan, misalnya, us-east1-b atau europe-west1-b.

Membuat file build orchestrator

Buat file bernama cloudbuild.yaml di direktori yang sama. File ini memanggil langkah-langkah container Image Builder untuk membangun, memvalidasi, dan memublikasikan image OS kustom.

substitutions:
  _GCS_WORKDIR: 'gs://STAGING_BUCKET_NAME/workdir/'
  _IMAGE_BUILDER_CONFIG_PATH: 'imagebuilder.yaml'
  _SERVICE_ACCOUNT: 'projects/PROJECT_ID/serviceAccounts/SERVICE_ACCOUNT_EMAIL'
  _IMAGE_OUTPUT_PATH: 'image-builder/binaryOut'
  _ARTIFACT_REGISTRY_RESOURCE_URI: 'projects/PROJECT_ID/locations/REGION/repositories/REPOSITORY_NAME/packages/PACKAGE_NAME/versions/v${BUILD_ID}'

steps:
  #  Step 1: Parse configs and run OS customization on worker VM
  - name: 'REGION-docker.pkg.dev/image-builder-official/release/builder:stable'
    script: |
      #!/usr/bin/env bash
      /build
    id: 'imagebuilder-customize'
    results:
      - name: image_builder_telemetry_metrics
      - name: base_image
        attestationType: "https://cloudbuild.googleapis.com/attestations/build_content_restrictions"
        attestationContent: base_image

  # Step 2: Validate by running system boot checks on a test VM
  - name: 'REGION-docker.pkg.dev/image-builder-official/release/validator:stable'
    script: |
      #!/usr/bin/env bash
      /validate
    id: 'imagebuilder-validate'
    results:
      - name: image_builder_telemetry_metrics

  # Step 3: Register image in Compute Engine and upload tar files to Artifact Registry
  - name: 'REGION-docker.pkg.dev/image-builder-official/release/builder:stable'
    script: |
      #!/usr/bin/env bash
      /publish
    id: 'imagebuilder-publish'
    results:
      - name: image_builder_telemetry_metrics

options:
  automapSubstitutions: true
  requestedVerifyOption: VERIFIED
  substitutionOption: ALLOW_LOOSE
  dynamicSubstitutions: true
  logging: CLOUD_LOGGING_ONLY

artifacts:
  generic_artifacts:
    - folder: '${_IMAGE_OUTPUT_PATH}'
      registry_path: '${_ARTIFACT_REGISTRY_RESOURCE_URI}'

timeout: '3600s'

Ganti nilai placeholder berikut:

  • STAGING_BUCKET_NAME: bucket Cloud Storage yang ada di project Anda untuk digunakan sebagai ruang kerja staging sementara. Jika belum memiliki bucket, Anda dapat membuatnya dengan menjalankan gcloud storage buckets create gs://STAGING_BUCKET_NAME. Jika Anda men-deploy pipeline menggunakan Terraform, Terraform akan membuat bucket ini secara otomatis.
  • PROJECT_ID: Project ID Google Cloud Anda.
  • REGION: region Google Cloud untuk repositori Artifact Registry Anda, misalnya, us-east1 atau europe-west1.
  • SERVICE_ACCOUNT_EMAIL: email untuk akun layanan yang Anda konfigurasi dengan izin IAM yang diperlukan.
  • REPOSITORY dan PACKAGE: repositori target dan nama paket yang dibuat di Artifact Registry. Untuk menyiapkan registry Artifact Registry, lihat Mengonfigurasi Artifact Registry.

Membuat dan mengirimkan pipeline build

Untuk menjalankan pipeline penyesuaian image, kirimkan build menggunakan gcloud CLI, deploy pipeline Anda menggunakan Terraform, atau kirimkan build menggunakan library klien Go. Pilih salah satu tab berikut:

gcloud

Untuk men-deploy dan menjalankan pipeline penyesuaian image, dari direktori terminal lokal yang berisi kedua file konfigurasi, jalankan perintah gcloud builds submit:

gcloud builds submit . \
    --config=cloudbuild.yaml \
    --project=PROJECT_ID \
    --service-account="projects/PROJECT_ID/serviceAccounts/SERVICE_ACCOUNT_EMAIL" \
    --region=REGION

Ganti kode berikut:

  • PROJECT_ID: Project ID Google Cloud Anda.
  • REGION: region Google Cloud untuk menjalankan tugas pipeline penyesuaian image Anda.
  • SERVICE_ACCOUNT_EMAIL: alamat email akun layanan yang dikonfigurasi dengan izin IAM yang diperlukan.

Perintah ini mengupload ruang kerja penyesuaian Anda, mendaftarkan eksekusi Cloud Build, dan meluncurkan container orkestrasi.

Terraform

Untuk menyediakan infrastruktur yang diperlukan guna membangun dan memvalidasi image OS yang disesuaikan secara otomatis, Anda dapat menggunakan Terraform. Konfigurasi Terraform ini menyelesaikan tugas-tugas berikut:

  • Mengaktifkan API Google Cloud yang diperlukan.
  • Membuat bucket Cloud Storage khusus (workdir_bucket) untuk menyimpan log sementara dan artefak build.
  • Mengonfigurasi pemicu Cloud Build (image_builder_trigger) yang ditautkan ke koneksi repositori GitHub Developer Connect Anda.

Membuat file konfigurasi Terraform

Untuk mengatur dan men-deploy infrastruktur pipeline Anda menggunakan Terraform, selesaikan langkah-langkah berikut:

  1. Buat direktori khusus di workstation lokal atau lingkungan CI/CD, terpisah dari repositori aplikasi, dan ubah ke direktori tersebut:

    mkdir terraform-image-builder && cd terraform-image-builder
    
  2. Di direktori ini, buat lima file konfigurasi Terraform berikut:

    • terraform.tfvars: menetapkan nilai untuk variabel khusus project.
    • main.tf: menyediakan resource di project Google Cloud Anda, termasuk mengaktifkan API yang diperlukan, membuat bucket staging Cloud Storage (workdir_bucket), dan men-deploy pemicu Cloud Build (image_builder_trigger).
    • outputs.tf: menentukan nilai output yang ditampilkan di terminal Anda setelah deployment, seperti ID pemicu dan nama bucket staging.
    • providers.tf: menentukan versi Terraform yang diperlukan (>= 1.3) dan mengonfigurasi penyedia Google Cloud (hashicorp/google).
    • variables.tf: menentukan variabel input, nilai default, dan aturan validasi untuk deployment.

    Pilih salah satu tab berikut untuk melihat dan menyalin konfigurasi setiap file ke direktori lokal Anda:

    terraform.tfvars

    File ini menentukan parameter value untuk lingkungan Anda untuk variabel yang dideklarasikan:

    project_id                = "PROJECT_ID"
    builder_service_account   = "SERVICE_ACCOUNT_EMAIL"
    github_repo_name          = "projects/PROJECT_ID/locations/LOCATION/connections/CONNECTION/repositories/REPO_NAME"
    region                    = "REGION"
    trigger_name              = "TRIGGER_NAME"
    cloudbuild_yaml_path      = "CLOUDBUILD_YAML_PATH"
    image_builder_config_path = "RECIPE_PATH"
    gcs_lifecycle_age_days    = LIFECYCLE_DAYS
    ar_repository_id          = "REPOSITORY_NAME"
    ar_package_name           = "PACKAGE_NAME"
    

    Ganti placeholder berikut untuk resource yang sudah ada:

    • PROJECT_ID: Project ID Google Cloud yang sudah ada.
    • SERVICE_ACCOUNT_EMAIL: alamat email akun layanan build Anda yang dikonfigurasi di Mengonfigurasi akun layanan Image Builder.
    • LOCATION, CONNECTION, dan REPO_NAME: region host Developer Connect, nama koneksi, dan link repositori yang dikonfigurasi di Menghubungkan repositori.
    • CLOUDBUILD_YAML_PATH: jalur relatif ke file cloudbuild.yaml di direktori lokal Anda. Anda tidak perlu memasukkan cloudbuild.yaml ke repositori Git Anda.
    • RECIPE_PATH: jalur relatif ke file resep penyesuaian imagebuilder.yaml yang di-check in ke repositori Git Anda.
    • REPOSITORY_NAME: repositori Artifact Registry generik yang sudah ada dan dibuat di Mengonfigurasi Artifact Registry.

    Ganti placeholder berikut untuk resource yang dibuat Terraform:

    • REGION: region target Google Cloud tempat Terraform menyediakan bucket staging Cloud Storage dan pemicu Cloud Build, misalnya, us-central1.
    • TRIGGER_NAME: nama untuk pemicu repositori Cloud Build baru yang dibuat oleh Terraform, misalnya, git-push-os-builder.
    • LIFECYCLE_DAYS: masa retensi dalam hari sebelum artefak perantara di bucket staging Cloud Storage yang dibuat oleh Terraform dihapus secara otomatis, misalnya, 30.
    • PACKAGE_NAME: nama yang Anda inginkan untuk digunakan Terraform untuk paket yang dibuat di dalam repositori Artifact Registry Anda. Paket ini menyimpan versi OS image yang dipublikasikan, misalnya, ubuntu-custom.

    main.tf

    File ini mendeklarasikan resource infrastruktur inti dan sumber data untuk deployment:

    # Main resource configurations for Image Builder.
    # 1. Enable Required APIs
    resource "google_project_service" "apis" {
      for_each = toset([
        "compute.googleapis.com",
        "cloudbuild.googleapis.com",
        "artifactregistry.googleapis.com",
        "serviceusage.googleapis.com",
        "cloudresourcemanager.googleapis.com",
        "iam.googleapis.com",
        "storage.googleapis.com"
      ])
      project = var.project_id
      service = each.key
      disable_on_destroy = false
    }
    
    # 2. Project data source to retrieve Project Number
    data "google_project" "project" {
      project_id = var.project_id
      depends_on = [google_project_service.apis]
    }
    
    locals {
      builder_sa = var.builder_service_account
    }
    
    # 3. Storage Bucket for Image Builder Workdir
    resource "google_storage_bucket" "workdir_bucket" {
      name                        = var.gcs_bucket_name != "" ? var.gcs_bucket_name : "${var.project_id}-vm-builder-workdir"
      project                     = var.project_id
      location                    = var.region
      force_destroy               = true
      uniform_bucket_level_access = true
      lifecycle_rule {
        action {
          type = "Delete"
        }
        condition {
          age = var.gcs_lifecycle_age_days
        }
      }
      depends_on = [google_project_service.apis]
    }
    
    # 4. Cloud Build Trigger
    resource "google_cloudbuild_trigger" "image_builder_trigger" {
      name        = var.trigger_name
      location    = var.region
      project     = var.project_id
      description = "Trigger that runs Image Builder customization"
    
      service_account = var.builder_service_account != "" ? "projects/${var.project_id}/serviceAccounts/${var.builder_service_account}" : null
    
      repository_event_config {
        repository = replace(var.github_repo_name, "gitRepositoryLinks", "repositories")
        push {
          branch = "^main$"
        }
      }
      filename = var.cloudbuild_yaml_path
    
      substitutions = {
        _GCS_WORKDIR                    = "gs://${google_storage_bucket.workdir_bucket.name}/workdir/"
        _SERVICE_ACCOUNT                = "projects/${var.project_id}/serviceAccounts/${local.builder_sa}"
        _IMAGE_OUTPUT_PATH              = "image-builder/binaryOut"
        _PROJECT_ID                     = var.project_id
        _LOCATION                       = var.region
        _REPOSITORY_NAME                = var.ar_repository_id
        _PACKAGE_NAME                   = var.ar_package_name
        _IMAGE_BUILDER_CONFIG_PATH      = var.image_builder_config_path
        _ARTIFACT_REGISTRY_RESOURCE_URI = "projects/${var.project_id}/locations/${var.region}/repositories/${var.ar_repository_id}/packages/${var.ar_package_name}/versions/v$${BUILD_ID}"
      }
      depends_on = [
        google_project_service.apis
      ]
    }
    

    outputs.tf

    File ini menentukan atribut output yang ditampilkan ke terminal Anda setelah deployment:

    output "builder_service_account" {
      value       = local.builder_sa
      description = "The email representation of the resolved Image Builder service account."
    }
    
    output "workdir_bucket" {
      value       = google_storage_bucket.workdir_bucket.name
      description = "The name of the storage workdir bucket."
    }
    
    output "artifact_registry_repository" {
      value       = "projects/${var.project_id}/locations/${var.region}/repositories/${var.ar_repository_id}"
      description = "The fully qualified resource path of the Artifact Registry repository."
    }
    
    output "cloud_build_trigger_id" {
      value       = google_cloudbuild_trigger.image_builder_trigger.trigger_id
      description = "The unique ID for the created Cloud Build Trigger."
    }
    

    providers.tf

    File ini mengonfigurasi setelan region dan versi Terraform yang diperlukan:

    terraform {
      required_version = ">= 1.3"
      required_providers {
        google = {
          source  = "hashicorp/google"
          version = ">= 5.0, < 7.0"
        }
      }
    }
    
    provider "google" {
      project = var.project_id
      region  = var.region
    }
    

    variables.tf

    File ini mendeklarasikan semua variabel input wajib dan opsional serta aturan validasi:

    variable "project_id" {
      type        = string
      description = "The target Project ID where resources will be created."
      validation {
        condition     = can(regex("^[a-z0-9-]{6,30}$", var.project_id))
        error_message = "The project_id must consist of lowercase letters, numbers, and hyphens, and be between 6 and 30 characters."
      }
    }
    
    variable "region" {
      type        = string
      default     = "us-central1"
      description = "Location used for cloud build triggers, storage buckets, and artifact registry."
    }
    
    variable "github_repo_name" {
      type        = string
      default     = ""
      description = "Developer Connect github repository details, format: projects/PROJECT_ID/locations/LOCATION/connections/CONNECTION/repositories/REPO_LINK"
      validation {
        condition     = can(regex("^projects/[^/]+/locations/[^/]+/connections/[^/]+/(gitRepositoryLinks|repositories)/[^/]+$", var.github_repo_name))
        error_message = "The github_repo_name must follow either the Cloud Build v2 repository link format (using '/repositories/') or the Developer Connect resource format (using '/gitRepositoryLinks/')."
      }
    }
    
    variable "builder_service_account" {
      type        = string
      default     = ""
      description = "The email representation of the pre-existing Image Builder service account. If omitted, the default Cloud Build service account will be used."
    }
    
    variable "gcs_bucket_name" {
      type        = string
      default     = ""
      description = "Custom name for the workdir storage bucket. If left empty, a default name using the project ID will be constructed."
    }
    
    variable "gcs_lifecycle_age_days" {
      type        = number
      default     = 30
      description = "The number of days after which temporary logs and artifacts in the storage workdir bucket are deleted."
    }
    
    variable "ar_repository_id" {
      type        = string
      default     = "vm-images"
      description = "The repository ID for the generic Artifact Registry hosting the final OS image tarballs."
    }
    
    variable "ar_package_name" {
      type        = string
      default     = "image-builder"
      description = "The package name under which the generic OS image artifact will be registered in Artifact Registry."
    }
    
    variable "trigger_name" {
      type        = string
      default     = "custom-os-image-builder"
      description = "The name of the Cloud Build trigger."
    }
    
    variable "cloudbuild_yaml_path" {
      type        = string
      default     = "cloudbuild.yaml"
      description = "The path to the cloudbuild.yaml configuration file relative to the repository root."
    }
    
    variable "image_builder_config_path" {
      type        = string
      default     = "imagebuilder.yaml"
      description = "The path to the imagebuilder.yaml configuration file relative to the repository root."
    }
    
  3. Untuk men-deploy konfigurasi, jalankan perintah berikut di direktori yang berisi file Terraform Anda:

    1. Lakukan inisialisasi direktori:
      terraform init
    2. Validasi sintaksis:
      terraform validate
    3. Melihat pratinjau deployment:
      terraform plan
    4. Terapkan konfigurasi:
      terraform apply

Go

Sebelum menjalankan contoh ini, upload file konfigurasi imagebuilder.yaml ke bucket staging Cloud Storage, misalnya, gs://STAGING_BUCKET_NAME/imagebuilder.yaml.

Untuk mengirimkan build Cloud Build yang menjalankan pipeline Image Builder menggunakan library klien Go Cloud Build, gunakan contoh berikut:

import (
	"context"
	"encoding/json"
	"fmt"
	"io"

	"google.golang.org/api/cloudbuild/v1"
)

// submitBuild submits a Cloud Build build that uses Image Builder to
// customize, validate, and publish a VM image to Compute Engine and Artifact
// Registry.
//
// Before you run this sample, upload your imagebuilder.yaml configuration file
// to Cloud Storage, create a generic Artifact Registry repository, and create a
// service account to run the build.
func submitBuild(w io.Writer, projectID, gcsWorkdir, configPath, serviceAccount string) error {
	// projectID := "my-project-id"
	region := "us-central1"
	// gcsWorkdir := "gs://my-bucket/workdir/"
	// configPath := "gs://my-bucket/imagebuilder.yaml"
	repository := "vm-images"
	packageName := "custom-os-images"
	// serviceAccount := "image-builder@my-project-id.iam.gserviceaccount.com"

	ctx := context.Background()
	cbService, err := cloudbuild.NewService(ctx)
	if err != nil {
		return fmt.Errorf("cloudbuild.NewService: %w", err)
	}

	sa := fmt.Sprintf("projects/%s/serviceAccounts/%s", projectID, serviceAccount)
	req := &cloudbuild.Build{
		ServiceAccount: sa,
		// AutomapSubstitutions exposes these substitutions to every step as
		// environment variables. See
		// https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values#mapping_substitutions_to_environment_variables
		Substitutions: map[string]string{
			"_GCS_WORKDIR":               gcsWorkdir,
			"_SERVICE_ACCOUNT":           sa,
			"_IMAGE_OUTPUT_PATH":         "image-builder/binaryOut",
			"_IMAGE_BUILDER_CONFIG_PATH": configPath,
			"_ARTIFACT_REGISTRY_RESOURCE_URI": fmt.Sprintf(
				"projects/%s/locations/%s/repositories/%s/packages/%s/versions/v${BUILD_ID}",
				projectID, region, repository, packageName,
			),
		},
		Steps: []*cloudbuild.BuildStep{
			{
				Id:     "imagebuilder-customize",
				Name:   "us-docker.pkg.dev/image-builder-official/release/builder:stable",
				Script: "#!/usr/bin/env bash\n/build",
				Results: []*cloudbuild.StepResult{
					{
						Name:            "base_image",
						AttestationType: "https://cloudbuild.googleapis.com/attestations/build_content_restrictions",
					},
					{Name: "image_builder_telemetry_metrics"},
				},
			},
			{
				Id:      "imagebuilder-validate",
				Name:    "us-docker.pkg.dev/image-builder-official/release/validator:stable",
				Script:  "#!/usr/bin/env bash\n/validate",
				Results: []*cloudbuild.StepResult{{Name: "image_builder_telemetry_metrics"}},
			},
			{
				Id:      "imagebuilder-publish",
				Name:    "us-docker.pkg.dev/image-builder-official/release/builder:stable",
				Script:  "#!/usr/bin/env bash\n/publish",
				Results: []*cloudbuild.StepResult{{Name: "image_builder_telemetry_metrics"}},
			},
		},
		Options: &cloudbuild.BuildOptions{
			AutomapSubstitutions:  true,
			DynamicSubstitutions:  true,
			RequestedVerifyOption: "VERIFIED",
			SubstitutionOption:    "ALLOW_LOOSE",
			// Builds that run as a user-specified service account must set a
			// logging option.
			Logging: "CLOUD_LOGGING_ONLY",
		},
		Artifacts: &cloudbuild.Artifacts{
			GenericArtifacts: []*cloudbuild.GenericArtifact{
				{
					Folder:       "${_IMAGE_OUTPUT_PATH}",
					RegistryPath: "${_ARTIFACT_REGISTRY_RESOURCE_URI}",
				},
			},
		},
		Timeout: "3600s",
	}

	parent := fmt.Sprintf("projects/%s/locations/%s", projectID, region)
	op, err := cbService.Projects.Locations.Builds.Create(parent, req).Context(ctx).Do()
	if err != nil {
		return fmt.Errorf("Builds.Create: %w", err)
	}

	// The operation metadata contains the build that was created.
	var metadata cloudbuild.BuildOperationMetadata
	if err := json.Unmarshal(op.Metadata, &metadata); err != nil {
		return fmt.Errorf("json.Unmarshal: %w", err)
	}
	fmt.Fprintf(w, "Build submitted. ID: %s\n", metadata.Build.Id)
	fmt.Fprintf(w, "Logs: %s\n", metadata.Build.LogUrl)
	return nil
}

Memverifikasi dan memantau build

Untuk melacak progres pipeline build Anda, selesaikan langkah-langkah berikut:

  1. Di konsol Google Cloud , buka halaman Cloud Build.

    Buka Cloud Build

  2. Di menu navigasi, klik History untuk melihat tugas yang aktif atau selesai.

  3. Dalam daftar Build, klik Build ID build Anda untuk memeriksa log operasi container. Log menampilkan langkah-langkah yang dilakukan di dalam worker VM, seperti update paket sistem atau perintah shell kustom, diikuti dengan hasil pengujian validasi dari VM pengujian, dan pendaftaran output akhir.

Langkah berikutnya