透過程式輔助方式建立自訂 OS 映像檔管道

您可以使用 gcloud CLI、Terraform 或 Go 等用戶端程式庫,透過程式輔助方式設定及提交 Image Builder 管道。透過程式輔助方式設定管道,即可在宣告式設定檔中定義基礎架構設定、基本 OS 映像檔、自訂動作和驗證測試。

事前準備

必要的角色

如要取得透過程式建立及提交圖片自訂管道所需的權限,請要求管理員在專案中授予您下列 IAM 角色:

如要進一步瞭解如何授予角色,請參閱「管理專案、資料夾和組織的存取權」。

您或許也能透過自訂角色或其他預先定義的角色,取得必要權限。

建立設定檔

如要使用 gcloud CLI 或 Terraform 設定管道,請建立兩個設定檔:

  • imagebuilder.yaml:定義映像檔的自訂配方,包括基礎 OS 映像檔、worker VM 基礎架構設定、目標映像檔輸出詳細資料,以及依序執行的自訂步驟,例如執行殼層指令碼、傳輸檔案或重新啟動。
  • cloudbuild.yaml:在 Cloud Build 中自動調度建構程序步驟,包括剖析、驗證、建構、測試及發布自訂 OS 映像檔。

如果您使用 Go 提交建構作業,則只需建立 imagebuilder.yaml 檔案並上傳至 Cloud Storage bucket,因為 Go 範例會在程式碼中定義 Cloud Build 協調步驟。

建立映像檔設定檔

如要指定映像檔設定,請在本機目錄中建立名為 imagebuilder.yaml 的檔案。如需所有支援的結構定義欄位和自訂動作完整清單,請參閱「自訂配方結構定義」和「支援的自訂動作」。

下列 imagebuilder.yaml 檔案範例會設定管道,使用指定區域和可用區中的 e2-standard-4 worker VM 建構自訂 Ubuntu 22.04 LTS 映像檔,並執行系統套件更新。

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"

替換下列預留位置值:

  • PROJECT_ID:您的專案 ID。
  • REGION:目標映像檔儲存位置,例如 us-east1 或 europe-west1。請確認您符合下列區域和可用區規定:
    • Image Builder 僅適用於支援 Cloud Build 的區域。
    • worker VM ZONE 必須位於您指定的 REGION 內。
    • 為盡可能縮短網路延遲時間,並避免產生跨區域輸出費用,請確保 worker VM 可用區、Cloud Storage 暫存 bucket、Artifact Registry 存放區和目標映像檔儲存位置都位於相同區域。
  • ZONE:位於指定 REGION 內的可用區,例如 us-east1-b 或 europe-west1-b。

建立自動調度管理工具建構檔案

在同一個目錄中建立名為 cloudbuild.yaml 的檔案。這個檔案會呼叫 Image Builder 容器步驟,建構、驗證及發布自訂 OS 映像檔。

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'

替換下列預留位置值:

  • STAGING_BUCKET_NAME:專案中現有的 Cloud Storage bucket,用來做為臨時暫存工作區。如果沒有 bucket,可以執行 gcloud storage buckets create gs://STAGING_BUCKET_NAME 來建立。如果您使用 Terraform 部署管道,Terraform 會自動建立這個 bucket。
  • PROJECT_ID:您的 Cloud de Confiance 專案 ID。
  • REGION:Artifact Registry 存放區的 Cloud de Confiance 區域,例如 us-east1 或 europe-west1。
  • SERVICE_ACCOUNT_EMAIL:已設定必要 IAM 權限的服務帳戶電子郵件地址。
  • REPOSITORY 和 PACKAGE:在 Artifact Registry 中建立的目標存放區和套件名稱。如要設定 Artifact Registry 存放區,請參閱「設定 Artifact Registry」。

建立並提交建構管道

如要執行圖片自訂管道,請使用 gcloud CLI 提交建構作業、使用 Terraform 部署管道,或使用 Go 用戶端程式庫提交建構作業。選取下列其中一個分頁:

gcloud

如要部署及執行映像檔自訂 pipeline,請在本機終端機目錄中,執行 gcloud builds submit 指令,該目錄應包含兩個設定檔:

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

更改下列內容:

  • PROJECT_ID:您的 Cloud de Confiance 專案 ID。
  • REGION:執行映像檔自訂管道工作的 Cloud de Confiance 區域。
  • SERVICE_ACCOUNT_EMAIL:服務帳戶的電子郵件地址,該帳戶已設定必要的 IAM 權限。

這個指令會上傳自訂工作區、註冊 Cloud Build 執行作業,並啟動自動化調度管理容器。

Terraform

