Add support for project templates to project factory module (#3317)

* add support for project templates to project factory module

* align project factory features in FAST org setup stage
This commit is contained in:
Ludovico Magnocavallo
2025-09-11 09:56:01 +02:00
committed by GitHub
parent 740ed270af
commit 78966f66c3
10 changed files with 211 additions and 109 deletions

View File

@@ -29,6 +29,7 @@ The code is meant to be executed by a high level service account with powerful p
- [Folder hierarchy](#folder-hierarchy)
- [Projects](#projects)
- [Factory-wide project defaults, merges, optionals](#factory-wide-project-defaults-merges-optionals)
- [Project templates](#project-templates)
- [Service accounts and buckets](#service-accounts-and-buckets)
- [Automation project and resources](#automation-project-and-resources)
- [Billing budgets](#billing-budgets)
@@ -74,6 +75,16 @@ In addition to the YAML-based project configurations, the factory accepts three
Some examples on where to use each of the three sets are [provided below](#example).
### Project templates
Project templates are project definitions that can be "inherited" and extended in YAML-based project configurations. Templates are YAML files which use the same schema as a project, but which don't directly trigger project creation by themselves.
When referenced in a project configuration file, a template attributes are used as the initial project definition, over which the project's own attributes are merged. The merge is shallow, so any attribute which is defined in the project configuration will take precedence and completely override the template's own definition.
For example, declaring `iam` or `org_policies` in the template and then doing the same in the project file will result in those two attributes in the template being ignored.
The set of available templates is defined via a dedicated path in the `factories_config` file, and then a template can be referenced from a project definition via the `project_template` YAML attribute.
### Service accounts and buckets
Service accounts and GCS buckets can be managed as part of each project's YAML configuration. This allows creation of default service accounts used for GCE instances, in firewall rules, or for application-level credentials without resorting to a separate Terraform configuration.
@@ -343,6 +354,7 @@ module "project-factory" {
gcp-devops = "group:gcp-devops@example.org"
}
tag_values = {
"context/gke" = "tagValues/654321"
"org-policies/drs-allow-all" = "tagValues/123456"
}
vpc_host_projects = {
@@ -376,8 +388,9 @@ module "project-factory" {
billing_account_id = var.billing_account_id
data = "data/budgets"
}
folders = "data/hierarchy"
projects = "data/projects"
folders = "data/hierarchy"
project_templates = "data/templates"
projects = "data/projects"
}
notification_channels = {
billing-default = {
@@ -389,7 +402,24 @@ module "project-factory" {
}
}
}
# tftest files=0,1,2,3,4,5,6,7,8,9 inventory=example.yaml
# tftest files=t0,0,1,2,3,4,5,6,7,8,9 inventory=example.yaml
```
A project template for GKE projects:
```yaml
services:
- compute.googleapis.com
- container.googleapis.com
- storage.googleapis.com
service_encryption_key_ids:
storage.googleapis.com:
- projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce
compute.googleapis.com:
- $kms_keys:compute-prod-ew1
tag_bindings:
context: $tag_values:context/gke
# tftest-file id=t0 path=data/templates/container/base.yaml schema=project.schema.json
```
A simple hierarchy of folders:
@@ -443,20 +473,14 @@ services:
More traditional project definitions via the project factory data:
```yaml
# inherit template attributes
project_template: container/base
# define project attributes (potentially overriding template)
billing_account: 012345-67890A-BCDEF0
labels:
app: app-0
team: team-a
parent: $folder_ids:team-a/app-0
service_encryption_key_ids:
storage.googleapis.com:
- projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce
compute.googleapis.com:
- $kms_keys:compute-prod-ew1
services:
- compute.googleapis.com
- container.googleapis.com
- storage.googleapis.com
iam_by_principals:
$iam_principals:service_accounts/dev-ta-app0-be/app-0-be:
- roles/storage.objectViewer
@@ -623,7 +647,7 @@ service_accounts:
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [factories_config](variables.tf#L173) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; folders &#61; optional&#40;string&#41;&#10; projects &#61; optional&#40;string&#41;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account_id &#61; string&#10; data &#61; string&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [factories_config](variables.tf#L173) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; folders &#61; optional&#40;string&#41;&#10; project_templates &#61; optional&#40;string&#41;&#10; projects &#61; optional&#40;string&#41;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account_id &#61; string&#10; data &#61; string&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [context](variables.tf#L17) | Context-specific interpolations. | <code title="object&#40;&#123;&#10; condition_vars &#61; optional&#40;map&#40;map&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; custom_roles &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; folder_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; kms_keys &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; locations &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; notification_channels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; project_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_values &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; vpc_host_projects &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; vpc_sc_perimeters &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [data_defaults](variables.tf#L36) | Optional default values used when corresponding project or folder data from files are missing. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; bucket &#61; optional&#40;object&#40;&#123;&#10; force_destroy &#61; optional&#40;bool&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; deletion_policy &#61; optional&#40;string&#41;&#10; factories_config &#61; optional&#40;object&#40;&#123;&#10; custom_roles &#61; optional&#40;string&#41;&#10; observability &#61; optional&#40;string&#41;&#10; org_policies &#61; optional&#40;string&#41;&#10; quotas &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; project_reuse &#61; optional&#40;object&#40;&#123;&#10; use_data_source &#61; optional&#40;bool, true&#41;&#10; attributes &#61; optional&#40;object&#40;&#123;&#10; name &#61; string&#10; number &#61; number&#10; services_enabled &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; shared_vpc_service_config &#61; optional&#40;object&#40;&#123;&#10; host_project &#61; string&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; network_users &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_agent_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_agent_subnet_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_iam_grants &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; network_subnet_users &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; storage_location &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; universe &#61; optional&#40;object&#40;&#123;&#10; prefix &#61; string&#10; unavailable_service_identities &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; unavailable_services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;object&#40;&#123;&#10; ADMIN_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_WRITE &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [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`. | <code title="object&#40;&#123;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |

View File

@@ -27,7 +27,9 @@ locals {
}
_projects_input = {
for k, v in merge(local._folder_projects_raw, local._projects_raw) :
basename(k) => v
basename(k) => merge(
try(local._templates_raw[v.project_template], {}), v
)
}
_projects_path = try(
pathexpand(var.factories_config.projects), null
@@ -36,6 +38,13 @@ locals {
for f in try(fileset(local._projects_path, "**/*.yaml"), []) :
trimsuffix(f, ".yaml") => yamldecode(file("${local._projects_path}/${f}"))
}
_templates_path = try(
pathexpand(var.factories_config.project_templates), null
)
_templates_raw = {
for f in try(fileset(local._templates_path, "**/*.yaml"), []) :
trimsuffix(f, ".yaml") => yamldecode(file("${local._templates_path}/${f}"))
}
ctx_project_ids = merge(local.ctx.project_ids, local.project_ids)
project_ids = {
for k, v in module.projects : k => v.project_id

View File

@@ -295,6 +295,9 @@
}
}
},
"project_template": {
"type": "string"
},
"service_accounts": {
"type": "object",
"additionalProperties": false,

View File

@@ -173,8 +173,9 @@ variable "data_overrides" {
variable "factories_config" {
description = "Path to folder with YAML resource description data files."
type = object({
folders = optional(string)
projects = optional(string)
folders = optional(string)
project_templates = optional(string)
projects = optional(string)
budgets = optional(object({
billing_account_id = string
data = string