Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions adrs/20251013-context-locals.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Draft

## Context

Terraform build dependency graph using variables and locals as nodes. If those are complex structures, such as lists, maps or objects, they can contain references to multiple resources. Because of that, any access to a complex variable creates an implicit dependency on all resources referenced within this variable. For example:
Terraform builds dependency graph using variables and locals as nodes. If those are complex structures, such as lists, maps or objects, they can contain references to multiple resources. Because of that, any access to a complex variable creates an implicit dependency on all resources referenced within this variable. For example:

```hcl
locals {
Expand Down Expand Up @@ -51,7 +51,7 @@ The primary reason is to make module calls concise and easy to use. As of now, t
## Alternatives Considered:

### Separating `local.ctx` by type of context
Currently, `local.ctx` is build like this (example from `modules/project`):
Currently, `local.ctx` is built like this (example from `modules/project`):
```hcl
locals {
ctx = {
Expand Down
3 changes: 3 additions & 0 deletions fast/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ Since we expect users to customize FAST to their specific needs, we strive to ma
We also recognize that FAST users don't need all of its features. Therefore, you don't need to use our project factory or our GKE implementation if you don't want to. Instead, remove those stages or pieces of code and keep what suits you.

Those familiar with Python will note that FAST follows many of the maxims in the [Zen of Python](https://www.python.org/dev/peps/pep-0020/#id2).

### Next Step
Continue with [stages](./stages/README.md).
121 changes: 103 additions & 18 deletions fast/stages/0-org-setup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
<!-- BEGIN TOC -->
- [Quickstart](#quickstart)
- [Prerequisites](#prerequisites)
- [Billing IAM prerequisites](#billing-iam-prerequisites)
- [If you cannot manage billing IAM](#if-you-cannot-manage-billing-iam)
- [Admin IAM prerequisites](#admin-iam-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)
- [Default project](#default-project)
- [Provider Configuration for edge cases](#provider-configuration-for-edge-cases)
- [Importing org policies](#importing-org-policies)
- [Importing existing organization level IAM bindings](#importing-existing-organization-level-iam-bindings)
- [IAM by Role (Authoritative)](#iam-by-role-authoritative)
- [IAM by Principal (Authoritative)](#iam-by-principal-authoritative)
- [IAM by Principal Additive (Non-Authoritative)](#iam-by-principal-additive-non-authoritative)
- [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)
Expand Down Expand Up @@ -68,16 +75,31 @@ The high-level flow for running this stage is:
This stage only requires minimal prerequisites:

- one organization
- credentials with admin access to the organization and one billing account
- credentials with admin access to the organization
- one billing account

@ludoo ludoo Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BA is optional


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, move legacy projects to a dedicated folder where the current org-level configuration can be replicated.
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 (see [Importing org policies](#importing-org-policies)). Ideally, move legacy projects to a dedicated folder where the current org-level configuration 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.
#### Billing IAM prerequisites

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.
You have sufficient permissions to manage billing IAM on either:
- an Organization-associated / Organization-level Billing Account: owned and managed directly within your Google Cloud Organization
- a Cross-Organization Billing or Reseller Billing Subaccount: externally managed billing account

@ludoo ludoo Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with reseller accounts you almost never manage them, you only use them

Get familiar with the [IAM relationship between organizations, projects and Cloud Billing accounts](https://docs.cloud.google.com/billing/docs/how-to/billing-access#relationships-between-resources) and refer to the [billing section](#billing-account-iam) for more details or non-standard configurations.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is too much detail to give here, and not needed imho


##### If you cannot manage billing IAM

If you do not have sufficient permissions to manage billing IAM (common in "brownfield" environments or when using External / Reseller Billing Subaccounts), the IAM bindings need to be assigned via an external flow. Therefore, you need to remove billing IAM bindings from the YAML configuration files:
1. remove billing IAM bindings (`iam_bindings_additive:`) from file `datasets/[dataset_name]/billing-accounts/default.yaml` and

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just remove the file or redirect the factory to a dummy path, easier and cleaner

2. remove billing IAM bindings (`iam: > roles/billing.creator:`) from file `datasets/[dataset_name]/organization/.config.yaml`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

harmless if they stay, so optional


#### Admin IAM prerequisites

The admin principal (`gcp-organization-admins`) is typically a group that includes the user running the first apply, but any kind of principal is supported. More principals (`gcp-network-admins`, `gcp-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 (`datasets/[dataset_name]/organization/.config.yaml`).

### Select/configure a factory dataset

Datasets are representations of resources based on directories and YAML files. [Factories](../../README.md#extensive-use-of-factories) consume datasets and deploy cloud resources. This repository contains several pre-configured datasets. See "[Resource Factories: A descriptive approach to Terraform](https://medium.com/google-cloud/resource-factories-a-descriptive-approach-to-terraform-581b3ebb59c)" for more details and the rationale behind factories.
The `factories_config` variable configures the location for the dataset, and the individual factories within it. Its default values point to the classic FAST compatible fileset in the `datasets/classic` folder.

If this configuration matches requirements, no changes are necessary 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).
Expand Down Expand Up @@ -172,7 +194,7 @@ for role in $FAST_ROLES; do
done
```

If you are using an externally managed billing account, make sure user has Billing Admin role assigned on the account.
If you are using an externally managed billing account, see [If you cannot manage billing IAM](#if-you-cannot-manage-billing-iam).

### First apply cycle

Expand All @@ -190,9 +212,28 @@ gcloud services enable \
orgpolicy.googleapis.com serviceusage.googleapis.com
```

##### Provider Configuration for edge cases

In some cases, i.e: if you are running the bootstrap process from a Google Cloud Compute Engine (GCE VM), rather than your local laptop, the setup behaves differently. The issue is that Terraform detects it is running inside a GCE VM instance and falls back to using the GCE VM management project to track API quota usage. The workaround is to create a temporary `providers.tf` file in the `fast/stages/0-org-setup/` directory to explicitly declare the billing project to be used during bootstrap.

```terraform
# Temporary providers.tf file to resolve quota project issues during bootstrap
provider "google" {
user_project_override = true
billing_project = "[project id]"
}

provider "google-beta" {
user_project_override = true
billing_project = "[project id]"
}
```

Comment on lines +215 to +231

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way we usually manage this is by setting the default (or quota) project via gcloud.

I wouldn't suggest creating a provider file this way as it can cause issues later with the one that we generate/link later.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this section because Terraform was ignoring the quota project I set via gcloud. Now, the machine I am running all this is a GCE VM (from a separate organization btw) and terraform was simply assuming the GCE VM's project as the quota project.

Do you know of another way to specify the quota project? Or do you think my use case is so niche that it might not be worth adding to the README.md?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My my concern is that you might end up with multiple provider files.

@ludoo wdyt?

@ludoo ludoo Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on not creating provider files, we configure the default project which is easier and works if left in place for subsequent runs

#### Importing org policies

If your dataset includes org policies which are already set in the organization, the first apply will fail with a `409 Conflict` error. In this case, you must either comment them out in the relevant YAML files or configure 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.
If your dataset includes org policies which are already set in the organization, the first apply will fail with a `409 Conflict` error. In this case, you must either comment them out in the relevant factory YAML files (`fast/stages/0-org-setup/datasets/<YOUR_CHOSEN_DATASET>/organization/org-policies/*.yaml`) or configure this stage to import them.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should link to the instructions for importing from here


To figure out which policies are already set in the organization, run `gcloud org-policies list --organization [your org id]`, then set the `org_policies_imports` variable in your tfvars file (`fast/stages/0-org-setup/0-org-setup.auto.tfvars`). The following is an example.

```bash
gcloud org-policies list --organization 1234567890
Expand All @@ -202,7 +243,7 @@ compute.disableSerialPortAccess - SET
```

```tfvars
# create or edit the 0-org-setup.auto.tfvars.file
# create or edit the 0-org-setup.auto.tfvars file
# do NOT include the 'constraints/' prefix, use the names matching the YAML files
org_policies_imports = [
"compute.managed.restrictProtocolForwardingCreationForTypes",
Expand All @@ -215,29 +256,73 @@ org_policies_imports = [
]
```

Once org policies have been imported, the variable definition can be removed from the tfvars file.
Once org policies have been imported, the `org_policies_imports` variable definition can be removed from the `0-org-setup.auto.tfvars` file.

#### Importing existing organization level IAM bindings

For brownfield implementations you may need to import existing organization IAM policies. These snippets can help you add existing settings into the YAML file.
For brownfield implementations you may need to import existing organization IAM policies. These snippets can help you add existing settings into the factory YAML file (`fast/stages/0-org-setup/datasets/<YOUR_CHOSEN_DATASET>/organization/.config.yaml`).

Scripts below require [yq](https://github.com/mikefarah/yq/) in at least version 4. It was tested using yq `v4.47.2`.

To create `iam:` part of the `/organization/.config.yaml` file, you can use following snippet:
##### IAM by Role (Authoritative)

The `iam:` block of the factory YAML file (`/organization/.config.yaml`) is authoritative, meaning that it will overwrite any existing IAM bindings on the resource, removing any binding for any role that is not explicitly listed. It is grouped by role name and contains a list of members.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meaning that it will overwrite any existing IAM bindings on the resource, removing any binding for any role that is not explicitly listed

This (seems) incorrect. Perhaps leave the introductory text but don't explain what an authoritative binding is

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm if these three use cases correspond to the Terraform resources linked here:

IAM by Role (Authoritative)
IAM by Principal (Authoritative)
IAM by Principal Additive (Non-Authoritative)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not exactly. iam and iam_by_principals is authoritative (uses *_iam_binding) while iam_by_principals_additive is non-authoritative (uses *_iam_member)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authoritative for the roles it includes, it's not a IAM policy so it's not authoritative for the resource, and I agree with Julio this is not the place to explain it


Example:

```yaml
iam:
roles/resourcemanager.organizationAdmin:
- user:alice@example.com
- user:bob@example.com
```

To create `iam:` block of the factory YAML file (`/organization/.config.yaml`), you can use following snippet:

```shell
```bash
gcloud <resource> get-iam-policy <resource name> | yq '.bindings | map({"key": .role, "value": .members}) | from_entries'
```

To create `iam_by_principals:` part of the factory YAML file, you can use following snippet:
##### IAM by Principal (Authoritative)

The `iam_by_principals:` block of the factory YAML file (`/organization/.config.yaml`) is authoritative, meaning that it will overwrite any existing IAM bindings on the resource, removing any binfing for any principal that is not explicitly listed. It behaves exactly like `iam:` and is provided as a convenience for those who prefer to group IAM bindings by principal.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as before


Example:

```shell
gcloud <resource> get-iam-policy <resource name> | yq '
```yaml
iam_by_principals:
user:alice@example.com:
- roles/resourcemanager.organizationAdmin
- roles/organization.admin
user:bob@example.com:
- roles/organization.admin
```

To create `iam_by_principals:` block of the factory YAML file, you can use following snippet:

```bash
gcloud <resource> get-iam-policy <resource name> | yq '
[.bindings | .[] | .members[] as $member | { "member": $member, "role": .role}] |
group_by(.member) | sort_by(.[0].member) | .[] | { .[0].member: map(.role)}
'
```

##### IAM by Principal Additive (Non-Authoritative)

The `iam_by_principals_additive:` block of the factory YAML file (`/organization/.config.yaml`) is non-authoritative, meaning that it will **not** overwrite existing IAM bindings on the resource, instead, it simply adds the specified users to the specified roles and it won't touch any other users who currently have that role.

For brownfield implementations, you may want to merge your existing IAM bindings with the new FAST configuration. You can either merge your existing IAM binding with the authoritative `iam_by_principals:` block, or append a new `iam_by_principals_additive:` block at the end of the factory YAML file.

Example:

```yaml
iam_by_principals_additive:
user:alice@example.com:
- roles/resourcemanager.organizationAdmin
```

To create `iam_by_principals_additive:` block of the factory YAML file (`/organization/.config.yaml`), you can use the same snippet as [`iam_by_principals:`](#iam-by-principal-authoritative).

#### 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.
Expand All @@ -249,7 +334,7 @@ To enable local output files storage, set the `output_files.local_path` attribut
```yaml
# defaults.yaml
output_files:
local_path: "~/fast-configs/test-0"
local_path: "~/fast-config/fast-test-00"
```

#### Init and apply the stage
Expand All @@ -265,7 +350,7 @@ terraform apply

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 first step is to link the generated provider file, either by 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-org-setup.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.

Expand All @@ -277,10 +362,10 @@ If local output files are available adjust the path, run the script, then copy/p
# File linking commands for FAST Organization Setup stage

# provider file
ln -s /home/user/fast-configs/test-0/providers/0-org-setup-providers.tf ./
ln -s ~/fast-configs/test-0/providers/0-org-setup-providers.tf ./

# conventional location for this stage terraform.tfvars (manually managed)
ln -s /home/user/fast-configs/test-0/0-org-setup.auto.tfvars ./
ln -s ~/fast-configs/test-0/0-org-setup.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.org_setup.automation.outputs_bucket` Terraform output. Adjust the path, run the script, then copy/paste the resulting commands.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@

id: $defaults:billing_account
# authoritative IAM must be used with care here, as it resets permissions
# three modes of managing billing IAM
# - at the org level (don't set anything here, set roles in the org)
# - at the billing account level (set the roles below)
# - no admin access to billing account (don't set roles here or in org)
# Choose one of the following three modes of managing billing IAM:
# 1. at the org level: don't set the roles here. Delete the
# "iam_bindings_additive" block below and set roles in the organization

@ludoo ludoo Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or simply delete the file, or change path for billing in factories_config (which preserves the file but ignores it)

# configuration (organization/.config.yaml).
# 2. at the billing account level: set the billing roles in the
# "iam_bindings_additive" block below
# 3. no admin access to billing account: if your billing account is either
# shared or externally managed, don't set roles here nor in org. Delete the
# "iam_bindings_additive" block below and don't set roles in the organization either.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

iam_bindings_additive:
billing_admin_org_admins:
role: roles/billing.admin
Expand Down
Loading