如要佈建自動建構及驗證自訂 OS 映像檔所需的基礎架構,可以使用 Terraform。這項 Terraform 設定會完成下列工作:

  • 啟用必要的 Cloud de Confiance API。
  • 建立專屬 Cloud Storage bucket (workdir_bucket),用於儲存臨時記錄和建構構件。
  • 設定連結至 Developer Connect GitHub 存放區連線的 Cloud Build 自動建構觸發條件 (image_builder_trigger)。

建立 Terraform 設定檔

如要使用 Terraform 整理及部署管道基礎架構,請完成下列步驟:

  1. 在本機工作站或 CI/CD 環境中,建立與應用程式存放區分開的專屬目錄,然後切換至該目錄:

    mkdir terraform-image-builder && cd terraform-image-builder
    
  2. 在這個目錄中,建立下列五個 Terraform 設定檔:

    • terraform.tfvars:設定專案專屬變數的值。
    • main.tf:在 Cloud de Confiance 專案中佈建資源,包括啟用必要的 API、建立 Cloud Storage 暫存 bucket (workdir_bucket),以及部署 Cloud Build 自動建構觸發條件 (image_builder_trigger)。
    • outputs.tf:定義部署後顯示在終端機的輸出值,例如觸發條件 ID 和暫存 bucket 名稱。
    • providers.tf:指定必要的 Terraform 版本 (>= 1.3),並設定 Cloud de Confiance by S3NS 供應商 (hashicorp/google)。
    • variables.tf:定義部署作業的輸入變數、預設值和驗證規則。

    選取下列其中一個分頁標籤,即可查看各檔案的設定,並複製到本機目錄:

    terraform.tfvars

    這個檔案會為環境指定已宣告變數的參數值:

    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"
    

    將下列預留位置替換為現有資源:

    • PROJECT_ID:現有的Cloud de Confiance by S3NS 專案 ID。
    • SERVICE_ACCOUNT_EMAIL:建構服務帳戶的電子郵件地址,該帳戶是在「設定 Image Builder 服務帳戶」中設定。
    • LOCATION、CONNECTION 和 REPO_NAME:您在「連結存放區」中設定的 Developer Connect 主機區域、連線名稱和存放區連結。
    • CLOUDBUILD_YAML_PATH:本機目錄中 cloudbuild.yaml 檔案的相對路徑。您不需要將 cloudbuild.yaml 簽入 Git 存放區。
    • RECIPE_PATH:imagebuilder.yaml 自訂配方檔案的相對路徑,該檔案已簽入 Git 存放區。
    • REPOSITORY_NAME:在「設定 Artifact Registry」中建立的現有一般 Artifact Registry 存放區。

    將下列預留位置替換為 Terraform 建立的資源:

    • REGION:Terraform 佈建 Cloud Storage 暫存 bucket 和 Cloud Build 自動建構觸發條件的目標 Cloud de Confiance區域,例如 us-central1。
    • TRIGGER_NAME:Terraform 建立的新 Cloud Build 存放區觸發條件名稱,例如 git-push-os-builder。
    • LIFECYCLE_DAYS:Terraform 建立的 Cloud Storage 暫存 bucket 中,中繼構件自動刪除前的保留天數,例如 30。
    • PACKAGE_NAME:Terraform 在 Artifact Registry 存放區中建立套件時,應使用的名稱。這個套件會儲存已發布的 OS 映像檔版本,例如 ubuntu-custom。

    main.tf

    這個檔案會宣告部署作業的核心基礎架構資源和資料來源:

    # 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

    這個檔案定義了部署後傳回終端機的輸出屬性:

    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

    這個檔案會設定必要的 Terraform 版本和區域設定:

    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

    這個檔案會宣告所有必要和選用的輸入變數和驗證規則:

    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. 如要部署設定,請在包含 Terraform 檔案的目錄中執行下列指令:

    1. 初始化目錄:
      terraform init
    2. 驗證語法:
      terraform validate
    3. 預覽部署作業:
      terraform plan
    4. 套用設定:
      terraform apply

Go

執行這個範例前,請先將 imagebuilder.yaml 設定檔上傳至 Cloud Storage 暫存 bucket,例如 gs://STAGING_BUCKET_NAME/imagebuilder.yaml。

如要使用 Cloud Build Go 用戶端程式庫,提交執行 Image Builder 管道的 Cloud Build 建構作業,請使用下列範例:

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
}

驗證及監控建構作業

如要追蹤建構管道的進度,請完成下列步驟:

  1. 前往 Cloud de Confiance 控制台的「Cloud Build」頁面。

    前往 Cloud Build

  2. 在導覽選單中,按一下「History」(記錄),查看執行中或已完成的工作。

  3. 在「Builds」(建構作業) 清單中,按一下建構作業的「Build ID」(建構 ID),檢查容器執行記錄。記錄會顯示在 worker VM 中執行的步驟,例如系統套件更新或自訂殼層指令,接著是測試 VM 的驗證測試結果,以及最終輸出註冊。

後續步驟