Merge remote-tracking branch 'origin/master' into fast-dev

This commit is contained in:
Ludo
2025-01-06 20:50:11 +01:00
26 changed files with 1670 additions and 123 deletions

76
adrs/fast/addon-stages.md Normal file
View File

@@ -0,0 +1,76 @@
# Add-on stages
**authors:** [Ludo](https://github.com/ludoo), [Julio](https://github.com/juliocc)
**date:** Jan 5, 2025
## Status
Under implementation
## Context
Some optional features are too complex to directly embed in stages, as they would complicate the variable scope, need to be replicated across parallel stages, and introduce a lot of nested code for the benefit of a small subset of users.
This need has surfaced with the network security stage, which has taken the approach of spreading its resources across different stages (security, networking, and its own netsec) and resulted in very layered, complicated code which is not easy to deploy or maintain.
This is how the current netsec stage looks like from a resource perspective:
![image](https://github.com/user-attachments/assets/c9778cd8-8dd4-4f7c-b74b-c5d8ad7e7d30)
Furthermore, the stage also tries to do "too much", by behaving as a full stage and adopting a design that statically maps its resources onto all FAST environments and networks. This results in code that is really hard to adapt for real life use cases and impossible to keep forward compatible, as changes are extensive and spread out across three stages.
## Proposal
The proposal is to adopt a completely different approach, where large optional featuresets that we don't want to embed in our default stages should become "addon stages" that:
- reuse the IaC service account and bucket of the stage they interact with (e.g. networking for network security) to eliminate the need for custom IAM
- encapsulate all their resources in a single root module (the add-on stage)
- don't implement a static design but deal with the smallest possible unit of work, so that they can be cloned to implement different designs via tfvars
- provide optional FAST output variables for the main stages
This is what the network security stage looks like, once refactored according this proposal:
![image](https://github.com/user-attachments/assets/748b8b53-8df7-444e-9c71-f74e462a96f1)
With this approach
- there are no dependencies in resman except for a providers file that adds a prefix to the state backend and reuses networking service accounts and bucket
- the stage design does not deal with environments, but simply implements one complete set of NGFW resources in a given project (typically the net landing or shared environment project) and allows free configuration of zones and VPC attachments
- any relevant resource already defined in the "main" stages can be referred to via interpolation, by using the stages outputs as contexts
The code then becomes really simple to use, read and evolve since it's essentially decoupled from the main stages except for a handful of FAST interface variables.
Add-on stages should live in a separate folder from stages, and once we finally manage to reafctor networking into a simple stage, we go back to having a clear progression for main stages that should make it easier for users to get to grips with FAST's complexity. We might also want to scrap the plugins folder, and replace with a short document explaining the pattern.
```bash
fast
├── addons
├── 1-tenant-factory
└── 2-network-security
├── assets
│   └── templates
├── extras
│   ├── 0-cicd-github
│   └── 0-cicd-gitlab
├── plugins
│   └── 2-networking-serverless-connector
└── stages
├── 0-bootstrap
├── 1-resman
├── 1-vpcsc
├── 2-networking-a-simple
├── 2-networking-b-nva
├── 2-networking-c-separate-envs
├── 2-project-factory
├── 2-security
├── 3-gcve-dev
└── 3-gke-dev
```
## Decision
TBD
## Consequences
This approach also maps well to the current tenant factory stage, which essentially acts as a parallel resman stage reusing the same set of IaC resources.

View File

@@ -97,5 +97,5 @@ module "test" {
prefix = "prefix"
}
# tftest modules=9 resources=68
# tftest modules=9 resources=69
```

View File

@@ -84,5 +84,5 @@ module "test" {
parent = "folders/467898377"
}
}
# tftest modules=8 resources=67
# tftest modules=8 resources=68
```

View File

@@ -72,7 +72,7 @@ module "test" {
project_id = "test-dev"
}
}
# tftest modules=11 resources=89
# tftest modules=11 resources=90
```
<!-- BEGIN TFDOC -->
## Variables
@@ -128,5 +128,5 @@ module "test" {
project_id = "test-dev"
}
}
# tftest modules=13 resources=94 e2e
# tftest modules=13 resources=95 e2e
```

View File

@@ -124,4 +124,7 @@ resource "google_workbench_instance" "playground" {
depends_on = [
google_project_iam_member.shared_vpc,
]
lifecycle {
ignore_changes = [gce_setup[0].metadata["resource-url"]]
}
}

View File

@@ -108,16 +108,16 @@ Access the management console leveraging credentials bootstrapped via terraform
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [bindplane_secrets](variables.tf#L26) | Bindplane secrets. | <code title="object&#40;&#123;&#10; license &#61; string&#10; user &#61; optional&#40;string, &#34;admin&#34;&#41;&#10; password &#61; optional&#40;string, null&#41;&#10; sessions_secret &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [network_config](variables.tf#L57) | Shared VPC network configurations to use for GKE cluster. | <code title="object&#40;&#123;&#10; host_project &#61; optional&#40;string&#41;&#10; network_self_link &#61; string&#10; subnet_self_link &#61; string&#10; ip_range_gke_master &#61; string&#10; secondary_pod_range_name &#61; optional&#40;string, &#34;pods&#34;&#41;&#10; secondary_services_range_name &#61; optional&#40;string, &#34;services&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [prefix](variables.tf#L79) | Prefix used for resource names. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L98) | Project id, references existing project if `project_create` is null. | <code>string</code> | ✓ | |
| [region](variables.tf#L103) | GCP region. | <code>string</code> | ✓ | |
| [bindplane_config](variables.tf#L17) | Bindplane config. | <code title="object&#40;&#123;&#10; tls_certificate_cer &#61; optional&#40;string, null&#41;&#10; tls_certificate_key &#61; optional&#40;string, null&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [cluster_config](variables.tf#L36) | GKE cluster configuration. | <code title="object&#40;&#123;&#10; cluster_name &#61; optional&#40;string, &#34;bindplane-op&#34;&#41;&#10; master_authorized_ranges &#61; optional&#40;map&#40;string&#41;, &#123;&#10; rfc-1918-10-8 &#61; &#34;10.0.0.0&#47;8&#34;&#10; &#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [dns_config](variables.tf#L47) | DNS config. | <code title="object&#40;&#123;&#10; bootstrap_private_zone &#61; optional&#40;bool, false&#41;&#10; domain &#61; optional&#40;string, &#34;example.com&#34;&#41;&#10; hostname &#61; optional&#40;string, &#34;bindplane&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [postgresql_config](variables.tf#L69) | Cloud SQL postgresql config. | <code title="object&#40;&#123;&#10; availability_type &#61; optional&#40;string, &#34;REGIONAL&#34;&#41;&#10; database_version &#61; optional&#40;string, &#34;POSTGRES_13&#34;&#41;&#10; tier &#61; optional&#40;string, &#34;db-g1-small&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [project_create](variables.tf#L89) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [bindplane_secrets](variables.tf#L27) | Bindplane secrets. | <code title="object&#40;&#123;&#10; license &#61; string&#10; user &#61; optional&#40;string, &#34;admin&#34;&#41;&#10; password &#61; optional&#40;string, null&#41;&#10; sessions_secret &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [network_config](variables.tf#L58) | Shared VPC network configurations to use for GKE cluster. | <code title="object&#40;&#123;&#10; host_project &#61; optional&#40;string&#41;&#10; network_self_link &#61; string&#10; subnet_self_link &#61; string&#10; ip_range_gke_master &#61; string&#10; secondary_pod_range_name &#61; optional&#40;string, &#34;pods&#34;&#41;&#10; secondary_services_range_name &#61; optional&#40;string, &#34;services&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [prefix](variables.tf#L80) | Prefix used for resource names. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L99) | Project id, references existing project if `project_create` is null. | <code>string</code> | ✓ | |
| [region](variables.tf#L104) | GCP region. | <code>string</code> | ✓ | |
| [bindplane_config](variables.tf#L17) | Bindplane config. | <code title="object&#40;&#123;&#10; image_tag &#61; optional&#40;string, &#34;&#34;&#41;&#10; tls_certificate_cer &#61; optional&#40;string, null&#41;&#10; tls_certificate_key &#61; optional&#40;string, null&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [cluster_config](variables.tf#L37) | GKE cluster configuration. | <code title="object&#40;&#123;&#10; cluster_name &#61; optional&#40;string, &#34;bindplane-op&#34;&#41;&#10; master_authorized_ranges &#61; optional&#40;map&#40;string&#41;, &#123;&#10; rfc-1918-10-8 &#61; &#34;10.0.0.0&#47;8&#34;&#10; &#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [dns_config](variables.tf#L48) | DNS config. | <code title="object&#40;&#123;&#10; bootstrap_private_zone &#61; optional&#40;bool, false&#41;&#10; domain &#61; optional&#40;string, &#34;example.com&#34;&#41;&#10; hostname &#61; optional&#40;string, &#34;bindplane&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [postgresql_config](variables.tf#L70) | Cloud SQL postgresql config. | <code title="object&#40;&#123;&#10; availability_type &#61; optional&#40;string, &#34;REGIONAL&#34;&#41;&#10; database_version &#61; optional&#40;string, &#34;POSTGRES_13&#34;&#41;&#10; tier &#61; optional&#40;string, &#34;db-g1-small&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [project_create](variables.tf#L90) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
## Outputs

View File

@@ -34,6 +34,13 @@ config:
# of pods is recommended.
replicas: 2
image:
# -- Image name to be used. Defaults to `ghcr.io/observiq/bindplane-ee`.
name: ""
# Overrides the image tag whose default is {{ .Chart.AppVersion }}
# -- Image tag to use. Defaults to the version defined in the Chart's release.
tag: ${tag}
resources:
# Allow cpu bursting by leaving limits.cpu unset
requests:

View File

@@ -215,6 +215,7 @@ resource "helm_release" "bindplane" {
gcp_project_id = module.project.project_id
hostname = "${var.dns_config.hostname}.${var.dns_config.domain}"
address = "ingress"
tag = var.bindplane_config.image_tag
})]
depends_on = [

View File

@@ -17,6 +17,7 @@
variable "bindplane_config" {
description = "Bindplane config."
type = object({
image_tag = optional(string, "")
tls_certificate_cer = optional(string, null)
tls_certificate_key = optional(string, null)
})

View File

@@ -440,10 +440,10 @@ update_rules:
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [factories_config](variables.tf#L100) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; folders_data_path &#61; optional&#40;string&#41;&#10; projects_data_path &#61; optional&#40;string&#41;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account &#61; string&#10; budgets_data_path &#61; string&#10; notification_channels &#61; optional&#40;map&#40;any&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; context &#61; optional&#40;object&#40;&#123;&#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; 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; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#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; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#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; 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;, &#123; host_project &#61; null &#125;&#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; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;map&#40;list&#40;string&#41;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [data_merges](variables.tf#L54) | 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> |
| [data_overrides](variables.tf#L73) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; storage_location &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;&#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;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;map&#40;list&#40;string&#41;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [factories_config](variables.tf#L112) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account &#61; string&#10; budgets_data_path &#61; string&#10; notification_channels &#61; optional&#40;map&#40;any&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; context &#61; optional&#40;object&#40;&#123;&#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; 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; notification_channels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; folders_data_path &#61; optional&#40;string&#41;&#10; projects_data_path &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#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; 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; 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;, &#123; host_project &#61; null &#125;&#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; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;map&#40;list&#40;string&#41;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [data_merges](variables.tf#L60) | 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> |
| [data_overrides](variables.tf#L79) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#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; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; storage_location &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;&#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;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;map&#40;list&#40;string&#41;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2024 Google LLC
* 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.
@@ -58,6 +58,37 @@ locals {
try(v.contacts, null),
var.data_defaults.contacts
)
factories_config = {
custom_roles = try(
coalesce(
var.data_overrides.factories_config.custom_roles,
try(v.factories_config.custom_roles, null),
var.data_defaults.factories_config.custom_roles
),
null
)
observability = try(
coalesce(
var.data_overrides.factories_config.observability,
try(v.factories_config.observability, null),
var.data_defaults.factories_config.observability
),
null)
org_policies = try(
coalesce(
var.data_overrides.factories_config.org_policies,
try(v.factories_config.org_policies, null),
var.data_defaults.factories_config.org_policies
),
null)
quotas = try(
coalesce(
var.data_overrides.factories_config.quotas,
try(v.factories_config.quotas, null),
var.data_defaults.factories_config.quotas
),
null)
}
labels = coalesce(
try(v.labels, null),
var.data_defaults.labels

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2024 Google LLC
* 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.
@@ -41,6 +41,7 @@ module "projects" {
local.context.folder_ids, each.value.parent, each.value.parent
)
prefix = each.value.prefix
alerts = try(each.value.alerts, null)
auto_create_network = try(each.value.auto_create_network, false)
compute_metadata = try(each.value.compute_metadata, {})
# TODO: concat lists for each key
@@ -49,6 +50,15 @@ module "projects" {
)
default_service_account = try(each.value.default_service_account, "keep")
descriptive_name = try(each.value.descriptive_name, null)
factories_config = {
custom_roles = each.value.factories_config.custom_roles
observability = each.value.factories_config.observability
org_policies = each.value.factories_config.org_policies
quotas = each.value.factories_config.quotas
context = {
notification_channels = var.factories_config.context.notification_channels
}
}
iam = {
for k, v in lookup(each.value, "iam", {}) : k => [
for vv in v : try(
@@ -93,13 +103,16 @@ module "projects" {
each.value.labels, var.data_merges.labels
)
lien_reason = try(each.value.lien_reason, null)
log_scopes = try(each.value.log_scopes, null)
logging_data_access = try(each.value.logging_data_access, {})
logging_exclusions = try(each.value.logging_exclusions, {})
logging_metrics = try(each.value.logging_metrics, null)
logging_sinks = try(each.value.logging_sinks, {})
metric_scopes = distinct(concat(
each.value.metric_scopes, var.data_merges.metric_scopes
))
org_policies = each.value.org_policies
notification_channels = try(each.value.notification_channels, null)
org_policies = each.value.org_policies
service_encryption_key_ids = merge(
each.value.service_encryption_key_ids,
var.data_merges.service_encryption_key_ids

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2024 Google LLC
* 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.
@@ -17,8 +17,14 @@
variable "data_defaults" {
description = "Optional default values used when corresponding project data from files are missing."
type = object({
billing_account = optional(string)
contacts = optional(map(list(string)), {})
billing_account = optional(string)
contacts = optional(map(list(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)
@@ -73,8 +79,14 @@ variable "data_merges" {
variable "data_overrides" {
description = "Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`."
type = object({
billing_account = optional(string)
contacts = optional(map(list(string)))
billing_account = optional(string)
contacts = optional(map(list(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)))
@@ -100,8 +112,6 @@ variable "data_overrides" {
variable "factories_config" {
description = "Path to folder with YAML resource description data files."
type = object({
folders_data_path = optional(string)
projects_data_path = optional(string)
budgets = optional(object({
billing_account = string
budgets_data_path = string
@@ -110,11 +120,14 @@ variable "factories_config" {
}))
context = optional(object({
# TODO: add KMS keys
folder_ids = optional(map(string), {})
iam_principals = optional(map(string), {})
tag_values = optional(map(string), {})
vpc_host_projects = optional(map(string), {})
folder_ids = optional(map(string), {})
iam_principals = optional(map(string), {})
tag_values = optional(map(string), {})
vpc_host_projects = optional(map(string), {})
notification_channels = optional(map(string), {})
}), {})
folders_data_path = optional(string)
projects_data_path = optional(string)
})
nullable = false
}

File diff suppressed because one or more lines are too long

285
modules/project/alerts.tf Normal file
View File

@@ -0,0 +1,285 @@
/**
* 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 {
_alerts_factory_data_raw = merge([
for k in local.observability_factory_data_raw :
lookup(k, "alerts", {})
]...)
_alerts_factory_data = {
for k, v in local._alerts_factory_data_raw :
k => {
combiner = v.combiner
display_name = try(v.display_name, null)
enabled = try(v.enabled, null)
notification_channels = try(v.notification_channels, [])
severity = try(v.severity, null)
user_labels = try(v.user_labels, null)
alert_strategy = !can(v.alert_strategy) ? null : {
auto_close = try(v.alert_strategy.auto_close, null)
notification_prompts = try(v.alert_strategy.notification_prompts, null)
notification_rate_limit = !can(v.alert_strategy.notification_rate_limit) ? null : {
period = try(v.alert_strategy.notification_rate_limit.period, null)
}
notification_channel_strategy = !can(v.alert_strategy.notification_channel_strategy) ? null : {
notification_channel_names = try(v.alert_strategy.notification_channel_strategy.notification_channel_names, null)
renotify_interval = try(v.alert_strategy.notification_channel_strategy.renotify_interval, null)
}
}
conditions = !can(v.conditions) ? null : [
for c in v.conditions : {
display_name = c.display_name
condition_absent = !can(c.condition_absent) ? null : {
duration = c.condition_absent.duration
filter = try(c.condition_absent.filter, null)
aggregations = !can(c.condition_absent.aggregations) ? null : {
per_series_aligner = try(c.condition_absent.aggregations.per_series_aligner, null)
group_by_fields = try(c.condition_absent.aggregations.group_by_fields, null)
cross_series_reducer = try(c.condition_absent.aggregations.cross_series_reducer, null)
alignment_period = try(c.condition_absent.aggregations.alignment_period, null)
}
trigger = !can(c.condition_absent.trigger) ? null : {
count = try(c.condition_absent.trigger.count, null)
percent = try(c.condition_absent.trigger.percent, null)
}
}
condition_matched_log = !can(c.condition_matched_log) ? null : {
filter = c.condition_matched_log.filter
label_extractors = try(c.condition_matched_log.label_extractors, null)
}
condition_monitoring_query_language = !can(c.condition_monitoring_query_language) ? null : {
duration = c.condition_monitoring_query_language.duration
query = c.condition_monitoring_query_language.query
evaluation_missing_data = try(c.condition_monitoring_query_language.evaluation_missing_data, null)
trigger = !can(c.condition_monitoring_query_language.trigger) ? null : {
count = try(c.condition_monitoring_query_language.trigger.count, null)
percent = try(c.condition_monitoring_query_language.trigger.percent, null)
}
}
condition_prometheus_query_language = !can(c.condition_prometheus_query_language) ? null : {
query = c.condition_prometheus_query_language.query
alert_rule = try(c.condition_prometheus_query_language.alert_rule, null)
disable_metric_validation = try(c.condition_prometheus_query_language.disable_metric_validation, null)
duration = try(c.condition_prometheus_query_language.duration, null)
evaluation_interval = try(c.condition_prometheus_query_language.evaluation_interval, null)
labels = try(c.condition_prometheus_query_language.labels, null)
rule_group = try(c.condition_prometheus_query_language.rule_group, null)
}
condition_threshold = !can(c.condition_threshold) ? null : {
comparison = c.condition_threshold.comparison
duration = c.condition_threshold.duration
denominator_filter = try(c.condition_threshold.denominator_filter, null)
evaluation_missing_data = try(c.condition_threshold.evaluation_missing_data, null)
filter = try(c.condition_threshold.filter, null)
threshold_value = try(c.condition_threshold.threshold_value, null)
aggregations = !can(c.condition_threshold.aggregations) ? null : {
per_series_aligner = try(c.condition_threshold.aggregations.per_series_aligner, null)
group_by_fields = try(c.condition_threshold.aggregations.group_by_fields, null)
cross_series_reducer = try(c.condition_threshold.aggregations.cross_series_reducer, null)
alignment_period = try(c.condition_threshold.aggregations.alignment_period, null)
}
denominator_aggregations = !can(c.condition_threshold.denominator_aggregations) ? null : {
per_series_aligner = try(c.condition_threshold.denominator_aggregations.per_series_aligner, null)
group_by_fields = try(c.condition_threshold.denominator_aggregations.group_by_fields, null)
cross_series_reducer = try(c.condition_threshold.denominator_aggregations.cross_series_reducer, null)
alignment_period = try(c.condition_threshold.denominator_aggregations.alignment_period, null)
}
forecast_options = !can(c.condition_threshold.forecast_options) ? null : {
forecast_horizon = c.condition_threshold.forecast_options.forecast_horizon
}
trigger = !can(c.condition_threshold.trigger) ? null : {
count = try(c.condition_threshold.trigger.count, null)
percent = try(c.condition_threshold.trigger.percent, null)
}
}
}
]
documentation = !can(v.documentation) ? null : {
content = try(v.documentation.content, null)
mime_type = try(v.documentation.mime_type, null)
subject = try(v.documentation.subject, null)
links = !can(v.documentation.links) ? null : [
for l in v.documentation.link : {
display_name = try(l.display_name, null)
url = try(l.url, null)
}]
}
}
}
alerts = merge(local._alerts_factory_data, var.alerts)
}
resource "google_monitoring_alert_policy" "alerts" {
for_each = local.alerts
project = local.project.project_id
combiner = each.value.combiner
display_name = each.value.display_name
enabled = each.value.enabled
notification_channels = [
for x in each.value.notification_channels :
try(
# first try to get a channel created by this module
google_monitoring_notification_channel.channels[x].name,
# otherwise check the context
var.factories_config.context.notification_channels[x],
# if nothing else, use the provided channel as is
x
)
]
severity = each.value.severity
user_labels = each.value.user_labels
dynamic "alert_strategy" {
for_each = each.value.alert_strategy[*]
content {
auto_close = alert_strategy.value.auto_close
notification_prompts = alert_strategy.value.notification_prompts
dynamic "notification_channel_strategy" {
for_each = alert_strategy.value.notification_channel_strategy[*]
content {
notification_channel_names = notification_channel_strategy.value.notification_channel_names
renotify_interval = notification_channel_strategy.value.renotify_interval
}
}
dynamic "notification_rate_limit" {
for_each = alert_strategy.value.notification_rate_limit[*]
content {
period = notification_rate_limit.value.period
}
}
}
}
dynamic "conditions" {
for_each = each.value.conditions
content {
display_name = conditions.value.display_name
dynamic "condition_absent" {
for_each = conditions.value.condition_absent[*]
content {
duration = condition_absent.value.duration
filter = condition_absent.value.filter
dynamic "aggregations" {
for_each = condition_absent.value.aggregations[*]
content {
alignment_period = aggregations.value.alignment_period
cross_series_reducer = aggregations.value.cross_series_reducer
group_by_fields = aggregations.value.group_by_fields
per_series_aligner = aggregations.value.per_series_aligner
}
}
dynamic "trigger" {
for_each = condition_absent.value.trigger[*]
content {
count = trigger.value.count
percent = trigger.value.percent
}
}
}
}
dynamic "condition_matched_log" {
for_each = conditions.value.condition_matched_log[*]
content {
filter = condition_matched_log.value.filter
label_extractors = condition_matched_log.value.label_extractors
}
}
dynamic "condition_monitoring_query_language" {
for_each = conditions.value.condition_monitoring_query_language[*]
content {
duration = condition_monitoring_query_language.value.duration
query = condition_monitoring_query_language.value.query
evaluation_missing_data = condition_monitoring_query_language.value.evaluation_missing_data
trigger {
count = condition_monitoring_query_language.value.trigger.count
percent = condition_monitoring_query_language.value.trigger.percent
}
}
}
dynamic "condition_prometheus_query_language" {
for_each = conditions.value.condition_prometheus_query_language[*]
content {
query = condition_prometheus_query_language.value.query
disable_metric_validation = condition_prometheus_query_language.value.disable_metric_validation
duration = condition_prometheus_query_language.value.duration
evaluation_interval = condition_prometheus_query_language.value.evaluation_interval
labels = condition_prometheus_query_language.value.labels
rule_group = condition_prometheus_query_language.value.rule_group
alert_rule = condition_prometheus_query_language.value.alert_rule
}
}
dynamic "condition_threshold" {
for_each = conditions.value.condition_threshold[*]
content {
comparison = condition_threshold.value.comparison
duration = condition_threshold.value.duration
denominator_filter = condition_threshold.value.denominator_filter
evaluation_missing_data = condition_threshold.value.evaluation_missing_data
filter = condition_threshold.value.filter
threshold_value = condition_threshold.value.threshold_value
dynamic "aggregations" {
for_each = condition_threshold.value.aggregations[*]
content {
alignment_period = aggregations.value.alignment_period
cross_series_reducer = aggregations.value.cross_series_reducer
group_by_fields = aggregations.value.group_by_fields
per_series_aligner = aggregations.value.per_series_aligner
}
}
dynamic "denominator_aggregations" {
for_each = condition_threshold.value.denominator_aggregations[*]
content {
alignment_period = denominator_aggregations.value.alignment_period
group_by_fields = denominator_aggregations.value.group_by_fields
per_series_aligner = denominator_aggregations.value.per_series_aligner
}
}
dynamic "forecast_options" {
for_each = condition_threshold.value.forecast_options[*]
content {
forecast_horizon = forecast_options.value.forecast_horizon
}
}
dynamic "trigger" {
for_each = condition_threshold.value.trigger[*]
content {
count = trigger.value.count
percent = trigger.value.percent
}
}
}
}
}
}
dynamic "documentation" {
for_each = each.value.documentation[*]
content {
content = documentation.value.content
mime_type = documentation.value.mime_type
subject = documentation.value.subject
dynamic "links" {
for_each = documentation.value.links[*]
content {
display_name = links.value.display_name
url = links.value.url
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
/**
* 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 {
_logging_metrics_factory_data_raw = merge([
for k in local.observability_factory_data_raw :
lookup(k, "logging_metrics", {})
]...)
_logging_metrics_factory_data = {
for k, v in local._logging_metrics_factory_data_raw :
k => {
filter = v.filter
bucket_name = try(v.bucket_name, null)
description = try(v.description, null)
disabled = try(v.disabled, null)
label_extractors = try(v.label_extractors, null)
value_extractor = try(v.value_extractor, null)
bucket_options = !can(v.bucket_options) ? null : {
explicit_buckets = !can(v.bucket_options.explicit_buckets) ? null : {
bounds = v.bucket_options.explicit_buckets.bounds
}
exponential_buckets = !can(v.bucket_options.exponential_buckets) ? null : {
num_finite_buckets = v.bucket_options.exponential_buckets.num_finite_buckets
growth_factor = v.bucket_options.exponential_buckets.growth_factor
scale = v.bucket_options.exponential_buckets.scale
}
linear_buckets = !can(v.bucket_options.linear_buckets) ? null : {
num_finite_buckets = v.bucket_options.linear_buckets.num_finite_buckets
width = v.bucket_options.linear_buckets.width
offset = v.bucket_options.linear_buckets.offset
}
}
metric_descriptor = !can(v.metric_descriptor) ? null : {
metric_kind = v.metric_descriptor.metric_kind
value_type = v.metric_descriptor.value_type
display_name = try(v.metric_descriptor.display_name, null)
unit = try(v.metric_descriptor.unit, null)
labels = !can(v.metric_descriptor.labels) ? [] : [
for vv in v.metric_descriptor.labels :
{
key = vv.key
description = try(vv.description, null)
value_type = try(vv.value_type, null)
}
]
}
}
}
metrics = merge(local._logging_metrics_factory_data, var.logging_metrics)
}
resource "google_logging_metric" "metrics" {
for_each = local.metrics
project = local.project.project_id
name = each.key
filter = each.value.filter
description = each.value.description
disabled = each.value.disabled
bucket_name = each.value.bucket_name
value_extractor = each.value.value_extractor
label_extractors = each.value.label_extractors
dynamic "bucket_options" {
for_each = each.value.bucket_options[*]
content {
dynamic "explicit_buckets" {
for_each = bucket_options.value.explicit_buckets[*]
content {
bounds = explicit_buckets.value.bounds
}
}
dynamic "exponential_buckets" {
for_each = bucket_options.value.exponential_buckets[*]
content {
num_finite_buckets = exponential_buckets.value.num_finite_buckets
growth_factor = exponential_buckets.value.growth_factor
scale = exponential_buckets.value.scale
}
}
dynamic "linear_buckets" {
for_each = bucket_options.value.linear_buckets[*]
content {
num_finite_buckets = linear_buckets.value.num_finite_buckets
width = linear_buckets.value.width
offset = linear_buckets.value.offset
}
}
}
}
dynamic "metric_descriptor" {
for_each = each.value.metric_descriptor[*]
content {
metric_kind = metric_descriptor.value.metric_kind
value_type = metric_descriptor.value.value_type
display_name = metric_descriptor.value.display_name
unit = metric_descriptor.value.unit
dynamic "labels" {
for_each = metric_descriptor.value.labels
content {
key = labels.value.key
description = labels.value.description
value_type = labels.value.value_type
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2024 Google LLC
* 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.
@@ -18,6 +18,10 @@ locals {
descriptive_name = (
var.descriptive_name != null ? var.descriptive_name : "${local.prefix}${var.name}"
)
observability_factory_data_raw = [
for f in try(fileset(var.factories_config.observability, "*.yaml"), []) :
yamldecode(file("${var.factories_config.observability}/${f}"))
]
parent_type = var.parent == null ? null : split("/", var.parent)[0]
parent_id = var.parent == null ? null : split("/", var.parent)[1]
prefix = var.prefix == null ? "" : "${var.prefix}-"

View File

@@ -0,0 +1,60 @@
/**
* 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 {
_channels_factory_data_raw = merge([
for k in local.observability_factory_data_raw :
lookup(k, "notification_channels", {})
]...)
_channels_factory_data = {
for k, v in local._channels_factory_data_raw :
k => {
type = v.type
description = try(v.description, null)
display_name = try(v.display_name, null)
enabled = try(v.enabled, null)
labels = try(v.labels, null)
user_labels = try(v.user_labels, null)
sensitive_labels = !can(v.sensitive_labels) ? null : {
auth_token = try(v.sensitive_labels.auth_token, null)
password = try(v.sensitive_labels.password, null)
service_key = try(v.sensitive_labels.service_key, null)
}
}
}
channels = merge(local._channels_factory_data, var.notification_channels)
}
resource "google_monitoring_notification_channel" "channels" {
for_each = local.channels
project = local.project.project_id
type = each.value.type
description = each.value.description
display_name = each.value.display_name
enabled = each.value.enabled
labels = each.value.labels
user_labels = each.value.user_labels
dynamic "sensitive_labels" {
for_each = each.value.sensitive_labels[*]
content {
auth_token = sensitive_labels.value.auth_token
password = sensitive_labels.value.password
service_key = sensitive_labels.value.service_key
}
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2024 Google LLC
* 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.
@@ -14,6 +14,14 @@
* limitations under the License.
*/
output "alert_ids" {
description = "Monitoring alert IDs."
value = {
for k, v in google_monitoring_alert_policy.alerts :
k => v.id
}
}
output "custom_role_id" {
description = "Map of custom role IDs created in the project."
value = {
@@ -29,7 +37,6 @@ output "custom_roles" {
value = google_project_iam_custom_role.roles
}
output "default_service_accounts" {
description = "Emails of the default service accounts for this project."
value = {
@@ -85,6 +92,19 @@ output "network_tag_values" {
}
}
output "notification_channel_names" {
description = "Notification channel names."
value = {
for k, v in google_monitoring_notification_channel.channels :
k => v.name
}
}
output "notification_channels" {
description = "Full notification channel objects."
value = google_monitoring_notification_channel.channels
}
output "number" {
description = "Project number."
value = local.project.number

View File

@@ -0,0 +1,514 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Observability Schema",
"type": "object",
"additionalProperties": false,
"properties": {
"alerts": {
"$ref": "#/$defs/alerts"
},
"logging_metrics": {
"$ref": "#/$defs/logging_metrics"
},
"notification_channels": {
"$ref": "#/$defs/notification_channels"
}
},
"$defs": {
"alerts": {
"title": "Alerts",
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z0-9-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"combiner": {
"type": "string"
},
"display_name": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"notification_channels": {
"type": "array",
"items": {
"type": "string"
}
},
"severity": {
"type": "string"
},
"user_labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"alert_strategy": {
"type": "object",
"additionalProperties": false,
"properties": {
"auto_close": {
"type": "string"
},
"notification_prompts": {
"type": "string"
},
"notification_rate_limit": {
"type": "object",
"additionalProperties": false,
"properties": {
"period": {
"type": "string"
}
}
},
"notification_channel_strategy": {
"type": "object",
"additionalProperties": false,
"properties": {
"notification_channel_names": {
"type": "array",
"items": {
"type": "string"
}
},
"renotify_interval": {
"type": "string"
}
}
}
}
},
"conditions": {
"type": "array",
"items": {
"$ref": "#/$defs/condition"
}
},
"documentation": {
"type": "object",
"additionalProperties": false,
"properties": {
"content": {
"type": "string"
},
"mime_type": {
"type": "string"
},
"subject": {
"type": "string"
},
"links": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"display_name": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
}
}
}
},
"required": [
"combiner"
]
}
}
},
"logging_metrics": {
"title": "Logging Metrics",
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z0-9-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"filter": {
"type": "string"
},
"bucket_name": {
"type": "string"
},
"description": {
"type": "string"
},
"disabled": {
"type": "boolean"
},
"label_extractors": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"value_extractor": {
"type": "string"
},
"bucket_options": {
"type": "object",
"additionalProperties": false,
"properties": {
"explicit_buckets": {
"type": "object",
"additionalProperties": false,
"properties": {
"bounds": {
"type": "array",
"items": {
"type": "number"
}
}
}
},
"exponential_buckets": {
"type": "object",
"additionalProperties": false,
"properties": {
"num_finite_buckets": {
"type": "number"
},
"growth_factor": {
"type": "number"
},
"scale": {
"type": "number"
}
}
},
"linear_buckets": {
"type": "object",
"additionalProperties": false,
"properties": {
"num_finite_buckets": {
"type": "number"
},
"width": {
"type": "number"
},
"offset": {
"type": "number"
}
}
}
}
},
"metric_descriptor": {
"type": "object",
"additionalProperties": false,
"properties": {
"metric_kind": {
"type": "string"
},
"value_type": {
"type": "string"
},
"display_name": {
"type": "string"
},
"unit": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"key": {
"type": "string"
},
"description": {
"type": "string"
},
"value_type": {
"type": "string"
}
},
"required": [
"key"
]
}
}
},
"required": [
"metric_kind",
"value_type"
]
}
},
"required": [
"filter"
]
}
}
},
"notification_channels": {
"title": "Notification Channels",
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z0-9-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"type": "string"
},
"description": {
"type": "string"
},
"display_name": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"user_labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"sensitive_labels": {
"type": "object",
"additionalProperties": false,
"properties": {
"auth_token": {
"type": "string"
},
"password": {
"type": "string"
},
"service_key": {
"type": "string"
}
}
}
},
"required": [
"type"
]
}
}
},
"condition": {
"type": "object",
"additionalProperties": false,
"properties": {
"display_name": {
"type": "string"
},
"condition_absent": {
"$ref": "#/$defs/absent_condition"
},
"condition_matched_log": {
"$ref": "#/$defs/matched_log_condition"
},
"condition_monitoring_query_language": {
"$ref": "#/$defs/monitoring_query_condition"
},
"condition_prometheus_query_language": {
"$ref": "#/$defs/prometheus_query_condition"
},
"condition_threshold": {
"$ref": "#/$defs/threshold_condition"
}
},
"required": [
"display_name"
]
},
"absent_condition": {
"type": "object",
"additionalProperties": false,
"properties": {
"duration": {
"type": "string"
},
"filter": {
"type": "string"
},
"aggregations": {
"$ref": "#/$defs/aggregations"
},
"trigger": {
"$ref": "#/$defs/trigger"
}
},
"required": [
"duration"
]
},
"matched_log_condition": {
"type": "object",
"additionalProperties": false,
"properties": {
"filter": {
"type": "string"
},
"label_extractors": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"filter"
]
},
"monitoring_query_condition": {
"type": "object",
"additionalProperties": false,
"properties": {
"duration": {
"type": "string"
},
"query": {
"type": "string"
},
"evaluation_missing_data": {
"type": "string"
},
"trigger": {
"$ref": "#/$defs/trigger"
}
},
"required": [
"duration",
"query"
]
},
"prometheus_query_condition": {
"type": "object",
"additionalProperties": false,
"properties": {
"query": {
"type": "string"
},
"alert_rule": {
"type": "string"
},
"disable_metric_validation": {
"type": "boolean"
},
"duration": {
"type": "string"
},
"evaluation_interval": {
"type": "string"
},
"labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"rule_group": {
"type": "string"
}
},
"required": [
"query"
]
},
"threshold_condition": {
"type": "object",
"additionalProperties": false,
"properties": {
"comparison": {
"type": "string"
},
"duration": {
"type": "string"
},
"denominator_filter": {
"type": "string"
},
"evaluation_missing_data": {
"type": "string"
},
"filter": {
"type": "string"
},
"threshold_value": {
"type": "number"
},
"aggregations": {
"$ref": "#/$defs/aggregations"
},
"denominator_aggregations": {
"$ref": "#/$defs/aggregations"
},
"forecast_options": {
"type": "object",
"additionalProperties": false,
"properties": {
"forecast_horizon": {
"type": "string"
}
}
},
"trigger": {
"$ref": "#/$defs/trigger"
}
},
"required": [
"comparison",
"duration"
]
},
"aggregations": {
"type": "object",
"additionalProperties": false,
"properties": {
"per_series_aligner": {
"type": "string"
},
"group_by_fields": {
"type": "array",
"items": {
"type": "string"
}
},
"cross_series_reducer": {
"type": "string"
},
"alignment_period": {
"type": "string"
}
}
},
"trigger": {
"type": "object",
"additionalProperties": false,
"properties": {
"count": {
"type": "number"
},
"percent": {
"type": "number"
}
}
}
}
}

View File

@@ -787,6 +787,13 @@
role: null
is_primary: false
aliases: []
- name: gcp-ri-contactcenterinsights
display_name: Contact Center Insights Resource Identity (prod)
api: contactcenterinsights.googleapis.com
identity: service-%s@gcp-ri-contactcenterinsights.iam.gserviceaccount.com
role: null
is_primary: false
aliases: []
- name: container-analysis
display_name: Container Analysis Service Agent
api: containeranalysis.googleapis.com
@@ -969,6 +976,13 @@
role: roles/firebaseapphosting.serviceAgent
is_primary: true
aliases: []
- name: crashlytics
display_name: Firebase Crashlytics Service Agent
api: firebasecrashlytics.googleapis.com
identity: service-%s@gcp-sa-crashlytics.iam.gserviceaccount.com
role: roles/firebasecrashlytics.serviceAgent
is_primary: true
aliases: []
- name: firebasedataconnect
display_name: Firebase Data Connect Service Account
api: firebasedataconnect.googleapis.com
@@ -1173,13 +1187,6 @@
role: null
is_primary: false
aliases: []
- name: chronicle-spanner
display_name: Internal Chronicle Spanner Service Account
api: chronicle.googleapis.com
identity: service-%s@gcp-sa-chronicle-spanner.iam.gserviceaccount.com
role: null
is_primary: false
aliases: []
- name: fs-spanner
display_name: Internal Cloud Firestore Spanner Service Agent
api: firestore.googleapis.com
@@ -1351,6 +1358,13 @@
role: null
is_primary: true
aliases: []
- name: progrollout
display_name: Progressive Rollout Service Agent
api: progressiverollout.googleapis.com
identity: service-%s@gcp-sa-progrollout.iam.gserviceaccount.com
role: roles/progressiverollout.serviceAgent
is_primary: true
aliases: []
- name: pubsublite
display_name: Pub/Sub Lite Service Account
api: pubsublite.googleapis.com
@@ -1572,7 +1586,7 @@
display_name: Vertex AI Online Prediction Service Agent
api: aiplatform.googleapis.com
identity: service-%s@gcp-sa-vertex-op.iam.gserviceaccount.com
role: null
role: roles/aiplatform.onlinePredictionServiceAgent
is_primary: false
aliases: []
- name: vertex-tune

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2024 Google LLC
* 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.
@@ -14,6 +14,116 @@
* limitations under the License.
*/
variable "alerts" {
description = "Monitoring alerts."
type = map(object({
combiner = string
display_name = optional(string)
enabled = optional(bool)
notification_channels = optional(list(string), [])
severity = optional(string)
user_labels = optional(map(string))
alert_strategy = optional(object({
auto_close = optional(string)
notification_prompts = optional(string)
notification_rate_limit = optional(object({
period = optional(string)
}))
notification_channel_strategy = optional(object({
notification_channel_names = optional(list(string))
renotify_interval = optional(string)
}))
}))
conditions = optional(list(object({
display_name = string
condition_absent = optional(object({
duration = string
filter = optional(string)
aggregations = optional(object({
per_series_aligner = optional(string)
group_by_fields = optional(list(string))
cross_series_reducer = optional(string)
alignment_period = optional(string)
}))
trigger = optional(object({
count = optional(number)
percent = optional(number)
}))
}))
condition_matched_log = optional(object({
filter = string
label_extractors = optional(map(string))
}))
condition_monitoring_query_language = optional(object({
duration = string
query = string
evaluation_missing_data = optional(string)
trigger = optional(object({
count = optional(number)
percent = optional(number)
}))
}))
condition_prometheus_query_language = optional(object({
query = string
alert_rule = optional(string)
disable_metric_validation = optional(bool)
duration = optional(string)
evaluation_interval = optional(string)
labels = optional(map(string))
rule_group = optional(string)
}))
condition_threshold = optional(object({
comparison = string
duration = string
denominator_filter = optional(string)
evaluation_missing_data = optional(string)
filter = optional(string)
threshold_value = optional(number)
aggregations = optional(object({
per_series_aligner = optional(string)
group_by_fields = optional(list(string))
cross_series_reducer = optional(string)
alignment_period = optional(string)
}))
denominator_aggregations = optional(object({
per_series_aligner = optional(string)
group_by_fields = optional(list(string))
cross_series_reducer = optional(string)
alignment_period = optional(string)
}))
forecast_options = optional(object({
forecast_horizon = string
}))
trigger = optional(object({
count = optional(number)
percent = optional(number)
}))
}))
})), [])
documentation = optional(object({
content = optional(string)
mime_type = optional(string)
subject = optional(string)
links = optional(list(object({
display_name = optional(string)
url = optional(string)
})))
}))
}))
nullable = false
default = {}
}
variable "log_scopes" {
description = "Log scopes under this project."
type = map(object({
description = optional(string)
resource_names = list(string)
}))
nullable = false
default = {}
}
variable "logging_data_access" {
description = "Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services."
type = map(map(list(string)))
@@ -36,11 +146,41 @@ variable "logging_exclusions" {
nullable = false
}
variable "log_scopes" {
description = "Log scopes under this project."
variable "logging_metrics" {
description = "Log-based metrics."
type = map(object({
description = optional(string)
resource_names = list(string)
filter = string
bucket_name = optional(string)
description = optional(string)
disabled = optional(bool)
label_extractors = optional(map(string))
value_extractor = optional(string)
bucket_options = optional(object({
explicit_buckets = optional(object({
bounds = list(number)
}))
exponential_buckets = optional(object({
num_finite_buckets = number
growth_factor = number
scale = number
}))
linear_buckets = optional(object({
num_finite_buckets = number
width = number
offset = number
}))
}))
metric_descriptor = optional(object({
metric_kind = string
value_type = string
display_name = optional(string)
unit = optional(string)
labels = optional(list(object({
key = string
description = optional(string)
value_type = optional(string)
})), [])
}))
}))
nullable = false
default = {}
@@ -83,3 +223,22 @@ variable "metric_scopes" {
default = []
nullable = false
}
variable "notification_channels" {
description = "Monitoring notification channels."
type = map(object({
type = string
description = optional(string)
display_name = optional(string)
enabled = optional(bool)
labels = optional(map(string))
user_labels = optional(map(string))
sensitive_labels = optional(object({
auth_token = optional(string)
password = optional(string)
service_key = optional(string)
}))
}))
nullable = false
default = {}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2024 Google LLC
* 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.
@@ -81,9 +81,13 @@ variable "descriptive_name" {
variable "factories_config" {
description = "Paths to data files and folders that enable factory functionality."
type = object({
custom_roles = optional(string)
org_policies = optional(string)
quotas = optional(string)
custom_roles = optional(string)
observability = optional(string)
org_policies = optional(string)
quotas = optional(string)
context = optional(object({
notification_channels = optional(map(string), {})
}), {})
})
nullable = false
default = {}

View File

@@ -0,0 +1,28 @@
{
"locals": {
"jit_services": {
"alloydb.googleapis.com": "roles/alloydb.serviceAgent",
"apigee.googleapis.com": "roles/apigee.serviceAgent",
"artifactregistry.googleapis.com": "roles/artifactregistry.serviceAgent",
"assuredworkloads.googleapis.com": "roles/assuredworkloads.serviceAgent",
"dns.googleapis.com": "roles/dns.serviceAgent",
"dataplex.googleapis.com": "roles/dataplex.serviceAgent",
"pubsub.googleapis.com": "roles/pubsub.serviceAgent",
"sqladmin.googleapis.com": "roles/cloudsql.serviceAgent",
"dataform.googleapis.com": "roles/dataform.serviceAgent",
"eventarc.googleapis.com": "roles/eventarc.serviceAgent",
"cloudkms.googleapis.com": null,
"dataproc.googleapis.com": "roles/dataproc.serviceAgent",
"cloudfunctions.googleapis.com": "roles/cloudfunctions.serviceAgent",
"run.googleapis.com": "roles/run.serviceAgent",
"iap.googleapis.com": null,
"container.googleapis.com": "roles/container.serviceAgent",
"looker.googleapis.com": "roles/looker.serviceAgent",
"monitoring.googleapis.com": "roles/monitoring.notificationServiceAgent",
"networkconnectivity.googleapis.com": "roles/networkconnectivity.serviceAgent",
"secretmanager.googleapis.com": null,
"vpcaccess.googleapis.com": "roles/vpcaccess.serviceAgent",
"servicenetworking.googleapis.com": "roles/servicenetworking.serviceAgent"
}
}
}

View File

@@ -14,15 +14,13 @@
locals {
prefix = "${var.prefix}-${var.timestamp}${var.suffix}"
jit_services = [
"alloydb.googleapis.com", # no permissions granted by default
"artifactregistry.googleapis.com", # roles/artifactregistry.serviceAgent
"pubsub.googleapis.com", # roles/pubsub.serviceAgent
"storage.googleapis.com", # no permissions granted by default
"sqladmin.googleapis.com", # roles/cloudsql.serviceAgent
]
services = [
# trimmed down list of services, to be extended as needed
# trimmed down list of services, to be extended as needed. If you
# update this list, make sure to update E2E_SERVICES in
# tools/built_service_agents.py and run:
#
# python tools/build_service_agents.py --e2e > tests/examples_e2e/setup_module/jit.tf.json
#
"alloydb.googleapis.com",
"analyticshub.googleapis.com",
"apigee.googleapis.com",
@@ -212,6 +210,7 @@ resource "google_service_networking_connection" "psa_connection" {
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.psa_ranges.name]
deletion_policy = "ABANDON"
depends_on = [google_project_iam_binding.agents]
}
### END OF PSA
@@ -223,33 +222,24 @@ resource "google_service_account" "service_account" {
}
resource "google_project_service_identity" "jit_si" {
for_each = toset(local.jit_services)
for_each = local.jit_services
provider = google-beta
project = google_project.project.project_id
service = each.value
service = each.key
depends_on = [google_project_service.project_service]
}
resource "google_project_iam_binding" "cloudsql_agent" {
members = ["serviceAccount:service-${google_project.project.number}@gcp-sa-cloud-sql.iam.gserviceaccount.com"]
project = google_project.project.project_id
role = "roles/cloudsql.serviceAgent"
depends_on = [google_project_service_identity.jit_si]
resource "google_project_iam_binding" "agents" {
for_each = {
for k, v in local.jit_services : k => v if v != null
}
members = [
google_project_service_identity.jit_si[each.key].member
]
project = google_project.project.project_id
role = each.value
}
resource "google_project_iam_binding" "artifactregistry_agent" {
members = ["serviceAccount:service-${google_project.project.number}@gcp-sa-artifactregistry.iam.gserviceaccount.com"]
project = google_project.project.project_id
role = "roles/artifactregistry.serviceAgent"
depends_on = [google_project_service_identity.jit_si]
}
resource "google_project_iam_binding" "pubsub_agent" {
members = ["serviceAccount:service-${google_project.project.number}@gcp-sa-pubsub.iam.gserviceaccount.com"]
project = google_project.project.project_id
role = "roles/pubsub.serviceAgent"
depends_on = [google_project_service_identity.jit_si]
}
resource "local_file" "terraform_tfvars" {
filename = "e2e_tests.tfvars"

View File

@@ -17,6 +17,8 @@
from dataclasses import asdict, dataclass
from itertools import chain
import click
import json
import requests
import yaml
from bs4 import BeautifulSoup
@@ -41,6 +43,42 @@ ALIASES = {
'serverless-robot-prod': ['cloudrun', 'run'],
}
E2E_SERVICES = [
"alloydb.googleapis.com",
"analyticshub.googleapis.com",
"apigee.googleapis.com",
"artifactregistry.googleapis.com",
"assuredworkloads.googleapis.com",
"bigquery.googleapis.com",
"cloudbuild.googleapis.com",
"cloudfunctions.googleapis.com",
"cloudkms.googleapis.com",
"cloudresourcemanager.googleapis.com",
"compute.googleapis.com",
"container.googleapis.com",
"dataform.googleapis.com",
"dataplex.googleapis.com",
"dataproc.googleapis.com",
"dns.googleapis.com",
"eventarc.googleapis.com",
"iam.googleapis.com",
"iap.googleapis.com",
"logging.googleapis.com",
"looker.googleapis.com",
"monitoring.googleapis.com",
"networkconnectivity.googleapis.com",
"pubsub.googleapis.com",
"run.googleapis.com",
"secretmanager.googleapis.com",
"servicenetworking.googleapis.com",
"serviceusage.googleapis.com",
"sqladmin.googleapis.com",
"stackdriver.googleapis.com",
"storage-component.googleapis.com",
"storage.googleapis.com",
"vpcaccess.googleapis.com",
]
PRIMARY_OVERRIDE = {
'storage-transfer-service': True,
}
@@ -57,7 +95,9 @@ class Agent:
aliases: list[str]
def main():
@click.command()
@click.option('--e2e', is_flag=True, default=False)
def main(e2e=False):
page = requests.get(SERVICE_AGENTS_URL).content
soup = BeautifulSoup(page, 'html.parser')
agents = []
@@ -115,11 +155,19 @@ def main():
aliases = set(chain.from_iterable(agent.aliases for agent in agents))
assert aliases.isdisjoint(names)
# take the header from the first lines of this file
header = open(__file__).readlines()[2:15]
print("".join(header))
# and print all the agents
print(yaml.safe_dump([asdict(a) for a in agents], sort_keys=False))
if not e2e:
# take the header from the first lines of this file
header = open(__file__).readlines()[2:15]
print("".join(header))
# and print all the agents
print(yaml.safe_dump([asdict(a) for a in agents], sort_keys=False))
else:
jit_services = {}
result = {"locals": {"jit_services": jit_services}}
for a in agents:
if a.is_primary and a.api in E2E_SERVICES:
jit_services[a.api] = a.role
print(json.dumps(result, indent=2))
if __name__ == '__main__':