diff --git a/fast/stages/2-project-factory/README.md b/fast/stages/2-project-factory/README.md
index cc72ad40a..a9f3e214e 100644
--- a/fast/stages/2-project-factory/README.md
+++ b/fast/stages/2-project-factory/README.md
@@ -11,6 +11,7 @@
- [Factory configuration](#factory-configuration)
- [Stage provider and Terraform variables](#stage-provider-and-terraform-variables)
- [Managing folders and projects](#managing-folders-and-projects)
+ - [Project defaults and overrides](#project-defaults-and-overrides)
- [Folder and hierarchy management](#folder-and-hierarchy-management)
- [Folder parent-child relationship and variable substitutions](#folder-parent-child-relationship-and-variable-substitutions)
- [Project Creation](#project-creation)
@@ -55,7 +56,7 @@ The bootstrap-specific setup is reproduced here to aid using it as a starting po
#### Automation resources
-The default design uses two service accounts (read-write and read-only) and a Cloud Storage folder in a pre-existing bucket, to enable this stage for Infrastructure as Code.
+The default design uses two service accounts (read-write and read-only) and a Cloud Storage folder in a pre-existing bucket, to enable this stage for Infrastructure as Code. This is an example snippet that shows how to configure the org setup stage IaC project.
```yaml
# data/projects/core/iac-0.yaml
@@ -208,6 +209,12 @@ terraform apply
The YAML data files are self-explanatory and the included [schema files](./schemas/) provide a reliable framework to allow editing the sample data, or starting from scratch to implement a different pattern. This section lists some general considerations on how folder and project files work to help getting up to speed with operations.
+### Project defaults and overrides
+
+The underlying module supports a way of defining sets of values that can be used as defaults of overrides for specific project attributes. This stage supports the same, and allows setting defaults and overrides either via Terraform variables, or via a dedicated YAML defaults file.
+
+An example defaults file is provided in the `data` folder, and the relevant schema (or the corresponding variable type) supports the full interface provided in the underlying module. Defaults from Terraform variables and the YAML file are merged, with the caveat that Where the same attribute (for example `billing_account`) is defined in both, the file takes precedence.
+
### Folder and hierarchy management
The project factory manages its folder hierarchy via a filesystem tree, rooted in the path defined via the `factories_config.folders` variable.
@@ -353,6 +360,7 @@ automation:
| [main.tf](./main.tf) | Project factory. | project-factory | |
| [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object |
| [variables-fast.tf](./variables-fast.tf) | None | | |
+| [variables-projects.tf](./variables-projects.tf) | None | | |
| [variables.tf](./variables.tf) | Module variables. | | |
## Variables
@@ -364,10 +372,10 @@ automation:
| [prefix](variables-fast.tf#L92) | Prefix used for resources that need unique names. Use a maximum of 9 chars for organizations, and 11 chars for tenants. | string | ✓ | | 0-org-setup |
| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | |
| [custom_roles](variables-fast.tf#L34) | Custom roles defined at the org level, in key => id format. | map(string) | | {} | 0-org-setup |
-| [data_defaults](variables.tf#L36) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} | |
-| [data_merges](variables.tf#L108) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | |
-| [data_overrides](variables.tf#L127) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | |
-| [factories_config](variables.tf#L173) | Path to folder with YAML resource description data files. | object({…}) | | {} | |
+| [data_defaults](variables-projects.tf#L17) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} | |
+| [data_merges](variables-projects.tf#L89) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | |
+| [data_overrides](variables-projects.tf#L108) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | |
+| [factories_config](variables.tf#L36) | Path to folder with YAML resource description data files. | object({…}) | | {} | |
| [folder_ids](variables-fast.tf#L42) | Folders created in the bootstrap stage. | map(string) | | {} | 0-org-setup |
| [host_project_ids](variables-fast.tf#L58) | Host project for the shared VPC. | map(string) | | {} | 2-networking |
| [iam_principals](variables-fast.tf#L50) | IAM-format principals. | map(string) | | {} | 0-org-setup |
@@ -376,7 +384,7 @@ automation:
| [perimeters](variables-fast.tf#L84) | Optional VPC-SC perimeter ids. | map(string) | | {} | 1-vpcsc |
| [project_ids](variables-fast.tf#L102) | Projects created in the bootstrap stage. | map(string) | | {} | 0-org-setup |
| [service_accounts](variables-fast.tf#L110) | Service accounts created in the bootstrap stage. | map(string) | | {} | 0-org-setup |
-| [stage_name](variables.tf#L193) | FAST stage name. Used to separate output files across different factories. | string | | "2-project-factory" | |
+| [stage_name](variables.tf#L57) | FAST stage name. Used to separate output files across different factories. | string | | "2-project-factory" | |
| [subnet_self_links](variables-fast.tf#L118) | Shared VPC subnet IDs. | map(map(string)) | | {} | 2-networking |
| [tag_values](variables-fast.tf#L126) | FAST-managed resource manager tag values. | map(string) | | {} | 0-org-setup |
diff --git a/fast/stages/2-project-factory/data/defaults.yaml b/fast/stages/2-project-factory/data/defaults.yaml
new file mode 100644
index 000000000..542f75161
--- /dev/null
+++ b/fast/stages/2-project-factory/data/defaults.yaml
@@ -0,0 +1,33 @@
+# 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.
+
+# yaml-language-server: $schema=../schemas/defaults.schema.json
+
+# environment-specific project defaults, merges and overrides can be defined here
+
+projects:
+ # defaults:
+ # storage_location: europe-west8
+ merges:
+ services:
+ - logging.googleapis.com
+ - monitoring.googleapis.com
+ # overrides:
+ # prefix: tf-playground
+
+# environment-specific static contexts can be defined here
+
+# context:
+# iam_principals:
+# foo: group:foo@example.com
\ No newline at end of file
diff --git a/fast/stages/2-project-factory/main.tf b/fast/stages/2-project-factory/main.tf
index 941103f56..ccb761fd9 100644
--- a/fast/stages/2-project-factory/main.tf
+++ b/fast/stages/2-project-factory/main.tf
@@ -17,6 +17,38 @@
# tfdoc:file:description Project factory.
locals {
+ _defaults = yamldecode(file(pathexpand(var.factories_config.defaults)))
+ context = merge(var.context, lookup(local._defaults, "context", {}))
+ fast_defaults = {
+ billing_account = coalesce(
+ var.data_defaults.billing_account,
+ var.billing_account.id
+ )
+ prefix = coalesce(
+ var.data_defaults.prefix, var.prefix
+ )
+ storage_location = coalesce(
+ var.data_defaults.storage_location, var.locations.storage
+ )
+ }
+ project_defaults = {
+ defaults = {
+ for k, v in var.data_defaults : k => try(
+ local._defaults.projects.defaults[k],
+ lookup(local.fast_defaults, k, v)
+ )
+ }
+ merges = {
+ for k, v in var.data_merges : k => try(
+ local._defaults.projects.merges[k], v
+ )
+ }
+ overrides = {
+ for k, v in var.data_overrides : k => try(
+ local._defaults.projects.overrides[k], v
+ )
+ }
+ }
subnet_self_links = flatten([
for net, subnets in var.subnet_self_links : [
for subnet_name, subnet_link in subnets : {
@@ -34,55 +66,29 @@ module "factory" {
subnet_self_links = {
for v in local.subnet_self_links : v.key => v.link
}
- }, var.context.condition_vars)
- custom_roles = merge(
- var.custom_roles, var.context.custom_roles
- )
- folder_ids = merge(
- var.folder_ids, var.context.folder_ids
- )
+ }, local.context.condition_vars)
+ custom_roles = merge(var.custom_roles, local.context.custom_roles)
+ folder_ids = merge(var.folder_ids, local.context.folder_ids)
iam_principals = merge(
var.iam_principals,
{
for k, v in var.service_accounts :
k => "serviceAccount:${v}" if v != null
},
- var.context.iam_principals
+ local.context.iam_principals
)
- kms_keys = merge(
- var.kms_keys, var.context.kms_keys
- )
- locations = merge(
- var.locations, var.context.locations
- )
- notification_channels = var.context.notification_channels
+ kms_keys = merge(var.kms_keys, local.context.kms_keys)
+ locations = merge(var.locations, local.context.locations)
+ notification_channels = local.context.notification_channels
project_ids = merge(
- var.project_ids, var.host_project_ids, var.context.project_ids
- )
- tag_values = merge(
- var.tag_values, var.context.tag_values
- )
- vpc_sc_perimeters = merge(
- var.perimeters, var.context.vpc_sc_perimeters
+ var.project_ids, var.host_project_ids, local.context.project_ids
)
+ tag_values = merge(var.tag_values, local.context.tag_values)
+ vpc_sc_perimeters = merge(var.perimeters, local.context.vpc_sc_perimeters)
}
- data_defaults = merge(var.data_defaults, {
- billing_account = coalesce(
- var.data_defaults.billing_account, var.billing_account.id
- )
- prefix = coalesce(var.data_defaults.prefix, var.prefix)
- storage_location = coalesce(
- var.data_defaults.storage_location, var.locations.storage
- )
- })
- data_merges = merge(var.data_merges, {
- services = length(var.data_merges.services) > 0 ? var.data_merges.services : [
- "logging.googleapis.com",
- "monitoring.googleapis.com"
- ]
- }
- )
- data_overrides = var.data_overrides
+ data_defaults = local.project_defaults.defaults
+ data_merges = local.project_defaults.merges
+ data_overrides = local.project_defaults.overrides
factories_config = merge(var.factories_config, {
budgets = {
billing_account_id = try(
diff --git a/fast/stages/2-project-factory/schemas/defaults.schema.json b/fast/stages/2-project-factory/schemas/defaults.schema.json
new file mode 100644
index 000000000..b455425fc
--- /dev/null
+++ b/fast/stages/2-project-factory/schemas/defaults.schema.json
@@ -0,0 +1,700 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Bootstrap Defaults",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "projects": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "defaults": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "billing_account": {
+ "type": "string"
+ },
+ "bucket": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "force_destroy": {
+ "type": "boolean"
+ }
+ }
+ },
+ "contacts": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-z0-9_-]+$": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "deletion_policy": {
+ "type": "string",
+ "enum": [
+ "PREVENT",
+ "DELETE",
+ "ABANDON"
+ ]
+ },
+ "labels": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-z0-9_-]+$": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "logging_data_access": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "ADMIN_READ": {
+ "type": "object",
+ "properties": {
+ "exempted_members": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "DATA_READ": {
+ "type": "object",
+ "properties": {
+ "exempted_members": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "DATA_WRITE": {
+ "type": "object",
+ "properties": {
+ "exempted_members": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "metric_scopes": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "parent": {
+ "type": "string"
+ },
+ "prefix": {
+ "type": "string"
+ },
+ "project_reuse": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "use_data_source": {
+ "type": "boolean",
+ "default": true
+ },
+ "attributes": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "number": {
+ "type": "number"
+ },
+ "services_enabled": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "name",
+ "number"
+ ]
+ }
+ }
+ },
+ "service_accounts": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "display_name": {
+ "type": "string",
+ "default": "Terraform-managed."
+ },
+ "iam_self_roles": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "service_encryption_key_ids": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-z0-9_-]+$": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "services": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "shared_vpc_service_config": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "host_project": {
+ "type": "string"
+ },
+ "iam_bindings_additive": {
+ "$ref": "#/$defs/iam_bindings_additive"
+ },
+ "network_users": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "service_agent_iam": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "service_agent_subnet_iam": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "service_iam_grants": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "network_subnet_users": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "required": [
+ "host_project"
+ ]
+ },
+ "storage_location": {
+ "type": "string"
+ },
+ "tag_bindings": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "universe": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "prefix"
+ ],
+ "properties": {
+ "prefix": {
+ "type": "string"
+ },
+ "unavailable_service_identities": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "vpc_sc": {
+ "type": "object",
+ "properties": {
+ "perimeter_name": {
+ "type": "string"
+ },
+ "is_dry_run": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ "required": [
+ "perimeter_name"
+ ]
+ }
+ }
+ },
+ "merges": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "contacts": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-z0-9_-]+$": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "labels": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-z0-9_-]+$": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "metric_scopes": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "service_encryption_key_ids": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-z0-9_-]+$": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "service_accounts": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "display_name": {
+ "type": "string",
+ "default": "Terraform-managed."
+ },
+ "iam_self_roles": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "services": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "overrides": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "billing_account": {
+ "type": "string"
+ },
+ "bucket": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "force_destroy": {
+ "type": "boolean"
+ }
+ }
+ },
+ "contacts": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-z0-9_-]+$": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "deletion_policy": {
+ "type": "string",
+ "enum": [
+ "PREVENT",
+ "DELETE",
+ "ABANDON"
+ ]
+ },
+ "logging_data_access": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "ADMIN_READ": {
+ "type": "object",
+ "properties": {
+ "exempted_members": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "DATA_READ": {
+ "type": "object",
+ "properties": {
+ "exempted_members": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "DATA_WRITE": {
+ "type": "object",
+ "properties": {
+ "exempted_members": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "parent": {
+ "type": "string"
+ },
+ "prefix": {
+ "type": "string"
+ },
+ "service_accounts": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "display_name": {
+ "type": "string",
+ "default": "Terraform-managed."
+ },
+ "iam_self_roles": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "service_encryption_key_ids": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-z0-9_-]+$": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "services": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "storage_location": {
+ "type": "string"
+ },
+ "tag_bindings": {
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "universe": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "prefix"
+ ],
+ "properties": {
+ "prefix": {
+ "type": "string"
+ },
+ "unavailable_service_identities": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "vpc_sc": {
+ "type": "object",
+ "properties": {
+ "perimeter_name": {
+ "type": "string"
+ },
+ "is_dry_run": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ "required": [
+ "perimeter_name"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "context": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "iam_principals": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "output_files": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "local_path": {
+ "type": "string"
+ },
+ "storage_bucket": {
+ "type": "string"
+ },
+ "providers": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-z0-9][a-z0-9_-]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "bucket",
+ "service_account"
+ ],
+ "properties": {
+ "bucket": {
+ "type": "string"
+ },
+ "prefix": {
+ "type": "string"
+ },
+ "service_account": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "$defs": {
+ "iam": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^(?:roles/|\\$custom_roles:)": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:||\\$iam_principals:[a-z0-9_-]+)"
+ }
+ }
+ }
+ },
+ "iam_bindings": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-z0-9_-]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "members": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:[a-z0-9_-]+)"
+ }
+ },
+ "role": {
+ "type": "string",
+ "pattern": "^(?:roles/|\\$custom_roles:)"
+ },
+ "condition": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "expression",
+ "title"
+ ],
+ "properties": {
+ "expression": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "iam_bindings_additive": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-z0-9_-]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "member": {
+ "type": "string",
+ "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:[a-z0-9_-]+)"
+ },
+ "role": {
+ "type": "string",
+ "pattern": "^(?:roles/|\\$custom_roles:)"
+ },
+ "condition": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "expression",
+ "title"
+ ],
+ "properties": {
+ "expression": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "iam_by_principals": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:[a-z0-9_-]+)": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^(?:roles/|\\$custom_roles:)"
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/fast/stages/2-project-factory/variables-projects.tf b/fast/stages/2-project-factory/variables-projects.tf
new file mode 100644
index 000000000..815021b1f
--- /dev/null
+++ b/fast/stages/2-project-factory/variables-projects.tf
@@ -0,0 +1,153 @@
+/**
+ * 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 "data_defaults" {
+ description = "Optional default values used when corresponding project or folder data from files are missing."
+ type = object({
+ billing_account = optional(string)
+ bucket = optional(object({
+ force_destroy = optional(bool)
+ }), {})
+ contacts = optional(map(list(string)), {})
+ deletion_policy = optional(string)
+ factories_config = optional(object({
+ custom_roles = optional(string)
+ observability = optional(string)
+ org_policies = optional(string)
+ quotas = optional(string)
+ }), {})
+ labels = optional(map(string), {})
+ metric_scopes = optional(list(string), [])
+ parent = optional(string)
+ prefix = optional(string)
+ project_reuse = optional(object({
+ use_data_source = optional(bool, true)
+ attributes = optional(object({
+ name = string
+ number = number
+ services_enabled = optional(list(string), [])
+ }))
+ }))
+ service_encryption_key_ids = optional(map(list(string)), {})
+ services = optional(list(string), [])
+ shared_vpc_service_config = optional(object({
+ host_project = string
+ iam_bindings_additive = optional(map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ })), {})
+ network_users = optional(list(string), [])
+ service_agent_iam = optional(map(list(string)), {})
+ service_agent_subnet_iam = optional(map(list(string)), {})
+ service_iam_grants = optional(list(string), [])
+ network_subnet_users = optional(map(list(string)), {})
+ }))
+ storage_location = optional(string)
+ tag_bindings = optional(map(string), {})
+ # non-project resources
+ service_accounts = optional(map(object({
+ display_name = optional(string, "Terraform-managed.")
+ iam_self_roles = optional(list(string))
+ })), {})
+ universe = optional(object({
+ prefix = string
+ unavailable_service_identities = optional(list(string), [])
+ unavailable_services = optional(list(string), [])
+ }))
+ vpc_sc = optional(object({
+ perimeter_name = string
+ is_dry_run = optional(bool, false)
+ }))
+ logging_data_access = optional(map(object({
+ ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
+ DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
+ DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
+ })), {})
+ })
+ nullable = false
+ default = {}
+}
+
+variable "data_merges" {
+ description = "Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`."
+ type = object({
+ contacts = optional(map(list(string)), {})
+ labels = optional(map(string), {})
+ metric_scopes = optional(list(string), [])
+ service_encryption_key_ids = optional(map(list(string)), {})
+ services = optional(list(string), [])
+ tag_bindings = optional(map(string), {})
+ # non-project resources
+ service_accounts = optional(map(object({
+ display_name = optional(string, "Terraform-managed.")
+ iam_self_roles = optional(list(string))
+ })), {})
+ })
+ nullable = false
+ default = {}
+}
+
+variable "data_overrides" {
+ description = "Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`."
+ type = object({
+ # data overrides default to null to mark that they should not override
+ billing_account = optional(string)
+ bucket = optional(object({
+ force_destroy = optional(bool)
+ }), {})
+ contacts = optional(map(list(string)))
+ deletion_policy = optional(string)
+ factories_config = optional(object({
+ custom_roles = optional(string)
+ observability = optional(string)
+ org_policies = optional(string)
+ quotas = optional(string)
+ }), {})
+ parent = optional(string)
+ prefix = optional(string)
+ service_encryption_key_ids = optional(map(list(string)))
+ storage_location = optional(string)
+ tag_bindings = optional(map(string))
+ services = optional(list(string))
+ # non-project resources
+ service_accounts = optional(map(object({
+ display_name = optional(string, "Terraform-managed.")
+ iam_self_roles = optional(list(string))
+ })))
+ universe = optional(object({
+ prefix = string
+ unavailable_service_identities = optional(list(string), [])
+ unavailable_services = optional(list(string), [])
+ }))
+ vpc_sc = optional(object({
+ perimeter_name = string
+ is_dry_run = optional(bool, false)
+ }))
+ logging_data_access = optional(map(object({
+ ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
+ DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
+ DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
+ })))
+ })
+ nullable = false
+ default = {}
+}
+
diff --git a/fast/stages/2-project-factory/variables.tf b/fast/stages/2-project-factory/variables.tf
index 2c86fed85..df0105199 100644
--- a/fast/stages/2-project-factory/variables.tf
+++ b/fast/stages/2-project-factory/variables.tf
@@ -33,146 +33,10 @@ variable "context" {
nullable = false
}
-variable "data_defaults" {
- description = "Optional default values used when corresponding project or folder data from files are missing."
- type = object({
- billing_account = optional(string)
- bucket = optional(object({
- force_destroy = optional(bool)
- }), {})
- contacts = optional(map(list(string)), {})
- deletion_policy = optional(string)
- factories_config = optional(object({
- custom_roles = optional(string)
- observability = optional(string)
- org_policies = optional(string)
- quotas = optional(string)
- }), {})
- labels = optional(map(string), {})
- metric_scopes = optional(list(string), [])
- parent = optional(string)
- prefix = optional(string)
- project_reuse = optional(object({
- use_data_source = optional(bool, true)
- attributes = optional(object({
- name = string
- number = number
- services_enabled = optional(list(string), [])
- }))
- }))
- service_encryption_key_ids = optional(map(list(string)), {})
- services = optional(list(string), [])
- shared_vpc_service_config = optional(object({
- host_project = string
- iam_bindings_additive = optional(map(object({
- member = string
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- })), {})
- network_users = optional(list(string), [])
- service_agent_iam = optional(map(list(string)), {})
- service_agent_subnet_iam = optional(map(list(string)), {})
- service_iam_grants = optional(list(string), [])
- network_subnet_users = optional(map(list(string)), {})
- }))
- storage_location = optional(string)
- tag_bindings = optional(map(string), {})
- # non-project resources
- service_accounts = optional(map(object({
- display_name = optional(string, "Terraform-managed.")
- iam_self_roles = optional(list(string))
- })), {})
- universe = optional(object({
- prefix = string
- unavailable_service_identities = optional(list(string), [])
- unavailable_services = optional(list(string), [])
- }))
- vpc_sc = optional(object({
- perimeter_name = string
- is_dry_run = optional(bool, false)
- }))
- logging_data_access = optional(map(object({
- ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
- DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
- DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
- })), {})
- })
- nullable = false
- default = {}
-}
-
-variable "data_merges" {
- description = "Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`."
- type = object({
- contacts = optional(map(list(string)), {})
- labels = optional(map(string), {})
- metric_scopes = optional(list(string), [])
- service_encryption_key_ids = optional(map(list(string)), {})
- services = optional(list(string), [])
- tag_bindings = optional(map(string), {})
- # non-project resources
- service_accounts = optional(map(object({
- display_name = optional(string, "Terraform-managed.")
- iam_self_roles = optional(list(string))
- })), {})
- })
- nullable = false
- default = {}
-}
-
-variable "data_overrides" {
- description = "Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`."
- type = object({
- # data overrides default to null to mark that they should not override
- billing_account = optional(string)
- bucket = optional(object({
- force_destroy = optional(bool)
- }), {})
- contacts = optional(map(list(string)))
- deletion_policy = optional(string)
- factories_config = optional(object({
- custom_roles = optional(string)
- observability = optional(string)
- org_policies = optional(string)
- quotas = optional(string)
- }), {})
- parent = optional(string)
- prefix = optional(string)
- service_encryption_key_ids = optional(map(list(string)))
- storage_location = optional(string)
- tag_bindings = optional(map(string))
- services = optional(list(string))
- # non-project resources
- service_accounts = optional(map(object({
- display_name = optional(string, "Terraform-managed.")
- iam_self_roles = optional(list(string))
- })))
- universe = optional(object({
- prefix = string
- unavailable_service_identities = optional(list(string), [])
- unavailable_services = optional(list(string), [])
- }))
- vpc_sc = optional(object({
- perimeter_name = string
- is_dry_run = optional(bool, false)
- }))
- logging_data_access = optional(map(object({
- ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
- DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
- DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
- })))
- })
- nullable = false
- default = {}
-}
-
variable "factories_config" {
description = "Path to folder with YAML resource description data files."
type = object({
+ defaults = optional(string, "data/defaults.yaml")
folders = optional(string, "data/folders")
projects = optional(string, "data/projects")
budgets = optional(object({
diff --git a/modules/project-factory/README.md b/modules/project-factory/README.md
index c0e417066..92b05cb72 100644
--- a/modules/project-factory/README.md
+++ b/modules/project-factory/README.md
@@ -692,11 +692,11 @@ service_accounts:
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [factories_config](variables.tf#L173) | Path to folder with YAML resource description data files. | object({…}) | ✓ | |
+| [factories_config](variables.tf#L171) | Path to folder with YAML resource description data files. | object({…}) | ✓ | |
| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} |
-| [data_defaults](variables.tf#L36) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} |
-| [data_merges](variables.tf#L108) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} |
-| [data_overrides](variables.tf#L127) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} |
+| [data_defaults](variables.tf#L36) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} |
+| [data_merges](variables.tf#L107) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} |
+| [data_overrides](variables.tf#L126) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} |
| [folders](variables-folders.tf#L17) | Folders data merged with factory data. | map(object({…})) | | {} |
| [notification_channels](variables-billing.tf#L17) | Notification channels used by budget alerts. | map(object({…})) | | {} |
| [projects](variables-projects.tf#L17) | Projects data merged with factory data. | map(object({…})) | | {} |
diff --git a/modules/project-factory/variables.tf b/modules/project-factory/variables.tf
index 7afdaf1f8..cad34971f 100644
--- a/modules/project-factory/variables.tf
+++ b/modules/project-factory/variables.tf
@@ -48,7 +48,12 @@ variable "data_defaults" {
org_policies = optional(string)
quotas = optional(string)
}), {})
- labels = optional(map(string), {})
+ labels = optional(map(string), {})
+ logging_data_access = optional(map(object({
+ ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
+ DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
+ DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
+ })), {})
metric_scopes = optional(list(string), [])
parent = optional(string)
prefix = optional(string)
@@ -60,6 +65,10 @@ variable "data_defaults" {
services_enabled = optional(list(string), [])
}))
}))
+ service_accounts = optional(map(object({
+ display_name = optional(string, "Terraform-managed.")
+ iam_self_roles = optional(list(string))
+ })), {})
service_encryption_key_ids = optional(map(list(string)), {})
services = optional(list(string), [])
shared_vpc_service_config = optional(object({
@@ -81,11 +90,6 @@ variable "data_defaults" {
}))
storage_location = optional(string)
tag_bindings = optional(map(string), {})
- # non-project resources
- service_accounts = optional(map(object({
- display_name = optional(string, "Terraform-managed.")
- iam_self_roles = optional(list(string))
- })), {})
universe = optional(object({
prefix = string
unavailable_service_identities = optional(list(string), [])
@@ -95,11 +99,6 @@ variable "data_defaults" {
perimeter_name = string
is_dry_run = optional(bool, false)
}))
- logging_data_access = optional(map(object({
- ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
- DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
- DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
- })), {})
})
nullable = false
default = {}
@@ -140,17 +139,21 @@ variable "data_overrides" {
org_policies = optional(string)
quotas = optional(string)
}), {})
- parent = optional(string)
- prefix = optional(string)
- service_encryption_key_ids = optional(map(list(string)))
- storage_location = optional(string)
- tag_bindings = optional(map(string))
- services = optional(list(string))
- # non-project resources
+ logging_data_access = optional(map(object({
+ ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
+ DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
+ DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
+ })))
+ parent = optional(string)
+ prefix = optional(string)
service_accounts = optional(map(object({
display_name = optional(string, "Terraform-managed.")
iam_self_roles = optional(list(string))
})))
+ service_encryption_key_ids = optional(map(list(string)))
+ services = optional(list(string))
+ storage_location = optional(string)
+ tag_bindings = optional(map(string))
universe = optional(object({
prefix = string
unavailable_service_identities = optional(list(string), [])
@@ -160,11 +163,6 @@ variable "data_overrides" {
perimeter_name = string
is_dry_run = optional(bool, false)
}))
- logging_data_access = optional(map(object({
- ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
- DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
- DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
- })))
})
nullable = false
default = {}