image_builder/modules/imagebuild/main.tf (272 lines of code) (raw):
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
resource "google_storage_bucket_object" "customize_script" {
count = var.customization_script_source == "" ? 0 : 1
name = "${local.gcs_prefix}/customize.pkr.hcl"
source = var.customization_script_source
bucket = local.gcs_bucket
}
resource "google_storage_bucket_object" "test_script" {
count = var.testing_script_source == "" ? 0 : 1
name = "${local.gcs_prefix}/test.pkr.hcl"
source = var.testing_script_source
bucket = local.gcs_bucket
}
resource "google_cloud_scheduler_job" "trigger_schedule" {
count = var.schedule_cron_pattern == "" ? 0 : 1
name = var.pipeline_name
project = var.project_id
region = var.region
schedule = var.schedule_cron_pattern
time_zone = var.schedule_timezone
http_target {
http_method = "POST"
uri = "https://cloudbuild.googleapis.com/v1/projects/${var.project_id}/locations/${var.region}/triggers/${var.pipeline_name}:run"
body = base64encode(
<<-EOT
{
projectId: '${var.project_id}',
triggerId: '${var.pipeline_name}',
}
EOT
)
oauth_token {
scope = "https://www.googleapis.com/auth/cloud-platform"
service_account_email = local.sa_email
}
}
}
resource "google_cloudbuild_trigger" "image_build_trigger" {
project = var.project_id
name = var.pipeline_name
description = var.pipeline_description
location = var.region
substitutions = merge(
{
_PROJECT_ID = var.project_id
_ZONE = var.zone
_GCS_FOLDER = var.gcs_folder
_TARGET_IMAGE_NAME = var.target_image_name
_TARGET_IMAGE_FAMILY = var.target_image_family
_TARGET_IMAGE_DESCRIPTION = var.target_image_description
_TARGET_IMAGE_REGION = var.target_image_region == "" ? var.region : var.target_image_region
_TARGET_IMAGE_ENCRYPTION_KEY = var.target_image_encryption_key
_TARGET_IMAGE_LABELS = jsonencode(var.target_image_labels)
_TARGET_IMAGE_LABELS_FLAT = join(",", [for k, v in var.target_image_labels : "${k}=${v}"])
}, local.src_subs)
repository_event_config {}
lifecycle {
ignore_changes = [
repository_event_config
]
}
service_account = var.service_account_id
tags = ["imagebuilder"]
build {
dynamic "step" {
for_each = flatten([
local.first_step,
var.customization_script_source == "" ? [local.create_target_image_step] : [],
var.customization_script_source == "" && var.testing_script_source == "" ? [] : [local.copy_packer_scripts_step],
var.customization_script_source != "" ? [local.customization_init_step, local.customization_step] : [],
local.add_label_step,
local.delete_temp_source_image_step,
var.testing_script_source != "" ? [local.test_init_step, local.test_step] : [],
var.target_image_family != "" ? [local.set_target_family_step] : []
])
iterator = custom_step
content {
name = custom_step.value.name
env = custom_step.value.env
args = custom_step.value.args
id = custom_step.value.id
}
}
options {
logging = "CLOUD_LOGGING_ONLY"
}
substitutions = {
_SUFFIXED_TARGET_IMAGE_NAME = "$${_TARGET_IMAGE_NAME}-$${BUILD_ID:0:8}"
_SUFFIXED_SOURCE_IMAGE_NAME = "$${_TARGET_IMAGE_NAME}-source-$${BUILD_ID:0:8}"
}
timeout = "3600s"
}
}
locals {
image_import_license = "https://www.googleapis.com/compute/v1/projects/prod-vmmig-images-public/global/licenses/image-builder-tf-license"
packer_image = "${var.region}-docker.pkg.dev/${var.project_id}/packer/packer"
gcs_bucket = split("/", trimprefix(var.gcs_folder, "gs://"))[0]
gcs_prefix = trim(trimprefix(trimprefix(var.gcs_folder, "gs://"), local.gcs_bucket), "/")
sa_split = split("/", var.service_account_id)
sa_email = element(local.sa_split, length(local.sa_split) - 1)
}
locals {
src_image = var.source_image.image != null ? regex("projects/(?P<project>[-a-z0-9]+)/global/images/(?P<image>[-a-z0-9]+)", var.source_image.image) : null
src_family = var.source_image.image_family != null ? regex("projects/(?P<project>[-a-z0-9]+)/global/images/family/(?P<family>[-a-z0-9]+)", var.source_image.image_family) : null
src_market_place = var.source_image.marketplace_image != null ? regex("projects/(?P<project>[-a-z0-9]+)/global/images/(?P<image>[-a-z0-9]+)", var.source_image.marketplace_image) : null
src_subs = (
local.src_image != null ? (
{
_SOURCE_IMAGE = local.src_image.image
_SOURCE_IMAGE_OS = local.src_image.project
}
) : local.src_family != null ? (
{
_SOURCE_IMAGE_FAMILY = local.src_family.family
_SOURCE_IMAGE_FAMILY_OS = local.src_family.project
}
) : local.src_market_place != null ? (
{
_MARKETPLACE_IMAGE = local.src_market_place.image
_MARKETPLACE_IMAGE_PROJECT = local.src_market_place.project
}
) : var.source_image.import != null ? (
{
_IMPORT_GCS_FILE = "TBD"
_IMPORT_SKIP_OS_ADAPTATION = "TBD"
_IMPORT_GENERALIZE = "TBD"
_IMPORT_LICENSE_TYPE = "TBD"
_IMPORT_ADDITIONAL_LICENSES = "TBD"
_IMPORT_OS = "TBD"
_IMPORT_CPU_ARCHITECTURE = "TBD"
_IMPORT_LOCATION = "TBD"
_IMPORT_TARGET_PROJECT = "TBD"
}
) : {}
)
}
locals {
create_temporary_source_image_step = {
name = "gcr.io/google.com/cloudsdktool/cloud-sdk"
env = []
args = ["gcloud", "compute", "images", "create", "$${_SUFFIXED_SOURCE_IMAGE_NAME}",
"--source-image=$${_SOURCE_IMAGE}",
"--source-image-project=$${_SOURCE_IMAGE_OS}",
"--licenses=${local.image_import_license}"
]
id = "create-temporary-source-image"
}
create_temporary_source_image_from_family_step = {
name = "gcr.io/google.com/cloudsdktool/cloud-sdk"
env = []
args = ["gcloud", "compute", "images", "create", "$${_SUFFIXED_SOURCE_IMAGE_NAME}",
"--source-image-family=$${_SOURCE_IMAGE_FAMILY}",
"--source-image-project=$${_SOURCE_IMAGE_FAMILY_OS}",
"--licenses=${local.image_import_license}"
]
id = "create-temporary-source-image"
}
copy_market_place_image_step = {
name = "gcr.io/google.com/cloudsdktool/cloud-sdk"
env = []
args = ["gcloud", "compute", "images", "create", "$${_SUFFIXED_SOURCE_IMAGE_NAME}",
"--source-image=$${_MARKETPLACE_IMAGE}",
"--source-image-project=$${_MARKETPLACE_IMAGE_PROJECT}",
"--licenses=${local.image_import_license}"
]
id = "copy-marketplace-image"
}
import_image_step = {
name = "gcr.io/google.com/cloudsdktool/cloud-sdk"
env = []
# entrypoint = "bash"
args = ["-eEuo", "pipefail", "-c",
# TBD
]
id = "import-image"
}
first_step = (
local.src_image != null ? (
local.create_temporary_source_image_step
) : local.src_family != null ? (
local.create_temporary_source_image_from_family_step
) : local.src_market_place != null ? (
local.copy_market_place_image_step
) : local.import_image_step
)
create_target_image_step = {
name = "gcr.io/google.com/cloudsdktool/cloud-sdk"
env = []
args = ["gcloud", "compute", "images", "create", "$${_SUFFIXED_TARGET_IMAGE_NAME}",
"--description=$${_TARGET_IMAGE_DESCRIPTION}",
"--storage-location=$${_TARGET_IMAGE_REGION}",
"--kms-key=$${_TARGET_IMAGE_ENCRYPTION_KEY}",
"--labels=$${_TARGET_IMAGE_LABELS_FLAT}",
"--source-image=$${_SUFFIXED_SOURCE_IMAGE_NAME}"
]
id = "create-target-image"
}
copy_packer_scripts_step = {
name = "gcr.io/google.com/cloudsdktool/cloud-sdk"
env = []
args = ["gcloud", "storage", "cp", "gs://$${_GCS_FOLDER}*.pkr.hcl", "."]
id = "copy-packer-scripts"
}
customization_init_step = {
name = local.packer_image
env = []
args = ["init", "customize.pkr.hcl"]
id = "initialize-customization"
}
customization_step = {
name = local.packer_image
env = [
"PKR_VAR_project_id=$${_PROJECT_ID}",
"PKR_VAR_zone=$${_ZONE}",
"PKR_VAR_target_image_name=$${_SUFFIXED_TARGET_IMAGE_NAME}",
"PKR_VAR_target_image_description=$${_TARGET_IMAGE_DESCRIPTION}",
"PKR_VAR_target_image_region=$${_TARGET_IMAGE_REGION}",
"PKR_VAR_target_image_encryption_key=$${_TARGET_IMAGE_ENCRYPTION_KEY}",
"PKR_VAR_target_image_labels=$${_TARGET_IMAGE_LABELS}",
"PKR_VAR_source_image=$${_SUFFIXED_SOURCE_IMAGE_NAME}",
]
args = ["build", "customize.pkr.hcl"]
id = "customize-image"
}
add_label_step = {
name = "gcr.io/google.com/cloudsdktool/cloud-sdk"
env = []
args = ["gcloud", "compute", "images", "add-labels", "$${_SUFFIXED_TARGET_IMAGE_NAME}", "--labels=pipeline=$${TRIGGER_NAME},build-id=$${BUILD_ID}"]
id = "add-pipeline-label"
}
delete_temp_source_image_step = {
name = "gcr.io/google.com/cloudsdktool/cloud-sdk"
env = []
args = ["gcloud", "compute", "images", "delete", "$${_SUFFIXED_SOURCE_IMAGE_NAME}"]
id = "delete-temporary-source-image"
}
test_init_step = {
name = local.packer_image
env = []
args = ["init", "test.pkr.hcl"]
id = "initialize-test"
}
test_step = {
name = local.packer_image
env = [
"PKR_VAR_project_id=$${_PROJECT_ID}",
"PKR_VAR_zone=$${_ZONE}",
"PKR_VAR_target_image_name=$${_SUFFIXED_TARGET_IMAGE_NAME}",
"PKR_VAR_target_image_encryption_key=$${_TARGET_IMAGE_ENCRYPTION_KEY}",
]
args = ["build", "test.pkr.hcl"]
id = "test-image"
}
set_target_family_step = {
name = "gcr.io/google.com/cloudsdktool/cloud-sdk"
env = []
args = ["gcloud", "compute", "images", "update", "$${_SUFFIXED_TARGET_IMAGE_NAME}", "--family=$${_TARGET_IMAGE_FAMILY}"]
id = "set-image-family"
}
}