diff --git a/fast/stages/0-bootstrap/README.md b/fast/stages/0-bootstrap/README.md
index 88bdceb3fb..20ed998f2c 100644
--- a/fast/stages/0-bootstrap/README.md
+++ b/fast/stages/0-bootstrap/README.md
@@ -69,8 +69,8 @@ One other design choice worth mentioning here is using a single automation proje
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 part of a different organization, billing IAM bindings will be set at the organization level in the billing account owning organization
- 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.
@@ -171,6 +171,18 @@ 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.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):
@@ -469,7 +481,16 @@ The remaining configuration is manual, as it regards the repositories themselves
| name | description | modules | resources |
|---|---|---|---|
| [automation.tf](./automation.tf) | Automation project and resources. | gcs
· iam-service-account
· project
| |
-| [billing.tf](./billing.tf) | Billing export project and dataset. | bigquery-dataset
· project
| google_billing_account_iam_member
|
+| [billing.tf](./billing.tf) | Billing export project and dataset. | bigquery-dataset
· project
|
+ )
+}
+
+# billing account in same org (IAM is in the organization.tf file)
+
+module
· ? local.billing_ext_admins : []
+ )
+ billing_account_id = var.billing_account.id
+ role =
· google_billing_account_iam_member
|
| [cicd.tf](./cicd.tf) | Workload Identity Federation configurations for CI/CD. | iam-service-account
· source-repository
| |
| [identity-providers.tf](./identity-providers.tf) | Workload Identity Federation provider definitions. | | google_iam_workload_identity_pool
· google_iam_workload_identity_pool_provider
|
| [log-export.tf](./log-export.tf) | Audit log project and sink. | bigquery-dataset
· gcs
· logging-bucket
· project
· pubsub
| |
@@ -484,21 +505,21 @@ The remaining configuration is manual, as it regards the repositories themselves
| 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. | object({…})
| ✓ | | |
-| [organization](variables.tf#L196) | Organization details. | object({…})
| ✓ | | |
-| [prefix](variables.tf#L211) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | |
-| [bootstrap_user](variables.tf#L29) | Email of the nominal user running this stage for the first time. | string
| | null
| |
-| [cicd_repositories](variables.tf#L35) | 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. | object({…})
| | null
| |
-| [custom_role_names](variables.tf#L81) | Names of custom roles defined at the org level. | object({…})
| | {…}
| |
-| [fast_features](variables.tf#L95) | Selective control for top-level FAST features. | object({…})
| | {}
| |
-| [federated_identity_providers](variables.tf#L108) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…}))
| | {}
| |
-| [groups](variables.tf#L122) | Group names to grant organization-level permissions. | map(string)
| | {…}
| |
-| [iam](variables.tf#L140) | Organization-level custom IAM settings in role => [principal] format. | map(list(string))
| | {}
| |
-| [iam_additive](variables.tf#L146) | Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string))
| | {}
| |
-| [locations](variables.tf#L152) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…})
| | {…}
| |
-| [log_sinks](variables.tf#L171) | Org-level log sinks, in name => {type, filter} format. | map(object({…}))
| | {…}
| |
-| [outputs_location](variables.tf#L205) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string
| | null
| |
-| [project_parent_ids](variables.tf#L221) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…})
| | {…}
| |
+| [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`. | object({…})
| ✓ | | |
+| [organization](variables.tf#L194) | Organization details. | object({…})
| ✓ | | |
+| [prefix](variables.tf#L209) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | |
+| [bootstrap_user](variables.tf#L27) | Email of the nominal user running this stage for the first time. | string
| | null
| |
+| [cicd_repositories](variables.tf#L33) | 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. | object({…})
| | null
| |
+| [custom_role_names](variables.tf#L79) | Names of custom roles defined at the org level. | object({…})
| | {…}
| |
+| [fast_features](variables.tf#L93) | Selective control for top-level FAST features. | object({…})
| | {}
| |
+| [federated_identity_providers](variables.tf#L106) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…}))
| | {}
| |
+| [groups](variables.tf#L120) | Group names to grant organization-level permissions. | map(string)
| | {…}
| |
+| [iam](variables.tf#L138) | Organization-level custom IAM settings in role => [principal] format. | map(list(string))
| | {}
| |
+| [iam_additive](variables.tf#L144) | Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string))
| | {}
| |
+| [locations](variables.tf#L150) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…})
| | {…}
| |
+| [log_sinks](variables.tf#L169) | Org-level log sinks, in name => {type, filter} format. | map(object({…}))
| | {…}
| |
+| [outputs_location](variables.tf#L203) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string
| | null
| |
+| [project_parent_ids](variables.tf#L219) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…})
| | {…}
| |
## Outputs
diff --git a/fast/stages/0-bootstrap/billing.tf b/fast/stages/0-bootstrap/billing.tf
index aee033bd8a..203ecbe865 100644
--- a/fast/stages/0-bootstrap/billing.tf
+++ b/fast/stages/0-bootstrap/billing.tf
@@ -24,13 +24,18 @@ locals {
module.automation-tf-bootstrap-sa.iam_email,
module.automation-tf-resman-sa.iam_email
]
+ billing_mode = (
+ var.billing_account.no_iam
+ ? null
+ : var.billing_account.is_org_level ? "org" : "resource"
+ )
}
# billing account in same org (IAM is in the organization.tf file)
module "billing-export-project" {
source = "../../../modules/project"
- count = var.billing_account.is_org_level ? 1 : 0
+ count = local.billing_mode == "org" ? 1 : 0
billing_account = var.billing_account.id
name = "billing-exp-0"
parent = coalesce(
@@ -52,7 +57,7 @@ module "billing-export-project" {
module "billing-export-dataset" {
source = "../../../modules/bigquery-dataset"
- count = var.billing_account.is_org_level ? 1 : 0
+ count = local.billing_mode == "org" ? 1 : 0
project_id = module.billing-export-project.0.project_id
id = "billing_export"
friendly_name = "Billing export."
@@ -63,7 +68,7 @@ module "billing-export-dataset" {
resource "google_billing_account_iam_member" "billing_ext_admin" {
for_each = toset(
- !var.billing_account.is_org_level ? local.billing_ext_admins : []
+ local.billing_mode == "resource" ? local.billing_ext_admins : []
)
billing_account_id = var.billing_account.id
role = "roles/billing.admin"
@@ -72,7 +77,7 @@ resource "google_billing_account_iam_member" "billing_ext_admin" {
resource "google_billing_account_iam_member" "billing_ext_cost_manager" {
for_each = toset(
- !var.billing_account.is_org_level ? local.billing_ext_admins : []
+ local.billing_mode == "resource" ? local.billing_ext_admins : []
)
billing_account_id = var.billing_account.id
role = "roles/billing.costsManager"
diff --git a/fast/stages/0-bootstrap/organization.tf b/fast/stages/0-bootstrap/organization.tf
index 154b37fe59..d75a25f2ff 100644
--- a/fast/stages/0-bootstrap/organization.tf
+++ b/fast/stages/0-bootstrap/organization.tf
@@ -85,7 +85,7 @@ locals {
# "domain:${var.organization.domain}"
# ]
},
- var.billing_account.is_org_level ? {
+ local.billing_mode == "org" ? {
"roles/billing.admin" = [
local.groups_iam.gcp-billing-admins,
local.groups_iam.gcp-organization-admins,
@@ -222,7 +222,7 @@ resource "google_organization_iam_binding" "org_admin_delegated" {
"roles/resourcemanager.organizationViewer",
module.organization.custom_role_id[var.custom_role_names.tenant_network_admin]
],
- var.billing_account.is_org_level ? [
+ local.billing_mode == "org" ? [
"roles/billing.admin",
"roles/billing.costsManager",
"roles/billing.user",
diff --git a/fast/stages/0-bootstrap/variables.tf b/fast/stages/0-bootstrap/variables.tf
index 4ab90debac..7aec437c7a 100644
--- a/fast/stages/0-bootstrap/variables.tf
+++ b/fast/stages/0-bootstrap/variables.tf
@@ -15,15 +15,13 @@
*/
variable "billing_account" {
- description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false."
+ 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)
})
- validation {
- condition = var.billing_account.is_org_level != null
- error_message = "Invalid `null` value for `billing_account.is_org_level`."
- }
+ nullable = false
}
variable "bootstrap_user" {
diff --git a/fast/stages/1-resman/README.md b/fast/stages/1-resman/README.md
index 971c69633d..781dc14d30 100644
--- a/fast/stages/1-resman/README.md
+++ b/fast/stages/1-resman/README.md
@@ -174,7 +174,18 @@ Due to its simplicity, this stage lends itself easily to customizations: adding
| name | description | modules | resources |
|---|---|---|---|
-| [billing.tf](./billing.tf) | Billing resources for external billing use cases. | | google_billing_account_iam_member
|
+| [billing.tf](./billing.tf) | Billing resources for external billing use cases. | |
+ )
+}
+
+# billing account in same org (resources is in the organization.tf file)
+
+# standalone billing account
+
+resource
· ? local.billing_ext_users : []
+ )
+ billing_account_id = var.billing_account.id
+ role =
· google_billing_account_iam_member
|
| [branch-data-platform.tf](./branch-data-platform.tf) | Data Platform stages resources. | folder
· gcs
· iam-service-account
| google_organization_iam_member
|
| [branch-gke.tf](./branch-gke.tf) | GKE multitenant stage resources. | folder
· gcs
· iam-service-account
| |
| [branch-networking.tf](./branch-networking.tf) | Networking stage resources. | folder
· gcs
· iam-service-account
| |
@@ -199,19 +210,19 @@ Due to its simplicity, this stage lends itself easily to customizations: adding
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…})
| ✓ | | 0-bootstrap
|
-| [billing_account](variables.tf#L38) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…})
| ✓ | | 0-bootstrap
|
-| [organization](variables.tf#L193) | Organization details. | object({…})
| ✓ | | 0-bootstrap
|
-| [prefix](variables.tf#L217) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 0-bootstrap
|
-| [cicd_repositories](variables.tf#L51) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…})
| | null
| |
-| [custom_roles](variables.tf#L133) | Custom roles defined at the org level, in key => id format. | object({…})
| | null
| 0-bootstrap
|
-| [data_dir](variables.tf#L142) | Relative path for the folder storing configuration data. | string
| | "data"
| |
-| [fast_features](variables.tf#L148) | Selective control for top-level FAST features. | object({…})
| | {}
| 0-0-bootstrap
|
-| [groups](variables.tf#L162) | Group names to grant organization-level permissions. | object({…})
| | {}
| 0-bootstrap
|
-| [locations](variables.tf#L175) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…})
| | {…}
| 0-bootstrap
|
-| [organization_policy_configs](variables.tf#L203) | Organization policies customization. | object({…})
| | null
| |
-| [outputs_location](variables.tf#L211) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string
| | null
| |
-| [tag_names](variables.tf#L228) | Customized names for resource management tags. | object({…})
| | {…}
| |
-| [team_folders](variables.tf#L247) | Team folders to be created. Format is described in a code comment. | map(object({…}))
| | null
| |
+| [billing_account](variables.tf#L38) | 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`. | object({…})
| ✓ | | 0-bootstrap
|
+| [organization](variables.tf#L191) | Organization details. | object({…})
| ✓ | | 0-bootstrap
|
+| [prefix](variables.tf#L215) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 0-bootstrap
|
+| [cicd_repositories](variables.tf#L49) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…})
| | null
| |
+| [custom_roles](variables.tf#L131) | Custom roles defined at the org level, in key => id format. | object({…})
| | null
| 0-bootstrap
|
+| [data_dir](variables.tf#L140) | Relative path for the folder storing configuration data. | string
| | "data"
| |
+| [fast_features](variables.tf#L146) | Selective control for top-level FAST features. | object({…})
| | {}
| 0-0-bootstrap
|
+| [groups](variables.tf#L160) | Group names to grant organization-level permissions. | object({…})
| | {}
| 0-bootstrap
|
+| [locations](variables.tf#L173) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…})
| | {…}
| 0-bootstrap
|
+| [organization_policy_configs](variables.tf#L201) | Organization policies customization. | object({…})
| | null
| |
+| [outputs_location](variables.tf#L209) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string
| | null
| |
+| [tag_names](variables.tf#L226) | Customized names for resource management tags. | object({…})
| | {…}
| |
+| [team_folders](variables.tf#L245) | Team folders to be created. Format is described in a code comment. | map(object({…}))
| | null
| |
## Outputs
diff --git a/fast/stages/1-resman/billing.tf b/fast/stages/1-resman/billing.tf
index ba20ab0534..c18708424a 100644
--- a/fast/stages/1-resman/billing.tf
+++ b/fast/stages/1-resman/billing.tf
@@ -30,6 +30,11 @@ locals {
local.branch_optional_sa_lists.pf-dev,
local.branch_optional_sa_lists.pf-prod,
)
+ billing_mode = (
+ var.billing_account.no_iam
+ ? null
+ : var.billing_account.is_org_level ? "org" : "resource"
+ )
}
# billing account in same org (resources is in the organization.tf file)
@@ -38,7 +43,7 @@ locals {
resource "google_billing_account_iam_member" "billing_ext_admin" {
for_each = toset(
- !var.billing_account.is_org_level ? local.billing_ext_users : []
+ local.billing_mode == "resource" ? local.billing_ext_users : []
)
billing_account_id = var.billing_account.id
role = "roles/billing.user"
@@ -47,7 +52,7 @@ resource "google_billing_account_iam_member" "billing_ext_admin" {
resource "google_billing_account_iam_member" "billing_ext_costsmanager" {
for_each = toset(
- !var.billing_account.is_org_level ? local.billing_ext_users : []
+ local.billing_mode == "resource" ? local.billing_ext_users : []
)
billing_account_id = var.billing_account.id
role = "roles/billing.costsManager"
diff --git a/fast/stages/1-resman/organization.tf b/fast/stages/1-resman/organization.tf
index a3bc2f0de0..4d5e3fa57d 100644
--- a/fast/stages/1-resman/organization.tf
+++ b/fast/stages/1-resman/organization.tf
@@ -46,7 +46,7 @@ module "organization" {
module.branch-network-sa.iam_email
]
},
- var.billing_account.is_org_level ? {
+ local.billing_mode == "org" ? {
"roles/billing.costsManager" = concat(
local.branch_optional_sa_lists.pf-dev,
local.branch_optional_sa_lists.pf-prod
diff --git a/fast/stages/1-resman/variables.tf b/fast/stages/1-resman/variables.tf
index 16c65ee83e..f2e413c981 100644
--- a/fast/stages/1-resman/variables.tf
+++ b/fast/stages/1-resman/variables.tf
@@ -37,15 +37,13 @@ variable "automation" {
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."
+ 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)
})
- validation {
- condition = var.billing_account.is_org_level != null
- error_message = "Invalid `null` value for `billing_account.is_org_level`."
- }
+ nullable = false
}
variable "cicd_repositories" {
diff --git a/fast/stages/2-networking-a-peering/README.md b/fast/stages/2-networking-a-peering/README.md
index c066423cdd..ac282c8d32 100644
--- a/fast/stages/2-networking-a-peering/README.md
+++ b/fast/stages/2-networking-a-peering/README.md
@@ -250,6 +250,20 @@ Variables in this stage -- like most other FAST stages -- are broadly divided in
The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document.
+### Using delayed billing association for projects
+
+This configuration is possible but unsupported and only exists for development purposes, use at your own risk:
+
+- temporarily switch `billing_account.id` to `null` in `globals.auto.tfvars.json`
+- for each project resources in the project modules used in this stage (`dev-spoke-project`, `landing-project`, `prod-spoke-project`)
+ - apply using `-target`, for example
+ `terraform apply -target 'module.landing-project.google_project.project[0]'`
+ - untaint the project resource after applying, for example
+ `terraform untaint 'module.landing-project.google_project.project[0]'`
+- go through the process to associate the billing account with the two projects
+- switch `billing_account.id` back to the real billing account id
+- resume applying normally
+
### Running the stage
Once provider and variable values are in place and the correct user is configured, the stage can be run:
diff --git a/fast/stages/2-networking-b-vpn/README.md b/fast/stages/2-networking-b-vpn/README.md
index 7a8983d81b..a34b41355d 100644
--- a/fast/stages/2-networking-b-vpn/README.md
+++ b/fast/stages/2-networking-b-vpn/README.md
@@ -264,6 +264,20 @@ Variables in this stage -- like most other FAST stages -- are broadly divided in
The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document.
+### Using delayed billing association for projects
+
+This configuration is possible but unsupported and only exists for development purposes, use at your own risk:
+
+- temporarily switch `billing_account.id` to `null` in `globals.auto.tfvars.json`
+- for each project resources in the project modules used in this stage (`dev-spoke-project`, `landing-project`, `prod-spoke-project`)
+ - apply using `-target`, for example
+ `terraform apply -target 'module.landing-project.google_project.project[0]'`
+ - untaint the project resource after applying, for example
+ `terraform untaint 'module.landing-project.google_project.project[0]'`
+- go through the process to associate the billing account with the two projects
+- switch `billing_account.id` back to the real billing account id
+- resume applying normally
+
### Running the stage
Once provider and variable values are in place and the correct user is configured, the stage can be run:
diff --git a/fast/stages/2-networking-c-nva/README.md b/fast/stages/2-networking-c-nva/README.md
index d0e62fd62a..33501984d9 100644
--- a/fast/stages/2-networking-c-nva/README.md
+++ b/fast/stages/2-networking-c-nva/README.md
@@ -323,6 +323,20 @@ Variables in this stage -- like most other FAST stages -- are broadly divided in
The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document.
+### Using delayed billing association for projects
+
+This configuration is possible but unsupported and only exists for development purposes, use at your own risk:
+
+- temporarily switch `billing_account.id` to `null` in `globals.auto.tfvars.json`
+- for each project resources in the project modules used in this stage (`dev-spoke-project`, `landing-project`, `prod-spoke-project`)
+ - apply using `-target`, for example
+ `terraform apply -target 'module.landing-project.google_project.project[0]'`
+ - untaint the project resource after applying, for example
+ `terraform untaint 'module.landing-project.google_project.project[0]'`
+- go through the process to associate the billing account with the two projects
+- switch `billing_account.id` back to the real billing account id
+- resume applying normally
+
### Running the stage
Once provider and variable values are in place and the correct user is configured, the stage can be run:
diff --git a/fast/stages/2-networking-d-separate-envs/README.md b/fast/stages/2-networking-d-separate-envs/README.md
index dfc199cd0f..3a3b70e7ec 100644
--- a/fast/stages/2-networking-d-separate-envs/README.md
+++ b/fast/stages/2-networking-d-separate-envs/README.md
@@ -212,6 +212,20 @@ Variables in this stage -- like most other FAST stages -- are broadly divided in
The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document.
+### Using delayed billing association for projects
+
+This configuration is possible but unsupported and only exists for development purposes, use at your own risk:
+
+- temporarily switch `billing_account.id` to `null` in `globals.auto.tfvars.json`
+- for each project resources in the project modules used in this stage (`dev-spoke-project`, `prod-spoke-project`)
+ - apply using `-target`, for example
+ `terraform apply -target 'module.prod-spoke-project.google_project.project[0]'`
+ - untaint the project resource after applying, for example
+ `terraform untaint 'module.prod-spoke-project.google_project.project[0]'`
+- go through the process to associate the billing account with the two projects
+- switch `billing_account.id` back to the real billing account id
+- resume applying normally
+
### Running the stage
Once provider and variable values are in place and the correct user is configured, the stage can be run:
diff --git a/fast/stages/2-security/README.md b/fast/stages/2-security/README.md
index 6486cd7418..ced74136ad 100644
--- a/fast/stages/2-security/README.md
+++ b/fast/stages/2-security/README.md
@@ -108,6 +108,20 @@ Variables in this stage -- like most other FAST stages -- are broadly divided in
The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document.
+### Using delayed billing association for projects
+
+This configuration is possible but unsupported and only exists for development purposes, use at your own risk:
+
+- temporarily switch `billing_account.id` to `null` in `globals.auto.tfvars.json`
+- for each project resources in the project modules used in this stage (`dev-sec-project`, `prod-sec-project`)
+ - apply using `-target`, for example
+ `terraform apply -target 'module.prod-sec-project.google_project.project[0]'`
+ - untaint the project resource after applying, for example
+ `terraform untaint 'module.prod-sec-project.google_project.project[0]'`
+- go through the process to associate the billing account with the two projects
+- switch `billing_account.id` back to the real billing account id
+- resume applying normally
+
### Running the stage
Once provider and variable values are in place and the correct user is configured, the stage can be run: