Rename FAST stages preparing for eventual deprecation (#3298)

* renames

* links

* readme

* docs

* update pf modules tests for renames

* condition_vars context in modules

* data platform dataset

* fix links in stage 3 docs

* schema changes

* schema docs

* tfdoc

* update duplicates check

* fast legacy tests

* legacy schema

* fix tests
This commit is contained in:
Ludovico Magnocavallo
2025-09-04 08:24:11 +02:00
committed by GitHub
parent 1f59fd6bc7
commit bc6950e205
475 changed files with 8947 additions and 11694 deletions

View File

@@ -36,14 +36,10 @@ FAST uses YAML-based factories to deploy subnets and firewall rules and, as its
### CI/CD ### CI/CD
One of our objectives with FAST is to provide a lightweight reference design for the IaC repositories, and a built-in implementation for running our code in automated pipelines. Our CI/CD approach leverages [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation), and provides sample workflow configurations for several major providers. Refer to the [CI/CD section in the bootstrap stage](./stages/0-bootstrap/README.md#cicd) for more details. We also provide separate [optional small stages](./extras/) to help you configure your CI/CD provider. One of our objectives with FAST is to provide a lightweight reference design for the IaC repositories, and a built-in implementation for running our code in automated pipelines. Our CI/CD approach leverages [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation), and provides sample workflow configurations for several major providers. Refer to the [CI/CD section in the bootstrap stage](./stages/0-bootstrap/README.md#cicd-configuration) for more details. We also provide separate [optional small stages](./extras/) to help you configure your CI/CD provider.
<!-- TODO: move CI/CD documentation to its own file --> <!-- TODO: move CI/CD documentation to its own file -->
### Multitenant organizations
FAST has built-in support for multitenancy implemented in [an add-on stage](./addons/1-resman-tenants/). Tenants can optionally be created with FAST compatibility, allowing them independent use of stages 1+ in their own context.
## Implementation ## Implementation
There are many decisions and tasks required to convert an empty GCP organization to one that can host production environments safely. Arguably, FAST could expose those decisions as configuration options to allow for different outcomes. However, supporting all the possible combinations is almost impossible and leads to code which is hard to maintain efficiently. There are many decisions and tasks required to convert an empty GCP organization to one that can host production environments safely. Arguably, FAST could expose those decisions as configuration options to allow for different outcomes. However, supporting all the possible combinations is almost impossible and leads to code which is hard to maintain efficiently.

View File

@@ -1,5 +0,0 @@
FAST_STAGE_DESCRIPTION="tenant factory"
FAST_STAGE_LEVEL=1
FAST_STAGE_NAME=tenant-factory
FAST_STAGE_DEPS="0-globals 0-bootstrap"
# FAST_STAGE_OPTIONAL=""

View File

@@ -1,350 +0,0 @@
# Tenant Factory Resource Manager Add-on
This add-on implements multitenancy on top of the resource management stage, where a limited number of tenants need a high degree of autonomy over their slice of the shared organization, while still being subject to a measure of central control.
Typical use cases include large organizations managing a single Cloud subscription for multiple semi-independent entities (governments, state-wide associations), multinational groups with different local subsidiaries, or even business units who own their cloud presence while still consuming centralized resources or services.
<!-- BEGIN TOC -->
- [Design overview and choices](#design-overview-and-choices)
- [Regular tenants](#regular-tenants)
- [FAST-compatible tenants](#fast-compatible-tenants)
- [How to run this stage](#how-to-run-this-stage)
- [Provider and Terraform variables](#provider-and-terraform-variables)
- [Impersonating the automation service account](#impersonating-the-automation-service-account)
- [Variable configuration](#variable-configuration)
- [Running the stage](#running-the-stage)
- [Organization policy errors](#organization-policy-errors)
- [Tenant configuration](#tenant-configuration)
- [Configurations for both simple and FAST tenants](#configurations-for-both-simple-and-fast-tenants)
- [Configurations for FAST tenants](#configurations-for-fast-tenants)
- [Deploying FAST stages](#deploying-fast-stages)
- [Files](#files)
- [Variables](#variables)
- [Outputs](#outputs)
<!-- END TOC -->
## Design overview and choices
Our tenant design creates two folders per tenant:
- a higher level folder under central control, where services specific for the tenant but not controlled by them can be created (log sinks, shared networking connections)
- a lower level folder under tenant control, where their projects and services can be created
Each tenant can optionally:
- use a separate billing account
- use a separate Cloud Identity / Workspace
- be configured for full FAST compatibility, to allow independent deployment of a FAST Landing Zone in their environment
This stage is configured as a factory and allows managing multiple tenants together. When a tenant is configured in FAST compatible mode, this stage effectively acts as its bootstrap stage.
The following is a high level diagram of this stage design.
![Stage diagram](diagram.png)
<!--
And the flow through stages when using multitenancy.
![Multitenant stage flows](diagram-flow.png)
-->
### Regular tenants
Where FAST compatibility is not needed this stage creates minimal tenant environments, configuring the minimum amount of resources to allow them to operate independently:
- a centrally-managed folder with
- one log sink to export audit-related tenant events
- DRS organization policy configuration to allow the tenants's own Cloud Identity in IAM policies (if one is used)
- a minimal set of automation resources (service account, bucket) in the organization-level IaC project
- a tenant-managed folder with IAM roles assigned to the tenant administrators principal and the automation service account
- an optional VPC-SC policy scoped to the tenant folder and managed by the tenant
This allows quick bootstrapping of a large number of tenants which are either self-managed or which use customized IaC code.
Tenants of this type can be "upgraded" at any time to FAST compatibility by simply extending their configuration.
### FAST-compatible tenants
Tenants can also be configured for FAST compatibility. This approach effectively emulates the org-level bootstrap stage, allowing tenants to independently bring up a complete Landing Zone in their environment using FAST.
The main differences compared to organization-level FAST are:
- no bootstrap service account is created for tenants, as this stage is their effective bootstrap
- tenant-mamaged log sinks are configured in stage 1, since their bootstrap stage (this one) is under central control
- secure tags are created in the tenant automation project since tenants cannot operate at the organization level
- tenants cannot self-manage organization policies on their folder (this might change in a future release)
While this stage's approach to organization policies is to keep them under centralized management, it's still possible to allow tenants limited or full control over organization policies by either
- assigning them permissions on secure tags used in policy conditions, or
- assignign them organization policy admin permissions on the organization, with a condition based on the secure tag value bound to their folder
Once a FAST-enabled tenant is created, the admin principal for the tenant has access to a dedicated resource management service account and set of input files (provider, tfvars) and can then proceed to setup FAST using the regular stage 1.
## How to run this stage
This stage is designed as an add-on to the [resource management](../../stages/1-resman/README.md) stage, and reuses its IaC resources and IAM configurations.
Once the bootstrap and resource management stages are applied, configure the bootstrap stage `fast_addon` variable to enable this stage, as explained in the [add-ons documentation](../README.md).
### Provider and Terraform variables
As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../../stages/0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here.
The commands to link or copy the provider and terraform variable files can be easily derived from the `fast-links.sh` script in the FAST stages folder, passing it a single argument with the local output files folder (if configured) or the GCS output bucket in the automation project (derived from stage 0 outputs). The following examples demonstrate both cases, and the resulting commands that then need to be copy/pasted and run.
```bash
../fast-links.sh ~/fast-config
# File linking commands for tenant factory stage
# provider file
ln -s ~/fast-config/fast-test-00/providers/1-resman-providers.tf ./
# input files from other stages
ln -s ~/fast-config/fast-test-00/tfvars/0-globals.auto.tfvars.json ./
ln -s ~/fast-config/fast-test-00/tfvars/0-bootstrap.auto.tfvars.json ./
# conventional place for stage tfvars (manually created)
ln -s ~/fast-config/fast-test-00/1-tenant-factory.auto.tfvars ./
```
```bash
../fast-links.sh gs://xxx-prod-iac-core-outputs-0
# File linking commands for tenant factory stage
# provider file
gcloud storage cp gs://xxx-prod-iac-core-outputs-0/providers/1-resman-providers.tf ./
# input files from other stages
gcloud storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-globals.auto.tfvars.json ./
gcloud storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./
# conventional place for stage tfvars (manually created)
gcloud storage cp gs://xxx-prod-iac-core-outputs-0/1-tenant-factory.auto.tfvars ./
```
### Impersonating the automation service account
The preconfigured provider file uses impersonation to run with this stage's automation service account's credentials. The `gcp-devops` and `organization-admins` groups have the necessary IAM bindings in place to do that, so make sure the current user is a member of one of those groups.
### Variable configuration
Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets:
- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `0-globals.auto.tfvars.json` file linked or copied above
- variables which refer to resources managed by previous stages, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` file linked or copied above
- and finally variables that optionally control this stage's behaviour and customizations, and should be defined in a custom `1-tenant-factory.auto.tfvars` file
The latter set is explained in the [Tenant configuration](#tenant-configuration) section below, and the full list can be found in the [Variables](#variables) table at the bottom of this document.
Note that the `outputs_location` variable is disabled by default, if you want output files to be generated by this stage you need to explicitly set it in your `tfvars` file like this:
```tfvars
outputs_location = "~/fast-config"
```
For additional details on output files and how they are used, refer to the [bootstrap stage documentation](../../stages/0-bootstrap/README.md#output-files-and-cross-stage-variables).
### Running the stage
Once provider and variable values are in place and the correct user is configured, the stage can be run:
```bash
terraform init
terraform apply
```
#### Organization policy errors
If you get an organization policy error assigning IAM roles or setting essential contacts on tenant-level resources, make sure the tenant configuration contains the right customer id and domain in the `cloud_identity` attributes, and the administrative principals and essential contacts for the tenant belong to the right Cloud Identity.
If both are correct, wait a couple of minutes for the organization policies to be enforced and retry. Remember to also check the organization-level IaC project org policies, which can be customized via the bootstrap stage variables.
## Tenant configuration
This stage has only three variables that can be customized:
- `root_node` specifies the top-level folder under which all tenant folders are created; if it's not specified (the default) tenants are created directly under the organization
- `tag_names.tenant` defines the name of the tag key used to hold one tag value per tenant, and defaults to `"tenant"`
- `tenant_configs` is a map containing the configuration for each tenant, and is explained below
### Configurations for both simple and FAST tenants
A small number of attributes can be configured for each tenant in `tenant_configs` regardless of its type (simple or FAST-enabled).
The key in the tenant map is used as the tenant shortname, and should be selected with care as it becomes part of resource names. If the tenant plans on using FAST stages, the total combined length of string `{fast-prefix}-{tenant-shortname}` should not exceed 11 characters combined, unless a custom prefix is also defined for the tenant.
`admin_principal` is a IAM-format principal (e.g. `"group:tenant-admins@example.org"`) which is assigned administrative permissions on the tenant environment, and impersonation permissions on the automation service account.
`descriptive_name` is the name used for the tenant folder, and in some resource descriptions.
`billing_account` is optional and defaults to the organization billing account if not specified. If a custom billing account is used by the tenant, set its id in `billing_account.id`. When a custom billing account is used, this stage can optionally manage billing account permissions for tenant principals and service accounts by setting `billing_account.no_iam` to `false`. By default IAM is not managed for external billing accounts.
`cloud_identity` is optional and defaults to the organization Cloud Identity instance if not specified. If the tenant manages users and group via a separate Cloud Identity, set its configuration in this attribute.
`locations` is optional and allows overriding the organization-level locations. It is only really meaningful for FAST-enabled tenants, where this field is used for the locations of automation and log-related resources (GCS, log buckets, etc.).
`vpc_sc_policy_create` is optional and when `true` creates a VPC-SC policy for the tenant scoped to its folder, assigning administrative permissions on it to the tenant's admin principal and service account.
This is an example of two simple non-FAST enabled tenants:
```hcl
root_node = "folders/1234567890"
tenant_configs = {
s0 = {
admin_principal = "group:gcp-admins@s0.example.org"
billing_account = {
id = "0123456-0123456-0123456"
no_iam = false
}
descriptive_name = "Simple 0"
cloud_identity = {
customer_id = "CCC000CCC"
domain = "s0.example.org"
id = 1234567890
}
vpc_sc_policy_create = true
}
s1 = {
admin_principal = "group:s1-admins@example.org"
descriptive_name = "Simple 1"
}
}
```
### Configurations for FAST tenants
FAST compatibility is enabled for a tenant by defining the `fast_config` attribute in their configuration, in addition to the attributes outlined above.
The `fast_config` attributes control the FAST bootstrap emulation for a tenant, and behave in a similar way to the corresponding variables that control the [bootstrap stage](../../stages/0-bootstrap/README.md#variables). They are all optional, and their behaviour is explained in the bootstrap stage documentation.
This is an example of two FAST-enabled tenants:
```hcl
tenant_configs = {
f0 = {
admin_principal = "group:gcp-admins@f0.example.org"
billing_account = {
# implicit use of org-level BA with IAM roles
no_iam = false
}
descriptive_name = "Fast 0"
cloud_identity = {
customer_id = "CdCdCdCd"
domain = "f0.example.org"
id = 1234567890
}
fast_config = {
groups = {
gcp-network-admins = "gcp-network-admins"
}
cicd_config = {
identity_provider = "github"
name = "ExampleF0/resman"
type = "github"
branch = "main"
}
workload_identity_providers = {
github = {
attribute_condition = "attribute.repository_owner==\"foobar\""
issuer = "github"
}
}
}
vpc_sc_policy_create = true
}
f1 = {
admin_principal = "group:f1-admins@example.org"
# implicit use of org-level BA without IAM roles
descriptive_name = "Fast 1"
# implicit use of org-level Cloud Identity
groups = {
gcp-billing-admins ="f1-gcp-billing-admins"
gcp-devops ="f1-gcp-devops"
gcp-network-admins ="f1-gcp-vpc-network-admins"
gcp-organization-admins ="f1-gcp-organization-admins"
gcp-security-admins ="f1-gcp-security-admins"
gcp-support ="f1-gcp-devops"
}
}
}
```
#### Deploying FAST stages
Mirroring the regular FAST behavior, the provider and variable files for a bootstrapped tenant will be generated on a tenant-specific storage bucket named `{prefix}-{tenant-shortname}-prod-iac-core-outputs-0` in (also tenant-specific) project `{prefix}-{tenant-shortname}-prod-iac-core-0`.
Since the tenant is already bootstrapped, a FAST deployment for tenants start from stage `1-resman`, which can be configured as usual, leveraging `stage-links.sh`, which should point to either the tenant-specific `var.outputs_location`, or to the tenant-specific GCS bucket.
For example:
```bash
/path/to/stage-links.sh ~/fast-config/tenants/tenant-a
# copy and paste the following commands for 'tenant-a/1-resman'
ln -s ~/fast-config/tenants/tenant-a/providers/1-tenant-factory-providers.tf ./
ln -s ~/fast-config/tenants/tenant-a/tfvars/0-globals.auto.tfvars.json ./
ln -s ~/fast-config/tenants/tenant-a/tfvars/0-bootstrap.auto.tfvars.json ./
```
```bash
/path/to/stage-links.sh gs://{prefix}-{tenant-shortname}-prod-iac-core-0
# copy and paste the following commands for 'tenant-a/1-resman'
gcloud storage cp gs://{prefix}-{tenant-shortname}-prod-iac-core-0/providers/1-tenant-factory-providers.tf ./
gcloud storage cp gs://{prefix}-{tenant-shortname}-prod-iac-core-0/tfvars/0-globals.auto.tfvars.json ./
gcloud storage cp gs://{prefix}-{tenant-shortname}-prod-iac-core-0/tfvars/0-bootstrap.auto.tfvars.json ./
```
<!-- TFDOC OPTS files:1 show_extra:1 exclude:1-tenant-factory-providers.tf -->
<!-- BEGIN TFDOC -->
## Files
| name | description | modules | resources |
|---|---|---|---|
| [identity-providers-defs.tf](./identity-providers-defs.tf) | Identity provider definitions. | | |
| [main.tf](./main.tf) | Module-level locals and resources. | <code>organization</code> | |
| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | <code>local_file</code> |
| [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | | <code>google_storage_bucket_object</code> |
| [outputs.tf](./outputs.tf) | Module outputs. | | |
| [tenant-billing-iam.tf](./tenant-billing-iam.tf) | Per-tenant billing IAM. | <code>billing-account</code> · <code>organization</code> | |
| [tenant-core.tf](./tenant-core.tf) | Per-tenant centrally managed resources. | <code>folder</code> · <code>logging-bucket</code> | |
| [tenant-fast-automation.tf](./tenant-fast-automation.tf) | Per-tenant FAST bootstrap emulation (automation). | <code>gcs</code> · <code>iam-service-account</code> · <code>project</code> | |
| [tenant-fast-cicd.tf](./tenant-fast-cicd.tf) | Per-tenant CI/CD resources. | <code>iam-service-account</code> | |
| [tenant-fast-identity-providers.tf](./tenant-fast-identity-providers.tf) | Per-tenant Workload Identity Federation providers. | | <code>google_iam_workload_identity_pool</code> · <code>google_iam_workload_identity_pool_provider</code> |
| [tenant-fast-logging.tf](./tenant-fast-logging.tf) | Per-tenant FAST bootstrap emulation (logging). | <code>project</code> | |
| [tenant-fast-vpcsc.tf](./tenant-fast-vpcsc.tf) | Per-tenant VPC-SC resources. | <code>vpc-sc</code> | |
| [tenant.tf](./tenant.tf) | Per-tenant resources. | <code>folder</code> · <code>gcs</code> · <code>iam-service-account</code> | |
| [variables-fast.tf](./variables-fast.tf) | FAST stage interface. | | |
| [variables.tf](./variables.tf) | Module variables. | | |
## Variables
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables-fast.tf#L19) | Automation resources created by the bootstrap stage. | <code title="object&#40;&#123;&#10; outputs_bucket &#61; string&#10; project_id &#61; string&#10; project_number &#61; string&#10; federated_identity_pool &#61; string&#10; federated_identity_providers &#61; map&#40;object&#40;&#123;&#10; audiences &#61; list&#40;string&#41;&#10; issuer &#61; string&#10; issuer_uri &#61; string&#10; name &#61; string&#10; principal_branch &#61; string&#10; principal_repo &#61; string&#10; &#125;&#41;&#41;&#10; service_accounts &#61; object&#40;&#123;&#10; resman &#61; string&#10; resman-r &#61; string&#10; &#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>0-bootstrap</code> |
| [billing_account](variables-fast.tf#L42) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | <code title="object&#40;&#123;&#10; id &#61; string&#10; is_org_level &#61; optional&#40;bool, true&#41;&#10; no_iam &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>0-bootstrap</code> |
| [environments](variables-fast.tf#L75) | Environment names. | <code title="map&#40;object&#40;&#123;&#10; name &#61; string&#10; short_name &#61; string&#10; tag_name &#61; string&#10; is_default &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | ✓ | | <code>0-globals</code> |
| [logging](variables-fast.tf#L121) | Logging resources created by the bootstrap stage. | <code title="object&#40;&#123;&#10; project_id &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>0-bootstrap</code> |
| [org_policy_tags](variables-fast.tf#L140) | Organization policy tags. | <code title="object&#40;&#123;&#10; key_id &#61; string&#10; key_name &#61; string&#10; values &#61; map&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>0-bootstrap</code> |
| [organization](variables-fast.tf#L130) | Organization details. | <code title="object&#40;&#123;&#10; domain &#61; string&#10; id &#61; number&#10; customer_id &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>0-bootstrap</code> |
| [prefix](variables-fast.tf#L157) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
| [custom_roles](variables-fast.tf#L53) | Custom roles defined at the org level, in key => id format. | <code title="object&#40;&#123;&#10; billing_viewer &#61; string&#10; dns_zone_binder &#61; string&#10; kms_key_encryption_admin &#61; string&#10; kms_key_viewer &#61; string&#10; organization_admin_viewer &#61; string&#10; project_iam_viewer &#61; string&#10; service_project_network_admin &#61; string&#10; storage_viewer &#61; string&#10; gcve_network_admin &#61; optional&#40;string&#41;&#10; gcve_network_viewer &#61; optional&#40;string&#41;&#10; network_firewall_policies_admin &#61; optional&#40;string&#41;&#10; ngfw_enterprise_admin &#61; optional&#40;string&#41;&#10; ngfw_enterprise_viewer &#61; optional&#40;string&#41;&#10; tenant_network_admin &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | <code>0-bootstrap</code> |
| [groups](variables-fast.tf#L93) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | <code title="object&#40;&#123;&#10; gcp-billing-admins &#61; optional&#40;string, &#34;gcp-billing-admins&#34;&#41;&#10; gcp-devops &#61; optional&#40;string, &#34;gcp-devops&#34;&#41;&#10; gcp-network-admins &#61; optional&#40;string, &#34;gcp-vpc-network-admins&#34;&#41;&#10; gcp-organization-admins &#61; optional&#40;string, &#34;gcp-organization-admins&#34;&#41;&#10; gcp-security-admins &#61; optional&#40;string, &#34;gcp-security-admins&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | <code>0-bootstrap</code> |
| [locations](variables-fast.tf#L108) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object&#40;&#123;&#10; bq &#61; optional&#40;string, &#34;EU&#34;&#41;&#10; gcs &#61; optional&#40;string, &#34;EU&#34;&#41;&#10; logging &#61; optional&#40;string, &#34;global&#34;&#41;&#10; pubsub &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | <code>0-bootstrap</code> |
| [names](variables.tf#L18) | Configuration for names used for resources and output files. | <code title="object&#40;&#123;&#10; output_files_prefix &#61; optional&#40;string, &#34;2-resman-tenants&#34;&#41;&#10; resource_short_name &#61; optional&#40;string, &#34;tn&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [outputs_location](variables.tf#L28) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
| [root_node](variables.tf#L34) | Root folder under which tenants are created, in folders/nnnn format. Defaults to the organization if null. | <code>string</code> | | <code>null</code> | |
| [tag_names](variables.tf#L47) | Customized names for resource management tags. | <code title="object&#40;&#123;&#10; tenant &#61; optional&#40;string, &#34;tenant&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [tenant_configs](variables.tf#L60) | Tenant configurations. Keys are the short names used for naming resources and should not be changed once defined. | <code title="map&#40;object&#40;&#123;&#10; admin_principal &#61; string&#10; descriptive_name &#61; string&#10; billing_account &#61; optional&#40;object&#40;&#123;&#10; id &#61; optional&#40;string&#41;&#10; no_iam &#61; optional&#40;bool, true&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; cloud_identity &#61; optional&#40;object&#40;&#123;&#10; customer_id &#61; string&#10; domain &#61; string&#10; id &#61; string&#10; &#125;&#41;&#41;&#10; locations &#61; optional&#40;object&#40;&#123;&#10; bq &#61; optional&#40;string, &#34;EU&#34;&#41;&#10; gcs &#61; optional&#40;string, &#34;EU&#34;&#41;&#10; logging &#61; optional&#40;string, &#34;global&#34;&#41;&#10; pubsub &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;&#10; fast_config &#61; optional&#40;object&#40;&#123;&#10; cicd_config &#61; optional&#40;object&#40;&#123;&#10; name &#61; string&#10; type &#61; string&#10; branch &#61; optional&#40;string&#41;&#10; identity_provider &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; groups &#61; optional&#40;object&#40;&#123;&#10; gcp-billing-admins &#61; optional&#40;string, &#34;gcp-billing-admins&#34;&#41;&#10; gcp-devops &#61; optional&#40;string, &#34;gcp-devops&#34;&#41;&#10; gcp-network-admins &#61; optional&#40;string, &#34;gcp-vpc-network-admins&#34;&#41;&#10; gcp-organization-admins &#61; optional&#40;string, &#34;gcp-organization-admins&#34;&#41;&#10; gcp-security-admins &#61; optional&#40;string, &#34;gcp-security-admins&#34;&#41;&#10; gcp-support &#61; optional&#40;string, &#34;gcp-devops&#34;&#41;&#10; &#125;&#41;&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; workload_identity_providers &#61; optional&#40;map&#40;object&#40;&#123;&#10; attribute_condition &#61; optional&#40;string&#41;&#10; issuer &#61; string&#10; custom_settings &#61; optional&#40;object&#40;&#123;&#10; issuer_uri &#61; optional&#40;string&#41;&#10; audiences &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; jwks_json &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; vpc_sc_policy_create &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
| [tenants](outputs.tf#L139) | Tenant base configuration. | | |
<!-- END TFDOC -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

View File

@@ -1,91 +0,0 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Identity provider definitions.
locals {
# tflint-ignore: terraform_unused_declarations
workforce_identity_providers_defs = {
azuread = {
attribute_mapping = {
"google.subject" = "assertion.subject"
"google.display_name" = "assertion.attributes.userprincipalname[0]"
"google.groups" = "assertion.attributes.groups"
"attribute.first_name" = "assertion.attributes.givenname[0]"
"attribute.last_name" = "assertion.attributes.surname[0]"
"attribute.user_email" = "assertion.attributes.mail[0]"
}
}
}
workload_identity_providers_defs = {
# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
github = {
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.sub" = "assertion.sub"
"attribute.actor" = "assertion.actor"
"attribute.repository" = "assertion.repository"
"attribute.repository_owner" = "assertion.repository_owner"
"attribute.ref" = "assertion.ref"
"attribute.fast_sub" = "\"repo:\" + assertion.repository + \":ref:\" + assertion.ref"
}
issuer_uri = "https://token.actions.githubusercontent.com"
principal_branch = "principalSet://iam.googleapis.com/%s/attribute.fast_sub/repo:%s:ref:refs/heads/%s"
principal_repo = "principalSet://iam.googleapis.com/%s/attribute.repository/%s"
}
# https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#token-payload
gitlab = {
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.sub" = "assertion.sub"
"attribute.environment" = "assertion.environment"
"attribute.environment_protected" = "assertion.environment_protected"
"attribute.namespace_id" = "assertion.namespace_id"
"attribute.namespace_path" = "assertion.namespace_path"
"attribute.pipeline_id" = "assertion.pipeline_id"
"attribute.pipeline_source" = "assertion.pipeline_source"
"attribute.project_id" = "assertion.project_id"
"attribute.project_path" = "assertion.project_path"
"attribute.repository" = "assertion.project_path"
"attribute.ref" = "assertion.ref"
"attribute.ref_protected" = "assertion.ref_protected"
"attribute.ref_type" = "assertion.ref_type"
}
issuer_uri = "https://gitlab.com"
principal_branch = "principalSet://iam.googleapis.com/%s/attribute.sub/project_path:%s:ref_type:branch:ref:%s"
principal_repo = "principalSet://iam.googleapis.com/%s/attribute.repository/%s"
}
# https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens#token-structure
terraform = {
attribute_mapping = {
"google.subject" = "assertion.terraform_workspace_id"
"attribute.aud" = "assertion.aud"
"attribute.terraform_run_phase" = "assertion.terraform_run_phase"
"attribute.terraform_project_id" = "assertion.terraform_project_id"
"attribute.terraform_project_name" = "assertion.terraform_project_name"
"attribute.terraform_workspace_id" = "assertion.terraform_workspace_id"
"attribute.terraform_workspace_name" = "assertion.terraform_workspace_name"
"attribute.terraform_organization_id" = "assertion.terraform_organization_id"
"attribute.terraform_organization_name" = "assertion.terraform_organization_name"
"attribute.terraform_run_id" = "assertion.terraform_run_id"
"attribute.terraform_full_workspace" = "assertion.terraform_full_workspace"
}
issuer_uri = "https://app.terraform.io"
principal_branch = "principalSet://iam.googleapis.com/%s/attribute.terraform_workspace_id/%s"
principal_repo = "principalSet://iam.googleapis.com/%s/attribute.terraform_project_id/%s"
}
}
}

View File

@@ -1,50 +0,0 @@
/**
* Copyright 2024 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 {
default_environment = [
for k, v in var.environments : v if v.is_default == true
][0]
tenants = {
for k, v in var.tenant_configs : k => merge(v, {
billing_account = merge(v.billing_account, {
id = coalesce(v.billing_account.id, var.billing_account.id)
# only set is_org_level when using the org billing account
is_org_level = (
v.billing_account.id == null ||
v.billing_account.id == var.billing_account.id
) ? var.billing_account.is_org_level : false
})
locations = coalesce(v.locations, var.locations)
organization = coalesce(v.cloud_identity, var.organization)
})
}
}
module "organization" {
source = "../../../modules/organization"
organization_id = "organizations/${var.organization.id}"
tags = {
(var.tag_names.tenant) = {
description = "Resource management tenant."
values = {
for k, v in local.tenants : k => {
description = v.descriptive_name
}
}
}
}
}

View File

@@ -1,78 +0,0 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Output files persistence to local filesystem.
resource "local_file" "providers-simple" {
for_each = var.outputs_location == null ? {} : {
for k, v in local.tenants : k => local.tenant_data[k]
}
file_permission = "0644"
filename = "${try(pathexpand(var.outputs_location), "")}/providers/${var.names.output_files_prefix}-${each.key}.tf"
content = templatefile(local._tpl_providers, {
backend_extra = null
bucket = each.value.gcs_bucket
name = each.key
sa = each.value.service_account
})
}
resource "local_file" "tfvars-simple" {
for_each = var.outputs_location == null ? {} : {
for k, v in local.tenants : k => local.tenant_data[k]
}
file_permission = "0644"
filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/${var.names.output_files_prefix}-${each.key}.auto.tfvars.json"
content = jsonencode(each.value)
}
resource "local_file" "providers" {
for_each = var.outputs_location == null ? {} : local.tenant_providers
file_permission = "0644"
filename = "${try(pathexpand(var.outputs_location), "")}/${var.names.output_files_prefix}/${each.key}/providers/1-resman-providers.tf"
content = try(each.value, null)
}
resource "local_file" "providers-r" {
for_each = var.outputs_location == null ? {} : local.tenant_providers_r
file_permission = "0644"
filename = "${try(pathexpand(var.outputs_location), "")}/${var.names.output_files_prefix}/${each.key}/providers/1-resman-r-providers.tf"
content = try(each.value, null)
}
resource "local_file" "tfvars" {
# work around Terraform's botched ternary type check on maps
for_each = {
for k, v in local.tenant_tfvars : k => v if var.outputs_location != null
}
file_permission = "0644"
filename = "${try(pathexpand(var.outputs_location), "")}/${var.names.output_files_prefix}/${each.key}/tfvars/0-bootstrap.auto.tfvars.json"
content = jsonencode(each.value)
}
resource "local_file" "tfvars_globals" {
for_each = var.outputs_location == null ? {} : local.tenant_globals
file_permission = "0644"
filename = "${try(pathexpand(var.outputs_location), "")}/${var.names.output_files_prefix}/${each.key}/tfvars/0-globals.auto.tfvars.json"
content = jsonencode(each.value)
}
resource "local_file" "workflows" {
for_each = var.outputs_location == null ? {} : local.tenant_cicd_workflows
file_permission = "0644"
filename = "${try(pathexpand(var.outputs_location), "")}/${var.names.output_files_prefix}/${each.key}/workflows/1-resman-workflow.yaml"
content = try(each.value, null)
}

View File

@@ -1,75 +0,0 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Output files persistence to automation GCS bucket.
resource "google_storage_bucket_object" "providers-simple" {
for_each = {
for k, v in local.tenants : k => local.tenant_data[k]
}
bucket = var.automation.outputs_bucket
name = "providers/${var.names.output_files_prefix}-${each.key}.tf"
content = templatefile(local._tpl_providers, {
backend_extra = null
bucket = each.value.gcs_bucket
name = each.key
sa = each.value.service_account
})
}
resource "google_storage_bucket_object" "tfvars-simple" {
for_each = {
for k, v in local.tenants : k => local.tenant_data[k]
}
bucket = var.automation.outputs_bucket
name = "tfvars/${var.names.output_files_prefix}-${each.key}.auto.tfvars.json"
content = jsonencode(each.value)
}
resource "google_storage_bucket_object" "providers" {
for_each = local.tenant_providers
bucket = module.tenant-automation-tf-output-gcs[each.key].name
name = "providers/1-resman-providers.tf"
content = each.value
}
resource "google_storage_bucket_object" "providers_r" {
for_each = local.tenant_providers_r
bucket = module.tenant-automation-tf-output-gcs[each.key].name
name = "providers/1-resman-r-providers.tf"
content = each.value
}
resource "google_storage_bucket_object" "tfvars" {
for_each = local.tenant_tfvars
bucket = module.tenant-automation-tf-output-gcs[each.key].name
name = "tfvars/0-bootstrap.auto.tfvars.json"
content = jsonencode(each.value)
}
resource "google_storage_bucket_object" "tfvars_globals" {
for_each = local.tenant_globals
bucket = module.tenant-automation-tf-output-gcs[each.key].name
name = "tfvars/0-globals.auto.tfvars.json"
content = jsonencode(each.value)
}
resource "google_storage_bucket_object" "workflows" {
for_each = local.tenant_cicd_workflows
bucket = module.tenant-automation-tf-output-gcs[each.key].name
name = "workflows/1-resman-workflow.yaml"
content = each.value
}

View File

@@ -1,142 +0,0 @@
/**
* Copyright 2024 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 {
_tpl_providers = "${path.module}/templates/providers.tf.tpl"
tenant_cicd_workflows = {
for k, v in local.cicd_repositories :
k => templatefile("${path.module}/templates/workflow-${v.type}.yaml", {
audiences = try(
local.identity_providers[v.tenant][v.identity_provider].audiences, null
)
identity_provider = try(
local.identity_providers[v.tenant][v.identity_provider].name, null
)
outputs_bucket = try(
module.tenant-automation-tf-output-gcs[k].name, null
)
service_accounts = {
apply = try(module.tenant-automation-tf-resman-sa[k].email, null)
plan = try(module.tenant-automation-tf-resman-r-sa[k].email, null)
}
stage_name = "1-resman"
tf_providers_files = {
apply = "1-resman-providers.tf"
plan = "1-resman-r-providers.tf"
}
tf_var_files = [
"0-bootstrap.auto.tfvars.json",
"0-globals.auto.tfvars.json"
]
})
}
tenant_data = {
for k, v in local.tenants : k => {
folder_id = module.tenant-folder[k].id
gcs_bucket = module.tenant-gcs[k].id
service_account = module.tenant-sa[k].email
vpcsc_policy_id = try(module.tenant-vpcsc-policy[k].id, null)
}
}
tenant_providers = {
for k, v in local.fast_tenants : k => templatefile(local._tpl_providers, {
backend_extra = null
bucket = module.tenant-automation-tf-resman-gcs[k].name
name = k
sa = module.tenant-automation-tf-resman-sa[k].email
})
}
tenant_providers_r = {
for k, v in local.fast_tenants : k => templatefile(local._tpl_providers, {
backend_extra = null
bucket = module.tenant-automation-tf-resman-gcs[k].name
name = k
sa = module.tenant-automation-tf-resman-r-sa[k].email
})
}
tenant_globals = {
for k, v in local.fast_tenants : k => {
billing_account = v.billing_account
groups = v.principals
environments = {
for k, v in var.environments : k => {
is_default = v.is_default
key = k
name = v.name
short_name = v.short_name != null ? v.short_name : k
tag_name = v.tag_name != null ? v.tag_name : lower(v.name)
}
}
locations = v.locations
organization = v.organization
prefix = v.prefix
}
}
tenant_tfvars = {
for k, v in local.fast_tenants : k => {
access_policy = try(module.tenant-vpcsc-policy[k].id, null)
automation = {
federated_identity_pool = null
federated_identity_providers = local.identity_providers[k]
outputs_bucket = module.tenant-automation-tf-output-gcs[k].name
project_id = module.tenant-automation-project[k].project_id
project_number = module.tenant-automation-project[k].number
service_accounts = {
resman = module.tenant-automation-tf-resman-sa[k].email
resman-r = module.tenant-automation-tf-resman-r-sa[k].email
}
tenant_service_accounts = {
network = module.tenant-automation-tf-network-sa[k].email
security = module.tenant-automation-tf-security-sa[k].email
security-r = module.tenant-automation-tf-security-r-sa[k].email
}
}
custom_roles = var.custom_roles
logging = {
log_sinks = {
audit-logs = {
filter = <<-FILTER
log_id("cloudaudit.googleapis.com/activity") OR
log_id("cloudaudit.googleapis.com/system_event") OR
log_id("cloudaudit.googleapis.com/policy") OR
log_id("cloudaudit.googleapis.com/access_transparency")
FILTER
type = "logging"
}
vpc-sc = {
filter = <<-FILTER
protoPayload.metadata.@type="type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata"
FILTER
type = "logging"
}
}
project_id = module.tenant-log-export-project[k].project_id
project_number = module.tenant-log-export-project[k].number
writer_identities = {}
}
org_policy_tags = var.org_policy_tags
root_node = module.tenant-folder[k].id
security = {
access_policy_id = try(module.tenant-vpcsc-policy[k].id, null)
}
}
}
}
output "tenants" {
description = "Tenant base configuration."
value = local.tenant_data
}

View File

@@ -1,229 +0,0 @@
# 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.
name: "FAST ${stage_name} stage"
on:
pull_request:
branches:
- main
types:
- closed
- opened
- synchronize
env:
FAST_SERVICE_ACCOUNT: ${service_accounts.apply}
FAST_SERVICE_ACCOUNT_PLAN: ${service_accounts.plan}
FAST_WIF_PROVIDER: ${identity_provider}
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
TF_PROVIDERS_FILE: ${tf_providers_files.apply}
TF_PROVIDERS_FILE_PLAN: ${tf_providers_files.plan}
TF_VERSION: 1.11.4
jobs:
fast-pr:
# Skip PRs which are closed without being merged.
if: >-
github.event.action == 'closed' &&
github.event.pull_request.merged == true ||
github.event.action == 'opened' ||
github.event.action == 'synchronize'
permissions:
contents: read
id-token: write
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- id: checkout
name: Checkout repository
uses: actions/checkout@v4
# set up SSH key authentication to the modules repository
- id: ssh-config
name: Configure SSH authentication
run: |
ssh-agent -a "$SSH_AUTH_SOCK" > /dev/null
ssh-add - <<< "$${{ secrets.CICD_MODULES_KEY }}"
# set up step variables for plan / apply
- id: vars-plan
if: github.event.pull_request.merged != true && success()
name: Set up plan variables
run: |
echo "plan_opts=-lock=false" >> "$GITHUB_ENV"
echo "provider_file=$${{env.TF_PROVIDERS_FILE_PLAN}}" >> "$GITHUB_ENV"
echo "service_account=$${{env.FAST_SERVICE_ACCOUNT_PLAN}}" >> "$GITHUB_ENV"
- id: vars-apply
if: github.event.pull_request.merged == true && success()
name: Set up apply variables
run: |
echo "provider_file=$${{env.TF_PROVIDERS_FILE}}" >> "$GITHUB_ENV"
echo "service_account=$${{env.FAST_SERVICE_ACCOUNT}}" >> "$GITHUB_ENV"
# set up authentication via Workload identity Federation and gcloud
- id: gcp-auth
name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: $${{env.FAST_WIF_PROVIDER}}
service_account: $${{env.service_account}}
access_token_lifetime: 900s
- id: gcp-sdk
name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
with:
install_components: alpha
# copy provider file
- id: tf-config-provider
name: Copy Terraform provider file
run: |
gcloud storage cp -r \
"gs://${outputs_bucket}/providers/$${{env.provider_file}}" ./
%{~ for f in tf_var_files ~}
gcloud storage cp -r \
"gs://${outputs_bucket}/tfvars/${f}" ./
%{~ endfor ~}
- id: tf-setup
name: Set up Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: $${{env.TF_VERSION}}
# run Terraform init/validate/plan
- id: tf-init
name: Terraform init
continue-on-error: true
run: |
terraform init -no-color
- id: tf-validate
continue-on-error: true
name: Terraform validate
run: terraform validate -no-color
- id: tf-plan
name: Terraform plan
continue-on-error: true
run: |
terraform plan -input=false -out ../plan.out -no-color $${{env.plan_opts}}
- id: tf-apply
if: github.event.pull_request.merged == true && success()
name: Terraform apply
continue-on-error: true
run: |
terraform apply -input=false -auto-approve -no-color ../plan.out
# PR comment with Terraform result from previous steps
# length is checked and trimmed for length so as to stay within the limit
- id: pr-comment
name: Post comment to Pull Request
continue-on-error: true
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
env:
PLAN: $${{steps.tf-plan.outputs.stdout}}\n$${{steps.tf-plan.outputs.stderr}}
with:
script: |
const output = `### Terraform Initialization \`$${{steps.tf-init.outcome}}\`
### Terraform Validation \`$${{steps.tf-validate.outcome}}\`
<details><summary>Validation Output</summary>
\`\`\`\n
$${{steps.tf-validate.outputs.stdout}}
\`\`\`
</details>
### Terraform Plan \`$${{steps.tf-plan.outcome}}\`
<details><summary>Show Plan</summary>
\`\`\`\n
$${process.env.PLAN.split('\n').filter(l => l.match(/^([A-Z\s].*|)$$/)).join('\n')}
\`\`\`
</details>
### Terraform Apply \`$${{steps.tf-apply.outcome}}\`
*Pusher: @$${{github.actor}}, Action: \`$${{github.event_name}}\`, Working Directory: \`$${{env.tf_actions_working_dir}}\`, Workflow: \`$${{github.workflow}}\`*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- id: pr-short-comment
name: Post comment to Pull Request (abbreviated)
uses: actions/github-script@v7
if: github.event_name == 'pull_request' && steps.pr-comment.outcome != 'success'
with:
script: |
const output = `### Terraform Initialization \`$${{steps.tf-init.outcome}}\`
### Terraform Validation \`$${{steps.tf-validate.outcome}}\`
### Terraform Plan \`$${{steps.tf-plan.outcome}}\`
Plan output is in the action log.
### Terraform Apply \`$${{steps.tf-apply.outcome}}\`
*Pusher: @$${{github.actor}}, Action: \`$${{github.event_name}}\`, Working Directory: \`$${{env.tf_actions_working_dir}}\`, Workflow: \`$${{github.workflow}}\`*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
# exit on error from previous steps
- id: check-init
name: Check init failure
if: steps.tf-init.outcome != 'success'
run: exit 1
- id: check-validate
name: Check validate failure
if: steps.tf-validate.outcome != 'success'
run: exit 1
- id: check-plan
name: Check plan failure
if: steps.tf-plan.outcome != 'success'
run: exit 1
- id: check-apply
name: Check apply failure
if: github.event.pull_request.merged == true && steps.tf-apply.outcome != 'success'
run: exit 1

View File

@@ -1,89 +0,0 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Per-tenant billing IAM.
locals {
# additive only since these operate at org level or BA level
_billing_bindings = flatten([
for k, v in local.tenants : [
v.fast_config == null
# non-fast tenant
? [
{
role = "roles/billing.user"
member = v.admin_principal
tenant = k
}
]
# fast tenant
: [
{
role = "roles/billing.admin"
member = local.fast_tenants[k].principals.gcp-billing-admins
tenant = k
},
{
role = "roles/billing.admin"
member = local.fast_tenants[k].principals.gcp-organization-admins
tenant = k
},
{
role = "roles/billing.admin"
member = module.tenant-automation-tf-resman-sa[k].iam_email
tenant = k
},
{
role = "roles/billing.viewer"
member = module.tenant-automation-tf-resman-r-sa[k].iam_email
tenant = k
},
]
] if v.billing_account.no_iam == false
])
# group bindings applied per billing account
_billing_ba_bindings = {
for v in local._billing_bindings :
local.tenants[v.tenant].billing_account.id => v...
if local.tenants[v.tenant].billing_account.is_org_level != true
}
# convert billing account grouped lists to maps
billing_ba_bindings = {
for k, v in local._billing_ba_bindings : k => {
for vv in v :
"${vv.tenant}-${vv.role}-${vv.member}" => vv
}
}
# convert org bindings to a map
billing_org_bindings = {
for v in local._billing_bindings :
"${v.tenant}-${v.role}-${v.member}" => v
if local.tenants[v.tenant].billing_account.is_org_level == true
}
}
module "billing-account" {
source = "../../../modules/billing-account"
for_each = local.billing_ba_bindings
id = each.key
iam_bindings_additive = each.value
}
module "organization-billing" {
source = "../../../modules/organization"
organization_id = "organizations/${var.organization.id}"
iam_bindings_additive = local.billing_org_bindings
}

View File

@@ -1,77 +0,0 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Per-tenant centrally managed resources.
locals {
root_node = coalesce(var.root_node, "organizations/${var.organization.id}")
}
module "tenant-core-logbucket" {
source = "../../../modules/logging-bucket"
for_each = local.tenants
parent = var.logging.project_id
name = "${var.names.resource_short_name}-${each.key}-audit"
location = var.locations.logging
log_analytics = { enable = true }
}
module "tenant-core-folder" {
source = "../../../modules/folder"
for_each = local.tenants
parent = local.root_node
name = "${each.value.descriptive_name} Core"
logging_sinks = {
"${var.names.resource_short_name}-${each.key}-audit" = {
destination = module.tenant-core-logbucket[each.key].id
filter = <<-FILTER
log_id("cloudaudit.googleapis.com/activity") OR
log_id("cloudaudit.googleapis.com/system_event") OR
log_id("cloudaudit.googleapis.com/policy") OR
log_id("cloudaudit.googleapis.com/access_transparency")
FILTER
type = "logging"
}
}
org_policies = each.value.cloud_identity == null ? {} : {
"essentialcontacts.allowedContactDomains" = {
rules = [{
allow = {
values = formatlist("@%s", compact([
var.organization.domain,
try(each.value.cloud_identity.domain, null)
]))
}
}]
}
"iam.allowedPolicyMemberDomains" = {
rules = [{
allow = {
values = compact([
var.organization.customer_id,
try(each.value.cloud_identity.customer_id, null)
])
}
}]
}
}
tag_bindings = {
tenant = try(
module.organization.tag_values["${var.tag_names.tenant}/${each.key}"].id,
null
)
}
}

View File

@@ -1,293 +0,0 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Per-tenant FAST bootstrap emulation (automation).
locals {
_fast_tenants = {
for k, v in local.tenants : k => merge(v, {
groups = coalesce(v.fast_config.groups, var.groups)
prefix = coalesce(v.fast_config.prefix, "${var.prefix}-${k}")
wif_provider = try(v.fast_config.cicd_config.identity_provider, "-")
}) if v.fast_config != null
}
fast_tenants = {
for k, v in local._fast_tenants : k => merge(v, {
stage_0_prefix = "${v.prefix}-${local.default_environment.short_name}"
principals = {
for gk, gv in v.groups : gk => (
can(regex("^[a-zA-Z]+:", gv))
? gv
: "group:${gv}@${v.organization.domain}"
)
}
})
}
}
module "tenant-automation-project" {
source = "../../../modules/project"
for_each = local.fast_tenants
billing_account = each.value.billing_account.id
name = "iac-core-0"
parent = module.tenant-folder[each.key].id
prefix = each.value.stage_0_prefix
# this is needed when destroying, resources cannot depend on the
# project-iam module to avoid circular dependencies
iam_bindings_additive = {
owner_org_resman = {
role = "roles/owner"
member = "serviceAccount:${var.automation.service_accounts.resman}"
}
}
services = [
"accesscontextmanager.googleapis.com",
"bigquery.googleapis.com",
"bigqueryreservation.googleapis.com",
"bigquerystorage.googleapis.com",
"billingbudgets.googleapis.com",
"cloudasset.googleapis.com",
"cloudbilling.googleapis.com",
"cloudbuild.googleapis.com",
"cloudkms.googleapis.com",
"cloudquotas.googleapis.com",
"cloudresourcemanager.googleapis.com",
"compute.googleapis.com",
"container.googleapis.com",
"essentialcontacts.googleapis.com",
"iam.googleapis.com",
"iamcredentials.googleapis.com",
"logging.googleapis.com",
"monitoring.googleapis.com",
"orgpolicy.googleapis.com",
"pubsub.googleapis.com",
"servicenetworking.googleapis.com",
"serviceusage.googleapis.com",
"storage-component.googleapis.com",
"storage.googleapis.com",
"sts.googleapis.com",
]
logging_data_access = {
"iam.googleapis.com" = {
ADMIN_READ = {}
}
}
}
module "tenant-automation-project-iam" {
source = "../../../modules/project"
for_each = local.fast_tenants
name = module.tenant-automation-project[each.key].project_id
project_reuse = {
use_data_source = false
attributes = {
name = module.tenant-automation-project[each.key].name
number = module.tenant-automation-project[each.key].number
}
}
# human (groups) IAM bindings
iam_by_principals = {
(each.value.principals.gcp-devops) = [
"roles/iam.serviceAccountAdmin",
"roles/iam.serviceAccountTokenCreator",
]
(each.value.principals.gcp-organization-admins) = [
"roles/iam.serviceAccountTokenCreator",
"roles/iam.workloadIdentityPoolAdmin"
]
}
# machine (service accounts) IAM bindings
iam = {
"roles/browser" = [
module.tenant-automation-tf-resman-r-sa[each.key].iam_email
]
"roles/cloudbuild.builds.editor" = [
module.tenant-automation-tf-resman-sa[each.key].iam_email
]
"roles/cloudbuild.builds.viewer" = [
module.tenant-automation-tf-resman-r-sa[each.key].iam_email
]
"roles/iam.serviceAccountAdmin" = [
module.tenant-automation-tf-resman-sa[each.key].iam_email
]
"roles/iam.serviceAccountViewer" = [
module.tenant-automation-tf-resman-r-sa[each.key].iam_email
]
"roles/iam.workloadIdentityPoolAdmin" = [
module.tenant-automation-tf-resman-sa[each.key].iam_email
]
"roles/iam.workloadIdentityPoolViewer" = [
module.tenant-automation-tf-resman-r-sa[each.key].iam_email
]
"roles/source.admin" = [
module.tenant-automation-tf-resman-sa[each.key].iam_email
]
"roles/source.reader" = [
module.tenant-automation-tf-resman-r-sa[each.key].iam_email
]
"roles/storage.admin" = [
module.tenant-automation-tf-resman-sa[each.key].iam_email
]
(var.custom_roles["storage_viewer"]) = [
module.tenant-automation-tf-resman-r-sa[each.key].iam_email
]
"roles/viewer" = [
"serviceAccount:${var.automation.service_accounts.resman-r}",
module.tenant-automation-tf-resman-r-sa[each.key].iam_email
]
}
iam_bindings = {
delegated_grants_resman = {
members = [module.tenant-automation-tf-resman-sa[each.key].iam_email]
role = "roles/resourcemanager.projectIamAdmin"
condition = {
title = "resman_delegated_grant"
description = "Resource manager service account delegated grant."
expression = format(
"api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['%s'])",
"roles/serviceusage.serviceUsageConsumer"
)
}
}
}
iam_bindings_additive = {
serviceusage_resman = {
member = module.tenant-automation-tf-resman-sa[each.key].iam_email
role = "roles/serviceusage.serviceUsageConsumer"
}
serviceusage_resman_r = {
member = module.tenant-automation-tf-resman-r-sa[each.key].iam_email
role = "roles/serviceusage.serviceUsageViewer"
}
}
depends_on = [module.tenant-automation-project]
}
# output files bucket
module "tenant-automation-tf-output-gcs" {
source = "../../../modules/gcs"
for_each = local.fast_tenants
project_id = module.tenant-automation-project[each.key].project_id
name = "iac-core-outputs-0"
prefix = each.value.stage_0_prefix
location = each.value.locations.gcs
versioning = true
}
# resource hierarchy stage's bucket and service account
module "tenant-automation-tf-resman-gcs" {
source = "../../../modules/gcs"
for_each = local.fast_tenants
project_id = module.tenant-automation-project[each.key].project_id
name = "iac-core-resman-0"
prefix = each.value.stage_0_prefix
location = each.value.locations.gcs
versioning = true
iam = {
"roles/storage.objectAdmin" = [
module.tenant-automation-tf-resman-sa[each.key].iam_email
]
"roles/storage.objectViewer" = [
module.tenant-automation-tf-resman-r-sa[each.key].iam_email
]
}
}
module "tenant-automation-tf-resman-sa" {
source = "../../../modules/iam-service-account"
for_each = local.fast_tenants
project_id = module.tenant-automation-project[each.key].project_id
name = "resman-0"
display_name = "Terraform stage 1 resman service account."
prefix = each.value.stage_0_prefix
# allow SA used by CI/CD workflow to impersonate this SA
# we use additive IAM to allow tenant CI/CD SAs to impersonate it
iam_bindings_additive = (
lookup(local.cicd_repositories, each.key, null) == null ? {} : {
cicd_token_creator = {
member = module.tenant-automation-tf-cicd-sa[each.key].iam_email
role = "roles/iam.serviceAccountTokenCreator"
}
}
)
iam_storage_roles = {
(module.tenant-automation-tf-output-gcs[each.key].name) = [
"roles/storage.admin"
]
}
}
module "tenant-automation-tf-resman-r-sa" {
source = "../../../modules/iam-service-account"
for_each = local.fast_tenants
project_id = module.tenant-automation-project[each.key].project_id
name = "resman-0r"
display_name = "Terraform stage 1 resman service account (read-only)."
prefix = each.value.stage_0_prefix
# allow SA used by CI/CD workflow to impersonate this SA
# we use additive IAM to allow tenant CI/CD SAs to impersonate it
iam_bindings_additive = (
lookup(local.cicd_repositories, each.key, null) == null ? {} : {
cicd_token_creator = {
member = module.automation-tf-cicd-r-sa[each.key].iam_email
role = "roles/iam.serviceAccountTokenCreator"
}
}
)
iam_storage_roles = {
(module.tenant-automation-tf-output-gcs[each.key].name) = [
var.custom_roles["storage_viewer"]
]
}
}
# tenant-level stage 2 service accounts are created here so that we can
# grant permissions on the org or VPC SC policy
module "tenant-automation-tf-network-sa" {
source = "../../../modules/iam-service-account"
for_each = local.fast_tenants
project_id = module.tenant-automation-project[each.key].project_id
name = "resman-net-0"
display_name = "Terraform resman networking service account."
prefix = each.value.stage_0_prefix
iam_organization_roles = {
(var.organization.id) = [
var.custom_roles.tenant_network_admin
]
}
}
module "tenant-automation-tf-security-sa" {
source = "../../../modules/iam-service-account"
for_each = local.fast_tenants
project_id = module.tenant-automation-project[each.key].project_id
name = "resman-sec-0"
display_name = "Terraform resman security service account."
prefix = each.value.stage_0_prefix
}
module "tenant-automation-tf-security-r-sa" {
source = "../../../modules/iam-service-account"
for_each = local.fast_tenants
project_id = module.tenant-automation-project[each.key].project_id
name = "resman-sec-0r"
display_name = "Terraform resman security service account (read-only)."
prefix = each.value.stage_0_prefix
}

View File

@@ -1,156 +0,0 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Per-tenant CI/CD resources.
locals {
# alias resources for readability
_wif_providers = {
for k, v in google_iam_workload_identity_pool_provider.default : k => v
}
# aggregate provider data from configurations and resources
_cicd_providers = [
for k, v in local.workload_identity_providers : {
audiences = concat(
local._wif_providers[k].oidc[0].allowed_audiences,
["https://iam.googleapis.com/${local._wif_providers[k].name}"]
)
issuer = v.issuer
issuer_uri = try(local._wif_providers[k].oidc[0].issuer_uri, null)
name = local._wif_providers[k].name
principal_branch = v.principal_branch
principal_repo = v.principal_repo
provider = v.provider
tenant = v.tenant
}
]
# group provider data by tenant
_cicd_tenant_providers = {
for v in local._cicd_providers : v.tenant => v...
}
# reconstitue per-tenant provider lists as maps
cicd_tenant_providers = {
for k, v in local._cicd_tenant_providers : k => {
for pv in v : pv.provider => pv
}
}
# filter tenant provider definitions to only keep valid ones
cicd_repositories = {
for k, v in local.fast_tenants :
k => merge(v.fast_config.cicd_config, {
tenant = k
})
# only keep CI/CD configurations that
if(
# are not null
try(v.fast_config.cicd_config, null) != null
&&
# are of a valid type (a template file exists for the type)
fileexists(
"${path.module}/templates/workflow-${try(v.fast_config.cicd_config.type, "")}.yaml"
)
&&
# either
(
# use an org-level WIF provider, or
try(var.automation.federated_identity_providers[v.wif_provider], null) != null
||
# use a tenant-level WIF provider
try(v.fast_config.workload_identity_providers[v.wif_provider], null) != null
)
)
}
# merge org-level and tenant-level providers for each tenant
identity_providers = {
for k, v in local.fast_tenants : k => merge(
try(var.automation.federated_identity_providers, {}),
try(local.cicd_tenant_providers[k], {})
)
}
}
# read-write (apply) SA used by CI/CD workflows to impersonate automation SA
module "tenant-automation-tf-cicd-sa" {
source = "../../../modules/iam-service-account"
for_each = local.cicd_repositories
project_id = var.automation.project_id
name = "${each.key}-1"
display_name = "Terraform CI/CD ${each.key} service account."
prefix = var.prefix
iam = {
"roles/iam.workloadIdentityUser" = [
each.value.branch == null
? format(
local.identity_providers[each.value.tenant][each.value.identity_provider].principal_repo,
var.automation.federated_identity_pool,
each.value.name
)
: length(regexall("%s", local.workload_identity_providers_defs[each.value.type].principal_branch)) == 2
? format(
local.identity_providers[each.value.tenant][each.value.identity_provider].principal_branch,
var.automation.federated_identity_pool,
each.value.branch
)
: format(
local.identity_providers[each.value.tenant][each.value.identity_provider].principal_branch,
var.automation.federated_identity_pool,
each.value.name,
each.value.branch
)
]
}
iam_project_roles = {
(module.tenant-automation-project[each.key].project_id) = [
"roles/logging.logWriter"
]
}
iam_storage_roles = {
(module.tenant-automation-tf-output-gcs[each.key].name) = [
"roles/storage.objectViewer"
]
}
}
# read-only (plan) SA used by CI/CD workflows to impersonate automation SA
module "automation-tf-cicd-r-sa" {
source = "../../../modules/iam-service-account"
for_each = local.cicd_repositories
project_id = var.automation.project_id
name = "${each.key}-1r"
display_name = "Terraform CI/CD ${each.key} service account (read-only)."
prefix = var.prefix
iam = {
"roles/iam.workloadIdentityUser" = [
format(
local.identity_providers[each.value.tenant][each.value.identity_provider].principal_repo,
var.automation.federated_identity_pool,
each.value.name
)
]
}
iam_project_roles = {
(module.tenant-automation-project[each.key].project_id) = [
"roles/logging.logWriter"
]
}
iam_storage_roles = {
(module.tenant-automation-tf-output-gcs[each.key].name) = [
"roles/storage.objectViewer"
]
}
}

View File

@@ -1,76 +0,0 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Per-tenant Workload Identity Federation providers.
locals {
# flatten tenant provider configurations into a single list and derive key
_workload_identity_providers = flatten([
for k, v in local.fast_tenants : [
for pk, pv in v.fast_config.workload_identity_providers : merge(
pv,
lookup(local.workload_identity_providers_defs, pv.issuer, {}),
{
key = "${k}-${pk}"
prefix = v.prefix
provider = pk
tenant = k
}
)
]
])
# identify FAST tenants with WIF configurations
workload_identity_pools = {
for k, v in local.fast_tenants : k => v.prefix
if length(v.fast_config.workload_identity_providers) > 0
}
# reconstitute all tenant provider configurations as a map
workload_identity_providers = {
for v in local._workload_identity_providers : v.key => v
}
}
resource "google_iam_workload_identity_pool" "default" {
provider = google-beta
for_each = local.workload_identity_pools
project = module.tenant-automation-project[each.key].project_id
workload_identity_pool_id = "${each.value}-bootstrap"
}
resource "google_iam_workload_identity_pool_provider" "default" {
provider = google-beta
for_each = local.workload_identity_providers
project = module.tenant-automation-project[each.value.tenant].project_id
workload_identity_pool_id = (
google_iam_workload_identity_pool.default[each.value.tenant].workload_identity_pool_id
)
workload_identity_pool_provider_id = "${each.value.prefix}-bootstrap-${each.value.provider}"
attribute_condition = each.value.attribute_condition
attribute_mapping = each.value.attribute_mapping
oidc {
# Setting an empty list configures allowed_audiences to the url of the provider
allowed_audiences = each.value.custom_settings.audiences
# If users don't provide an issuer_uri, we set the public one for the platform chosen.
issuer_uri = (
each.value.custom_settings.issuer_uri != null
? each.value.custom_settings.issuer_uri
: try(each.value.issuer_uri, null)
)
# OIDC JWKs in JSON String format. If no value is provided, they key is
# fetched from the `.well-known` path for the issuer_uri
jwks_json = each.value.custom_settings.jwks_json
}
}

View File

@@ -1,42 +0,0 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Per-tenant FAST bootstrap emulation (logging).
module "tenant-log-export-project" {
source = "../../../modules/project"
for_each = local.fast_tenants
billing_account = each.value.billing_account.id
name = "audit-logs-0"
parent = module.tenant-folder[each.key].id
prefix = each.value.stage_0_prefix
iam = {
"roles/owner" = [
"serviceAccount:${var.automation.service_accounts.resman}"
]
"roles/viewer" = [
"serviceAccount:${var.automation.service_accounts.resman-r}"
]
}
services = [
# "cloudresourcemanager.googleapis.com",
# "iam.googleapis.com",
# "serviceusage.googleapis.com",
"bigquery.googleapis.com",
"storage.googleapis.com",
"stackdriver.googleapis.com"
]
}

View File

@@ -1,57 +0,0 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Per-tenant VPC-SC resources.
module "tenant-vpcsc-policy" {
source = "../../../modules/vpc-sc"
for_each = {
for k, v in local.tenants : k => v if v.vpc_sc_policy_create == true
}
access_policy = null
access_policy_create = {
parent = "organizations/${var.organization.id}"
title = "tenant-${each.key}"
scopes = [module.tenant-core-folder[each.key].id]
}
iam_bindings_additive = merge(
{
# uncomment this if tenant admins are allowed by org-level DRS policy
# tenant_admins = {
# role = "roles/accesscontextmanager.policyAdmin"
# member = each.value.admin_principal
# }
tenant_sa = {
role = "roles/accesscontextmanager.policyAdmin"
member = module.tenant-sa[each.key].iam_email
}
},
each.value.fast_config == null ? {} : {
tenant_sa_resman = {
role = "roles/accesscontextmanager.policyAdmin"
member = module.tenant-automation-tf-resman-sa[each.key].iam_email
}
tenant_sa_security = {
role = "roles/accesscontextmanager.policyAdmin"
member = module.tenant-automation-tf-security-sa[each.key].iam_email
}
tenant_sa_security_r = {
role = "roles/accesscontextmanager.policyReader"
member = module.tenant-automation-tf-security-r-sa[each.key].iam_email
}
}
)
}

View File

@@ -1,124 +0,0 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Per-tenant resources.
module "tenant-folder" {
source = "../../../modules/folder"
for_each = local.tenants
parent = module.tenant-core-folder[each.key].id
name = each.value.descriptive_name
contacts = (
each.value.fast_config != null
? {}
: {
(split(":", each.value.admin_principal)[1]) = ["ALL"]
}
)
}
module "tenant-folder-iam" {
source = "../../../modules/folder"
for_each = local.tenants
id = module.tenant-folder[each.key].id
folder_create = false
iam = {
"roles/logging.admin" = compact([
each.value.admin_principal,
module.tenant-sa[each.key].iam_email,
try(module.tenant-automation-tf-resman-sa[each.key].iam_email, null)
])
"roles/owner" = [
each.value.admin_principal,
module.tenant-sa[each.key].iam_email
]
"roles/resourcemanager.folderAdmin" = compact([
each.value.admin_principal,
module.tenant-sa[each.key].iam_email,
try(module.tenant-automation-tf-resman-sa[each.key].iam_email, null)
])
"roles/resourcemanager.projectCreator" = compact([
each.value.admin_principal,
module.tenant-sa[each.key].iam_email,
try(module.tenant-automation-tf-resman-sa[each.key].iam_email, null)
])
"roles/serviceusage.serviceUsageViewer" = compact([
try(module.tenant-automation-tf-resman-r-sa[each.key].iam_email, null)
])
"roles/resourcemanager.tagAdmin" = compact([
try(module.tenant-automation-tf-resman-sa[each.key].iam_email, null)
])
"roles/resourcemanager.tagUser" = compact([
try(module.tenant-automation-tf-resman-sa[each.key].iam_email, null)
])
"roles/viewer" = compact([
try(module.tenant-automation-tf-resman-r-sa[each.key].iam_email, null)
])
}
iam_bindings = each.value.fast_config == null ? {} : {
tenant_iam_admin_conditional = {
members = [module.tenant-automation-tf-resman-sa[each.key].iam_email]
role = "roles/resourcemanager.folderIamAdmin"
condition = {
expression = format(
"api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
join(",", formatlist("'%s'", [
"roles/accesscontextmanager.policyAdmin",
"roles/cloudasset.viewer",
"roles/compute.orgFirewallPolicyAdmin",
"roles/compute.xpnAdmin",
var.custom_roles["tenant_network_admin"]
]))
)
title = "tenant_automation_sa_delegated_grants"
description = "Automation service account delegated grants."
}
}
}
depends_on = [module.tenant-automation-project]
}
# automation service account
module "tenant-sa" {
source = "../../../modules/iam-service-account"
for_each = local.tenants
project_id = var.automation.project_id
name = "${var.names.resource_short_name}-${each.key}-0"
display_name = "Terraform tenant ${each.key} service account."
prefix = var.prefix
iam = {
"roles/iam.serviceAccountTokenCreator" = [each.value.admin_principal]
}
iam_project_roles = {
(var.automation.project_id) = ["roles/serviceusage.serviceUsageConsumer"]
}
}
# automation bucket
module "tenant-gcs" {
source = "../../../modules/gcs"
for_each = local.tenants
project_id = var.automation.project_id
name = "${var.names.resource_short_name}-${each.key}-0"
prefix = var.prefix
location = each.value.locations.gcs
versioning = true
iam = {
"roles/storage.objectAdmin" = [module.tenant-sa[each.key].iam_email]
}
}

View File

@@ -1,161 +0,0 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description FAST stage interface.
variable "automation" {
# tfdoc:variable:source 0-bootstrap
description = "Automation resources created by the bootstrap stage."
type = object({
outputs_bucket = string
project_id = string
project_number = string
federated_identity_pool = string
federated_identity_providers = map(object({
audiences = list(string)
issuer = string
issuer_uri = string
name = string
principal_branch = string
principal_repo = string
}))
service_accounts = object({
resman = string
resman-r = string
})
})
}
variable "billing_account" {
# tfdoc:variable:source 0-bootstrap
description = "Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`."
type = object({
id = string
is_org_level = optional(bool, true)
no_iam = optional(bool, false)
})
nullable = false
}
variable "custom_roles" {
# tfdoc:variable:source 0-bootstrap
description = "Custom roles defined at the org level, in key => id format."
type = object({
billing_viewer = string
dns_zone_binder = string
kms_key_encryption_admin = string
kms_key_viewer = string
organization_admin_viewer = string
project_iam_viewer = string
service_project_network_admin = string
storage_viewer = string
gcve_network_admin = optional(string)
gcve_network_viewer = optional(string)
network_firewall_policies_admin = optional(string)
ngfw_enterprise_admin = optional(string)
ngfw_enterprise_viewer = optional(string)
tenant_network_admin = string
})
default = null
}
variable "environments" {
# tfdoc:variable:source 0-globals
description = "Environment names."
type = map(object({
name = string
short_name = string
tag_name = string
is_default = optional(bool, false)
}))
nullable = false
validation {
condition = anytrue([
for k, v in var.environments : v.is_default == true
])
error_message = "At least one environment should be marked as default."
}
}
variable "groups" {
# tfdoc:variable:source 0-bootstrap
# https://cloud.google.com/docs/enterprise/setup-checklist
description = "Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated."
type = object({
gcp-billing-admins = optional(string, "gcp-billing-admins")
gcp-devops = optional(string, "gcp-devops")
gcp-network-admins = optional(string, "gcp-vpc-network-admins")
gcp-organization-admins = optional(string, "gcp-organization-admins")
gcp-security-admins = optional(string, "gcp-security-admins")
})
nullable = false
default = {}
}
variable "locations" {
# tfdoc:variable:source 0-bootstrap
description = "Optional locations for GCS, BigQuery, and logging buckets created here."
type = object({
bq = optional(string, "EU")
gcs = optional(string, "EU")
logging = optional(string, "global")
pubsub = optional(list(string), [])
})
nullable = false
default = {}
}
variable "logging" {
# tfdoc:variable:source 0-bootstrap
description = "Logging resources created by the bootstrap stage."
type = object({
project_id = string
})
nullable = false
}
variable "organization" {
# tfdoc:variable:source 0-bootstrap
description = "Organization details."
type = object({
domain = string
id = number
customer_id = string
})
}
variable "org_policy_tags" {
# tfdoc:variable:source 0-bootstrap
description = "Organization policy tags."
type = object({
key_id = string
key_name = string
values = map(string)
})
}
check "prefix_validator" {
assert {
condition = (try(length(var.prefix), 0) < 10) || (try(length(var.prefix), 0) < 12 && var.root_node != null)
error_message = "var.prefix must be 9 characters or shorter for organizations, and 11 chars or shorter for tenants."
}
}
variable "prefix" {
# tfdoc:variable:source 0-bootstrap
description = "Prefix used for resources that need unique names. Use 9 characters or less."
type = string
}

View File

@@ -1,125 +0,0 @@
/**
* Copyright 2024 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.
*/
# TODO: backport names variable from resman stage
variable "names" {
description = "Configuration for names used for resources and output files."
type = object({
output_files_prefix = optional(string, "2-resman-tenants")
resource_short_name = optional(string, "tn")
})
nullable = false
default = {}
}
variable "outputs_location" {
description = "Path where providers and tfvars files for the following stages are written. Leave empty to disable."
type = string
default = null
}
variable "root_node" {
description = "Root folder under which tenants are created, in folders/nnnn format. Defaults to the organization if null."
type = string
default = null
validation {
condition = (
var.root_node == null ||
startswith(coalesce(var.root_node, "-"), "folders/")
)
error_message = "Root node must be a folder in folders/nnnn format."
}
}
variable "tag_names" {
description = "Customized names for resource management tags."
type = object({
tenant = optional(string, "tenant")
})
default = {}
nullable = false
validation {
condition = alltrue([for k, v in var.tag_names : v != null])
error_message = "Tag names cannot be null."
}
}
variable "tenant_configs" {
description = "Tenant configurations. Keys are the short names used for naming resources and should not be changed once defined."
type = map(object({
admin_principal = string
descriptive_name = string
billing_account = optional(object({
id = optional(string)
# is_org_level is only meaningful when using the org BA
# and set implicitly in tenant locals
no_iam = optional(bool, true)
}), {})
cloud_identity = optional(object({
customer_id = string
domain = string
id = string
}))
locations = optional(object({
bq = optional(string, "EU")
gcs = optional(string, "EU")
logging = optional(string, "global")
pubsub = optional(list(string), [])
}))
fast_config = optional(object({
cicd_config = optional(object({
name = string
type = string
branch = optional(string)
identity_provider = optional(string)
}))
groups = optional(object({
gcp-billing-admins = optional(string, "gcp-billing-admins")
gcp-devops = optional(string, "gcp-devops")
gcp-network-admins = optional(string, "gcp-vpc-network-admins")
gcp-organization-admins = optional(string, "gcp-organization-admins")
gcp-security-admins = optional(string, "gcp-security-admins")
gcp-support = optional(string, "gcp-devops")
}))
prefix = optional(string)
workload_identity_providers = optional(map(object({
attribute_condition = optional(string)
issuer = string
custom_settings = optional(object({
issuer_uri = optional(string)
audiences = optional(list(string), [])
jwks_json = optional(string)
}), {})
})), {})
}))
vpc_sc_policy_create = optional(bool, false)
}))
nullable = false
default = {}
validation {
condition = alltrue([
for k, v in var.tenant_configs :
length(coalesce(try(v.fast_config.prefix, null), "-")) < 11
])
error_message = "Tenant prefix too long, use a maximum of 10 characters."
}
validation {
condition = alltrue([
for k, v in var.tenant_configs : length(k) <= 3
])
error_message = "Tenant short name too long, use a maximum of 3 characters."
}
}

View File

@@ -1,648 +0,0 @@
# FAST Light Bootstrap (Experimental)
<!-- BEGIN TOC -->
- [TODO](#todo)
- [Quickstart](#quickstart)
- [Prerequisites](#prerequisites)
- [Select/configure a factory dataset](#selectconfigure-a-factory-dataset)
- [Configure defaults](#configure-defaults)
- [Initial user permissions](#initial-user-permissions)
- [First apply cycle](#first-apply-cycle)
- [Importing org policies](#importing-org-policies)
- [Local output files storage](#local-output-files-storage)
- [Init and apply the stage](#init-and-apply-the-stage)
- [Provider setup and final apply cycle](#provider-setup-and-final-apply-cycle)
- [Default factory datasets](#default-factory-datasets)
- ["Classic FAST" dataset](#classic-fast-dataset)
- ["Minimal" dataset](#minimal-dataset)
- ["Tenants" dataset](#tenants-dataset)
- [Detailed configuration](#detailed-configuration)
- [Factory data](#factory-data)
- [Defaults configuration](#defaults-configuration)
- [Billing account IAM](#billing-account-iam)
- [Context-based replacement in the billing account factory](#context-based-replacement-in-the-billing-account-factory)
- [Organization configuration](#organization-configuration)
- [Context-based replacement in organization factories](#context-based-replacement-in-organization-factories)
- [Resource management hierarchy](#resource-management-hierarchy)
- [Context-based replacement in the folders factory](#context-based-replacement-in-the-folders-factory)
- [Project factory](#project-factory)
- [CI/CD configuration](#cicd-configuration)
- [Leveraging classic FAST Stages](#leveraging-classic-fast-stages)
- [VPC Service Controls](#vpc-service-controls)
- [Security](#security)
- [Files](#files)
- [Variables](#variables)
- [Outputs](#outputs)
<!-- END TOC -->
## TODO
- [x] add support for log sinks to billing schema/code
- [ ] minimal dataset
- [ ] tenants dataset
- [x] clean up classic dataset
- [x] finish and review documentation
This stage implements a flexible approach to organization bootstrapping and resource management, that offers full customization via YAML factories.
It heavily relies on a new [project factory module](../../../modules/project-factory-experimental/) for folder and project configurations, and leverages a new approach to [context-based interpolation](../../../modules/project-factory-experimental/README.md#context-based-interpolation) that allows writing legible, portable YAML definitions.
The default set of YAML configuration files in the `data` folder mirrors the traditional FAST layout, and implements full compatibility with existing FAST stages like VPC-SC, security, networking, etc.
The default configuration can be used as a starting point to implement radically different Landing Zone designs, or trimmed down to its bare minimum where the requirements are simply to have a secure organization-level configuration (possibly with VPC-SC), and a working project factory.
## Quickstart
The high-level flow for running this stage is:
- ensure all **pre-requisites** are in place, and identify at least one GCP organization admin principal (ideally a group)
- select the **factory data set** for the factories among those available - populate the **defaults file** with attributes matching your configuration (organization id, billing account, etc.)
(`data`, `data-minimal`, etc.) or edit/create your own
- assign a set of **initial IAM roles** to the admin principal
- run a **first init/apply cycle** using user credentials
- copy the generated provider file, **migrate state**, then run a second init/apply cycle using service account impersonated credentials
### Prerequisites
This stage only requires minimal prerequisites:
- one organization
- credentials with admin access to the organization and one billing account
The organization ideally needs to be empty. If pre-existing resources are present some care needs to be put into preserving their existing IAM and org policies, ideally my moving legacy projects to a dedicated folder where the current org-level configuration (IAM and org policies) can be replicated.
Billing admin permissions are ideally available on either an org-contained billing account or an external one. If those are unavailable, the YAML configuration files need to be updated to remove billing IAM bindings, and those need to be assigned via an external flow. Refer to the [billing section](#billing-account-iam) for more details or non-standard configurations.
The admin principal is typically a group that includes the user running the first apply, but any kind of principal is supported. More principals (network admins, security admins, etc.) are present in some of the [default factories datasets](#default-factory-datasets), and others can be added if needed by editing the YAML configuration files.
### Select/configure a factory dataset
The `factories_config` variable points to several paths containing the YAML configuration files used by this stage. The default variable configuration points to the legacy FAST compatible fileset in the `data` folder.
If you are fine with this configuration nothing needs to be changed at this stage. To select a different setup create a `tfvars` file and set paths to the desired data folder, like shown in the example below. The different configurations produced by each fileset are described [later in this document](#default-factory-datasets).
```bash
# create a file named 0-bootstrap.auto.tfvars containing the following
# and replace paths by pointing them to the desired data folder
factories_config = {
billing_accounts = "data/billing-accounts"
cicd = "data/cicd.yaml"
defaults = "data/defaults.yaml"
folders = "data/folders"
organization = "data/organization"
projects = "data/projects"
}
```
### Configure defaults
Configurations defaults are stored in the `defaults.yaml` file in the dataset selected above. Before starting, edit the following attributes in the file to match your configuration.
The standard datasets use the `gcp-organization-admins` alias to assign administrator roles. The alias is expanded via the `context.iam_principals` attribute in the default file, which should be set to a valid group. Also make sure that the user running the initial apply is a member.
```yaml
global:
# gcloud beta billing accounts list
billing_account: 123456-123456-123456
locations:
bigquery: europe-west1
logging: europe-west1
organization:
# gcloud organizations list
domain: example.org
id: 1234567890
customer_id: ABC0123CDE
projects:
defaults:
# define a unique prefix with a maximum of 9 characters
prefix: foo-1
storage_location: europe-west1
context:
iam_principals:
# make sure the user running apply is a member of this group
gcp-organization-admins: group:fabric-fast-owners@example.com
```
A more detailed example containing a few other attributes that can be set in the file is in a [later section](#defaults-configuration) in this document.
### Initial user permissions
Like in classic FAST, the user running the first apply cycle needs specific permissions on the organization and billing account. Copy the following snippet, edit it to match your organization/billing account ids, then run each command.
To quickly self-grant the above roles, run the following code snippet as the initial Organization Admin. The best approach is to use the same group used for organization admins above.
```bash
export FAST_PRINCIPAL="group:fabric-fast-owners@example.com"
# find your organization and export its id in the FAST_ORG variable
gcloud organizations list
export FAST_ORG_ID=123456
# set needed roles (billing role only needed for organization-owned account)
export FAST_ROLES="\
roles/billing.admin \
roles/logging.admin \
roles/iam.organizationRoleAdmin \
roles/orgpolicy.policyAdmin \
roles/resourcemanager.folderAdmin \
roles/resourcemanager.organizationAdmin \
roles/resourcemanager.projectCreator \
roles/resourcemanager.tagAdmin \
roles/owner"
for role in $FAST_ROLES; do
gcloud organizations add-iam-policy-binding $FAST_ORG_ID \
--member $FAST_PRINCIPAL --role $role --condition None
done
```
If you are using an externally managed billing account, make sure user has Billing Admin role assigned on the account.
### First apply cycle
#### Importing org policies
If your dataset includes org policies which are already set in the organization, you need to either comment them out from the relevant YAML files or tell this stage to import them. To figure out which policies are set, run `gcloud org-policies list --organization [your org id]`, then set the `org_policies_imports` variable in your tfvars file. The following is an example.
```bash
gcloud org-policies list --organization 1234567890
CONSTRAINT LIST_POLICY BOOLEAN_POLICY
iam.allowedPolicyMemberDomains SET -
compute.disableSerialPortAccess - SET
```
```tfvars
# create or edit the 0-bootstrap.auto.tfvars.file
org_policies_imports = [
'iam.allowedPolicyMemberDomains',
'compute.disableSerialPortAccess'
]
```
Once org policies have been imported, the variable definition can be removed from the tfvars file.
#### Local output files storage
Like any other FAST stage, this stage creates output files that contain information about the resources it manages, or provide initial provider and backend configuration for the following stages.
These files are only persisted by default on a special outputs bucket, but can additionally be also persisted to a local path. This is very useful during the initial deployment, as it allows rapid apply iteration cycles between stages, and provides an easy way to check or derive resource ids.
To enable local output files storage, set the `outputs_location` variable in your tfvars file to a filesystem path dedicated to this organization's output files. The following snippet provides an example.
```tfvars
# create or edit the 0-bootstrap.auto.tfvars.file
outputs_location = "~/fast-configs/test-0"
```
#### Init and apply the stage
Once everything has been configured go through the standard Terraform init/apply cycle.
```bash
terraform init
terraform apply
```
### Provider setup and final apply cycle
When the first apply cycle has completed successfully, you are ready to switch Terraform to use the new GCS backend and service account credentials.
The first step is to link the generated provider file, either copying it from the GCS bucket or linking it from the local path if it has been configured in the previous step.
The instructions also assume that you have moved the `0-bootstrap.auto.tfvars` file (if you have one) to the GCS bucket or the local config files. This is good practice in order to have the tfvars file persisted, either via GCS or by committing it to a repository with the source code in a dedicated config folder. The file needs to be copied or moved by hand. Alternatively, the last copy/link command can be ignored.
If local output files are available adjust the path, run the script, then copy/paste the resulting commands.
```bash
# if local outputs file are available
../fast-links.sh ~/fast-configs/test-0
# File linking commands for FAST Bootstrap. stage
# provider file
ln -s /home/user/fast-configs/test-0/providers/0-bootstrap-providers.tf ./
# conventional location for this stage terraform.tfvars (manually managed)
ln -s /home/user/fast-configs/test-0/0-bootstrap.auto.tfvars ./
```
If you did not configure local output files use the GCS bucket to fetch output files. The bucket name can be derived from the `tfvars.bootstrap.automation.outputs_bucket` Terraform output. Adjust the path, run the script, then copy/paste the resulting commands.
```bash
../fast-links.sh gs://test0-prod-iac-core-0-iac-outputs
# File linking commands for FAST Bootstrap. stage
# provider file
gcloud storage cp gs://test0-prod-iac-core-0-iac-outputs/providers/0-bootstrap-providers.tf ./
# conventional location for this stage terraform.tfvars (manually managed)
gcloud storage cp gs://test0-prod-iac-core-0-iac-outputs/0-bootstrap.auto.tfvars ./
```
Once the provider file has been setup, migrate local state to the GCS backend and re-run apply.
```bash
terraform init -migrate-state
terraform apply
```
## Default factory datasets
A few example datasets are included with the stage, each implementing a different widely used organizational design. The datasets can be used as-is, potentially with slight changes to better suit specific use cases, or they can serve as a starting point to implement radically different approaches.
### "Classic FAST" dataset
This dataset implements a Classic FAST design that replicates legacy bootstrap and resource management stages. The resulting layout is easy to customize, and supports VPC SC, networking, security and potentially any FAST stage 3 directly as explained in a [later section](#leveraging-classic-fast-stages).
The organizational layout mirrors the consolidated FAST one, where shared infrastructure (stage 2 and 3) is partitioned via folders at the top, and further subdivided in environment-level folders for data or fleet management (Stage 3). An example "Teams" folder allows hooking up an application-level project factory as a separate stage, which is then used to define per-team subdivisions and create projects.
<p align="center">
<img src="diagram-classic-fast.png" alt="Classic FAST organization-level diagram.">
</p>
### "Minimal" dataset
This dataset is meant as a minimalistic starting point for organizations where a security baseline and a project factory are all that's needed, at least initially. The design can then organically grow to support more functionality, converging to the Classic or other types of layouts.
### "Tenants" dataset
TBD
## Detailed configuration
The following sections explain how to configure and run this stage, and should be read in sequence when using it for the first time.
### Factory data
The resources created by this stage are controlled by several factories, which point to YAML configuration files and folders. Data locations for each factory are controlled via the `var.factories_config` variable, and each factory path can be overridden individually.
The default paths point to the dataset in the `data` folder which deploys a FAST-compliant configuration. These are the available factories in this stage, with file-level factories based on a single YAML file, and folder-level factories based on sets of YAML files contained withing a filesystem folder:
- **defaults** (`data/defaults.yaml`) \
file-level factory to define stage defaults (organization id, locations, prefix, etc.) and static context mappings
- **billing_accounts** (`data/billing-accounts`) \
folder-level factory where each YAML file defines billing-account level IAM for one billing account; only used for externally managed accounts
- **organization** (`data/organization/.config.yaml`) \
file-level factory to define organization IAM and log sinks
- **custom roles** (`data/organization/custom-roles`) \
folder-level factory to define organization-level custom roles
- **org policies** (`data/organization/org-policies`) \
folder-level factory to define organization-level org policies
- **tags** (`data/organization/tags`) \
folder-level factory to define organization-level resource management tags
- **folders** (`data/folders`) \
folder-level factory to define the resource management hierarchy and individual folder attributes (IAM, org policies, tag bindings, etc.); also supports defining folder-level IaC resources
- **projects** (`data/projects`) \
folder-level factory to define projects and their attributes (projejct factory)
- **cicd** (`data/cicd.yaml`) \
file-level factory to define CI/CD configurations for this and subsequent stages
### Defaults configuration
The prerequisite configuration for this stage is done via a `defaults.yaml` file, which implements part or all of the [relevant JSON schema](./schemas/defaults.schema.json). The location of the file defaults to `data/defaults.yaml` but can be easily changed via the `factories_config.defaults` variable.
This is a commented example of a defaults file, showing a minimal working configuration. Refer to the YAML schema for all available options.
```yaml
# global defaults used by bootstrap and persisted in the globals output file
global:
# billing account also set as default in the internal project factory
billing_account: 123456-123456-123456
# default locations for this stage resources
locations:
bigquery: europe-west1
logging: europe-west1
# organization attributes (id is required)
organization:
domain: example.org
id: 1234567890
customer_id: ABC0123CDE
# project defaults and overrides used by the internal project factory
projects:
defaults:
# setting a prefix either here or in overrides is required
prefix: foo-1
# default location for storage buckets
storage_location: europe-west1
overrides: {}
# FAST output files generated by this stage
output_files:
# optional path for locally persisted output files
local_path: ~/fast-config/foo-1
# required storage bucket for output files (supports context interpolation)
storage_bucket: $storage_buckets:iac-0/iac-outputs
# FAST stage provider files (supports context interpolation)
providers:
0-bootstrap:
bucket: $storage_buckets:iac-0/iac-bootstrap-state
service_account: $iam_principals:service_accounts/iac-0/iac-bootstrap-rw
# [...]
# static values added to context interpolation tables and used in factories
context:
iam_principals:
gcp-organization-admins: group:fabric-fast-owners@example.com
```
### Billing account IAM
FAST traditionally supports three different billing configurations:
- billing account in the same organization, where billing IAM is set via organization-level bindings
- external billing account, where billing IAM is set via account-level bindings
- no billing IAM, where FAST assumes bindings are managed by some externally defined process
This stage allows the same flexibility, and even makes it possible to mix and match approaches by making billing IAM explicit:
- if billing-account level IAM bindings are needed, they can be set via the billing account factory
- if organization-level IAM bindings are needed, they can be set via the organization factory
- if no billing IAM can be managed here, it's enough to disable the billing account factory by pointing it to an empty or non-existent filesystem folder
The default dataset assumes an externally managed billing account is used, and configures its IAM accordingly via the billing account factory. The example below shows some of the IAM bindings configured at the billing account level, and how context-based interpolation is used there.
<details>
<summary>Context-based replacement examples for the billing acccounts factory</summary>
#### Context-based replacement in the billing account factory
Principal expansion leverages the `$iam_principals:` context, which is populated from the static mappings defined in defaults, and the service accounts generated via the internal project factory [described in a later section](#project-factory). Log sink definitions also support `$project_ids:` and `$storage_buckets` expansions.
```yaml
# example billing account factory file
# file: billing-accounts/default.yaml
id: $defaults:billing_account
iam_bindings_additive:
billing_admin_org_admins:
role: roles/billing.admin
# statically defined principal (via defaults.yaml)
member: $iam_principals:gcp-organization-admins
billing_admin_bootstrap_sa:
role: roles/billing.admin
# internally managed principal (project factory service account)
member: $iam_principals:service_accounts/iac-0/iac-bootstrap-rw
logging_sinks:
test:
description: Test sink
destination: $project_ids:log-0
type: project
```
</details>
### Organization configuration
The default dataset implements a classic FAST design, re-creating the required custom roles, IAM bindings, org policies, tags, and log sinks via the factories described in a previous section.
Compared to classic FAST this approach makes org-level configuration explicit, allowing easy customization of IAM and all other attributes. Before running this stage, check that the data files match your expected design.
Context-based interpolation is heavily used in the organization configuration files to refer to external or project-level resources, so as to make the factory files portable. Some examples are provided below to better illustrate usage and facilitate editing organization-level data.
<details>
<summary>Context-based replacement examples for organization factories</summary>
#### Context-based replacement in organization factories
Principal expansion leverages the `$iam_principals:` context, which is populated from the static mappings defined in defaults, and the service accounts generated via the internal project factory [described in a later section](#project-factory).
```yaml
# example principal-level context interpolation
# file: data/organization/.config.yaml
iam_by_principals:
# statically defined principal (via defaults.yaml)
$iam_principals:gcp-organization-admins:
- roles/cloudasset.owner
- roles/cloudsupport.admin
- roles/compute.osAdminLogin
# [...]
# internally managed principal (project factory service account)
$iam_principals:service_accounts/iac-0/iac-bootstrap-rw:
- roles/accesscontextmanager.policyAdmin
- roles/cloudasset.viewer
- roles/essentialcontacts.admin
# [...]
```
Log sinks can refer to project-level destination via different contexts.
```yaml
# example log sinks showing different destination contexts
# file: data/organization/.config.yaml
logging:
storage_location: $locations:default
sinks:
# log bucket destination
audit-logs:
destination: $log_buckets:log-0/audit-logs
filter: |
log_id("cloudaudit.googleapis.com/activity") OR
log_id("cloudaudit.googleapis.com/system_event") OR
log_id("cloudaudit.googleapis.com/policy") OR
log_id("cloudaudit.googleapis.com/access_transparency")
# storage bucket destination
iam:
destination: $storage_buckets:log-0/iam-sink
filter: |
protoPayload.serviceName="iamcredentials.googleapis.com" OR
protoPayload.serviceName="iam.googleapis.com" OR
protoPayload.serviceName="sts.googleapis.com"
# project destination
vpc-sc:
destination: $projject_ids:log-0
filter: |
protoPayload.metadata.@type="type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata"
```
Context-based expansion is not limited to the organization's `.config.yaml` file, but is also available in the other factories, like in this example for the organization-level tag factory.
```yaml
# example usage of context interpolation in tag values IAM
# file: data/organization/tags/environment.yaml
description: "Organization-level environments."
values:
development:
description: "Development."
iam:
"roles/resourcemanager.tagUser":
- $iam_principals:service_accounts/iac-0/iac-networking-rw
- $iam_principals:service_accounts/iac-0/iac-security-rw
- $iam_principals:service_accounts/iac-0/iac-pf-rw
"roles/resourcemanager.tagViewer":
- $iam_principals:service_accounts/iac-0/iac-networking-ro
- $iam_principals:service_accounts/iac-0/iac-security-ro
- $iam_principals:service_accounts/iac-0/iac-pf-ro
# [...]
```
An exception to the namespaced-based context replacements is in IAM conditions, where Terraform limitations force use of native string templating, as in the example below.
```yaml
iam_bindings:
pf_org_policy_admin:
role: roles/orgpolicy.policyAdmin
members:
- $iam_principals:service_accounts/iac-0/iac-pf-rw
condition:
# $organization is set as a string template variable by the module
expression: resource.matchTag('${organization}/context', 'project-factory')
title: Project factory org policy admin
```
</details>
### Resource management hierarchy
The folder hierarchy is managed via a filesystem tree of YAML configuration files, and leverages the [project factory module](../../../modules/project-factory-experimental/README.md#folder-hierarchy) implementation, which supports up to 3 levels of folders (4 or more can be easily implemented in the module if needed). The module documentation provides additional information on this factory usage and formats.
The default dataset implements a classic FAST layout, with top-level folders for stage 2 and stage 3, and can be easily tweaked by adding or removing any needed folder.
```bash
data/folders
├── networking
│   ├── .config.yaml
│   ├── dev
│   │   └── .config.yaml
│   └── prod
│   └── .config.yaml
├── security
│   └── .config.yaml
└── teams
└── .config.yaml
```
Different layouts are very easy to implement by simply modeling the desired hierarchy in the filesystem, and configuring each folder via `.config.yaml` files.
The project factory also supports embedding folder-aware project definitions in folders, but that approach is best used with caution to prevent potential race conditions when moving or deleting folders and projects.
As with the factories described above, context replacements can be used in folder configurations. Some examples are provided below.
<details>
<summary>Context-based replacement examples for the folder factory</summary>
#### Context-based replacement in the folders factory
As with other examples before, the main use case is to infer IAM principals from either the static or internally defined context. One additional context which is often useful here is tag values, which allows defining a scope for organization-level conditional IAM bindings or org policies.
```yaml
# file: data/folders/teams/.config.yaml
name: Teams
iam_by_principals:
$iam_principals:service_accounts/iac-0/iac-pf-rw:
- roles/owner
- roles/resourcemanager.folderAdmin
# [...]
tag_bindings:
context: $tag_values:context/project-factory
```
</details>
### Project factory
The project factory is managed via a set of YAML configuration files, which like folders leverages the [project factory module](../../../modules/project-factory-experimental/README.md#folder-hierarchy) implementation. The module documentation provides additional information on this factory usage and formats.
The default dataset implements a classic FAST layout, with two top-level projects for log exports and IaC resources. Those projects can easily be changed, for example rooting them in a folder by specifying the folder id or context name in their `parent` attribute.
The provided project configurations also create several key resources for the stage like log buckets, storage buckets, and service accounts. Context-based expansions for projects are very similar to the ones defined for folders, you can refer to the above section for details.
### CI/CD configuration
CI/CD support is implemented in a similar way to classic/legacy FAST, except for being driven by a factory tha points to a single file.
This allows defining a single Workload Identity provider that will be used to exchange external tokens for the pipelines, and one or more workflows that can interpolate internal (from the project factory) or external (user defined) attributes.
This is the default file which implements a workflow for this stage. To enable it, pass the file path to the `factories_config.cicd` variable.
```yaml
workload_identity_federation:
pool_name: iac-0
project: $project_ids:iac-0
providers:
github:
# the condition is optional but recommented, use your GitHub org name
attribute_condition: attribute.repository_owner=="my_org"
issuer: github
# custom_settings:
# issuer_uri:
# audiences: []
# jwks_json_path:
workflows:
bootstrap:
template: github
workload_identity_provider:
id: $wif_providers:github
audiences: []
repository:
name: bootstrap
branch: main
output_files:
storage_bucket: $storage_buckets:iac-0/iac-outputs
providers:
apply: $output_files:providers/0-bootstrap
plan: $output_files:providers/0-bootstrap-ro
files:
- tfvars/0-boostrap.auto.tfvars.json
service_accounts:
apply: $iam_principals:service_accounts/iac-0/iac-bootstrap-cicd-rw
plan: $iam_principals:service_accounts/iac-0/iac-bootstrap-cicd-ro
```
## Leveraging classic FAST Stages
Classic Fast stage 2 and 3 can be directly used after applying this if the [Classic FAST layout](#classic-fast-dataset) is used, or similar identities and permissions are implemented in a different design.
Specific changes or considerations needed for each stage are described below.
### VPC Service Controls
To use the predefined logging ingress policy in the VPC SC stage, define it like in the following example.
```yaml
from:
access_levels:
- "*"
identities:
- $identity_sets:logging_identities
to:
operations:
- service_name: "*"
resources:
- $project_numbers:log-0
```
### Security
Define values for the `var.environments` variable in a tfvars file.
<!-- TFDOC OPTS files:1 -->
<!-- BEGIN TFDOC -->
## Files
| name | description | modules | resources |
|---|---|---|---|
| [billing.tf](./billing.tf) | None | <code>billing-account</code> | |
| [cicd.tf](./cicd.tf) | None | | <code>google_iam_workload_identity_pool</code> · <code>google_iam_workload_identity_pool_provider</code> · <code>google_storage_bucket_object</code> · <code>local_file</code> |
| [factory.tf](./factory.tf) | None | <code>project-factory-experimental</code> | |
| [imports.tf](./imports.tf) | None | | |
| [main.tf](./main.tf) | Module-level locals and resources. | | <code>terraform_data</code> |
| [organization.tf](./organization.tf) | None | <code>organization</code> | |
| [output-files.tf](./output-files.tf) | None | | <code>google_storage_bucket_object</code> · <code>local_file</code> |
| [outputs.tf](./outputs.tf) | Module outputs. | | |
| [variables.tf](./variables.tf) | Module variables. | | |
| [wif-definitions.tf](./wif-definitions.tf) | Workload Identity provider definitions. | | |
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [bootstrap_user](variables.tf#L17) | Email of the nominal user running this stage for the first time. | <code>string</code> | | <code>null</code> |
| [context](variables.tf#L23) | Context-specific interpolations. | <code title="object&#40;&#123;&#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; locations &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; kms_keys &#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; service_account_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_keys &#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> |
| [factories_config](variables.tf#L43) | Configuration for the resource factories or external data. | <code title="object&#40;&#123;&#10; billing_accounts &#61; optional&#40;string, &#34;data&#47;billing-accounts&#34;&#41;&#10; cicd &#61; optional&#40;string&#41;&#10; defaults &#61; optional&#40;string, &#34;data&#47;defaults.yaml&#34;&#41;&#10; folders &#61; optional&#40;string, &#34;data&#47;folders&#34;&#41;&#10; organization &#61; optional&#40;string, &#34;data&#47;organization&#34;&#41;&#10; projects &#61; optional&#40;string, &#34;data&#47;projects&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [org_policies_imports](variables.tf#L57) | List of org policies to import. These need to also be defined in data files. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [iam_principals](outputs.tf#L17) | IAM principals. | |
| [locations](outputs.tf#L22) | Default locations. | |
| [projects](outputs.tf#L27) | Attributes for managed projects. | |
| [tfvars](outputs.tf#L32) | Stage tfvars. | |
<!-- END TFDOC -->

View File

@@ -1,73 +0,0 @@
/**
* 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 {
_billing_accounts_path = try(
pathexpand(var.factories_config.billing_accounts), null
)
_billing_accounts_raw = {
for f in try(fileset(local._billing_accounts_path, "*.yaml"), []) :
trimsuffix(f, ".yaml") => merge(
{ id = null },
yamldecode(file("${local._billing_accounts_path}/${f}"))
)
}
billing_accounts = {
for k, v in local._billing_accounts_raw : k => merge(v, {
id = (
local.defaults.billing_account != null && v.id == "$defaults:billing_account"
? local.defaults.billing_account
: v.id
)
logging_sinks = lookup(v, "logging_sinks", {})
}) if v.id != null
}
}
module "billing-accounts" {
source = "../../../modules/billing-account"
for_each = local.billing_accounts
id = each.value.id
context = merge(local.ctx, {
custom_roles = merge(
local.ctx.custom_roles, module.organization[0].custom_role_id
)
iam_principals = merge(
local.ctx.iam_principals,
module.factory.iam_principals
)
project_ids = merge(
local.ctx.project_ids, module.factory.project_ids
)
storage_buckets = module.factory.storage_buckets
tag_keys = merge(
local.ctx.tag_keys,
local.org_tag_keys
)
tag_values = merge(
local.ctx.tag_values,
local.org_tag_values
)
})
iam = lookup(each.value, "iam", {})
iam_by_principals = lookup(each.value, "iam_by_principals", {})
iam_bindings = lookup(each.value, "iam_bindings", {})
iam_bindings_additive = lookup(each.value, "iam_bindings_additive", {})
logging_sinks = {
for k, v in each.value.logging_sinks : k => v
if lookup(v, "destination", null) != null && lookup(v, "type", null) != null
}
}

View File

@@ -1,138 +0,0 @@
/**
* 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 {
_cicd = try(yamldecode(file(local.paths.cicd)), {})
_cicd_identity_providers = {
for k, v in google_iam_workload_identity_pool_provider.default :
"$wif_providers:${k}" => v.id
}
_cicd_output_files = {
for k, v in google_storage_bucket_object.providers :
"$output_files:providers/${k}" => v.name
}
cicd_project_ids = {
for k, v in merge(
var.context.project_ids, module.factory.project_ids
) : "$project_ids:${k}" => v
}
cicd_workflows = {
for k, v in lookup(local._cicd, "workflows", {}) : k => {
outputs_bucket = lookup(
local.of_buckets,
v.output_files.storage_bucket,
v.output_files.storage_bucket
)
workflow = templatefile("assets/workflow-${v.template}.yaml", {
identity_provider = lookup(
local._cicd_identity_providers,
v.workload_identity_provider.id,
v.workload_identity_provider.id
)
service_accounts = {
apply = lookup(
local.of_service_accounts,
v.service_accounts.apply,
v.service_accounts.apply
)
plan = lookup(
local.of_service_accounts,
v.service_accounts.plan,
v.service_accounts.plan
)
}
outputs_bucket = lookup(
local.of_buckets,
v.output_files.storage_bucket,
v.output_files.storage_bucket
)
stage_name = k
tf_providers_files = {
apply = lookup(
local._cicd_output_files,
v.output_files.providers.apply,
v.output_files.providers.apply
)
plan = lookup(
local._cicd_output_files,
v.output_files.providers.plan,
v.output_files.providers.plan
)
}
tf_var_files = try(v.output_files.files, [])
})
}
}
wif_project = try(local._cicd.workload_identity_federation.project, null)
wif_providers = {
for k, v in try(local._cicd.workload_identity_federation.providers, {}) :
k => merge(v, lookup(local.wif_defs, v.issuer, {}))
}
}
resource "google_iam_workload_identity_pool" "default" {
count = local.wif_project == null ? 0 : 1
project = lookup(
local.cicd_project_ids, local.wif_project, local.wif_project
)
workload_identity_pool_id = try(
local._cicd.workload_identity_federation.pool_name, "iac-0"
)
}
resource "google_iam_workload_identity_pool_provider" "default" {
for_each = local.wif_providers
project = (
google_iam_workload_identity_pool.default[0].project
)
workload_identity_pool_id = (
google_iam_workload_identity_pool.default[0].workload_identity_pool_id
)
workload_identity_pool_provider_id = each.key
attribute_condition = lookup(
each.value, "attribute_condition", null
)
attribute_mapping = lookup(
each.value, "attribute_mapping", {}
)
oidc {
# Setting an empty list configures allowed_audiences to the url of the provider
allowed_audiences = try(each.value.custom_settings.audiences, [])
# If users don't provide an issuer_uri, we set the public one for the platform chosen.
issuer_uri = (
try(each.value.custom_settings.issuer_uri, null) != null
? each.value.custom_settings.issuer_uri
: try(each.value.issuer_uri, null)
)
# OIDC JWKs in JSON String format. If no value is provided, they key is
# fetched from the `.well-known` path for the issuer_uri
jwks_json = try(each.value.custom_settings.jwks_json, null)
}
}
resource "local_file" "workflows" {
for_each = local.of_path == null ? {} : local.cicd_workflows
file_permission = "0644"
filename = "${local.of_path}/workflows/${each.key}.yaml"
content = each.value.workflow
}
resource "google_storage_bucket_object" "workflows" {
for_each = local.cicd_workflows
bucket = each.value.outputs_bucket
name = "workflows/${each.key}.yaml"
content = each.value.workflow
}

View File

@@ -1,96 +0,0 @@
/**
* 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 {
paths = {
for k, v in var.factories_config : k => try(pathexpand(v), null)
}
# fail if we have no valid defaults
_defaults = yamldecode(file(local.paths.defaults))
ctx = merge(var.context, {
iam_principals = local.iam_principals
locations = {
for k, v in local.defaults.locations :
k => v if k != "pubsub"
}
})
defaults = {
billing_account = try(local._defaults.global.billing_account, null)
locations = merge(try(local._defaults.global.locations, {}), {
bigquery = "eu"
logging = "global"
pubsub = []
storage = "eu"
})
organization = (
try(local._defaults.global.organization.id, null) == null
? null
: local._defaults.global.organization
)
prefix = try(
local.project_defaults.defaults.prefix,
local.project_defaults.overrides.prefix,
null
)
}
iam_principals = merge(
local.org_iam_principals,
var.context.iam_principals,
try(local._defaults.context.iam_principals, {})
)
output_files = {
local_path = try(local._defaults.output_files.local_path, null)
storage_bucket = try(local._defaults.output_files.storage_bucket, null)
providers = try(local._defaults.output_files.providers, {})
}
project_defaults = {
defaults = try(local._defaults.projects.defaults, {})
overrides = try(local._defaults.projects.overrides, {})
}
}
# TODO: streamine location replacements
resource "terraform_data" "precondition" {
lifecycle {
precondition {
condition = try(local.defaults.billing_account, null) != null
error_message = "No billing account set in global defaults."
}
precondition {
condition = (
local.organization_id != null ||
try(local.project_defaults.defaults.parent, null) != null ||
try(local.project_defaults.overrides.parent, null) != null
)
error_message = "Project parent must be set in project defaults or overrides if no organization id is set."
}
precondition {
condition = (
try(local.project_defaults.defaults.prefix, null) != null ||
try(local.project_defaults.overrides.prefix, null) != null
)
error_message = "Prefix must be set in project defaults or overrides."
}
precondition {
condition = (
try(local.project_defaults.defaults.storage_location, null) != null ||
try(local.project_defaults.overrides.storage_location, null) != null
)
error_message = "Storage location must be set in project defaults or overrides."
}
}
}

View File

@@ -1,125 +0,0 @@
/**
* 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 {
# prepare organization data
organization = merge(
# initialize required attributes
{ domain = null, id = null },
# merge defaults
lookup(local.defaults, "organization", {}),
# merge attributes defined in yaml
try(yamldecode(file("${local.paths.organization}/.config.yaml")), {})
)
# interpolate organization id if required
organization_id = (
local.organization.id == "$defaults:organization/id"
? try(local.defaults.organization.id, local.organization.id)
: local.organization.id
)
# build map of predefined groups if organization domain is set
org_iam_principals = local.organization.domain == null ? {} : {
domain = "domain:${local.organization.domain}"
gcp-billing-admins = "group:gcp-billing-admins@${local.organization.domain}"
gcp-devops = "group:gcp-devops@${local.organization.domain}"
gcp-network-admins = "group:gcp-network-admins@${local.organization.domain}"
gcp-organization-admins = "group:gcp-organization-admins@${local.organization.domain}"
gcp-secops-admins = "group:gcp-secops-admins@${local.organization.domain}"
gcp-security-admins = "group:gcp-security-admins@${local.organization.domain}"
gcp-support = "group:gcp-support@${local.organization.domain}"
}
org_tag_keys = {
for k, v in module.organization[0].tag_keys : k => v.id
}
org_tag_values = {
for k, v in module.organization[0].tag_values : k => v.id
}
}
module "organization" {
source = "../../../modules/organization"
count = local.organization_id != null ? 1 : 0
organization_id = "organizations/${local.organization_id}"
logging_settings = lookup(local.organization, "logging", null)
context = {
locations = {
default = local.defaults.locations.logging
}
}
factories_config = {
org_policy_custom_constraints = "${local.paths.organization}/custom-constraints"
custom_roles = "${local.paths.organization}/custom-roles"
tags = "${local.paths.organization}/tags"
}
tags_config = {
ignore_iam = true
}
}
module "organization-iam" {
source = "../../../modules/organization"
count = local.organization.id != null ? 1 : 0
organization_id = module.organization[0].id
context = merge(local.ctx, {
custom_roles = merge(
local.ctx.custom_roles, module.organization[0].custom_role_id
)
iam_principals = merge(
local.ctx.iam_principals,
module.factory.iam_principals
)
log_buckets = module.factory.log_buckets
org_policies = {
organization = local.defaults.organization
tags = merge(
local.ctx.tag_values,
local.org_tag_values
)
}
project_ids = merge(
local.ctx.project_ids, module.factory.project_ids
)
storage_buckets = module.factory.storage_buckets
tag_keys = merge(
local.ctx.tag_keys,
local.org_tag_keys
)
tag_values = merge(
local.ctx.tag_values,
local.org_tag_values
)
})
factories_config = {
org_policies = "${local.paths.organization}/org-policies"
tags = "${local.paths.organization}/tags"
}
iam = lookup(
local.organization, "iam", {}
)
iam_by_principals = lookup(
local.organization, "iam_by_principals", {}
)
iam_bindings = lookup(
local.organization, "iam_bindings", {}
)
iam_bindings_additive = lookup(
local.organization, "iam_bindings_additive", {}
)
logging_sinks = try(local.organization.logging.sinks, {})
tags_config = {
force_context_ids = true
}
}

View File

@@ -1,35 +0,0 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
output "iam_principals" {
description = "IAM principals."
value = local.iam_principals
}
output "locations" {
description = "Default locations."
value = local.defaults.locations
}
output "projects" {
description = "Attributes for managed projects."
value = module.factory.projects
}
output "tfvars" {
description = "Stage tfvars."
value = local.of_tfvars
}

View File

@@ -1,18 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Custom Role",
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"includedPermissions": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[a-zA-Z-]+\\.[a-zA-Z-]+\\.[a-zA-Z-]+$"
}
}
}
}

View File

@@ -1,76 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Organization Policies",
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z-]+[a-zA-Z0-9\\.]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"inherit_from_parent": {
"type": "boolean"
},
"reset": {
"type": "boolean"
},
"rules": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"allow": {
"$ref": "#/$defs/allow-deny"
},
"deny": {
"$ref": "#/$defs/allow-deny"
},
"enforce": {
"type": "boolean"
},
"condition": {
"type": "object",
"additionalProperties": false,
"properties": {
"description": {
"type": "string"
},
"expression": {
"type": "string"
},
"location": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"parameters": {
"type": "string"
}
}
}
}
}
}
},
"$defs": {
"allow-deny": {
"type": "object",
"additionalProperties": false,
"properties": {
"all": {
"type": "boolean"
},
"values": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}

View File

@@ -1,62 +0,0 @@
/**
* 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 "bootstrap_user" {
description = "Email of the nominal user running this stage for the first time."
type = string
default = null
}
variable "context" {
description = "Context-specific interpolations."
type = object({
custom_roles = optional(map(string), {})
folder_ids = optional(map(string), {})
iam_principals = optional(map(string), {})
locations = optional(map(string), {})
kms_keys = optional(map(string), {})
notification_channels = optional(map(string), {})
project_ids = optional(map(string), {})
service_account_ids = optional(map(string), {})
tag_keys = optional(map(string), {})
tag_values = optional(map(string), {})
vpc_host_projects = optional(map(string), {})
vpc_sc_perimeters = optional(map(string), {})
})
default = {}
nullable = false
}
variable "factories_config" {
description = "Configuration for the resource factories or external data."
type = object({
billing_accounts = optional(string, "data/billing-accounts")
cicd = optional(string)
defaults = optional(string, "data/defaults.yaml")
folders = optional(string, "data/folders")
organization = optional(string, "data/organization")
projects = optional(string, "data/projects")
})
nullable = false
default = {}
}
variable "org_policies_imports" {
description = "List of org policies to import. These need to also be defined in data files."
type = list(string)
nullable = false
default = []
}

View File

@@ -1,4 +1,4 @@
FAST_STAGE_DESCRIPTION="FAST Bootstrap." FAST_STAGE_DESCRIPTION="organization bootstrap"
FAST_STAGE_LEVEL=0 FAST_STAGE_LEVEL=0
FAST_STAGE_NAME=bootstrap FAST_STAGE_NAME=bootstrap
# FAST_STAGE_DEPS="0-globals 0-bootstrap" # FAST_STAGE_DEPS="0-globals 0-bootstrap"

View File

@@ -0,0 +1,726 @@
# Organization bootstrap (legacy)
The primary purpose of this stage is to enable critical organization-level functionalities that depend on broad administrative permissions, and prepare the prerequisites needed to enable automation in this and future stages.
It is intentionally simple, to minimize usage of administrative-level permissions and enable simple auditing and troubleshooting, and only deals with three sets of resources:
- project, service accounts, and GCS buckets for automation
- projects, BQ datasets, and sinks for audit log and billing exports
- IAM bindings on the organization
Use the following diagram as a simple high level reference for the following sections, which describe the stage and its possible customizations in detail.
<p align="center">
<img src="diagram.png" alt="Organization-level diagram">
</p>
<!-- BEGIN TOC -->
- [Design overview and choices](#design-overview-and-choices)
- [User groups](#user-groups)
- [Organization-level IAM](#organization-level-iam)
- [Organization policies](#organization-policies)
- [Security Command Center Enterprise](#security-command-center-enterprise)
- [Tags and Organization Policy conditions](#tags-and-organization-policy-conditions)
- [Automation project and resources](#automation-project-and-resources)
- [Billing account](#billing-account)
- [Organization-level logging](#organization-level-logging)
- [Naming](#naming)
- [Workforce Identity Federation](#workforce-identity-federation)
- [Workload Identity Federation and CI/CD](#workload-identity-federation-and-cicd)
- [How to run this stage](#how-to-run-this-stage)
- [Prerequisites](#prerequisites)
- [Standalone billing account](#standalone-billing-account)
- [Preventing creation of billing-related IAM bindings](#preventing-creation-of-billing-related-iam-bindings)
- [Groups](#groups)
- [Configure variables](#configure-variables)
- [Output files and cross-stage variables](#output-files-and-cross-stage-variables)
- [Running the stage](#running-the-stage)
- [Customizations](#customizations)
- [Group names](#group-names)
- [IAM](#iam)
- [Log sinks and log destinations](#log-sinks-and-log-destinations)
- [Names and naming convention](#names-and-naming-convention)
- [Workload Identity Federation](#workload-identity-federation)
- [Project folders](#project-folders)
- [CI/CD repositories](#cicd-repositories)
- [Add-ons](#add-ons)
- [Files](#files)
- [Variables](#variables)
- [Outputs](#outputs)
<!-- END TOC -->
## Design overview and choices
As mentioned above, this stage only does the bare minimum required to bootstrap automation, and ensure that base audit and billing exports are in place from the start to provide some measure of accountability, even before the security configurations are applied in a later stage.
It also sets up organization-level IAM bindings so the Organization Administrator role is only used here, trading off some design freedom for ease of auditing and troubleshooting, and reducing the risk of costly security mistakes down the line. The only exception to this rule is for the [Resource Management stage](../1-resman-legacy) service account, described below.
### User groups
User groups are important, not only here but throughout the whole automation process. They provide a stable frame of reference that allows decoupling the final set of permissions for each group, from the stage where entities and resources are created and their IAM bindings defined. For example, the final set of roles for the networking group is contributed by this stage at the organization level (XPN Admin, Cloud Asset Viewer, etc.), and by the Resource Management stage at the folder level.
We have standardized the initial set of groups on those outlined in the [GCP Enterprise Setup Checklist](https://cloud.google.com/docs/enterprise/setup-checklist) to simplify adoption. They provide a comprehensive and flexible starting point that can suit most users. Adding new groups, or deviating from the initial setup is possible and reasonably simple, and it's briefly outlined in the customization section below.
### Organization-level IAM
The service account used in the [Resource Management stage](../1-resman-legacy) needs to be able to grant specific permissions at the organizational level, to enable specific functionality for subsequent stages that deal with network or security resources, or billing-related activities.
In order to be able to assign those roles without having the full authority of the Organization Admin role, this stage defines a custom role that only allows setting IAM policies on the organization, and grants it via a [delegated role grant](https://cloud.google.com/iam/docs/setting-limits-on-granting-roles) that only allows it to be used to grant a limited subset of roles.
In this way, the Resource Management service account can effectively act as an Organization Admin, but only to grant the specific roles it needs to control.
One consequence of the above setup is the need to configure IAM bindings that can be assigned via the condition as non-authoritative, since those same roles are effectively under the control of two stages: this one and Resource Management. Using authoritative bindings for these roles (instead of non-authoritative ones) would generate potential conflicts, where each stage could try to overwrite and negate the bindings applied by the other at each `apply` cycle.
A full reference of IAM roles managed by this stage [is available here](./IAM.md).
### Organization policies
It's often desirable to have organization policies deployed before any other resource in the org, so as to ensure compliance with specific requirements (e.g. location restrictions), or control the configuration of specific resources (e.g. default network at project creation or service account grants).
To cover this use case, organization policies have been moved from the resource management to the bootstrap stage in FAST versions after 26.0.0. They are managed via the usual factory approach, and a [sample set of data files](./data/org-policies/) is included with this stage. They are not applied during the initial run when the `bootstrap_user` variable is set, to work around incompatibilities with user credentials.
FAST uses unmanaged organization policies by default. For those who prefer managed policies, a separate sample set is available. To use these managed policies, configure `factories_config` as shown below.
```tfvars
factories_config = {
org_policies = "data/org-policies-managed"
}
```
#### Security Command Center Enterprise
The DRS policy mentioned above might make it complex to [enable Security Command Center Enterprise](https://cloud.google.com/security-command-center/docs/activate-enterprise-tier#verify_organization_policies). If this is the case, you can temporarily disable it via the Cloud Console, enable SCC Enterprise, then re-enable the policy.
#### Tags and Organization Policy conditions
Organization policy exceptions are managed via a dedicated resource management tag hierarchy, rooted in the `org-policies` tag key. A default condition is already present for the the `iam.allowedPolicyMemberDomains` constraint, that relaxes the policy on resources that have the `org-policies/allowed-policy-member-domains-all` tag value bound or inherited, and similarly for `essentialcontacts.allowedContactDomains` via the `allowed-essential-contacts-domains-all` tag value.
Further tag values can be defined via the `org_policies_config.tag_values` variable, and IAM access can be granted on them via the same variable. Once a tag value has been created, its id can be used in constraint rule conditions. Note that only one tag value from a given tag key can be bound to a node (organization, folder, or project) in the resource hierarchy. Since these tag values are all rooted in the `org-policies` key, this limits the ability to apply fine-grained policy constraints. It may be more desirable to model policy overrides using coarser groups of tag values to create a policy "profile". For example, instead of separating `compute.skipDefaultNetworkCreation` and `compute.vmExternalIpAccess`, enforce both constraints by default and relax them both using the same tag value such as `sandbox`. See [tags overview](https://cloud.google.com/resource-manager/docs/tags/tags-overview) for more information.
Management of the rest of the tag hierarchy is delegated to the resource management stage, as that is often intimately tied to the folder hierarchy design.
The organization policy tag key and values managed by this stage have been added to the `0-bootstrap.auto.tfvars` stage, so that IAM can be delegated to the resource management or successive stages via their ids.
The following example shows an example on how to define an additional tag value, and use it in a boolean constraint rule.
This snippet defines a new tag value under the `org-policies` tag key via the `org_policies_config` variable, and assigns the permission to bind it to a group.
```hcl
# stage 0 custom tfvars
org_policies_config = {
tag_values = {
compute-require-oslogin-false = {
description = "Bind this tag to set oslogin to false."
iam = {
"roles/resourcemanager.tagUser" = [
"group:foo@example.com"
]
}
}
}
}
# tftest skip
```
The above tag can be used to define a constraint condition via the `data/org-policies/compute.yaml` or similar factory file. The name of the tag can be referenced from the factory files using `tags.org_policies_config`, as shown below.
```yaml
compute.requireOsLogin:
rules:
- enforce: true
- enforce: false
condition:
expression: resource.matchTag('${tags.org_policies_tag_name}', 'compute-require-oslogin-false')
```
### Automation project and resources
One other design choice worth mentioning here is using a single automation project for all foundational stages. We trade off some complexity on the API side (single source for usage quota, multiple service activation) for increased flexibility and simpler operations, while still effectively providing the same degree of separation via resource-level IAM.
### Billing account
We support three use cases in regards to billing:
- the billing account is part of this same organization, IAM bindings will be set at the organization level
- the billing account is not considered part of an organization (even though it might be), billing IAM bindings are set on the billing account itself
- billing IAM is managed separately, and no bindings should (or can) be set via Terraform, this requires a few extra steps and is definitely not recommended and mainly used for development purposes
For same-organization billing, we configure a custom organization role that can set IAM bindings, via a delegated role grant to limit its scope to the relevant roles.
For details on configuring the different billing account modes, refer to the [How to run this stage](#how-to-run-this-stage) section below.
Because of limitations of API availability, manual steps have to be followed to enable billing export within billing project to BigQuery dataset `billing_export` which will be created as part of the bootstrap stage. The process to share billing data [is outlined here](https://cloud.google.com/billing/docs/how-to/export-data-bigquery-setup#enable-bq-export).
### Organization-level logging
We create organization-level log sinks early in the bootstrap process to ensure a proper audit trail is in place from the very beginning. By default, we provide log filters to capture [Cloud Audit Logs](https://cloud.google.com/logging/docs/audit), [VPC Service Controls violations](https://cloud.google.com/vpc-service-controls/docs/troubleshooting#vpc-sc-errors) and [Workspace Logs](https://cloud.google.com/logging/docs/audit/configure-gsuite-audit-logs) into logging buckets in the top-level audit logging project.
An organization-level sink captures IAM data access logs, including authentication and impersonation events for service accounts. To manage logging costs, the default configuration enables IAM data access logging only within the automation project (where sensitive service accounts reside). For enhanced security across the entire organization, consider enabling these logs at the organization level.
The [Customizations](#log-sinks-and-log-destinations) section explains how to change the logs captured and their destination.
### Naming
We are intentionally not supporting random prefix/suffixes for names, as that is an antipattern typically only used in development. It does not map to our customer's actual production usage, where they always adopt a fixed naming convention.
What is implemented here is a fairly common convention, composed of tokens ordered by relative importance:
- an organization-level static prefix less or equal to 9 characters (e.g. `myco` or `myco-gcp`)
- an optional tenant-level prefix, if using tenant factory
- an environment identifier (e.g. `prod`)
- a team/owner identifier (e.g. `sec` for Security)
- a context identifier (e.g. `core` or `kms`)
- an arbitrary identifier used to distinguish similar resources (e.g. `0`, `1`)
> [!WARNING]
> When using tenant factory, a tenant prefix will be automatically generated as `{prefix}-{tenant-shortname}`. The maximum length of such prefix must be 11 characters or less, which means that the longer org-level prefix you use, the less chars you'll have available for the `tenant-shortname`.
Tokens are joined by a `-` character, making it easy to separate the individual tokens visually, and to programmatically split them in billing exports to derive initial high-level groupings for cost attribution.
The convention is used in its full form only for specific resources with globally unique names (projects, GCS buckets). Other resources adopt a shorter version for legibility, as the full context can always be derived from their project.
The [Customizations](#names-and-naming-convention) section on names below explains how to configure tokens, or implement a different naming convention.
### Workforce Identity Federation
This stage supports configuration of [Workforce Identity Federation](https://cloud.google.com/iam/docs/workforce-identity-federation) which lets an external identity provider (IdP) to authenticate and authorize a group of users (usually employees) using IAM, so that the users can access Google Cloud services.
The following example shows an example on how to define a Workforce Identity pool for the organization.
```hcl
# stage 0 wif tfvars
workforce_identity_providers = {
test = {
issuer = "azuread"
display_name = "wif-provider"
description = "Workforce Identity pool"
saml = {
idp_metadata_xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>..."
}
}
}
# tftest skip
```
### Workload Identity Federation and CI/CD
This stage also implements initial support for two interrelated features
- configuration of [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) pools and providers
- configuration of CI/CD repositories to allow impersonation via Workload identity Federation, and stage running via provided workflow templates
Workload Identity Federation support allows configuring external providers independently from CI/CD, and offers predefined attributes for a few well known ones (more can be easily added by editing the `identity-providers-wlif.tf` file). Once providers have been configured their names are passed to the following stages via interface outputs, and can be leveraged to set up access or impersonation in IAM bindings.
CI/CD support is fully implemented for GitHub, Gitlab, and Cloud Source Repositories / Cloud Build. For GitHub, we also offer a [separate supporting setup](../../extras/0-cicd-github/) to quickly create / configure repositories. The same applies for Gitlab with the [following extra stage](../../extras/0-cicd-gitlab/).
<!-- TODO: add a general overview of our design -->
For details on how to configure both features, refer to the Customizations sections below on [Workload Identity Federation](#workload-identity-federation) and [CI/CD repositories](#cicd-repositories).
These features are optional and only enabled if the relevant variables have been populated.
## How to run this stage
This stage has straightforward initial requirements, as it is designed to work on newly created GCP organizations. Four steps are needed to bring up this stage:
- an Organization Admin self-assigns the required roles listed below
- the same administrator runs the first `init/apply` sequence passing a special variable to `apply`
- the providers configuration file is derived from the Terraform output or linked from the generated file
- a second `init` is run to migrate state, and from then on, the stage is run via impersonation
### Prerequisites
The roles that the Organization Admin used in the first `apply` needs to self-grant are:
- Billing Account Administrator (`roles/billing.admin`)
either on the organization or the billing account (see the following section for details)
- Logging Admin (`roles/logging.admin`)
- Organization Role Administrator (`roles/iam.organizationRoleAdmin`)
- Organization Administrator (`roles/resourcemanager.organizationAdmin`)
- Project Creator (`roles/resourcemanager.projectCreator`)
- Tag Admin (`roles/resourcemanager.tagAdmin`)
- Owner (`roles/owner`)
To quickly self-grant the above roles, run the following code snippet as the initial Organization Admin:
```bash
# set variable for current logged in user
export FAST_BU=$(gcloud config list --format 'value(core.account)')
# find and set your org id
gcloud organizations list
export FAST_ORG_ID=123456
# set needed roles
export FAST_ROLES="roles/billing.admin roles/logging.admin \
roles/iam.organizationRoleAdmin roles/resourcemanager.projectCreator \
roles/resourcemanager.organizationAdmin roles/resourcemanager.tagAdmin \
roles/owner"
for role in $FAST_ROLES; do
gcloud organizations add-iam-policy-binding $FAST_ORG_ID \
--member user:$FAST_BU --role $role --condition None
done
```
Then make sure the same user is also part of the `gcp-organization-admins` group so that impersonating the automation service account later on will be possible.
#### Standalone billing account
If you are using a standalone billing account, the identity applying this stage for the first time needs to be a billing account administrator:
```bash
export FAST_BILLING_ACCOUNT_ID=ABCD-01234-ABCD
gcloud beta billing accounts add-iam-policy-binding $FAST_BILLING_ACCOUNT_ID \
--member user:$FAST_BU --role roles/billing.admin
```
#### Preventing creation of billing-related IAM bindings
This configuration is possible but unsupported and only present for development purposes, use at your own risk:
- configure `billing_account.id` as `null` and `billing_account.no_iam` to `true` in your `tfvars` file
- apply with `terraform apply -target 'module.automation-project.google_project.project[0]'` in addition to the initial user variable
- once Terraform raises an error run `terraform untaint 'module.automation-project.google_project.project[0]'`
- repeat the two steps above for `'module.log-export-project.google_project.project[0]'`
- go through the process to associate the billing account with the two projects
- configure `billing_account.id` with the real billing account id
- resume applying normally
#### Groups
Before the first run, the following IAM groups must exist to allow IAM bindings to be created (actual names are flexible, see the [Customization](#customizations) section):
- `gcp-billing-admins`
- `gcp-devops`
- `gcp-vpc-network-admins`
- `gcp-organization-admins`
- `gcp-security-admins`
You can refer to [this animated image](./groups.gif) for a step by step on group creation via the [Google Cloud Enterprise Checklist](https://cloud.google.com/docs/enterprise/setup-checklist).
Please note that not all groups defined by the Checklist are actually used by FAST, as our approach to IAM is slightly different. As an example, we do not centralize monitoring functions as in our experience those are typically domain-specific (e.g. networking or application-level), so we don't leverage the corresponding groups. You are free of course to create those groups via the Checklist, and assign them roles via the IAM variables exposed by this stage.
One more difference compared to the Checklist is the use in FAST of an additional group to centralize support functions like viewing tickets and accessing logging and monitoring data. To remain consistent with the [Google Cloud Enterprise Checklist](https://cloud.google.com/docs/enterprise/setup-checklist) we map these permissions to the `gcp-devops` group by default. However, we recommend creating a dedicated `gcp-support` group and updating the `groups` variable with the right value.
#### Configure variables
Then make sure you have configured the correct values for the following variables by providing a `terraform.tfvars` file:
- `billing_account`
an object containing `id` as the id of your billing account, derived from the Cloud Console UI or by running `gcloud beta billing accounts list`, and the `is_org_level` flag that controls whether organization or account-level bindings are used, and a billing export project and dataset are created
- `groups`
the name mappings for your groups, if you're following the default convention you can leave this to the provided default
- `organization.id`, `organization.domain`, `organization.customer_id`
the id, domain and customer id of your organization, derived from the Cloud Console UI or by running `gcloud organizations list`
- `prefix`
the fixed org-level prefix used in your naming, maximum 9 characters long. Note that if you are using multitenant stages, then you will later need to configure a `tenant prefix`.
This `tenant prefix` can have a maximum length of 2 characters,
plus any unused characters from the from the `prefix`.
For example, if you specify a `prefix` that is 7 characters long,
then your `tenant prefix` can have a maximum of 4 characters.
You can also adapt the example that follows to your needs:
```tfvars
# use `gcloud beta billing accounts list`
# if you have too many accounts, check the Cloud Console :)
billing_account = {
id = "012345-67890A-BCDEF0"
}
# use `gcloud organizations list`
organization = {
domain = "example.org"
id = 1234567890
customer_id = "C000001"
}
# local path to store tfvars/provider outputs generated by this stage
outputs_location = "~/fast-config"
# locations for GCS, BigQuery, and logging buckets created here
locations = {
bq = "EU"
gcs = "EU"
logging = "global"
pubsub = []
}
# use something unique and no longer than 9 characters
prefix = "abcd"
```
### Output files and cross-stage variables
Each foundational FAST stage generates provider configurations and variable files can be consumed by the following stages, and saves them in a dedicated GCS bucket in the automation project. These files are a handy way to simplify stage configuration, and are also used by our CI/CD workflows to configure the repository files in the pipelines that validate and apply the code.
Alongside the GCS stored files, you can also configure a second copy to be saves on the local filesystem, as a convenience when developing or bringing up the infrastructure before a proper CI/CD setup is in place.
This second set of files is disabled by default, you can enable it by setting the `outputs_location` variable to a valid path on a local filesystem, e.g.
```tfvars
outputs_location = "~/fast-config"
```
Once the variable is set, `apply` will generate and manage providers and variables files, including the initial one used for this stage after the first run. You can then link these files in the relevant stages, instead of manually transferring outputs from one stage, to Terraform variables in another.
Below is the outline of the output files generated by all stages, which is identical for both the GCS and local filesystem copies:
```bash
[path specified in outputs_location]
├── providers
│   ├── 0-bootstrap-providers.tf
│   ├── 1-resman-providers.tf
│   ├── 2-networking-providers.tf
│   ├── 2-security-providers.tf
│   ├── 2-project-factory-dev-providers.tf
│   ├── 2-project-factory-prod-providers.tf
│   └── 9-sandbox-providers.tf
└── tfvars
│ ├── 0-bootstrap.auto.tfvars.json
│ ├── 1-resman.auto.tfvars.json
│ ├── 2-networking.auto.tfvars.json
│ └── 2-security.auto.tfvars.json
└── workflows
└── [optional depending on the configured CI/CD repositories]
```
### Running the stage
Before running `init` and `apply`, check your environment so no extra variables that might influence authentication are present (e.g. `GOOGLE_IMPERSONATE_SERVICE_ACCOUNT`). In general you should use user application credentials, and FAST will then take care to provision automation identities and configure impersonation for you.
When running the first `apply` as a user, you need to pass a special runtime variable so that the user roles are preserved when setting IAM bindings.
```bash
terraform init
terraform apply \
-var bootstrap_user=$(gcloud config list --format 'value(core.account)')
```
> If you see an error related to project name already exists, please make sure the project name is unique or the project was not deleted recently
Once the initial `apply` completes successfully, configure a remote backend using the new GCS bucket, and impersonation on the automation service account for this stage. To do this you can use the generated `providers.tf` file from either
- the local filesystem if you have configured output files as described above
- the GCS bucket where output files are always stored
- Terraform outputs (not recommended as it's more complex)
The following two snippets show how to leverage the `fast-links.sh` script in the FAST stages folder to fetch the commands required for output files linking or copying, using either the local output folder configured via Terraform variables, or the GCS bucket which can be derived from the `automation` output.
```bash
../fast-links.sh ~/fast-config
# File linking commands for organization bootstrap stage
# provider file
ln -s ~/fast-config/fast-test-00/providers/0-bootstrap-providers.tf ./
# conventional place for stage tfvars (manually created)
ln -s ~/fast-config/fast-test-00/0-bootstrap.auto.tfvars ./
```
```bash
../fast-links.sh gs://xxx-prod-iac-core-outputs-0
# File linking commands for organization bootstrap stage
# provider file
gcloud storage cp gs://xxx-prod-iac-core-outputs-0/providers/0-bootstrap-providers.tf ./
# conventional place for stage tfvars (manually created)
gcloud storage cp gs://xxx-prod-iac-core-outputs-0/0-bootstrap.auto.tfvars ./
```
- important for CI/CD
The `0-bootstrap.auto.tfvars` file is a crucial component of the CI/CD pipeline and must be manually created. This file is essentially the `terraform.tfvars` file renamed to avoid being ignored in version control systems like GitHub or GitLab, where `terraform.tfvars` is often included in `.gitignore`. By renaming it and committing `0-bootstrap.auto.tfvars` to your source control, you ensure that the necessary configurations are available in the pipeline.
Copy/paste the command returned by the script to link or copy the provider file, then migrate state with `terraform init` and run `terraform apply`. If your organization was created with "Secure by Default Org Policy", that is with some of the org policies enabled, add `-var 'org_policies_config={"import_defaults": true}'` to `terraform apply`:
```bash
terraform init -migrate-state
terraform apply
```
or
```bash
terraform init -migrate-state
terraform apply -var 'org_policies_config={"import_defaults": true}'
```
if there default policies are enabled.
Make sure the user you're logged in with is a member of the `gcp-organization-admins` group or impersonation will not be possible.
## Customizations
Most variables (e.g. `billing_account` and `organization`) are only used to input actual values and should be self-explanatory. The only meaningful customizations that apply here are groups, and IAM roles.
### Group names
As we mentioned above, groups reflect the convention used in the [GCP Enterprise Setup Checklist](https://cloud.google.com/docs/enterprise/setup-checklist), with an added level of indirection: the `groups` variable maps logical names to actual names, so that you don't need to delve into the code if your group names do not comply with the checklist convention.
For example, if your network admins team is called `net-rockstars@example.com`, simply set that name in the variable, minus the domain which is interpolated internally with the organization domain:
```hcl
variable "groups" {
description = "Group names to grant organization-level permissions."
type = map(string)
default = {
gcp-network-admins = "net-rockstars"
# [...]
}
}
# tftest skip
```
If your groups layout differs substantially from the checklist, define all relevant groups in the `groups` variable, then rearrange IAM roles in the code to match your setup.
### IAM
One other area where we directly support customizations is IAM. The code here, as in all stages, follows a simple pattern derived from best practices:
- operational roles for humans are assigned to groups
- any other principal is a service account
In code, the distinction above reflects on how IAM bindings are specified in the underlying module variables:
- group roles "for humans" always use `iam_by_principals` variables
- service account roles always use `iam` variables
This makes it easy to tweak user roles by adding mappings to the `iam_by_principals` variables of the relevant resources, without having to understand and deal with the details of service account roles.
One more critical difference in IAM bindings is between authoritative and additive:
- authoritative bindings have complete control on principals for a given role; this is the recommended best practice when a single automation actor controls the role, as it removes drift each time Terraform runs
- additive bindings have control only on given role/principal pairs, and need to be used whenever multiple automation actors need to control the role, as is the case for the network user role in Shared VPC setups, and many other situations
This stage groups all IAM definitions in the [organization-iam.tf](./organization-iam.tf) file, to allow easy parsing of roles assigned to each group and machine identity.
When customizations are needed, three stage-level variables allow injecting additional bindings to match the desired setup:
- `iam_by_principals` allows adding authoritative bindings for groups
- `iam` allows adding authoritative bindings for any type of supported principal, and is merged with the internal `iam` local and then with group bindings at the module level
- `iam_bindings_additive` allows adding individual role/member pairs, and also supports IAM conditions
Refer to the [project module](../../../modules/project/) for examples on how to use the IAM variables, and they are an interface shared across all our modules.
### Log sinks and log destinations
You can customize organization-level logs through the `log_sinks` variable in two ways:
- creating additional log sinks to capture more logs
- changing the destination of captured logs
By default, all logs are exported to a log bucket, but FAST can create sinks to BigQuery, GCS, or PubSub.
If you need to capture additional logs, please refer to GCP's documentation on [scenarios for exporting logging data](https://cloud.google.com/architecture/exporting-stackdriver-logging-for-security-and-access-analytics), where you can find ready-made filter expressions for different use cases.
When using Pubsub or BigQuery destinations, make sure the read-only stage service account (`prefix-prod-bootstrap-0r@prefix-prod-iac-core-0.iam.gserviceaccount.com`) has the necessary permissions to view destination resources. You can add them manually via the authoritative `iam` or the additive `iam_bindings_additive` variables. Refer to issue [#2540](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/issues/2540) for a discussion on this topic, and simple commands to verify proper permissions have been added.
### Names and naming convention
Configuring the individual tokens for the naming convention described above, has varying degrees of complexity:
- the static prefix can be set via the `prefix` variable once
- the environment identifier is set to `prod` as resources here influence production and are considered as such, and can be changed in `main.tf` locals
All other tokens are set directly in resource names, as providing abstractions to manage them would have added too much complexity to the code, making it less readable and more fragile.
If a different convention is needed, identify names via search/grep (e.g. with `^\s+name\s+=\s+"`) and change them in an editor: it should take a couple of minutes at most, as there's just a handful of modules and resources to change.
Names used in internal references (e.g. `module.foo-prod.id`) are only used by Terraform and do not influence resource naming, so they are best left untouched to avoid having to debug complex errors.
### Workload Identity Federation
At any time during this stage's lifecycle you can configure a Workload Identity Federation pool, and one or more providers. These are part of this stage's interface, included in the automatically generated `.tfvars` files and accepted by the Resource Managent stage that follows.
The variable maps each provider's `issuer` attribute with the definitions in the `identity-providers-wlif.tf` file. We currently support GitHub and Gitlab directly, and extending to definitions to support more providers is trivial (send us a PR if you do!).
Provider key names are used by the `cicd_repositories` variable to configure authentication for CI/CD repositories, and generally from your Terraform code whenever you need to configure IAM access or impersonation for federated identities.
This is a sample configuration of a GitHub and a Gitlab provider. Every parameter is optional.
The `custom_settings` attributes are used to configure the provider to work with privately managed installations of Github and Gitlab:
- `issuer_uri` (defaults to the public platforms one if not set)
- `audience` (defaults to the public URL of the provider if not set, as recommended in the [WIF FAQ section](https://cloud.google.com/iam/docs/best-practices-for-using-workload-identity-federation#provider-audience))
- `jwks_json` for public key upload
```tfvars
workload_identity_providers = {
# Use the public GitHub and specify an attribute condition
github-public-sample = {
attribute_condition = "attribute.repository_owner==\"my-github-org\""
issuer = "github"
}
# Use a private instance of Gitlab and specify a custom issuer_uri
gitlab-private-sample = {
issuer = "gitlab"
custom_settings = {
issuer_uri = "https://gitlab.fast.example.com"
}
}
# Use a private instance of Gitlab.
# Specify a custom audience and a custom issuer_uri
gitlab-private-aud-sample = {
attribute_condition = "attribute.namespace_path==\"my-gitlab-org\""
issuer = "gitlab"
custom_settings = {
audiences = ["https://gitlab.fast.example.com"]
issuer_uri = "https://gitlab.fast.example.com"
}
}
}
```
### Project folders
By default this stage creates all its projects directly under the orgaization node. If desired, projects can be moved under a folder using the `project_parent_ids` variable.
```tfvars
project_parent_ids = {
automation = "folders/1234567890"
billing = "folders/9876543210"
logging = "folders/1234567890"
}
```
### CI/CD repositories
FAST is designed to directly support running in automated workflows from separate repositories for each stage. The `cicd_repositories` variable allows you to configure impersonation from external repositories leveraging Workload identity Federation, and pre-configures a FAST workflow file that can be used to validate and apply the code in each repository.
The repository design we support is fairly simple, with a repository for modules that enables centralization and versioning, and one repository for each stage optionally configured from the previous stage.
This is an example of configuring the bootstrap and resource management repositories in this stage. CI/CD configuration is optional, so the entire variable or any of its attributes can be set to null if not needed.
```tfvars
cicd_config = {
bootstrap = {
identity_provider = "github-sample"
repository = {
branch = null
name = "my-gh-org/fast-bootstrap"
type = "github"
}
}
resman = {
identity_provider = "github-sample"
repository = {
branch = "main"
name = "my-gh-org/fast-resman"
type = "github"
}
}
}
```
The `type` attribute can be set to one of the supported repository types: `github` or `gitlab`.
Once the stage is applied the generated output files will contain pre-configured workflow files for each repository, that will use Workload Identity Federation via a dedicated service account for each repository to impersonate the automation service account for the stage.
You can use Terraform to automate creation of the repositories using the extra stage defined in [fast/extras/0-cicd-github](../../extras/0-cicd-github/) (only for Github for now).
The remaining configuration is manual, as it regards the repositories themselves:
- create a repository for modules
- clone and populate it with the Fabric modules
- configure authentication to the modules repository
- for GitHub
- create a key pair
- create a [deploy key](https://docs.github.com/en/developers/overview/managing-deploy-keys#deploy-keys) in the modules repository with the public key
- create a `CICD_MODULES_KEY` secret with the private key in each of the repositories that need to access modules (for Gitlab, please Base64 encode the private key for masking)
- for Gitlab
- TODO
- for Source Repositories
- assign the reader role to the CI/CD service accounts
- create one repository for each stage
- do an initial apply cycle for the stage so that state exists
- clone and populate them with the stage source
- edit the modules source to match your modules repository
- a simple way is using the "Replace in files" function of your editor
- search for `source\s*= "../../../modules/([^"]+)"`
- replace with:
- modules stored on GitHub: `source = "git@github.com:my-org/fast-modules.git//$1?ref=v1.0"`
- modules stored on Gitlab: `source = "git::ssh://git@gitlab.com/my-org/fast-modules.git//$1?ref=v1.0"`
- modules stored on Source Repositories: `"source = git::https://source.developers.google.com/p/my-project/r/my-repository//$1?ref=v1.0"`. You may need to run `git config --global credential.'https://source.developers.google.com'.helper gcloud.sh` first as documented [here](https://cloud.google.com/source-repositories/docs/adding-repositories-as-remotes#add_the_repository_as_a_remote)
- copy the generated workflow file for the stage from the GCS output files bucket or from the local clone if enabled
- for GitHub, place it in a `.github/workflows` folder in the repository root
- for Gitlab, rename it to `.gitlab-ci.yml` and place it in the repository root
- for Source Repositories, place it in `.cloudbuild/workflow.yaml`
- To prevent the creation of local files in the CI/CD pipeline, comment out the `outputs_location` line in the `terraform.tfvars` file by adding a `#` at the beginning, like so: `# outputs_location = "~/fast-config"`. This configuration is only necessary for the initial local deployments and should not be used in the CI/CD environment.
### Add-ons
FAST defines a simple mechanism to extend stage functionality via the use of [add-ons](../../addons/). Configuration for stage 1 add-ons happens here via the `fast_addon` variable. Refer to the add-ons documentation for more details on their use.
<!-- TFDOC OPTS files:1 show_extra:1 exclude:0-bootstrap-providers.tf -->
<!-- BEGIN TFDOC -->
## Files
| name | description | modules | resources |
|---|---|---|---|
| [automation.tf](./automation.tf) | Automation project and resources. | <code>gcs</code> · <code>iam-service-account</code> · <code>project</code> | |
| [billing.tf](./billing.tf) | Billing export project and dataset. | <code>bigquery-dataset</code> · <code>billing-account</code> · <code>logging-bucket</code> · <code>project</code> | |
| [cicd.tf](./cicd.tf) | CI/CD locals and resources. | <code>iam-service-account</code> | |
| [identity-providers-wfif-defs.tf](./identity-providers-wfif-defs.tf) | Workforce Identity provider definitions. | | |
| [identity-providers-wfif.tf](./identity-providers-wfif.tf) | Workforce Identity Federation provider definitions. | | <code>google_iam_workforce_pool</code> · <code>google_iam_workforce_pool_provider</code> |
| [identity-providers-wlif-defs.tf](./identity-providers-wlif-defs.tf) | Workload Identity provider definitions. | | |
| [identity-providers-wlif.tf](./identity-providers-wlif.tf) | Workload Identity Federation provider definitions. | | <code>google_iam_workload_identity_pool</code> · <code>google_iam_workload_identity_pool_provider</code> |
| [log-export.tf](./log-export.tf) | Audit log project and sink. | <code>bigquery-dataset</code> · <code>gcs</code> · <code>logging-bucket</code> · <code>project</code> · <code>pubsub</code> | |
| [main.tf](./main.tf) | Module-level locals and resources. | | |
| [organization-iam.tf](./organization-iam.tf) | Organization-level IAM bindings locals. | | |
| [organization.tf](./organization.tf) | Organization-level IAM. | <code>organization</code> | |
| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | <code>local_file</code> |
| [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | | <code>google_storage_bucket_object</code> |
| [outputs-providers.tf](./outputs-providers.tf) | Locals for provider output files. | | |
| [outputs.tf](./outputs.tf) | Module outputs. | | |
| [variables-addons.tf](./variables-addons.tf) | None | | |
| [variables.tf](./variables.tf) | Module variables. | | |
## Variables
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
| [billing_account](variables.tf#L17) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | <code title="object&#40;&#123;&#10; id &#61; string&#10; force_create &#61; optional&#40;object&#40;&#123;&#10; dataset &#61; optional&#40;bool, false&#41;&#10; project &#61; optional&#40;bool, false&#41;&#10; log_bucket &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; is_org_level &#61; optional&#40;bool, true&#41;&#10; no_iam &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | |
| [organization](variables.tf#L282) | Organization details. | <code title="object&#40;&#123;&#10; id &#61; number&#10; domain &#61; optional&#40;string&#41;&#10; customer_id &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | |
| [prefix](variables.tf#L297) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | |
| [bootstrap_user](variables.tf#L39) | Email of the nominal user running this stage for the first time. | <code>string</code> | | <code>null</code> | |
| [cicd_config](variables.tf#L45) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | <code title="object&#40;&#123;&#10; bootstrap &#61; optional&#40;object&#40;&#123;&#10; identity_provider &#61; string&#10; repository &#61; object&#40;&#123;&#10; name &#61; string&#10; branch &#61; optional&#40;string&#41;&#10; type &#61; optional&#40;string, &#34;github&#34;&#41;&#10; &#125;&#41;&#10; &#125;&#41;&#41;&#10; resman &#61; optional&#40;object&#40;&#123;&#10; identity_provider &#61; string&#10; repository &#61; object&#40;&#123;&#10; name &#61; string&#10; branch &#61; optional&#40;string&#41;&#10; type &#61; optional&#40;string, &#34;github&#34;&#41;&#10; &#125;&#41;&#10; &#125;&#41;&#41;&#10; vpcsc &#61; optional&#40;object&#40;&#123;&#10; identity_provider &#61; string&#10; repository &#61; object&#40;&#123;&#10; name &#61; string&#10; branch &#61; optional&#40;string&#41;&#10; type &#61; optional&#40;string, &#34;github&#34;&#41;&#10; &#125;&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [custom_roles](variables.tf#L86) | Map of role names => list of permissions to additionally create at the organization level. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [environments](variables.tf#L93) | Environment names. When not defined, short name is set to the key and tag name to lower(name). | <code title="map&#40;object&#40;&#123;&#10; name &#61; string&#10; is_default &#61; optional&#40;bool, false&#41;&#10; short_name &#61; optional&#40;string&#41;&#10; tag_name &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code title="&#123;&#10; dev &#61; &#123;&#10; name &#61; &#34;Development&#34;&#10; &#125;&#10; prod &#61; &#123;&#10; name &#61; &#34;Production&#34;&#10; is_default &#61; true&#10; &#125;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [essential_contacts](variables.tf#L133) | Email used for essential contacts, unset if null. | <code>string</code> | | <code>null</code> | |
| [factories_config](variables.tf#L139) | Configuration for the resource factories or external data. | <code title="object&#40;&#123;&#10; custom_constraints &#61; optional&#40;string, &#34;data&#47;custom-constraints&#34;&#41;&#10; custom_roles &#61; optional&#40;string, &#34;data&#47;custom-roles&#34;&#41;&#10; org_policies &#61; optional&#40;string, &#34;data&#47;org-policies&#34;&#41;&#10; org_policies_iac &#61; optional&#40;string, &#34;data&#47;org-policies-iac&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [fast_addon](variables-addons.tf#L17) | FAST addons configurations for stages 1. Keys are used as short names for the add-on resources. | <code title="map&#40;object&#40;&#123;&#10; parent_stage &#61; string&#10; cicd_config &#61; optional&#40;object&#40;&#123;&#10; identity_provider &#61; string&#10; repository &#61; object&#40;&#123;&#10; name &#61; string&#10; branch &#61; optional&#40;string&#41;&#10; type &#61; optional&#40;string, &#34;github&#34;&#41;&#10; &#125;&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [groups](variables.tf#L151) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | <code title="object&#40;&#123;&#10; gcp-billing-admins &#61; optional&#40;string, &#34;gcp-billing-admins&#34;&#41;&#10; gcp-devops &#61; optional&#40;string, &#34;gcp-devops&#34;&#41;&#10; gcp-network-admins &#61; optional&#40;string, &#34;gcp-vpc-network-admins&#34;&#41;&#10; gcp-organization-admins &#61; optional&#40;string, &#34;gcp-organization-admins&#34;&#41;&#10; gcp-secops-admins &#61; optional&#40;string, &#34;gcp-security-admins&#34;&#41;&#10; gcp-security-admins &#61; optional&#40;string, &#34;gcp-security-admins&#34;&#41;&#10; gcp-support &#61; optional&#40;string, &#34;gcp-devops&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [iam](variables.tf#L168) | Organization-level custom IAM settings in role => [principal] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [iam_bindings_additive](variables.tf#L175) | Organization-level custom additive IAM bindings. Keys are arbitrary. | <code title="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;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [iam_by_principals](variables.tf#L190) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [locations](variables.tf#L197) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object&#40;&#123;&#10; bq &#61; optional&#40;string, &#34;EU&#34;&#41;&#10; gcs &#61; optional&#40;string, &#34;EU&#34;&#41;&#10; logging &#61; optional&#40;string, &#34;global&#34;&#41;&#10; pubsub &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [log_sinks](variables.tf#L211) | Org-level log sinks, in name => {type, filter} format. | <code title="map&#40;object&#40;&#123;&#10; filter &#61; string&#10; type &#61; string&#10; disabled &#61; optional&#40;bool, false&#41;&#10; exclusions &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code title="&#123;&#10; audit-logs &#61; &#123;&#10; filter &#61; &#60;&#60;-FILTER&#10; log_id&#40;&#34;cloudaudit.googleapis.com&#47;activity&#34;&#41; OR&#10; log_id&#40;&#34;cloudaudit.googleapis.com&#47;system_event&#34;&#41; OR&#10; log_id&#40;&#34;cloudaudit.googleapis.com&#47;policy&#34;&#41; OR&#10; log_id&#40;&#34;cloudaudit.googleapis.com&#47;access_transparency&#34;&#41;&#10; FILTER&#10; type &#61; &#34;logging&#34;&#10; &#125;&#10; iam &#61; &#123;&#10; filter &#61; &#60;&#60;-FILTER&#10; protoPayload.serviceName&#61;&#34;iamcredentials.googleapis.com&#34; OR&#10; protoPayload.serviceName&#61;&#34;iam.googleapis.com&#34; OR&#10; protoPayload.serviceName&#61;&#34;sts.googleapis.com&#34;&#10; FILTER&#10; type &#61; &#34;logging&#34;&#10; &#125;&#10; vpc-sc &#61; &#123;&#10; filter &#61; &#60;&#60;-FILTER&#10; protoPayload.metadata.&#64;type&#61;&#34;type.googleapis.com&#47;google.cloud.audit.VpcServiceControlAuditMetadata&#34;&#10; FILTER&#10; type &#61; &#34;logging&#34;&#10; &#125;&#10; workspace-audit-logs &#61; &#123;&#10; filter &#61; &#60;&#60;-FILTER&#10; protoPayload.serviceName&#61;&#34;admin.googleapis.com&#34; OR&#10; protoPayload.serviceName&#61;&#34;cloudidentity.googleapis.com&#34; OR&#10; protoPayload.serviceName&#61;&#34;login.googleapis.com&#34;&#10; FILTER&#10; type &#61; &#34;logging&#34;&#10; &#125;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [org_policies_config](variables.tf#L267) | Organization policies customization. | <code title="object&#40;&#123;&#10; iac_policy_member_domains &#61; optional&#40;list&#40;string&#41;&#41;&#10; import_defaults &#61; optional&#40;bool, false&#41;&#10; tag_name &#61; optional&#40;string, &#34;org-policies&#34;&#41;&#10; tag_values &#61; optional&#40;map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string, &#34;Managed by the Terraform organization module.&#34;&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; id &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [outputs_location](variables.tf#L291) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
| [project_parent_ids](variables.tf#L306) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | <code title="object&#40;&#123;&#10; automation &#61; optional&#40;string&#41;&#10; billing &#61; optional&#40;string&#41;&#10; logging &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [resource_names](variables.tf#L317) | Resource names overrides for specific resources. Prefix is always set via code, except where noted in the variable type. | <code title="object&#40;&#123;&#10; bq-billing &#61; optional&#40;string, &#34;billing_export&#34;&#41;&#10; bq-logs &#61; optional&#40;string, &#34;logs&#34;&#41;&#10; gcs-bootstrap &#61; optional&#40;string, &#34;prod-iac-core-bootstrap-0&#34;&#41;&#10; gcs-logs &#61; optional&#40;string, &#34;prod-logs&#34;&#41;&#10; gcs-outputs &#61; optional&#40;string, &#34;prod-iac-core-outputs-0&#34;&#41;&#10; gcs-resman &#61; optional&#40;string, &#34;prod-iac-core-resman-0&#34;&#41;&#10; gcs-vpcsc &#61; optional&#40;string, &#34;prod-iac-core-vpcsc-0&#34;&#41;&#10; project-automation &#61; optional&#40;string, &#34;prod-iac-core-0&#34;&#41;&#10; project-billing &#61; optional&#40;string, &#34;prod-billing-exp-0&#34;&#41;&#10; project-logs &#61; optional&#40;string, &#34;prod-audit-logs-0&#34;&#41;&#10; pubsub-logs_template &#61; optional&#40;string, &#34;&#36;&#36;&#123;key&#125;&#34;&#41;&#10; sa-bootstrap &#61; optional&#40;string, &#34;prod-bootstrap-0&#34;&#41;&#10; sa-bootstrap_ro &#61; optional&#40;string, &#34;prod-bootstrap-0r&#34;&#41;&#10; sa-cicd_template &#61; optional&#40;string, &#34;prod-&#36;&#36;&#123;key&#125;-1&#34;&#41;&#10; sa-cicd_template_ro &#61; optional&#40;string, &#34;prod-&#36;&#36;&#123;key&#125;-1r&#34;&#41;&#10; sa-resman &#61; optional&#40;string, &#34;prod-resman-0&#34;&#41;&#10; sa-resman_ro &#61; optional&#40;string, &#34;prod-resman-0r&#34;&#41;&#10; sa-vpcsc &#61; optional&#40;string, &#34;prod-vpcsc-0&#34;&#41;&#10; sa-vpcsc_ro &#61; optional&#40;string, &#34;prod-vpcsc-0r&#34;&#41;&#10; wf-bootstrap &#61; optional&#40;string, &#34;&#36;&#36;&#123;prefix&#125;-bootstrap&#34;&#41;&#10; wf-provider_template &#61; optional&#40;string, &#34;&#36;&#36;&#123;prefix&#125;-bootstrap-&#36;&#36;&#123;key&#125;&#34;&#41;&#10; wif-bootstrap &#61; optional&#40;string, &#34;&#36;&#36;&#123;prefix&#125;-bootstrap&#34;&#41;&#10; wif-provider_template &#61; optional&#40;string, &#34;&#36;&#36;&#123;prefix&#125;-bootstrap-&#36;&#36;&#123;key&#125;&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [universe](variables.tf#L349) | Target GCP universe. | <code title="object&#40;&#123;&#10; domain &#61; string&#10; prefix &#61; string&#10; unavailable_services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | |
| [workforce_identity_providers](variables.tf#L359) | Workforce Identity Federation pools. | <code title="map&#40;object&#40;&#123;&#10; attribute_condition &#61; optional&#40;string&#41;&#10; issuer &#61; string&#10; display_name &#61; string&#10; description &#61; string&#10; disabled &#61; optional&#40;bool, false&#41;&#10; saml &#61; optional&#40;object&#40;&#123;&#10; idp_metadata_xml &#61; string&#10; &#125;&#41;, null&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [workload_identity_providers](variables.tf#L375) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | <code title="map&#40;object&#40;&#123;&#10; attribute_condition &#61; optional&#40;string&#41;&#10; issuer &#61; string&#10; custom_settings &#61; optional&#40;object&#40;&#123;&#10; issuer_uri &#61; optional&#40;string&#41;&#10; audiences &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; jwks_json &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
| [automation](outputs.tf#L113) | Automation resources. | | |
| [billing_dataset](outputs.tf#L118) | BigQuery dataset prepared for billing export. | | |
| [cicd_repositories](outputs.tf#L123) | CI/CD repository configurations. | | |
| [custom_roles](outputs.tf#L135) | Organization-level custom roles. | | |
| [outputs_bucket](outputs.tf#L140) | GCS bucket where generated output files are stored. | | |
| [project_ids](outputs.tf#L145) | Projects created by this stage. | | |
| [providers](outputs.tf#L155) | Terraform provider files for this stage and dependent stages. | ✓ | <code>stage-01</code> |
| [service_accounts](outputs.tf#L162) | Automation service accounts created by this stage. | | |
| [tfvars](outputs.tf#L171) | Terraform variable files for the following stages. | ✓ | |
| [tfvars_globals](outputs.tf#L177) | Terraform Globals variable files for the following stages. | ✓ | |
| [workforce_identity_pool](outputs.tf#L183) | Workforce Identity Federation pool. | | |
| [workload_identity_pool](outputs.tf#L192) | Workload Identity Federation pool and providers. | | |
<!-- END TFDOC -->

View File

@@ -0,0 +1,127 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Billing export project and dataset.
locals {
billing_mode = (
var.billing_account.no_iam
? null
: var.billing_account.is_org_level ? "org" : "resource"
)
_billing_iam_bindings = {
"roles/billing.admin" = [
local.principals.gcp-billing-admins,
local.principals.gcp-organization-admins,
module.automation-tf-bootstrap-sa.iam_email,
module.automation-tf-resman-sa.iam_email
],
"roles/billing.viewer" = [
module.automation-tf-bootstrap-r-sa.iam_email,
module.automation-tf-resman-r-sa.iam_email
],
"roles/logging.configWriter" = local.billing_mode == "org" || !var.billing_account.force_create.log_bucket ? [] : [
module.automation-tf-bootstrap-sa.iam_email
]
}
_billing_iam_bindings_add = flatten([for role, bindings in local._billing_iam_bindings : [
for member in bindings : {
member = member,
role = role
}
]])
billing_iam_bindings_additive = {
for b in local._billing_iam_bindings_add : "${b.role}-${b.member}" => {
member = b.member
role = b.role
}
}
}
# billing account in same org (IAM is in the organization.tf file)
module "billing-export-project" {
source = "../../../modules/project"
count = (
local.billing_mode == "org" || var.billing_account.force_create.project == true ? 1 : 0
)
billing_account = var.billing_account.id
name = var.resource_names["project-billing"]
parent = coalesce(
var.project_parent_ids.billing, "organizations/${var.organization.id}"
)
prefix = var.prefix
universe = var.universe
contacts = (
var.bootstrap_user != null || var.essential_contacts == null
? {}
: { (var.essential_contacts) = ["ALL"] }
)
iam = {
"roles/owner" = [module.automation-tf-bootstrap-sa.iam_email]
"roles/viewer" = [module.automation-tf-bootstrap-r-sa.iam_email]
}
services = [
# "cloudresourcemanager.googleapis.com",
# "iam.googleapis.com",
# "serviceusage.googleapis.com",
"bigquery.googleapis.com",
"bigquerydatatransfer.googleapis.com",
"storage.googleapis.com"
]
}
module "billing-export-dataset" {
source = "../../../modules/bigquery-dataset"
count = (
local.billing_mode == "org" || var.billing_account.force_create.dataset == true ? 1 : 0
)
project_id = module.billing-export-project[0].project_id
id = var.resource_names["bq-billing"]
friendly_name = "Billing export."
location = local.locations.bq
}
# standalone billing account
module "billing-account-logbucket" {
source = "../../../modules/logging-bucket"
count = local.billing_mode == "resource" && var.billing_account.force_create.log_bucket ? 1 : 0
parent_type = "project"
parent = module.log-export-project.project_id
name = "billing-account"
location = local.locations.logging
log_analytics = { enable = true }
# org-level logging settings ready before we create any logging buckets
depends_on = [module.organization-logging]
}
module "billing-account" {
source = "../../../modules/billing-account"
count = local.billing_mode == "resource" ? 1 : 0
id = var.billing_account.id
iam_bindings_additive = local.billing_iam_bindings_additive
logging_sinks = !var.billing_account.force_create.log_bucket ? {} : {
billing_bucket_log_sink = {
destination = module.billing-account-logbucket[0].id
type = "logging"
description = "billing-account sink (Terraform-managed)."
}
}
}

View File

@@ -0,0 +1,127 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description CI/CD locals and resources.
locals {
_cicd_configs = merge(
# stages
{
for k, v in var.cicd_config : k => merge(v, {
level = k == "bootstrap" ? 0 : 1
stage = k
}) if v != null
},
# addons
{
for k, v in var.fast_addon : k => merge(v.cicd_config, {
level = 1
stage = substr(v.parent_stage, 2, -1)
}) if v.cicd_config != null
}
)
cicd_providers = {
for k, v in google_iam_workload_identity_pool_provider.default :
k => {
audiences = concat(
v.oidc[0].allowed_audiences,
["https://iam.googleapis.com/${v.name}"]
)
issuer = local.workload_identity_providers[k].issuer
issuer_uri = try(v.oidc[0].issuer_uri, null)
name = v.name
principal_branch = local.workload_identity_providers[k].principal_branch
principal_repo = local.workload_identity_providers[k].principal_repo
}
}
cicd_repositories = {
for k, v in local._cicd_configs : k => v if(
contains(keys(local.workload_identity_providers), v.identity_provider) &&
fileexists("${path.module}/templates/workflow-${v.repository.type}.yaml")
)
}
cicd_workflow_providers = merge(
{
for k, v in local.cicd_repositories :
k => "${v.level}-${k}-providers.tf"
},
{
for k, v in local.cicd_repositories :
"${k}-r" => "${v.level}-${k}-r-providers.tf"
}
)
}
# SAs used by CI/CD workflows to impersonate automation SAs
module "automation-tf-cicd-sa" {
source = "../../../modules/iam-service-account"
for_each = local.cicd_repositories
project_id = module.automation-project.project_id
name = templatestring(
var.resource_names["sa-cicd_template"], { key = each.key }
)
display_name = "Terraform CI/CD ${each.key} service account."
prefix = var.prefix
iam = {
"roles/iam.workloadIdentityUser" = [
each.value.repository.branch == null
? format(
local.workload_identity_providers_defs[each.value.repository.type].principal_repo,
google_iam_workload_identity_pool.default[0].name,
each.value.repository.name
)
: format(
local.workload_identity_providers_defs[each.value.repository.type].principal_branch,
google_iam_workload_identity_pool.default[0].name,
each.value.repository.name,
each.value.repository.branch
)
]
}
iam_project_roles = {
(module.automation-project.project_id) = ["roles/logging.logWriter"]
}
iam_storage_roles = {
(module.automation-tf-output-gcs.name) = ["roles/storage.objectViewer"]
}
}
module "automation-tf-cicd-r-sa" {
source = "../../../modules/iam-service-account"
for_each = local.cicd_repositories
project_id = module.automation-project.project_id
name = templatestring(
var.resource_names["sa-cicd_template_ro"], { key = each.key }
)
display_name = "Terraform CI/CD ${each.key} service account (read-only)."
prefix = var.prefix
iam = {
"roles/iam.workloadIdentityUser" = [
format(
local.workload_identity_providers_defs[each.value.repository.type].principal_repo,
google_iam_workload_identity_pool.default[0].name,
each.value.repository.name
)
]
}
iam_project_roles = {
(module.automation-project.project_id) = ["roles/logging.logWriter"]
}
iam_storage_roles = {
(module.automation-tf-output-gcs.name) = ["roles/storage.objectViewer"]
}
}

View File

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 MiB

After

Width:  |  Height:  |  Size: 6.6 MiB

View File

@@ -1,5 +1,5 @@
/** /**
* Copyright 2022 Google LLC * Copyright 2024 Google LLC
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -14,14 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
output "projects" { locals {
description = "Attributes for managed projects." principals = {
value = module.factory.projects for k, v in var.groups : k => (
} can(regex("^[a-zA-Z]+:", v))
? v
resource "google_storage_bucket_object" "version" { : "group:${v}@${var.organization.domain}"
count = fileexists("fast_version.txt") ? 1 : 0 )
bucket = var.automation.outputs_bucket }
name = "versions/2-${var.stage_name}-version.txt" locations = {
source = "fast_version.txt" bq = var.locations.bq
gcs = var.locations.gcs
logging = var.locations.logging
pubsub = var.locations.pubsub
}
} }

View File

@@ -0,0 +1,237 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Organization-level IAM.
locals {
# reassemble logical bindings into the formats expected by the module
_iam_bindings = merge(
local.iam_domain_bindings,
local.iam_sa_bindings,
local.iam_user_bootstrap_bindings,
{
for k, v in local.iam_principal_bindings : k => {
authoritative = []
additive = v.additive
}
}
)
_iam_bindings_auth = flatten([
for member, data in local._iam_bindings : [
for role in data.authoritative : {
member = member
role = role
}
]
])
_iam_bindings_add = flatten([
for member, data in local._iam_bindings : [
for role in data.additive : {
member = member
role = role
}
]
])
org_policies_tag_name = "${var.organization.id}/${var.org_policies_config.tag_name}"
iam_principals = {
for k, v in local.iam_principal_bindings : k => v.authoritative
}
iam = merge(
{
for r in local.iam_delete_roles : r => []
},
{
for b in local._iam_bindings_auth : b.role => b.member...
}
)
iam_bindings_additive = {
for b in local._iam_bindings_add : "${b.role}-${b.member}" => {
member = b.member
role = b.role
}
}
}
# TODO: add a check block to ensure our custom roles exist in the factory files
# import org policy constraints enabled by default in new orgs since February 2024
import {
for_each = (
!var.org_policies_config.import_defaults || var.bootstrap_user != null
? toset([])
: toset([
# source: https://cloud.google.com/resource-manager/docs/secure-by-default-organizations#organization_policies_enforced_on_organization_resources
# listed in the order as on page
"iam.disableServiceAccountKeyCreation",
"iam.disableServiceAccountKeyUpload",
"iam.automaticIamGrantsForDefaultServiceAccounts",
"iam.allowedPolicyMemberDomains",
"essentialcontacts.allowedContactDomains",
"storage.uniformBucketLevelAccess",
"compute.setNewProjectDefaultToZonalDNSOnly", # Verified as of 2024-09-13
"compute.restrictProtocolForwardingCreationForTypes", # Verified as of 2025-02-13
])
)
id = "organizations/${var.organization.id}/policies/${each.key}"
to = module.organization.google_org_policy_policy.default[each.key]
}
module "organization-logging" {
# Preconfigure organization-wide logging settings to ensure project
# log buckets (_Default, _Required) are created in the location
# specified by `var.locations.logging`. This separate
# organization-block prevents circular dependencies with later
# project creation.
source = "../../../modules/organization"
organization_id = "organizations/${var.organization.id}"
logging_settings = {
storage_location = var.locations.logging
}
}
module "organization" {
source = "../../../modules/organization"
organization_id = module.organization-logging.id
# human (groups) IAM bindings
iam_by_principals = {
for key in distinct(concat(
keys(local.iam_principals),
keys(var.iam_by_principals),
)) :
key => distinct(concat(
lookup(local.iam_principals, key, []),
lookup(var.iam_by_principals, key, []),
))
}
# machine (service accounts) IAM bindings
iam = merge(
{
for k, v in local.iam : k => distinct(concat(v, lookup(var.iam, k, [])))
},
{
for k, v in var.iam : k => v if lookup(local.iam, k, null) == null
}
)
# additive bindings, used for roles co-managed by different stages
iam_bindings_additive = merge(
local.iam_bindings_additive,
var.iam_bindings_additive
)
# delegated role grant for resource manager service account
iam_bindings = merge(
{
organization_iam_admin_conditional = {
members = [module.automation-tf-resman-sa.iam_email]
role = module.organization.custom_role_id["organization_iam_admin"]
condition = {
expression = (
format(
<<-EOT
api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])
|| api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])
|| api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])
EOT
, join(",", formatlist("'%s'", [
"roles/accesscontextmanager.policyEditor",
"roles/accesscontextmanager.policyReader",
"roles/cloudasset.viewer",
"roles/compute.orgFirewallPolicyAdmin",
"roles/compute.orgFirewallPolicyUser",
"roles/compute.xpnAdmin",
"roles/orgpolicy.policyAdmin",
"roles/orgpolicy.policyViewer",
"roles/resourcemanager.organizationViewer",
]))
, join(",", formatlist("'%s'", [
"roles/iam.workforcePoolAdmin",
"roles/iam.workforcePoolViewer"
]))
, join(",", formatlist("'%s'", [
module.organization.custom_role_id["billing_viewer"],
module.organization.custom_role_id["network_firewall_policies_admin"],
module.organization.custom_role_id["ngfw_enterprise_admin"],
module.organization.custom_role_id["ngfw_enterprise_viewer"],
module.organization.custom_role_id["service_project_network_admin"],
module.organization.custom_role_id["tenant_network_admin"]
]))
)
)
title = "automation_sa_delegated_grants"
description = "Automation service account delegated grants."
}
}
},
local.billing_mode != "org" ? {} : {
organization_billing_conditional = {
members = [module.automation-tf-resman-sa.iam_email]
role = module.organization.custom_role_id["organization_iam_admin"]
condition = {
expression = format(
"api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
join(",", formatlist("'%s'", [
"roles/billing.admin",
"roles/billing.costsManager",
"roles/billing.user",
]))
)
title = "automation_sa_delegated_grants"
description = "Automation service account delegated grants."
}
}
}
)
custom_roles = var.custom_roles
context = {
condition_vars = {
organization = var.organization
tags = {
org_policies_tag_name = local.org_policies_tag_name
}
}
}
factories_config = {
custom_roles = var.factories_config.custom_roles
org_policy_custom_constraints = (
var.bootstrap_user != null ? null : var.factories_config.custom_constraints
)
org_policies = (
var.bootstrap_user != null ? null : var.factories_config.org_policies
)
}
logging_sinks = {
for name, attrs in var.log_sinks : name => {
bq_partitioned_table = attrs.type == "bigquery"
destination = local.log_sink_destinations[name].id
filter = attrs.filter
type = attrs.type
disabled = attrs.disabled
exclusions = attrs.exclusions
}
}
tags = {
(var.org_policies_config.tag_name) = {
description = "Organization policy conditions."
iam = {}
values = merge(
{
allowed-essential-contacts-domains-all = {}
allowed-policy-member-domains-all = {}
},
var.org_policies_config.tag_values
)
}
}
}

View File

@@ -0,0 +1,200 @@
/**
* 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 {
_tpl_providers = "${path.module}/templates/providers.tf.tpl"
# render CI/CD workflow templates
cicd_workflows = {
for k, v in local.cicd_repositories : "${v.level}-${k}" => templatefile(
"${path.module}/templates/workflow-${v.repository.type}.yaml", {
# If users give a list of custom audiences we set by default the first element.
# If no audiences are given, we set https://iam.googleapis.com/{PROVIDER_NAME}
audiences = try(
local.cicd_providers[v.identity_provider].audiences, []
)
identity_provider = try(
local.cicd_providers[v.identity_provider].name, ""
)
outputs_bucket = module.automation-tf-output-gcs.name
service_accounts = {
apply = try(module.automation-tf-cicd-sa[k].email, "")
plan = try(module.automation-tf-cicd-r-sa[k].email, "")
}
stage_name = k
tf_providers_files = {
apply = local.cicd_workflow_providers[k]
plan = local.cicd_workflow_providers["${k}-r"]
}
tf_var_files = k == "bootstrap" ? [] : [
"0-bootstrap.auto.tfvars.json",
"0-globals.auto.tfvars.json"
]
}
)
}
tfvars = {
automation = {
federated_identity_pool = try(
google_iam_workload_identity_pool.default[0].name, null
)
federated_identity_providers = local.cicd_providers
outputs_bucket = module.automation-tf-output-gcs.name
project_id = module.automation-project.project_id
project_number = module.automation-project.number
service_accounts = {
bootstrap = module.automation-tf-bootstrap-sa.email
bootstrap-r = module.automation-tf-bootstrap-r-sa.email
resman = module.automation-tf-resman-sa.email
resman-r = module.automation-tf-resman-r-sa.email
vpcsc = module.automation-tf-vpcsc-sa.email
vpcsc-r = module.automation-tf-vpcsc-r-sa.email
}
}
billing = {
dataset = try(module.billing-export-dataset[0].id, null)
project_id = try(module.billing-export-project[0].project_id, null)
project_number = try(module.billing-export-project[0].number, null)
}
custom_roles = module.organization.custom_role_id
logging = {
project_id = module.log-export-project.project_id
project_number = module.log-export-project.number
writer_identities = module.organization.sink_writer_identities
destinations = {
bigquery = try(module.log-export-dataset[0].id, null)
logging = { for k, v in module.log-export-logbucket : k => v.id }
pubsub = { for k, v in module.log-export-pubsub : k => v.id }
storage = try(module.log-export-gcs[0].id, null)
}
}
org_policy_tags = {
key_id = (
module.organization.tag_keys[var.org_policies_config.tag_name].id
)
key_name = var.org_policies_config.tag_name
values = {
for k, v in module.organization.tag_values :
split("/", k)[1] => v.id
}
}
universe = var.universe
}
tfvars_globals = {
billing_account = var.billing_account
groups = local.principals
environments = {
for k, v in var.environments : k => {
is_default = v.is_default
key = k
name = v.name
short_name = v.short_name != null ? v.short_name : k
tag_name = v.tag_name != null ? v.tag_name : lower(v.name)
}
}
locations = local.locations
organization = var.organization
prefix = var.prefix
}
}
output "automation" {
description = "Automation resources."
value = local.tfvars.automation
}
output "billing_dataset" {
description = "BigQuery dataset prepared for billing export."
value = try(module.billing-export-dataset[0].id, null)
}
output "cicd_repositories" {
description = "CI/CD repository configurations."
value = {
for k, v in local.cicd_repositories : k => {
branch = v.repository.branch
name = v.repository.name
provider = try(local.cicd_providers[v.identity_provider].name, null)
service_account = try(module.automation-tf-cicd-sa[k].email, null)
}
}
}
output "custom_roles" {
description = "Organization-level custom roles."
value = module.organization.custom_role_id
}
output "outputs_bucket" {
description = "GCS bucket where generated output files are stored."
value = module.automation-tf-output-gcs.name
}
output "project_ids" {
description = "Projects created by this stage."
value = {
automation = module.automation-project.project_id
billing-export = try(module.billing-export-project[0].project_id, null)
log-export = module.log-export-project.project_id
}
}
# ready to use provider configurations for subsequent stages when not using files
output "providers" {
# tfdoc:output:consumers stage-01
description = "Terraform provider files for this stage and dependent stages."
sensitive = true
value = local.providers
}
output "service_accounts" {
description = "Automation service accounts created by this stage."
value = {
bootstrap = module.automation-tf-bootstrap-sa.email
resman = module.automation-tf-resman-sa.email
}
}
# ready to use variable values for subsequent stages
output "tfvars" {
description = "Terraform variable files for the following stages."
sensitive = true
value = local.tfvars
}
output "tfvars_globals" {
description = "Terraform Globals variable files for the following stages."
sensitive = true
value = local.tfvars_globals
}
output "workforce_identity_pool" {
description = "Workforce Identity Federation pool."
value = {
pool = try(
google_iam_workforce_pool.default[0].name, null
)
}
}
output "workload_identity_pool" {
description = "Workload Identity Federation pool and providers."
value = {
pool = try(
google_iam_workload_identity_pool.default[0].name, null
)
providers = local.cicd_providers
}
}

View File

@@ -0,0 +1 @@
../../../../modules/organization/schemas/custom-role.schema.json

Some files were not shown because too many files have changed in this diff Show More