diff --git a/README.md b/README.md
index 8061f777c..bdd0fc203 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ Currently available modules:
- **compute** - [VM/VM group](./modules/compute-vm), [MIG](./modules/compute-mig), [COS container](./modules/cloud-config-container/cos-generic-metadata/) (coredns, mysql, onprem, squid), [GKE cluster](./modules/gke-cluster-standard), [GKE hub](./modules/gke-hub), [GKE nodepool](./modules/gke-nodepool), [GCVE private cloud](./modules/gcve-private-cloud)
- **data** - [AlloyDB instance](./modules/alloydb), [Analytics Hub](./modules/analytics-hub), [BigQuery dataset](./modules/bigquery-dataset), [Biglake Catalog](./modules/biglake-catalog), [Bigtable instance](./modules/bigtable-instance), [Dataplex](./modules/dataplex), [Dataplex Aspect Types](./modules/dataplex-aspect-types/), [Dataplex DataScan](./modules/dataplex-datascan), [Cloud SQL instance](./modules/cloudsql-instance), [Spanner instance](./modules/spanner-instance), [Firestore](./modules/firestore), [Data Catalog Policy Tag](./modules/data-catalog-policy-tag), [Data Catalog Tag](./modules/data-catalog-tag), [Data Catalog Tag Template](./modules/data-catalog-tag-template), [Datafusion](./modules/datafusion), [Dataproc](./modules/dataproc), [GCS](./modules/gcs), [Pub/Sub](./modules/pubsub), [Dataform Repository](./modules/dataform-repository/), [Looker Core](./modules/looker-core)
- **AI** - [AI Applications](./modules/ai-applications/README.md)
-- **development** - [API Gateway](./modules/api-gateway), [Apigee](./modules/apigee), [Artifact Registry](./modules/artifact-registry), [Container Registry](./modules/container-registry), [Cloud Source Repository](./modules/source-repository), [Secure Source Manager instance](./modules/secure-source-manager-instance), [Workstation cluster](./modules/workstation-cluster)
+- **development** - [API Gateway](./modules/api-gateway), [Apigee](./modules/apigee), [Artifact Registry](./modules/artifact-registry), [Container Registry](./modules/container-registry), [Cloud Source Repository](./modules/source-repository), [Cloud Deploy](./modules/cloud-deploy), [Secure Source Manager instance](./modules/secure-source-manager-instance), [Workstation cluster](./modules/workstation-cluster)
- **security** - [Binauthz](./modules/binauthz/), [Certificate Authority Service (CAS)](./modules/certificate-authority-service), [KMS](./modules/kms), [SecretManager](./modules/secret-manager), [VPC Service Control](./modules/vpc-sc), [Certificate Manager](./modules/certificate-manager/)
- **serverless** - [Cloud Function v1](./modules/cloud-function-v1), [Cloud Function v2](./modules/cloud-function-v2), [Cloud Run](./modules/cloud-run), [Cloud Run v2](./modules/cloud-run-v2)
diff --git a/fast/stages/1-vpcsc/main.tf b/fast/stages/1-vpcsc/main.tf
index 827c69fee..f3a29e4b3 100644
--- a/fast/stages/1-vpcsc/main.tf
+++ b/fast/stages/1-vpcsc/main.tf
@@ -43,6 +43,7 @@ module "vpc-sc-discovery" {
ignore_folders = var.resource_discovery.ignore_folders
ignore_projects = var.resource_discovery.ignore_projects
include_projects = var.resource_discovery.include_projects
+ query = "state:ACTIVE"
}
module "vpc-sc" {
diff --git a/modules/README.md b/modules/README.md
index cb194698c..7529d614d 100644
--- a/modules/README.md
+++ b/modules/README.md
@@ -112,6 +112,7 @@ These modules are used in the examples included in this repository. If you are u
- [Artifact Registry](./artifact-registry)
- [Container Registry](./container-registry)
- [Cloud Source Repository](./source-repository)
+- [Cloud Deploy](./cloud-deploy)
- [Secure Source Manager instance](./secure-source-manager-instance)
- [Workstation cluster](./workstation-cluster)
diff --git a/modules/ai-applications/README.md b/modules/ai-applications/README.md
index c31e09e1e..c9abd12c1 100644
--- a/modules/ai-applications/README.md
+++ b/modules/ai-applications/README.md
@@ -236,8 +236,8 @@ module "ai-applications" {
|---|---|:---:|:---:|:---:|
| [name](variables.tf#L159) | The name of the resources. | string | ✓ | |
| [project_id](variables.tf#L165) | The ID of the project where the data stores and the agents will be created. | string | ✓ | |
-| [data_stores_configs](variables.tf#L17) | The ai-applications datastore configurations. | map(object({…})) | | {} |
-| [engines_configs](variables.tf#L112) | The ai-applications engines configurations. | map(object({…})) | | {} |
+| [data_stores_configs](variables.tf#L17) | The ai-applications datastore configurations. | map(object({…})) | | {} |
+| [engines_configs](variables.tf#L112) | The ai-applications engines configurations. | map(object({…})) | | {} |
| [location](variables.tf#L153) | Location where the data stores and agents will be created. | string | | "global" |
## Outputs
diff --git a/modules/ai-applications/engines.tf b/modules/ai-applications/engines.tf
index 4c84f9b79..02b69cc74 100644
--- a/modules/ai-applications/engines.tf
+++ b/modules/ai-applications/engines.tf
@@ -56,8 +56,12 @@ resource "google_discovery_engine_chat_engine" "default" {
}
}
- common_config {
- company_name = each.value.chat_engine_config.company_name
+ dynamic "common_config" {
+ for_each = each.value.chat_engine_config.company_name == null ? [] : [""]
+
+ content {
+ company_name = each.value.chat_engine_config.company_name
+ }
}
}
diff --git a/modules/ai-applications/main.tf b/modules/ai-applications/main.tf
index 49ec255cb..1443aef67 100644
--- a/modules/ai-applications/main.tf
+++ b/modules/ai-applications/main.tf
@@ -123,7 +123,7 @@ resource "google_discovery_engine_data_store" "default" {
}
dynamic "parsing_config_overrides" {
- for_each = each.value.document_processing_config.parsing_config_overrides
+ for_each = coalesce(each.value.document_processing_config.parsing_config_overrides, {})
content {
file_type = parsing_config_overrides.key
diff --git a/modules/ai-applications/variables.tf b/modules/ai-applications/variables.tf
index 6739b44ed..37ce13efa 100644
--- a/modules/ai-applications/variables.tf
+++ b/modules/ai-applications/variables.tf
@@ -39,13 +39,13 @@ variable "data_stores_configs" {
}))
}))
# Accepted keys: docx, html, pdf
- parsing_config_overrides = map(object({
+ parsing_config_overrides = optional(map(object({
digital_parsing_config = optional(bool)
layout_parsing_config = optional(bool)
ocr_parsing_config = optional(object({
use_native_text = optional(bool)
}))
- }))
+ })))
}))
industry_vertical = optional(string, "GENERIC")
json_schema = optional(string)
@@ -115,7 +115,7 @@ variable "engines_configs" {
data_store_ids = list(string)
collection_id = optional(string, "default_collection")
chat_engine_config = optional(object({
- allow_cross_region = optional(bool)
+ allow_cross_region = optional(bool, null)
business = optional(string)
company_name = optional(string)
default_language_code = optional(string)
diff --git a/modules/cloud-deploy/README.md b/modules/cloud-deploy/README.md
new file mode 100644
index 000000000..268e53dcc
--- /dev/null
+++ b/modules/cloud-deploy/README.md
@@ -0,0 +1,454 @@
+# Cloud Deploy Module
+
+Cloud Deploy Module for creating and managing Delivery Pipelines, Targets, Automations, Deploy Policies and resource-level IAM roles.
+
+
+- [Limitations](#limitations)
+- [Examples](#examples)
+- [Single Target Canary Deployment](#single-target-canary-deployment)
+- [Single Target Canary Deployment with Custom Traffic Limits](#single-target-canary-deployment-with-custom-traffic-limits)
+- [Single Target Canary Deployment with Verification](#single-target-canary-deployment-with-verification)
+- [Delivery Pipeline with Existing Target](#delivery-pipeline-with-existing-target)
+- [Multiple Targets in Serial Deployment](#multiple-targets-in-serial-deployment)
+- [Multi Target Multi Project Deployment](#multi-target-multi-project-deployment)
+- [Multi Target with Serial and Parallel deployment](#multi-target-with-serial-and-parallel-deployment)
+- [Automation for Delivery Pipelines](#automation-for-delivery-pipelines)
+- [Deployment Policy](#deployment-policy)
+- [IAM for Delivery Pipeline and Target resource level](#iam-for-delivery-pipeline-and-target-resource-level)
+- [Variables](#variables)
+- [Outputs](#outputs)
+
+
+## Limitations
+> [!WARNING]
+> Currently this module only supports Cloud Run deployments and does not include GKE or Custom Target deployments.
+
+## Examples
+
+## Single Target Canary Deployment
+
+This deploys a Cloud Deploy Delivery Pipeline with a single target using the Canary deployment strategy, which by default routes 10% of traffic initially and upon success, shifts to 100% (making it the stable revision). By default `strategy = "STANDARD"` is set, to use canary strategy this needs to be changed to `strategy = "CANARY"`.
+
+
+```hcl
+module "cloud_deploy" {
+ source = "./fabric/modules/cloud-deploy"
+ project_id = var.project_id
+ region = var.region
+ name = "deployment-pipeline"
+
+ targets = [
+ {
+ name = "dev-target"
+ description = "Dev Target"
+ profiles = ["dev"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ }
+ ]
+}
+# tftest modules=1 resources=2
+```
+
+## Single Target Canary Deployment with Custom Traffic Limits
+
+This deploys a Cloud Deploy Delivery Pipeline with a single target with the Canary deployment strategy. `deployment_percentages` can be set to specify the traffic stages that would be applied during the canary deployment. It accepts integer values in ascending order and between 0 to 99.
+
+
+```hcl
+module "cloud_deploy" {
+ source = "./fabric/modules/cloud-deploy"
+ project_id = var.project_id
+ region = var.region
+ name = "deployment-pipeline"
+
+
+ targets = [
+ {
+ name = "dev-target"
+ description = "Dev Target"
+ profiles = ["dev"]
+ strategy = "CANARY"
+ deployment_percentages = [10, 50, 70]
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ }
+ ]
+}
+# tftest modules=1 resources=2
+```
+
+## Single Target Canary Deployment with Verification
+
+This deployments enables the rollout to have a verification step by setting `verify = true`. The verification step and configurations need to be passed within the skaffold file.
+
+```hcl
+module "cloud_deploy" {
+ source = "./fabric/modules/cloud-deploy"
+ project_id = var.project_id
+ region = var.region
+ name = "deployment-pipeline"
+
+
+ targets = [
+ {
+ name = "dev-target"
+ description = "Dev Target"
+ profiles = ["dev"]
+ strategy = "CANARY"
+ verify = true
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ }
+ ]
+}
+# tftest modules=1 resources=2
+```
+
+## Delivery Pipeline with Existing Target
+
+This deployment demonstrates the ability to create a delivery pipeline by reusing existing targets. By default a `create_target = true` is set, creating and assigning a target to the delivery pipeline. Setting it to false directs the code to assign the target to the delivery pipeline and skip its creation during execution.
+
+```hcl
+module "cloud_deploy" {
+ source = "./fabric/modules/cloud-deploy"
+ project_id = var.project_id
+ region = var.region
+ name = "deployment-pipeline"
+
+
+ targets = [
+ {
+ create_target = false
+ name = "dev-target"
+ description = "Dev Target"
+ profiles = ["dev"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ }
+ ]
+}
+# tftest modules=1 resources=1
+```
+
+## Multiple Targets in Serial Deployment
+
+Cloud Deployment supports deployments to multiple targets. This example shows how to create 3 targets and to set them in sequence.
+The sequence of deployment is defined by the sequence of the target configuration object within the list. `require_approval` can be set to true for any target that requires an approval prior to its deployment/rollout.
+
+
+```hcl
+module "cloud_deploy" {
+ source = "./fabric/modules/cloud-deploy"
+ project_id = var.project_id
+ region = var.region
+ name = "deployment-pipeline"
+
+
+ targets = [
+ {
+ name = "dev-target"
+ description = "Dev Target"
+ profiles = ["dev"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ },
+ {
+ name = "qa-target"
+ description = "QA Target"
+ profiles = ["qa"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ },
+ {
+ name = "prod-target"
+ description = "Prod Target"
+ profiles = ["prod"]
+ require_approval = true
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ }
+ ]
+}
+# tftest modules=1 resources=4
+```
+
+
+## Multi Target Multi Project Deployment
+
+Targets in this deployment can deploy to different projects. For instance, `qa-target` deploys to a separate `project_id` and `region`. To direct Cloud Run deployments to a different project, specify the `project_id` and `region` under `cloud_run_configs`. By default, Cloud Run services will use the target's own `project_id` and `region`.
+
+
+```hcl
+module "cloud_deploy" {
+ source = "./fabric/modules/cloud-deploy"
+ project_id = var.project_id
+ region = var.region
+ name = "deployment-pipeline"
+
+
+ targets = [
+ {
+ name = "dev-target"
+ description = "Dev Target"
+ profiles = ["dev"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ },
+ {
+ name = "qa-target"
+ description = "QA Target"
+ profiles = ["qa"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ project_id = ""
+ region = ""
+ automatic_traffic_control = true
+ }
+ },
+ {
+ name = "prod-target"
+ description = "Prod Target"
+ profiles = ["prod"]
+ require_approval = true
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ }
+ ]
+}
+# tftest modules=1 resources=4
+```
+
+## Multi Target with Serial and Parallel deployment
+
+Cloud Deploy allows deploying to targets in a serial and parallel order. By defining a multi-target target configuration using `multi_target_target_ids` cloud deploy would execute the deployments in parallel. `require_approval` should only be applied to the multi-target target configuration and not the the child targets. As the child targets would execute within the multi-target target configuration, they are excluded from being directly assigned in the serial sequence of the delivery pipeline, using `exclude_from_pipeline = true`.
+
+```hcl
+module "cloud_deploy" {
+ source = "./fabric/modules/cloud-deploy"
+ project_id = var.project_id
+ region = var.region
+ name = "deployment-pipeline"
+
+
+ targets = [
+ {
+ name = "dev-target"
+ description = "Dev Target"
+ profiles = ["dev"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ },
+ {
+ name = "multi-qa-target"
+ description = "Multi QA target"
+ profiles = ["multi-qa"]
+ multi_target_target_ids = ["qa-target-1", "qa-target-2"]
+ strategy = "STANDARD"
+ },
+ {
+ exclude_from_pipeline = true
+ name = "qa-target-1"
+ description = "QA target-1"
+ profiles = ["qa-1"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ },
+ {
+ exclude_from_pipeline = true
+ name = "qa-target-2"
+ description = "QA target-2"
+ profiles = ["qa-2"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ }
+ ]
+}
+# tftest modules=1 resources=5
+```
+
+## Automation for Delivery Pipelines
+
+This deployment incorporates automations that are supported within a delivery pipeline. If automations are defined at least 1 rule needs to be specified. Rules are defined as `"automation-name" = { }` format. Multiple automations can be defined and multiple rules can be specified within an automation. A `service_account` can be provided to execute the automation using the defined service account. If this is missing it defaults to the compute engine default service account (`-compute@developer.gserviceaccount.com`).
+
+```hcl
+module "cloud_deploy" {
+ source = "./fabric/modules/cloud-deploy"
+ project_id = var.project_id
+ region = var.region
+ name = "deployment-pipeline"
+
+ automations = {
+ "advance-rollout" = {
+ description = "advance_rollout_rule"
+ service_account = "@.iam.gserviceaccount.com"
+ advance_rollout_rule = {
+ source_phases = ["canary"]
+ wait = "200s"
+ }
+ },
+ "repair-rollout" = {
+ description = "repair_rollout_rule"
+ service_account = "@.iam.gserviceaccount.com"
+ repair_rollout_rule = {
+ jobs = ["predeploy", "deploy", "postdeploy", "verify"]
+ phases = ["canary-10", "stable"]
+ rollback = {
+ destination_phase = "stable"
+ }
+ }
+ }
+ }
+
+ targets = [
+ {
+ name = "dev-target"
+ description = "Dev Target"
+ profiles = ["dev"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ }
+ ]
+}
+# tftest modules=1 resources=4
+```
+
+## Deployment Policy
+
+This example provides a way to define a deployment policy along with the delivery pipeline. Each deploy policy can be defined as `"deploy_policy_name" = { }` format. Rollout restrictions are defined as `"restriction_name" = { }` format.
+By default, the deployment policy defined below applies to all delivery pipelines. If this requires a change, modify the selector option. Selector types supported are: "DELIVERY_PIPELINE" and "TARGET".
+
+```hcl
+module "cloud_deploy" {
+ source = "./fabric/modules/cloud-deploy"
+ project_id = var.project_id
+ region = var.region
+ name = "deployment-pipeline"
+
+ targets = [
+ {
+ name = "dev-target"
+ description = "Dev Target"
+ profiles = ["dev"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ }
+ ]
+
+ deploy_policies = {
+ "deploy-policy" = {
+ selectors = [{
+ id = "*"
+ type = "DELIVERY_PIPELINE"
+ }]
+ rollout_restrictions = {
+ "restriction-1" = {
+ time_zone = "Australia/Melbourne"
+ weekly_windows = [{
+ days_of_week = ["MONDAY", "TUESDAY"]
+
+ start_time = {
+ hours = "10"
+ minutes = "30"
+ seconds = "00"
+ nanos = "00"
+ }
+
+ end_time = {
+ hours = "12"
+ minutes = "30"
+ seconds = "00"
+ nanos = "00"
+ }
+ }]
+ } }
+ }
+ }
+}
+# tftest modules=1 resources=3
+```
+
+## IAM for Delivery Pipeline and Target resource level
+
+This example specifies the option to set IAM roles at the Delivery Pipeline and Target resource level. IAM bindings support the usual syntax.
+`iam`, `iam_bindings`, `iam_bindings_additive`, `iam_by_principals` are supported for delivery pipelines and targets.
+
+```hcl
+module "cloud_run" {
+ source = "./fabric/modules/cloud-deploy"
+ project_id = var.project_id
+ region = var.region
+ name = "deployment-pipeline"
+
+ iam = { "roles/clouddeploy.developer" = ["user:allUsers"] }
+
+ targets = [
+ {
+ name = "dev-target"
+ description = "Dev Target"
+ profiles = ["dev"]
+ strategy = "CANARY"
+ cloud_run_configs = {
+ automatic_traffic_control = true
+ }
+ iam = { "roles/clouddeploy.operator" = ["user:allUsers"] }
+ }
+ ]
+}
+# tftest modules=1 resources=4
+```
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [name](variables.tf#L191) | Cloud Deploy Delivery Pipeline name. | string | ✓ | |
+| [project_id](variables.tf#L201) | Project id used for resources, if not explicitly specified. | string | ✓ | |
+| [region](variables.tf#L206) | Region used for resources, if not explicitly specified. | string | ✓ | |
+| [annotations](variables.tf#L17) | Resource annotations. | map(string) | | {} |
+| [automations](variables.tf#L24) | Configuration for automations associated with the deployment pipeline in a name => attributes format. | map(object({…})) | | {} |
+| [deploy_policies](variables.tf#L84) | Configurations for Deployment Policies in a name => attributes format. | map(object({…})) | | {} |
+| [description](variables.tf#L165) | Cloud Deploy Delivery Pipeline description. | string | | "Terraform managed." |
+| [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_by_principals](variables-iam.tf#L54) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} |
+| [labels](variables.tf#L176) | Cloud Deploy Delivery Pipeline resource labels. | map(string) | | {} |
+| [suspended](variables.tf#L211) | Configuration to suspend a delivery pipeline. | bool | | false |
+| [targets](variables.tf#L218) | Configuration for new targets associated with the delivery pipeline in a list format. Order of the targets are defined by the order within the list. | list(object({…})) | | [] |
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [automation_ids](outputs.tf#L18) | Automation ids. | |
+| [deploy_policy_ids](outputs.tf#L23) | Deploy Policy ids. | |
+| [pipeline_id](outputs.tf#L28) | Delivery pipeline id. | |
+| [target_ids](outputs.tf#L33) | Target ids. | |
+
diff --git a/modules/cloud-deploy/automation.tf b/modules/cloud-deploy/automation.tf
new file mode 100644
index 000000000..aaff9dd30
--- /dev/null
+++ b/modules/cloud-deploy/automation.tf
@@ -0,0 +1,134 @@
+/**
+ * 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_clouddeploy_automation" "default" {
+ for_each = local.validated_automations
+
+ project = coalesce(each.value.project_id, var.project_id)
+ location = coalesce(each.value.region, var.region)
+ name = each.key
+ annotations = each.value.annotations
+ delivery_pipeline = google_clouddeploy_delivery_pipeline.default.name
+ description = each.value.description
+ labels = each.value.labels
+ service_account = coalesce(each.value.service_account, local.compute_default_service_account)
+ suspended = each.value.suspended
+
+ dynamic "rules" {
+ for_each = each.value.advance_rollout_rule == null ? [] : [""]
+
+ content {
+ dynamic "advance_rollout_rule" {
+ for_each = each.value.advance_rollout_rule == null ? [] : [""]
+
+ content {
+ id = each.value.advance_rollout_rule.id
+ source_phases = each.value.advance_rollout_rule.source_phases
+ wait = each.value.advance_rollout_rule.wait
+ }
+ }
+ }
+ }
+
+ dynamic "rules" {
+ for_each = each.value.promote_release_rule == null ? [] : [""]
+
+ content {
+ dynamic "promote_release_rule" {
+ for_each = each.value.promote_release_rule == null ? [] : [""]
+
+ content {
+ id = each.value.promote_release_rule.id
+ destination_phase = each.value.promote_release_rule.destination_target_id
+ destination_target_id = each.value.promote_release_rule.destination_target_id
+ wait = each.value.promote_release_rule.wait
+ }
+ }
+ }
+ }
+
+ dynamic "rules" {
+ for_each = each.value.repair_rollout_rule == null ? [] : [""]
+
+ content {
+ dynamic "repair_rollout_rule" {
+ for_each = each.value.repair_rollout_rule == null ? [] : [""]
+
+ content {
+ id = each.value.repair_rollout_rule.id
+ jobs = each.value.repair_rollout_rule.jobs
+ phases = each.value.repair_rollout_rule.phases
+
+ dynamic "repair_phases" {
+ for_each = each.value.repair_rollout_rule.retry == null ? [] : [""]
+
+ content {
+ retry {
+ attempts = each.value.repair_rollout_rule.retry.attempts
+ backoff_mode = each.value.repair_rollout_rule.retry.backoff_mode
+ wait = each.value.repair_rollout_rule.retry.wait
+ }
+ }
+ }
+ dynamic "repair_phases" {
+ for_each = each.value.repair_rollout_rule.rollback == null ? [] : [""]
+
+ content {
+ rollback {
+ destination_phase = each.value.repair_rollout_rule.rollback.destination_phase
+ disable_rollback_if_rollout_pending = each.value.repair_rollout_rule.rollback.disable_rollback_if_rollout_pending
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ dynamic "rules" {
+ for_each = each.value.timed_promote_release_rule == null ? [] : [""]
+
+ content {
+ dynamic "timed_promote_release_rule" {
+ for_each = each.value.timed_promote_release_rule == null ? [] : [""]
+
+ content {
+ id = each.value.timed_promote_release_rule.id
+ destination_phase = each.value.timed_promote_release_rule.destination_phase
+ destination_target_id = each.value.timed_promote_release_rule.destination_target_id
+ schedule = each.value.timed_promote_release_rule.schedule
+ time_zone = each.value.timed_promote_release_rule.time_zone
+ }
+ }
+ }
+ }
+
+ selector {
+ dynamic "targets" {
+ for_each = {
+ for k, v in each.value.selector :
+ k => v
+ }
+ iterator = et
+
+ content {
+ id = et.value.id
+ labels = et.value.labels
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/cloud-deploy/delivery-pipeline.tf b/modules/cloud-deploy/delivery-pipeline.tf
new file mode 100644
index 000000000..5abdef68f
--- /dev/null
+++ b/modules/cloud-deploy/delivery-pipeline.tf
@@ -0,0 +1,158 @@
+/**
+ * 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_clouddeploy_delivery_pipeline" "default" {
+ project = var.project_id
+ location = var.region
+ name = var.name
+ annotations = var.annotations
+ description = var.description
+ labels = var.labels
+ suspended = var.suspended
+
+ dynamic "serial_pipeline" {
+ for_each = lower(local.pipeline_type) == "serial" ? [""] : []
+
+ content {
+ dynamic "stages" {
+ for_each = {
+ for k, v in var.targets :
+ k => v if v.exclude_from_pipeline == false
+ }
+ iterator = et
+
+ content {
+ profiles = et.value.profiles
+ target_id = et.value.name
+
+ dynamic "deploy_parameters" {
+ for_each = {
+ for k, v in et.value.delivery_pipeline_deploy_parameters :
+ k => v
+ }
+ iterator = edp
+
+ content {
+ match_target_labels = edp.value.matching_target_labels
+ values = edp.value.values
+ }
+ }
+
+ dynamic "strategy" {
+ for_each = et.value.strategy == null ? [] : [""]
+
+ content {
+ dynamic "canary" {
+ for_each = upper(et.value.strategy) == "CANARY" ? [""] : []
+
+ content {
+ canary_deployment {
+ percentages = et.value.deployment_percentages
+ verify = et.value.verify
+
+ dynamic "postdeploy" {
+ for_each = et.value.postdeploy_actions == null ? [] : [""]
+ content {
+ actions = et.value.postdeploy_actions
+ }
+ }
+
+ dynamic "predeploy" {
+ for_each = et.value.predeploy_actions == null ? [] : [""]
+ content {
+ actions = et.value.predeploy_actions
+ }
+ }
+ }
+
+ dynamic "custom_canary_deployment" {
+ for_each = length(et.value.custom_canary_phase_configs) > 0 ? [""] : []
+
+ content {
+ dynamic "phase_configs" {
+ for_each = et.value.custom_canary_phase_configs
+ iterator = epc
+
+ content {
+ percentage = epc.value.percentage
+ phase_id = epc.key
+ verify = epc.value.verify
+
+ dynamic "postdeploy" {
+ for_each = epc.value.postdeploy_actions == null ? [] : [""]
+
+ content {
+ actions = epc.value.postdeploy_actions
+ }
+ }
+
+ dynamic "predeploy" {
+ for_each = epc.value.predeploy_actions == null ? [] : [""]
+
+ content {
+ actions = epc.value.predeploy_actions
+ }
+ }
+ }
+ }
+ }
+ }
+
+ runtime_config {
+ dynamic "cloud_run" {
+ for_each = et.value.cloud_run_configs == null ? [] : [""]
+
+ content {
+ automatic_traffic_control = et.value.cloud_run_configs.automatic_traffic_control
+ canary_revision_tags = et.value.cloud_run_configs.canary_revision_tags
+ prior_revision_tags = et.value.cloud_run_configs.prior_revision_tags
+ stable_revision_tags = et.value.cloud_run_configs.stable_revision_tags
+ }
+ }
+ }
+ }
+ }
+
+ dynamic "standard" {
+ for_each = upper(et.value.strategy) == "STANDARD" ? [""] : []
+
+ content {
+ verify = et.value.verify
+
+ dynamic "postdeploy" {
+ for_each = et.value.postdeploy_actions == null ? [] : [""]
+ content {
+ actions = et.value.postdeploy_actions
+ }
+ }
+
+ dynamic "predeploy" {
+ for_each = et.value.predeploy_actions == null ? [] : [""]
+ content {
+ actions = et.value.predeploy_actions
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/modules/cloud-deploy/deploy-policy.tf b/modules/cloud-deploy/deploy-policy.tf
new file mode 100644
index 000000000..0cb6db85c
--- /dev/null
+++ b/modules/cloud-deploy/deploy-policy.tf
@@ -0,0 +1,159 @@
+/**
+ * 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_clouddeploy_deploy_policy" "default" {
+ for_each = var.deploy_policies
+
+ project = coalesce(each.value.project_id, var.project_id)
+ location = coalesce(each.value.region, var.region)
+ name = each.key
+ annotations = each.value.annotations
+ description = each.value.description
+ labels = each.value.labels
+ suspended = each.value.suspended
+
+ dynamic "rules" {
+ for_each = each.value.rollout_restrictions
+ iterator = er
+
+ content {
+ rollout_restriction {
+ id = er.key
+ actions = er.value.actions
+ invokers = er.value.invokers
+
+ time_windows {
+ time_zone = er.value.time_zone
+
+ dynamic "one_time_windows" {
+ for_each = {
+ for k, v in er.value.one_time_windows :
+ k => v
+ }
+ iterator = eotw
+
+ content {
+ dynamic "end_date" {
+ for_each = eotw.value.end_date == null ? [] : [""]
+
+ content {
+ day = eotw.value.end_date.day
+ month = eotw.value.end_date.month
+ year = eotw.value.end_date.year
+ }
+ }
+
+ dynamic "end_time" {
+ for_each = eotw.value.end_time == null ? [] : [""]
+
+ content {
+ hours = eotw.value.end_time.hours
+ minutes = eotw.value.end_time.minutes
+ seconds = eotw.value.end_time.seconds
+ nanos = eotw.value.end_time.nanos
+ }
+ }
+
+ dynamic "start_date" {
+ for_each = eotw.value.start_date == null ? [] : [""]
+
+ content {
+ day = eotw.value.start_date.day
+ month = eotw.value.start_date.month
+ year = eotw.value.start_date.year
+ }
+ }
+
+ dynamic "start_time" {
+ for_each = eotw.value.start_time == null ? [] : [""]
+
+ content {
+ hours = eotw.value.start_time.hours
+ minutes = eotw.value.start_time.minutes
+ seconds = eotw.value.start_time.seconds
+ nanos = eotw.value.start_time.nanos
+ }
+ }
+ }
+ }
+
+ dynamic "weekly_windows" {
+ for_each = {
+ for k, v in er.value.weekly_windows :
+ k => v
+ }
+ iterator = eww
+
+ content {
+ days_of_week = eww.value.days_of_week
+
+ dynamic "end_time" {
+ for_each = eww.value.end_time == null ? [] : [""]
+
+ content {
+ hours = eww.value.end_time.hours
+ minutes = eww.value.end_time.minutes
+ seconds = eww.value.end_time.seconds
+ nanos = eww.value.end_time.nanos
+ }
+ }
+
+ dynamic "start_time" {
+ for_each = eww.value.start_time == null ? [] : [""]
+
+ content {
+ hours = eww.value.start_time.hours
+ minutes = eww.value.start_time.minutes
+ seconds = eww.value.start_time.seconds
+ nanos = eww.value.start_time.nanos
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ dynamic "selectors" {
+ for_each = {
+ for k, v in each.value.selectors :
+ k => v
+ }
+ iterator = es
+
+ content {
+ dynamic "delivery_pipeline" {
+ for_each = upper(es.value.type) == "DELIVERY_PIPELINE" ? [""] : []
+
+ content {
+ id = es.value.id
+ labels = es.value.labels
+ }
+ }
+
+ dynamic "target" {
+ for_each = upper(es.value.type) == "TARGET" ? [""] : []
+
+ content {
+ id = es.value.id
+ labels = es.value.labels
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/cloud-deploy/iam.tf b/modules/cloud-deploy/iam.tf
new file mode 100644
index 000000000..cb1c8533e
--- /dev/null
+++ b/modules/cloud-deploy/iam.tf
@@ -0,0 +1,160 @@
+/**
+ * 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.
+ */
+
+
+# tfdoc:file:description IAM bindings.
+
+locals {
+ _iam_principal_roles = distinct(flatten(values(var.iam_by_principals)))
+ _iam_principals = {
+ for r in local._iam_principal_roles : r => [
+ for k, v in var.iam_by_principals :
+ k if try(index(v, r), null) != null
+ ]
+ }
+ _merge_target_iam = flatten([
+ for kt, vt in var.targets : [
+ for role in distinct(concat(keys(vt.iam), keys(local._target_iam_principals[vt.name]))) :
+ {
+ "project_id" = vt.project_id
+ "region" = vt.region
+ "name" = vt.name
+ "role" = role
+ "members" = concat(
+ try(vt.iam[role], []),
+ try(local._target_iam_principals[vt.name][role], [])
+ )
+ }
+ ]
+ ])
+ _target_iam_principal_roles = { for k, v in var.targets : v.name => distinct(flatten(values(v.iam_by_principals))) }
+ _target_iam_principals = {
+ for k, v in var.targets : v.name => {
+ for r in local._target_iam_principal_roles[v.name] : r => [
+ for kp, vp in v.iam_by_principals :
+ kp if try(index(vp, r), null) != null
+ ]
+ }
+ }
+ iam = {
+ for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) :
+ role => concat(
+ try(var.iam[role], []),
+ try(local._iam_principals[role], [])
+ )
+ }
+ target_iam = {
+ for k, v in local._merge_target_iam : k => v
+ }
+ target_iam_bindings = merge([
+ for k, v in var.targets : {
+ for ki, vi in v.iam_bindings :
+ "${ki}_${k}" => merge(vi, { "project_id" = v.project_id, "region" = v.region, "name" = v.name })
+ }
+ ]...)
+ target_iam_bindings_additive = merge([
+ for k, v in var.targets : {
+ for ki, vi in v.iam_bindings_additive :
+ "${ki}_${k}" => merge(vi, { "project_id" = v.project_id, "region" = v.region, "name" = v.name })
+ }
+ ]...)
+}
+
+resource "google_clouddeploy_delivery_pipeline_iam_binding" "authoritative" {
+ for_each = local.iam
+ project = var.project_id
+ location = var.region
+ name = var.name
+ role = each.key
+ members = each.value
+}
+
+resource "google_clouddeploy_delivery_pipeline_iam_binding" "bindings" {
+ for_each = var.iam_bindings
+ project = var.project_id
+ location = var.region
+ name = var.name
+ role = each.value.role
+ members = each.value.members
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
+
+resource "google_clouddeploy_delivery_pipeline_iam_member" "bindings" {
+ for_each = var.iam_bindings_additive
+ project = var.project_id
+ location = var.region
+ name = var.name
+ role = each.value.role
+ member = each.value.member
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
+
+resource "google_clouddeploy_target_iam_binding" "authoritative" {
+ for_each = local.target_iam
+ project = coalesce(each.value.project_id, var.project_id)
+ location = coalesce(each.value.region, var.region)
+ name = each.value.name
+ role = each.value.role
+ members = each.value.members
+}
+
+resource "google_clouddeploy_target_iam_binding" "bindings" {
+ for_each = local.target_iam_bindings
+ project = coalesce(each.value.project_id, var.project_id)
+ location = coalesce(each.value.region, var.region)
+ name = each.value.name
+ role = each.value.role
+ members = each.value.members
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
+
+resource "google_clouddeploy_target_iam_member" "bindings" {
+ for_each = local.target_iam_bindings_additive
+ project = coalesce(each.value.project_id, var.project_id)
+ location = coalesce(each.value.region, var.region)
+ name = each.value.name
+ role = each.value.role
+ member = each.value.member
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
diff --git a/modules/cloud-deploy/main.tf b/modules/cloud-deploy/main.tf
new file mode 100644
index 000000000..45675d594
--- /dev/null
+++ b/modules/cloud-deploy/main.tf
@@ -0,0 +1,29 @@
+/**
+ * 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.
+ */
+
+locals {
+ compute_default_service_account = length(data.google_compute_default_service_account.default) > 0 ? data.google_compute_default_service_account.default[0].email : null
+ pipeline_type = "serial"
+ validated_automations = {
+ for k, v in var.automations :
+ k => v if v != null && (try(length(v.promote_release_rule), 0) > 0 || try(length(v.advance_rollout_rule), 0) > 0 || try(length(v.repair_rollout_rule), 0) > 0 || try(length(v.timed_promote_release_rule), 0) > 0)
+ }
+}
+
+data "google_compute_default_service_account" "default" {
+ count = alltrue([for k, v in var.automations : v.service_account != null]) ? 0 : 1
+ project = var.project_id
+}
\ No newline at end of file
diff --git a/modules/cloud-deploy/outputs.tf b/modules/cloud-deploy/outputs.tf
new file mode 100644
index 000000000..efa0bc2a3
--- /dev/null
+++ b/modules/cloud-deploy/outputs.tf
@@ -0,0 +1,37 @@
+
+/**
+ * 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.
+ */
+
+output "automation_ids" {
+ description = "Automation ids."
+ value = values(google_clouddeploy_automation.default)[*].id
+}
+
+output "deploy_policy_ids" {
+ description = "Deploy Policy ids."
+ value = values(google_clouddeploy_deploy_policy.default)[*].id
+}
+
+output "pipeline_id" {
+ description = "Delivery pipeline id."
+ value = google_clouddeploy_delivery_pipeline.default.id
+}
+
+output "target_ids" {
+ description = "Target ids."
+ value = values(google_clouddeploy_target.default)[*].id
+}
+
diff --git a/modules/cloud-deploy/target.tf b/modules/cloud-deploy/target.tf
new file mode 100644
index 000000000..227b49773
--- /dev/null
+++ b/modules/cloud-deploy/target.tf
@@ -0,0 +1,58 @@
+/**
+ * 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_clouddeploy_target" "default" {
+ for_each = {
+ for name, target in var.targets :
+ name => target if target.create_target == true
+ }
+
+ project = coalesce(each.value.project_id, var.project_id)
+ location = coalesce(each.value.region, var.region)
+ name = each.value.name
+ annotations = each.value.annotations
+ deploy_parameters = each.value.target_deploy_parameters
+ description = each.value.description
+ labels = each.value.labels
+ require_approval = each.value.require_approval
+
+ dynamic "execution_configs" {
+ for_each = each.value.execution_configs_usages == null && each.value.execution_configs_timeout == null ? [] : [""]
+
+ content {
+ execution_timeout = each.value.execution_configs_timeout
+ usages = each.value.execution_configs_usages
+ }
+ }
+
+ dynamic "multi_target" {
+ for_each = each.value.multi_target_target_ids == null ? [] : [""]
+
+ content {
+ target_ids = each.value.multi_target_target_ids
+ }
+ }
+
+ dynamic "run" {
+ for_each = each.value.cloud_run_configs == null ? [] : [""]
+
+ content {
+ location = "projects/${coalesce(each.value.cloud_run_configs.project_id, each.value.project_id, var.project_id)}/locations/${coalesce(each.value.cloud_run_configs.region, each.value.region, var.region)}"
+ }
+ }
+}
+
diff --git a/modules/cloud-deploy/variables-iam.tf b/modules/cloud-deploy/variables-iam.tf
new file mode 100644
index 000000000..7e861d6bc
--- /dev/null
+++ b/modules/cloud-deploy/variables-iam.tf
@@ -0,0 +1,59 @@
+/**
+ * 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.
+ */
+
+variable "iam" {
+ description = "IAM bindings in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
+ type = map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_bindings_additive" {
+ description = "Individual additive IAM bindings. Keys are arbitrary."
+ type = map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
diff --git a/modules/cloud-deploy/variables.tf b/modules/cloud-deploy/variables.tf
new file mode 100644
index 000000000..763af1ab8
--- /dev/null
+++ b/modules/cloud-deploy/variables.tf
@@ -0,0 +1,297 @@
+/**
+ * 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.
+ */
+
+variable "annotations" {
+ description = "Resource annotations."
+ type = map(string)
+ default = {}
+ nullable = false
+}
+
+variable "automations" {
+ description = "Configuration for automations associated with the deployment pipeline in a name => attributes format."
+ type = map(object({
+ project_id = optional(string, null)
+ region = optional(string, null)
+ annotations = optional(map(string))
+ description = optional(string, null)
+ labels = optional(map(string))
+ service_account = optional(string, null)
+ suspended = optional(bool, false)
+ advance_rollout_rule = optional(object({
+ id = optional(string, "advance-rollout")
+ source_phases = optional(list(string), null)
+ wait = optional(string, null)
+ }))
+ promote_release_rule = optional(object({
+ id = optional(string, "promote-release")
+ wait = optional(string, null)
+ destination_target_id = optional(string, null)
+ destination_phase = optional(string, null)
+ }))
+ repair_rollout_rule = optional(object({
+ id = optional(string, "repair-rollout")
+ phases = optional(list(string), null)
+ jobs = optional(list(string), null)
+ retry = optional(object({
+ attempts = optional(string, null)
+ wait = optional(string, null)
+ backoff_mode = optional(string, null)
+ }))
+ rollback = optional(object({
+ destination_phase = optional(string, null)
+ disable_rollback_if_rollout_pending = optional(bool, true)
+ }))
+ }))
+ timed_promote_release_rule = optional(object({
+ id = optional(string, "timed-promote-release")
+ destination_target_id = optional(string, null)
+ schedule = optional(string, null)
+ time_zone = optional(string, null)
+ destination_phase = optional(string, null)
+ }))
+ selector = optional(list(object({
+ id = optional(string, "*")
+ labels = optional(map(string), {})
+ })), [{ id = "*" }])
+ }))
+ default = {}
+ nullable = false
+ validation {
+ condition = alltrue([
+ for k, v in var.automations :
+ !(v.promote_release_rule == null && v.advance_rollout_rule == null && v.repair_rollout_rule == null && v.timed_promote_release_rule == null)
+ ])
+ error_message = < 0 || length(vv.one_time_windows) > 0) ? 0 : 1]) == 0
+ ])
+ error_message = <= 0 && p < 100 : p >= 0 && p < 100 && p > v.deployment_percentages[i - 1]
+ ])
+ ])
+ error_message = "For canary strategy, deployment percentages must be in ascending order and each percentage must be between 0 and 99."
+ }
+}
diff --git a/modules/cloud-deploy/versions.tf b/modules/cloud-deploy/versions.tf
new file mode 100644
index 000000000..0ebb55c97
--- /dev/null
+++ b/modules/cloud-deploy/versions.tf
@@ -0,0 +1,35 @@
+# 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
+#
+# https://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.
+
+# Fabric release: 40.1.0
+
+terraform {
+ required_version = ">= 1.11.4"
+ required_providers {
+ google = {
+ source = "hashicorp/google"
+ version = ">= 6.33.0, < 7.0.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 6.33.0, < 7.0.0" # tftest
+ }
+ }
+ provider_meta "google" {
+ module_name = "google-pso-tool/cloud-foundation-fabric/modules/cloud-deploy:40.1.0-tf"
+ }
+ provider_meta "google-beta" {
+ module_name = "google-pso-tool/cloud-foundation-fabric/modules/cloud-deploy:40.1.0-tf"
+ }
+}
diff --git a/modules/cloud-deploy/versions.tofu b/modules/cloud-deploy/versions.tofu
new file mode 100644
index 000000000..5d4839e8d
--- /dev/null
+++ b/modules/cloud-deploy/versions.tofu
@@ -0,0 +1,35 @@
+# 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
+#
+# https://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.
+
+# Fabric release: 40.1.0
+
+terraform {
+ required_version = ">= 1.9.0"
+ required_providers {
+ google = {
+ source = "hashicorp/google"
+ version = ">= 6.33.0, < 7.0.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 6.33.0, < 7.0.0" # tftest
+ }
+ }
+ provider_meta "google" {
+ module_name = "google-pso-tool/cloud-foundation-fabric/modules/cloud-deploy:40.1.0-tofu"
+ }
+ provider_meta "google-beta" {
+ module_name = "google-pso-tool/cloud-foundation-fabric/modules/cloud-deploy:40.1.0-tofu"
+ }
+}
diff --git a/modules/gke-cluster-standard/README.md b/modules/gke-cluster-standard/README.md
index 2c8b9820b..fca8d922b 100644
--- a/modules/gke-cluster-standard/README.md
+++ b/modules/gke-cluster-standard/README.md
@@ -488,8 +488,8 @@ module "cluster-1" {
|---|---|:---:|:---:|:---:|
| [location](variables.tf#L269) | Cluster zone or region. | string | ✓ | |
| [name](variables.tf#L384) | Cluster name. | string | ✓ | |
-| [project_id](variables.tf#L418) | Cluster project id. | string | ✓ | |
-| [vpc_config](variables.tf#L429) | VPC-level configuration. | object({…}) | ✓ | |
+| [project_id](variables.tf#L435) | Cluster project id. | string | ✓ | |
+| [vpc_config](variables.tf#L446) | VPC-level configuration. | object({…}) | ✓ | |
| [access_config](variables.tf#L17) | Control plane endpoint and nodes access configurations. | object({…}) | | {} |
| [backup_configs](variables.tf#L45) | Configuration for Backup for GKE. | object({…}) | | {} |
| [cluster_autoscaling](variables.tf#L67) | Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler. | object({…}) | | null |
@@ -507,7 +507,8 @@ module "cluster-1" {
| [monitoring_config](variables.tf#L330) | Monitoring configuration. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} |
| [node_config](variables.tf#L389) | Node-level configuration. | object({…}) | | {} |
| [node_locations](variables.tf#L411) | Zones in which the cluster's nodes are located. | list(string) | | [] |
-| [release_channel](variables.tf#L423) | Release channel for GKE upgrades. | string | | null |
+| [node_pool_auto_config](variables.tf#L418) | Node pool configs that apply to auto-provisioned node pools in autopilot clusters and node auto-provisioning-enabled clusters. | object({…}) | | {} |
+| [release_channel](variables.tf#L440) | Release channel for GKE upgrades. | string | | null |
## Outputs
diff --git a/modules/gke-cluster-standard/main.tf b/modules/gke-cluster-standard/main.tf
index ae2083bbb..51e53dd9e 100644
--- a/modules/gke-cluster-standard/main.tf
+++ b/modules/gke-cluster-standard/main.tf
@@ -88,6 +88,18 @@ resource "google_container_cluster" "cluster" {
}
}
}
+ node_pool_auto_config {
+ network_tags {
+ tags = var.node_pool_auto_config.network_tags
+ }
+ resource_manager_tags = var.node_pool_auto_config.resource_manager_tags
+ node_kubelet_config {
+ insecure_kubelet_readonly_port_enabled = upper(var.node_pool_auto_config.kubelet_readonly_port_enabled)
+ }
+ linux_node_config {
+ cgroup_mode = var.node_pool_auto_config.cgroup_mode
+ }
+ }
addons_config {
cloudrun_config {
disabled = !var.enable_addons.cloudrun
diff --git a/modules/gke-cluster-standard/variables.tf b/modules/gke-cluster-standard/variables.tf
index 6ced01a27..4d33815f2 100644
--- a/modules/gke-cluster-standard/variables.tf
+++ b/modules/gke-cluster-standard/variables.tf
@@ -415,6 +415,23 @@ variable "node_locations" {
nullable = false
}
+variable "node_pool_auto_config" {
+ description = "Node pool configs that apply to auto-provisioned node pools in autopilot clusters and node auto-provisioning-enabled clusters."
+ type = object({
+ cgroup_mode = optional(string)
+ kubelet_readonly_port_enabled = optional(bool, true)
+ network_tags = optional(list(string), [])
+ resource_manager_tags = optional(map(string), {})
+ })
+ default = {}
+ nullable = false
+ validation {
+ condition = contains(["CGROUPMODE_UNSPECIFIED", "CGROUPMODE_V1", "CGROUPMODE_V2"],
+ coalesce(var.node_pool_auto_config.cgroup_mode, "CGROUPMODE_UNSPECIFIED"))
+ error_message = "node_pool_auto_config.cgroup_mode must be CGROUPMODE_UNSPECIFIED, CGROUPMODE_V1 or CGROUPMODE_V2"
+ }
+}
+
variable "project_id" {
description = "Cluster project id."
type = string
diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md
index 2968c54db..61ab2bd46 100644
--- a/modules/net-vpc/README.md
+++ b/modules/net-vpc/README.md
@@ -752,7 +752,7 @@ module "vpc" {
}
]
}
-# tftest modules=1 resources=6 inventory=ipv6_only.yaml e2e
+# tftest modules=1 resources=6 inventory=ipv6_only.yaml
```
## Variables
diff --git a/modules/net-vpc/subnets.tf b/modules/net-vpc/subnets.tf
index b66721cef..3eef14384 100644
--- a/modules/net-vpc/subnets.tf
+++ b/modules/net-vpc/subnets.tf
@@ -150,6 +150,7 @@ resource "google_compute_subnetwork" "subnetwork" {
ip_cidr_range = try(each.value.ipv6.ipv6_only, false) ? null : each.value.ip_cidr_range
allow_subnet_cidr_routes_overlap = each.value.allow_subnet_cidr_routes_overlap
description = (
+ # Set description to an empty string (eg "") to create subnet without a description.
each.value.description == null
? "Terraform-managed."
: each.value.description
@@ -201,9 +202,11 @@ resource "google_compute_subnetwork" "proxy_only" {
name = each.value.name
region = each.value.region
ip_cidr_range = each.value.ip_cidr_range
- description = coalesce(
- each.value.description,
- "Terraform-managed proxy-only subnet for Regional HTTPS, Internal HTTPS or Cross-Regional HTTPS Internal LB."
+ description = (
+ # Set description to an empty string (eg "") to create subnet without a description.
+ each.value.description == null
+ ? "Terraform-managed proxy-only subnet for Regional HTTPS, Internal HTTPS or Cross-Regional HTTPS Internal LB."
+ : each.value.description
)
purpose = each.value.global ? "GLOBAL_MANAGED_PROXY" : "REGIONAL_MANAGED_PROXY"
role = each.value.active ? "ACTIVE" : "BACKUP"
@@ -216,9 +219,11 @@ resource "google_compute_subnetwork" "private_nat" {
name = each.value.name
region = each.value.region
ip_cidr_range = each.value.ip_cidr_range
- description = coalesce(
- each.value.description,
- "Terraform-managed private NAT subnet."
+ description = (
+ # Set description to an empty string (eg "") to create subnet without a description.
+ each.value.description == null
+ ? "Terraform-managed private NAT subnet."
+ : each.value.description
)
purpose = "PRIVATE_NAT"
}
@@ -230,9 +235,11 @@ resource "google_compute_subnetwork" "psc" {
name = each.value.name
region = each.value.region
ip_cidr_range = each.value.ip_cidr_range
- description = coalesce(
- each.value.description,
- "Terraform-managed subnet for Private Service Connect (PSC NAT)."
+ description = (
+ # Set description to an empty string (eg "") to create subnet without a description.
+ each.value.description == null
+ ? "Terraform-managed subnet for Private Service Connect (PSC NAT)."
+ : each.value.description
)
purpose = "PRIVATE_SERVICE_CONNECT"
}
diff --git a/modules/project-factory/automation.tf b/modules/project-factory/automation.tf
index 3c4b96152..e03d850ac 100644
--- a/modules/project-factory/automation.tf
+++ b/modules/project-factory/automation.tf
@@ -53,6 +53,7 @@ module "automation-bucket" {
prefix = each.value.prefix
name = "tf-state"
encryption_key = lookup(each.value, "encryption_key", null)
+ force_destroy = lookup(each.value, "force_destroy", null)
iam = {
for k, v in lookup(each.value, "iam", {}) : k => [
for vv in v : try(
diff --git a/modules/project-factory/factory-projects.tf b/modules/project-factory/factory-projects.tf
index eb0996f39..0df5b260a 100644
--- a/modules/project-factory/factory-projects.tf
+++ b/modules/project-factory/factory-projects.tf
@@ -73,6 +73,7 @@ locals {
name = name
description = lookup(opts, "description", "Terraform-managed.")
encryption_key = lookup(opts, "encryption_key", null)
+ force_destroy = lookup(opts, "force_destroy", null)
iam = lookup(opts, "iam", {})
iam_bindings = lookup(opts, "iam_bindings", {})
iam_bindings_additive = lookup(opts, "iam_bindings_additive", {})
diff --git a/modules/project-factory/main.tf b/modules/project-factory/main.tf
index 71022959e..f9e2e9ef7 100644
--- a/modules/project-factory/main.tf
+++ b/modules/project-factory/main.tf
@@ -325,6 +325,7 @@ module "buckets" {
prefix = each.value.prefix
name = "${each.value.project_name}-${each.value.name}"
encryption_key = each.value.encryption_key
+ force_destroy = each.value.force_destroy
iam = {
for k, v in each.value.iam : k => [
for vv in v : try(
diff --git a/modules/project-factory/schemas/project.schema.json b/modules/project-factory/schemas/project.schema.json
index e2c4620c3..15ab12e98 100644
--- a/modules/project-factory/schemas/project.schema.json
+++ b/modules/project-factory/schemas/project.schema.json
@@ -459,6 +459,9 @@
"iam_bindings_additive": {
"$ref": "#/$defs/iam_bindings_additive"
},
+ "force_destroy": {
+ "type": "boolean"
+ },
"labels": {
"type": "object",
"additionalProperties": {
diff --git a/modules/project-factory/schemas/project.schema.md b/modules/project-factory/schemas/project.schema.md
index cdd081ef4..0be198251 100644
--- a/modules/project-factory/schemas/project.schema.md
+++ b/modules/project-factory/schemas/project.schema.md
@@ -136,6 +136,7 @@
- **iam**: *reference([iam](#refs-iam))*
- **iam_bindings**: *reference([iam_bindings](#refs-iam_bindings))*
- **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))*
+ - **force_destroy**: *boolean*
- **labels**: *object*
*additional properties: String*
- **location**: *string*
diff --git a/tests/examples_e2e/setup_module/main.tf b/tests/examples_e2e/setup_module/main.tf
index d5d6470b4..1551c5af7 100644
--- a/tests/examples_e2e/setup_module/main.tf
+++ b/tests/examples_e2e/setup_module/main.tf
@@ -83,11 +83,12 @@ resource "google_project_service" "project_service" {
}
resource "google_storage_bucket" "bucket" {
- location = var.region
- name = "${local.prefix}-bucket"
- project = google_project.project.project_id
- force_destroy = true
- depends_on = [google_project_service.project_service]
+ location = var.region
+ name = "${local.prefix}-bucket"
+ project = google_project.project.project_id
+ force_destroy = true
+ uniform_bucket_level_access = true
+ depends_on = [google_project_service.project_service]
}
resource "google_compute_network" "network" {