Refactor FAST project factory and supporting documentation (#2505)

* untested

* teams pattern

* rework doc

* README

* boierplate

* tflint

* Fix tflint for project factory

* Correct path to pf

* resman changes

* fix factory variable default

* fix links

* project factory module substitutions

* tflint

* stage test

* tfdoc

* rename schema, address review comments

* README typos and wording

* tfdoc

* review comments

* remove test from yaml

* revert output workflow changes

* fix sa reference errors

* tfdoc

* pf tag roles

* schema validation

* pf tag roles

* avoid null values in pf context

---------

Co-authored-by: Wiktor Niesiobędzki <wiktorn@google.com>
This commit is contained in:
Ludovico Magnocavallo
2024-08-20 17:45:42 +01:00
committed by GitHub
parent dff209f565
commit ad5de9b7ea
57 changed files with 1948 additions and 2392 deletions

View File

@@ -27,6 +27,7 @@ The code is meant to be executed by a high level service accounts with powerful
- [Service accounts](#service-accounts)
- [Automation project and resources](#automation-project-and-resources)
- [Billing budgets](#billing-budgets)
- [Substitutions in YAML configurations attributes](#substitutions-in-yaml-configurations-attributes)
- [Example](#example)
- [Files](#files)
- [Variables](#variables)
@@ -38,12 +39,9 @@ The code is meant to be executed by a high level service accounts with powerful
The hierarchy supports up to three levels of folders, which are defined via filesystem directories each including a `_config.yaml` files detailing their attributes.
The hierarchy factory is configured via the `factories_config.hierarchy` variable via one mandatory and one optional argument:
The hierarchy factory is configured via the `factories_config.folders_data_path` variable, which sets the the path containing the YAML definitions for folders.
- `factories_config.hierarchy.folders_data_path` is required to enable the hierarchy factory, and must be set to the path containing the YAML definitions
- `factories_config.hierarchy.parent_ids` is an optional map where keys are arbitrary and values are set to resource node ids
Top-level folders in the filesystem hierarchy have no explicit parent, so their parent ids need to be provided in the YAML by either referencing the full id (e.g. `folders/12345678`) or by referencing a key in the `parent_ids` attribute described above. As a shortcut, a `default` key can be defined whose value is used for any top-level folder which does not directly provide a parent id.
Parent ids for top-level folders can either be set explicitly (e.g. `folders/12345678`) or via substitutions, by referring to keys in the `context.folder_ids` variable. The special `default` key in the substitutions folder variable is used if present and no folder id/key has been specified in the YAML.
Filesystem directories can also contain project definitions in the same YAML format described below. This approach must be used with caution and is best adopted for stable scenarios, as problems in the filesystem hierarchy definitions might result in the project files not being read and the resources being deleted by Terraform.
@@ -67,7 +65,7 @@ Some examples on where to use each of the three sets are [provided below](#examp
Service accounts can be managed as part of each project's YAML configuration. This allows creation of default service accounts used for GCE instances, in firewall rules, or for application-level credentials without resorting to a separate Terraform configuration.
Each service account is represented by one key and a set of optional key/value pairs in the `service_accounts` top-level YAML map, which expose most of the variables available in the `iam-service-account` module:
Each service account is represented by one key and a set of optional key/value pairs in the `service_accounts` top-level YAML map, which exposes most of the variables available in the `iam-service-account` module:
```yaml
service_accounts:
@@ -154,6 +152,58 @@ billing_budgets:
A simple billing budget example is show in the [example](#example) below.
## Substitutions in YAML configurations attributes
Substitutions allow referring via short mnemonic names to resources which are either created at runtime, or externally manages.
This feature has two main benefits:
- being able to refer to resource ids which cannot be known before creation, for example project automation service accounts in IAM bindings
- making YAML configuration files more easily readable and portable, by using mnemonic keys which are not specific to an organization or project
One example of both types of substitutions is in this project snippet. The automation service account is used in IAM bindings via its `rw` key, while the parent folder is set by referring to its path in the hierarchy factory.
```yaml
parent: teams/team-a
iam:
"roles/owner":
- rw
automation:
project: ta-app0-0
service_accounts:
rw:
description: Read/write automation sa for team a app 0.
buckets:
state:
description: Terraform state bucket for team a app 0.
iam:
roles/storage.objectCreator:
- rw
```
Substitutions come from two separate context sources: an internal set for resources managed by the project factory (folders, service accounts, etc.), and an external user-defined set passed in via the `factories_config.context` variable.
Internal substitutions are:
- hierarchy folders, used to set project parents via the filesystem path of folders (e.g. `teams/team-a`)
- automation service accounts, used in project IAM bindings via their keys; this does not work in folder IAM bindings
External substitution are:
- the map of folder ids in `factories_config.context.folder_ids`, used to set top-level folder parents; the `default` key if present is used when no explicit parent has been set in the YAML file
- the map of IAM principals in `factories_config.context.iam_principals`, used in IAM bindings for folders and projects; the exception is the `iam_by_principals` attribute which uses no interpolation to prevent dynamic cycles
- the map of tag value ids in `factories_config.context.tag_values` used in tag bindings for folders and projects
- the map of Shared VPC host project ids in `factories_config.context.vpc_host_projects` used in service project configurations for projects
External substitution maps are optional, and there's no harm in not defining them if not used.
Some caveats on substitutions:
- project-own service accounts are not part of substitutions to prevent cycles, you can use the `iam_project_roles` and `iam_self_roles` attributes for additive IAM on projects
- project shared vpc configurations and project-own service accounts only support external substitutions to prevent cycles
- projects for automation service accounts and buckets do not support substitutions to prevent cycles
- no substitutions are implemented (yet) for budgets
## Example
The module invocation using all optional features:
@@ -177,7 +227,7 @@ module "project-factory" {
# always use this contaxt and prefix, regardless of what is in the yaml file
data_overrides = {
contacts = {
"admin@example.com" = ["ALL"]
"admin@example.org" = ["ALL"]
}
prefix = "test-pf"
}
@@ -191,47 +241,69 @@ module "project-factory" {
project_id = "foo-billing-audit"
type = "email"
labels = {
email_address = "gcp-billing-admins@example.com"
email_address = "gcp-billing-admins@example.org"
}
}
}
}
hierarchy = {
folders_data_path = "data/hierarchy"
parent_ids = {
default = "folders/12345678"
folders_data_path = "data/hierarchy"
projects_data_path = "data/projects"
context = {
folder_ids = {
default = "folders/5678901234"
teams = "folders/5678901234"
}
iam_principals = {
gcp-devops = "group:gcp-devops@example.org"
}
tag_values = {
"org-policies/drs-allow-all" = "tagValues/123456"
}
vpc_host_projects = {
dev-spoke-0 = "test-pf-dev-net-spoke-0"
}
}
projects_data_path = "data/projects"
}
}
# tftest modules=16 resources=70 files=prj-app-1,prj-app-2,prj-app-3,budget-test-100,h-0-0,h-1-0,h-0-1,h-1-1,h-1-1-p0 inventory=example.yaml
# tftest modules=15 resources=56 files=0,1,2,3,4,5,6,7,8 inventory=example.yaml
```
A simple hierarchy of folders:
```yaml
name: Foo (level 1)
name: Team A
# implicit parent definition via 'default' key
iam:
roles/viewer:
- group:a@example.com
# tftest-file id=h-0-0 path=data/hierarchy/foo/_config.yaml schema=folder.schema.json
- group:team-a-admins@example.org
- gcp-devops
# tftest-file id=0 path=data/hierarchy/team-a/_config.yaml schema=folder.schema.json
```
```yaml
name: Bar (level 1)
parent: folders/4567890
# tftest-file id=h-1-0 path=data/hierarchy/bar/_config.yaml schema=folder.schema.json
name: Team B
# explicit parent definition via key
parent: teams
# tftest-file id=1 path=data/hierarchy/team-b/_config.yaml schema=folder.schema.json
```
```yaml
name: Foo Baz (level 2)
# tftest-file id=h-0-1 path=data/hierarchy/foo/baz/_config.yaml schema=folder.schema.json
name: Team C
# explicit parent definition via folder id
parent: folders/5678901234
# tftest-file id=2 path=data/hierarchy/team-c/_config.yaml schema=folder.schema.json
```
```yaml
name: Bar Baz (level 2)
# tftest-file id=h-1-1 path=data/hierarchy/bar/baz/_config.yaml schema=folder.schema.json
name: App 0
# tftest-file id=3 path=data/hierarchy/team-a/app-0/_config.yaml schema=folder.schema.json
```
```yaml
name: App 0
tag_bindings:
drs-allow-all: org-policies/drs-allow-all
# tftest-file id=4 path=data/hierarchy/team-b/app-0/_config.yaml schema=folder.schema.json
```
One project defined within the folder hierarchy:
@@ -241,83 +313,58 @@ billing_account: 012345-67890A-BCDEF0
services:
- container.googleapis.com
- storage.googleapis.com
# tftest-file id=h-1-1-p0 path=data/hierarchy/bar/baz/bar-baz-iac-0.yaml schema=project.schema.json
# tftest-file id=5 path=data/hierarchy/teams-iac-0.yaml schema=project.schema.json
```
More traditional project definitions via the project factory data:
```yaml
# project app-1
billing_account: 012345-67890A-BCDEF0
labels:
app: app-1
team: foo
parent: folders/12345678
app: app-0
team: team-a
parent: team-a/app-0
service_encryption_key_ids:
storage.googleapis.com:
- projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce
- projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce
services:
- container.googleapis.com
- storage.googleapis.com
service_accounts:
app-1-be:
app-0-be:
display_name: "Backend instances."
iam_project_roles:
dev-spoke-0:
- roles/compute.networkUser
iam_self_roles:
- roles/logging.logWriter
- roles/monitoring.metricWriter
- roles/logging.logWriter
- roles/monitoring.metricWriter
app-0-fe:
display_name: "Frontend instances."
iam_project_roles:
my-host-project:
- roles/compute.networkUser
app-1-fe:
display_name: "Test app 1 frontend."
iam_project_roles:
my-host-project:
dev-spoke-0:
- roles/compute.networkUser
iam_self_roles:
- roles/logging.logWriter
- roles/monitoring.metricWriter
shared_vpc_service_config:
host_project: dev-spoke-0
network_users:
- gcp-devops
service_agent_iam:
"roles/container.hostServiceAgentUser":
- container-engine
"roles/compute.networkUser":
- container-engine
billing_budgets:
- test-100
# tftest-file id=prj-app-1 path=data/projects/prj-app-1.yaml schema=project.schema.json
# tftest-file id=6 path=data/projects/dev-ta-app0-be.yaml schema=project.schema.json
```
```yaml
# project app-2
labels:
app: app-2
team: foo
parent: folders/12345678
org_policies:
"compute.restrictSharedVpcSubnetworks":
rules:
- allow:
values:
- projects/foo-host/regions/europe-west1/subnetworks/prod-default-ew1
service_accounts:
app-2-be: {}
services:
- compute.googleapis.com
- container.googleapis.com
- run.googleapis.com
- storage.googleapis.com
shared_vpc_service_config:
host_project: foo-host
service_agent_iam:
"roles/vpcaccess.user":
- cloudrun
"roles/container.hostServiceAgentUser":
- container-engine
service_agent_subnet_iam:
europe-west1/prod-default-ew1:
- cloudservices
- container-engine
network_subnet_users:
europe-west1/prod-default-ew1:
- group:team-1@example.com
# tftest-file id=prj-app-2 path=data/projects/prj-app-2.yaml schema=project.schema.json
```
This project uses a reference to a hierarchy folder, and defines a controlling project via the `automation` attributes:
This project defines a controlling project via the `automation` attributes:
```yaml
parent: bar/baz
parent: team-b/app-0
services:
- run.googleapis.com
- storage.googleapis.com
@@ -329,28 +376,28 @@ iam:
shared_vpc_host_config:
enabled: true
automation:
project: bar-baz-iac-0
project: test-pf-teams-iac-0
service_accounts:
rw:
description: Read/write automation sa for app example 0.
description: Team B app 0 read/write automation sa.
ro:
description: Read-only automation sa for app example 0.
description: Team B app 0 read-only automation sa.
buckets:
state:
description: Terraform state bucket for app example 0.
description: Team B app 0 Terraform state bucket.
iam:
roles/storage.objectCreator:
- rw
roles/storage.objectViewer:
- gcp-devops
- group:team-b-admins@example.org
- rw
- ro
- group:devops@example.org
# tftest-file id=prj-app-3 path=data/projects/prj-app-3.yaml schema=project.schema.json
# tftest-file id=7 path=data/projects/dev-tb-app0-0.yaml schema=project.schema.json
```
And a billing budget:
A billing budget:
```yaml
# billing budget test-100
@@ -370,7 +417,7 @@ update_rules:
disable_default_iam_recipients: true
monitoring_notification_channels:
- billing-default
# tftest-file id=budget-test-100 path=data/budgets/test-100.yaml schema=budget.schema.json
# tftest-file id=8 path=data/budgets/test-100.yaml schema=budget.schema.json
```
<!-- TFDOC OPTS files:1 -->
@@ -392,7 +439,7 @@ update_rules:
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [factories_config](variables.tf#L96) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; hierarchy &#61; optional&#40;object&#40;&#123;&#10; folders_data_path &#61; string&#10; parent_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; projects_data_path &#61; optional&#40;string&#41;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account &#61; string&#10; budgets_data_path &#61; string&#10; notification_channels &#61; optional&#40;map&#40;any&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [factories_config](variables.tf#L96) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; folders_data_path &#61; optional&#40;string&#41;&#10; projects_data_path &#61; optional&#40;string&#41;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account &#61; string&#10; budgets_data_path &#61; string&#10; notification_channels &#61; optional&#40;map&#40;any&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; context &#61; optional&#40;object&#40;&#123;&#10; folder_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_values &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; vpc_host_projects &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; shared_vpc_service_config &#61; optional&#40;object&#40;&#123;&#10; host_project &#61; string&#10; network_users &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_agent_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_agent_subnet_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_iam_grants &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; network_subnet_users &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;, &#123; host_project &#61; null &#125;&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [data_merges](variables.tf#L52) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | <code title="object&#40;&#123;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [data_overrides](variables.tf#L71) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |