From 9395fbc822671037c2bc7adb13ca0502ea65a8d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sat, 11 Feb 2023 10:59:50 +0000 Subject: [PATCH 01/47] Add documentation about JIT-ed service accounts --- modules/project/README.md | 32 +++++++++++++++++++++++++++++ modules/project/service-accounts.tf | 21 +++++++++++-------- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/modules/project/README.md b/modules/project/README.md index 3753a5da..de37503e 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -138,6 +138,38 @@ module "project" { # tftest modules=1 resources=2 ``` +### Service identities requiring manual IAM grants +The module will create service identities at the time of the creation of the project instead creation of them at the time of first use. +This allows granting these service identities roles in other projects which is usually necessary in Shared VPC context. +You can grant those roles using following construct: + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "project-example" + iam = { + "roles/apigee.serviceAgent" = [ + "serviceAccount:${module.project.service_accounts.robots.apigee}" + ] + } +} +# tftest modules=1 resources=2 +``` + +This table lists all affected services and roles that you need to grant to service identities + +| service | service identity | role | +|---|---|---| +| apigee.googleapis.com | apigee | roles/apigee.serviceAgent | +| artifactregistry.googleapis.com | artifactregistry | roles/artifactregistry.serviceAgent | +| cloudasset.googleapis.com | cloudasset | roles/cloudasset.serviceAgent | +| cloudbuild.googleapis.com | cloudbuild | roles/cloudbuild.builds.builder | +| gkehub.googleapis.com | fleet | roles/gkehub.serviceAgent | +| multiclusteringress.googleapis.com | multicluster-ingress | roles/multiclusteringress.serviceAgent | +| pubsub.googleapis.com | pubsub | roles/pubsub.serviceAgent | +| sqladmin.googleapis.com | sqladmin | roles/cloudsql.serviceAgent | + + ## Shared VPC The module allows managing Shared VPC status for both hosts and service projects, and includes a simple way of assigning Shared VPC roles to service identities. diff --git a/modules/project/service-accounts.tf b/modules/project/service-accounts.tf index 1979958b..e93978a8 100644 --- a/modules/project/service-accounts.tf +++ b/modules/project/service-accounts.tf @@ -70,16 +70,19 @@ locals { gke-mcs-importer = "${local.project.project_id}.svc.id.goog[gke-mcs/gke-mcs-importer]" } ) + # JIT-ed service accounts are created without default roles granted, these needs to be assigned manually to them + # Roles can be found here: https://cloud.google.com/iam/docs/service-agents + # Remember to update "Service identities requiring manual IAM grants" in README.md when updating this list service_accounts_jit_services = [ - "apigee.googleapis.com", - "artifactregistry.googleapis.com", - "cloudasset.googleapis.com", - "gkehub.googleapis.com", - "multiclusteringress.googleapis.com", - "pubsub.googleapis.com", - "secretmanager.googleapis.com", - "sqladmin.googleapis.com", - "cloudbuild.googleapis.com", + "apigee.googleapis.com", # grant roles/apigee.serviceAgent to apigee + "artifactregistry.googleapis.com", # grant roles/artifactregistry.serviceAgent to artifactregistry + "cloudasset.googleapis.com", # grant roles/cloudasset.serviceAgent to cloudasset + "cloudbuild.googleapis.com", # grant roles/cloudbuild.builds.builder to cloudbuild + "gkehub.googleapis.com", # grant roles/gkehub.serviceAgent to fleet + "multiclusteringress.googleapis.com", # grant roles/multiclusteringress.serviceAgent to multicluster-ingress + "pubsub.googleapis.com", # grant roles/pubsub.serviceAgent to pubsub + "secretmanager.googleapis.com", # no grants needed + "sqladmin.googleapis.com", # grant roles/cloudsql.serviceAgent to sqladmin (TODO: verify) ] service_accounts_cmek_service_keys = distinct(flatten([ for s in keys(var.service_encryption_key_ids) : [ From 98a08c159a7a74ad8a5e095a9055fa4269017242 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Sat, 11 Feb 2023 13:20:24 +0100 Subject: [PATCH 02/47] Update README.md --- modules/project/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/project/README.md b/modules/project/README.md index de37503e..fbc4ab29 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -139,9 +139,10 @@ module "project" { ``` ### Service identities requiring manual IAM grants -The module will create service identities at the time of the creation of the project instead creation of them at the time of first use. -This allows granting these service identities roles in other projects which is usually necessary in Shared VPC context. -You can grant those roles using following construct: + +The module will create service identities at project creation instead of creating of them at the time of first use. This allows granting these service identities roles in other projects, something which is usually necessary in a Shared VPC context. + +You can grant roles to service identities using the following construct: ```hcl module "project" { From 3a0a1e2f6d999923ae69db3ff45f0b593a2c87c5 Mon Sep 17 00:00:00 2001 From: Luca Prete Date: Sat, 11 Feb 2023 17:45:16 +0100 Subject: [PATCH 03/47] net-ilb: add example about ref existing MIG example (#1151) --- modules/net-ilb/README.md | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/modules/net-ilb/README.md b/modules/net-ilb/README.md index c284c4c6..48c1d908 100644 --- a/modules/net-ilb/README.md +++ b/modules/net-ilb/README.md @@ -12,6 +12,62 @@ One other issue is a `Provider produced inconsistent final plan` error which is ## Examples +### Reference existing MIGs + +This example shows how to reference existing Managed Infrastructure Groups (MIGs). + +```hcl +module "instance_template" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + create_template = true + name = "vm-test" + service_account_create = true + zone = "europe-west1-b" + + network_interfaces = [ + { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + } + ] + + tags = [ + "http-server" + ] +} + +module "mig" { + source = "./fabric/modules/compute-mig" + project_id = var.project_id + location = "europe-west1" + name = "mig-test" + target_size = 1 + instance_template = module.instance_template.template.self_link +} + +module "ilb" { + source = "./fabric/modules/net-ilb" + project_id = var.project_id + region = "europe-west1" + name = "ilb-test" + service_label = "ilb-test" + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + } + backends = [{ + group = module.mig.group_manager.instance_group + }] + health_check_config = { + http = { + port = 80 + } + } +} +# tftest modules=3 resources=6 +``` + ### Externally managed instances This examples shows how to create an ILB by combining externally managed instances (in a custom module or even outside of the current root module) in an unmanaged group. When using internally managed groups, remember to run `terraform apply` each time group instances change. From 15487078d438e5fd13a7b32427db7bda7dad7392 Mon Sep 17 00:00:00 2001 From: James D'Alfonso Date: Mon, 13 Feb 2023 10:00:50 +0100 Subject: [PATCH 04/47] add missing iam properties to factory_subnets --- modules/net-vpc/subnets.tf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/net-vpc/subnets.tf b/modules/net-vpc/subnets.tf index ae094ecf..7c03bfca 100644 --- a/modules/net-vpc/subnets.tf +++ b/modules/net-vpc/subnets.tf @@ -31,6 +31,9 @@ locals { flow_logs_config = try(v.flow_logs, null) ipv6 = try(v.ipv6, null) secondary_ip_ranges = try(v.secondary_ip_ranges, null) + iam_groups = try(v.iam_groups, []) + iam_users = try(v.iam_users, []) + iam_service_accounts = try(v.iam_service_accounts, []) } } _factory_subnets_iam = [ From ebc4bc51a519247f4a900d021e4d913bebd4e8d9 Mon Sep 17 00:00:00 2001 From: lcaggio Date: Mon, 13 Feb 2023 15:25:24 +0100 Subject: [PATCH 05/47] Workaround to mitigate issue 9164 --- blueprints/data-solutions/data-playground/main.tf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/blueprints/data-solutions/data-playground/main.tf b/blueprints/data-solutions/data-playground/main.tf index b87e8e73..548bee37 100644 --- a/blueprints/data-solutions/data-playground/main.tf +++ b/blueprints/data-solutions/data-playground/main.tf @@ -217,6 +217,11 @@ resource "google_notebooks_instance" "playground" { service_account = module.service-account-notebook.email + # Remove once terraform-provider-google/issues/9164 is fixed + lifecycle { + ignore_changes = [disk_encryption, kms_key] + } + #TODO Uncomment once terraform-provider-google/issues/9273 is fixed # tags = ["ssh"] depends_on = [ From 7bbeac805e1dde5d1c14e52368606073755342a9 Mon Sep 17 00:00:00 2001 From: lcaggio Date: Tue, 14 Feb 2023 08:43:15 +0100 Subject: [PATCH 06/47] Add 'max_time_travel_hours ' on BQ module --- modules/bigquery-dataset/README.md | 9 +++++---- modules/bigquery-dataset/main.tf | 2 +- modules/bigquery-dataset/variables.tf | 13 +++++-------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/modules/bigquery-dataset/README.md b/modules/bigquery-dataset/README.md index 381ffab7..6dbd120c 100644 --- a/modules/bigquery-dataset/README.md +++ b/modules/bigquery-dataset/README.md @@ -67,6 +67,7 @@ module "bigquery-dataset" { default_table_expiration_ms = 3600000 default_partition_expiration_ms = null delete_contents_on_destroy = false + max_time_travel_hours = 168 } } # tftest modules=1 resources=1 @@ -178,7 +179,7 @@ module "bigquery-dataset" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [id](variables.tf#L69) | Dataset id. | string | ✓ | | -| [project_id](variables.tf#L100) | Id of the project where datasets will be created. | string | ✓ | | +| [project_id](variables.tf#L97) | Id of the project where datasets will be created. | string | ✓ | | | [access](variables.tf#L17) | Map of access rules with role and identity type. Keys are arbitrary and must match those in the `access_identities` variable, types are `domain`, `group`, `special_group`, `user`, `view`. | map(object({…})) | | {} | | [access_identities](variables.tf#L33) | Map of access identities used for basic access roles. View identities have the format 'project_id\|dataset_id\|table_id'. | map(string) | | {} | | [dataset_access](variables.tf#L39) | Set access in the dataset resource instead of using separate resources. | bool | | false | @@ -188,9 +189,9 @@ module "bigquery-dataset" { | [iam](variables.tf#L63) | IAM bindings in {ROLE => [MEMBERS]} format. Mutually exclusive with the access_* variables used for basic roles. | map(list(string)) | | {} | | [labels](variables.tf#L74) | Dataset labels. | map(string) | | {} | | [location](variables.tf#L80) | Dataset location. | string | | "EU" | -| [options](variables.tf#L86) | Dataset options. | object({…}) | | {…} | -| [tables](variables.tf#L105) | Table definitions. Options and partitioning default to null. Partitioning can only use `range` or `time`, set the unused one to null. | map(object({…})) | | {} | -| [views](variables.tf#L133) | View definitions. | map(object({…})) | | {} | +| [options](variables.tf#L86) | Dataset options. | object({…}) | | {} | +| [tables](variables.tf#L102) | Table definitions. Options and partitioning default to null. Partitioning can only use `range` or `time`, set the unused one to null. | map(object({…})) | | {} | +| [views](variables.tf#L130) | View definitions. | map(object({…})) | | {} | ## Outputs diff --git a/modules/bigquery-dataset/main.tf b/modules/bigquery-dataset/main.tf index 47f8fcb5..f832cd85 100644 --- a/modules/bigquery-dataset/main.tf +++ b/modules/bigquery-dataset/main.tf @@ -42,7 +42,7 @@ resource "google_bigquery_dataset" "default" { delete_contents_on_destroy = var.options.delete_contents_on_destroy default_table_expiration_ms = var.options.default_table_expiration_ms default_partition_expiration_ms = var.options.default_partition_expiration_ms - + max_time_travel_hours = var.options.max_time_travel_hours dynamic "access" { for_each = var.dataset_access ? local.access_domain : {} content { diff --git a/modules/bigquery-dataset/variables.tf b/modules/bigquery-dataset/variables.tf index 5f8028ab..b44b6658 100644 --- a/modules/bigquery-dataset/variables.tf +++ b/modules/bigquery-dataset/variables.tf @@ -86,15 +86,12 @@ variable "location" { variable "options" { description = "Dataset options." type = object({ - default_table_expiration_ms = number - default_partition_expiration_ms = number - delete_contents_on_destroy = bool + default_table_expiration_ms = optional(number, null) + default_partition_expiration_ms = optional(number, null) + delete_contents_on_destroy = optional(bool, false) + max_time_travel_hours = optional(number, 168) }) - default = { - default_table_expiration_ms = null - default_partition_expiration_ms = null - delete_contents_on_destroy = false - } + default = {} } variable "project_id" { From 742b5bab62ab29f573f707cd9f43d417893f91e0 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 14 Feb 2023 11:29:19 +0200 Subject: [PATCH 07/47] Fix tfvars sample for fast bootstrap stage --- fast/stages/0-bootstrap/terraform.tfvars.sample | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/fast/stages/0-bootstrap/terraform.tfvars.sample b/fast/stages/0-bootstrap/terraform.tfvars.sample index 66710ba4..a134f8dc 100644 --- a/fast/stages/0-bootstrap/terraform.tfvars.sample +++ b/fast/stages/0-bootstrap/terraform.tfvars.sample @@ -1,15 +1,14 @@ # use `gcloud beta billing accounts list` # if you have too many accounts, check the Cloud Console :) billing_account = { - id = "012345-67890A-BCDEF0" - organization_id = 1234567890 + id = "012345-67890A-BCDEF0" } # use `gcloud organizations list` organization = { - domain = "example.org" - id = 1234567890 - customer_id = "C000001" + domain = "example.org" + id = 1234567890 + customer_id = "C000001" } outputs_location = "~/fast-config" From e8334857ff46b12c2396214675bb8e15c6284eec Mon Sep 17 00:00:00 2001 From: Chema Polo Date: Wed, 15 Feb 2023 06:28:47 +0100 Subject: [PATCH 08/47] Update main.tf (#1158) replaced .secondary_pod_range by var.pod_range.secondary_pod_range that is the object which contins create, cidr an name attributes. --- modules/gke-nodepool/main.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/gke-nodepool/main.tf b/modules/gke-nodepool/main.tf index ad0c053f..9ae4cf28 100644 --- a/modules/gke-nodepool/main.tf +++ b/modules/gke-nodepool/main.tf @@ -115,9 +115,9 @@ resource "google_container_node_pool" "nodepool" { dynamic "network_config" { for_each = var.pod_range != null ? [""] : [] content { - create_pod_range = var.pod_range.create - pod_ipv4_cidr_block = var.pod_range.cidr - pod_range = var.pod_range.name + create_pod_range = var.pod_range.secondary_pod_range.create + pod_ipv4_cidr_block = var.pod_range.secondary_pod_range.cidr + pod_range = var.pod_range.secondary_pod_range.name } } From 36a7347744b84fe38e463feff18d785d890afc32 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Wed, 15 Feb 2023 06:42:14 +0100 Subject: [PATCH 09/47] FAST stage docs cleanup (#1145) * top-level and stage 0 * stage 1 * net peering * networking * networking * security * gke, dp * checks --- fast/stage-links.sh | 5 + fast/stages/0-bootstrap/README.md | 111 ++++++----- fast/stages/1-resman/README.md | 76 +++++--- fast/stages/2-networking-a-peering/README.md | 176 ++++++++++------- fast/stages/2-networking-b-vpn/README.md | 174 ++++++++++------- fast/stages/2-networking-c-nva/README.md | 183 +++++++++++------- .../2-networking-d-separate-envs/README.md | 163 ++++++++++------ fast/stages/2-security/README.md | 84 ++++---- fast/stages/3-data-platform/dev/README.md | 111 ++++++----- fast/stages/3-gke-multitenant/dev/README.md | 108 +++++++---- fast/stages/CLEANUP.md | 14 +- fast/stages/COMPANION.md | 56 +++--- fast/stages/FAQ.md | 40 ++-- 13 files changed, 757 insertions(+), 544 deletions(-) diff --git a/fast/stage-links.sh b/fast/stage-links.sh index 79d1973f..52c9e5ae 100755 --- a/fast/stage-links.sh +++ b/fast/stage-links.sh @@ -78,6 +78,11 @@ case $STAGE_NAME in TFVARS="tfvars/0-bootstrap.auto.tfvars.json tfvars/1-resman.auto.tfvars.json" ;; +"2-security"*) + PROVIDER="providers/2-security-providers.tf" + TFVARS="tfvars/0-bootstrap.auto.tfvars.json + tfvars/1-resman.auto.tfvars.json" + ;; *) # check for a "dev" stage 3 echo "no stage found, trying for parent stage 3..." diff --git a/fast/stages/0-bootstrap/README.md b/fast/stages/0-bootstrap/README.md index 2cab1151..e1bb2948 100644 --- a/fast/stages/0-bootstrap/README.md +++ b/fast/stages/0-bootstrap/README.md @@ -14,6 +14,28 @@ Use the following diagram as a simple high level reference for the following sec Organization-level diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [User groups](#user-groups) + - [Organization-level IAM](#organization-level-iam) + - [Automation project and resources](#automation-project-and-resources) + - [Billing account](#billing-account) + - [Organization-level logging](#organization-level-logging) + - [Naming](#naming) + - [Workload Identity Federation and CI/CD](#workload-identity-federation-and-cicd) +- [How to run this stage](#how-to-run-this-stage) + - [Prerequisites](#prerequisites) + - [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) + - [CI/CD repositories](#cicd-repositories) + ## 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. @@ -80,7 +102,7 @@ The convention is used in its full form only for specific resources with globall The [Customizations](#names-and-naming-convention) section on names below explains how to configure tokens, or implement a different naming convention. -## Workload Identity Federation and CI/CD +### Workload Identity Federation and CI/CD This stage also implements initial support for two interrelated features @@ -124,7 +146,7 @@ To quickly self-grant the above roles, run the following code snippet as the ini export FAST_BU=$(gcloud config list --format 'value(core.account)') # find and set your org id -gcloud organizations list --filter display_name:$partofyourdomain +gcloud organizations list export FAST_ORG_ID=123456 # set needed roles @@ -139,25 +161,6 @@ 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. -#### Billing account in a different organization - -If you are using a billing account belonging to a different organization (e.g. in multiple organization setups), some initial configurations are needed to ensure the identities running this stage can assign billing-related roles. - -If the billing organization is managed by another version of this stage, we leverage the `organizationIamAdmin` role created there, to allow restricted granting of billing roles at the organization level. - -If that's not the case, an equivalent role needs to exist, or the predefined `resourcemanager.organizationAdmin` role can be used if not managed authoritatively. The role name then needs to be manually changed in the `billing.tf` file, in the `google_organization_iam_binding` resource. - -The identity applying this stage for the first time also needs two roles in billing organization, they can be removed after the first `apply` completes successfully: - -```bash -export FAST_BILLING_ORG_ID=789012 -export FAST_ROLES=(roles/billing.admin roles/resourcemanager.organizationAdmin) -for role in $FAST_ROLES; do - gcloud organizations add-iam-policy-binding $FAST_BILLING_ORG_ID \ - --member user:$FAST_BU --role $role -done -``` - #### 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: @@ -187,7 +190,7 @@ Please note that FAST also supports an additional group for users with permissio 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 `organization_id` as the id of the organization owning it, or `null` to use the billing account in isolation + 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` @@ -202,7 +205,6 @@ You can also adapt the example that follows to your needs: # if you have too many accounts, check the Cloud Console :) billing_account = { id = "012345-67890A-BCDEF0" - organization_id = 1234567890 } # use `gcloud organizations list` @@ -237,18 +239,18 @@ Below is the outline of the output files generated by all stages, which is ident ```bash [path specified in outputs_location] ├── providers -│   ├── 00-bootstrap-providers.tf -│   ├── 01-resman-providers.tf -│   ├── 02-networking-providers.tf -│   ├── 02-security-providers.tf -│   ├── 03-project-factory-dev-providers.tf -│   ├── 03-project-factory-prod-providers.tf -│   └── 99-sandbox-providers.tf +│   ├── 0-bootstrap-providers.tf +│   ├── 1-resman-providers.tf +│   ├── 2-networking-providers.tf +│   ├── 2-security-providers.tf +│   ├── 3-project-factory-dev-providers.tf +│   ├── 3-project-factory-prod-providers.tf +│   └── 9-sandbox-providers.tf └── tfvars -│ ├── 00-bootstrap.auto.tfvars.json -│ ├── 01-resman.auto.tfvars.json -│ ├── 02-networking.auto.tfvars.json -│ └── 02-security.auto.tfvars.json +│ ├── 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] ``` @@ -267,17 +269,34 @@ terraform apply \ > 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 if you have configured output files as described above, or extract its contents from Terraform's output, then migrate state with `terraform init`: +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 `stage-links.sh` script in the root FAST 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 +../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '0-bootstrap' + +ln -s ~/fast-config/providers/0-bootstrap-providers.tf ./ +``` + +```bash +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '0-bootstrap' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/0-bootstrap-providers.tf ./ +``` + +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`: ```bash -# if using output files via the outputs_location and set to `~/fast-config` -ln -s ~/fast-config/providers/00-bootstrap* ./ -# or from outputs if not using output files -terraform output -json providers | jq -r '.["00-bootstrap"]' \ - > providers.tf -# migrate state to GCS bucket configured in providers file terraform init -migrate-state -# run terraform apply to remove the bootstrap_user iam binding terraform apply ``` @@ -334,7 +353,7 @@ You can customize organization-level logs through the `log_sinks` variable in tw - creating additional log sinks to capture more logs - changing the destination of captured logs -By default, all logs are exported to Bigquery, but FAST can create sinks to Cloud Logging Buckets, GCS, or PubSub. +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. @@ -400,12 +419,6 @@ cicd_repositories = { name = "my-gh-org/fast-bootstrap" type = "github" } - cicd = { - branch = null - identity_provider = "github-sample" - name = "my-gh-org/fast-cicd" - type = "github" - } resman = { branch = "main" identity_provider = "github-sample" diff --git a/fast/stages/1-resman/README.md b/fast/stages/1-resman/README.md index c2091eb5..971c6963 100644 --- a/fast/stages/1-resman/README.md +++ b/fast/stages/1-resman/README.md @@ -13,6 +13,22 @@ The following diagram is a high level reference of the resources created and man Resource-management diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [Multitenancy](#multitenancy) + - [Workload Identity Federation and CI/CD](#workload-identity-federation-and-cicd) +- [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) +- [Customizations](#customizations) + - [Team folders](#team-folders) + - [Organization Policies](#organization-policies) + - [IAM](#iam) + - [Additional folders](#additional-folders) + ## Design overview and choices Despite its simplicity, this stage implements the basics of a design that we've seen working well for a variety of customers, where the hierarchy is laid out following two conceptually different approaches: @@ -54,51 +70,49 @@ It's of course possible to run this stage in isolation, but that's outside the s Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. -### Providers configuration +### Provider and Terraform variables -The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during bootstrap, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). +As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. -To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the bootstrap stage (see the [bootstrap stage README](../0-bootstrap/#output-files-and-cross-stage-variables) for more details), simply link the relevant `providers.tf` file from this stage's folder in the path you specified: +The commands to link or copy the provider and terraform variable files can be easily derived from the `stage-links.sh` script in the FAST root 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 -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/01-resman-providers.tf . -``` +../../stage-links.sh ~/fast-config -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: +# copy and paste the following commands for '1-resman' + +ln -s ~/fast-config/providers/1-resman-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +``` ```bash -cd ../0-bootstrap -terraform output -json providers | jq -r '.["01-resman"]' \ - > ../1-resman/providers.tf +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '1-resman' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/1-resman-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ ``` -If you want to continue to rely on `outputs_location` logic, create a `terraform.tfvars` file and configure it as described [here](../0-bootstrap/#output-files-and-cross-stage-variables). +### 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 -There are two broad sets of variables you will need to fill in: +Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets: -- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage +- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, 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 can to be set in a custom `terraform.tfvars` file -To avoid the tedious job of filling in the first group of variable with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. +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. -If you configured a valid path for `outputs_location` in the bootstrap stage, simply link the relevant `*.auto.tfvars.json` files from the outputs folder. For this stage, you need the `globals.auto.tfvars.json` file containing global values compiled manually for the bootstrap stage, and `0-bootstrap.auto.tfvars.json` containing values derived from resources managed by the bootstrap stage: +### Running the stage -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json . -``` - -A second set of variables is specific to this stage, they are all optional so if you need to customize them, create an extra `terraform.tfvars` file. - -Refer to the [Variables](#variables) table at the bottom of this document, for a full list of variables, their origin (e.g. a stage or specific to this one), and descriptions explaining their meaning. The sections below also describe some of the possible customizations. For billing configurations, refer to the [Bootstrap documentation on billing](../0-bootstrap/README.md#billing-account) as the `billing_account` variable is identical across all stages. - -Once done, you can run this stage: +Once provider and variable values are in place and the correct user is configured, the stage can be run: ```bash terraform init @@ -139,9 +153,9 @@ This allows to centralize the minimum set of resources to delegate control of ea ### Organization policies -Organization policies are laid out in an explicit manner in the `organization.tf` file, so it's fairly easy to add or remove specific policies. +Organization policies leverage -- with one exception -- the built-in factory implemented in the organization module, and configured via the yaml files in the `data` folder. To edit organization policies, check and edit the files there. -For policies where additional data is needed, a root-level `organization_policy_configs` variable allows passing in specific data. Its built-in use to add additional organizations to the [Domain Restricted Sharing](https://cloud.google.com/resource-manager/docs/organization-policy/restricting-domains) policy, can be taken as an example on how to leverage it for additional customizations. +The one exception is [Domain Restricted Sharing](https://cloud.google.com/resource-manager/docs/organization-policy/restricting-domains), which is made dynamic and implemented in code so as to auto-add the current organization's customer id. The `organization_policy_configs` variable allow to easily add ids from third party organizations if needed. ### IAM diff --git a/fast/stages/2-networking-a-peering/README.md b/fast/stages/2-networking-a-peering/README.md index 7966ce80..c066423c 100644 --- a/fast/stages/2-networking-a-peering/README.md +++ b/fast/stages/2-networking-a-peering/README.md @@ -15,6 +15,33 @@ The following diagram illustrates the high-level design, and should be used as a Networking diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [VPC design](#vpc-design) + - [External connectivity](#external-connectivity) + - [Internal connectivity](#internal-connectivity) + - [IP ranges, subnetting, routing](#ip-ranges-subnetting-routing) + - [Internet egress](#internet-egress) + - [VPC and Hierarchical Firewall](#vpc-and-hierarchical-firewall) + - [DNS](#dns) +- [Stage structure and files layout](#stage-structure-and-files-layout) + - [VPCs](#vpcs) + - [VPNs](#vpns) + - [Routing and BGP](#routing-and-bgp) + - [Firewall](#firewall) + - [DNS architecture](#dns-architecture) + - [Private Google Access](#private-google-access) +- [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) + - [Post-deployment activities](#post-deployment-activities) +- [Customizations](#customizations) + - [Changing default regions](#changing-default-regions) + - [Adding an environment](#adding-an-environment) + ## Design overview and choices ### VPC design @@ -44,13 +71,13 @@ As mentioned initially, there are of course other ways to implement internal con This is a summary of the main options: -- [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) (implemented by [02-networking-vpn](../2-networking-b-vpn/)) +- [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) (implemented by [2-networking-vpn](../2-networking-b-vpn/)) - Pros: simple compatibility with GCP services that leverage peering internally, better control on routes, avoids peering groups shared quotas and limits - Cons: additional cost, marginal increase in latency, requires multiple tunnels for full bandwidth - [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (implemented here) - Pros: no additional costs, full bandwidth with no configurations, no extra latency, total environment isolation - Cons: no transitivity (e.g. to GKE masters, Cloud SQL, etc.), no selective exchange of routes, several quotas and limits shared between VPCs in a peering group -- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [02-networking-nva](../2-networking-c-nva/)) +- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [2-networking-nva](../2-networking-c-nva/)) - Pros: additional security features (e.g. IPS), potentially better integration with on-prem systems by using the same vendor - Cons: complex HA/failover setup, limited by VM bandwidth and scale, additional costs for VMs and licenses, out of band management of a critical cloud component @@ -120,58 +147,7 @@ From cloud, the `example.com` domain (used as a placeholder) is forwarded to on- This configuration is battle-tested, and flexible enough to lend itself to simple modifications without subverting its design, for example by forwarding and peering root zones to bypass Cloud DNS external resolution. -## How to run this stage - -This stage is meant to be executed after the [resman](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. - -It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. - -Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. - -### Providers configuration - -The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during the [resource management](../1-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). - -To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the bootstrap stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/02-networking-providers.tf . -``` - -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: - -```bash -cd ../1-resman -terraform output -json providers | jq -r '.["02-networking"]' \ - > ../02-networking/providers.tf -``` - -### Variable configuration - -There are two broad sets of variables you will need to fill in: - -- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you have set a valid value for `outputs_location` in the bootstrap and in the resman stage, simply link the relevant `*.auto.tfvars.json` files from this stage's folder in the path you specified. -The `*` above is set to the name of the stage that produced it, except for `globals.auto.tfvars.json` which is also generated by the bootstrap stage, containing global values compiled manually for the bootstrap stage. -For this stage, link the following files: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -``` - -A second set of variables is specific to this stage, they are all optional so if you need to customize them, create an extra `terraform.tfvars` file. - -Please refer to the [Variables](#variables) table below for a map of the variable origins, and to the sections below on how to adapt this stage to your networking configuration. +## Stage structure and files layout ### VPCs @@ -224,7 +200,72 @@ DNS queries sent to the on-premises infrastructure come from the `35.199.192.0/1 The [Inbound DNS Policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) defined in module `landing-vpc` ([`landing.tf`](./landing.tf)) automatically reserves the first available IP address on each created subnet (typically the third one in a CIDR) to expose the Cloud DNS service so that it can be consumed from outside of GCP. -### Private Google Access +## How to run this stage + +This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. + +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### 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](../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 `stage-links.sh` script in the FAST root 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 +../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '2-networking-a-peering' + +ln -s ~/fast-config/providers/2-networking-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ +``` + +```bash +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '2-networking-a-peering' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/2-networking-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +``` + +### 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 `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` and `1-resman.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +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. + +### 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 +``` + +### Post-deployment activities + +- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. +- On-prem routers should accept BGP sessions from their cloud peers. +- On-prem DNS servers should have forward zones for GCP-managed ones. + +#### Private Google Access [Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) enables VMs and on-prem systems to consume Google APIs from within the Google network, and is already fully configured on this environment. @@ -238,22 +279,15 @@ Per variable `vpn_onprem_configs` such ranges are advertised to onprem - further - A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in [`dns-landing.tf`](./dns-landing.tf) -### Preliminar activities - -Before running `terraform apply` on this stage, make sure to adapt all of `variables.tf` to your needs, to update all reference to regions (e.g. `europe-west1` or `ew1`) in the whole directory to match your preferences. - -If you're not using FAST, you'll also need to create a `providers.tf` file to configure the GCS backend and the service account to use to run the deployment. - -You're now ready to run `terraform init` and `apply`. - -### Post-deployment activities - -- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. -- On-prem routers should accept BGP sessions from their cloud peers. -- On-prem DNS servers should have forward zones for GCP-managed ones. - ## Customizations +### Changing default regions + +Regions are defined via the `regions` variable which sets up a mapping between the `regions.primary` and `regions.secondary` logical names and actual GCP region names. If you need to change regions from the defaults: + +- change the values of the mappings in the `regions` variable to the regions you are going to use +- change the regions in the factory subnet files in the `data` folder + ### Adding an environment To create a new environment (e.g. `staging`), a few changes are required. @@ -262,10 +296,10 @@ Create a `spoke-staging.tf` file by copying `spoke-prod.tf` file, and adapt the new file by replacing the value "prod" with the value "staging". Running `diff spoke-dev.tf spoke-prod.tf` can help to see how environment files differ. -The new VPC requires a set of dedicated CIDRs, one per region, added to variable `custom_adv` (for example as `spoke_staging_ew1` and `spoke_staging_ew4`). +The new VPC requires a set of dedicated CIDRs, one per region, added to variable `custom_adv` (for example as `spoke_staging_primary` and `spoke_staging_secondary`). >`custom_adv` is a map that "resolves" CIDR names to actual addresses, and will be used later to configure routing. > -Variables managing L7 Interal Load Balancers (`l7ilb_subnets`) and Private Service Access (`psa_ranges`) should also be adapted, and subnets and firewall rules for the new spoke should be added as described above. +Variables managing L7 Internal Load Balancers (`l7ilb_subnets`) and Private Service Access (`psa_ranges`) should also be adapted, and subnets and firewall rules for the new spoke should be added as described above. DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS resolution to Landing through DNS peering, and optionally define a private zone (e.g. `dev.gcp.example.com`) which the landing peers to. To configure DNS for a new environment, copy one of the other environments DNS files [e.g. (dns-dev.tf](dns-dev.tf)) into a new `dns-*.tf` file suffixed with the environment name (e.g. `dns-staging.tf`), and update its content accordingly. Don't forget to add a peering zone from the landing to the newly created environment private zone. diff --git a/fast/stages/2-networking-b-vpn/README.md b/fast/stages/2-networking-b-vpn/README.md index 2177f311..7a8983d8 100644 --- a/fast/stages/2-networking-b-vpn/README.md +++ b/fast/stages/2-networking-b-vpn/README.md @@ -15,6 +15,33 @@ The following diagram illustrates the high-level design, and should be used as a Networking diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [VPC design](#vpc-design) + - [External connectivity](#external-connectivity) + - [Internal connectivity](#internal-connectivity) + - [IP ranges, subnetting, routing](#ip-ranges-subnetting-routing) + - [Internet egress](#internet-egress) + - [VPC and Hierarchical Firewall](#vpc-and-hierarchical-firewall) + - [DNS](#dns) +- [Stage structure and files layout](#stage-structure-and-files-layout) + - [VPCs](#vpcs) + - [VPNs](#vpns) + - [Routing and BGP](#routing-and-bgp) + - [Firewall](#firewall) + - [DNS architecture](#dns-architecture) + - [Private Google Access](#private-google-access) +- [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) + - [Post-deployment activities](#post-deployment-activities) +- [Customizations](#customizations) + - [Changing default regions](#changing-default-regions) + - [Adding an environment](#adding-an-environment) + ## Design overview and choices ### VPC design @@ -45,10 +72,10 @@ This is a summary of the main options: - [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) (implemented here) - Pros: simple compatibility with GCP services that leverage peering internally, better control on routes, avoids peering groups shared quotas and limits - Cons: additional cost, marginal increase in latency, requires multiple tunnels for full bandwidth -- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (implemented by [02-networking-peering](../2-networking-a-peering/)) +- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (implemented by [2-networking-peering](../2-networking-a-peering/)) - Pros: no additional costs, full bandwidth with no configurations, no extra latency - Cons: no transitivity (e.g. to GKE masters, Cloud SQL, etc.), no selective exchange of routes, several quotas and limits shared between VPCs in a peering group -- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [02-networking-nva](../2-networking-c-nva/)) +- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [2-networking-nva](../2-networking-c-nva/)) - Pros: additional security features (e.g. IPS), potentially better integration with on-prem systems by using the same vendor - Cons: complex HA/failover setup, limited by VM bandwidth and scale, additional costs for VMs and licenses, out of band management of a critical cloud component @@ -126,58 +153,7 @@ From cloud, the `example.com` domain (used as a placeholder) is forwarded to on- This configuration is battle-tested, and flexible enough to lend itself to simple modifications without subverting its design, for example by forwarding and peering root zones to bypass Cloud DNS external resolution. -## How to run this stage - -This stage is meant to be executed after the [resman](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. - -It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. - -Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. - -### Providers configuration - -The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during the [resource management](../1-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). - -To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the bootstrap stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/02-networking-providers.tf . -``` - -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: - -```bash -cd ../1-resman -terraform output -json providers | jq -r '.["02-networking"]' \ - > ../02-networking/providers.tf -``` - -### Variable configuration - -There are two broad sets of variables you will need to fill in: - -- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you have set a valid value for `outputs_location` in the bootstrap and in the resman stage, simply link the relevant `*.auto.tfvars.json` files from this stage's folder in the path you specified. -The `*` above is set to the name of the stage that produced it, except for `globals.auto.tfvars.json` which is also generated by the bootstrap stage, containing global values compiled manually for the bootstrap stage. -For this stage, link the following files: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -``` - -A second set of variables is specific to this stage, they are all optional so if you need to customize them, create an extra `terraform.tfvars` file. - -Please refer to the [Variables](#variables) table below for a map of the variable origins, and to the sections below on how to adapt this stage to your networking configuration. +## Stage structure and files layout ### VPCs @@ -238,7 +214,72 @@ DNS queries sent to the on-premises infrastructure come from the `35.199.192.0/1 The [Inbound DNS Policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) defined in module `landing-vpc` ([`landing.tf`](./landing.tf)) automatically reserves the first available IP address on each created subnet (typically the third one in a CIDR) to expose the Cloud DNS service so that it can be consumed from outside of GCP. -### Private Google Access +## How to run this stage + +This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. + +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### 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](../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 `stage-links.sh` script in the FAST root 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 +../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '2-networking-a-peering' + +ln -s ~/fast-config/providers/2-networking-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ +``` + +```bash +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '2-networking-a-peering' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/2-networking-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +``` + +### 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 `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` and `1-resman.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +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. + +### 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 +``` + +### Post-deployment activities + +- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. +- On-prem routers should accept BGP sessions from their cloud peers. +- On-prem DNS servers should have forward zones for GCP-managed ones. + +#### Private Google Access [Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) enables VMs and on-prem systems to consume Google APIs from within the Google network, and is already fully configured on this environment. @@ -250,24 +291,17 @@ Subnets created by the `net-vpc` module are PGA-enabled by default. - 199.36.153.4/30 (`restricted.googleapis.com`) and 199.36.153.8/30 (`private.googleapis.com`) should be routed from on-prem to VPC, and from there to the `default-internet-gateway`. \ Per variable `vpn_onprem_configs` such ranges are advertised to onprem - furthermore every VPC (e.g. see `landing-vpc` in [`landing.tf`](./landing.tf)) has explicit routes set in case the `0.0.0.0/0` route is changed. -- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in [dns-landing.tf](./dns-landing.tf) - -### Preliminary activities - -Before running `terraform apply` on this stage, make sure to adapt all of `variables.tf` and `vpn-variables.tf` to your needs, to update all references to regions (e.g. `europe-west1` or `ew1`) in the whole directory to match your preferences. - -If you're not using FAST, you'll also need to create a `providers.tf` file to configure the GCS backend and the service account to use to run the deployment. - -You're now ready to run `terraform init` and `apply`. - -### Post-deployment activities - -- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. -- On-prem routers should accept BGP sessions from their cloud peers. -- On-prem DNS servers should have forward zones for GCP-managed ones. +- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in [`dns-landing.tf`](./dns-landing.tf) ## Customizations +### Changing default regions + +Regions are defined via the `regions` variable which sets up a mapping between the `regions.primary` and `regions.secondary` logical names and actual GCP region names. If you need to change regions from the defaults: + +- change the values of the mappings in the `regions` variable to the regions you are going to use +- change the regions in the factory subnet files in the `data` folder + ### Adding an environment To create a new environment (e.g. `staging`), a few changes are required. diff --git a/fast/stages/2-networking-c-nva/README.md b/fast/stages/2-networking-c-nva/README.md index 425e1d19..d0e62fd6 100644 --- a/fast/stages/2-networking-c-nva/README.md +++ b/fast/stages/2-networking-c-nva/README.md @@ -21,6 +21,34 @@ The final number of subnets, and their IP addressing will depend on the user-spe Networking diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [Multi-regional deployment](#multi-regional-deployment) + - [VPC design](#vpc-design) + - [External connectivity](#external-connectivity) + - [Internal connectivity](#internal-connectivity) + - [IP ranges, subnetting, routing](#ip-ranges-subnetting-routing) + - [Internet egress](#internet-egress) + - [VPC and Hierarchical Firewall](#vpc-and-hierarchical-firewall) + - [DNS](#dns) +- [Stage structure and files layout](#stage-structure-and-files-layout) + - [VPCs](#vpcs) + - [VPNs](#vpns) + - [Routing and BGP](#routing-and-bgp) + - [Firewall](#firewall) + - [DNS architecture](#dns-architecture) + - [Private Google Access](#private-google-access) +- [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) + - [Post-deployment activities](#post-deployment-activities) +- [Customizations](#customizations) + - [Changing default regions](#changing-default-regions) + - [Adding an environment](#adding-an-environment) + ## Design overview and choices ### Multi-regional deployment @@ -190,58 +218,7 @@ In GCP, a forwarding zone in the landing project is configured to forward querie This configuration is battle-tested, and flexible enough to lend itself to simple modifications without subverting its design. -## How to run this stage - -This stage is meant to be executed after the [resman](../1-resman) stage has run. It leverages the automation service account and the storage bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. - -It's possible to run this stage in isolation, but that's outside of the scope of this document. Please, refer to the previous stages for the environment requirements. - -Before running this stage, you need to make sure you have the correct credentials and permissions. You'll also need identify the module variables and make sure you assign them the values that match your configuration. - -### Providers configuration - -The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage, during the [resource management](../1-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). - -To simplify the setup, the previous stage pre-configures a valid providers file in its output and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the bootstrap stage, simply link the relevant `providers.tf` file from this stage folder in the path you selected: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/02-networking-providers.tf . -``` - -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage outputs: - -```bash -cd ../1-resman -terraform output -json providers | jq -r '.["02-networking"]' \ - > ../2-networking-c-nva/providers.tf -``` - -### Variable configuration - -There are two broad sets of variables you will need to fill in: - -- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -To avoid the tedious job of filling in the first group of variables with values derived from other stages outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you have set a valid value for `outputs_location` in the bootstrap and in the resman stage, simply link the relevant `*.auto.tfvars.json` files from this stage's folder in the path you specified. -The `*` above is set to the name of the stage that produced it, except for `globals.auto.tfvars.json` which is also generated by the bootstrap stage, containing global values compiled manually for the bootstrap stage. -For this stage, link the following files: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -``` - -A second set of variables is specific to this stage, they are all optional so if you need to customize them, create an extra `terraform.tfvars` file. - -Please, refer to the [variables](#variables) table below for a map of the variable origins, and use the sections below to understand how to adapt this stage to your networking configuration. +## Stage structure and files layout ### VPCs @@ -286,46 +263,104 @@ Cloud DNS manages onprem forwarding, the main GCP zone (in this example `gcp.exa The root DNS zone defined in the landing project acts as the source of truth for DNS within the Cloud environment. The resources defined in the spoke VPCs consume the landing DNS infrastructure through DNS peering (e.g. `prod-landing-root-dns-peering`). The spokes can optionally define private zones (e.g. `prod-dns-private-zone`). Granting visibility both to the trusted and untrusted landing VPCs ensures that the whole cloud environment can query such zones. -#### Cloud to on-premises +#### Cloud to on-prem Leveraging the forwarding zone defined in the landing project (e.g. `onprem-example-dns-forwarding` and `reverse-10-dns-forwarding`), the cloud environment can resolve `in-addr.arpa.` and `onprem.example.com.` using the on-premise DNS infrastructure. On-premise resolver IPs are set in the variable `dns.onprem`. DNS queries sent to the on-premise infrastructure come from the `35.199.192.0/19` source range. -#### On-premises to cloud +#### On-prem to cloud The [Inbound DNS Policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) defined in the *trusted landing VPC module* ([`landing.tf`](./landing.tf)) automatically reserves the first available IP address on each subnet (typically the third one in a CIDR) to expose the Cloud DNS service, so that it can be consumed from outside of GCP. -### Private Google Access +## How to run this stage -[Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) is configured in this environment. It enables VMs and on-premise systems to consume Google APIs from within the Google network. +This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. + +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### 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](../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 `stage-links.sh` script in the FAST root 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 +../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '2-networking-a-peering' + +ln -s ~/fast-config/providers/2-networking-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ +``` + +```bash +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '2-networking-a-peering' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/2-networking-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +``` + +### 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 `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` and `1-resman.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +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. + +### 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 +``` + +### Post-deployment activities + +- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. +- On-prem routers should accept BGP sessions from their cloud peers. +- On-prem DNS servers should have forward zones for GCP-managed ones. + +#### Private Google Access + +[Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) enables VMs and on-prem systems to consume Google APIs from within the Google network, and is already fully configured on this environment. For PGA to work: - Private Google Access should be enabled on the subnet. \ -Subnets created using the `net-vpc` module are PGA-enabled by default. +Subnets created by the `net-vpc` module are PGA-enabled by default. -- 199.36.153.4/30 (`restricted.googleapis.com`) and 199.36.153.8/30 (`private.googleapis.com`) should be routed from on-premises to the trusted landing VPC, and from there to the `default-internet-gateway`. \ -The `vpn_onprem_configs` variable contains the ranges advertised from GCP to on-premises. Furthermore, the trusted landing VPC (e.g. see `landing-trusted-vpc` in [`landing.tf`](./landing.tf)) has explicit routes to send traffic destined to restricted and private - googleapis.com to the Internet gateway (which works for Google APIs only, and not for the whole Internet, since Cloud NAT is not configured in the trusted landing VPC). +- 199.36.153.4/30 (`restricted.googleapis.com`) and 199.36.153.8/30 (`private.googleapis.com`) should be routed from on-prem to VPC, and from there to the `default-internet-gateway`. \ +Per variable `vpn_onprem_configs` such ranges are advertised to onprem - furthermore every VPC (e.g. see `landing-vpc` in [`landing.tf`](./landing.tf)) has explicit routes set in case the `0.0.0.0/0` route is changed. -- On-premises, a private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain). Its configuration can be copied from the module `googleapis-private-zone` in [`dns-landing.tf`](./dns-landing.tf) - -### Preliminar activities - -Before running `terraform apply`, make sure to adapt `variables.tf` to your needs, to update the variable values using a new `terraform.tfvars` file, and to update the references to the regions in the whole directory, in order to match your preferences (e.g. `europe-west1` or `ew1`). - -If you're not using other FAST stages, you'll also need to create a `providers.tf` file to configure the GCS backend and the service account to use to run the deployment. - -You're now ready to run `terraform init` and `terraform apply`. - -### Post-deployment activities - -- On-premise routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recommend aggregating routes as much as possible -- On-premise routers should accept BGP sessions from their cloud peers -- On-premise DNS servers should have forward zones configured, in order to resolve GCP-managed domains +- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in [`dns-landing.tf`](./dns-landing.tf) ## Customizations +### Changing default regions + +Regions are defined via the `regions` variable which sets up a mapping between the `regions.primary` and `regions.secondary` logical names and actual GCP region names. If you need to change regions from the defaults: + +- change the values of the mappings in the `regions` variable to the regions you are going to use +- change the regions in the factory subnet files in the `data` folder + ### Adding an environment To create a new environment (e.g. `staging`), a few changes are required: diff --git a/fast/stages/2-networking-d-separate-envs/README.md b/fast/stages/2-networking-d-separate-envs/README.md index a461dc97..dfc199cd 100644 --- a/fast/stages/2-networking-d-separate-envs/README.md +++ b/fast/stages/2-networking-d-separate-envs/README.md @@ -1,4 +1,4 @@ -# Networking +# Networking with separated single environment This stage sets up the shared network infrastructure for the whole organization. It implements a single shared VPC per environment, where each environment is independently connected to the on-premise environment, to maintain a fully separated routing domain on GCP. @@ -14,6 +14,31 @@ The following diagram illustrates the high-level design, and should be used as a Networking diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [VPC design](#vpc-design) + - [External connectivity](#external-connectivity) + - [IP ranges, subnetting, routing](#ip-ranges-subnetting-routing) + - [Internet egress](#internet-egress) + - [VPC and Hierarchical Firewall](#vpc-and-hierarchical-firewall) + - [DNS](#dns) +- [Stage structure and files layout](#stage-structure-and-files-layout) + - [VPCs](#vpcs) + - [VPNs](#vpns) + - [Routing and BGP](#routing-and-bgp) + - [Firewall](#firewall) + - [DNS architecture](#dns-architecture) + - [Private Google Access](#private-google-access) +- [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) + - [Post-deployment activities](#post-deployment-activities) +- [Customizations](#customizations) + - [Changing default regions](#changing-default-regions) + ## Design overview and choices ### VPC design @@ -87,57 +112,7 @@ From cloud, the `example.com` domain (used as a placeholder) is forwarded to on- This configuration is battle-tested, and flexible enough to lend itself to simple modifications without subverting its design, for example by forwarding and peering root zones to bypass Cloud DNS external resolution. -## How to run this stage - -This stage is meant to be executed after the [resman](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. - -It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. - -Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. - -### Providers configuration - -The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during the [resource management](../1-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). - -To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the bootstrap stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/02-networking-providers.tf . -``` - -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: - -```bash -cd ../1-resman -terraform output -json providers | jq -r '.["02-networking"]' \ - > ../02-networking/providers.tf -``` - -### Variable configuration - -There are two broad sets of variables you will need to fill in: - -- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you have set a valid value for `outputs_location` in the bootstrap and in the resman stage, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's folder in the path you specified, where the `*` above is set to the name of the stage that produced it. For this stage, a single `.tfvars` file is available: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ../../configs/example/02-networking/terraform-bootstrap.auto.tfvars.json -ln -s ../../configs/example/02-networking/terraform-resman.auto.tfvars.json -# also copy the tfvars file used for the bootstrap stage -cp ../0-bootstrap/terraform.tfvars . -``` - -A second set of variables is specific to this stage, they are all optional so if you need to customize them, add them to the file copied from bootstrap. - -Please refer to the [Variables](#variables) table below for a map of the variable origins, and to the sections below on how to adapt this stage to your networking configuration. +## Stage structure and files layout ### VPCs @@ -187,7 +162,72 @@ When implementing this architecture, make sure you'll be able to route packets c The [Inbound DNS Policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) defined on eachVPC automatically reserves the first available IP address on each created subnet (typically the third one in a CIDR) to expose the Cloud DNS service so that it can be consumed from outside of GCP. -### Private Google Access +## How to run this stage + +This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. + +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### 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](../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 `stage-links.sh` script in the FAST root 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 +../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '2-networking-a-peering' + +ln -s ~/fast-config/providers/2-networking-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ +``` + +```bash +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '2-networking-a-peering' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/2-networking-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +``` + +### 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 `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` and `1-resman.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +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. + +### 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 +``` + +### Post-deployment activities + +- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. +- On-prem routers should accept BGP sessions from their cloud peers. +- On-prem DNS servers should have forward zones for GCP-managed ones. + +#### Private Google Access [Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) enables VMs and on-prem systems to consume Google APIs from within the Google network, and is already fully configured on this environment. @@ -199,21 +239,16 @@ Subnets created by the `net-vpc` module are PGA-enabled by default. - 199.36.153.4/30 (`restricted.googleapis.com`) and 199.36.153.8/30 (`private.googleapis.com`) should be routed from on-prem to VPC, and from there to the `default-internet-gateway`. \ Per variable `vpn_onprem_configs` such ranges are advertised to onprem - furthermore every VPC has explicit routes set in case the `0.0.0.0/0` route is changed. -- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in `dns-xxx.tf` +- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain) -### Preliminar activities +## Customizations -Before running `terraform apply` on this stage, make sure to adapt all of `variables.tf` to your needs, to update all reference to regions (e.g. `europe-west1` or `ew1`) in the whole directory to match your preferences. +### Changing default regions -If you're not using FAST, you'll also need to create a `providers.tf` file to configure the GCS backend and the service account to use to run the deployment. +Regions are defined via the `regions` variable which sets up a mapping between the `regions.primary` and `regions.secondary` logical names and actual GCP region names. If you need to change regions from the defaults: -You're now ready to run `terraform init` and `apply`. - -### Post-deployment activities - -- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. -- On-prem routers should accept BGP sessions from their cloud peers. -- On-prem DNS servers should have forward zones for GCP-managed ones. +- change the values of the mappings in the `regions` variable to the regions you are going to use +- change the regions in the factory subnet files in the `data` folder diff --git a/fast/stages/2-security/README.md b/fast/stages/2-security/README.md index a609cd81..6486cd74 100644 --- a/fast/stages/2-security/README.md +++ b/fast/stages/2-security/README.md @@ -12,6 +12,24 @@ The following diagram illustrates the high-level design of created resources and Security diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [Cloud KMS](#cloud-kms) + - [VPC Service Controls](#vpc-service-controls) +- [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) +- [Customizations](#customizations) + - [KMS keys](#kms-keys) + - [VPC Service Controls configuration](#vpc-service-controls-configuration) + - [Dry-run vs. enforced](#dry-run-vs-enforced) + - [Access levels](#access-levels) + - [Ingress and Egress policies](#ingress-and-egress-policies) + - [Perimeters](#perimeters) + ## Design overview and choices Project-level security resources are grouped into two separate projects, one per environment. This setup matches requirements we frequently observe in real life and provides enough separation without needlessly complicating operations. @@ -42,57 +60,57 @@ Some care needs to be taken with project membership in perimeters, which can onl ## How to run this stage -This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the folder and automation resources created there. The relevant user groups must also exist, but that's one of the requirements for the previous stages too, so if you ran those successfully, you're good to go. +This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. -It's possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the bootstrap stage for the required roles. +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. -Before running this stage, you need to ensure you have the correct credentials and permissions, and customize variables by assigning values that match your configuration. +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. -### Providers configuration +### Provider and Terraform variables -The default way of making sure you have the correct permissions is to use the identity of the service account pre-created for this stage during bootstrap, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). +As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. -To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the resource management stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified: +The commands to link or copy the provider and terraform variable files can be easily derived from the `stage-links.sh` script in the FAST root 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 -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/02-security-providers.tf . -``` +../../stage-links.sh ~/fast-config -If you have not configured `outputs_location` in resource management, you can derive the providers file from that stage's outputs: +# copy and paste the following commands for '2-security' + +ln -s ~/fast-config/providers/2-security-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ +``` ```bash -cd ../1-resman -terraform output -json providers | jq -r '.["02-security"]' \ - > ../02-security/providers.tf +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '2-security' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/2-security-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ ``` +### 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 -There are two broad sets of variables you will need to fill in: +Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets: -- variables shared by other stages (organization id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage +- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` and `1-resman.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. +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. -If you configured a valid path for `outputs_location` in the previous stages, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's output folder (under the path you specified), where the `*` above is set to the name of the stage that produced it. For this stage, two `.tfvars` files are available: +### Running the stage -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -# also copy the tfvars file used for the bootstrap stage -cp ../0-bootstrap/terraform.tfvars . -``` - -A second set of optional variables is specific to this stage. If you need to customize them add them to the file copied from bootstrap. - -Refer to the [Variables](#variables) table at the bottom of this document, for a full list of variables, their origin (e.g., a stage or specific to this one), and descriptions explaining their meaning. The sections below also describe some of the possible customizations. - -Once done, you can run this stage: +Once provider and variable values are in place and the correct user is configured, the stage can be run: ```bash terraform init diff --git a/fast/stages/3-data-platform/dev/README.md b/fast/stages/3-data-platform/dev/README.md index 615dbde8..48d09eaf 100644 --- a/fast/stages/3-data-platform/dev/README.md +++ b/fast/stages/3-data-platform/dev/README.md @@ -78,7 +78,70 @@ In the case your Data Warehouse need to handle confidential data and you have th ## How to run this stage -This stage can be run in isolation by prviding the necessary variables, but it's really meant to be used as part of the FAST flow after the "foundational stages" ([`00-bootstrap`](../../0-bootstrap), [`01-resman`](../../1-resman), [`02-networking`](../../2-networking-b-vpn) and [`02-security`](../../2-security)). +This stage is meant to be executed after the FAST "foundational" stages: bootstrap, resource management, security and networking stages. + +It's of course possible to run this stage in isolation, refer to the *[Running in isolation](#running-in-isolation)* section below for details. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### 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](../../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 `stage-links.sh` script in the FAST root 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 +../../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '3-data-platform' + +ln -s /home/ludomagno/fast-config/providers/3-data-platform-providers.tf ./ +ln -s /home/ludomagno/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/1-resman.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/2-networking.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/2-security.auto.tfvars.json ./ +``` + +```bash +../../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '3-data-platform' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/3-data-platform-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/2-networking.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/2-security.auto.tfvars.json ./ +``` + +### 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 `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `*.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +The full list can be found in the [Variables](#variables) table at the bottom of this document. + +### 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 +``` + +### Running in isolation + +This stage can be run in isolation by providing the necessary variables, but it's really meant to be used as part of the FAST flow after the "foundational stages" ([`0-bootstrap`](../../0-bootstrap), [`1-resman`](../../1-resman), [`2-networking`](../../2-networking-b-vpn) and [`2-security`](../../2-security)). When running in isolation, the following roles are needed on the principal used to apply Terraform: @@ -100,52 +163,6 @@ When running in isolation, the following roles are needed on the principal used The VPC host project, VPC and subnets should already exist. -### Providers configuration - -If you're running this on top of Fast, you should run the following commands to create the providers file, and populate the required variables from the previous stage. - -```bash -# Variable `outputs_location` is set to `~/fast-config` in stage 01-resman -ln -s ~/fast-config/providers/03-data-platform-dev-providers.tf . -``` - -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: - -```bash -cd ../../1-resman -terraform output -json providers | jq -r '.["03-data-platform-dev"]' \ - > ../3-data-platform/dev/providers.tf -``` - -### Variable configuration - -There are two broad sets of variables that can be configured: - -- variables shared by other stages (organization id, billing account id, etc.) or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you configured a valid path for `outputs_location` in the bootstrap security and networking stages, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's outputs folder under the path you specified. This will also link the providers configuration file: - -```bash -# Variable `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -ln -s ~/fast-config/tfvars/02-networking.auto.tfvars.json . -# also copy the tfvars file used for the bootstrap stage -cp ../../0-bootstrap/terraform.tfvars . -``` - -If you're not using FAST or its output files, refer to the [Variables](#variables) table at the bottom of this document for a full list of variables, their origin (e.g., a stage or specific to this one), and descriptions explaining their meaning. - -Once the configuration is complete you can apply this stage: - -```bash -terraform init -terraform apply -``` - ## Demo pipeline The application layer is out of scope of this script. As a demo purpuse only, several Cloud Composer DAGs are provided. Demos will import data from the `landing` area to the `DataWarehouse Confidential` dataset suing different features. diff --git a/fast/stages/3-gke-multitenant/dev/README.md b/fast/stages/3-gke-multitenant/dev/README.md index 4accf8e1..f0460c06 100644 --- a/fast/stages/3-gke-multitenant/dev/README.md +++ b/fast/stages/3-gke-multitenant/dev/README.md @@ -39,7 +39,68 @@ This stage creates a project containing and as many clusters and node pools as r ## How to run this stage -This stage is meant to be executed after "foundational stages" (i.e., stages [`00-bootstrap`](../../0-bootstrap), [`01-resman`](../../1-resman), 02-networking (either [VPN](../../2-networking-b-vpn) or [NVA](../../2-networking-c-nva)) and [`02-security`](../../2-security)) have been run. +This stage is meant to be executed after the FAST "foundational" stages: bootstrap, resource management, security and networking stages. + +It's of course possible to run this stage in isolation, refer to the *[Running in isolation](#running-in-isolation)* section below for details. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### 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](../../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 `stage-links.sh` script in the FAST root 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 +../../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '3-gke-multitenant' + +ln -s /home/ludomagno/fast-config/providers/3-gke-multitenant-providers.tf ./ +ln -s /home/ludomagno/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/1-resman.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/2-networking.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/2-security.auto.tfvars.json ./ +``` + +```bash +../../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '3-gke-multitenant' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/3-gke-multitenant-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/2-networking.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/2-security.auto.tfvars.json ./ +``` + +### 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 `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `*.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +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. + +### 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 +``` + +### Running in isolation It's of course possible to run this stage in isolation, by making sure the architectural prerequisites are satisfied (e.g., networking), and that the Service Account running the stage is granted the roles/permissions below: @@ -62,39 +123,9 @@ It's of course possible to run this stage in isolation, by making sure the archi The VPC host project, VPC and subnets should already exist. -### Providers configuration +## Customizations -If you're running this on top of FAST, you should run the following commands to create the providers file, and populate the required variables from the previous stage. - -```bash -# Variable `outputs_location` is set to `~/fast-config` in stage 01-resman -$ cd fabric-fast/stages/03-gke-multitenant/dev -ln -s ~/fast-config/providers/03-gke-dev-providers.tf . -``` - -### Variable configuration - -There are two broad sets of variables you will need to fill in: - -- variables shared by other stages (organization id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -#### Variables passed in from other stages - -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you configured a valid path for `outputs_location` in the bootstrap and networking stage, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's outputs folder (under the path you specified), where the `*` above is set to the name of the stage that produced it. For this stage, a single `.tfvars` file is available: - -```bash -# Variable `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -ln -s ~/fast-config/tfvars/02-networking.auto.tfvars.json . -``` - -If you're not using FAST, refer to the [Variables](#variables) table at the bottom of this document for a full list of variables, their origin (e.g., a stage or specific to this one), and descriptions explaining their meaning. - -#### Cluster and node pools +### Cluster and node pools This stage is designed with multi-tenancy in mind, and the expectation is that GKE clusters will mostly share a common set of defaults. Variables are designed to support this approach for both clusters and node pools: @@ -105,7 +136,7 @@ This stage is designed with multi-tenancy in mind, and the expectation is that There are two additional variables that influence cluster configuration: `authenticator_security_group` to configure [Google Groups for RBAC](https://cloud.google.com/kubernetes-engine/docs/how-to/google-groups-rbac), `dns_domain` to configure [Cloud DNS for GKE](https://cloud.google.com/kubernetes-engine/docs/how-to/cloud-dns). -#### Fleet management +### Fleet management Fleet management is entirely optional, and uses three separate variables: @@ -116,15 +147,6 @@ Fleet management is entirely optional, and uses three separate variables: Leave all these variables unset (or set to `null`) to disable fleet management. -## Running Terraform - -Once the [provider](#providers-configuration) and [variable](#variable-configuration) configuration is complete, you can apply this stage: - -```bash -terraform init -terraform apply -``` - diff --git a/fast/stages/CLEANUP.md b/fast/stages/CLEANUP.md index 3bc581f9..4b2667c8 100644 --- a/fast/stages/CLEANUP.md +++ b/fast/stages/CLEANUP.md @@ -7,7 +7,7 @@ Destruction must be done in reverse order, from stage 3 to stage 0 ## Stage 3 (Project Factory) ```bash -cd $FAST_PWD/03-project-factory/prod/ +cd $FAST_PWD/3-project-factory/dev/ terraform destroy ``` @@ -16,7 +16,7 @@ terraform destroy Terraform refuses to delete non-empty GCS buckets and BigQuery datasets, so they need to be removed manually from the state. ```bash -cd $FAST_PWD/03-project-factory/prod/ +cd $FAST_PWD/3-gke-multitenant/dev/ # remove BQ dataset manually for x in $(terraform state list | grep google_bigquery_dataset); do @@ -29,14 +29,14 @@ terraform destroy ## Stage 2 (Security) ```bash -cd $FAST_PWD/02-security/ +cd $FAST_PWD/2-security/ terraform destroy ``` ## Stage 2 (Networking) ```bash -cd $FAST_PWD/02-networking-XXX/ +cd $FAST_PWD/2-networking-XXX/ terraform destroy ``` @@ -47,7 +47,7 @@ A minor glitch can surface running `terraform destroy`, where the service projec Stage 1 is a little more complicated because of the GCS buckets containing your terraform statefiles. By default, Terraform refuses to delete non-empty buckets, which is good to protect your terraform state, but it makes destruction a bit harder. Use the commands below to remove the GCS buckets from the state and then execute `terraform destroy` ```bash -cd $FAST_PWD/01-resman/ +cd $FAST_PWD/1-resman/ # remove buckets from state since terraform refuses to delete them for x in $(terraform state list | grep google_storage_bucket.bucket); do @@ -64,10 +64,10 @@ terraform destroy Just like before, we manually remove several resources (GCS buckets and BQ datasets). Note that `terrafom destroy` will fail. This is expected; just continue with the rest of the steps. ```bash -cd $FAST_PWD/00-bootstrap/ +cd $FAST_PWD/0-bootstrap/ # remove provider config to execute without SA impersonation -rm 00-bootstrap-providers.tf +rm 0-bootstrap-providers.tf # migrate to local state terraform init -migrate-state diff --git a/fast/stages/COMPANION.md b/fast/stages/COMPANION.md index d5d7752f..96506d00 100644 --- a/fast/stages/COMPANION.md +++ b/fast/stages/COMPANION.md @@ -8,7 +8,7 @@ The detailed explanation of each stage, their configuration, possible modificati ## Prerequisites -1. FAST uses the recommended groups from the [GCP Enterprise Setup checklist](). Go to [Workspace / Cloud Identity](https://admin.google.com) and ensure all the following groups exist: +1. FAST uses the recommended groups from the [GCP Enterprise Setup checklist](https://cloud.google.com/docs/enterprise/setup-checklist). Go to [Workspace / Cloud Identity](https://admin.google.com) and ensure all the following groups exist: - `gcp-billing-admins@` - `gcp-devops@` @@ -80,8 +80,8 @@ If you are using a billing account in a different organization, please follow [t This initial stage will create common projects for IaC, Logging & Billing, and bootstrap IAM policies. ```bash -# move to the 00-bootstrap directory -cd $FAST_PWD/00-bootstrap +# move to the 0-bootstrap directory +cd $FAST_PWD/0-bootstrap # copy the template terraform tfvars file and save as `terraform.tfvars` # then edit to match your environment! @@ -114,11 +114,12 @@ outputs_location = "~/fast-config" terraform init terraform apply -var bootstrap_user=$FAST_BU -# link the generated provider file -ln -s ~/fast-config/providers/0-0-bootstrap* . +# link providers file +ln -s ~/fast-config/providers/0-bootstrap-providers.tf ./ # re-run init and apply to remove user-level IAM terraform init -migrate-state + # answer 'yes' to terraform's question terraform apply ``` @@ -132,14 +133,14 @@ This stage performs two important tasks: ```bash # move to the 01-resman directory -cd $FAST_PWD/01-resman +cd $FAST_PWD/1-resman -# Link providers and variables from previous stages -ln -s ~/fast-config/providers/1-0-resman-providers.tf . -ln -s ~/fast-config/tfvars/0-0-bootstrap.auto.tfvars.json . +# link providers and variables from previous stages +ln -s ~/fast-config/providers/1-resman-providers.tf . +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json . ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -# Edit your terraform.tfvars to append Teams configuration (optional) +# edit your terraform.tfvars to append Teams configuration (optional) edit terraform.tfvars ``` @@ -178,15 +179,15 @@ In this stage, we will deploy one of the 3 available Hub&Spoke networking topolo ```bash # move to the 02-networking-XXX directory (where XXX should be one of vpn|peering|nva) -cd $FAST_PWD/02-networking-XXX +cd $FAST_PWD/2-networking-XXX # setup providers and variables from previous stages -ln -s ~/fast-config/providers/2-0-networking-providers.tf . -ln -s ~/fast-config/tfvars/0-0-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/1-0-resman.auto.tfvars.json . +ln -s ~/fast-config/providers/2-networking-providers.tf . +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json . +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json . ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -# Create terraform.tfvars. output_location variable is required to generate networking stage output file +# create terraform.tfvars. output_location variable is required to generate networking stage output file edit terraform.tfvars ``` @@ -212,12 +213,12 @@ This stage sets up security resources (KMS and VPC-SC) and configurations which cd $FAST_PWD/02-security # link providers and variables from previous stages -ln -s ~/fast-config/providers/2-0-security-providers.tf . -ln -s ~/fast-config/tfvars/0-0-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/1-0-resman.auto.tfvars.json . +ln -s ~/fast-config/providers/2-security-providers.tf . +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json . +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json . ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -# Edit terraform.tfvars to include KMS and/or VPC-SC configuration +# edit terraform.tfvars to include KMS and/or VPC-SC configuration edit terraform.tfvars ``` @@ -234,19 +235,20 @@ terraform apply The Project Factory stage builds on top of your foundations to create and set up projects (and related resources) to be used for your workloads. It is organized in folders representing environments (e.g. "dev", "prod"), each implemented by a stand-alone terraform resource factory. ```bash -# Variable `outputs_location` is set to `~/fast-config` -cd $FAST_PWD/3-0-project-factory/ENVIRONMENT -ln -s ~/fast-config/providers/3-0-project-factory-ENVIRONMENT-providers.tf . +# variable `outputs_location` is set to `~/fast-config` +cd $FAST_PWD/3-project-factory/ENVIRONMENT +ln -s ~/fast-config/providers/3-project-factory-ENVIRONMENT-providers.tf . -ln -s ~/fast-config/tfvars/0-0-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/1-0-resman.auto.tfvars.json . -ln -s ~/fast-config/tfvars/2-0-networking.auto.tfvars.json . +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json . +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json . +ln -s ~/fast-config/tfvars/2-networking.auto.tfvars.json . ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -# Define your environment default values (eg for billing alerts and labels) +# define your environment default values (eg for billing alerts and labels) edit data/defaults.yaml -# Create one yaml file per project to be created. Yaml file will include project configuration. Projects will be named after the filename +# create one YAML file per project to be created with project configuration +# filenames will be used for project ids cp data/projects/project.yaml.sample data/projects/YOUR_PROJECT_NAME.yaml edit data/projects/YOUR_PROJECT_NAME.yaml diff --git a/fast/stages/FAQ.md b/fast/stages/FAQ.md index bd9559d4..5245c8a9 100644 --- a/fast/stages/FAQ.md +++ b/fast/stages/FAQ.md @@ -1,29 +1,13 @@ +# FAST Mini FAQ -## 00-bootstrap - -1. How to handle requests where automation, logging and/or billing export projects are not under organization but in different folders. - - Run bootstrap stage and let automation, logging and/or billing projects be created under organization. - - Run resource manager stage or any other custom stage which creates the folders where these projects will reside. - - Once folders are created add folder ids to varibale "project_parent_ids" in bootstrap stage and run bootstrap stage. - - This step will move the projects from organization to the parent folders specificed. - -## cicd - -1. Why do we need two seperate ServiceAccounts when configuring cicd pipelines (cicd SA and IaC SA) - - Having seperate service accounts helps shutdown the pipeline incase of any issues and still keep IaC SA and ability to run terraform plan/apply manually. - - A pipeline can only generate a token that can get access to an SA. It cannot directly call a provider file to impersonate IaC SA. - - Having providers file that allows impersonation to IaC SA allows flexibility to run terraform manually or from CICD Pipelines. - ![CICD SA and IaC SA](IaC_SA.png) - -## Authenciation - -1. If you are seeing "Permission Issues" when doing terraform apply and the identity with which you are running terraform has correct permissions; - run below command so that correct auth credentials are picked by ADC when terraform commands are executed - - ````bash - gcloud auth application-default login - ```` - - - Refer to [GCP Authentication](https://cloud.google.com/docs/authentication - ) and [Terraform Provider](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference) for more information +- **How can the automation, logging and/or billing export projects be placed under specific folders instead of the org?** + - Run the bootstrap stage and let automation, logging and/or billing projects be created under the organization. + - Add the needed folders to the resource manager stage, or create them outside the stage in the console/gcloud or from a custom Terraform setup. + - Once folders have been created go back to the bootstrap stage, and edit your tfvars file by adding their ids to the `project_parent_ids` variable. + - Run the bootstrap stage again, the projects will be moved under the desired folders. +- **Why do we need two separate service accounts when configuring CI/CD pipelines (CI/CD SA and IaC SA)?** + - To have the pipeline workflow follow the same impersonation flow ([CI/CD SA impersonates IaC SA](IaC_SA.png)) used when applying Terraform manually (user impersonates IaC SA), which allows the pipeline to consume the same auto-generated provider files. + - To allow disabling pipeline credentials in case of issues with a single operation, by removing the ability of the CI/CD SA to impersonate the IaC SA. +- **How can I fix permission issues when running Terraform apply?** + - Make sure your account is part of the organization admin group defined in variables. + - Make sure you have configured [application default credentials](https://cloud.google.com/docs/authentication/application-default-credentials), rerun `gcloud auth login --update-adc` to fix them. From e64e8db20dd041d229983d50ae93f32343c86452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sat, 11 Feb 2023 12:53:28 +0000 Subject: [PATCH 10/47] Allow additive IAM grants by robots name Regreatablly member name will be known after apply, hence changes in the tests --- modules/project/README.md | 23 +++++++++++++++++++ modules/project/iam.tf | 13 ++++++++++- .../examples/iam-additive-members.yaml | 3 --- .../project/examples/iam-additive.yaml | 4 ---- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/modules/project/README.md b/modules/project/README.md index fbc4ab29..e7a645fe 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -138,6 +138,29 @@ module "project" { # tftest modules=1 resources=2 ``` +### Using shortcodes for Service Identities in additive IAM +Most Service Identities contains project number in their e-mail address and this prevents additive IAM to work, as these values are not known at moment of execution of `terraform plan` (its not an issue for authoritative IAM). To refer current project Service Identities you may use shortcodes for Service Identities similarly as for `service_identity_iam` when configuring Shared VPC. + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "project-example" + + services = [ + "run.googleapis.com", + "container.googleapis.com", + ] + + iam_additive = { + "roles/editor" = ["cloudservices"] + "roles/vpcaccess.user" = ["cloudrun"] + "roles/container.hostServiceAgentUser" = ["container-engine"] + } +} +# tftest modules=1 resources=6 +``` + + ### Service identities requiring manual IAM grants The module will create service identities at project creation instead of creating of them at the time of first use. This allows granting these service identities roles in other projects, something which is usually necessary in a Shared VPC context. diff --git a/modules/project/iam.tf b/modules/project/iam.tf index 69925cc7..3ed2d2a6 100644 --- a/modules/project/iam.tf +++ b/modules/project/iam.tf @@ -47,7 +47,18 @@ locals { } iam_additive = { for pair in concat(local._iam_additive_pairs, local._iam_additive_member_pairs) : - "${pair.role}-${pair.member}" => pair + "${pair.role}-${pair.member}" => { + role = pair.role + member = ( + pair.member == "cloudservices" + ? "serviceAccount:${local.service_account_cloud_services}" + : pair.member == "default-compute" + ? "serviceAccount:${local.service_accounts_default.compute}" + : pair.member == "default-gae" + ? "serviceAccount:${local.service_accounts_default.gae}" + : try("serviceAccount:${local.service_accounts_robots[pair.member]}", pair.member) + ) + } } } diff --git a/tests/modules/project/examples/iam-additive-members.yaml b/tests/modules/project/examples/iam-additive-members.yaml index 5832e4dc..6a517a4a 100644 --- a/tests/modules/project/examples/iam-additive-members.yaml +++ b/tests/modules/project/examples/iam-additive-members.yaml @@ -17,17 +17,14 @@ values: project_id: project-example module.project.google_project_iam_member.additive["roles/editor-user:two@example.org"]: condition: [] - member: user:two@example.org project: project-example role: roles/editor module.project.google_project_iam_member.additive["roles/owner-user:one@example.org"]: condition: [] - member: user:one@example.org project: project-example role: roles/owner module.project.google_project_iam_member.additive["roles/owner-user:two@example.org"]: condition: [] - member: user:two@example.org project: project-example role: roles/owner diff --git a/tests/modules/project/examples/iam-additive.yaml b/tests/modules/project/examples/iam-additive.yaml index f07b0df6..5bab8223 100644 --- a/tests/modules/project/examples/iam-additive.yaml +++ b/tests/modules/project/examples/iam-additive.yaml @@ -16,22 +16,18 @@ values: module.project.google_project.project[0]: {} module.project.google_project_iam_member.additive["roles/owner-group:three@example.org"]: condition: [] - member: group:three@example.org project: project-example role: roles/owner module.project.google_project_iam_member.additive["roles/storage.objectAdmin-group:two@example.org"]: condition: [] - member: group:two@example.org project: project-example role: roles/storage.objectAdmin module.project.google_project_iam_member.additive["roles/viewer-group:one@example.org"]: condition: [] - member: group:one@example.org project: project-example role: roles/viewer module.project.google_project_iam_member.additive["roles/viewer-group:two@xample.org"]: condition: [] - member: group:two@xample.org project: project-example role: roles/viewer From a853dc4fe207db2d26f57a37570c0ec01df7d8f2 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Thu, 16 Feb 2023 15:45:20 +0100 Subject: [PATCH 11/47] update Data Platform blueprint README with more example Dataflow commands --- .../demo/dataflow-csv2bq/README.md | 65 ++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md index 44f178fa..14b34213 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md @@ -3,7 +3,66 @@ This demo serves as a simple example of building and launching a Flex Template D ![Dataflow pipeline overview](../../images/df_demo_pipeline.png "Dataflow pipeline overview") -## Example build run + +## Local development run + +For local development, the pipeline can be launched from the local machine for testing purposes using different runners depending on the scope of the test. + +### Using the Beam DirectRunner +The below example uses the Beam DirectRunner. The use case for this runner is mainly for quick local run tests on low volume of data. + +``` +CSV_FILE=gs://[TEST-BUCKET]/customers.csv +JSON_SCHEMA=gs://[TEST-BUCKET]/customers_schema.json +OUTPUT_TABLE=[TEST-PROJ].[TEST-DATASET].customers +PIPELINE_STAGIN_PATH="gs://[TEST-STAGING-BUCKET]" + +python src/csv2bq.py \ +--runner="DirectRunner" \ +--csv_file=$CSV_FILE \ +--json_schema=$JSON_SCHEMA \ +--output_table=$OUTPUT_TABLE \ +--temp_location=$PIPELINE_STAGIN_PATH/tmp +``` + +*Note:* All paths mentioned can be local paths or on GCS. For cloud resources referenced (GCS and BigQuery), make sure that the user launching the command is authenticated to GCP via `gcloud auth application-default login` and has the required access privileges to those resources. + +### Using the DataflowRunner with a local CLI launch + +The below example uses the DataflowRunner locally. The use case for this is for running local tests on larger volumes of test data and verifying that the pipeline runs well on Dataflow, before compiling it into a template. + +``` +PROJECT_ID=[TEST-PROJECT] +REGION=[REGION] +SUBNET=[SUBNET-NAME] +DEV_SERVICE_ACCOUNT=[DEV-SA] + +PIPELINE_STAGIN_PATH="gs://[TEST-STAGING-BUCKET]" +CSV_FILE=gs://[TEST-BUCKET]/customers.csv +JSON_SCHEMA=gs://[TEST-BUCKET]/customers_schema.json +OUTPUT_TABLE=[TEST-PROJ].[TEST-DATASET].customers + +python src/csv2bq.py \ +--runner="Dataflow" \ +--project=$PROJECT_ID \ +--region=$REGION \ +--csv_file=$CSV_FILE \ +--json_schema=$JSON_SCHEMA \ +--output_table=$OUTPUT_TABLE \ +--temp_location=$PIPELINE_STAGIN_PATH/tmp +--staging_location=$PIPELINE_STAGIN_PATH/stage \ +--subnetwork="regions/$REGION/subnetworks/$SUBNET" \ +--impersonate_service_account=$DEV_SERVICE_ACCOUNT \ +--no_use_public_ips +``` + +In terms of resource access priveleges, you can choose to impersonate another service account, which could be defined for development resource access. The authenticated user launching this pipeline will need to have the role `roles/iam.serviceAccountTokenCreator`. If you choose to launch the pipeline without service account impersonation, it will use the default compute service account assigned of the target project. + +## Dataflow Flex Template run + +For production, and as outline in the Data Platform demo, we build and launch the pipeline as a Flex Template, making it available for other cloud services(such as Apache Airflow) and users to trigger launch instances of it on demand. + +### Build launch Below is an example for triggering the Dataflow flex template build pipeline defined in `cloudbuild.yaml`. The Terraform output provides an example as well filled with the parameters values based on the generated resources in the data platform. @@ -28,9 +87,9 @@ gcloud builds submit \ **Note:** For the scope of the demo, the launch of this build is manual, but in production, this build would be launched via a configured cloud build trigger when new changes are merged into the code branch of the Dataflow template. -## Example Dataflow pipeline launch in bash (from flex template) +### Dataflow Flex Template run -Below is an example of launching a dataflow pipeline manually, based on the built template. When launched manually, the Dataflow pipeline would be launched via the orchestration service account, which is what the Airflow DAG is also using in the scope of this demo. +After the build step succeeds. You can launch dataflow pipeline from CLI (outline in this example) or the API via Airflow's operator. For the use case of the data platform, the Dataflow pipeline would be launched via the orchestration service account, which is what the Airflow DAG is also using in the scope of this demo. **Note:** In the data platform demo, the launch of this Dataflow pipeline is handled by the airflow operator (DataflowStartFlexTemplateOperator). From fcdadf521d54e201c26f829b2376cffaa5c8584d Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Thu, 16 Feb 2023 15:48:12 +0100 Subject: [PATCH 12/47] Fix spelling mistake --- .../data-platform-foundations/demo/dataflow-csv2bq/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md index 14b34213..ece7efbb 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md @@ -56,7 +56,7 @@ python src/csv2bq.py \ --no_use_public_ips ``` -In terms of resource access priveleges, you can choose to impersonate another service account, which could be defined for development resource access. The authenticated user launching this pipeline will need to have the role `roles/iam.serviceAccountTokenCreator`. If you choose to launch the pipeline without service account impersonation, it will use the default compute service account assigned of the target project. +In terms of resource access privilege, you can choose to impersonate another service account, which could be defined for development resource access. The authenticated user launching this pipeline will need to have the role `roles/iam.serviceAccountTokenCreator`. If you choose to launch the pipeline without service account impersonation, it will use the default compute service account assigned of the target project. ## Dataflow Flex Template run From eac2065ae2a06838aac02b0ce89661ce78dcd472 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Thu, 16 Feb 2023 17:04:05 +0100 Subject: [PATCH 13/47] Update on docs wording --- .../data-platform-foundations/demo/dataflow-csv2bq/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md index ece7efbb..b052fab0 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md @@ -9,7 +9,7 @@ This demo serves as a simple example of building and launching a Flex Template D For local development, the pipeline can be launched from the local machine for testing purposes using different runners depending on the scope of the test. ### Using the Beam DirectRunner -The below example uses the Beam DirectRunner. The use case for this runner is mainly for quick local run tests on low volume of data. +The below example uses the Beam DirectRunner. The use case for this runner is mainly for quick local tests on the development environment with low volume of data. ``` CSV_FILE=gs://[TEST-BUCKET]/customers.csv @@ -29,7 +29,7 @@ python src/csv2bq.py \ ### Using the DataflowRunner with a local CLI launch -The below example uses the DataflowRunner locally. The use case for this is for running local tests on larger volumes of test data and verifying that the pipeline runs well on Dataflow, before compiling it into a template. +The below example triggers the pipeline on Dataflow from your local development environment. The use case for this is for running local tests on larger volumes of test data and verifying that the pipeline runs well on Dataflow, before compiling it into a template. ``` PROJECT_ID=[TEST-PROJECT] From a497aef707ee6d044f1aebca1af65728c9de141b Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Sat, 18 Feb 2023 21:36:51 +0100 Subject: [PATCH 14/47] feat: new version of projects-data-source based on AssetInventory ds --- modules/projects-data-source/README.md | 60 ++++++--- modules/projects-data-source/main.tf | 142 +++------------------- modules/projects-data-source/outputs.tf | 13 +- modules/projects-data-source/variables.tf | 52 ++++++-- modules/projects-data-source/versions.tf | 2 +- 5 files changed, 116 insertions(+), 153 deletions(-) diff --git a/modules/projects-data-source/README.md b/modules/projects-data-source/README.md index 6fd7dd8a..d4476923 100644 --- a/modules/projects-data-source/README.md +++ b/modules/projects-data-source/README.md @@ -1,9 +1,14 @@ # Projects Data Source Module -This module extends functionality of [google_projects](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/projects) data source by retrieving all the projects and folders under a specific `parent` recursively. +This module extends functionality of [google_projects](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/projects) data source by retrieving all the projects under a specific `parent` recursively with only one API call against [Cloud Asset Inventory](https://cloud.google.com/asset-inventory) service. A good usage pattern would be when we want all the projects under a specific folder (including nested subfolders) to be included into [VPC Service Controls](../vpc-sc/). Instead of manually maintaining the list of project numbers as an input to the `vpc-sc` module we can use that module to retrieve all the project numbers dynamically. +### IAM Permissions required + +- `roles/cloudasset.viewer` on the `parent` level or above + + ## Examples ### All projects in my org @@ -15,11 +20,7 @@ module "my-org" { } output "projects" { - value = module.my-org.projects -} - -output "folders" { - value = module.my-org.folders + value = module.my-org.projects_numbers } # tftest skip (uses data sources) @@ -31,34 +32,65 @@ output "folders" { module "my-dev" { source = "./fabric/modules/projects-data-source" parent = "folders/123456789" - filter = "labels.env:DEV lifecycleState:ACTIVE" + query = "labels.env:DEV state:ACTIVE" } output "dev-projects" { value = module.my-dev.projects } -output "dev-folders" { - value = module.my-dev.folders +# tftest skip (uses data sources) +``` + +### Projects under org with folder/project exclusions +```hcl +module "my-filtered" { + source = "./fabric/modules/projects-data-source" + parent = "organizations/123456789" + ignore_projects = [ + "sandbox-*", # wildcard ignore + "project-full-id", # specific project id + "0123456789" # specific project number + ] + + include_projects = [ + "sandbox-114", # include specific project which was excluded by wildcard + "415216609246" # include specific project which was excluded by wildcard (by project number) + ] + + ignore_folders = [ # subfolders are ingoner as well + "343991594985", + "437102807785", + "345245235245" + ] + query = "state:ACTIVE" +} + +output "filtered-projects" { + value = module.my-filtered.projects } # tftest skip (uses data sources) + ``` + ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [parent](variables.tf#L23) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | ✓ | | -| [filter](variables.tf#L17) | A string filter as defined in the [REST API](https://cloud.google.com/resource-manager/reference/rest/v1/projects/list#query-parameters). | string | | "lifecycleState:ACTIVE" | +| [parent](variables.tf#L17) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | ✓ | | +| [ignore_folders](variables.tf#L58) | A list of folder IDs or numbers to be excluded from the output, all the subfolders and projects are exluded from the output regardless of the include_projects variable. | list(string) | | [] | +| [ignore_projects](variables.tf#L32) | A list of project IDs, numbers or prefixes to exclude matching projects from the module output. | list(string) | | [] | +| [include_projects](variables.tf#L44) | A list of project IDs/numbers to include to the output if some of them are excluded by `ignore_projects` wilcard entries. | list(string) | | [] | +| [query](variables.tf#L26) | A string query as defined in the [Query Syntax](https://cloud.google.com/asset-inventory/docs/query-syntax). | string | | "state:ACTIVE" | ## Outputs | name | description | sensitive | |---|---|:---:| -| [folders](outputs.tf#L17) | Map of folders attributes keyed by folder id. | | -| [project_numbers](outputs.tf#L22) | List of project numbers. | | -| [projects](outputs.tf#L27) | Map of projects attributes keyed by projects id. | | +| [project_numbers](outputs.tf#L17) | List of project numbers. | | +| [projects](outputs.tf#L22) | List of projects in [StandardResourceMetadata](https://cloud.google.com/asset-inventory/docs/reference/rest/v1p1beta1/resources/searchAll#StandardResourceMetadata) format. | | diff --git a/modules/projects-data-source/main.tf b/modules/projects-data-source/main.tf index 76df425e..5bb16b9d 100644 --- a/modules/projects-data-source/main.tf +++ b/modules/projects-data-source/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,129 +15,27 @@ */ locals { - folders_l1_map = { for item in data.google_folders.folders_l1.folders : item.name => item } - - folders_l2_map = merge([ - for _, v in data.google_folders.folders_l2 : - { for item in v.folders : item.name => item } - ]...) - - folders_l3_map = merge([ - for _, v in data.google_folders.folders_l3 : - { for item in v.folders : item.name => item } - ]...) - - folders_l4_map = merge([ - for _, v in data.google_folders.folders_l4 : - { for item in v.folders : item.name => item } - ]...) - - folders_l5_map = merge([ - for _, v in data.google_folders.folders_l5 : - { for item in v.folders : item.name => item } - ]...) - - folders_l6_map = merge([ - for _, v in data.google_folders.folders_l6 : - { for item in v.folders : item.name => item } - ]...) - - folders_l7_map = merge([ - for _, v in data.google_folders.folders_l7 : - { for item in v.folders : item.name => item } - ]...) - - folders_l8_map = merge([ - for _, v in data.google_folders.folders_l8 : - { for item in v.folders : item.name => item } - ]...) - - folders_l9_map = merge([ - for _, v in data.google_folders.folders_l9 : - { for item in v.folders : item.name => item } - ]...) - - folders_l10_map = merge([ - for _, v in data.google_folders.folders_l10 : - { for item in v.folders : item.name => item } - ]...) - - all_folders = merge( - local.folders_l1_map, - local.folders_l2_map, - local.folders_l3_map, - local.folders_l4_map, - local.folders_l5_map, - local.folders_l6_map, - local.folders_l7_map, - local.folders_l8_map, - local.folders_l9_map, - local.folders_l10_map + _ignore_folder_numbers = [for folder_id in var.ignore_folders: trimprefix(folder_id, "folders/")] + _ignore_folders_query = join(" AND NOT folders:", concat([""], local._ignore_folder_numbers)) + query = var.query != "" ? ( + format("%s%s", var.query, local._ignore_folders_query) + ) : ( + format("%s%s", var.query, trimprefix(local._ignore_folders_query, " AND ")) ) - parent_ids = toset(concat( - [split("/", var.parent)[1]], - [for k, _ in local.all_folders : split("/", k)[1]] - )) - - projects = merge([ - for _, v in data.google_projects.projects : - { for item in v.projects : item.project_id => item } - ]...) + ignore_patterns = [for item in var.ignore_projects: "^${replace(item, "*", ".*")}$"] + ignore_regexp = length(local.ignore_patterns) > 0 ? join("|", local.ignore_patterns) : "^NO_PROJECTS_TO_IGNORE$" + projects_after_ignore = [ for item in data.google_cloud_asset_resources_search_all.projects.results : item if ( + length(concat(try(regexall(local.ignore_regexp, trimprefix(item.project, "projects/")), []), try(regexall(local.ignore_regexp, trimprefix(item.name, "//cloudresourcemanager.googleapis.com/projects/")), []))) == 0 + ) || contains(var.include_projects, trimprefix(item.name, "//cloudresourcemanager.googleapis.com/projects/")) || contains(var.include_projects, trimprefix(item.project, "projects/")) + ] } -# 10 datasources are used to cover 10 possible nested layers in GCP organization hirerarcy. -data "google_folders" "folders_l1" { - parent_id = var.parent -} - -data "google_folders" "folders_l2" { - for_each = local.folders_l1_map - parent_id = each.value.name -} - -data "google_folders" "folders_l3" { - for_each = local.folders_l2_map - parent_id = each.value.name -} - -data "google_folders" "folders_l4" { - for_each = local.folders_l3_map - parent_id = each.value.name -} - -data "google_folders" "folders_l5" { - for_each = local.folders_l4_map - parent_id = each.value.name -} - -data "google_folders" "folders_l6" { - for_each = local.folders_l5_map - parent_id = each.value.name -} - -data "google_folders" "folders_l7" { - for_each = local.folders_l6_map - parent_id = each.value.name -} - -data "google_folders" "folders_l8" { - for_each = local.folders_l7_map - parent_id = each.value.name -} - -data "google_folders" "folders_l9" { - for_each = local.folders_l8_map - parent_id = each.value.name -} - -data "google_folders" "folders_l10" { - for_each = local.folders_l9_map - parent_id = each.value.name -} - -# Getting all projects parented by any of the folders in the tree including root prg/folder provided by `parent` variable. -data "google_projects" "projects" { - for_each = local.parent_ids - filter = "parent.id:${each.value} ${var.filter}" +data google_cloud_asset_resources_search_all projects { + provider = google-beta + scope = var.parent + asset_types = [ + "cloudresourcemanager.googleapis.com/Project" + ] + query = local.query } diff --git a/modules/projects-data-source/outputs.tf b/modules/projects-data-source/outputs.tf index b7e38ae2..b1710fa2 100644 --- a/modules/projects-data-source/outputs.tf +++ b/modules/projects-data-source/outputs.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,17 +14,12 @@ * limitations under the License. */ -output "folders" { - description = "Map of folders attributes keyed by folder id." - value = local.all_folders -} - output "project_numbers" { description = "List of project numbers." - value = [for _, v in local.projects : v.number] + value = [for item in local.projects_after_ignore : trimprefix(item.project, "projects/")] } output "projects" { - description = "Map of projects attributes keyed by projects id." - value = local.projects + description = "List of projects in [StandardResourceMetadata](https://cloud.google.com/asset-inventory/docs/reference/rest/v1p1beta1/resources/searchAll#StandardResourceMetadata) format." + value = local.projects_after_ignore } diff --git a/modules/projects-data-source/variables.tf b/modules/projects-data-source/variables.tf index a7f393d3..3273959a 100644 --- a/modules/projects-data-source/variables.tf +++ b/modules/projects-data-source/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,6 @@ * limitations under the License. */ -variable "filter" { - description = "A string filter as defined in the [REST API](https://cloud.google.com/resource-manager/reference/rest/v1/projects/list#query-parameters)." - type = string - default = "lifecycleState:ACTIVE" -} - variable "parent" { description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." type = string @@ -28,3 +22,47 @@ variable "parent" { error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." } } + +variable "query" { + description = "A string query as defined in the [Query Syntax](https://cloud.google.com/asset-inventory/docs/query-syntax)." + type = string + default = "state:ACTIVE" +} + +variable "ignore_projects" { + description = "A list of project IDs, numbers or prefixes to exclude matching projects from the module output." + type = list(string) + default = [] + # example + #ignore_projects = [ + # "dev-proj-1", + # "uat-proj-2", + # "0123456789", + # "prd-proj-*" + #] +} +variable "include_projects" { + description = "A list of project IDs/numbers to include to the output if some of them are excluded by `ignore_projects` wilcard entries." + type = list(string) + default = [] + # example excluding all the projects starting with "prf-" except "prd-123457" + #ignore_projects = [ + # "prd-*" + #] + #include_projects = [ + # "prd-123457", + # "0123456789" + #] +} + +variable "ignore_folders" { + description = "A list of folder IDs or numbers to be excluded from the output, all the subfolders and projects are exluded from the output regardless of the include_projects variable." + type = list(string) + default = [] + # example exlusing a folder + # ignore_folders = [ + # "folders/0123456789", + # "2345678901" + # ] +} + diff --git a/modules/projects-data-source/versions.tf b/modules/projects-data-source/versions.tf index 286536a6..23f38edb 100644 --- a/modules/projects-data-source/versions.tf +++ b/modules/projects-data-source/versions.tf @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 1c302c7ab331afbe32b78655aea4a1b02d6e4efb Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Sat, 18 Feb 2023 21:48:05 +0100 Subject: [PATCH 15/47] TF formatting --- modules/projects-data-source/main.tf | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/projects-data-source/main.tf b/modules/projects-data-source/main.tf index 5bb16b9d..6bd5631c 100644 --- a/modules/projects-data-source/main.tf +++ b/modules/projects-data-source/main.tf @@ -15,25 +15,25 @@ */ locals { - _ignore_folder_numbers = [for folder_id in var.ignore_folders: trimprefix(folder_id, "folders/")] - _ignore_folders_query = join(" AND NOT folders:", concat([""], local._ignore_folder_numbers)) + _ignore_folder_numbers = [for folder_id in var.ignore_folders : trimprefix(folder_id, "folders/")] + _ignore_folders_query = join(" AND NOT folders:", concat([""], local._ignore_folder_numbers)) query = var.query != "" ? ( format("%s%s", var.query, local._ignore_folders_query) - ) : ( + ) : ( format("%s%s", var.query, trimprefix(local._ignore_folders_query, " AND ")) ) - ignore_patterns = [for item in var.ignore_projects: "^${replace(item, "*", ".*")}$"] + ignore_patterns = [for item in var.ignore_projects : "^${replace(item, "*", ".*")}$"] ignore_regexp = length(local.ignore_patterns) > 0 ? join("|", local.ignore_patterns) : "^NO_PROJECTS_TO_IGNORE$" - projects_after_ignore = [ for item in data.google_cloud_asset_resources_search_all.projects.results : item if ( - length(concat(try(regexall(local.ignore_regexp, trimprefix(item.project, "projects/")), []), try(regexall(local.ignore_regexp, trimprefix(item.name, "//cloudresourcemanager.googleapis.com/projects/")), []))) == 0 + projects_after_ignore = [for item in data.google_cloud_asset_resources_search_all.projects.results : item if( + length(concat(try(regexall(local.ignore_regexp, trimprefix(item.project, "projects/")), []), try(regexall(local.ignore_regexp, trimprefix(item.name, "//cloudresourcemanager.googleapis.com/projects/")), []))) == 0 ) || contains(var.include_projects, trimprefix(item.name, "//cloudresourcemanager.googleapis.com/projects/")) || contains(var.include_projects, trimprefix(item.project, "projects/")) ] } -data google_cloud_asset_resources_search_all projects { +data "google_cloud_asset_resources_search_all" "projects" { provider = google-beta - scope = var.parent + scope = var.parent asset_types = [ "cloudresourcemanager.googleapis.com/Project" ] From 8174890331d3204adc770d0cbdf001aad59ba354 Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Sat, 18 Feb 2023 22:04:40 +0100 Subject: [PATCH 16/47] Sort variables --- modules/projects-data-source/variables.tf | 44 +++++++++++------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/modules/projects-data-source/variables.tf b/modules/projects-data-source/variables.tf index 3273959a..9fef35ab 100644 --- a/modules/projects-data-source/variables.tf +++ b/modules/projects-data-source/variables.tf @@ -14,19 +14,15 @@ * limitations under the License. */ -variable "parent" { - description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." - type = string - validation { - condition = can(regex("(organizations|folders)/[0-9]+", var.parent)) - error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." - } -} - -variable "query" { - description = "A string query as defined in the [Query Syntax](https://cloud.google.com/asset-inventory/docs/query-syntax)." - type = string - default = "state:ACTIVE" +variable "ignore_folders" { + description = "A list of folder IDs or numbers to be excluded from the output, all the subfolders and projects are exluded from the output regardless of the include_projects variable." + type = list(string) + default = [] + # example exlusing a folder + # ignore_folders = [ + # "folders/0123456789", + # "2345678901" + # ] } variable "ignore_projects" { @@ -41,6 +37,7 @@ variable "ignore_projects" { # "prd-proj-*" #] } + variable "include_projects" { description = "A list of project IDs/numbers to include to the output if some of them are excluded by `ignore_projects` wilcard entries." type = list(string) @@ -55,14 +52,17 @@ variable "include_projects" { #] } -variable "ignore_folders" { - description = "A list of folder IDs or numbers to be excluded from the output, all the subfolders and projects are exluded from the output regardless of the include_projects variable." - type = list(string) - default = [] - # example exlusing a folder - # ignore_folders = [ - # "folders/0123456789", - # "2345678901" - # ] +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + validation { + condition = can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } } +variable "query" { + description = "A string query as defined in the [Query Syntax](https://cloud.google.com/asset-inventory/docs/query-syntax)." + type = string + default = "state:ACTIVE" +} From ff4b2fffe274d7b58dbc2f75477a03ccbc5b3198 Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Sat, 18 Feb 2023 22:11:44 +0100 Subject: [PATCH 17/47] Regenerate docs --- modules/projects-data-source/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/modules/projects-data-source/README.md b/modules/projects-data-source/README.md index d4476923..8fcfad96 100644 --- a/modules/projects-data-source/README.md +++ b/modules/projects-data-source/README.md @@ -73,18 +73,17 @@ output "filtered-projects" { # tftest skip (uses data sources) ``` - ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [parent](variables.tf#L17) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | ✓ | | -| [ignore_folders](variables.tf#L58) | A list of folder IDs or numbers to be excluded from the output, all the subfolders and projects are exluded from the output regardless of the include_projects variable. | list(string) | | [] | -| [ignore_projects](variables.tf#L32) | A list of project IDs, numbers or prefixes to exclude matching projects from the module output. | list(string) | | [] | -| [include_projects](variables.tf#L44) | A list of project IDs/numbers to include to the output if some of them are excluded by `ignore_projects` wilcard entries. | list(string) | | [] | -| [query](variables.tf#L26) | A string query as defined in the [Query Syntax](https://cloud.google.com/asset-inventory/docs/query-syntax). | string | | "state:ACTIVE" | +| [parent](variables.tf#L55) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | ✓ | | +| [ignore_folders](variables.tf#L17) | A list of folder IDs or numbers to be excluded from the output, all the subfolders and projects are exluded from the output regardless of the include_projects variable. | list(string) | | [] | +| [ignore_projects](variables.tf#L28) | A list of project IDs, numbers or prefixes to exclude matching projects from the module output. | list(string) | | [] | +| [include_projects](variables.tf#L41) | A list of project IDs/numbers to include to the output if some of them are excluded by `ignore_projects` wilcard entries. | list(string) | | [] | +| [query](variables.tf#L64) | A string query as defined in the [Query Syntax](https://cloud.google.com/asset-inventory/docs/query-syntax). | string | | "state:ACTIVE" | ## Outputs From befc73ec126f46c0544a4491fa6a14b4d2aa3d99 Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Sun, 19 Feb 2023 00:51:49 +0100 Subject: [PATCH 18/47] feat: Update TFC+WIF blueprint with TFC Dynamic Credentials feature. --- blueprints/README.md | 2 +- blueprints/cloud-operations/README.md | 4 +- .../README.md | 57 ++++++++++-------- .../diagram.png | Bin 0 -> 75704 bytes .../gcp-workload-identity-provider/README.md | 26 ++++---- .../gcp-workload-identity-provider/main.tf | 32 +++++----- .../gcp-workload-identity-provider/outputs.tf | 25 +++----- .../variables.tf | 16 ++--- .../tfc-workflow-using-wif/README.md | 16 +++++ .../backend.tf.template | 6 +- .../tfc-workflow-using-wif/main.tf | 4 +- .../tfc-workflow-using-wif/provider.tf} | 12 +--- .../terraform.auto.tfvars.template | 5 +- .../tfc-workflow-using-wif/variables.tf} | 11 +--- .../terraform-enterprise-wif/diagram.png | Bin 29084 -> 0 bytes .../terraform.auto.tfvars.template | 20 ------ .../tfc-workflow-using-wif/README.md | 17 ------ .../tfc-workflow-using-wif/provider.tf | 24 -------- .../tfc-workflow-using-wif/tfc-oidc/README.md | 38 ------------ .../tfc-workflow-using-wif/tfc-oidc/main.tf | 23 ------- .../tfc-oidc/outputs.tf | 27 --------- .../tfc-oidc/variables.tf | 26 -------- .../tfc-oidc/versions.tf | 29 --------- .../tfc-workflow-using-wif/variables.tf | 24 -------- .../__init__.py | 0 .../__init__.py | 0 .../fixture/main.tf | 8 +-- .../fixture/variables.tf | 16 ++--- .../test_plan.py | 0 29 files changed, 124 insertions(+), 344 deletions(-) rename blueprints/cloud-operations/{terraform-enterprise-wif => terraform-cloud-dynamic-credentials}/README.md (50%) create mode 100644 blueprints/cloud-operations/terraform-cloud-dynamic-credentials/diagram.png rename blueprints/cloud-operations/{terraform-enterprise-wif => terraform-cloud-dynamic-credentials}/gcp-workload-identity-provider/README.md (54%) rename blueprints/cloud-operations/{terraform-enterprise-wif => terraform-cloud-dynamic-credentials}/gcp-workload-identity-provider/main.tf (77%) rename blueprints/cloud-operations/{terraform-enterprise-wif => terraform-cloud-dynamic-credentials}/gcp-workload-identity-provider/outputs.tf (53%) rename blueprints/cloud-operations/{terraform-enterprise-wif => terraform-cloud-dynamic-credentials}/gcp-workload-identity-provider/variables.tf (83%) create mode 100644 blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/README.md rename blueprints/cloud-operations/{terraform-enterprise-wif => terraform-cloud-dynamic-credentials}/tfc-workflow-using-wif/backend.tf.template (89%) rename blueprints/cloud-operations/{terraform-enterprise-wif => terraform-cloud-dynamic-credentials}/tfc-workflow-using-wif/main.tf (91%) rename blueprints/cloud-operations/{terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh => terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/provider.tf} (74%) rename blueprints/cloud-operations/{terraform-enterprise-wif => terraform-cloud-dynamic-credentials}/tfc-workflow-using-wif/terraform.auto.tfvars.template (75%) rename blueprints/cloud-operations/{terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/get_audience.sh => terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/variables.tf} (81%) delete mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/diagram.png delete mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template delete mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md delete mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf delete mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md delete mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf delete mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf delete mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.tf delete mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf delete mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf rename tests/blueprints/cloud_operations/{terraform_enterprise_wif => terraform-cloud-dynamic-credentials}/__init__.py (100%) rename tests/blueprints/cloud_operations/{terraform_enterprise_wif => terraform-cloud-dynamic-credentials}/gcp_workload_identity_provider/__init__.py (100%) rename tests/blueprints/cloud_operations/{terraform_enterprise_wif => terraform-cloud-dynamic-credentials}/gcp_workload_identity_provider/fixture/main.tf (81%) rename tests/blueprints/cloud_operations/{terraform_enterprise_wif => terraform-cloud-dynamic-credentials}/gcp_workload_identity_provider/fixture/variables.tf (83%) rename tests/blueprints/cloud_operations/{terraform_enterprise_wif => terraform-cloud-dynamic-credentials}/gcp_workload_identity_provider/test_plan.py (100%) diff --git a/blueprints/README.md b/blueprints/README.md index 60a84912..b19b02d3 100644 --- a/blueprints/README.md +++ b/blueprints/README.md @@ -5,7 +5,7 @@ This section provides **[networking blueprints](./networking/)** that implement Currently available blueprints: - **apigee** - [Apigee Hybrid on GKE](./apigee/hybrid-gke/), [Apigee X analytics in BigQuery](./apigee/bigquery-analytics), [Apigee network patterns](./apigee/network-patterns/) -- **cloud operations** - [Active Directory Federation Services](./cloud-operations/adfs), [Cloud Asset Inventory feeds for resource change tracking and remediation](./cloud-operations/asset-inventory-feed-remediation), [Fine-grained Cloud DNS IAM via Service Directory](./cloud-operations/dns-fine-grained-iam), [Cloud DNS & Shared VPC design](./cloud-operations/dns-shared-vpc), [Delegated Role Grants](./cloud-operations/iam-delegated-role-grants), [Networking Dashboard](./cloud-operations/network-dashboard), [Managing on-prem service account keys by uploading public keys](./cloud-operations/onprem-sa-key-management), [Compute Image builder with Hashicorp Packer](./cloud-operations/packer-image-builder), [Packer example](./cloud-operations/packer-image-builder/packer), [Compute Engine quota monitoring](./cloud-operations/quota-monitoring), [Scheduled Cloud Asset Inventory Export to Bigquery](./cloud-operations/scheduled-asset-inventory-export-bq), [Configuring workload identity federation for Terraform Cloud/Enterprise workflow](./cloud-operations/terraform-enterprise-wif), [TCP healthcheck and restart for unmanaged GCE instances](./cloud-operations/unmanaged-instances-healthcheck), [Migrate for Compute Engine (v5) blueprints](./cloud-operations/vm-migration), [Configuring workload identity federation to access Google Cloud resources from apps running on Azure](./cloud-operations/workload-identity-federation) +- **cloud operations** - [Active Directory Federation Services](./cloud-operations/adfs), [Cloud Asset Inventory feeds for resource change tracking and remediation](./cloud-operations/asset-inventory-feed-remediation), [Fine-grained Cloud DNS IAM via Service Directory](./cloud-operations/dns-fine-grained-iam), [Cloud DNS & Shared VPC design](./cloud-operations/dns-shared-vpc), [Delegated Role Grants](./cloud-operations/iam-delegated-role-grants), [Networking Dashboard](./cloud-operations/network-dashboard), [Managing on-prem service account keys by uploading public keys](./cloud-operations/onprem-sa-key-management), [Compute Image builder with Hashicorp Packer](./cloud-operations/packer-image-builder), [Packer example](./cloud-operations/packer-image-builder/packer), [Compute Engine quota monitoring](./cloud-operations/quota-monitoring), [Scheduled Cloud Asset Inventory Export to Bigquery](./cloud-operations/scheduled-asset-inventory-export-bq), [Configuring workload identity federation with Terraform Cloud/Enterprise workflows](./cloud-operations/terraform-cloud-dynamic-credentials), [TCP healthcheck and restart for unmanaged GCE instances](./cloud-operations/unmanaged-instances-healthcheck), [Migrate for Compute Engine (v5) blueprints](./cloud-operations/vm-migration), [Configuring workload identity federation to access Google Cloud resources from apps running on Azure](./cloud-operations/workload-identity-federation) - **data solutions** - [GCE and GCS CMEK via centralized Cloud KMS](./data-solutions/cmek-via-centralized-kms), [Cloud Composer version 2 private instance, supporting Shared VPC and external CMEK key](./data-solutions/composer-2), [Cloud SQL instance with multi-region read replicas](./data-solutions/cloudsql-multiregion), [Data Platform](./data-solutions/data-platform-foundations), [Spinning up a foundation data pipeline on Google Cloud using Cloud Storage, Dataflow and BigQuery](./data-solutions/gcs-to-bq-with-least-privileges), [#SQL Server Always On Groups blueprint](./data-solutions/sqlserver-alwayson), [Data Playground](./data-solutions/data-playground), [MLOps with Vertex AI](./data-solutions/vertex-mlops), [Shielded Folder](./data-solutions/shielded-folder) - **factories** - [The why and the how of Resource Factories](./factories), [Google Cloud Identity Group Factory](./factories/cloud-identity-group-factory), [Google Cloud BQ Factory](./factories/bigquery-factory), [Google Cloud VPC Firewall Factory](./factories/net-vpc-firewall-yaml), [Minimal Project Factory](./factories/project-factory) - **GKE** - [Binary Authorization Pipeline Blueprint](./gke/binauthz), [Storage API](./gke/binauthz/image), [Multi-cluster mesh on GKE (fleet API)](./gke/multi-cluster-mesh-gke-fleet-api), [GKE Multitenant Blueprint](./gke/multitenant-fleet), [Shared VPC with GKE support](./networking/shared-vpc-gke/) diff --git a/blueprints/cloud-operations/README.md b/blueprints/cloud-operations/README.md index 863aee58..99013624 100644 --- a/blueprints/cloud-operations/README.md +++ b/blueprints/cloud-operations/README.md @@ -64,9 +64,9 @@ This [blueprint](./onprem-sa-key-management) shows how to manage IAM Service Acc
-## Workload identity federation for Terraform Enterprise workflow +## Workload identity federation with Terraform Cloud workflows - This [blueprint](./terraform-enterprise-wif) shows how to configure [Wokload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between [Terraform Cloud/Enterprise](https://developer.hashicorp.com/terraform/enterprise) instance and Google Cloud. + This [blueprint](./terraform-cloud-dynamic-credentials) shows how to configure [Wokload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between [Terraform Cloud/Enterprise](https://developer.hashicorp.com/terraform/enterprise) instance and Google Cloud.
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/README.md b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/README.md similarity index 50% rename from blueprints/cloud-operations/terraform-enterprise-wif/README.md rename to blueprints/cloud-operations/terraform-cloud-dynamic-credentials/README.md index 4bb282c5..3cec722e 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/README.md +++ b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/README.md @@ -1,10 +1,10 @@ -# Configuring workload identity federation for Terraform Cloud/Enterprise workflow +# Configuration of workload identity federation for Terraform Cloud/Enterprise workflows -The most common way to use Terraform Cloud for GCP deployments is to store a GCP Service Account Key as a part of TFE Workflow configuration, as we all know there are security risks due to the fact that keys are long term credentials that could be compromised. +The most common way to use Terraform Cloud for GCP deployments is to store a GCP Service Account Key as a part of TFC Workflow configuration, as we all know there are security risks due to the fact that keys are long term credentials that could be compromised. Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. -This blueprint shows how to set up [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between [Terraform Cloud/Enterprise](https://developer.hashicorp.com/terraform/enterprise) instance and Google Cloud. This will be possible by configuring workload identity federation to trust oidc tokens generated for a specific workflow in a Terraform Enterprise organization. +This blueprint shows how to set up [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between [Terraform Cloud/Enterprise](https://developer.hashicorp.com/terraform/enterprise) instance and Google Cloud. This will be possible by configuring workload identity federation and [Terraform Cloud Dynamic Provider Credentials](https://www.hashicorp.com/blog/terraform-cloud-adds-dynamic-provider-credentials-vault-official-cloud-providers). The following diagram illustrates how the VM will get a short-lived access token and use it to access a resource: @@ -12,8 +12,8 @@ The following diagram illustrates how the VM will get a short-lived access token ## Running the blueprint -### Create Terraform Enterprise Workflow -If you don't have an existing Terraform Enterprise organization you can sign up for a [free trial](https://app.terraform.io/public/signup/account) account. +### Create Terraform Cloud Workflow +If you don't have an existing Terraform Cloud organization you can sign up for a [free trial](https://app.terraform.io/public/signup/account) account. Create a new Workspace for a `CLI-driven workflow` (Identity Federation will work for any workflow type, but for simplicity of the blueprint we use CLI driven workflow). @@ -21,7 +21,7 @@ Note workspace name and id (id starts with `ws-`), we will use them on a later s Go to the organization settings and note the org name and id (id starts with `org-`). -### Deploy GCP Workload Identity Pool Provider for Terraform Enterprise +### Deploy GCP Workload Identity Pool Provider for Terraform Cloud integration > **_NOTE:_** This is a preparation part and should be executed on behalf of a user with enough permissions. @@ -32,7 +32,7 @@ Required permissions when new project is created: - Workload Identity Admin on the project level - Project IAM Admin on the project level -Fill out required variables, use TFE Org and Workspace IDs from the previous steps (IDs are not the names). +Fill out required variables, use TFC Org and Workspace IDs from the previous steps (IDs are not the names). ```bash cd gcp-workload-identity-provider @@ -50,34 +50,41 @@ terraform init terraform apply ``` -As a result a set of outputs will be provided (your values will be different), note the output since we will use it on the next steps. +You will receive a set of outputs (your values may be different), note them because we will need them in the next steps. ``` -impersonate_service_account_email = "sa-tfe@fe-test-oidc.iam.gserviceaccount.com" -project_id = "tfe-test-oidc" -workload_identity_audience = "//iam.googleapis.com/projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" -workload_identity_pool_provider_id = "projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +project_id = "tfc-dynamic-creds-gcp" +tfc_workspace_wariables = { + "TFC_GCP_PROJECT_NUMBER" = "200635100209" + "TFC_GCP_PROVIDER_AUTH" = "true" + "TFC_GCP_RUN_SERVICE_ACCOUNT_EMAIL" = "sa-tfc@tfc-dynamic-creds-gcp.iam.gserviceaccount.com" + "TFC_GCP_WORKLOAD_POOL_ID" = "tfc-pool" + "TFC_GCP_WORKLOAD_PROVIDER_ID" = "tfc-provider" +} ``` -### Configure OIDC provider for your TFE Workflow +### Configure Dynamic Provider Credentials for your TFC Workflow -To enable OIDC for a TFE workflow it's enough to setup an environment variable `TFC_WORKLOAD_IDENTITY_AUDIENCE`. +To configure [GCP Dynamic Provider Credentials](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/gcp-configuration) for a TFC workflow, you need to set a set of environment variables: +- `TFC_GCP_PROVIDER_AUTH` +- `TFC_GCP_PROJECT_NUMBER` +- `TFC_GCP_RUN_SERVICE_ACCOUNT_EMAIL` +- `TFC_GCP_WORKLOAD_POOL_ID` +- `TFC_GCP_WORKLOAD_PROVIDER_ID` -Go the the Workflow -> Variables and add a new variable `TFC_WORKLOAD_IDENTITY_AUDIENCE` equal to the value of `workload_identity_audience` output, in our example it's: +Go to the Workflow -> Variables page and click the + Add variable button. For variable type select ` Environment variable`. The variable names listed above are the names of the variables that you need to set. The values provided in the terraform output in the previous step are the values that you need to provide for each variable. -``` -TFC_WORKLOAD_IDENTITY_AUDIENCE = "//iam.googleapis.com/projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" -``` - -At that point we setup GCP Identity Federation to trust TFE generated OIDC tokens, so the TFE workflow can use the token to impersonate a GCP Service Account. +At that point we set up GCP Identity Federation to trust TFC generated OIDC tokens, workflow should be able to use Dynamic Provider Credentials to impersonate a GCP Service Account. ## Testing the blueprint -In order to test the setup we will deploy a GCS bucket from TFE Workflow using OIDC token for Service Account Impersonation. +To test the setup, we will deploy a GCS bucket from the TFC Workflow created in the previous step. + +This will allow us to verify that the workflow can successfully interact with GCP services using the TFC Dynamic Provider Credentials. ### Configure backend and variables -First, we need to configure TFE Remote backend for our testing terraform code, use TFE Organization name and workspace name (names are not the same as ids) +First, we need to configure the TFC Remote backend for our testing Terraform code. Use the TFC Organization name and workspace name (names are not the same as ids). ``` cd ../tfc-workflow-using-wif @@ -89,7 +96,7 @@ vi backend.tf ``` -Fill out variables based on the output from the preparation steps: +Fill out `project_id` variable based on the output from the preparation steps: ``` mv terraform.auto.tfvars.template terraform.auto.tfvars @@ -100,7 +107,7 @@ vi terraform.auto.tfvars ### Authenticate terraform for triggering CLI-driven workflow -Follow this [documentation](https://learn.hashicorp.com/tutorials/terraform/cloud-login) to login ti terraform cloud from the CLI. +Follow this [documentation](https://learn.hashicorp.com/tutorials/terraform/cloud-login) to login to terraform cloud from the CLI. ### Trigger the workflow @@ -110,6 +117,6 @@ terraform init terraform apply ``` -As a result we have a successfully deployed GCS bucket from Terraform Enterprise workflow using Workload Identity Federation. +As a result we have a successfully deployed GCS bucket from Terraform Cloud workflow using Workload Identity Federation. Once done testing, you can clean up resources by running `terraform destroy` first in the `tfc-workflow-using-wif` and then `gcp-workload-identity-provider` folders. diff --git a/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/diagram.png b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..9216226ad1323e94178e51f56d083ffa47e50fa1 GIT binary patch literal 75704 zcmY&Y|= z@Ae2PK-^FC9`}FWXi+O3snGtbwOI(P_;uGQ`SlZ+U?Xx8%@;i}&y-{s+W!h96#lP( z`vg2h90JCn zN@)atB!{u(gF2nb5TC#-z6y_WOoJ8RavBUi{i^PlI&`io*{q5safkI^ zHFu#4GO0&RSW0j2h42uwr)ZbVasVEd_sV#|<-Lf25Y!I78_t3dO!}!0@r&KX$K3-{ z93OLsn)Hy4redNlcZ)sa8|o6(c~iz3Os{%W&UY+=B#R|d&4F2$5bvqUo1qSq^OR_7*z zo;q&o%E`nvf)Yafhe%6A`pojr*!DBSUa1kvxE5vVxYB#mZ0g<+o;HgNwzfyt(PJ1+sgbr0Onj)Avg)cL|)xE|1K$RF2FG&q0!m3Qhutu zYC9{RNqzWgUidQIr>o^G-;9-YYqI)m+M{57NcWg8rU}*@el?u_C0&c@N`Jh>Fean z5|T3aJM8f>V1hHUvP|~qc2tH)!6V@t;?SQ+tT^cR-BA!xOwQX(KIso^+1UuVR@DkPM8w$W=sxYm+AS`XiYNVyjsqW3d179q@yBVFWmqi`hwu7hZy%~H>1byOb zeZx9KXe8KQ-}v%oAzmOk2vCS>H<}r7{nf&7sh%6xUUq*ZGTcRpm0@ks-#v17r%suA z==E5T+Id30us*MUh(c=hm$DwrIhP-os@QHTqMKteu?0u>X6145cXjPjh&G@kTkVy| z&m;vxA|ljiv9huPc)SNz;+Sv65h5D>1EaoO4Muz?4W$qFi(XiUEkHgBrLW^{-ku6! zA^5NNhwF^$&Zb(%1?B;Hyhqi&-+TD9vYaXD@Aa0otv4W0%W zeNG}j?+re`_q4s;jE7bk!}|W#GheF1JLk!vFr=>{C8){zK5i(o;T~Hx9V3PlA68&n z!+-$a4xn|GSsb8ux$Jr*;PHN%$rrs!u=BmmgoDUvu$ZTKQx_E#l~RX9#9OX+qV%&} z?R@V2cz>$fHa<7HO`&H(8S=ObLk`t!Fx->q9#6aM+>4>DZ9grk^**q%^TIXu`nb)V zULJ^~WWoBmf3A9ruqA?rG8EFDI9U{wUhmTkfgL~~oG@U)HQaKaW@B;G#xUBlY5)g! zEf-mMb**NKc3Qq+gAQCx^+7>HH2AR=_rP@oiw^FENPK)UnksW}>G%2Cvhh&;dF%Hv z&DirSU+X?d%VD<}r8j(K$NTAB^;#G!05h1rH-JoswA;w#i!nUso}~dH5d>c_-tF`H zK)?lcveMypsgiS(w9(;a;4p|D6_kx~*y*<=@HAZWdeL!yGk&@C=WK4Af!Ft3lXz>q z6!G6?v;kZV;8l*pYs=@cPmfD;$MtdD=aF~wI%eAE!YPg;{5*t7SU0X?o7p-SOz`vV z*6?S)-4z5$6g0XH>kQ!PDFr;|c1$Bro=V6GK~g&8za7wta4>U`CucPlKPoyl1Rho1 zh}5P(Df+hq<_S(@`yJ)5g7MPu@G#K86-M>CRQpg#*m&o$neatPsoi(Qc7CsGN-Sh= z*bgc#4AlxNLi)_l<`JQNN@U!!`uPGBd0Y{fZAUqb-aD{%OlklWSoTj#Z?H#W3U{qT4_7SvoO8G1-m{J`M~vKR+H>pAx>W zBWMG7tV6r@zV9vXa3bLk5KAGBOD~|)Y)@qeGVXm64zf}P$a2JeB(7W{@O1!VT3diIxi1KyneE3ddgMQfh)(46jrF8 zg>#w?$K2-JSDC*L*}V-7Vj4s}E8fx8!au95i8!Q@LNmdDwB9nI)_9Z5{i=-vmL^E_dG zvC?>i==+&{>9zGbvj4KW)ctVC+4GD&?LH(aaDxmr$U!kGragYqo{r+J0$$^60@$@t-IHf<6{!{xZ zbE)&Cl-}`8Z|-}Grn>{%Wsfe;0k6X!)d_IlzfsCLsG2h3AWDh`iutwXk2zh0pRbA^{fu2F zBiFM;iS%k#np+?2qP{#T<(w7KX2-2>q{e$LY&$qiGtKB}lNnt7;M$Tvvx3jUn&PP+(t^2ke|C*=$K5h~D zu0TYPEA@Qt7q0RvYg*|NZyHaw2?g#SKerfr4$e5Y*w~goyB|NEH@15AQ2kz&C@u9p zo{R0=->o2DJZ;PAj zczE6;hg5PdC{oi&w5!drS4J@S$4s}n?V!S-qO>yF+LX`Do=z=2%&C}4!^P3GM{7(j z$?@PM%Zp?$`rx#9eOc~e*5UC7ImrjVMsx-|r>k`MxGA)Br7}sL;TXPysn3!uB(+%s z<{-CVvaD%?J_za_p|HUl>#UU-!cAk}hO`M*yp8QmJYAGrQ0C<`^%Cr|G8Xq+VsN>0 zibe`2pjFuvfv0mPLaL3L*c+vNDt7|gV|oFt5Ak_Eq{S=pg)&Q7w@DgLr=NwKx}mW< zcoQ2Phz;*Jr|$G#%UrS~NJQg3M3k0JE?k&93)L>Y8!Lqx%+ zZN|5L#ynOZ;F{P&w+Iy?mqG^!jbrnBCROK?<)g<#PeZIA6h-74!8+vj)sS_E8gpNE z-Y(YYjrXyLD8TA-?}pYTg(4Dm?-A?0lv1(BLyv`5*?skmTs{JK$$M@3}4-O%O|LaJYs42LUT=XrUgBpthV2=-jR%SYeZHFvUU*?{2S_mSMA z%jIvgeFY<;+$DGMk_3j0V&te>fs3G!B)O5_u5uV{R~p2}Xi*EnNQKi3FI#(;(Z*Nk zlmx6LjS}-&mIXn}u=%Vl0A^rWMsL!m7`(yl4$cV^gI*XY!(ik__y}2oE2OnqW#5(W zQn4?YpWSkbdE?0i5kB!agYQsqVgK<`rxdIPDa^aA_csx?b|D0l#fwM`XB`SKZcW_5 z_u5L3n{1t?ZFjmmBYiDB>?E_m5txF8TJF>PV>Lhg}-CI8=Ldv#7 ziHJkHNeTWXBjTJu~5FqC&U4h$k z!eKWkPRpvUZp;Lmbo}L>9PpcelZ+)86i7ePn+Jn!(IsLJJ!6kT6;EV-X3r@owgbxo zyN#{ydA%Tfy9yNydgpsxnGSh>JC5d5kXJDBVxoydtb*bQVQ+gywR)@le5oBE1s?JI z%b*2;b!x{X_c9)v*L9>GLW((;pamz$wG_%%EXuQ~AO?8xuq2%8@FEqm)39}VX^o0$&_4wpuIxljDU$+fZ}m3w zc8NB<&vdp#jQl6x-nTyaYq{#Kg8N*M>LsRhAmRI?2kW3kDUQ>C?n3BJpV^3tXsIFe zVMM84h~yQL@Ys^Rm(8g^qNI!eniyzOfQ}fp6sl#&Ev%p>67Ht-K!lUCfyYuI;-=5%-+F*QFLsg4 zLY=xb-@4c3-PJ>Q_L6>Pn{(sRJm%jrqZOejYyu{QUS(s7>`5}8NitB1lN+;jX~TK0 zOp4*glXbk!#;d1b5otU%6dq%fBHbYl?)*$0wUo>-k}XWkB9mKauap`m5%r)cneeOT zUrk)u5}*&kB7dytvg^qtzd)7G)t1E>#+1Z(fVy~D3+Jz4fzlxnk}2@IEzPt1`1p`n zk5jM53(5h=@zin5po3+s$zS6sleIqotm;iZf!M|D>@|E<7~NPzC+2*sCzg-80=9vg zEsgMP8**RyebC@-&|Khl1(kBBE~vR3Q6M!PH=_5x4 z{rkVA0_fv38+~F~QfN@-T8s2mkS<2SHZ~I@zT4V2|3v*8#2$!1*q;H?UXyh^?5rMa zUh~6`|JvWTMcvfR5c0R(pYwK)DE9i^S=%Hj-T-QhddK)!r;lE&HCti$s1)l}OQbq~ z66X7Gr|)lqLB~rYSyl71@;JUZ!^9Y!>SM?WHIR#Df6#G*-KzhFTg&!L3``KDq>vL$ z79pO72nuEbOs2znbA!#j1&xnMrPXv&FDr%TP{J!)<4+wHOxXzJqE<@c@5+c%uxf*szAbPJn!v2#a*!|<%-Y`>RM zzn2!qk5DT-ufNVxG)J}1-^sRsK&Vqjc0n~hB# z=L@}AJhDD`xGRBUsn`8*qk&fhkail?=?V+@J$$YvkD?uJ@V+nj;GIkB&J}h{Sh^ZA zh%q67g_H6N6TzV+jrT-<_8Ou&`K*S6F%e6$mAja+F}>tyjJG3`P*$;UK1>2O+&&pj zLB$?KNWbfO+^a1a_`Tb2;gpizaC#(i@$>SIX3nq0WJ=!=Kf{a>vVvbLLP7Q;_T$B@ zhhIB9kPoI8&2U^|I*#6rU|dfY5_-}kCRm5FPO>F_iQG<XJ$F|BH2w6w$Df-oT2&^7fE zoyoy>k8)IT#08eh5dlDyhkt+dBdC5T8ZxJujpAinJg#JeO~3hjHyeF{;KY7%jf0aQ zj0cbTzFio)%|A&+c6=HT$q<0L)o~^eu*+S^f70nBgC}R6qmHkCnFju7HG9+t&$(_t>9S-DBJk9QxalyAKO7T%Q*L6* zJH)P@0IADNMs}RGN_P+8=4c(o*03Ejts!T1E5XGyWB#g>9+F2&3K)*fWkT$mee>v7 zE{nUC0_6)dwBjP2YwP^$kaD3dX$^tB2x~QV%YPnvz-MUetBGCGzj~~htu$mEcWq3n zh!+_B9ZO2!KnG6kl&XPL3T{OJ1%btu8bRl{p!;Ms60aRNN^s#-)hQq#JfB-NaK z)wktweao?LQB?q|j@y3NR*bBNUh&saR5gI~#n~Bg#^#hnK#Xw>@_dNaF!*l?C;eCg z{+RKq($IY;$Z0xMA!j;h^LHKsMYf4rxhasa`` z<1<9Rn7d_DL?g)o<70{6LztO`uha!^u^j)b?HWhwrKPooNkbFi#EAi0ac&A<0Nj7) z!f$_sSu0xSGp+pmC~Z+=Fx}%ZawSgE#NvtkQj3e3AD}Om84qPu70LETDx#k}c#lbH zBSv-r_gbI4LAYO)=XL~Kd6jDxS6x2xJ};fbcPwe=T5bn)xLw=FDbHo8s6r##eXbne zu_D&}`a|TfuF)m(a>v94rx-Q53z2(||9bmpBU7homqB==+or6LqFsGX+u28fpS+@h znU8?tK|jR-jFUpGi7fK>X*$pQN&}yFm!-yz`cY_x-4yvuFHcWg!$8mwE(!6qj+2s1 zL-YYXW+xz145@>Uk6-wd7>7%iet#xu8Dg`99MS5{WZlLP7)YTXT0SypY9Pnaw)@1{ zV!fI6%P}e6^Jyn^HtTpYfQo8%xbsMb3XLozU*GHBJpf>}eazJ#L_ga}o($u9xfwh5 zs}i@UoO&=efQu=OKLS}!JE33f)v!_SgRIFz!MFR1@VeB}-`3KP;{OXN8_1q8P!3Qz5qMJ8+$<=LhF*b)FnSm_ zf2n5Hzd2jDklFJAP<9_h*Lc)TWB!omwqVbs5tlRu@OdcwH*x|JVbbTxNx{79@zvX8AlD`Y6~dwLIZW>MZQJ$G zQKD$Kkb@vIABW+XJNAIpQ2+8Rc}$uTGY7{wYIBl=oYC)~$EFVOi5}P$DBL7>o4=$x z{L*+sS!ST69I6KV(`OV}D{%ylZE}H-%4__T2QM*rMC9KihfB;5yrxX-{8%^dbeHJa zE%(y3TT9GKTP{Q>H;$4b0(~j4Wc2(Fn%^ns5D95wrsViL&CS-Paw@DU(2*8JgE%T; zui-dOg9-{`>_O8B9=BQ|aaLk4*2d9;V&LErSed&&-!JDe{#D!g&|*H~ZaqP0?{#e6 z$0+*VBo5&8eB9TD%87<|1FxBQUD3EbKvF#+&5Y zeM=DK#aMxCEP`QYf_q~A)~2#FlDELi)jEUueAL-cDWY{F!5dORkw&PSF7Z-}#fsYt z4QI@2A#$;S2f{FNa7oP=Z~<9=J?ZE8J3K={*dM4`TBoNnt(B@s7tTLjWCThr`6>aw zuH+Q|052*^sC($sm{g8Y69V~a87bmP89T*w-XvOhy?6wgmBS>J{G=TU%C41i z^Rq!qR?GuW6;3`U*#}?q2ASgVx2Nm$^l*4XLX99#sKYxMbUTK2Rj>BDIawv2FjBJw zIQSZWDxykz;%zovBln@zKccROs?zkpu)(E@9W(@B!rv10@8B4?q`_@dDKoCaAWTbM z**9AX_i!deXUt82c9?ERcOB?BF&m2xS*5Ha7_nv*HMeb)N@pb%XJBASI>Q2;q0~s? zNN&(`CWs9$ed1r|auG3MaP`D?XgnN4(I|T9k=#@s+AnjDd8J_1K`w(_92Rujiw>*) z1Ua@uxFU<7U41e5FDbezg4 zL*lF?^=A#hNIi8a`6YM8`!1?rm^QCE{xUgQZ#;;~8vrZ!LIBsaX?^$bSRC+oXfm_x zTHadT&yJAJWU_a=nO-NqUJ?O+cPmo=h2<%Nt0(MmXKc*ckob~VdNEs@tN}7x0>%bV z3?+&|14n42U++T&YoP@tJA-yD-F2!~yJ~&PnjYt^vs&7_=@8c!y1grxnMF;MWZvKC zvzRIaj>2ZPw#&mc612qxA-&qLH6|5ri?IvZ@$q8C*nUhx?Lj0-j3*m`?3~gt^Yj(u z(v0bZ1S9gPAeWSOG`!;dLHM1t&zMd1a0u7V`>|t7eigGHk=?5f0}CC@Vf5gJAfYbQCBq$D zV3 zE@f-nGl>*Q%KUaU<5?Ip;q#^$>Vu26RA{pt#Zvsy}> zYcH4U-{I}t!GsH&8eYR^$bIMNskB+pNb z`|=d1XgmZpUQ~~Gw(jSeuR6^N(bx6I<~rp%92tdB3^{ z{b6)v&<`iWDn2EcFmW{$tvmA-6u!;<@jr409kCW0G9B}O1^?`N z-d&4ix)C-ERjvKwOb)B%P>7r%uc)n~fCQMF)Ux122tKh6ipgd}c6beOs}6Lf3O|*7e^d;OCf3v?HVFzYB!-;wsWqxJhjOQktm*W;SW4W$?zG(2ZS0$oA8|n;LukoMm^NNuhu$<9oei* zz*Jdy;w;yEH>#gXN!C8~oUG^k-s7;_8?NGv#9Z6&W+m>r8<(rb!@}+K%X+sr9}qhe zzHi0kAbJ^-Re0{j1j{>~e9k>cqtmg|aWieAOza#= zgOSv59HnF^Wl0MuYXAb9MyGcig5N0a&5}%C9|%mR@aY~sCpU zkP!kR%J2=y6;lo+KjVEz`OGaYh%WQh_Cji=_FrFIY61Gh85LB zfK1Ps&~RLffcJR!vS{Pkk6~`%8W)!$m?+Vq;0#dEEpD_sSmL4H3<)`g1Xu*1Ebep| zhyKvCMFMI3-4>cH<%3^@ye2p)@W3ATm*hG~CV}jCUJS9;OoI z&A>pVPz6SV&fgV#8$mZUJZ6cYPH`%KOMndHRj^|IfsG>FdLQGn6{kEdM zlzHTQx}u1xd=EF86vk17)L^l?o8?JT_Kw~uX@87-_FV@o{f-A8zvt9AVl23txZKya zTv2$Eu>JQ-Kfwh9rU-X7V08O&=8k|7V(Z=S^YT-P5@LMdrAT^jyG&y&oLp9{oGL>7 zUlag9{(MR~m@|{%P3nn!6d2i{Ygxq7hQG$R)!=c&;m72-T2IY2*iXQC>IP8N0?XdELGu6na$9DJPB%R)VrRsU0qyMDJlTBfHIMMLC*U*CWfKCDI379&~ z`hOgKzU8uU5<#St;m@5z8<5XQos(B0Uqn0v&fWSWdjGc9=ueOWO&Bm!sjk_$6iFg5 z)T`va4B5_^A|lqQTG1UaLr*5lJ=w|N%s^i)dzSJ&$&ufb0%tCWY1Eoz{Yxp+=Wdyk z_^15hm#`9`oRcCzbyRVI-yG1{k^IZq5RL_vYXOSSG2NuX-qeHf-!hqi1ZcFWZC%L^ zj83IiE@aT>2W`#f(b9@2hfKJsplQ2nC>JEEa%mA<(u0>kP%z#b6ffHb z?yx{Eh(VNOHDw(5<((@GHk|3KlzW81Bl&%^maqn+R%vZFG2L8Sof@JhT39RcQ!lln zWrl#2oFMbzA5~1AH7JoTE;tTz;=yJ9cIh){P_!(zElXX>lU8E4lK7<|)#VbZt z*(3vGf>n0E7rN}h>i43IwFrf9k?3r#;BB-OJr@0|O7x4r)@VN@6vNQ(v#44Uot^~O zSHwHS`6E}8LI0P9|K3_L@Z1%XnG92%)sQd24T431*xpll>Sw<6XGCA;R_YDL_PjEc zvJ_&JiVRtuE>yIx?GEQn{J;T-2Pvj3sFi(CJ+c&HJ3eM=bZ??byNrv>isNpJ%&&-J zPH9M#S-UiaW=L6;W=1?cZ1&iK~bAtxzg zJvgt4WCG<3q%c$mxfiAg7-T`rI;qzKlOvwa2$(he8I+gt!FX*eXj>TiqTRB+rNEM< z9mV!!j!S)Au4kd8?9$!i{XL{_GcZRdIW31I=>ZGA1$NjmTBSA!%q9ozuHCMOM@` zP=xr(;@9bw!O;Ef#siHwsJ|3NxU7=%fS6}|!n!hv)?4Wnw1qfHyoq&0r^DQM+g+Xh zF|0;hUhV+ypk*fq-2mgz!-{nQJ@jmRz&153c5=`wR4KBSRKdm|JS8BdyE#ECWr8#P zvjLMf&BFdG2i7@QapYe)c%CWEd9_v%)bI_+ty#D{i{WrT2nz1+zTWmYc2N@a=Ppeu zEU=t!Lo7@)p|GG8_LJ<2`R6d)>T{QkIy9G{{)i|uH1{a}?s!m^-3N?`WI~o9c8Vx;re# z7hVuXW}%XOWUwwFT}Z1XPt+glo=OHKSCp}zK>k5ps>ftMRUWrNZd%Oqo;h{y+vH>> zQu^6)D}+){bDeG(E&@yu46rYe+sx{?j@aSffI*Rk7ZM4PrVa(MS+4W7v`j#dUEjYJ z`~ej@VpJXyB`KW+wQ=srnLV1m!6+gpYw!t@>^1UJa>?H-z7vd=gb5oul&04Nn3E~q z%m26@IB}cO(m4qJXuS@V@Bfkq*$eo|7bZH_o*-la^O=WHhesXe$a3#WHI&6rRGISF z)bDA8TY=Z&9=lRy(%fMqJd*139Ds=(Z6*3no_sH7X;$*@!=g4PL_9krCb zloP*Y*w4+A*xB^?wE@;LIA%2P@qrXK@ zLmKpYN>oy!2^k^S^^)t#{kI04lx$c1(RO!jK?Y*kZ}{KG}2Rt>T`CYvW2+%=pgh&inzG_Ea2 zEs=;UW(P<1%=YjycbbtctUICy`lLrClL&50R%$G%YGLJ+MuQM-LNtFRZlqtIkiY9q zRu0ksaFl+I1Fmg4S~}jYQrrZ-TQSTNG^qp}`tU*;-8{~gUAj~L$o+4ICgeh>9ipcx zlq>fXRi(zPQv5ZyTrMnvwhRe>Jb0wPbQp31@GX0CJT{@%l~~wYd}lepD! z`Qll2#sGVkwesqH9tJRV_5P>)k3sX~$Vy>zF>4ui(DGR=E|&n?6@VK~+MKiG_j`4t{{ERSF=p{sJq^Vy|6K8p0$S;Aq}v#t$998CdK zP503Ml3fnk-SnN)u_E`GBmEe@6d{Ay=6p4KQ*HAMN~*$jJX1?;Nszt_{ScYM&A$}I zb|{u(k6^5JK8IMXjpHX?-Q%YrFG%k5HII`2>ojjC->xgpT``=l*7Y@iX%3DlUnQK< zI`v`O)OkquP1sEL{zn06?ZzMkgoSS7*9f5B$FF&=!#ryxvY zY;XQjkLfG{kkWZ8{dyT+6rdMxLgHDaGWJ8iMcmEm4@76_j}fG6YfJM&uwZ0#UW;ur zS;cgjgh`6zKl0D{*VpNNefn?3NyuYI)MYj_ze|;U|7fSiG_wrLIVA}6DElinQUv#B z4osEKAMO-t+q{$u)rJAzosZjfOX`iHhP=7>faJUvwN|T;(_zq&lAnWn!@XXvO=Rzi zg?Ta>BH39tExMRpfz+{a1x}86~nFz zMFj;>%Q){jZ0)co=kN93g&OCU#k4E>Y@TEqSGEcFLk@lvYDX)Asgk@V|5l(>Un-wd z9T2d@x(S3Mt3U)V&p-H2k_D6yU7L_J$4}gQvgUHq{|s!JH{MQ;uvlxDl>JUnHTcLs zKeZUmYSE)_XX1#RRM6`-UtX2Z`b*j&34xCX(4Y=b1O(x*cbb(u&wwonON&_El*e6Y z*N+AXLDs+sd~F^fmnYX%6YJ`~6+6WoCNq*MH9zRgH)mY-UWz}fY~Za@dR32+BL$;$ zT{D`Bc0`+N5NlVG0;$BBrv5(dPwbmm8+&VBETn#LMXW$)DHr>{Ojj)1bvSBd{JKb~E?uBC>l^Ytfioch$+#&^2HRq|<12j%c0G&7?}o(z7Oz>=~;Mw$9<6V&pFEfq-t9H z##88zg^IO}#~<(q&U@V>7ChTF2Nseqak*G{T<;e&5}RBe`5!M<+_ty-0(Dw!ea8MQ ziMj;3+38p@j|DakW zx+z&<8|*l{FYDmIE(xIg@z!w>IqxbpaFvKJ(>Idl-e2i2z-?RG=ZM?5Qr5s-*JX z;>>NO$Suy?l9lKsJ9{~yO)%hmEa|8o^iO0|Gg(R!!}<$oXq63 z##-^)K0cg0LnIW?Wk?NH@ZZ_lG^!61OB#h+QNN<791jucPz_j2tOf3MKqW&2CHmhz zV&ZYOLaE|yDr1Pk{d&9ArtoF_JAxDG@vgpZ0hP))xf+SW zW?z${Qc9ijyM9Lq2o|(MZlL-m-o_pM2-6c0p(;P0Aiyh6;Mx0QJD-%FKb@9gLn7*S zLBqgzyEp!JdP@lWRvc3g+=j*D{2xaPQn@9&^23*o^?!fWY?ESq5=OlUS1r7%CCDPw zLoZ+?Ty6py2MPJ=pahSII8$&pGE2RW_rd3j2dN&SNr>ZGZP3%LY%zBd(0Zslq}Aa{ zZxBRWuX`NRbdhixjDk``X#jkcB%o+kktWVYrWk^*NY~bOt$ET_eRxMLcH?(!N$EH+ zVeu&Ku;|3O{@1!170(QgYS!i^C=UWTT;$-~jJ=^_!#BbJh-mr0|IB2$`nQ!!`a1j> zxph0OT>OEt1Ym@RAb58a94<&||Buxx?!wh>B;%B6*u8XgVsOq>HSP(03dyfHcn}yb zuiFJnXk>13KS|_p3VWA`i4-cEM4n1 zI+1b7)d^Nx{{;ulwOjsGJ!JnQt2->yMN2Tl_5q9`)JaVb(wm88B`n z1qvRvO+w68v{v{jlM1do+~mpRoBgo9?cNxi(<{-QrM>Od{cPN_H)TV$)by;;;Cqwd z-L>Wcura-)xDYF#x^}4oETkhhK*hp>*D@&Z@*5q^`TV2|Lk8=qDlc{ z!6wwsoit19d25FHEM1c+oK-DXuFoR&2Oo{*WJa>!gyVB+DdiF@8I2Zsk@LEc1|aJs zlGVna9VnOdTDNpON;lS+dm9S9+Na`idt=Q7?twK@LJMEzt9f*acv^!dQZ~-Gd**c- z9#1SW73*}$VufVm1x$)yA9RwdsNWRrPusFCIGKv9@r>q}zbK{kt5ZI=`^?igp}P?( zww$34+O}-%RP6tWSb(=xdeFdw6`nRzdCT3(p3(^x@t8gdh=CLf%2SI&kw@n%khn@l z5+6?xUl~sp?S#K%PFk@Vyv=yL_oT=~@8|*P(fW<(fsw_wJl&Zg!--GsEFvI6%>q8i zQ7alIaw)x-TlgrHgyEtGoNVgYt-`?=BXi}dgsACJ;u-g+=}N#!n1vA*bbt_$%-?E9 z`28o8TX7XExnUVgZGQekBf)fMG1<5&|_bE79gK@$!*i?xXsgZ&tyLc?C9|-d1oguDAaCi8h+76|tVbA#YRb7VIBR5fbD52-^Hc^Kn z=k0bGJf|aiVj`SSz`8c`3t7K%a>s&d7qTtco5G`44aD<*Y&xyRp}5XXB!oaX^Yw!7 zooY9lO;+&f@9;>Zh3h!?iE*{uwkSqhU6eeMebjOXfua1NvPCA(h_@ML5T2}Q{6(qC znLVUeBvAUnIOsTZ>>f?+`IjR1a4yNro_dO#-)2nZWb}Vy`s+1l$mx8UdDH1KZ;+E8 zY3^?;uWu)=qW0|YS#!CfwTgO7PHC9I=!@{mCxs_IUim7LF^b8$m?y8W=baqt+OQbzxMuTj}8x5vhF|en(YE%S*m*MN~ zi}Q|r<0dr%Cameec%zBqQpLjV1e_jIDFtQti*O5)Xilp|0^F4;`^TWR#gauMQcd-r z>$!sU_^_<`?vaE%2Di1Tr<}o?{t-mE?FO-bKU({|v*j8S&D-#4&{huW{~t%^7#&I1 zMd3IT+nlIl+sPYDl8H01ZQHhO+qNdQZQFLfp0EGc>Z(<%?m2g#y`S4M!jHP$g}2B!cl;N0>L@ zRne4khDgL<@E?B|CcOYlL+JC$%-n%E#gL9Byf)xbjSUdWYgM;8m&|Wa+z#f^Af&$WCMM*c+(QUWG|Zd zNv^rHZyzl+RmKKivhiFg3f4aSYJ0T?<4M{YRzlJ4|K4$Lna1_s?%RrLVnqQB(nq`8 z=_%E6v;@}@Z>r)}5u^yDm#8h5bU!bQjJ^>y+M~I&r`bhfQJL8twrvRn?4X*?a2Pl` zI&u}}PczUisc#bQMjY@$`L6&nb~b$7FQ#EB^(vrIvxsw<9vrI*9f6MT$oNUc=*RS} z0(laOwh_UMiCeh^h)Evllay^4O+Kma<-n?sDeba`8xu>o{$FpZXTd{g2-mG=m1P(P z9!)W8p=`+~0NS-)yH<5$V1vG)-z)N6L;UuqnMAw#d8*T;M>##nzrJ&;p%!fK7rrcQ zJ+vnDHj*~585}tP=flZA?x5Qo@M9+G7eu--D zWUiV0<&z*x*6^uz{d)+AkmgzXZ$L-S>5q)#qLVU*(lpSzDK+R3&ssf#^0(KbttHfv zNvd-x{;B*HcG*K0Jnn=qmT&crENodW8cNi_~(E?VOozDFYqtqYH6(L3tj zJ$0J=RPEo^=ar#ZTV{dUM!$gxy!`=++BK}R3+kv0d22atgYV{tQ2ltckarsTXqT+v zJKa~Z{dS$nem<)zJ{bO)RsIhYk{tSsjioGZ8*@S=7rlVl<*CMYuo@d9Pfv(E%#t20Sf)*L^w^=Us*X)uU)4JN zKV)nLxv~Y8pltVp#PTLTKd|xh$JRf~Ls_B>`Q+Au+mpbV63YnkdG=`GOM|inW2+{8 zsS-_bK`K#dt|gu&q>#~yeCT#`1qP(d!LT&I`mGp!KRo8jFFNCb+k-fp%Gz|?N<8jb zq)9!hJwXULFbbFGmI5|IsEsI^Fy8ruTY4MjQ|w@oqzAR_PY36n@OBIs!u-WfQZc!{Y3f41db&y6lYX%Oo!i?S*N$=99Pg>_cceuZnmxU*#*3*D^6N;GPqe z|FsV|3+GPZrib>cp8Qg;mF`u3W+R98gZiT_H~xvCh#Qsh9!qyZ1t=1`{D^^e_Oy%U zsqk}F{cdxaB3jPO{MtC!CwX3H6Oj@lXP1-ALt$b1-La?={Yr_=UzL>nSQW{mM*in= z6Lg4?q6Z3n9vg|Lt^Qnr&%KJm3`Vea9s~A82-kjaP}xL9AB>kn-B|w-Av8o;ji@X< z`H59?x|BKiHegxfx?#(qU0xsfYnqrW4byIwehv~6!4GG71xs^GIj=4lh7HwHp@)+n z^Qq!u|5vc9Z>-Bzy!i*h#&)@qiuhyUT)sftAdi(7y0U2_J6Gg!7cm_%QEcf9B zUG$QS&#I1E`;2qU+CApl7_SKmV4KS@j*N?NH+Tah61PDm%t>j9O^zY2qw8!XmeCcQqN(7Zje>RUZxc#ET2Y~UIu(#%D- zwa*=wktrZfM)DrJ$=s%$$nO+SJK)h6bg+NWB!gN@AxoZpsjE;kI9lZLZ)*__@O!o$ z_Pgyxt<=_ELDJo=D==v7LiTp|VE*+rIg@&3+;GZzJ|6M-;yr(&WUX2;@%uL-A|fj_ zc>xY9?yV;ue)3!Qnlu6pdOZuvA!RVmAPD)@m00OBxptmT;GusTG^7xGQDoeKFlu3XqjMPZVtyuYrM$ zIq3TIcQdKM@LWi{w2qa8-du@VI~;b)Ipr+s6VYfo;Ny^wyOjOoaX}4Sl#{We%$;m& zIg((Fjnn0FdPTt7P6W<_L(-)U!owHM=ab(pPWk@cF?Y?GCGs3c-%l5~B-ymrh!uw= z{SlLSbjYe&H@7QJoj%MpEf|iuKkn@Xk<=*C97B=A;E9T*7O|sAm(2E%#W6+=?^Z`q zYpgEgL;ApQlg4A&Yx4KBbPc3|Ra>B-ZI%t&XO~S^nJO}*ABuZ04U=OM{kq*y9_D`a zYofCT{Kt;+YxB(XobS}`TS@k(pfykTO<|7IKi*BJNtuf;t3pN4VSQNG=olU=3ZuFmT`D|EP+Wv6Ne+0E- z79q><4_X`R5xBtpykNjcE6&Lz1IJwdWxU_#KunLNsi}04!Xe*=|FTRMz%5+!Yn6wWzx~+n-0_EdrCgprf5n-novdf>v6`ADXbC@%C07ww4UJk< zJ66j{S`S}MD{Aj$&iJ@5q_INgp6uL*V~e{x2o{!{#}r@;ZZ4ozkYcpAdP||Z;2LLl z;dzXs=7;+x?zirlPdHwp_|snHsZg`&yu64T4Aj?0cle^*NfQv?-k0o;R~t;Gub{m{ znYQW-hKr>$GD{^EL>siN7JgSdQ5>4gLvonYSD+0f=wlBnqM;pUxo86ob3;&4;}C`3 z9O`lL)fuX_fS=WE8tIoWVf&0D54r;I^)-MVyU!otxCMO9FSgq|JGjf!&o9_!33H;M z^ufH28x9wLQ!4sB&ku@*kQ5xUWF$$nX_tTK$o=NGygB1S04zExcs3n1h88T?-Br~Z z*thnvXVpi`yBP!oP9)wO+3B`vU9@Z^Vt#>62w03Jn~Kv9vv*S8Qn=G~R{)g%>A;=P z97}6W;Dh>?TOL>0si`Y%4##d996lVUP+_I=PkNX<-9i0_#lo3D=XLjZO?&uE;nvsJs6Jm3 z1^FaJL0US?<${ieXs?q>XGRdR_3}_GIfN1f*R7PgjY^0#1jeukAJNp)`6qOWlrhYjQikIUZ8X~ zvH_<6(|JwHh5sIJ@DiK2M6uCyJ~s%#nO7i&*-o4w%(#AycItTQEm>_l!)Cms!KGz? zn*Q7cPgW&+neB!rJLP9Pd+e}TiFFq32u=&xx@q%T$v=ID7Nd4JNx|xx)Ps@h7lj6g z&A>h*7n8L@0F{G27*%>50&^XbMPU<1@*>QcvQD)IzU+#oIz~|jUg0_?5OP7^a}mEU z9t~fk#lg+Z&d8}$xunIlN+5CGd|}v{9>2oV#Bm;ap|Ab%kB)Ri$Cc}XTf3Dqd#!lm>*tMH3RgJiO%^Ty(Zt>Zr zPNDS}<+!jMb>)b6`O)kaH*ZcKX1Bh6(Pw)a+n8?wmo0rCz8Lvw5i=;rZ78(C)LJxNNP24EjP20IgT%+o))}zDm zIEUrz{%2n1+mkxt`Ia(d%WC!ybJ8b8>*V~vIz=1Y$?1q#Kl8NToA+beTTUyZ#?azW zV(yv7n$lbeTnXQQ!U}`|d8ac!_Y%x}6%I1mY_cKO&K-??vShX=UD@4EB_G-W@l7rZ zOuElZsb?Qzxv!c#wed&&ta(0Aq`C7L3cDIb0M2F>HR5EcJU3?V#(0{JPm9S)|-Y!$O?m+ zYnSTF&AJ(PnYtDJya*a8(u$8)t~C}nG!SZ$fJjHp&97T`^aoGCUYLM=RSWAb;jBM- zc9$A2#qECUf_zs~)5f;0p1DJE*ITOOy+9rurcja6l2m9fRX zq$P=yrHz8TLE2;4$&kVCu))ItkM{KMQnk$LET65V5JK-j)e6mP!7hdIM?>ZFK`lQeorUbSmX3?y9>v_kgG3>*r zv82*uc@`<@9VKVe>yt1o4>68PjgGn=U6Q=4z07uytOrq7!HuD^z+N*fIag0I*Quc_ z=S)R~zcz?O@#F-LpZLeDdk?Z&dVbrYefKhacyRb5mBncP<_jNgNL-5r1qJKPR(SX) zfN|U|K+gEggXFK64OS~rpR;l?>rVZ#hn%X8+Lm8DE4iR%`0=CNKyaEkk(cB6(|rs9 zk<@YEvxvioaRqS*H`VA0b4B$%_UU+K(%7y!Nq5_>A;l>7hK85z^zWjr^O6&nV4bDY z@%=s$uNr)I$!rbtKx;edQq};m%v*)7G<e{zbH{UZ@Pc`82K78nzmB zQEa-HTT2CjJw~+|a7+EOT zto-Lz0{7|6S}WkYcb#)Zvg0){Fj&@fKp^;4Hq(E(g?UHSa`%V!G^~ioJ_Noy@*!Sy z=$Y2(lh7q@A3n0{#-eGgxhkW_XPVE@UQs-EmF?vXzY&dVVW@mkJp|=K;NZgD_Tk0! z0=i$1lt6qYtDQ)8xarizo~zJvCMwa&;^Czu*B5%tRzHic>H-w zfGZPk#3pY_mi=&sriyAv750Q2lZ)M(@-!JU$8n~$VJ)SdfO9fzO~Q+tN++Insjcu{ z+K^Mh(Y|B`x1$j6TH*|85?1N#>pe?_TYYC3>@fZQzXbB~w|`&zlm`$n6LEmXj)#Vp z5Xg#zh`4u2(hA6c-EejfCg;mWcn<3`ZOh7kz1~-P2vWX(q- z<*U*n)?DUf!c?_3sAt4xA0U+233nMEET9ZB2S4NUDRa5%wyT%S?62*?H#euj^a<1B zYwv4$-o9dKaW?8zDVC?BKX(mh@B$?D+j>z`@z%`+%!t-Jvgk&tpZV!X@e$?i5Bkg=^z*)mlJ`3Imx3u%1)4 z2HY!8fn8G>D0KERrS(+9u$!?10Dw6B-#xO!u&dTk?=+&Hq}KL!iZx?ks+QGmJ177` z`N}rUYGW2R^S8utifrmZNJjMl=>i8!vI;w@)Xl+9z&mbIZ`G2iH~;dGBnVKHlQh>8b;K8L*`f6@@d>y5TLP1uSe1s@$* zx8ls(tZdD6kCs*L_zdB`x1M&G?(v+N> z_NTEbykPdK)5(Nger1=XlB{ud&d0B?J-ORBzdep?Y@fn*=i)}=$g1L&3G{xO>l*8J z;taf8uAxT7>E>x)n^EzO42=a~nA^M8V`>hVw^7Q2%xp}=Tc&jt_AQJ@=pCyM)Io?6 zS2f)`z(wc|eUAst zX*gwsyJ3U;C1`IxGJ3q>q*f43G9Oo^jEYkc6g1hq8yr0gP>UZh*bl`r5Z0v`bSFpC z(!>ef0=qPw7XNszm8Tel{G>C*)*j=i!H4WHnA9J|Un6kYusbbEu}7ypo_WDM&*8Mw z?+->ejhvR54GtSH&M49^tEq{Z9n$`9>QBkdJPy}Faby*jF)Iman4;?Tpv z{lM6@9tu;~MGreeR24&`Xt$R`f7YWfdX25MX=a5!PobdEyX^^imb8an0ZtHt_d_wi zN>?ma!S_RuAAK76#p1sMOMa7@k2FKS7Kkc7`@f$Ik zSPtY!S9e3I9r`Hye8YAPwd{&Whe86)TXI+(((Nwj9RzU_^6w@3&3`P|>!R`4A8fWd z?cCcX6k#rCFtHlEbhir>6VNm`tjJn)oQJ9y|D#aR_0~aSnfgRZe!j}U$Hg6j##qAT z;+TR;tIBAx+-}Zeu&O-%>YCSioAP9*hTvO#|J9@Ei3DDX`%B@^u6!%gKkHxm&)IP zkzFi0CN<9Biz`I+tv>{*A0|+_Uha&EkjEMRtXKZKU*Mx3c!_#z#Kn)J0h;yW4oj2o zuTRU@e_*WXkBRA8QsJvo8c;Z<*{e2uypN4z{%0V^EIp}=3U*Kr{vuuWCw*t!m%C;J z5CPGbxdI06n?7TE1n{{7{GA{oJqHEDgh6+KW193?dD+zGv8z8SH(v*_|^YDMl{Wbv;3IW zz=Ti)dNW|Ic}DRf@q>}^!mM}>qZ{P1oioS=<2f%WCWSU$9|B&NuwOlp_G5oG%wq3Ta{=9}9fDr7k%w zYYj*1GFHN$=JmsuIKDq!%8_*6r2%irZQ6sd6QS?nUx?y!2*?|(Pw9gHdAz>;#jZXz zpY!GDJm7bO0fm%?CVKY1{rv^x8vcIXfMe*4C9rek*mOh_Ifi6#1*O?=nC62MK3YXIb{ENmb<~78ZM;bB^)*#zwv#XeXN|LYq-JA!FM>I z0cvB$V`veZt1r>(z$J+^q=4$oI5B#!jNT+{dfb_Vm30B|OKjm9_2urJF6$uzWJd&j z0f`u&K|oHVA2i`eaTbv66U#)5P%rcYnzx@dLb!5{A_&Uw`I&H}Lq-_K; zQ9X~JyPvzRy`MW~aJ#*qf#8W&FH~^yU zd1(F{&}JR>YH4U0a8UP~ztDName9(#9zD+Ud)aK;dDQ9EJVhsG}Ud_HTwA!m0fSN_36(T7ImZP$FB zU3^jsWu~}n2)$qQj)wlt5oLZXMEwQQpSu3R-VikOEs0)7#8tm~zMsF1SCGpDUK0fL zf&_q|08F+Mj2hRVuX^+n6BiR4-L{0(cd?ZjACp_6JTPngyJ;3C?D#Q0B0hC}<9wif z#*9LFCSASH1DR3PU&p}Ea3E`w*J3xMsuU9MW<7k@)`Ui+;Y0J$ot2xr$?IIV({3M# zvz*jq_Zlop@je&mc@19fLF_6ii3LoJx@t2wQn74Oq2(^#W595g=oZh>b4SwycKUiR@a<3v1vUYv^CJ9fVtwnWxy$3( zJ|Fw_Un0Jee-9y>iW47haB&coL^m3r%m3?kyys(_HpH-(th)0~vJfQ@2N6xIPvu`e zmLvpOen;>EMHIRXQhA^RpObqsLg!=e-xnPjq7?ysX+ZRj9SG$X93+PfHA?>96cDk{ z?H~UT%+a|E@ZJL=jF8#rDeI8~^p@e182F#trbTC8fqy481U{_aW6QIlE%4qTF#g9_ zyq5k20YAD%Fy5HpJgpbi{*oLET1^EM01-CXP4xcf9_uljUPP?p=yk~bq z=+?@-X=LuS=#jku$LM0Go(&@H@oQO2@H|F(GTAjO@ShQh+>bNfH6Umx%Wty(HwoFz zMo($0@m;@kMS`4bj4W@1S^X?Z)Tz@A;X>iewC-|U@n;t#=h6_uIWn~=O@zqp!+G}8 z9#)sFbPtTrPx_RsbmOA3+`jKDT~G5&S3+1uMWqItRyk_axGWg^&!ToJA=QXqkM9xX6#7 z-}3|l41?%)u=XS1gJU9hhMg?Zz|sm6n#1k$}kfPV0_ zcd6QZF-F&aRJs_Dp|EvdIFn89mY!XxGznGC_nc7UL~ z8aSIOqvvHyyyu2B=s~g#(TgjaaL5;JMt>B1JIX0C9meAplY$^7ZqjUd!ROo=80`<^ zGijj>e*GF}B2b}D55=F?G~^dA14#!ubekS!%ztT#gR{y(`1^Ovvk^xK0xI&R*o9C; zv<&9(d0JYr!?{(9EIv_(flpqn6Orf4W}VfExQ?wF=-K>;A&?Z57ZxvLa<&?&LLG(h zBz~OLGI}dJq%y@Nwr6vrfub4N1EUvkLEUGh{dJeJztaAClz}28r=|-(a7A1s$P=r# z!cQ7fCWxqt(+ujgbC4T!6MUHI(A}2|lO@FIXOW-Id6~|E{DU51iXjzk9T-90MywEw z-_5UOa|RuBTddHAl=d9g*a8C&2xaX2$0DXMu&|G(Yrs-I>OjqyfHW$Bfd5+>dhM6C zTWC;ngK>hTW7vS&6DcyVLR`Se1r@U#$(hXrr;2DdnF;#?;o!PL=aX7~?k4a1Y5NP$ z5eEtP7y*E*fc76oNd~a&#lTR7V`z9#cZw?=X;If%@x*}TdDCs{0cj{2pMXf+wq(g*4F#2~5yPU;}<;2gl@6q(awsy|ZqdfgEH`u*_A8!Vox^0KGF&2U9wZ&9 zj@@H{_ZZ$0)PVspO4R)f4rwmkY#o|KZjRQ`GMoffjVdl_#gf2`%EBZ23M*B{$U@Ol zk>K>558@q!n1(z23Gt0XwKP2`Cp3QH@8js-zXAj6>y4hqJ=mwXE2jx^f`%R=)O9%q zrPchAT$5NWuNF26m_RJm6nw~oR4oDPLQf`a26iRIWPOP-t}(mEpQlSR8sTu3q=xzm z3H}Qv{LewlollR8#W)D<41^?TKkzfeFe#(8E$JnZRmdDtPxR98kW)0PcceOHF0zgS z6i5z!QDJFwjs9b<3q0f@(V2hbnaD7B(<=hzrkD^ZB4x#XOx`-MZ7gISW>d+bi%yD} z8HamT$K|n!@QFLGrACQN{jQ5Y8^0v7@IH#zcDn)LQpX~|ks5rsI|-9K6o2!GVDP$C z^iY_R{(d70LBgkhu+GPw+5LQ9sv>wTOXkdY)B<<8=3;Eo_-KF@`#oUk;3DTfzg zw+uwqHAFpX%k)&j+!{|G?dBf7`EGco5KJyq@w!e~pX^wuYN}X1b#iB){1q@08M_Ax zK9>*_=q9M^N4VnaS4(R7pY8|#v>rS=VjniCGa&@BB`7bbJ0dYLnSt6bW5{t&CLK%+$t+pd zVq;tL5+(V&C^j-S{M#{?_1Ycj;FL{(lSP9_7g$II0ZK*VKpN& zIBw$XMh{6Oe`Y&n^rui$P#tjxG0D|*Y=K8>z-*-0+tuK0_q$d(69h6PVXeSME0oZ* z26`m2MTVhAnrhLPi0Orsz(eAwXBUu`U5sIt+%);giKtD{zVD*f#QoVm0TJ0& zSx42z5?Z(DG<6-(@~t|}!G5S^@|2r-j5U|z z-_Sx9_O%p5fZ6%Us3pdGqL_KKYCk5|fxKr0@(Pc~AkMta`>Vf7qJJ3MfL9HSp z1VMh&5Nqg5qsq?Tt3$|aKex{y<52A}gu{=@dKtBGD>U-g zGvjM7l{UEJ3Kw)g-fA|%o!!j)nNA;^w^aVX+T^W@bLqWJQ`2Fr(nN(3=Q7H3VH)YZ z*gjyrZjOl?BD9`1xf?Gz?Z&#Lq=B5EJ(IEw`YPi}z|pAK<*Lt;E%Peb|51hmpJtfu z`}BBPR{Dak-lbC=NBnyIQp|<0!^XVN=VE-)B&XvdZo{7HNuTc~tMB=6-v$4peEG5h zB95W?zc0Z~A`oP7J1wTMrRWpUEIAanSa5|fCN+S!Lxg>TB01uN%1nWWVpN+t00|^C zlL|*rfGc!V2VGv?8(Y)?bdW;Xbl;p}%UKLkUZurKr?n@q(iaDa~!8 zCcsiva?f97+JW;GtD0*R1^E?J_#%eKd_u^I z$orONFoGXX8H6MJA|p7te!m1j!qER5aA3z^^vsfGcpFRp3}uei*d6TUZ^;k;_z@AJ zJ|H-FiBSx1n45ULimZyhmEp5mk(=h%;L=5zgI7gFecYRTDq(^IMRpVkYredUJMn-W zDucht(Lr4H;c#pWprC-WTC?w^1H^wkA*ABFMZJ=>Nx8Il_jA{%?2DLu`!}CB{b?l; z1i(1T>63GvXc|Cr0#K^Vo@*T~b_u1)V3JTJa8EW~BW@^77&J4peK~L-oqeS!jrF8# zky62F!9Kw&raQ35Fmi8Hwg%^?hR*bAjA$=FR|Zayu66^^p_ni!-}I1t0fzBz>VD4( z=>1Q*G@(U+98XlB30>_%nZ@E@ZN=9RCq&4Po`P}*@RaNNgLO!Tlh+=hQ&|uBhlG*D z7zjp5XAM)0@gpDen%PJ#X)8jz^TWe{5DPo|r4P?hn=qW0?b)rz0fG#E8Q)9q>>-1; znz{yU%21T0)G@>Wc_~w{tWogg)d*$K@ig=< z%WJ!g{$4o;eu;sMsv*_eOrWZCQrw57O>?!&wA-v?q!AtKIMhRz%D7%O?tn_^Bd^)U zrfdbL8BUdiU)nWb+IZ>iBDEc}SF&NuD{6i`5a#W%_}>m)*d0ONd(PKP&mNRutoOIq z@L{s5In9|9fewRk`c^R$UxO!u?tO(7?}S4as%7^f%96UecOv1Hn5_VO0{JrBUgej} zFpl@b#J2huU;KKxvM49MQxgeRr!b1moRuHDFI=c)H9D*|JLV)%&vCGFsbv-a2$B%I&{?THruthjG% zQOmiWo!V{D>dOt}pMdx&iEm7Du9(pDeoY|2vD|U@a%f+b`QUz;)D&0CytmjvvpLu> znYP7yTrU;XL6l6h=vFlIBh54!vDnmw z-k8{rVkG$IbBNTSl31%{^t#_lA_A)^8aTVO;;S_z<>-lGHnY|L99v1R!V$brKWMY& zC5ZIbLJ27!5SRTCEl2eIGK>>}T~oT8;Q6Tup4YkLvm%&%mdH(tu5}Fcu03VZ;_aLz+XXM!}SQX$g#N!Y84@?Erozt7mb3Y0n zn<+SnnovR3i!mYI)lGE5NsaaP*z#nIS*zhU+WQ;fY;4$QO4W7iWcD=Tv`(&(xgB+L zXyZLA(guf@cb*_R4DL-bYy6@OS zy}bN8bw-m<6=npaGWT!*Ub<--1m)wlcKZL=GT#VPE@D*PyF8ym8K|0IGXWUU*WK`&W_LW$<;Lil03i(nt!yWe|wvu2P95D@P zjAGgN5GhrWZkpw9L3(~35#L z>hOn(Az}m2hbm~E!k0vRdzx!JO8TDvX{hrP?99 zvGg@p(>#G_rDE_ex(o9SUHFYP$nj~q-y@GuPF2WTx=;?D+;#vd=wklK&bpPkOxsod zL;4!W-<1)3q^nFl6Em}{B+UnN4Ak@yVbDpalSCGufZZW2af1vm*up;2rY0?(4N-4o z_}y`uwnXsuBli)9kd@-LQUOVRF+$|vdw-jghW?AZ?j`eXAXE+L5RkDZYwIP)KKKS|DK=)O zmX(1C!`UtOcpnhjyIdi#U`wSO^b99h2r^&~x@prnzw%M~i9I->1Y_(4oui!(+7j1H)DIMz!v+Y{)4*XA-WW6umMxR0mh!RqnU@2F%}PxW2J zDq(VnewOrZoyPcIOS8yxmMC;qsrKF-T zm{zMDr#bRQoh3!9osy4dkqRBV7Tq-=b$xa73}Dn@G2~;yiWi1;;1M+zSfo#Cz9Ae+ zxAXNj%6hghUN!n=Kn>ejIpz!-TbJ)C7BjHJR>srLaSfuyYY&?KhHtrGZ{Dc6aldD> zQRlj$d(eqqec5~UuBUtN$&7+|MFjlZ+znJ6mHfKq|9k4F&;mG8qF6$bIIwt^asE~< zZxCSG$wo=S`x2~-F*A~NT*1rqWdLftx%5@#L?KiDZ4&ZTs&Mc@}G1~6DMi3WRm&gT~QFxr;5PufR*q4 z&1-H~ThzEbcW1{Xj;DR=_Bd-=-s2}qWR#{5Zl~K7v89|%CypqAD={4Pbet)!jGoL5 zemjbz-BXtbpiFG;<#)N!AGfNTIB>>Jg3%p~C#A#xSM04XnoJ>k=JI{*H6Bk^Qq#vw z$C5y}vvCrEbe(Koiw4`VwRY*4o~N+4Teg}fr3Ii7yQDWP!A92u-~LdvKe(BVaq-Xw z9#krt%hl!LFJINybyFPi51uZi8h@L%9Hl$drC0x6jA!lL z=XUE>@vOu{nP{h z3g1iJ2BD&|Qq=*_1#Qz*A|Z$9`F9J<@UnUZvi7wmb9@2MoPcaZwnVNCE5v3Lopb=z z{=VA&rMi{9J zvbS>0?_H@h?IV2)>YJOI<39aMv z92Wpxo=vb-x}Uu1m^I)**S*vPI_C)8{Ea!~-gz*Ch#sSt{+FA|+AJ<8RG*cEtyztI zAzJ{9xd>z@dqBD~B~xnlIG=_?Hy$RhbWB*Cq<|YyA9igp245QrcCH26!o!o+TrT)q zMx}feX1N<85Iao0L~EPpkpL*J<3EkXKfshNmmkI_)>{+hSl5c=Y1Ee2lc)?g_{W~7 zB+ehxII;NfmbpGQYEhV~4GX3t>WF# zVZv}?%!=kHaBIXlK?5;L9A%Wb6EUAFF}Ha@DQ_i!%z{7(vuV1pQ-t`1hZX zL)Rra-fwlogaK=MAvxb4$4pdEd^1C#F^U@ntdX$tE_lzj{I7hVpIs(rbpY8cey=Z^ z@2`EL?}HLDv(z6XyTbT<^~+?6rmlIFLlzzj4O38lBBO-2-84y1#j7er+YW3J)mHgc z##f3QwhleUAf{5!Jl!P|J%dbr@#xV6`Pea6?fn9F!?R6O^2P)QnI3hIK~B=E(fl^uU1{a!;TY*IW8ESsoT8D8#j1-E0AcUNB z0nvq6NG~|1o>)zM>+$`*jl==Y?tTy_E(k)5YHw&cExC2a;d9SXSlY|_@c9a)Sv`gL zyo9id?)92xLJ)#U*%mN*MK-@4w zl!$?7ys0l9V)p)lsk~UlNaA&=uu`{f=33o`eu_e|%}Tzd|}oScdHGfhpE)C*0qq ztHPSo%jKMCE;4jWvER&M$*}r!lblP!Nt(`}WdYE}NfR8xUG4+X8Wl+b`GKPT?4Ky?f=BVsgE1$p?4qH_m9 z3E4s=du+4dY_xMw8~F5siZaO=Gi?2WBf2tnoJQ3szEXnKj@$bSJn zIjL^s!?Z-MgjXO8CDMhBg+vYY5rz{&^o#w~T^hAprdR?Z2E-4jFKsx?Q}a}Q16;XeUG zHS1Fy&|h=v&&#<}a5=*fSoAkeLnTi$r*8;0o+qyj$0^FVJ3S>io|&t=NFTjOox5LX z5fK)bkAGgx*<@F7>HYoXd1{jzl~0QH3F(;UmbTlWfMjM#*KocP?Mvhr62_lYwgNJ! zmJqJua&QH4Ai0bYu;e^7il~&yB3>Qa8#B{Ppg)&QurEf5I(y?zwJa0fs#7Z5?N{tb z6N#-baJQfCC*eV2ZStX!J!N++f3HJ(X9&Yodqm0P3^=}P%leybN3l0?8s8uiS{B!) z$sSIAQ7DZK478Y+^@>YlC-qxeQjub=rLA?$%2j1M6$eV1x@DyKp8I)-xJA^kB`8HW zxay)R9C#rG-N^sid$S=z?jfr7CV~|7p0X7|O!havmll%FWoJrU} zzl6AQcXJHO1fkS95LH1nb|SzDg*D+ZAT$ChQ->BDEsIfDks;uSIPvBrfTs=ld{n!1 zMTElD^m+?&^``$L0{R^DY{LwKNr_}w+({n{t`JfRq9Uk*=jwe^g8r3YL?c^h^oJMz zu{r(E6Y}+s!c1*Eg36!HhL3%Kmhbo|4!Tqu-(~0e;SF@{z&8CC>9hU5HT>7tulxGF z8!0lVJEW$7kEZdotl4Jc@}Ae>FdSP#Mf{~+x2Z*`0*U`6*g6qxXX`b(#bM2kl6^@= z;tyxlqM8-s>Z#-6MRX07*FsGlLs@wG-#?zQ3w&zbArT`jQyJ2@4Su&eRVN)nLxlkl zNQvG{oipX0i3m5WPnLmfm+yocJm|sNmTX3pQbvjK9W1n5OS@_%w%N7G@kzF`9jXGOAW!cn*9d>w7HEmvJvY`9esQRRG&6P1i4N5gOn^!9x|h_l1#yEUlxGj}$Y z)vH9}Vlxso&Fogrnyr$C>1_sYoI2e-W*^HhNNiAv0&=awieHSMz=KaIGh>b8X3B0d zSbog>9fXq&5Iwy=!x=J~SRV zzT(|`d?hUK`+ay-)+knJR0Gje-EceaVCi}UC@y+%76|M3j8Te~x@{%YT1JJ4+cBLC z1_`1?F>K2nS!zus@SdBjp+ox#X)$1=NR^UFOI{$)v*{+lu_Vh+y4EZ){-`Rf5rwf5 zOqeaoV)}}jD+>tKC@Mt5e!V?t-dRwI*Jis63QoVE3x9TuosJ06p5eLS>!9O*_?q{5 zljYH?Z*E!{g%_ZGXzq4i+F#x(DntJvd9C!m-c$rSg~`wEbv6pP>U!2*PKVIw;V6)F z#$apV>GQAT(8!;1=vsQiC3-;EzCq}Pql1I?lv|y8GW@Lzmum$=G+?tc0`!}ip|e|p z_xZ+yj!^4jmE`_TlXo4l*FF)v=Lr=o@>tI_jq z<`|OYyHifME;Q*`5o> z8K6-aHay#p15J9bi-SgYa$?*osJ^mJFG;NBt6CG2)winx)v>uK32l*D6*Z%gT(dCxuRLP0|`v?B)+rI7F;91!Tz6+8ukkZXu zQ0eBsG1qXDo-&+DPUJEm5>jGNVW|r>Hq;}BR|xH*4uQ&}sJKKo@a-a&D|P{hXy|2Q zt?g9lPhPoQXy7LbT(w=+oK#*wp$VXEPpIl(*i5Uz*^E7~f9)ogccaX&&2IY{>ZCVC zVC(ts|Fi%8o&V=QZbwqJ4I1&S5HIbS9umTCM<$n2t=sOm9Z(~~)SmWQ_I$=a?P6q8)}Uny*9uebO*^%2Tj zsQ4ADe#+`y=W0H!+x$`T8crd5(x(towSY^DsS~V&OtQPaXRg1aepw%!a!~5X6reX! zsS3fWFtb5i_u+FvUBoCq-V8V%y~JvN51<{laN_l@pPwoNlWXQTb^TlBU;5!A^eXNE&SK-P41nJW_TN@%#d_w~ZXLfOQX=QzEVlpg;tqvH#VICez z27>ZQP$Sk3X28E^06$dh^!!C}kAI_$UuyB9&_{Z9^cPO0`oZK@uzI^_H8^*SI?ZUN zOyQbttQAR0LMVr!M*jR$HPp(`3{;ip%?`Oa(!5u>UDyflRrBbri}r^vcD>Hs`SLY4 zfnViIvFRDm>2P3b9U0xyeRg+zF&N8O}r@AS63~08Eb}-o1_!1Y)<&P zI&-uzV8hYQ>8F$kNE<8D0U1nU$pB+}o@L{*7l*t0y}TU?ifo&1alKMn5t{S$uYW!A zwUNy$1MsEAwClcQFYAC5)SytWN+Mk^9ZYBl3me8(SUmKfU>DYiSh zl@KZQ))&cI#9_Vnc7i*vQYK)>eI&`QuzhJE*^~C?{rX)p+;*ejY<)E5Tc%zyWu}eo z)U0>p8Sl27_9`r*gFn*B5{@jeh#}NYIW*-8d3|%}uo<27-O#k7o)bt-rv&v#$Up>_2$$=<(xWxt#SX*{m+#@zSRyb2BVE z-Ce56hq9y%G!G@;m@nFku+j4RSSQ(}wsdlv-JFtl#astJUU{h94ikDu@KdvAuFRMZ zd{mkr6E-;E>PM;?MB<80wDe}<3~R5@_dZfRd`G!!rSa7J1-2gAwhfMR&z>|s&Z@?&&8x z%L~oL`J}N<*sr<4Sfy4kSK|ln8asBy+?Y*I3^i8EVfVedTk zb#?iJY(ywsVUi>r0A03+}UGdk8QY+_?>NLFeW%RKv`<->|; zw3dkXYW9PhR!!a&xDep9Vmc2KId7zGE46ZnBiD*%KAKNKQ;RTH1GI+MA?~?`B~K?6 zvqCzV%NrULsnKoE?@o=Hb$r*$ub@ZUI_47NB1NVjq#v#Kjly1CpEtYSn&(o;wb2aA zE?FJ;=B_2zG5;E344!z(_ej%#H0u{}&Gx@nJ^oz8qbKA#Hu-~u<`VGSYDt~xSBpgc z!CLtjcxbYt@+%Y`JS7k4rqsbu|*zsgH#ZY-hlpAGaDz_DtX_~kfc_E zqib9&{>uNw9XhX}j09k){?09Vzv*vz=}t<4n>jciq~P z&&-XC9}FhJUViFFlJieGGW8I#tfzcWVzT-(X|K|+ryEeEB-+%%6b3*me8{bhGxntK zW0ZB**k+!R7Uy*eLbSQVtDNWRqqMEPD)XGz4TX4g0pTW5C+M^Vre2=yeEQSpzxajD z>}9Xn^t;Wmb_X++Fmb}97c8}S^3OR@>*>>#XD=T4-@b9|{s&X32%*WBy`tINDeaFo zA`_GhvlU$cL3EM~(D}1J`!kAlI5`MIal6&NNhm}?7}99Sy1gKrU;_p;iiNds1OY4X z1;B){3vdMYmd||VGdiDPG+_V0CQW`RLqlmZ9R&zv$Y;OnyS@wH^55*D-TgGu=>?(= zhP#^eU2Ceu5h|0UIE>#kLcqWU#jOGF0F0PTXYhd+e%g7i|l_lU|2;TYe-tpt<_^n zaX@5s31V(<0e&pSBfBB8zCj#fsOc5H*UjQYWM|>FR^91G_q1QbB=Jg0z_u&lj-5?# z$CkHR=%HB>oA9!^l77%kcGs8E*#SzHE49@|i}CKzq3KUP`srq?^Cy4$fp7kY|A0QM zwo0KmbC9^h`PqpUyeV+cPWPB6*<0$+k)tO%ukmazfgR~BdqUHKrC?T!EV{9@2TJjz zREmVh`C0SSnfdc`cw2=q$eqYaMk*uR?yKa$oFI@;J1wV0lC^NUT; zDo=G?Z@5zggF$D$4C0{y*AtJQ|NBo|{Osq_`GqmpDaou+iWcyUq_2q=J6vvxLt1OC zg>JLaefE+&b|5M5x#zy>q$uP|QWO^1SxPE06-PXMTs!~Fc^5_`fW@Ex`JaO%=(G_ zqA2FzcA_pcFP6`z^}w!9N?j-16N$vLz;CHQ>eUT%MPKIY@_iGNQCOrqG~Op#7>0sd zm3nDG*^|4~>^#v;WD4HzSD=&VLVris3V~di$@NQf+}$0@-5qr0jERhuM~_tg=Z7C{ zwYqoQag^^q~4&|MT4?QUGDPp&wn$4@=~Y#csN8ohh;uwS-%L8Nk) z4#xh%<9_U0GXYSvCz7U0|MXA)l->7V{-25O3t zA%R%K3wlk`VH#`Pulu^Mg9m!ko8CmbNq`Xn5g`sHy8I^X^&kJ^e;l_9mCN){qL5ip z3)Xk4aMj|WTxo=nFCCl+ju6K5{O04Jh-ih$(PA7!PXd}U{m$?F4ls+O1v$}}Qehe2 zD7dlv*!wr&G39dLBKxY)L{;LZufi#M{%-Yq*xCY9JE$|ntJss+pi+5deR2g){@x4< z^!HHYY<8kX@?>gWzNGI}G1>s=xsDPa`@N)?!%@|n*lHy&Z*+L+^Y82UeJRK*o^vG* zMRr=8_dA;dpfiy8u$rDsb}Dek*0QUknWRDzCktA!lYybc*>!a5?y&zoQ}}!?gr%v^ ze&+K>4j=o%7rwwa_{A@L`JuPGS;e+_ZXroBys!CZN$s zpwnR7Rcq&FBRqBbN_}Gg$e1Zrw89ycYwW5SlcSu2y(nnR;%x|+eBM<+CnnDq@e(I-w>U+NDd)S7424EeUOX|&oJB)S# z=byA%VhxrqARd)Avl6oyvRD7*zx)^MT4=n@!LTv%YE7!39pvqw{NyKp^;drtvgaco z`3RVaKdC>9=3$1o=Ngv0{%rsTaR+Gnvq;#eI!9j=K~J9H%8^I@S8LqY(?%9tAyxdq z0jgf7C?482Kb3PxJB_pI*Oyt_STS`GGu=3p4~NF^FK7=h3QwkZS?_eu+>(pR`f`=J z4m2_yFY0!4ob2}Ht{bxDL@T;tm7J!I@%_u4^! z(^`r49+;Y$Z=An$ks*nLZ+6;We*CeOl{J+*+Nwj1pjY?HzPa;UQ7?ObQ&-KX7(K`_ zc-uXW$)*6D4#Z(sth)o*XsKb&U7Wi@p>^b~HD)tg;SvC-lO|oH5BvA~QZM0`dnt}2 zt*G15(SXPirKQW4mo8ok>1s1lk>YjR|0I?&UFo?c0d3pxmlsmq?TWCisS3vVmge!t7VRH2d7Cao?R7t96&8 zPqrKQhfW--+;K2Ce#GqCYpUgLC%QN@du4Xce{5xjIaX-)?AzaVD$kuh7nhh;zhV4% z#a;%__K_0~T0OzR8R}vWQ=&3tUH~ru6Ue zMfwajh)?7sV2t;LpRWS!%%8noJNuY?0w+w}wd4-z}> z1eUMBGYRSkHGz1IB-SSEm_6i_S-uJ zN3qSW%K#ZNzhDh zdNo4erd?7$XFZY)bkpw-kd^5buw&XiWO_Aenho~#n=;+E?Y7e>EC+SodtuR>zF@xa z=fWG#-GA(^x^ZnmQ8=tOfJ+Zr=6T1~8Z7@>|(#O0fx z6A`POeF}rnb#!Vv|ON~|ZqH7BvW8*HNt|6L&Ml{xN|Rl8fA!$WH;4DW&FuSXbKnioIY@WFuX{QR zVQcy9-gRi6FDLeOV|T{%^!2% z3vCGiLtJr?4LlGhpZ)Ng3>tpI)Ty;Lh(%0&zLf}+K7Jfhb?6caCM~hX}Sx#MExf3+)o?F`ysQ5%$>wNP! ze>2;IPPAVQD+zeSLj3EQ!SfcCZV(Z*>SthHQ4~jOMTmZ!A}2jaHXuyunr%a4))~!{ zI)A}cc=!U&sw3Mb(DFHGnvrzH_1S;S67n6^(@TKOP)4KU zLBdQIOZpJd;TxF__=dC>O%9L>aZ%uh$erDzz)CZo4)DoZ+|-tyl3n}D<|$f za$$e)2Y&$ef&l_xQENu0CembpFmATI-?nIiRZ7wHQZ?%j2hh$G=Q@JMTKRVkm-MFd5}B9uw< z5x}gS{Gue5tC^$q-!EGRYqILBuBAKhJ^k|dP0kY15=r2y>um0+W6oT>eEiS>GE)1e z_qt%d#LUku#zE+YWzYIKF#Qwo*+HD#P1d!#8+##AWyYV0MUB^9c zn*Er}nZmKmq*Z*Tn-UXFdJE9%H(~_T;k($w(zaf7y}Z=gT)S`2aJlg@ZjvQ}2|`vj zH#{XTWu9i;a|ZYxu~+ty!ae<5`@-eL)%8-ZE)=o?k4MFbyF*6t<^7GIRC}Z zegSk+b)v)5UsDau)R;`|8}qXx^95gn|G~pT2eMns@}G}$Q(IzfOO#g{d?yw zUL3ED1=W!~Q_Ppuk^~EneH$Pq;^Y5(e ztJQqTt~>lH`|>rEY~xI7Av&m$%cow2N}p)%34o$Knk0#7p(nZ@CDk}f6kh$|AO0bH z50*Ut^}qgC(q7yN{D}>~JvjR0Mm``>ZUg?r{-O7Z-7UDOc+ExBkKh=%sWcy|#q;n$ z89Akq^-fx< z)B!`Lm)87Xq%2cAlr|;LOpY=0-N(;JwN6JT_5`mm;dr#5|D^Gc&>0wfBn~g z{WpK}H|c`R+#M=~aP|K8zn{rBISW*%V3&XKFa8B-6vE!W{@4E+a6{wd-}sH+AguCs zEggaxB3!XgzzSUyc`qx8g`~-#XGpssXysD~e&6_w-$;b!NWe_Q&iB3VeE=HBgYW## z@8q9+%CG&}uNmu}_lv*yi@YF*0*WHIsy^P_(0wTjZu23 zi(mL*3GzQk`I0YFy}8A?@BQ9?%=ZdHnuOOA6XF$+D+3gXl+0kPIA73As{R_2~woq4*~oPju+F!clBfpzj`RkQyfV7HEn zij0}>mW@C8)VandpT6?>Gs`nwhg2EDz?fShGFp_Q2|;6R)**Eib7;(^n9`bH-mnCEtS_xVEVGEyqAC#=9U#~8EcJL+(w(d z09g5G^&)3`O?`4milVeyV%Vo5V{!V)a_u;5o9zn9QD>7Rw}2aDFo(;l0&?{1Iy#AU zx1e5aOrG>Nh=*tF7UFv{rbpPkBpLEOFO3~dwalVlIHf*K9)9024redSw0q-DrP^H= zUL>#$(hcwd{K~uE{cf}ulrEn-bqX8<*8+t=GJ>!NLxWI*a7Dp0H9n+5RF91Q1MY{F z>7%=W-9;0|C2g2&ig8`d%~QW`P|t6(YC zlN@>To8L_S1(!UIoW1?zPyQri65oXb*eTCt+p3hog6qu|dnQ{&96d2vSzL{`<$KZ% zz9${vd#vJFT0O>M&n^pFtasKA>^;znqsJcq@~1xjiEsPXfAsZlfBWN4pV@P8KaEDw z9ZQm{bEUWPOmp?AG(KPTmKh>@?KntB(k|uMsUM8FZJYpW5P)vnF1I_CbwM&E7mF~n z;;o}qM>aiKjZ4vLd#!?*GwfiaB!Qv1VI}=Gx6P>ZP;uYmOOV>x;8dtLN`KbmxT&7cbARO-+vM88fY(S+)l8 z&6T!0j{8e%v{H_f6`A~bl0Nh8M@oyzR6Ot;{D9h>uv!9eyzOmo15%Li(lQ^eBRUVF z6j}{cR6xnVyTAYuZ_r+_5`l=8+YpmTkr-Qh1;<0!Vzv(YO_#^+<{~gk#P-TuOgL#;fuLZB!O13-WTRnt z=K*u+N;2PW&9_s*Mb$k^9q-h|*4{mhLsMgS){Q5$2yTJWE?3VRNJYI`^tTq%X>AlQ zxE3HzPO_F(aVZtVsIfAjmKR(d{-k*^#Vfh5iozpBRH{i=){Q@Dg`lxgbXJCT(F><1 zlfzRB=G^YJxCg~LfnAK%pXGg3Z)eaZn?SI9ie;W z-+c7Zb64oYJ;tQ@{KekV^7Pvud4O0tv)E{1=;4!gF^!ex{;6_jMdY7^qSWFZ_@t`m z_F9pxqmpF7!t4^l(m(mY9}$EgTJXt*006!KfC!L}KKkhEUiUhpAczF*4!a^FMx_rR ziFrK13`_&{0?k9>i5Fy@5J!{nLcKxfrWam#fglQGQB;KDN1b`OZ9qR{rT|6&ER;PY zypW7QCVlVselL!|Z2a&4{lEYDpZ|HV3wC*+ABupeaE7x zUtUd3ZLA#5^;!#6+K^7)VJ1!hc1)+N9+WjsXR=la!UvA~op$BSlJm?==ki+MR_kl) z?##-<=}XbfNppCVQm&rqEiNsI7mn(9IJatA$Q5LbI?O520>04=G$W=&ix%ifGA{@e z^k}UXR~D3`L=jg+d#x*eVsEV1T6Es7$ccUCpC}eneV;rvDPTQ2(e?&v_@a$9)bL5O zmbss>?m0Iaw-w`shfVsSKwkl)g5OG*8E` z$z*-Ft}>1fhvcg)`bgx(%X&xYquU=ESPxd3oURa^J4D4=dR1E=U$lW#I43ymB$L-tIpU+ z>FmYX(--#ED(2ML#TPF3MkZ@AU8YxQO5nS`ZM%$UuQ4 zfHgW1C(eO>sQN*4Oibb}Kr~*w=RNPC!dcZh&^DAM6X{`(;Az-7@8VCO92Bx7S->mM z2D}CO=f{8i$5FrlmB@*(Taea2`6vH`LLNSmj1@N$dLslqFjBmi6d0rZLk~R!(BUh9 zY>3AwUKwqaLg2_@p%6Q>0cF+jE<52K<$F>&8yWL~qB)IR?G(Hf_m~abV~Tu_(471t zrAWgflV~M>EXLU4nhmxfsrLgv{ryQ7gMwP(c9$0yruU7I&@#P7x3wI1=SNB{Cs}GZ zN5{sF)uz7MjJ(l=cbTb4)9xj~Vi3=Vqple%o1l(KR1&0Okm;C^Ac&|Zp1bGJ==9!$ zkDWK&FD=eJvkDSutUA*Z2QSYqoV%2K)nT{YTP_EU(NQr5_Nfiqja4*rUGz^%7v;U8^N#D#HA{Uyhs^QljX_=XckC%KfpTK zxVev(u_CpyS?BqLD7)*;oeuvi7G8g{FZmACVn zE!jh!>@a;VS-ruuB_fg4b|@<;Nub<=o>dM^1s?+R*|d{x>?KZlX?c0?-Vx88GwmlD z=RUN2{zDVtg|gGQ6u)85z6Yv@9x?SdoAjhX*PFT=S3dTo)`vg6wA4MgxMD^trW((`^DU#_^^K*fIafA|YilPp<)>^4 zS09T}&XUydm&@P!t=|ge#s$qaH(Vh8#FbTGq+o^ZRd>v9t}FnT^=b=btkRr|t0y_P zmo$AlrF=`Jg^bDkEj8`MC#o1I_v3wq?X+`u$<6NvHh=*fV$;&fIwW6M4qF}YpOf@j zp=ChD&|Dh2%B4@@-bTFBzKVMF9k&v=FU4(q*Fgq1;=Tii7Uq|LOMDs3@|RW`BlS8L z(eq32$Nad`n=fE=7${a8F4q+2bTr9(uj+FmXw>(4FqnBU^aLe&PCUOqJ#sc~9nE+R z&*X5qy%*OMyUk|9v}j@p7l-zm<98t1aoz5sX)RPt(<{3a`COQ3Jn`h(a>Jas`{ASa zJV3)HXQX=Q&U;_~rmtUIaX$UIvsY%xb43&T#%exl=6TayKDcM(-s1=MPf3Zp$f8r} z67L6|b~%LvqNsXes;>GBl?)T}RCc58IK(i31woNOs1tf11up;`stX~W5l6Mdw$=v` z0#)=%XhdqmUx7jFfq$y)iVO}km$wNNg-yJj4>kSnK&;O@N#&iwbXG(B);tJ@s-<3f zFveD9!6&h2j*vBZU&Y;PSG;x4Sz8a{db@Skop%T2QfF-$yh$(7cDrfwHFOtXWVHRQ zlgQ;qyzIRa5T!DT;+)2!Yh6QrlG9NgKOBZ+34ttBg-PRCO8KPcTN7p=Fx_OW*+@*c z=9$s467Agmq>Gl>?Lj?7y-w7opMeek3VKu>r7Qzf9Go1s0;tY^>$iT3`vKI*31kdc zDn$__=MY3fR*@rzP~b9g^0^08*~3>-(8HZTmXmLeiGW^5y&~KLtmnqypKPhFm^v>~ zS$a-hfPJ+YRI$zF&Hj+@y1oaq1Zv~Pr}xaz#h$rHsT{`Xdb=l}$AWemTX>R}f!?I0 zh1sdXaae-;qPS<^zRa6)@MwMR3tz#-f!ByJDHyTi;Q!0(@KTxk8sE04m8p%C*sB)lg zp84|HSH65cazZHWByG_jp`ET#?{o5wi8~MD$cJtg=DcI+3^6D!SHeh|$jFQsutkv5 zUfEh6tSy{a)OlHZ0ToN2iC$W<%o-RS&6OCd)gYW}sMGO*EwuorNEmAhgJ`PZQ%NJ8 z{j0=;&GM;#ZWyia?MhFwEagGIMl&g(AI-23Ty;&JZF$Xz@FE7{G`Cy5wCpE643or& zpgJ}Zt@flZy`!!5xK0>Ix^kWpLjw-KN4Hr<%uedGdrlbJvSiH9k{E0hOFL)J@vyZN z;Sk?{%HBAY#BFs=9niYgrogF{cGvu*ig)B#$r-PPp6sNIJz~X<(pnXz=Y;c#SPD!- zCXNe)zKRV{ZsErRh{0TdbR_vllvPNwuv+FCVI06;5n54>MDNa;~r7Gc}i6Gr(nX0Rs*j^ z$+~M!++kK5d*|mD&t6>eBZtJ4RH z7X7GOT`sxE7obcIeR2hccBn8mufgO;`4}Y(@*SE&tun6(lPXQsH;OU_#X`BvCu%_u zTPmZh&_|nBXtvAr6D8`cr(=;O*EIj719k@BCLhQ1vPsP!G^8qbQ zOvkH5rq(d#^pZI@i}4d@hoQ9*M!l)Ybb3r`xqLH`2<;;IEEUA)(9IeIRg}VkOF$l| z*U@-k2LR$>J%V&3?SLK{f&yz`YxoT|LN$e)3#}D`ban`4;#7lX@EYJk<9UdM0jv_} z=kNdi@3Fh2*97g>>4Sj~9tS22mSC8;bJ!uBDA<5c0lab@;R5rXwPuT;V#xPwzl0TO z?{ehO^r6E?z_4F_>?@qj@yR`oJeJZDT*5OfsWs>1KJ&d7zk`R%)~(h@ICii);>-A3!pBaW3yl=YCmsv}9iC;tL+tFDyrY(ujAX}@ zBzgZ)O@yhj3jZW@sZLfSsIfUR4O%J+5z^P<5(P22Oh?%xEi&h?8i`d!o2ypYKt;-m zZ|xhT_5lc@|PZ{U@8rBGf<@!4yDcs6yZ&l@B|ew7T>Hc5A_QVY7RRl0|D$t$K6 zbVZ)ntyIENy;2_aom$dcX$R?SYcZZV)m%5-(MGR&DDmnEgb@g*7nQnRkWvp-1)+8# z!|00q2T#h5Z@HRRYupctp>LTe zyz97!`4gpG?Vh>+^$(nR{*R1}$gd5T;ZlG(&K7&m)DogSMk>N1nXU1fdElte}Ic}I#{N&hyJ@=nH zcwkBdLzve&^yr2R02xT7kvzn))%M!@d7~0$o@i+=Wu-Y&h4ilIRK4D$iKe6YKrfk$lIj8faw~P)ezobE^ zi>Qg{zg~?aoS@f6!{(8_`=&=t(m`~MoGVlsPdmlxY2-T6>62C17C=!T}I%*^c_X8eMyF%?pK_U#bkkb6dYMfQe%$B_1?s}J9jMW%{ zjc5vnAp#x*m`sxSVh`%!q)E)d;6BWA?X-x%YXA6!D@*&R;k)BVePrzXg|i>|$VcwE z>+aFfk@3+{Yn;FXCPe|%zNb-Z$oIS)Ud@hNAboCe06vcaH!;J|q2ov2^6;A%R#vS( zk+zA*^f2f$JEYf##dO1ExNJAA=(WX}!!_Lw6E<_=xS5$d^up;g&n{i5g_Bd&iD-$Y zq|xa9F|X2EXfH+G*1pMUce&PHK{wkAM?<$1U$}hk&asJu`z8(@G!>hnN<`t>A*q!{ zRL54t$;T1B4E#Z2O=xsJlQS-02<<;gDAH6B5l%~&wHSxL@}w8}6>Vb5N~Md`!9^2K zsl%IZ?@NdZgu6U<;$1ii>+`^ z5{$ZWrN&**sg~Mxx4V{7AfLEa4iwLIU+B5jmN&8zuP;Yo?1c5|$Vk;oS1$35_l{MM zAK7W)KV~jYeiPMaMn>ItM2wUi|f6|22q*v-g1ye1MDyS4|y?Mwo&r{ZZIPNp^_TI3=?+) z_#Sd$a6&M4>HwrS$*mNEuf2{P0kTPLIkAbQf*k@O5MNAmJhs5N-~P$fnG%)hYUGpk z_z0;=XoLDMT|5sU<#aP^Nuu7yV^*pRN7&9kk2m6PmYa_j|tF_j2AbQ?Nn-QADwPjdRvxj6%fSJ&s_Zc7r*quryphn zGnSa)?Tm8lkS9hv59EJRClywH@7F$#bmPR4!>;IHIyLbyV?34J>m9AK*`&|ImadVG zz1G-tqlL1(;$n7j8HhGIDrIPC_E6__b}xPtTj>%xyS9}K`!h3(tT}mnua~SM2arxA z3~&?@rwp4B+&E7LjX@X&qZk&=ZMRlVpMK`T#dD8&k6xZ%p|yI;ue8J3n(K5zH?5SK z?T(X#rOsrzbEv$2q})DUY9I1@qf3+c=$NS637n`BVt3h2+KX{(Hfde?>en55bQ zfHhR=(RB-O2?<17kHy7h5?i25e2Ml>Pp&qacu2zfK(?U};>P1v1jI~?*Soz0uWkNH z3lU;OwNjd2SjKLfTNEMDXuUc&zeJ-FFwj>$_yAqfr14gzbn()R7KKSzaE$v3ZW<{O z*zzngr1!+Ez~NM{thdJ}CQ+_+x;#e?S7JBn*k2>RW}RC|bKOYrlf6P;)9c1sYrvnV z-_+XA9cZF?8k z$P)Ny*OQ^wE|XIeCr_T(GaIk2w#A*#kw-7}yVf}d-i85<{WJya)1S#jPAilQ$_ZxP zx(D3Lw0u>MMGuH+rSt}KZqxj8($^iTZ(-)_ERpQm<};dVvmV4G6HGt2n6ew{pKWQE zD^VP90!WQyHgB7=@##)EtWHh}>GH)hpNarQ&6H%7lx;a_x7CgsO%h}j))JHXCc;~U>lVjsxmq5A;JNx!b)vFao`&v2V<3?-Mcf9Qp zCY4&PGG4FVedh_@UaM8)*4i_f-{a-Lz3=Y3jvqNJP34ewbtCrh@K?XlDtI93n)~)l zy!{&<;jet?2>n{^8~094dRqD`Hhr%!dbk6Y=+}oP*!A%Rksxp#Zv)GeYDSq1*UF0c z;~|&VGDw|!+17DG-19QWalIjx!5$wUwXaij2AqC@It)(LW+ET00puj2?o3%s=v)CC<^_GI=hl#A@ zs_3e;Z{PSm_e@dBsti%d7%Nj4~mOaZph8QjYacVWR?ZyhZvNL9FqkDVyOmd=VvA|9= zjgXfOzDjrF)uB)SPs{+!Zjtx9j(e0(PDS3#;~(JJBm<3S6mRNSBW4DD?-{S3$sR7( zv{XaavOcj|cc`4u8lXvP$bs5586g;Nnaidd9Z83v^VV9N#0;zSu7&ThU_<(~xK7^U zQs(9m@5Uk;72ou1bBXa$?;pKmqMc0GbXMj)CN}6EqGO{978TN=w_yh(B{S9^^}Rfb zq_$!{-3a@--m{xIFZX-B)a{*}@W=JJZF1aW!?!vExm%*5<|g&t}2ILz1RgrJ(!|ANr27cU^w=g-e$fpmMsa;2Dw@UN4w- zq_cwa_ur1b{G${Gyy^|9sj_x_U&y0ko<{PO3vp~`NOY_QMh&HqwjuVmDI4oIT8BYTA ztBwo$5Xas2502U0CfTn=Jr}2)@dQ{2<%p=Cvrc%H3Wd#?bw(UD*BTDWFJnQ(jH*Dr zRz*i;RZ^X9&|Yh+#k(sA$bFF!a9G=Qi-DfBIEzZQ9cYHjz;sg-W6q{9Z1UNzHG2@6 zAhzTX6AUKMv}vkk+)1kG6M#-##?~CnM4=e8hBm(nI`)p*aSA4>{?GcN)-VMID9l2Zbbg*(1& z-jk@f%ZgV0gJVYzJb15TMj}&+q_hvv+i4rzc_M4kT=vbVhXG3@EKV;x==6^-7F;4* zgyQ=wd*ri&>}#-dkaV1*G_9&r<+y5U^Wr;I784n59babJq^&@U_DlLg^MfjQ(7Hd3 zu*H*;3(Psa0H(UMPbjGtUR#hB_pHK+0jH=UV|uOE*SX3?bdqeQvGwV@xp0piaf!Jp zCS2eips?P}dYv3Q8|pi1`U*yeL+{I2NNkBfGUZlIxRsmD*!FwvPK{SAo{`j+&0x#i zG9H9G_90D&md&?R{2nqrSLa2FK5V(?r)$X`XTaO4$i`SRpK{63#EX}U`TkV1BG=!tC{k`InQj>Pln=m)RcC8u-uohcCBuTs3mg!Zgl1uvU2N>zeF_Eok0o~Z*AYpE} ziCyj9rB!$;fv}{GZ5f`u5rfQ%b(BZB%R*B9fd@NO@FU^k9Z8KAahOpPb z^k|E&ZSOAyJhpTtb znvJ|pvT1snwJ)$CODueBip$X=bAllW3awpDr9U&F#5UZt)dRTm*vAb6W}r??I|7%d zRAjW4d9}NA6!v6rCEWyi`j72xcNc4D(9z6_A-rt0c9VDsFi1;B@zTiv_w zAaUQO7trbePy(@^ZnuPkOYFs$Pp}Va+Y4-w&<3> za-Rj#KRS)Hw6;j$WT@TlA?OQ55~~XL3p|aNCt~e;^1o_lN6$Jo8EfN|`BnBgq<%5) ztGKvnp}Fj4=#vcG2{$F~!RH%yCgCCMWk4xaqJz5$R#zKAzy2sob=kAvIYS=)N_-2*R`fCvkIb<9x&`bY=&4@9lE(&6FD((!DDx`y3%IOU zh{TE@q$u9;x}?`QHM!=z!wojgjMA*C-{xy|jQHDlC<_0F+IW}rrc)g}t+2s56 zW^+8G&MKAJxXiR;anb3t#PFIRT#T+*S=;D59`;C+Y!8;0i^jvjXt+MqK(hrSo zPm+qwm%UmnwCP{PbO(X6t2LGBE@pmS1HOmMX*4;8AV2JN+i{f6T$wjgVtpCn5&xdG z#)@jP0=sPM3prd~{6hLZJ`LQ;)XL({jNJnE3`|6^K;QwE+%278OVw5{?scO|wG>6$ z4zfegc~c{vje&({>fqk$m09Ew(b(9ijIdExXV1j|d24jrc^Qk&ee*62YM4uuX|9Ua zP^%r`V|ZAwVGiU!jZ}uGDD9tU-_L;M#@xGq7X4B-yU2c{lP&uNbW*5P7rJKHUU{v@`FuioI0^mDp#24gd5 z=(+j`r0PySQX#oU!?<3l|D3uO=Se#zC(WBt-nh8uz;yM6^YiO#lyTN3$7+p6>xJi^ z2kYRlG&MP1tKC(rl+`O;rCX(8J=e=xB>QU{@nCyJ8hd$hVSaXo^LhM^=_AKZjMwW+ z4II#$<#LUBb~+LZ?Y0^5NM+}c4!F6Ojo45dsm#oYU)g~JU^m|Lp_%j-i4*C5}M zBLK!yqAdB2^>!DmW6J@^Pq@8|26DFH)2@*(iIeTQmf|qZW0-8bzSdcT#8m!VLiWeJX&nq}M>cnk7 zibQLPr8JKk;~q3gRaiY?;J{w7xPC~(9}YsSm8DCpXlx{6W(kSZ?@v4AWTsAbHR<5C z(~Ly;BPY9m@^27aGv9X8Pfh#UxxqRbN0e|uDbUs^Ns`>me9zq6TD@9DYqGl1eEhK| zXr2I~e9N03R!h^8pLKFkD_?oad+p(%{4$owu@SY4#rJCJ?mHPW=H}*T-a9jM8M*%0 z)E*vcl3?~3wQd!;a&u$!9Ts%mBA|yTjTbO8-0A5tdQ)PLPTrHL8~=C1p&$Atz9g@j zu_{C7!ljE+L)7hQL5=I!s^2IsP$QcIkAH1A(vAH5nM(=}-v$rYCtb5ih`S@Nv+a5< z)p_cvr)b;6NUKfJ`3>+bnp5WhDB5AJbG`V#uvD?v!eL?tV_hlNeCv4oY9X>z2{yG( zv0x>(EJ9bTxN!znKVr`&IK9xRbh1liRbH-&ZTn1~Mo%X{s6;svvwiZ-cHWk4ta)tq zPjkC@$fku#buDD<6OggBy0Lg5p~zT-y^ z@7XiHwnomY*J}1ufF@F0&n1OvHR{9VnwQzjmuOD`TNe7HftjGDm~2jskDYq@@e3Eu z%`Ge)IDBM$bZlk4LqGP>vDz@5cniXvKIpO3PA&A!#f!7NIDE%cr_&lA4QA(ulZs7o zT@gT@{sS1CIC(cda;O}%Tcq&NQLN@J0|-F@NzY%qrq2zc5oL)odSJ7*)7@0kq6Ebb zSAB=su(scN{K==^@Ks;UEa=EFX^QTb%1nM@tC=-6U7`_1kkAblc9SG)SVRvAxHrx} zS{x2L@V!@s>gdLqJEdz2s$5&(>IkWqii*d6L>_IeAja>P)@$KwEEmn$F!WA0o)f(SUzSVWvl&IlkM4cXcYS|Y4rtc)-T(Lp zQPlnR@Bh*8Bo)z5p6AM%FN*04^RK}4ErV^4Gu~Eq)ILOAw{cCMSW4=;m$ZO>k^+yU zh$r`Li__?u_!uw^4VcbfytLMAanTt8wBe;nd=&R+ul1}SV&LCUSEM9)C^@i5JpEkj z=tld;;MI$}j@Dl(&yw^n1R??2a;5!ij2cHZUapt-Pqn5$S?o>+t)2S08?7Tqoh-EH z*OWD0_E#^-cH!NIQ+%hu`V=gyjAkITtXrz=Jjr(^X}n8CUAunMV~R~~Yk6sLYI0(F zdOD4Iao3?csc$_LWDb>dBP(T6!=-w4$iu&!1q3-izrtyR9G)1hcgrEOJ@ozme|zTv zX2(^Y|7o{v-&L0^xnLU`xrUMuOi3V=gcL$T`;&waNF%*hQVDs|>yzFaExl0#3B(YJ z!NxW&vL)M+tg_Pfy>({(zjNN1*}LV+T9wtVbUwuT?(Ez%?aVpvdB5*_KS*zG>>~#s z=P3+~j17&ABx&Ew%zUNVx12IgoQA-~!u_B|PobZ?@7@2hul~wUe)5x#A399mpu)4e zM_xY8dRe5)LZAr>gmfTyVsnX1Qe@XUe&Ifc{=jPIU5)$MjN65z;@rt) zRJMv&vqxNW*h52?@PSW%=B`#d{^X}VedA3xpyt&-K&rNl3=Ij+jtZ8`R_09C+Y*TC z@tL#34;6=GgKb&MottP66Wi@)`E=IRlp6VC3LhsO3Skl$Y%=9-r4cJJ;Q1Z9zg5ez zV5O5}UaH8;IHLmhB$mR7G7OYIX@BuOE_gnD=$rvzvY}TR{sK|vca2(vj^k-W+K(5{ zgw};2Aznr^B0Mh;<-Fsb!M@6YhbE33I!L+K?OQe@kIf3IuU0WDk2E8-B2&mMLSvY# ztQ(c{OC)cOZhAoi4S+}VaF zGT?j4VLxu=n}eG_{pn8wf`^BPsRZtBjZ6Z*X`Trj*nDz>Cv3cbqM9!3kofI5Q+x9= zKU9))Gv-6ms;us~X-gAc>}Sv?j~~f!m5OtPxwk6`(;n@tynS+f=FYqChAR2=U3Y!O zq!|R-4dgANhRvDH&|X)d51~t^z=dmXIP0_LlG;k5R%#FZo*+0kQxwAjAV9tK^ipUuo;7dIWK7lr8ddSw366|*$GU%RLRTB zEEw6JG_aoo`fz^z#K;OPcR;ZC-3PFMFSNmd~ zr_(s^xaa86V{j;(Khuj#Akr zZO+X^CV)|UWNgzQi@wrH+#EzW_79DkqAd#i(9Kt4gJEnI-ftO#!L2;|Aq3!O=;U{$Z$E zUMEY^2996IQVPiEwZl@7EL2A{gH@0TtR^-C zp^{9{OZgXjVeCh-@qk3BRQ2=qdT732%N>wI7cZ<@x<8z=d>Mab{(o*}esHKC_a^B) zuYUEb-}I3;z41jatW&VfUo16qu}6AE5E>my5=0H2ex(X=Ff-TeAFPwWQGo*34!aWc2=b^vEi}v@I?JWO{sYoz9Ts?VmjyYr7 zSnb$2LKThsK6~fMqmL6&Fg-JU=O;gY({)#m4i3<($(LqE@3a;XMnsXu>rS^RG;uFs zF@M%h z+!jsYqdt>h}gpwi_Bfji~Fv4 z4&&EPw4^urs}R=)ENLMY+ZxVV*4xEcLUDma(HQ)0VKT)0C834dSsi z?-Lu$$brgB5YO`A`0TE2<#ht8EjAY>tA+Q5H@yCeEB0{MPUq6%drF~^pYyFh-HJHq z=5LtuSNYP#xF<`+@{A^7LgX?rF~BJ?zWeq)Fg7*T!Pau}{No!748b;+`UdsG6J zL{@@+JR37#sYRWOomTVng?e#A)D>AUgb2G)?}nwmpH?df@v?>p!z9SO42HvIjxdgV zFHgD=4Mck?j&cyS6#IIq=!_@`IC@wws_{s79_c% zM3T!*otOK+7Pu!Pdu*5(j#WDYrH(&5Pl%_mZe_N%O=Km4iZKx{&!uzki@X?#m!JYd z(V56*DO*W*!>L-q4&Pr$AvXMuiR-X&95(zye=)w@av?P57juTKL}|UXs(=$HDaR(v zHsg}l6)oW}=+vE8*kck)xZYYN8mJ9ZXxh;Ffwy^dAil&_9POrg?je56Ak1}&{SCD2 zN|NOrMI&k`*8l-VP(F(4yW@^KY=ypnga(5728RdkzyEVL-+1--N&GpnWb%@3$0$*^ zE}k+U9~|zvfR@)BTd@4jpBH|5?Zg{0Hnf$c;%c~=vDF*P)ad!b^}#u(p*~opV&x)cX{xoa@$0zDwAvD!}dMxqeQ|HF9 z7WOC_;pVBeF~wJPvpy=Na-PPkXl*WuYlsZbOl%zv_mF@Sl8lZXJ$mI;R~G|1uD4f#GY z8VL8(O=M5Lj+CE<0}e;MFbX(ypPp8PoSO&|jL!@u9D)czzQPAu;ACKrZ84W}(@LOn z6k5ZT&K46%U{l14?ET`y`Cd0Hc|W_6jnQmvxBbw_G;?~5X5#x7?II>u05@PHQ;1he zq)}J(vg(pns6H&viD;W3>QEhyx`pP!*Xpf+2il6gR@*CA%+76#XPVxiz7*svP|3zT zmup6OiVj|~<0sLkM&YM)$^hmiLC#C&Tk^RX3@o}5mBHQedvKKHqdTBAdyM-Cq5SyQABnaV?l9>09| zjz<49~VD+6&r z6%Rl1=#{&70$C0oJb3x#mxExy9K5mYfczRnuB2#iU4t-=9XrMcc7WYwBY)-nXHkoj zn=fwPzFp{tFFy9zV>Avt0;4popj2`c~W2GSbU2H?Eth0w-%D4z48T|ons&$$ggUMYZJ<2gyMbs(!>_= zB>8Z^K;UG*o7uD!mZQXn;|Zg(_xMR~V#3CO`d-uS29Rjw0YScs3D}=u+8L}yn?}5W ziVgVb#O0dJ2s6_4mEQAx7w!=cu^3)tXkf)Rd|~4g&0FaTY{Bw&N~%wpPl^T^q)8U$ z2*98qpaLNZ+dntyF4sT>u|C)C&rTC^Yzs`@b`1m((47<}x#p_J_V0(7WvLCq0I~oVz#qKQzzdKNI7YQ{9(w2@3V!KVDsGmnwXLDYPvZb7 zT99ZBuRV#Kj1(LK6a}UM34vz#%wK8z$dMyZIPAusgqHY~qpLxh0%mba?9We53CIN! z+O=yJ=!X(W{0j783+KR=0=@b!l<5M@2U*cUR6SFvi9`MPR#$LQr1PSR%BjiMN)UPR z{0g2diUT~j-}~>6+;{MCl43^caDL5M$~99axS7{uN{!?jUup$uBk<-w|LJ=_@21PI z+Ga?gB$$p2UTVbNeBfPqs`D75of2~vOx!k{#rYq1bMy@z0&MG5sigbk2 z$;ZsPXu;&$%Lu$x)Y37aO)EjQw(1WZc({vsK+2O`kNN) zWcgg_X`*Il&MwAj;zAvm70JE_ALQkPvjL85-@eT=<)s=>DF7Y7hjd6`ZonL%mYq9y z!sxJ_FA}M@ckf;lx<2)(Pl16TV0h=*6SBv~`o-*Eq#kQEaE#R{ZX7>;oDl(}0*C<& z+0h{YuW(Qr2hia5Vwhl0kPRpB$Rm%y53zHfHB~VL({MQ86z9ecrpi^CvhlLZE_?97 z2aOA<(TJ>xm-2G`B}ewG-F#ptT${AzFRHnnkv(ZQ?V&&)&jkD84C}NRm`Juw#9Kag&*6I> zIXT`j?l9Wyc)(x&%?dMED3&hzXZ#gb^x23c28l{s_$ta||3n0lcCh;_|D5JkEKmK< zTzcwv{hFOm_Vpml{+T$KG%k*=QwmHiA)Q{Fiv3wQ?G^v&>e#u{wR4i6ZLfbTU*<~x z(qe}m|8&A#?OdG32v%s?YL&&9Q!b-TRi->~wbEy90jZHCC13ibUn=ATY}&JD4_|r4GoAq=`P}C| z2VS8;0Mm^(-iTWYyaL+!zH;uUy~fHe%G_uhLiK!m6ra1W%;AOGM#HD zFM(_9&)^Ob1t$dG1Fqs6mMAYdSJ&DSK9b~Bx|T2D^W|AQ7WCLDIh{3GSysQNWBvsb zGv0y2jZfe6=v;F+to2XNdDC^GeZo0EnVO4t_zlF5+Fsc4DWgjkd?^ZQVbIqO8V8Rg z_dhtdeW-tMEC`d$K^zhD40!MqNNrP!7BUn#F%^I{AuEYF!NAnF&_)wAT$K1ty9uqm) z8}2g@52Vxs4?Msbg2oOUIKZ(u85pC#`J2D_u^;;}KGQzqHRto^fBxrn4KT|@CtaY= znpqGO^+!#ah^#i8a>_mBq<1fR*`e;6V%5ZV0(#=vBzo-eiO)T7JPxYm!EJt2Cf27k z(9esKlsbK5TSt<5D@o^D@mz4n;+ggi&KDMLh z;#FFgdwFe4lUnZ!5ZQ93rnsp&V->*Y3ZzN9>5yCstN4&EtANqk`=8 zv2Z$3xnzKsMHE34=Ge%dPp|+-6iVV*Jq^-oCllZk8h>wS=HqtUs~&>#yItW9Pv1v~_9u zUgCSp@2x9S;p|eJCN9*0p+@jDL79MEU{io8P%I3l3f55=4cG`I@v2w7>RZ3{Tc7v5 z=l#Mj`~tiVHE2Hn^FJS8#Lmz^d>#0LzYO0KOB_Wj(-f!(3Wq^~MGuXGg#PD${s%zw zU;p)AgaLts*aztI{`bHCJHPWgi4!6f3p4|W0S4i;`qMxC)4T7!8|T+A{^BpvGLb+W z6nOMyU-o5x^;dsIvjp_e=9}L1reFT$U;f*_{oCh1|M|c5TfYTjt68g99XUC zyC)XWRj0{nb>Wv&{>uuh>YqBK(hL<6tIB0r$?jZpK|6H13Yv5SuT#XUskaOL%gN>E z&b8CJ9A~Fz+~A@+@vtPiVQ+rO@C7Z6bAM=Pfbo55YQ~hL3%wQpIdkT4&sx5>3D^a0 z1}wtc3w)8M3AJ_JTP2Wdu?VAsqw1gj>7QQoq8I7)ChiAt19rhdM(7Vj5p)ad8_*9f zD%xbch8*I%>`ZtveyCTz@|6VWfN%cuKmQZ3qIhJ$3ebcx0=E}T5+OV6@ReWrm4G2& z5U2O#CqJ2%ITlUbcH3>*nJ>acan4l3;X*iD*ru=j+OPe=AN)Z=e>&ERA!$i$#ByWI z(pNOu6ra!4edgvAiTcI#Q=AyH8@GtV@h9LQxQ(C6nB!tap)J?VBo<`F5?_3G$<89W zXq=Wkt0bh`;^nebx>wR_lDv>Y$cr;nn>J>%tp1>{vz>{!T!wfEa)Ku@)fr1(8v=#u zjJ41&MNy?3nYtXEPMgk9V$IuWc8rb>R87a!;F^z0O|Nt`=`{S&x;JvyLj#|Feq#3a3GV^Vo9^=(!v_l_A;l@Il$+{i3yA1fKG3Um6ZjLKh1+xk z`2d|Mq@vzp#1{gu(X8{c4XI+hP~PO!BtKj2rV(!p+1{W|S0`@G&P|)08x1ebyhlOsXr9YmsgcvwMtRwk6vMwPw^J( z#TC6pkCGO2s=5z+;MKOF=VWciURFjeS;_@v5!@NtgR5#Zi7w}kdDGKVwLY@DJJl*O z$EJb@{f9rbT8*d_v^z*xrsOc#?Sc9py-Qv9LTw-D^P2TJii?nv*l9JW4yWzrHEg0w zSxHO{m5{rCd}0bI!@=S3@+ui zZWw$SADLO=>1HjK z;?@a^lufF4Hfw6C))+0}IC&wZBfz0GgYs+5>gj=crLV}KPb1>YEMn^uKA^+ULZt$3 zpweoJqyUEkfYFc4DOF1q>tUIjYYB6j1g(w{?C11oL@ldSlV)Xw-zG74JFEl)EiZiZ zaA)7XgE(3ThepG)SE_}z!P4Fvp7i_|Jm*Va^2`@J|H><_@WuwVmcrp1uDX@@`Y(9a z)1UhEtGDeKg8uOXD!Uu4%c-g9M-I%BDcCY|a@y*VhtpnV{=*)1&P{?b^`^qc}lws7v^Tp^UB|z!uFj+DP65m5?4LiWreB>h^ z`IA5S6MiMg20zp{e8V@$rA0!P<_W;i2S4~hb|9RYc;sh2>sbuq-}PPJ^>=^wcZ|zY zcIJqhZv;LF;$f^Oo|>Q{a20_^47l(&w9JV~2T1EJf4AYvi zR?B;7`70))_7@9 z)UC#uN`+|zcUp_L3ltVy7hAYp+;hJOL zrp?eBm5l@F@2E@@#wl>z0s5&s`KC z2K}=n;lnLqralRzLGIKf_da`|YYrY0P>8F0`r$}z+i1^5jwtb^PbP*@X=uD(D z6A9lUwwgg1g!J`a|MkpUoCBCi7uAdJS#i7OV=otUE~x?6ieTgk}WAj4G0Kz@ZKzPXt)iBbwbzmD0OqiC*eTS|aPorx)bVkV0H(Ne@!5zxY z%59?DO{j%$f$Rk( z)xl_!SBzJNzp@>|k~85mFiWJ-d*1UN@I5VXyFT%WPjEOO4sRLkDVz=Yj6nZuue}zQ zlyl~AfBn~g4gbnUjxICV?i|miC}r94$&RMGq-a&%ucnvuv z8d0Shy$<;sv^vch;&wS74K;e=%9g{!MPCCw=h_pdAJTN0fb_76b9K@7*e5Z3U2V5y zC`zrd1^RyBF@&GBaet}@)?gkq>-fyhg%3Rx_L*~rUG zmKKArGQTu`14J0O8Dv>{qT@^)G-J11Dj)smM@isfZeSE<YsZR#|dv+3`LzA`?G(6`i-g-4)_W-Gga%mS=;={b-JDAMrI@4fr35B}E& z_Fng7pzYqLTt{Ex!L{P3)%JlbAHDtV&pzCl?d%%fv^i=U*}bqDgq4mLHA1gya=;RA zxUb^3N49UN&d)`|1Eq$I6|N3;AbUDVJUiE^RW|q6Hpk82@gvg@J~V#q)jLMRg>q5C ze}Z%%awron7TC@7Pd5WPfCjir1d7p<>8|V#Ere$l9tdYHDTnki9_Sn1@P=>uwr^uk z@CO|pq{0^B+wdjv>|XMcmwfN{e(%jU-^@J(uM^16Zr~~q4D1&jm3;)5gg*HItYQ7| zLqGIGoC91Ahk%~p)^S7@9)JJ$f6q#TtO7CsST^t{X{>YpyiG*T@sa}C^z7hbIVH{q zE|#MJjHETnL#IJN52wuu^G<=2I2_M{xAl9z=X*Zy^FEIkkUj$pVo?Em#QAd*+|MjO?(@m8;Z)h!-xSg+vz)O zryrv0zOJ?-jUzW6eyn>e+BgLndyPygC#P%(JUi1lLuori&R=u6gF#9 z=q1jO`Gygh;hyiZMDB%edhtMol$_U9miAoTXQ@aFqCpZsOddvBd%~pa*_aX$IXE0arQBpxw%JYoFK!guc?6JlH{EoTqGiiw;+f^R?9X=Q@+7Pn z-(n@4n3}%t{s$jBe0ckgU3+i5nb{BkSFHx~vkkNVqX&5Ox88JU=Tsj+5K-X)BIJugBdCnumYi2z4 zE~}a#An%sFHy?bYZ`T#k=Iv9l1wDv6ht;HisB~hyL0oL7?Xy0Q%0n}=lLrnSZ>Bp0 z;0nZ3JY2B*hd=ybcp6w7q!~CR0LoAQ^iR`Kxif^=vkyHMuM>CX8^7@zzx>O;oEyU3 z10_I*aL4GMfJl%8G4p-@D829U8xP{=uVo73ZE0p;+j zf{?!N`@Zkr|NY-dM^Iv;cs|@)w9orXC!r~z7zvWR=G+0G-oO6qzj7bm^{#jErf_KP z1`kwXqyo@t#G>)ltt&u#2Od56q-(Cd;l`Uw)r$OFbjyAF9y1DFdoFv%GoD7!e3Dd5 z-Ps1)_pzNbwA+2N-D;DrtL^xQ>GoWjUN@uEZ2jZ(5QZb1^$c9_g`=Ykrx`iwW;3kPz zXWSM#W!4nC&d{kCWDdl5&A2R-!x~g;R)%;+UK=mcVn$SC1H-Q7Cr(3^*f~|zzTt~} z<%l)r9%U6+ML?x#s{E1iY1iKL}lmK|RYGnfFd_I^7IX#74|S zLJ!Nf1IL5)ww+6(Cy;;2>tmRz5O*GVbD;)Cy|#9ZVZz|_6r4G8c7gdGIo-{I%+w6Z zrpd(UZ}}o}Sw6CqVcE)Ps4Oag5g!=)7;_nU8HE)L3vHm>5S&m9^t8zstx{PGd3>D@ z5dNxYqmo-T3)tc&sfjk0C+~ocn#*45|JL^;`eIuizNFs5u+$GPts5GhqAEm3r4V3GOTgu(w>+H`10*~@n zGtgGb` z=Q%e!%|jm=Do-~Ub<5;0mY#PtbOjEU}QXXhG>ZS~Bt%8Cj;A8L2H;r!H z6V`(F-d>rRjoG=K)(8%sJxQoj(nNhu^l)w(jeNyddT{|ul?Gu(f^r2X%pfNlm=d;t8g;3!*0Bu z>b;7cr(emr{NfkC_@}M02;oiP1h_^(lDY>k5#QBo!VdHSLYJVaxHPaDJMeUf1cJ3< z2TogKg@V1zLda{cs{eB=E|cU2ZtSK_{U;_HNTiPSliHSYZrpi*+Q@Kp@}#Lfymiwc z2kPE_;$+hJtGJyd^7;UT882cEPSw0E_v4xSbla~-;;0A~oKYgvn(nmRB z$_L?v=Ub}l4wu7ECBJ};Xhfm7BHkIe8PhekGW#+db0Fq^W=-1U^k`6tHjMR93lKca zA50I*b<*Kzibjew9KO!j#14$wO`C4S2NBmSaM_2+gR@m~HA9;a8z;}X(Kv%wp|hf8 zF-I@NZ&tb8(X&ydsf(ulR0$o(f#}O zO;1hq*Qz!cqHpJwSAd^xz2zodS}UF>sQ!4}yZ*_S9sc;SB#s7$H=$P?1^(p3Oetxd zI8JntQM|qD?n7xkXjO-XHVuC2vv;ZU_i!foh9tAe7J^zh8Ez2>fYZYV1o|Wa3cvw~VLN;BQUDr&n?&?O#t=Zm ztHGb-SHf$?GbONwJC7rbDs^-rzzRFCk+3;Z0eA{haG=+LHF^8!>YND;vOg#g;KDr0 z8}SoA@e}yP*oTMC`^6UJ$4O3!ehV%kxCgf>2OY2mWrg#L7#)s0 z^=;32-dw#gGXo&5Mx_+sO}NbU*FTv)KnI$h;r7;GKqn`W<}xsp?Hti=M{%k$Ioaf` zTvyw9i41DryVvGF;_+l~Ao{1tOGm_*u6=GUDcsP`oH<(X3aE- zrE(Ev`81}$a_mP5nK0B_Hb2R=L2`bn)K833mMIsRm;scb++-lP zI*fFLZV(Od?)SZa%jl5OvRHi*-$!aK6<_P;Ri2uls{0IXm zNm$yML7c&tMKj+5YDmLI63=oXfN2!I!**hcxL5)mSZ!BrE)XLx3u_6c(UcryfHP%x zOVI~tqC5+fSEy6*tH1iIig-|=E?^U9CC@tN2?A3%5tA143X?r+G8k-j1N1PTaaX+ZMpT9 zo5|xlrfcc=3vYYcNtB(kRnU8h5e0HgoVPs+K`*_gf90z{w#bR$UJNxg}1ldKK&SAa6TNI&Is zWV@o)XpIkIJ`~Mk)A|PKpIQ~b8!0PZomsfV&YE*NmPi_Hm+p>koph~~fzO`YRj!B^ z4FraZjp!dvUNBw9m5{oe*K~3@wRLp0sYFY*=xphDyce($$cj=tO9n~s14~d_^uQJu1VZ?xF!VyjW&8T;C;p;ZDOf_TuP?qw zk+zJRGqWf7z=8%6h1J|!e&)>#h{RsN+rRd;ujMtCqY7-q&irIPP-sHanxu3XKv@-m zlUS{SY#2Z#IS59|Dk>*?kTB!sTKkYwp;@IczJ3e&7j$K#X>W8}?n>sunH}>q< zb;UK8Pu0D&C-a3l1Io{9ZVy@uXyFxMB(UVFu5Pq}h5@Q$VFXmJy|tE1B^=pmlX z-{vPhik}KZqf78v^F5DSI)p+1ijHCjek!U)-XeXS7ev@a(4hb;x_fj{>k1swx0FD~ zo;FvgDomsO>F~UF;20qi5tE#oXeV}{TXI%nA~DtsbXOe(-7K=t;H%idFu1lsP>wE2 zUzdV2AC%uNLX;jZvR29V0^D4(gc5XyXQOMCGi~0|UYb`$?Jj~=^jH;C+i=^RIdiri zagXwxiAFZiXJ)3=j&UUMS#U&yKEovt0U&~^>@WS&FEPF&^2QJkH3CL~C&$4-b}q|Y zf(e-3{_!9G@teQ-n*j%;#X=f^gpOH*R)P74_@QM2BcT1sa3Q#oh3Oyt(I1fx!`KRH z!4aVBS`ZFK!b*`GsVJ0fz5sF02 zLvTNZlW0}Lz3(leWYQe37&a^WYuY{T_rg6!`+rGc^E^+)@3Fo6uV;{-U%&-jhVYrG zG@2LQ4ziXLeJ0kZS}j`)Vb;F*iNafN-MVFnw6D3DDWeKggTM6Tm@~m!X$h7nEzJ)^ z)!p05SM90H%(o_HCpQgVHZ?QZj-xObJ9?sX;IKEarE=qr-O+)*W-@yD)mz96df=fJ zOi$7t8LSPq=1+#W!P>KZW&f((Ls#z^Wn%Am?Iko=VRSG_76}KLeH8c||1olyQqz9=A4C`3p(g8g-?+0)AvHN89(k6S0+ zNFQiNG$eQk(4*~~01PWj1)Yf+l%rXvszrjA=lUs}OCU^JBxbGqAaY0x6`hX}lu8%+ z^XY@;q6dm}gv(s$KUoQSZ4Zk5^X=Y-+wRPnvvrDl7}4atyK?cOx**W4X zNogTgfY%sDH9dWGTolgpHyuYKjgm)~(jeWPhlq3@B}gNElyrA@cZVQxgfs|9cXzk= z9Dcsf@BY1e-A=xDW_M>MPymsJoD>TlHBgLmkjCKRWjf zpVhzK{mzc#Gw^m?k8_>I!n{+STYEx~utOYOKAaf?8v7$7E`30UK6s3J0s zLysQ8ZZ96ZlOMI!z0(C+9&U5@v_uL+DBD9Kxx`%HhS3=2?VS#@i_F;^HD`G|-R;j( z2UTphr|sk09{FX);t(5wfXikljb{Pc^6v(0=WiqFof$nMhH>||TjbwY7CD@Y zkw(&3HsF3Z&8>YsE%YPHqcT&sm%MtJTUhv=?_Z@h`j&5lNNw^LZ>V4S1u)O4Kw;NXsi<6_P2FZ7>BZI|or_nY;8pt@-;l-_TjVBXO2 zRlY~+Bp(YqP`hZdGi|Oj@BTXIR4_&9|6YS?t2l3!z`5d%3;CJFRLqv@5%p3 zEa&fWN0!>eHR7=)4uyN8#{3?^tZe?? zMW%POUApg4Y(MmF(y^hG;M&FH$4csZ5#jKIjqX>IC;O8Yf}*;jt$%BKFY60BPQ|Oi zPjirU&O{n|RGZ53TfMKp;yWgN>^oPOZnZq_?+B7)ktaoZ)Ub5;R| zXXP}eTg=RRGe`l#QK8H#T}E@K zbcjaKfNf~DvvhDBxoXAbaaCOh7x<9)_HEU>etC(QT&zev38yUQ|ju4P?m%iM_G z2qSj#3Hqb|Oj!X>)Inn}u-i4_;tBYLoGjD=-nFDF_B|qI4;VEYYN(Zihl{d@=kEU+ zL7R!ltWnL>!7Ex%TBb~tt$*|ieK4JltqO|B*2mq)T@Up2Y@D!$zeOVT;EHt5^WxT? z{|d@h{;6bPp1+cW1Uu&xzcM?}O!#v?Fi>hEEW{lml9xP8PimguvNON1;xj>S;oI`V zasDRPrfTmmv>2ToTM%pWX#bG%#Ql2THD7pD$aTwI&6?+zNBw3Zv!qXm|Cmr;y`s5e ztExeh&99Zirf96Muk(qt6tjMKNgNlwQ=)2(v^lw9u{vfK=^v(DCpcvV8Yhs*%3^3| zo|IYQue_m4x_m&xm%o&mY2@Y!qtlRU90Wczxl?s&g~;viJ;^_F_FWuqT|?qy{Y#+k z&V&uB$@E()r6?Hzohktvg@8g7AAJcPCacYR8E>5rd339FuwHc?u zuL&le>btp&ZFSGh?K-2W^_q*wo6?Oj<|RRD#&C4p_=7!W<7;DYi?3Po>zO!>0>toO zPcxBz_h_#?%r+w!l6)z=r{4B7LaIkwLyv((qjLslYBiwY;0@J8JcwaI{M^=KW`XNTJ>n>EG}7UNnlfLcx!xM|GK+ISVyGg|GS3M+(VP$)@{E9zyxfGl z=xC-y>w9N>U*v|UG;>haBJcnO$R9R)Qs46HKAC`~)`D}Esw?#S+~Wu_JD}Bf)BJ*i z`r7@lzgS0}W7-5A_%O?dICef6d7e1afgXZQ+6*QHl9%tNA)Vw$5^UMGlrnUD|ZfR`?Wq%j~$?R*T2wt$rejtL&i? zq+Q}C8^oYEjhFn>Em4KBg|^0jAJ-!hNvG|VZy@B3C^EceeMfF5-Dj@SL%U#eKho$A zRh8VkXF*g0Qrrjv0=fY$H3Dfu-bNiWgAiALI<0HHKV>$oQv=R+!{Xzmk6Aw=h;?S~ zwOto(R=}oSt~(<-92lW!1|qF{qM(!6I#pJP+;rCtRtgn{&x80u`bNSH*yvk)p*_V| z(DDtUKhb^(Wg*O zK`w**Z{5^gloiGDJOkZ<_*G5ptgTeL>QhZ@!gwFznIfaO&!XB7wRzEs9t8;F6yQqga%){41EL^fhiR&G32 z1*{|f_&6O}C-(px?w?3-_(WCOIv5)y{&z6Qm19M(S9 zPl!-lzY$k5B(~b21Pdx%OFbGIGARO3w;LTHn<1PE`^-l;@>>0}(S`^~zyf+PiJ|fS z2t+Ni*OQJ=DotaFyLf2*j^=nGq-<1iFG&W?vuBy!Tp&oBWdSkZ&&KPuur`}yBJaZA zF$2p5o~DBjni*mqeOpA}5$2IIYowNX89UzPw68 z;+C^|TwT9lkG=H(R?!WDY$^%`&Gd>_;P*fb=NJ@Wk8lhu*B#qq!xcrzAF>$Y0)|T7 zse?x0v5xn(e{f(GLM&(jKm6HJ+)_`)5fwPT<-%*pjp78tllHo<;k8d9OhShSk)z;P zfsgcExl{^bn5)~WM~KgA?no3YDHMKqBtdLp`O{3%iB zf}ThGzWm*OrWnyH=7zKB^s0{YG&WO2BkHbCM~U+vILSO!WbBT!?1^=bM^a%?5Z3Kr zfW?QE&|Cbpawa@PB``;%us&d7@#K466I|YyKEn= z0H4-KOd1Wf2sh0(->0?WLAK|zB7Wabn@9xLrMkH>AzT>ety_0fzfvBECZZ=bd8;Ao zE%>iCB7Ovg#4V)UPI0U#k^=7X=i5-%rKX6nW9#f)(J;d;NGQh{jXmE=d%2bDB%ali zithtrS?|>I7DCd4e?N7*;fZ8nxI65O%(rz5OZx#KNc5(>?{tkUtR?#;3&fCSsx|_q ze&74a%1g`BodH9|Rvul<3!jdHY1=1tN*I zZV(7dcNn43VkBY!C0arY2e~q4zeD9OvR63CDD#Nb`9`XpgOe^IQ`g#AX<)jeE#mw) zO=~-*lxhk1PM{LK6W*W@!S`=4z9;sERuotGiC7|#eWN@P%P` zE&z>WKcOTnDNuC_BmR!OTk@!S&bpDc6;EU{58>^(_E_azUYICi;+YJ5nHaC_Ky6{q z4qy(XMPKrUjM$;Cxj=DvA3lrUiL9Fk)H{K2e{%*tJUK{=N4y^h4dHwg%bb9IV4!Cd;+|VJVSuMeC!|dCKN8?| zY@(k%-gUH%eo5TkoT&GQ#acI>3)8O;vO<-&T-BTP`hFryZJFY$Bbli`!72nk!ab|) zy~9Pq1O@_m2kQ?$!(mf_V*k?QJDjttaW+t4@oi5ia0YTU&A`sx^RPlUKAVKggpiJEj=az%nhA%=Yd5UaU($@qdI_cyIwb3OMgHcA5K8tkz*IPP-N zP=J*_7;Geu^7#mCzLBhif@{F0<`xqaM3SwQKVL@UhF143^z8HYy6#eHzr14EB%JB$ z6M#tePxWhED>x1uGv4P9_&dwWhNhhZu|h955#&pz8wr!Thn5~xxBOa`D#gnLaWN>q z>u?Yjp&#pS)n&i;#l!u%K+v4%{?BQ$-Ph9H^Ea%cr!Q>8eF-Ra?J5Px&36fq_g{eIl*P zP6oJpk6@ui@O4H!I5R*Icp9er`>=J3(@lK9W6#p|K&pO0d$_lpUF8t_MBY|LB$_8A zBKuuZH4I|ktM4^lro54e-`wbYFmA=C;}u?^KPz@`!VEH9GL}IU!~_tgcHLRhLYO6y z%yWiBY&k?=(k!@quBp z;ZYpLhZHL6lZ;3d&L2Akk&&jb%m3HTTv)hr^dATLdpJX7LJBkv+8)TR{}MJgZ;ILE zPE4;3a_4mLI}-}QPD@*My;7m*(jCxD=#~c0P9n|8$+H0EZ982$wB%fJ{+b`TTlh@o z{lG&Jn4>ZsaXWyZd;03=X-TOlOYDNGk!q?yt4WJsD&ohx7KX-LQTZ%%eKcTElWkI9 zbbCyU$XuGSg@JNJfh7$l)IKrl)tSz#?~90o5fOHIb0O$G7+C0UKBn$7Uf>Tv|5cfM z<=g-f0Df7|zCWHVip>Sujq~ojIT4`T^l@VY6ButrQ8>o7{yW3CXjq$6El^V(8gd z%#?FJ`*x{!qSoQ`$^qt!p4Clk)NJL<~Y1H*y)0)-acgU`NC$`giVfI6_vSIKx*w zM6N~+zTOyNACvIFc?}Ys9p{DPV+F@9kn5|Xw2w=@Zi*X7WW?`TGr<+ zRfC*Hnll+pp8nek-Fpzq|MKum#cqR zDRn37-6g4bMc$lJJWRx^w0R2E-r-f2F86ZV>~qlvVPH(8{oN7XkbCQ?1CMGOb?YKx;_lb zmYmWqZH%U!M=b4U`$q^y0CcN|T`UC-EMm-b7mF6SfS4t#!j+ipqjPz`b-8#oUarRX>&%3q936>^Lz|6egss`kU&RTy#6ck;Zt8g>Wi&h-*aF)XtG`W34CJ}xy)rM;2rO*f{(2g8 zqnNj-dGS3cqoT&9I_ghyyrRIg&W4PG^LS97q*W`3?YHiZ!!OiNZ(e0l1kDGaCq>a> zWBzl#R-G_9v|DObCGp_*CoP)_rtLVCB1Lc*JD+x}Mq8R3`wlj!*;}1cDLmdbVa~@e z)eD+VjIT|g!TLn3)5=Cs1i~*gnJ=DAlrRa|5p^>4?X>s4j&SPl0KfzW3r^OvoWxa; z2(*d=XR4Ol@1OSaqfb2Vj2+utPnMXx&FUf(Y^)EeH1+c8+nDaA;tNmD2RDF>iPRGZ zl})7zva&NN7Qd(BvWSZ93MLUp$bqLZ^iSPju@4v64qtd+HX_nVxDnEj z-t-w~h!Da1r1dWZm10FTlNI0;XEx2pjkz5#HGVW=>%02Gj&Podc@AHb)vu)`A@vM? z2ejY9j#nfuZ|||Yd^S=j!T(tLGM-9aN1CXC zTr`lxqAJVxnUsW#8sFT@%gfs1A@>g@!?+?{IY_MGooGkd4TrGk?;&b5T#F?wJF4zs zr0DXixhVa1j>THi63uM;JW(b7V%6&)pmgV37Xh~a6PaCN=d(<^NJDse*efIVqOAf?xkHS1pZ!Y}{y5zb6RLp~BK%&ch|TD#~w!->Fb| zFB9%%vuSok%&dxB^igZWu?mL$|Kde3ALTdkdX=6pOTV>Crx8FFwuw_W6KbMXeMM46 zaU*;tEV&ri%ygykgzsP3G%vu&@At^Z9s+ih&+yKWFw^NqF9BDPk6ZKspY2=?pwlol zywKjVtG6(sSFTr@R6c0o9M@)8de``@G0m<#%1vX#xU}*09PwGI3GcFb^Ys{1Z5wed zP@P|oyy2i>$1nkOP|q-ETMd*^ASo)IUA3nR`Yev(8(ThX?(&c57i@Zf+pg0(yq%af z{q%WDV$@tWPtljtyyhqLB*nJ_iCa%-sI*esJ5Qmpdj0?|WTL!K5EiqXfA$`AmiKce znmiIK{{M8c+BVvi7sxKzMROM^s#)5aTWXYE;d(`+-(Wbp%;SS#vsPMXu=oDOXD`)d z+1CKs)~h_wht3TH=M?U6WDk5%+@*K1AQq}DXz7gLi&$a5%lHg;xV_7%HjprK1(=Bc zOLwLx^b)+j$Oi2Fsk$v7Ijiz0H4gXEkt@`(Q!Q%ShgG-0aU)n_*uD(I8g`xqlO*cXiHd-y-5z~Bf^1^MCN2q|F306p!(w2++g zdt;Lf1_%xK&k`bGR=Lu?vsOBXf$lbPa(q#8DSAvlay{)_0XRXs^lWsNo5z=}c&FQn zVLJqQKPWKOn5_7N#iZo&RDK%?n4*|b#ceqR?tVWnU4Yd3MF5LmUZp2p&G(21SO4;K zmSKS1H}HYJ4T+5-s0{v{C|or?hYAXPjSo&ri&wh%rQ(HbhS5eRojoGbF39jAzozrq z%jceOW>jt~yr@D2qZc_pH*)D5snrZn+--8c9`K5-%{d)Uw| zAsSiO%H+C8leS||*P7wbN~$}2zqU{Nm$O5&^FI}l8+o6Y7uNl;#Y^829Uo3;@aP;c zM0+KF{B3%an;h=SELtgNVE9Sb=Ufd#r~#Y9NFYV!f=d;kPc!@gtQ!3O&m`U#R;HcG zJ7y*~pl)y4WV?L89KDhd z^#=!PV&W-x#@utPLK&N%w~{7-Y;EW9(?gd)`MkQq+WDjE_j3p7e%rv>pneVMwtI{j z=#x^2;CY+l!avlRmq{yj%!nxp=Ov4XEh-{|KXl!BTW%az1a21-mF%!6%A3OhJo_;* zeDq@O0}`~jhhr>dgQ%GKz%X0p+PT`E98BpmRKBR=5CUC8G zDw|g59BS8q5bSeA(e}c#{%efeOXKo>VI{7iUHm$9+h-jU37-Y&*TLDU*^ty~n%g7x zf>dy9N`%X04(lvaObIf9Or}8OSF`Si=}wgRT%d9_*IuLn>YaQW2_?trxA|)0gOC4& z)XqP9{NV@GBL#Xr7s_%yJ{IjvpCdIf5GS1G?jKXP>iTdbVRa8oYrDP;YUl#F#PCK> zj7!YBz{bMkB!i7(y+aO9o|sgPz4Uj;Hm2JSg*@pX*{3yfE%Q#3uzEt+i`m#Uig8h# zjPZj_&m)fodMsGyPo?*+yjWqOWuDYoZFgC)mx>YD7|yTKE_CH^NFHQ=t`J=nK$I;| zL4k)CPrX!|u1_(~773mj^29rT6qZM{-(d#~>ifQcRDF22M!08P$cEEyEPp`Dj?!Tv z`2&d}zyK^CW3@-Ig51cOW~hJ8emz1{uI8pR_4=2h$>OU*U&6GFj4 z=7tc^#v7h;wFEfPZc+|??dQkJK^S6qG_ ztrEvsj|RY|`9Z_3$%d_rH`%R;y)n4Kvzd?#eb~pxwG$$ulUYk)AL^EMF!ygKtLJH= z+TW7PC)x6jZ)$g5 z{T~WcaxMKT&$Zkx@)~b_JDZ;x#&C!XVY}%2Z|eddqTOD7O1r&qg0<2RIhIgoUB2s| ze97NoP}8xQWdKW(`s<9m8J`5|2L7Q+rOi_ORmkaiKE=JBimaXfLYqGsd*SeZ-{GC? zZ`)8k=j*ZakwtzeN9OCYD<1PY;jwSMw>8yiUuLJ^drfhH1m203~Pj6BO7--=loz6jUeBP?WB!|}#1iJ@s9r@ Y1|oM}3QZ`J!ofcB(kfC_5=Q?24@*C&^Z)<= literal 0 HcmV?d00001 diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/README.md similarity index 54% rename from blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md rename to blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/README.md index 35198e8d..a7dfac57 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md +++ b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/README.md @@ -1,10 +1,14 @@ -# GCP Workload Identity Provider for Terraform Enterprise +# GCP Workload Identity Provider for Terraform Cloud Dynamic Credentials -This terraform code is a part of [GCP Workload Identity Federation for Terraform Enterprise](../) blueprint. +This terraform code is a part of [GCP Workload Identity Federation for Terraform Cloud](../) blueprint. The codebase provisions the following list of resources: -- GCS Bucket +- (optional) GCP Project +- IAM Service Account +- Workload Identity Pool +- Workload Identity Provider +- IAM Permissins ## Variables @@ -13,21 +17,19 @@ The codebase provisions the following list of resources: |---|---|:---:|:---:|:---:| | [billing_account](variables.tf#L16) | Billing account id used as default for new projects. | string | ✓ | | | [project_id](variables.tf#L43) | Existing project id. | string | ✓ | | -| [tfe_organization_id](variables.tf#L48) | TFE organization id. | string | ✓ | | -| [tfe_workspace_id](variables.tf#L53) | TFE workspace id. | string | ✓ | | -| [issuer_uri](variables.tf#L21) | Terraform Enterprise uri. Replace the uri if a self hosted instance is used. | string | | "https://app.terraform.io/" | +| [tfc_organization_id](variables.tf#L48) | TFC organization id. | string | ✓ | | +| [tfc_workspace_id](variables.tf#L53) | TFC workspace id. | string | ✓ | | +| [issuer_uri](variables.tf#L21) | Terraform Cloud/Enterprise uri. Replace the uri if a self hosted instance is used. | string | | "https://app.terraform.io/" | | [parent](variables.tf#L27) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | | [project_create](variables.tf#L37) | Create project instead of using an existing one. | bool | | true | -| [workload_identity_pool_id](variables.tf#L58) | Workload identity pool id. | string | | "tfe-pool" | -| [workload_identity_pool_provider_id](variables.tf#L64) | Workload identity pool provider id. | string | | "tfe-provider" | +| [workload_identity_pool_id](variables.tf#L58) | Workload identity pool id. | string | | "tfc-pool" | +| [workload_identity_pool_provider_id](variables.tf#L64) | Workload identity pool provider id. | string | | "tfc-provider" | ## Outputs | name | description | sensitive | |---|---|:---:| -| [impersonate_service_account_email](outputs.tf#L16) | Service account to be impersonated by workload identity. | | -| [project_id](outputs.tf#L21) | GCP Project ID. | | -| [workload_identity_audience](outputs.tf#L26) | TFC Workload Identity Audience. | | -| [workload_identity_pool_provider_id](outputs.tf#L31) | GCP workload identity pool provider ID. | | +| [project_id](outputs.tf#L15) | GCP Project ID. | | +| [tfc_workspace_wariables](outputs.tf#L20) | Variables to be set on the TFC workspace. | | diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/main.tf similarity index 77% rename from blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf rename to blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/main.tf index 5ced2e3c..e4275350 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf +++ b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/main.tf @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -36,25 +36,27 @@ module "project" { # Workload Identity Pool and Provider # ############################################################################### -resource "google_iam_workload_identity_pool" "tfe-pool" { +resource "google_iam_workload_identity_pool" "tfc-pool" { project = module.project.project_id workload_identity_pool_id = var.workload_identity_pool_id - display_name = "TFE Pool" - description = "Identity pool for Terraform Enterprise OIDC integration" + display_name = "TFC Pool" + description = "Identity pool for Terraform Cloud Dynamic Credentials integration" } -resource "google_iam_workload_identity_pool_provider" "tfe-pool-provider" { +resource "google_iam_workload_identity_pool_provider" "tfc-pool-provider" { project = module.project.project_id - workload_identity_pool_id = google_iam_workload_identity_pool.tfe-pool.workload_identity_pool_id + workload_identity_pool_id = google_iam_workload_identity_pool.tfc-pool.workload_identity_pool_id workload_identity_pool_provider_id = var.workload_identity_pool_provider_id - display_name = "TFE Pool Provider" - description = "OIDC identity pool provider for TFE Integration" - # Use condition to make sure only token generated for a specific TFE Org can be used across org workspaces - attribute_condition = "attribute.terraform_organization_id == \"${var.tfe_organization_id}\"" + display_name = "TFC Pool Provider" + description = "OIDC identity pool provider for Terraform Cloud Dynamic Credentials integration" + # Use condition to make sure only token generated for a specific TFC Org can be used across org workspaces + attribute_condition = "attribute.terraform_organization_id == \"${var.tfc_organization_id}\"" attribute_mapping = { "google.subject" = "assertion.sub" "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" @@ -72,15 +74,15 @@ resource "google_iam_workload_identity_pool_provider" "tfe-pool-provider" { # Service Account and IAM bindings # ############################################################################### -module "sa-tfe" { +module "sa-tfc" { source = "../../../../modules/iam-service-account" project_id = module.project.project_id - name = "sa-tfe" + name = "sa-tfc" iam = { - # We allow only tokens generated by a specific TFE workspace impersonation of the service account, - # that way one identity pool can be used for a TFE Organization, but every workspace will be able to impersonate only a specifc SA - "roles/iam.workloadIdentityUser" = ["principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.tfe-pool.name}/attribute.terraform_workspace_id/${var.tfe_workspace_id}"] + # We allow only tokens generated by a specific TFC workspace impersonation of the service account, + # that way one identity pool can be used for a TFC Organization, but every workspace will be able to impersonate only a specifc SA + "roles/iam.workloadIdentityUser" = ["principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.tfc-pool.name}/attribute.terraform_workspace_id/${var.tfc_workspace_id}"] } iam_project_roles = { diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/outputs.tf similarity index 53% rename from blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf rename to blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/outputs.tf index 46d7f6b0..e38a4da6 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf +++ b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/outputs.tf @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,23 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. - -output "impersonate_service_account_email" { - description = "Service account to be impersonated by workload identity." - value = module.sa-tfe.email -} - output "project_id" { description = "GCP Project ID." value = module.project.project_id } -output "workload_identity_audience" { - description = "TFC Workload Identity Audience." - value = "//iam.googleapis.com/${google_iam_workload_identity_pool_provider.tfe-pool-provider.name}" -} - -output "workload_identity_pool_provider_id" { - description = "GCP workload identity pool provider ID." - value = google_iam_workload_identity_pool_provider.tfe-pool-provider.name +output "tfc_workspace_wariables" { + description = "Variables to be set on the TFC workspace." + value = { + TFC_GCP_PROVIDER_AUTH = "true", + TFC_GCP_PROJECT_NUMBER = module.project.number, + TFC_GCP_WORKLOAD_POOL_ID = google_iam_workload_identity_pool.tfc-pool.workload_identity_pool_id, + TFC_GCP_WORKLOAD_PROVIDER_ID = google_iam_workload_identity_pool_provider.tfc-pool-provider.workload_identity_pool_provider_id, + TFC_GCP_RUN_SERVICE_ACCOUNT_EMAIL = module.sa-tfc.email + } } diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/variables.tf similarity index 83% rename from blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf rename to blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/variables.tf index 3719b183..3d4da658 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf +++ b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider/variables.tf @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ variable "billing_account" { } variable "issuer_uri" { - description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." + description = "Terraform Cloud/Enterprise uri. Replace the uri if a self hosted instance is used." type = string default = "https://app.terraform.io/" } @@ -45,24 +45,24 @@ variable "project_id" { type = string } -variable "tfe_organization_id" { - description = "TFE organization id." +variable "tfc_organization_id" { + description = "TFC organization id." type = string } -variable "tfe_workspace_id" { - description = "TFE workspace id." +variable "tfc_workspace_id" { + description = "TFC workspace id." type = string } variable "workload_identity_pool_id" { description = "Workload identity pool id." type = string - default = "tfe-pool" + default = "tfc-pool" } variable "workload_identity_pool_provider_id" { description = "Workload identity pool provider id." type = string - default = "tfe-provider" + default = "tfc-provider" } diff --git a/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/README.md b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/README.md new file mode 100644 index 00000000..262472d0 --- /dev/null +++ b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/README.md @@ -0,0 +1,16 @@ +# Test GCP Workload Identity Provider for Terraform Dynamic Credentials + +This terraform code is a part of [GCP Workload Identity Federation for Terraform Cloud](../) blueprint. For instructions please refer to the blueprint [readme](../README.md). + +The codebase provisions the following list of resources: + +- GCS Bucket + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [project_id](variables.tf#L15) | GCP project ID. | string | ✓ | | + + diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/backend.tf.template similarity index 89% rename from blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template rename to blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/backend.tf.template index 87d4737d..01781fe9 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template +++ b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/backend.tf.template @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,10 +18,10 @@ terraform { backend "remote" { - organization = "" + organization = "" workspaces { - name = "" + name = "" } } diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/main.tf b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/main.tf similarity index 91% rename from blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/main.tf rename to blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/main.tf index 5e03ada5..692447d1 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/main.tf +++ b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/main.tf @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ resource "google_storage_bucket" "test-bucket" { project = var.project_id - name = "${var.project_id}-tfe-oidc-test-bucket" + name = "${var.project_id}-test" location = "US" force_destroy = true } diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/provider.tf similarity index 74% rename from blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh rename to blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/provider.tf index 2f7e30a2..cc83fd71 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh +++ b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/provider.tf @@ -1,5 +1,4 @@ -#!/bin/bash -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,11 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Exit if any of the intermediate steps fail -set -e - -FILENAME=$@ - -echo $TFC_WORKLOAD_IDENTITY_TOKEN > $FILENAME - -echo -n "{\"file\":\"${FILENAME}\"}" +provider "google" {} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/terraform.auto.tfvars.template similarity index 75% rename from blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template rename to blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/terraform.auto.tfvars.template index fc2811db..55bfe82e 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template +++ b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/terraform.auto.tfvars.template @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,5 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -project_id = "tfe-oidc-workflow" -impersonate_service_account_email = "sa-tfe@tfe-oidc-workflow2.iam.gserviceaccount.com" +project_id = "tfc-dynamic-creds-gcp" diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/get_audience.sh b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/variables.tf similarity index 81% rename from blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/get_audience.sh rename to blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/variables.tf index 251fe321..3fc54afb 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/get_audience.sh +++ b/blueprints/cloud-operations/terraform-cloud-dynamic-credentials/tfc-workflow-using-wif/variables.tf @@ -1,4 +1,3 @@ -#!/bin/bash # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,11 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Exit if any of the intermediate steps fail -set -e - -cat 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

w%=&~_s@(isigS^wDyk9P9`wMxNtgc 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 - -## Variables - -| name | description | type | required | default | -|---|---|:---:|:---:|:---:| -| [impersonate_service_account_email](variables.tf#L16) | Service account to be impersonated by workload identity. | string | ✓ | | -| [project_id](variables.tf#L21) | GCP project ID. | string | ✓ | | - - diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf deleted file mode 100644 index ae132fd4..00000000 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2022 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 -# -# https://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. - - -module "tfe_oidc" { - source = "./tfc-oidc" - - impersonate_service_account_email = var.impersonate_service_account_email -} - -provider "google" { - credentials = module.tfe_oidc.credentials -} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md deleted file mode 100644 index fd869ae1..00000000 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Terraform Enterprise OIDC Credential for GCP Workload Identity Federation - -This is a helper module to prepare GCP Credentials from Terraform Enterprise workload identity token. For more information see [Terraform Enterprise Workload Identity Federation](../) blueprint. - -## Example -```hcl -module "tfe_oidc" { - source = "./tfc-oidc" - - impersonate_service_account_email = "tfe-test@tfe-test-wif.iam.gserviceaccount.com" -} - -provider "google" { - credentials = module.tfe_oidc.credentials -} - -provider "google-beta" { - credentials = module.tfe_oidc.credentials -} - -# tftest skip -``` - - -## Variables - -| name | description | type | required | default | -|---|---|:---:|:---:|:---:| -| [impersonate_service_account_email](variables.tf#L17) | Service account to be impersonated by workload identity federation. | string | ✓ | | -| [tmp_oidc_token_path](variables.tf#L22) | Name of the temporary file where TFC OIDC token will be stored to authentificate terraform provider google. | string | | ".oidc_token" | - -## Outputs - -| name | description | sensitive | -|---|---|:---:| -| [credentials](outputs.tf#L17) | Credentials in format to pass the to gcp provider. | | - - diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf deleted file mode 100644 index f40b8459..00000000 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright 2022 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. - */ - -data "external" "oidc_token_file" { - program = ["bash", "${path.module}/write_token.sh", "${var.tmp_oidc_token_path}"] -} - -data "external" "workload_identity_pool" { - program = ["bash", "${path.module}/get_audience.sh"] -} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf deleted file mode 100644 index a642b850..00000000 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright 2022 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 "credentials" { - description = "Credentials in format to pass the to gcp provider." - value = jsonencode({ - "type" : "external_account", - "audience" : data.external.workload_identity_pool.result.audience, - "subject_token_type" : "urn:ietf:params:oauth:token-type:jwt", - "token_url" : "https://sts.googleapis.com/v1/token", - "credential_source" : data.external.oidc_token_file.result - "service_account_impersonation_url" : "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${var.impersonate_service_account_email}:generateAccessToken" - }) -} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.tf deleted file mode 100644 index 05645314..00000000 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.tf +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright 2022 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 "impersonate_service_account_email" { - description = "Service account to be impersonated by workload identity federation." - type = string -} - -variable "tmp_oidc_token_path" { - description = "Name of the temporary file where TFC OIDC token will be stored to authentificate terraform provider google." - type = string - default = ".oidc_token" -} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf deleted file mode 100644 index 08492c6f..00000000 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2022 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 -# -# https://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. - -terraform { - required_version = ">= 1.3.1" - required_providers { - google = { - source = "hashicorp/google" - version = ">= 4.50.0" # tftest - } - google-beta = { - source = "hashicorp/google-beta" - version = ">= 4.50.0" # tftest - } - } -} - - diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf deleted file mode 100644 index bc9ca9f8..00000000 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2022 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 -# -# https://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 "impersonate_service_account_email" { - description = "Service account to be impersonated by workload identity." - type = string -} - -variable "project_id" { - description = "GCP project ID." - type = string -} diff --git a/tests/blueprints/cloud_operations/terraform_enterprise_wif/__init__.py b/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/__init__.py similarity index 100% rename from tests/blueprints/cloud_operations/terraform_enterprise_wif/__init__.py rename to tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/__init__.py diff --git a/tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/__init__.py b/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/__init__.py similarity index 100% rename from tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/__init__.py rename to tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/__init__.py diff --git a/tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/fixture/main.tf b/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/fixture/main.tf similarity index 81% rename from tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/fixture/main.tf rename to tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/fixture/main.tf index 3552740c..b926bf2a 100644 --- a/tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/fixture/main.tf +++ b/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/fixture/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,13 @@ */ module "test" { - source = "../../../../../../blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider" + source = "../../../../../../blueprints/cloud-operations/terraform-cloud-dynamic-credentials/gcp-workload-identity-provider" billing_account = var.billing_account project_create = var.project_create project_id = var.project_id parent = var.parent - tfe_organization_id = var.tfe_organization_id - tfe_workspace_id = var.tfe_workspace_id + tfc_organization_id = var.tfe_organization_id + tfc_workspace_id = var.tfe_workspace_id workload_identity_pool_id = var.workload_identity_pool_id workload_identity_pool_provider_id = var.workload_identity_pool_provider_id issuer_uri = var.issuer_uri diff --git a/tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/fixture/variables.tf b/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/fixture/variables.tf similarity index 83% rename from tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/fixture/variables.tf rename to tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/fixture/variables.tf index d99981c0..8d7c2719 100644 --- a/tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/fixture/variables.tf +++ b/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/fixture/variables.tf @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -37,14 +37,14 @@ variable "parent" { } } -variable "tfe_organization_id" { - description = "TFE organization id." +variable "tfc_organization_id" { + description = "TFC organization id." type = string default = "org-123" } -variable "tfe_workspace_id" { - description = "TFE workspace id." +variable "tfc_workspace_id" { + description = "TFC workspace id." type = string default = "ws-123" } @@ -52,17 +52,17 @@ variable "tfe_workspace_id" { variable "workload_identity_pool_id" { description = "Workload identity pool id." type = string - default = "tfe-pool" + default = "tfc-pool" } variable "workload_identity_pool_provider_id" { description = "Workload identity pool provider id." type = string - default = "tfe-provider" + default = "tfc-provider" } variable "issuer_uri" { - description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." + description = "Terraform Cloud uri. Replace the uri if a self hosted instance is used." type = string default = "https://app.terraform.io/" } diff --git a/tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/test_plan.py b/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/test_plan.py similarity index 100% rename from tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/test_plan.py rename to tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/test_plan.py From 4ad30b812bc3cb30fa0282c322ca78bde547447b Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Sun, 19 Feb 2023 01:14:24 +0100 Subject: [PATCH 19/47] Fix tests for tf-cloud-dynamic-credentials blueprint --- .../__init__.py | 0 .../gcp_workload_identity_provider/__init__.py | 0 .../gcp_workload_identity_provider/fixture/main.tf | 4 ++-- .../gcp_workload_identity_provider/fixture/variables.tf | 0 .../gcp_workload_identity_provider/test_plan.py | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename tests/blueprints/cloud_operations/{terraform-cloud-dynamic-credentials => terraform_cloud_dynamic_credentials}/__init__.py (100%) rename tests/blueprints/cloud_operations/{terraform-cloud-dynamic-credentials => terraform_cloud_dynamic_credentials}/gcp_workload_identity_provider/__init__.py (100%) rename tests/blueprints/cloud_operations/{terraform-cloud-dynamic-credentials => terraform_cloud_dynamic_credentials}/gcp_workload_identity_provider/fixture/main.tf (90%) rename tests/blueprints/cloud_operations/{terraform-cloud-dynamic-credentials => terraform_cloud_dynamic_credentials}/gcp_workload_identity_provider/fixture/variables.tf (100%) rename tests/blueprints/cloud_operations/{terraform-cloud-dynamic-credentials => terraform_cloud_dynamic_credentials}/gcp_workload_identity_provider/test_plan.py (100%) diff --git a/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/__init__.py b/tests/blueprints/cloud_operations/terraform_cloud_dynamic_credentials/__init__.py similarity index 100% rename from tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/__init__.py rename to tests/blueprints/cloud_operations/terraform_cloud_dynamic_credentials/__init__.py diff --git a/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/__init__.py b/tests/blueprints/cloud_operations/terraform_cloud_dynamic_credentials/gcp_workload_identity_provider/__init__.py similarity index 100% rename from tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/__init__.py rename to tests/blueprints/cloud_operations/terraform_cloud_dynamic_credentials/gcp_workload_identity_provider/__init__.py diff --git a/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/fixture/main.tf b/tests/blueprints/cloud_operations/terraform_cloud_dynamic_credentials/gcp_workload_identity_provider/fixture/main.tf similarity index 90% rename from tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/fixture/main.tf rename to tests/blueprints/cloud_operations/terraform_cloud_dynamic_credentials/gcp_workload_identity_provider/fixture/main.tf index b926bf2a..800201f0 100644 --- a/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/fixture/main.tf +++ b/tests/blueprints/cloud_operations/terraform_cloud_dynamic_credentials/gcp_workload_identity_provider/fixture/main.tf @@ -20,8 +20,8 @@ module "test" { project_create = var.project_create project_id = var.project_id parent = var.parent - tfc_organization_id = var.tfe_organization_id - tfc_workspace_id = var.tfe_workspace_id + tfc_organization_id = var.tfc_organization_id + tfc_workspace_id = var.tfc_workspace_id workload_identity_pool_id = var.workload_identity_pool_id workload_identity_pool_provider_id = var.workload_identity_pool_provider_id issuer_uri = var.issuer_uri diff --git a/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/fixture/variables.tf b/tests/blueprints/cloud_operations/terraform_cloud_dynamic_credentials/gcp_workload_identity_provider/fixture/variables.tf similarity index 100% rename from tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/fixture/variables.tf rename to tests/blueprints/cloud_operations/terraform_cloud_dynamic_credentials/gcp_workload_identity_provider/fixture/variables.tf diff --git a/tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/test_plan.py b/tests/blueprints/cloud_operations/terraform_cloud_dynamic_credentials/gcp_workload_identity_provider/test_plan.py similarity index 100% rename from tests/blueprints/cloud_operations/terraform-cloud-dynamic-credentials/gcp_workload_identity_provider/test_plan.py rename to tests/blueprints/cloud_operations/terraform_cloud_dynamic_credentials/gcp_workload_identity_provider/test_plan.py From f16511b8da56c60c19b0c4439f906d3d1950c241 Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Sun, 19 Feb 2023 15:22:48 +0100 Subject: [PATCH 20/47] Fix typo in readme --- modules/projects-data-source/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/projects-data-source/README.md b/modules/projects-data-source/README.md index 8fcfad96..5d35f1ab 100644 --- a/modules/projects-data-source/README.md +++ b/modules/projects-data-source/README.md @@ -19,8 +19,8 @@ module "my-org" { parent = "organizations/123456789" } -output "projects" { - value = module.my-org.projects_numbers +output "project_numbers" { + value = module.my-org.project_numbers } # tftest skip (uses data sources) From 5cf60cbcf478ae3433dc28d4f20a14ba46ff42de Mon Sep 17 00:00:00 2001 From: Anton KOVACH <2207136+antonkovach@users.noreply.github.com> Date: Sun, 19 Feb 2023 18:01:38 +0100 Subject: [PATCH 21/47] Fix Terraform formatting and add module_prefix attribute to modules_config (#1162) * Fix Terraform formatting and add module/ prefix to path in 0-cicd-github repository population fix the formatting of Terraform files and adds the module/ prefix to the module path in 0-cicd-github under repository population. Without proper formatting and module path, generated repositories may show formatting mismatches and examples in the README.md file may not run as expected. The changes include updating the replace function with a new regular expression pattern to correctly apply the git source for modules and updating the each.value.file attribute to include the module/ prefix in the Terraform file path. This ensures that the examples in the README.md file work as intended and that the generated repositories follow best practices for Terraform code. * revert modules/ prefix change * Add module_prefix to modules_config - Add module_prefix to modules_config - Add example to Readme.md - use module_prefix variable to specify the path * fix tfdoc --- fast/extras/0-cicd-github/README.md | 16 +++++++++++++--- fast/extras/0-cicd-github/main.tf | 5 +++-- fast/extras/0-cicd-github/variables.tf | 1 + 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/fast/extras/0-cicd-github/README.md b/fast/extras/0-cicd-github/README.md index 58407b5e..0bd0b5be 100644 --- a/fast/extras/0-cicd-github/README.md +++ b/fast/extras/0-cicd-github/README.md @@ -39,6 +39,16 @@ modules_config = { # tftest skip ``` +If the modules are located in a non modules only repository, use the module_prefix attribute to set the location of your modules within the repository: + +```hcl +modules_config = { + repository_name = "GoogleCloudPlatform/cloud-foundation-fabric" + module_prefix = "modules/" +} +# tftest skip +``` + In the above example, no key options are set so it's assumed modules will be fetched from a public repository. If modules repository authentication is needed the `key_config` attribute also needs to be set. If no keypair path is specified an internally generated key will be stored as an access key in the modules repository, and as secrets in the stage repositories: @@ -125,10 +135,10 @@ Finally, a `commit_config` variable is optional: it can be used to configure aut | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization](variables.tf#L50) | GitHub organization. | string | ✓ | | +| [organization](variables.tf#L51) | GitHub organization. | string | ✓ | | | [commmit_config](variables.tf#L17) | Configure commit metadata. | object({…}) | | {} | -| [modules_config](variables.tf#L28) | Configure access to repository module via key, and replacement for modules sources in stage repositories. | object({…}) | | null | -| [repositories](variables.tf#L55) | Repositories to create. | map(object({…})) | | {} | +| [modules_config](variables.tf#L28) | Configure access to repository module via key, and replacement for modules sources in stage repositories. | object({…}) | | null | +| [repositories](variables.tf#L56) | Repositories to create. | map(object({…})) | | {} | ## Outputs diff --git a/fast/extras/0-cicd-github/main.tf b/fast/extras/0-cicd-github/main.tf index d91ab970..9cf319bb 100644 --- a/fast/extras/0-cicd-github/main.tf +++ b/fast/extras/0-cicd-github/main.tf @@ -18,6 +18,7 @@ locals { _repository_files = flatten([ for k, v in var.repositories : [ for f in concat( + [for f in fileset(path.module, "${v.populate_from}/*.svg") : f], [for f in fileset(path.module, "${v.populate_from}/*.md") : f], [for f in fileset(path.module, "${v.populate_from}/*.tf") : f] ) : { @@ -143,8 +144,8 @@ resource "github_repository_file" "default" { endswith(each.value.name, ".tf") && local.modules_repo != null ? replace( file(each.value.file), - "/source\\s*=\\s*\"../../../modules/([^/\"]+)\"/", - "source = \"git@github.com:${local.modules_repo}.git//$1${local.modules_ref}\"" # " + "/source(\\s*)=\\s*\"../../../modules/([^/\"]+)\"/", + "source$1= \"git@github.com:${local.modules_repo}.git//${local.module_prefix}$2${local.modules_ref}\"" # " ) : file(each.value.file) ) diff --git a/fast/extras/0-cicd-github/variables.tf b/fast/extras/0-cicd-github/variables.tf index 8e5d0832..ea378ee7 100644 --- a/fast/extras/0-cicd-github/variables.tf +++ b/fast/extras/0-cicd-github/variables.tf @@ -30,6 +30,7 @@ variable "modules_config" { type = object({ repository_name = string source_ref = optional(string) + module_prefix = optional(string, "") key_config = optional(object({ create_key = optional(bool, false) create_secrets = optional(bool, false) From 5905903d6e69a0cebf6611a354a3b94680b18ff6 Mon Sep 17 00:00:00 2001 From: Anton KOVACH <2207136+antonkovach@users.noreply.github.com> Date: Sun, 19 Feb 2023 19:22:42 +0100 Subject: [PATCH 22/47] fix module_prefix (#1164) Add module_prefix to locals --- fast/extras/0-cicd-github/main.tf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fast/extras/0-cicd-github/main.tf b/fast/extras/0-cicd-github/main.tf index 9cf319bb..3c42b5cf 100644 --- a/fast/extras/0-cicd-github/main.tf +++ b/fast/extras/0-cicd-github/main.tf @@ -33,7 +33,8 @@ locals { ? "" : "?ref=${var.modules_config.source_ref}" ) - modules_repo = try(var.modules_config.repository_name, null) + modules_repo = try(var.modules_config.repository_name, null) + module_prefix = try(var.modules_config.module_prefix, null) repositories = { for k, v in var.repositories : k => v.create_options == null ? k : github_repository.default[k].name From ad6667a8fa658cbc8725e17436473fb7ab7357df Mon Sep 17 00:00:00 2001 From: lcaggio Date: Sun, 19 Feb 2023 22:37:32 +0100 Subject: [PATCH 23/47] First commit --- .../data-platform-foundations/01-dropoff.tf | 7 ++-- .../data-platform-foundations/02-load.tf | 7 ++-- .../03-orchestration.tf | 7 ++-- .../04-transformation.tf | 7 ++-- .../05-datawarehouse.tf | 21 +++++++----- .../data-platform-foundations/06-common.tf | 7 ++-- .../data-platform-foundations/07-exposure.tf | 7 ++-- .../data-platform-foundations/variables.tf | 34 +++++++++++++++++++ 8 files changed, 70 insertions(+), 27 deletions(-) diff --git a/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf b/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf index 177f940a..f1dc492e 100644 --- a/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf +++ b/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf @@ -23,9 +23,10 @@ locals { module "drop-project" { source = "../../../modules/project" parent = var.folder_id - billing_account = var.billing_account_id - prefix = var.prefix - name = "drp${local.project_suffix}" + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.drop : "${var.project_config.project_ids.drop}${local.project_suffix}" group_iam = { (local.groups.data-engineers) = [ "roles/bigquery.dataEditor", diff --git a/blueprints/data-solutions/data-platform-foundations/02-load.tf b/blueprints/data-solutions/data-platform-foundations/02-load.tf index 74cb9f8b..9c025e69 100644 --- a/blueprints/data-solutions/data-platform-foundations/02-load.tf +++ b/blueprints/data-solutions/data-platform-foundations/02-load.tf @@ -36,9 +36,10 @@ locals { module "load-project" { source = "../../../modules/project" parent = var.folder_id - billing_account = var.billing_account_id - prefix = var.prefix - name = "lod${local.project_suffix}" + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.load : "${var.project_config.project_ids.load}${local.project_suffix}" group_iam = { (local.groups.data-engineers) = [ "roles/compute.viewer", diff --git a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf index a202afdd..c7f59578 100644 --- a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf +++ b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf @@ -35,9 +35,10 @@ locals { module "orch-project" { source = "../../../modules/project" parent = var.folder_id - billing_account = var.billing_account_id - prefix = var.prefix - name = "orc${local.project_suffix}" + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.orc : "${var.project_config.project_ids.orc}${local.project_suffix}" group_iam = { (local.groups.data-engineers) = [ "roles/bigquery.dataEditor", diff --git a/blueprints/data-solutions/data-platform-foundations/04-transformation.tf b/blueprints/data-solutions/data-platform-foundations/04-transformation.tf index 3d3a818c..c2b9b1e2 100644 --- a/blueprints/data-solutions/data-platform-foundations/04-transformation.tf +++ b/blueprints/data-solutions/data-platform-foundations/04-transformation.tf @@ -30,9 +30,10 @@ locals { module "transf-project" { source = "../../../modules/project" parent = var.folder_id - billing_account = var.billing_account_id - prefix = var.prefix - name = "trf${local.project_suffix}" + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.trf : "${var.project_config.project_ids.trf}${local.project_suffix}" group_iam = { (local.groups.data-engineers) = [ "roles/bigquery.jobUser", diff --git a/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf b/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf index 0db5ce44..6bc1a28e 100644 --- a/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf +++ b/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf @@ -83,9 +83,10 @@ locals { module "dwh-lnd-project" { source = "../../../modules/project" parent = var.folder_id - billing_account = var.billing_account_id - prefix = var.prefix - name = "dwh-lnd${local.project_suffix}" + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-lnd : "${var.project_config.project_ids.dwh-lnd}${local.project_suffix}" group_iam = local.dwh_group_iam iam = local.dwh_lnd_iam services = local.dwh_services @@ -98,9 +99,10 @@ module "dwh-lnd-project" { module "dwh-cur-project" { source = "../../../modules/project" parent = var.folder_id - billing_account = var.billing_account_id - prefix = var.prefix - name = "dwh-cur${local.project_suffix}" + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-cur : "${var.project_config.project_ids.dwh-cur}${local.project_suffix}" group_iam = local.dwh_group_iam iam = local.dwh_iam services = local.dwh_services @@ -113,9 +115,10 @@ module "dwh-cur-project" { module "dwh-conf-project" { source = "../../../modules/project" parent = var.folder_id - billing_account = var.billing_account_id - prefix = var.prefix - name = "dwh-conf${local.project_suffix}" + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-conf : "${var.project_config.project_ids.dwh-conf}${local.project_suffix}" group_iam = local.dwh_group_iam iam = local.dwh_iam services = local.dwh_services diff --git a/blueprints/data-solutions/data-platform-foundations/06-common.tf b/blueprints/data-solutions/data-platform-foundations/06-common.tf index 80451500..64df392b 100644 --- a/blueprints/data-solutions/data-platform-foundations/06-common.tf +++ b/blueprints/data-solutions/data-platform-foundations/06-common.tf @@ -17,9 +17,10 @@ module "common-project" { source = "../../../modules/project" parent = var.folder_id - billing_account = var.billing_account_id - prefix = var.prefix - name = "cmn${local.project_suffix}" + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.common : "${var.project_config.project_ids.common}${local.project_suffix}" group_iam = { (local.groups.data-analysts) = [ "roles/datacatalog.viewer", diff --git a/blueprints/data-solutions/data-platform-foundations/07-exposure.tf b/blueprints/data-solutions/data-platform-foundations/07-exposure.tf index 030be0b8..4a8071c0 100644 --- a/blueprints/data-solutions/data-platform-foundations/07-exposure.tf +++ b/blueprints/data-solutions/data-platform-foundations/07-exposure.tf @@ -17,7 +17,8 @@ module "exp-project" { source = "../../../modules/project" parent = var.folder_id - billing_account = var.billing_account_id - prefix = var.prefix - name = "exp${local.project_suffix}" + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.exp : "${var.project_config.project_ids.exp}${local.project_suffix}" } diff --git a/blueprints/data-solutions/data-platform-foundations/variables.tf b/blueprints/data-solutions/data-platform-foundations/variables.tf index 6c25406a..de033f72 100644 --- a/blueprints/data-solutions/data-platform-foundations/variables.tf +++ b/blueprints/data-solutions/data-platform-foundations/variables.tf @@ -177,6 +177,40 @@ variable "prefix" { } } +variable "project_config" { + description = "Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format." + type = object({ + billing_account_id = optional(string, null) + project_ids = optional(object({ + drop = string + load = string + orc = string + trf = string + dwh-lnd = string + dwh-cur = string + dwh-conf = string + common = string + exp = string + }), { + drop = "drp" + load = "lod" + orc = "orc" + trf = "trf" + dwh-lnd = "dwh-lnd" + dwh-cur = "dwh-cur" + dwh-conf = "dwh-conf" + common = "cmn" + exp = "exp" + } + ) + }) + default = {} + validation { + condition = var.project_config.billing_account_id != null || var.project_config.project_ids != null + error_message = "At least one attribute should be set." + } +} + variable "project_services" { description = "List of core services enabled on all projects." type = list(string) From c8f25512eb5094cb9bcd73a28200a8e2cfe346ea Mon Sep 17 00:00:00 2001 From: lcaggio Date: Mon, 20 Feb 2023 00:39:37 +0100 Subject: [PATCH 24/47] Fix variabler to support existing projects. --- .../data-platform-foundations/01-dropoff.tf | 34 +++++----- .../data-platform-foundations/02-load.tf | 43 +++++++------ .../03-orchestration.tf | 63 ++++++++++--------- .../04-transformation.tf | 51 ++++++++------- .../05-datawarehouse.tf | 29 +++++---- .../data-platform-foundations/06-common.tf | 23 ++++--- .../data-platform-foundations/07-exposure.tf | 2 +- .../data-platform-foundations/README.md | 4 ++ .../data-platform-foundations/variables.tf | 2 +- 9 files changed, 136 insertions(+), 115 deletions(-) diff --git a/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf b/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf index f1dc492e..4c4264d3 100644 --- a/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf +++ b/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf @@ -15,37 +15,37 @@ # tfdoc:file:description drop off project and resources. locals { - drop_orch_service_accounts = [ - module.load-sa-df-0.iam_email, module.orch-sa-cmp-0.iam_email - ] -} - -module "drop-project" { - source = "../../../modules/project" - parent = var.folder_id - billing_account = var.project_config.billing_account_id - project_create = var.project_config.billing_account_id != null - prefix = var.project_config.billing_account_id == null ? null : var.prefix - name = var.project_config.billing_account_id == null ? var.project_config.project_ids.drop : "${var.project_config.project_ids.drop}${local.project_suffix}" - group_iam = { + group_iam_drp = { (local.groups.data-engineers) = [ "roles/bigquery.dataEditor", "roles/pubsub.editor", "roles/storage.admin", ] } - iam = { + iam_drp = { "roles/bigquery.dataEditor" = [module.drop-sa-bq-0.iam_email] "roles/bigquery.user" = [module.load-sa-df-0.iam_email] "roles/pubsub.publisher" = [module.drop-sa-ps-0.iam_email] - "roles/pubsub.subscriber" = concat( - local.drop_orch_service_accounts, [module.load-sa-df-0.iam_email] - ) + "roles/pubsub.subscriber" = [ + module.orch-sa-cmp-0.iam_email, module.load-sa-df-0.iam_email + ] "roles/storage.objectAdmin" = [module.load-sa-df-0.iam_email] "roles/storage.objectCreator" = [module.drop-sa-cs-0.iam_email] "roles/storage.objectViewer" = [module.orch-sa-cmp-0.iam_email] "roles/storage.admin" = [module.load-sa-df-0.iam_email] } +} + +module "drop-project" { + source = "../../../modules/project" + parent = var.project_config.parent + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.drop : "${var.project_config.project_ids.drop}${local.project_suffix}" + # group_iam = local.group_iam_drp + iam = var.project_config.billing_account_id != null ? local.iam_drp : null + iam_additive = var.project_config.billing_account_id == null ? local.iam_drp : null services = concat(var.project_services, [ "bigquery.googleapis.com", "bigqueryreservation.googleapis.com", diff --git a/blueprints/data-solutions/data-platform-foundations/02-load.tf b/blueprints/data-solutions/data-platform-foundations/02-load.tf index 9c025e69..b547f050 100644 --- a/blueprints/data-solutions/data-platform-foundations/02-load.tf +++ b/blueprints/data-solutions/data-platform-foundations/02-load.tf @@ -15,6 +15,22 @@ # tfdoc:file:description Load project and VPC. locals { + group_iam_load = { + (local.groups.data-engineers) = [ + "roles/compute.viewer", + "roles/dataflow.admin", + "roles/dataflow.developer", + "roles/viewer", + ] + } + iam_load = { + "roles/bigquery.jobUser" = [module.load-sa-df-0.iam_email] + "roles/dataflow.admin" = [ + module.orch-sa-cmp-0.iam_email, module.load-sa-df-0.iam_email + ] + "roles/dataflow.worker" = [module.load-sa-df-0.iam_email] + "roles/storage.objectAdmin" = local.load_service_accounts + } load_service_accounts = [ "serviceAccount:${module.load-project.service_accounts.robots.dataflow}", module.load-sa-df-0.iam_email @@ -35,27 +51,14 @@ locals { module "load-project" { source = "../../../modules/project" - parent = var.folder_id + parent = var.project_config.parent billing_account = var.project_config.billing_account_id project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.load : "${var.project_config.project_ids.load}${local.project_suffix}" - group_iam = { - (local.groups.data-engineers) = [ - "roles/compute.viewer", - "roles/dataflow.admin", - "roles/dataflow.developer", - "roles/viewer", - ] - } - iam = { - "roles/bigquery.jobUser" = [module.load-sa-df-0.iam_email] - "roles/dataflow.admin" = [ - module.orch-sa-cmp-0.iam_email, module.load-sa-df-0.iam_email - ] - "roles/dataflow.worker" = [module.load-sa-df-0.iam_email] - "roles/storage.objectAdmin" = local.load_service_accounts - } + # group_iam = local.group_iam_load + iam = var.project_config.billing_account_id != null ? local.iam_load : null + iam_additive = var.project_config.billing_account_id == null ? local.iam_load : null services = concat(var.project_services, [ "bigquery.googleapis.com", "bigqueryreservation.googleapis.com", @@ -108,11 +111,11 @@ module "load-vpc" { source = "../../../modules/net-vpc" count = local.use_shared_vpc ? 0 : 1 project_id = module.load-project.project_id - name = "${var.prefix}-default" + name = "${var.prefix}-lod" subnets = [ { ip_cidr_range = "10.10.0.0/24" - name = "default" + name = "${var.prefix}-lod" region = var.region } ] @@ -132,7 +135,7 @@ module "load-nat" { source = "../../../modules/net-cloudnat" count = local.use_shared_vpc ? 0 : 1 project_id = module.load-project.project_id - name = "${var.prefix}-default" + name = "${var.prefix}-lod" region = var.region router_network = module.load-vpc.0.name } diff --git a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf index c7f59578..f720fc7f 100644 --- a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf +++ b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf @@ -15,31 +15,7 @@ # tfdoc:file:description Orchestration project and VPC. locals { - orch_subnet = ( - local.use_shared_vpc - ? var.network_config.subnet_self_links.orchestration - : values(module.orch-vpc.0.subnet_self_links)[0] - ) - orch_vpc = ( - local.use_shared_vpc - ? var.network_config.network_self_link - : module.orch-vpc.0.self_link - ) - - # Note: This formatting is needed for output purposes since the fabric artifact registry - # module doesn't yet expose the docker usage path of a registry folder in the needed format. - orch_docker_path = format("%s-docker.pkg.dev/%s/%s", - var.region, module.orch-project.project_id, module.orch-artifact-reg.name) -} - -module "orch-project" { - source = "../../../modules/project" - parent = var.folder_id - billing_account = var.project_config.billing_account_id - project_create = var.project_config.billing_account_id != null - prefix = var.project_config.billing_account_id == null ? null : var.prefix - name = var.project_config.billing_account_id == null ? var.project_config.project_ids.orc : "${var.project_config.project_ids.orc}${local.project_suffix}" - group_iam = { + group_iam_orch = { (local.groups.data-engineers) = [ "roles/bigquery.dataEditor", "roles/bigquery.jobUser", @@ -54,7 +30,7 @@ module "orch-project" { "roles/serviceusage.serviceUsageConsumer", ] } - iam = { + iam_orch = { "roles/bigquery.dataEditor" = [ module.load-sa-df-0.iam_email, module.transf-sa-df-0.iam_email, @@ -85,7 +61,34 @@ module "orch-project" { ] "roles/storage.objectViewer" = [module.load-sa-df-0.iam_email] } - oslogin = false + orch_subnet = ( + local.use_shared_vpc + ? var.network_config.subnet_self_links.orchestration + : values(module.orch-vpc.0.subnet_self_links)[0] + ) + orch_vpc = ( + local.use_shared_vpc + ? var.network_config.network_self_link + : module.orch-vpc.0.self_link + ) + + # Note: This formatting is needed for output purposes since the fabric artifact registry + # module doesn't yet expose the docker usage path of a registry folder in the needed format. + orch_docker_path = format("%s-docker.pkg.dev/%s/%s", + var.region, module.orch-project.project_id, module.orch-artifact-reg.name) +} + +module "orch-project" { + source = "../../../modules/project" + parent = var.project_config.parent + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.orc : "${var.project_config.project_ids.orc}${local.project_suffix}" + # group_iam = local.group_iam_orch + iam = var.project_config.billing_account_id != null ? local.iam_orch : null + iam_additive = var.project_config.billing_account_id == null ? local.iam_orch : null + oslogin = false services = concat(var.project_services, [ "artifactregistry.googleapis.com", "bigquery.googleapis.com", @@ -133,11 +136,11 @@ module "orch-vpc" { source = "../../../modules/net-vpc" count = local.use_shared_vpc ? 0 : 1 project_id = module.orch-project.project_id - name = "${var.prefix}-default" + name = "${var.prefix}-orch" subnets = [ { ip_cidr_range = "10.10.0.0/24" - name = "default" + name = "${var.prefix}-orch" region = var.region secondary_ip_ranges = { pods = "10.10.8.0/22" @@ -161,7 +164,7 @@ module "orch-nat" { count = local.use_shared_vpc ? 0 : 1 source = "../../../modules/net-cloudnat" project_id = module.orch-project.project_id - name = "${var.prefix}-default" + name = "${var.prefix}-orch" region = var.region router_network = module.orch-vpc.0.name } diff --git a/blueprints/data-solutions/data-platform-foundations/04-transformation.tf b/blueprints/data-solutions/data-platform-foundations/04-transformation.tf index c2b9b1e2..63d3f399 100644 --- a/blueprints/data-solutions/data-platform-foundations/04-transformation.tf +++ b/blueprints/data-solutions/data-platform-foundations/04-transformation.tf @@ -15,32 +15,13 @@ # tfdoc:file:description Trasformation project and VPC. locals { - transf_subnet = ( - local.use_shared_vpc - ? var.network_config.subnet_self_links.orchestration - : values(module.transf-vpc.0.subnet_self_links)[0] - ) - transf_vpc = ( - local.use_shared_vpc - ? var.network_config.network_self_link - : module.transf-vpc.0.self_link - ) -} - -module "transf-project" { - source = "../../../modules/project" - parent = var.folder_id - billing_account = var.project_config.billing_account_id - project_create = var.project_config.billing_account_id != null - prefix = var.project_config.billing_account_id == null ? null : var.prefix - name = var.project_config.billing_account_id == null ? var.project_config.project_ids.trf : "${var.project_config.project_ids.trf}${local.project_suffix}" - group_iam = { + group_iam_trf = { (local.groups.data-engineers) = [ "roles/bigquery.jobUser", "roles/dataflow.admin", ] } - iam = { + iam_trf = { "roles/bigquery.jobUser" = [ module.transf-sa-bq-0.iam_email, ] @@ -55,6 +36,28 @@ module "transf-project" { "serviceAccount:${module.transf-project.service_accounts.robots.dataflow}" ] } + transf_subnet = ( + local.use_shared_vpc + ? var.network_config.subnet_self_links.orchestration + : values(module.transf-vpc.0.subnet_self_links)[0] + ) + transf_vpc = ( + local.use_shared_vpc + ? var.network_config.network_self_link + : module.transf-vpc.0.self_link + ) +} + +module "transf-project" { + source = "../../../modules/project" + parent = var.project_config.parent + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.trf : "${var.project_config.project_ids.trf}${local.project_suffix}" + # group_iam = local.group_iam_trf + iam = var.project_config.billing_account_id != null ? local.iam_orch : null + iam_additive = var.project_config.billing_account_id == null ? local.iam_orch : null services = concat(var.project_services, [ "bigquery.googleapis.com", "bigqueryreservation.googleapis.com", @@ -132,11 +135,11 @@ module "transf-vpc" { source = "../../../modules/net-vpc" count = local.use_shared_vpc ? 0 : 1 project_id = module.transf-project.project_id - name = "${var.prefix}-default" + name = "${var.prefix}-trf" subnets = [ { ip_cidr_range = "10.10.0.0/24" - name = "default" + name = "${var.prefix}-trf" region = var.region } ] @@ -156,7 +159,7 @@ module "transf-nat" { source = "../../../modules/net-cloudnat" count = local.use_shared_vpc ? 0 : 1 project_id = module.transf-project.project_id - name = "${var.prefix}-default" + name = "${var.prefix}-trf" region = var.region router_network = module.transf-vpc.0.name } diff --git a/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf b/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf index 6bc1a28e..d22cf0aa 100644 --- a/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf +++ b/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf @@ -82,14 +82,15 @@ locals { module "dwh-lnd-project" { source = "../../../modules/project" - parent = var.folder_id + parent = var.project_config.parent billing_account = var.project_config.billing_account_id project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-lnd : "${var.project_config.project_ids.dwh-lnd}${local.project_suffix}" - group_iam = local.dwh_group_iam - iam = local.dwh_lnd_iam - services = local.dwh_services + # group_iam = local.dwh_group_iam + iam = var.project_config.billing_account_id != null ? local.dwh_lnd_iam : {} + iam_additive = var.project_config.billing_account_id == null ? local.dwh_lnd_iam : {} + services = local.dwh_services service_encryption_key_ids = { bq = [try(local.service_encryption_keys.bq, null)] storage = [try(local.service_encryption_keys.storage, null)] @@ -98,14 +99,15 @@ module "dwh-lnd-project" { module "dwh-cur-project" { source = "../../../modules/project" - parent = var.folder_id + parent = var.project_config.parent billing_account = var.project_config.billing_account_id project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-cur : "${var.project_config.project_ids.dwh-cur}${local.project_suffix}" - group_iam = local.dwh_group_iam - iam = local.dwh_iam - services = local.dwh_services + # group_iam = local.dwh_group_iam + iam = var.project_config.billing_account_id != null ? local.dwh_iam : {} + iam_additive = var.project_config.billing_account_id == null ? local.dwh_iam : {} + services = local.dwh_services service_encryption_key_ids = { bq = [try(local.service_encryption_keys.bq, null)] storage = [try(local.service_encryption_keys.storage, null)] @@ -114,14 +116,15 @@ module "dwh-cur-project" { module "dwh-conf-project" { source = "../../../modules/project" - parent = var.folder_id + parent = var.project_config.parent billing_account = var.project_config.billing_account_id project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-conf : "${var.project_config.project_ids.dwh-conf}${local.project_suffix}" - group_iam = local.dwh_group_iam - iam = local.dwh_iam - services = local.dwh_services + # group_iam = local.dwh_group_iam + iam = var.project_config.billing_account_id != null ? local.dwh_iam : null + iam_additive = var.project_config.billing_account_id == null ? local.dwh_iam : null + services = local.dwh_services service_encryption_key_ids = { bq = [try(local.service_encryption_keys.bq, null)] storage = [try(local.service_encryption_keys.storage, null)] @@ -141,7 +144,7 @@ module "dwh-lnd-bq-0" { module "dwh-cur-bq-0" { source = "../../../modules/bigquery-dataset" project_id = module.dwh-cur-project.project_id - id = "${replace(var.prefix, "-", "_")}_dwh_lnd_bq_0" + id = "${replace(var.prefix, "-", "_")}_dwh_cur_bq_0" location = var.location encryption_key = try(local.service_encryption_keys.bq, null) } diff --git a/blueprints/data-solutions/data-platform-foundations/06-common.tf b/blueprints/data-solutions/data-platform-foundations/06-common.tf index 64df392b..059d6b5e 100644 --- a/blueprints/data-solutions/data-platform-foundations/06-common.tf +++ b/blueprints/data-solutions/data-platform-foundations/06-common.tf @@ -14,14 +14,8 @@ # tfdoc:file:description common project. -module "common-project" { - source = "../../../modules/project" - parent = var.folder_id - billing_account = var.project_config.billing_account_id - project_create = var.project_config.billing_account_id != null - prefix = var.project_config.billing_account_id == null ? null : var.prefix - name = var.project_config.billing_account_id == null ? var.project_config.project_ids.common : "${var.project_config.project_ids.common}${local.project_suffix}" - group_iam = { +locals { + group_iam_common = { (local.groups.data-analysts) = [ "roles/datacatalog.viewer", ] @@ -35,7 +29,7 @@ module "common-project" { "roles/datacatalog.admin" ] } - iam = { + iam_common = { "roles/dlp.user" = [ module.load-sa-df-0.iam_email, module.transf-sa-df-0.iam_email @@ -52,6 +46,17 @@ module "common-project" { # local.groups_iam.data-analysts ] } +} +module "common-project" { + source = "../../../modules/project" + parent = var.project_config.parent + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + name = var.project_config.billing_account_id == null ? var.project_config.project_ids.common : "${var.project_config.project_ids.common}${local.project_suffix}" + # group_iam = local.group_iam_common + iam = var.project_config.billing_account_id != null ? local.iam_common : null + iam_additive = var.project_config.billing_account_id == null ? local.iam_common : null services = concat(var.project_services, [ "datacatalog.googleapis.com", "dlp.googleapis.com", diff --git a/blueprints/data-solutions/data-platform-foundations/07-exposure.tf b/blueprints/data-solutions/data-platform-foundations/07-exposure.tf index 4a8071c0..ea8fca09 100644 --- a/blueprints/data-solutions/data-platform-foundations/07-exposure.tf +++ b/blueprints/data-solutions/data-platform-foundations/07-exposure.tf @@ -16,7 +16,7 @@ module "exp-project" { source = "../../../modules/project" - parent = var.folder_id + parent = var.project_config.parent billing_account = var.project_config.billing_account_id project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix diff --git a/blueprints/data-solutions/data-platform-foundations/README.md b/blueprints/data-solutions/data-platform-foundations/README.md index 08b24b21..f6aaac71 100644 --- a/blueprints/data-solutions/data-platform-foundations/README.md +++ b/blueprints/data-solutions/data-platform-foundations/README.md @@ -233,6 +233,10 @@ To create Cloud Key Management keys in the Data Platform you can uncomment the C To handle multiple groups of `data-analysts` accessing the same Data Warehouse layer projects but only to the dataset belonging to a specific group, you may want to assign roles at BigQuery dataset level instead of at project-level. To do this, you need to remove IAM binging at project-level for the `data-analysts` group and give roles at BigQuery dataset level using the `iam` variable on `bigquery-dataset` modules. +### Project Configuration + +The solution can be deployed creating projects on a given parent (organization or folder) or on existing projects. Configure variable `project_config` accordingly. + ## Demo pipeline The application layer is out of scope of this script. As a demo purpuse only, several Cloud Composer DAGs are provided. Demos will import data from the `drop off` area to the `Data Warehouse Confidential` dataset suing different features. diff --git a/blueprints/data-solutions/data-platform-foundations/variables.tf b/blueprints/data-solutions/data-platform-foundations/variables.tf index de033f72..ca5f754f 100644 --- a/blueprints/data-solutions/data-platform-foundations/variables.tf +++ b/blueprints/data-solutions/data-platform-foundations/variables.tf @@ -181,6 +181,7 @@ variable "project_config" { description = "Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format." type = object({ billing_account_id = optional(string, null) + parent = string project_ids = optional(object({ drop = string load = string @@ -204,7 +205,6 @@ variable "project_config" { } ) }) - default = {} validation { condition = var.project_config.billing_account_id != null || var.project_config.project_ids != null error_message = "At least one attribute should be set." From db6a4f9ac7f2c5bb510eb72b4fd287e21167257f Mon Sep 17 00:00:00 2001 From: lcaggio Date: Mon, 20 Feb 2023 00:45:40 +0100 Subject: [PATCH 25/47] Remove variables --- .../data-solutions/data-platform-foundations/README.md | 6 ++++-- .../data-platform-foundations/variables.tf | 10 ---------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/blueprints/data-solutions/data-platform-foundations/README.md b/blueprints/data-solutions/data-platform-foundations/README.md index f6aaac71..e1bb5f5e 100644 --- a/blueprints/data-solutions/data-platform-foundations/README.md +++ b/blueprints/data-solutions/data-platform-foundations/README.md @@ -213,9 +213,11 @@ While this blueprint can be used as a standalone deployment, it can also be call ```hcl module "data-platform" { source = "./fabric/blueprints/data-solutions/data-platform-foundations" - billing_account_id = var.billing_account_id - folder_id = var.folder_id organization_domain = "example.com" + project_config = { + billing_account_id = var.billing_account_id + parent = "folders/12345678" + } prefix = "myprefix" } diff --git a/blueprints/data-solutions/data-platform-foundations/variables.tf b/blueprints/data-solutions/data-platform-foundations/variables.tf index ca5f754f..4ec2fd7e 100644 --- a/blueprints/data-solutions/data-platform-foundations/variables.tf +++ b/blueprints/data-solutions/data-platform-foundations/variables.tf @@ -14,11 +14,6 @@ # tfdoc:file:description Terraform Variables. -variable "billing_account_id" { - description = "Billing account id." - type = string -} - variable "composer_config" { description = "Cloud Composer config." type = object({ @@ -119,11 +114,6 @@ variable "data_force_destroy" { default = false } -variable "folder_id" { - description = "Folder to be used for the networking resources in folders/nnnn format." - type = string -} - variable "groups" { description = "User groups." type = map(string) From f4c1fa6c20f28e0f071d81d8c1e6762fc7c64f8d Mon Sep 17 00:00:00 2001 From: lcaggio Date: Mon, 20 Feb 2023 00:56:32 +0100 Subject: [PATCH 26/47] Fix tests. --- .../data-platform-foundations/README.md | 4 ++-- fast/stages/3-data-platform/dev/main.tf | 10 ++++++---- .../data_platform_foundations/fixture/main.tf | 8 +++++--- .../data_platform_foundations/test_plan.py | 2 +- tests/fast/stages/s3_data_platform/common.tfvars | 12 +++++++----- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/blueprints/data-solutions/data-platform-foundations/README.md b/blueprints/data-solutions/data-platform-foundations/README.md index e1bb5f5e..d004cf94 100644 --- a/blueprints/data-solutions/data-platform-foundations/README.md +++ b/blueprints/data-solutions/data-platform-foundations/README.md @@ -215,13 +215,13 @@ module "data-platform" { source = "./fabric/blueprints/data-solutions/data-platform-foundations" organization_domain = "example.com" project_config = { - billing_account_id = var.billing_account_id + billing_account_id = "123456-123456-123456" parent = "folders/12345678" } prefix = "myprefix" } -# tftest modules=43 resources=297 +# tftest modules=43 resources=264 ``` ## Customizations diff --git a/fast/stages/3-data-platform/dev/main.tf b/fast/stages/3-data-platform/dev/main.tf index 53d901d1..c600a758 100644 --- a/fast/stages/3-data-platform/dev/main.tf +++ b/fast/stages/3-data-platform/dev/main.tf @@ -18,13 +18,15 @@ module "data-platform" { source = "../../../../blueprints/data-solutions/data-platform-foundations" - billing_account_id = var.billing_account.id composer_config = var.composer_config data_force_destroy = var.data_force_destroy data_catalog_tags = var.data_catalog_tags - folder_id = var.folder_ids.data-platform-dev - groups = var.groups - location = var.location + project_config = { + billing_account_id = var.billing_account.id + parent = var.folder_ids.data-platform-dev + } + groups = var.groups + location = var.location network_config = { host_project = var.host_project_ids.dev-spoke-0 network_self_link = var.vpc_self_links.dev-spoke-0 diff --git a/tests/blueprints/data_solutions/data_platform_foundations/fixture/main.tf b/tests/blueprints/data_solutions/data_platform_foundations/fixture/main.tf index 52317d6f..5acb29e8 100644 --- a/tests/blueprints/data_solutions/data_platform_foundations/fixture/main.tf +++ b/tests/blueprints/data_solutions/data_platform_foundations/fixture/main.tf @@ -17,7 +17,9 @@ module "test" { source = "../../../../../blueprints/data-solutions/data-platform-foundations/" organization_domain = "example.com" - billing_account_id = "123456-123456-123456" - folder_id = "folders/12345678" - prefix = "prefix" + project_config = { + billing_account_id = "123456-123456-123456" + parent = "folders/12345678" + } + prefix = "prefix" } diff --git a/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py b/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py index 785f4705..f3ed2ba0 100644 --- a/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py +++ b/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py @@ -23,4 +23,4 @@ def test_resources(e2e_plan_runner): modules, resources = e2e_plan_runner(FIXTURES_DIR) assert len(modules) == 42 - assert len(resources) == 296 + assert len(resources) == 264 diff --git a/tests/fast/stages/s3_data_platform/common.tfvars b/tests/fast/stages/s3_data_platform/common.tfvars index 2ec41d37..97d8bebc 100644 --- a/tests/fast/stages/s3_data_platform/common.tfvars +++ b/tests/fast/stages/s3_data_platform/common.tfvars @@ -1,11 +1,13 @@ automation = { outputs_bucket = "test" } -billing_account = { - id = "012345-67890A-BCDEF0", -} -folder_ids = { - data-platform-dev = "folders/12345678" +project_config = { + billing_account = { + id = "012345-67890A-BCDEF0", + }, + parent = { + data-platform-dev = "folders/12345678" + } } host_project_ids = { dev-spoke-0 = "fast-dev-net-spoke-0" From c523dce3a6890566743a3c9a0c017470d28abd0c Mon Sep 17 00:00:00 2001 From: lcaggio Date: Mon, 20 Feb 2023 01:00:57 +0100 Subject: [PATCH 27/47] Fix --- blueprints/data-solutions/data-platform-foundations/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/data-solutions/data-platform-foundations/README.md b/blueprints/data-solutions/data-platform-foundations/README.md index d004cf94..98c84ee4 100644 --- a/blueprints/data-solutions/data-platform-foundations/README.md +++ b/blueprints/data-solutions/data-platform-foundations/README.md @@ -221,7 +221,7 @@ module "data-platform" { prefix = "myprefix" } -# tftest modules=43 resources=264 +# tftest modules=43 resources=265 ``` ## Customizations From eda9597f0b01bd58635988f0c23b42761b3c5f54 Mon Sep 17 00:00:00 2001 From: lcaggio Date: Mon, 20 Feb 2023 01:03:38 +0100 Subject: [PATCH 28/47] Fix linting --- .../data-platform-foundations/README.md | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/blueprints/data-solutions/data-platform-foundations/README.md b/blueprints/data-solutions/data-platform-foundations/README.md index 98c84ee4..027c6299 100644 --- a/blueprints/data-solutions/data-platform-foundations/README.md +++ b/blueprints/data-solutions/data-platform-foundations/README.md @@ -250,20 +250,19 @@ You can find examples in the `[demo](./demo)` folder. | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [billing_account_id](variables.tf#L17) | Billing account id. | string | ✓ | | -| [folder_id](variables.tf#L122) | Folder to be used for the networking resources in folders/nnnn format. | string | ✓ | | -| [organization_domain](variables.tf#L166) | Organization domain. | string | ✓ | | -| [prefix](variables.tf#L171) | Prefix used for resource names. | string | ✓ | | -| [composer_config](variables.tf#L22) | Cloud Composer config. | object({…}) | | {…} | -| [data_catalog_tags](variables.tf#L105) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | -| [data_force_destroy](variables.tf#L116) | Flag to set 'force_destroy' on data services like BiguQery or Cloud Storage. | bool | | false | -| [groups](variables.tf#L127) | User groups. | map(string) | | {…} | -| [location](variables.tf#L137) | Location used for multi-regional resources. | string | | "eu" | -| [network_config](variables.tf#L143) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | object({…}) | | null | -| [project_services](variables.tf#L180) | List of core services enabled on all projects. | list(string) | | […] | -| [project_suffix](variables.tf#L191) | Suffix used only for project ids. | string | | null | -| [region](variables.tf#L197) | Region used for regional resources. | string | | "europe-west1" | -| [service_encryption_keys](variables.tf#L203) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | +| [organization_domain](variables.tf#L156) | Organization domain. | string | ✓ | | +| [prefix](variables.tf#L161) | Prefix used for resource names. | string | ✓ | | +| [project_config](variables.tf#L170) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | ✓ | | +| [composer_config](variables.tf#L17) | Cloud Composer config. | object({…}) | | {…} | +| [data_catalog_tags](variables.tf#L100) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | +| [data_force_destroy](variables.tf#L111) | Flag to set 'force_destroy' on data services like BiguQery or Cloud Storage. | bool | | false | +| [groups](variables.tf#L117) | User groups. | map(string) | | {…} | +| [location](variables.tf#L127) | Location used for multi-regional resources. | string | | "eu" | +| [network_config](variables.tf#L133) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | object({…}) | | null | +| [project_services](variables.tf#L204) | List of core services enabled on all projects. | list(string) | | […] | +| [project_suffix](variables.tf#L215) | Suffix used only for project ids. | string | | null | +| [region](variables.tf#L221) | Region used for regional resources. | string | | "europe-west1" | +| [service_encryption_keys](variables.tf#L227) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | ## Outputs From 63a81a9b9b1e50f10df9882bfdd052985e9067ef Mon Sep 17 00:00:00 2001 From: lcaggio Date: Mon, 20 Feb 2023 01:12:19 +0100 Subject: [PATCH 29/47] Fix Fast test --- fast/stages/3-data-platform/dev/variables.tf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fast/stages/3-data-platform/dev/variables.tf b/fast/stages/3-data-platform/dev/variables.tf index 74a5dbe1..d0aad16f 100644 --- a/fast/stages/3-data-platform/dev/variables.tf +++ b/fast/stages/3-data-platform/dev/variables.tf @@ -169,6 +169,14 @@ variable "prefix" { type = string } +variable "project_config" { + description = "Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format." + type = object({ + billing_account_id = string + parent = string + }) +} + variable "project_services" { description = "List of core services enabled on all projects." type = list(string) From 970b8ff2557417038db447d0cd4e6a9faa48305b Mon Sep 17 00:00:00 2001 From: lcaggio Date: Mon, 20 Feb 2023 01:16:22 +0100 Subject: [PATCH 30/47] Fix DP Fast variables. --- fast/stages/3-data-platform/dev/README.md | 11 +++++----- fast/stages/3-data-platform/dev/variables.tf | 21 -------------------- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/fast/stages/3-data-platform/dev/README.md b/fast/stages/3-data-platform/dev/README.md index 48d09eaf..518e4516 100644 --- a/fast/stages/3-data-platform/dev/README.md +++ b/fast/stages/3-data-platform/dev/README.md @@ -190,6 +190,7 @@ You can find examples in the `[demo](../../../../blueprints/data-solutions/data- | [host_project_ids](variables.tf#L120) | Shared VPC project ids. | object({…}) | ✓ | | 2-networking | | [organization](variables.tf#L150) | Organization details. | object({…}) | ✓ | | 00-globals | | [prefix](variables.tf#L166) | Unique prefix used for resource names. Not used for projects if 'project_create' is null. | string | ✓ | | 00-globals | +| [project_config](variables.tf#L172) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | ✓ | | | | [composer_config](variables.tf#L38) | Cloud Composer configuration options. | object({…}) | | {…} | | | [data_catalog_tags](variables.tf#L85) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | | | [data_force_destroy](variables.tf#L96) | Flag to set 'force_destroy' on data services like BigQery or Cloud Storage. | bool | | false | | @@ -197,11 +198,11 @@ You can find examples in the `[demo](../../../../blueprints/data-solutions/data- | [location](variables.tf#L128) | Location used for multi-regional resources. | string | | "eu" | | | [network_config_composer](variables.tf#L134) | Network configurations to use for Composer. | object({…}) | | {…} | | | [outputs_location](variables.tf#L160) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [project_services](variables.tf#L172) | List of core services enabled on all projects. | list(string) | | […] | | -| [region](variables.tf#L183) | Region used for regional resources. | string | | "europe-west1" | | -| [service_encryption_keys](variables.tf#L189) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | -| [subnet_self_links](variables.tf#L201) | Shared VPC subnet self links. | object({…}) | | null | 2-networking | -| [vpc_self_links](variables.tf#L210) | Shared VPC self links. | object({…}) | | null | 2-networking | +| [project_services](variables.tf#L180) | List of core services enabled on all projects. | list(string) | | […] | | +| [region](variables.tf#L191) | Region used for regional resources. | string | | "europe-west1" | | +| [service_encryption_keys](variables.tf#L197) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | +| [subnet_self_links](variables.tf#L209) | Shared VPC subnet self links. | object({…}) | | null | 2-networking | +| [vpc_self_links](variables.tf#L218) | Shared VPC self links. | object({…}) | | null | 2-networking | ## Outputs diff --git a/fast/stages/3-data-platform/dev/variables.tf b/fast/stages/3-data-platform/dev/variables.tf index d0aad16f..392e2dc9 100644 --- a/fast/stages/3-data-platform/dev/variables.tf +++ b/fast/stages/3-data-platform/dev/variables.tf @@ -22,19 +22,6 @@ 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." - type = object({ - id = string - is_org_level = optional(bool, true) - }) - validation { - condition = var.billing_account.is_org_level != null - error_message = "Invalid `null` value for `billing_account.is_org_level`." - } -} - variable "composer_config" { description = "Cloud Composer configuration options." type = object({ @@ -99,14 +86,6 @@ variable "data_force_destroy" { default = false } -variable "folder_ids" { - # tfdoc:variable:source 1-resman - description = "Folder to be used for the networking resources in folders/nnnn format." - type = object({ - data-platform-dev = string - }) -} - variable "groups" { description = "Groups." type = map(string) From 2564c9b06a4b51616e9d3351cfd60628d39e2c78 Mon Sep 17 00:00:00 2001 From: lcaggio Date: Mon, 20 Feb 2023 01:17:08 +0100 Subject: [PATCH 31/47] Fix README --- fast/stages/3-data-platform/dev/README.md | 34 +++++++++++------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/fast/stages/3-data-platform/dev/README.md b/fast/stages/3-data-platform/dev/README.md index 518e4516..f2990310 100644 --- a/fast/stages/3-data-platform/dev/README.md +++ b/fast/stages/3-data-platform/dev/README.md @@ -185,24 +185,22 @@ You can find examples in the `[demo](../../../../blueprints/data-solutions/data- | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| | [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | -| [folder_ids](variables.tf#L102) | Folder to be used for the networking resources in folders/nnnn format. | object({…}) | ✓ | | 1-resman | -| [host_project_ids](variables.tf#L120) | Shared VPC project ids. | object({…}) | ✓ | | 2-networking | -| [organization](variables.tf#L150) | Organization details. | object({…}) | ✓ | | 00-globals | -| [prefix](variables.tf#L166) | Unique prefix used for resource names. Not used for projects if 'project_create' is null. | string | ✓ | | 00-globals | -| [project_config](variables.tf#L172) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | ✓ | | | -| [composer_config](variables.tf#L38) | Cloud Composer configuration options. | object({…}) | | {…} | | -| [data_catalog_tags](variables.tf#L85) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | | -| [data_force_destroy](variables.tf#L96) | Flag to set 'force_destroy' on data services like BigQery or Cloud Storage. | bool | | false | | -| [groups](variables.tf#L110) | Groups. | map(string) | | {…} | | -| [location](variables.tf#L128) | Location used for multi-regional resources. | string | | "eu" | | -| [network_config_composer](variables.tf#L134) | Network configurations to use for Composer. | object({…}) | | {…} | | -| [outputs_location](variables.tf#L160) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [project_services](variables.tf#L180) | List of core services enabled on all projects. | list(string) | | […] | | -| [region](variables.tf#L191) | Region used for regional resources. | string | | "europe-west1" | | -| [service_encryption_keys](variables.tf#L197) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | -| [subnet_self_links](variables.tf#L209) | Shared VPC subnet self links. | object({…}) | | null | 2-networking | -| [vpc_self_links](variables.tf#L218) | Shared VPC self links. | object({…}) | | null | 2-networking | +| [host_project_ids](variables.tf#L99) | Shared VPC project ids. | object({…}) | ✓ | | 2-networking | +| [organization](variables.tf#L129) | Organization details. | object({…}) | ✓ | | 00-globals | +| [prefix](variables.tf#L145) | Unique prefix used for resource names. Not used for projects if 'project_create' is null. | string | ✓ | | 00-globals | +| [project_config](variables.tf#L151) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | ✓ | | | +| [composer_config](variables.tf#L25) | Cloud Composer configuration options. | object({…}) | | {…} | | +| [data_catalog_tags](variables.tf#L72) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | | +| [data_force_destroy](variables.tf#L83) | Flag to set 'force_destroy' on data services like BigQery or Cloud Storage. | bool | | false | | +| [groups](variables.tf#L89) | Groups. | map(string) | | {…} | | +| [location](variables.tf#L107) | Location used for multi-regional resources. | string | | "eu" | | +| [network_config_composer](variables.tf#L113) | Network configurations to use for Composer. | object({…}) | | {…} | | +| [outputs_location](variables.tf#L139) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| [project_services](variables.tf#L159) | List of core services enabled on all projects. | list(string) | | […] | | +| [region](variables.tf#L170) | Region used for regional resources. | string | | "europe-west1" | | +| [service_encryption_keys](variables.tf#L176) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | +| [subnet_self_links](variables.tf#L188) | Shared VPC subnet self links. | object({…}) | | null | 2-networking | +| [vpc_self_links](variables.tf#L197) | Shared VPC self links. | object({…}) | | null | 2-networking | ## Outputs From 3085922ceae8b1c081b7a8993fa85539c589437d Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Mon, 20 Feb 2023 12:19:36 +0100 Subject: [PATCH 32/47] Fix tests --- modules/net-vpc/README.md | 2 +- tests/modules/net_vpc/examples/factory.yaml | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index dbd85502..bd5675d2 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -314,7 +314,7 @@ module "vpc" { name = "my-network" data_folder = "config/subnets" } -# tftest modules=1 resources=3 files=subnet-simple,subnet-detailed inventory=factory.yaml +# tftest modules=1 resources=4 files=subnet-simple,subnet-detailed inventory=factory.yaml ``` ```yaml diff --git a/tests/modules/net_vpc/examples/factory.yaml b/tests/modules/net_vpc/examples/factory.yaml index 48671c29..0724b597 100644 --- a/tests/modules/net_vpc/examples/factory.yaml +++ b/tests/modules/net_vpc/examples/factory.yaml @@ -44,7 +44,18 @@ values: region: europe-west4 role: null secondary_ip_range: [] + module.vpc.google_compute_subnetwork_iam_binding.binding["europe-west1/subnet-detailed.roles/compute.networkUser"]: + condition: [] + members: + - group:lorem@example.com + - serviceAccount:fbz@prj.iam.gserviceaccount.com + - user:foobar@example.com + project: my-project + region: europe-west1 + role: roles/compute.networkUser + subnetwork: subnet-detailed counts: google_compute_network: 1 google_compute_subnetwork: 2 + google_compute_subnetwork_iam_binding: 1 From 0ca0b2e99bb59b896ea317033770b8b1cf8a402d Mon Sep 17 00:00:00 2001 From: Julio Diez Date: Mon, 20 Feb 2023 14:37:33 +0100 Subject: [PATCH 33/47] Fix variable name --- modules/vpc-sc/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md index 8e412bcf..7ad0cba5 100644 --- a/modules/vpc-sc/README.md +++ b/modules/vpc-sc/README.md @@ -147,8 +147,8 @@ module "test" { from = { identities = [ "serviceAccount:test-tf@myproject.iam.gserviceaccount.com", - ], - source_access_levels = ["*"] + ] + access_levels = ["*"] } to = { operations = [{ service_name = "*" }] From 4a9ce1c5cea3102fdff5906a30f91548de492ad9 Mon Sep 17 00:00:00 2001 From: Julio Diez Date: Tue, 21 Feb 2023 14:05:03 +0100 Subject: [PATCH 34/47] Update README Remove unused field --- modules/project/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/project/README.md b/modules/project/README.md index e7a645fe..0cf77df7 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -213,7 +213,6 @@ module "service-project" { source = "./fabric/modules/project" name = "my-service-project" shared_vpc_service_config = { - attach = true host_project = module.host-project.project_id service_identity_iam = { "roles/compute.networkUser" = [ From 6b767c90358fe0910587f6443ea88326d11d3d02 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 21 Feb 2023 12:24:40 +0100 Subject: [PATCH 35/47] Simplify org policies data model in resman modules. --- modules/folder/README.md | 30 +++++---- modules/folder/organization-policies.tf | 51 ++++----------- modules/folder/variables.tf | 21 ++---- modules/organization/README.md | 61 +++++++++++++----- modules/organization/organization-policies.tf | 52 ++++----------- modules/organization/variables.tf | 20 ++---- modules/project/README.md | 64 +++++++++++-------- modules/project/organization-policies.tf | 51 ++++----------- modules/project/variables.tf | 21 ++---- .../modules/folder/examples/org-policies.yaml | 4 +- .../folder/org_policies_boolean.tfvars | 6 +- tests/modules/folder/org_policies_list.tfvars | 14 ++-- .../modules/organization/examples/basic.yaml | 33 +++++++++- .../organization/org_policies_boolean.tfvars | 7 +- .../organization/org_policies_list.tfvars | 15 +++-- .../test_plan_org_policies_modules.py | 55 ++++------------ .../project/examples/org-policies.yaml | 4 +- .../project/org_policies_boolean.tfvars | 6 +- .../modules/project/org_policies_list.tfvars | 15 +++-- 19 files changed, 242 insertions(+), 288 deletions(-) diff --git a/modules/folder/README.md b/modules/folder/README.md index e1ad6809..fb84ec42 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -42,40 +42,46 @@ module "folder" { name = "Folder name" org_policies = { "compute.disableGuestAttributesAccess" = { - enforce = true + rules = [{ enforce = true }] } "constraints/compute.skipDefaultNetworkCreation" = { - enforce = true + rules = [{ enforce = true }] } "iam.disableServiceAccountKeyCreation" = { - enforce = true + rules = [{ enforce = true }] } "iam.disableServiceAccountKeyUpload" = { - enforce = false rules = [ { condition = { - expression = "resource.matchTagId(\"tagKeys/1234\", \"tagValues/1234\")" + expression = "resource.matchTagId('tagKeys/1234', 'tagValues/1234')" title = "condition" description = "test condition" location = "somewhere" } enforce = true + }, + { + enforce = false } ] } "constraints/iam.allowedPolicyMemberDomains" = { - allow = { - values = ["C0xxxxxxx", "C0yyyyyyy"] - } + rules = [{ + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + }] } "constraints/compute.trustedImageProjects" = { - allow = { - values = ["projects/my-project"] - } + rules = [{ + allow = { + values = ["projects/my-project"] + } + }] } "constraints/compute.vmExternalIpAccess" = { - deny = { all = true } + rules = [{ deny = { all = true } }] } } } diff --git a/modules/folder/organization-policies.tf b/modules/folder/organization-policies.tf index 47532f21..2bf79c4a 100644 --- a/modules/folder/organization-policies.tf +++ b/modules/folder/organization-policies.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,16 +28,6 @@ locals { k => { inherit_from_parent = try(v.inherit_from_parent, null) reset = try(v.reset, null) - allow = can(v.allow) ? { - all = try(v.allow.all, null) - values = try(v.allow.values, null) - } : null - deny = can(v.deny) ? { - all = try(v.deny.all, null) - values = try(v.deny.values, null) - } : null - enforce = try(v.enforce, true) - rules = [ for r in try(v.rules, []) : { allow = can(r.allow) ? { @@ -48,7 +38,7 @@ locals { all = try(r.deny.all, null) values = try(r.deny.values, null) } : null - enforce = try(r.enforce, true) + enforce = try(r.enforce, null) condition = { description = try(r.condition.description, null) expression = try(r.condition.expression, null) @@ -67,8 +57,9 @@ locals { k => merge(v, { name = "${local.folder.name}/policies/${k}" parent = local.folder.name - - is_boolean_policy = v.allow == null && v.deny == null + is_boolean_policy = ( + alltrue([for r in v.rules : r.allow == null && r.deny == null]) + ) has_values = ( length(coalesce(try(v.allow.values, []), [])) > 0 || length(coalesce(try(v.deny.values, []), [])) > 0 @@ -90,11 +81,9 @@ resource "google_org_policy_policy" "default" { for_each = local.org_policies name = each.value.name parent = each.value.parent - spec { inherit_from_parent = each.value.inherit_from_parent reset = each.value.reset - dynamic "rules" { for_each = each.value.rules iterator = rule @@ -106,11 +95,14 @@ resource "google_org_policy_policy" "default" { ? upper(tostring(rule.value.enforce)) : null ) - condition { - description = rule.value.condition.description - expression = rule.value.condition.expression - location = rule.value.condition.location - title = rule.value.condition.title + dynamic "condition" { + for_each = rule.value.condition.expression != null ? [1] : [] + content { + description = rule.value.condition.description + expression = rule.value.condition.expression + location = rule.value.condition.location + title = rule.value.condition.title + } } dynamic "values" { for_each = rule.value.has_values ? [1] : [] @@ -121,22 +113,5 @@ resource "google_org_policy_policy" "default" { } } } - - rules { - allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null - deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null - enforce = ( - each.value.is_boolean_policy && each.value.enforce != null - ? upper(tostring(each.value.enforce)) - : null - ) - dynamic "values" { - for_each = each.value.has_values ? [1] : [] - content { - allowed_values = try(each.value.allow.values, null) - denied_values = try(each.value.deny.values, null) - } - } - } } } diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index a93ea1aa..e0abc612 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -143,19 +143,6 @@ variable "org_policies" { type = map(object({ inherit_from_parent = optional(bool) # for list policies only. reset = optional(bool) - - # default (unconditional) values - allow = optional(object({ - all = optional(bool) - values = optional(list(string)) - })) - deny = optional(object({ - all = optional(bool) - values = optional(list(string)) - })) - enforce = optional(bool, true) # for boolean policies only. - - # conditional values rules = optional(list(object({ allow = optional(object({ all = optional(bool) @@ -165,13 +152,13 @@ variable "org_policies" { all = optional(bool) values = optional(list(string)) })) - enforce = optional(bool, true) # for boolean policies only. - condition = object({ + enforce = optional(bool) # for boolean policies only. + condition = optional(object({ description = optional(string) expression = optional(string) location = optional(string) title = optional(string) - }) + }), {}) })), []) })) default = {} diff --git a/modules/organization/README.md b/modules/organization/README.md index b6caa3cd..907cf5f4 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -25,50 +25,77 @@ module "org" { iam_additive_members = { "user:compute@example.org" = ["roles/compute.admin", "roles/container.viewer"] } - + tags = { + allowexternal = { + description = "Allow external identities." + values = { + true = {}, false = {} + } + } + } org_policies = { "custom.gkeEnableAutoUpgrade" = { - enforce = true + rules = [{ enforce = true }] } "compute.disableGuestAttributesAccess" = { - enforce = true + rules = [{ enforce = true }] } "constraints/compute.skipDefaultNetworkCreation" = { - enforce = true + rules = [{ enforce = true }] } "iam.disableServiceAccountKeyCreation" = { - enforce = true + rules = [{ enforce = true }] } "iam.disableServiceAccountKeyUpload" = { - enforce = false rules = [ { condition = { - expression = "resource.matchTagId(\"tagKeys/1234\", \"tagValues/1234\")" + expression = "resource.matchTagId('tagKeys/1234', 'tagValues/1234')" title = "condition" description = "test condition" location = "somewhere" } enforce = true + }, + { + enforce = false } ] } "constraints/iam.allowedPolicyMemberDomains" = { - allow = { - values = ["C0xxxxxxx", "C0yyyyyyy"] - } + rules = [ + { + allow = { all = true } + condition = { + expression = "resource.matchTag('1234567890/allowexternal', 'true')" + title = "Allow external identities" + description = "Allow external identities when resource has the `allowexternal` tag set to true." + } + }, + { + allow = { values = ["C0xxxxxxx", "C0yyyyyyy"] } + condition = { + expression = "!resource.matchTag('1234567890/allowexternal', 'true')" + title = "" + description = "For any resource without allowexternal=true, only allow identities from restricted domains." + } + } + ] } + "constraints/compute.trustedImageProjects" = { - allow = { - values = ["projects/my-project"] - } + rules = [{ + allow = { + values = ["projects/my-project"] + } + }] } "constraints/compute.vmExternalIpAccess" = { - deny = { all = true } + rules = [{ deny = { all = true } }] } } } -# tftest modules=1 resources=13 inventory=basic.yaml +# tftest modules=1 resources=16 inventory=basic.yaml ``` ## IAM @@ -111,7 +138,7 @@ module "org" { # not necessarily to enforce on the org level, policy may be applied on folder/project levels org_policies = { "custom.gkeEnableAutoUpgrade" = { - enforce = true + rules = [{ enforce = true }] } } } @@ -131,7 +158,7 @@ module "org" { org_policy_custom_constraints_data_path = "configs/custom-constraints" org_policies = { "custom.gkeEnableAutoUpgrade" = { - enforce = true + rules = [{ enforce = true }] } } } diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf index 1a99ef9a..b43c5955 100644 --- a/modules/organization/organization-policies.tf +++ b/modules/organization/organization-policies.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,16 +28,6 @@ locals { k => { inherit_from_parent = try(v.inherit_from_parent, null) reset = try(v.reset, null) - allow = can(v.allow) ? { - all = try(v.allow.all, null) - values = try(v.allow.values, null) - } : null - deny = can(v.deny) ? { - all = try(v.deny.all, null) - values = try(v.deny.values, null) - } : null - enforce = try(v.enforce, true) - rules = [ for r in try(v.rules, []) : { allow = can(r.allow) ? { @@ -48,7 +38,7 @@ locals { all = try(r.deny.all, null) values = try(r.deny.values, null) } : null - enforce = try(r.enforce, true) + enforce = try(r.enforce, null) condition = { description = try(r.condition.description, null) expression = try(r.condition.expression, null) @@ -67,8 +57,9 @@ locals { k => merge(v, { name = "${var.organization_id}/policies/${k}" parent = var.organization_id - - is_boolean_policy = v.allow == null && v.deny == null + is_boolean_policy = ( + alltrue([for r in v.rules : r.allow == null && r.deny == null]) + ) has_values = ( length(coalesce(try(v.allow.values, []), [])) > 0 || length(coalesce(try(v.deny.values, []), [])) > 0 @@ -90,11 +81,9 @@ resource "google_org_policy_policy" "default" { for_each = local.org_policies name = each.value.name parent = each.value.parent - spec { inherit_from_parent = each.value.inherit_from_parent reset = each.value.reset - dynamic "rules" { for_each = each.value.rules iterator = rule @@ -106,11 +95,14 @@ resource "google_org_policy_policy" "default" { ? upper(tostring(rule.value.enforce)) : null ) - condition { - description = rule.value.condition.description - expression = rule.value.condition.expression - location = rule.value.condition.location - title = rule.value.condition.title + dynamic "condition" { + for_each = rule.value.condition.expression != null ? [1] : [] + content { + description = rule.value.condition.description + expression = rule.value.condition.expression + location = rule.value.condition.location + title = rule.value.condition.title + } } dynamic "values" { for_each = rule.value.has_values ? [1] : [] @@ -121,25 +113,7 @@ resource "google_org_policy_policy" "default" { } } } - - rules { - allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null - deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null - enforce = ( - each.value.is_boolean_policy && each.value.enforce != null - ? upper(tostring(each.value.enforce)) - : null - ) - dynamic "values" { - for_each = each.value.has_values ? [1] : [] - content { - allowed_values = try(each.value.allow.values, null) - denied_values = try(each.value.deny.values, null) - } - } - } } - depends_on = [ google_organization_iam_audit_config.config, google_organization_iam_binding.authoritative, diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index ced5cad3..619056a0 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -183,18 +183,6 @@ variable "org_policies" { type = map(object({ inherit_from_parent = optional(bool) # for list policies only. reset = optional(bool) - - # default (unconditional) values - allow = optional(object({ - all = optional(bool) - values = optional(list(string)) - })) - deny = optional(object({ - all = optional(bool) - values = optional(list(string)) - })) - enforce = optional(bool, true) # for boolean policies only. - # conditional values rules = optional(list(object({ allow = optional(object({ all = optional(bool) @@ -204,13 +192,13 @@ variable "org_policies" { all = optional(bool) values = optional(list(string)) })) - enforce = optional(bool, true) # for boolean policies only. - condition = object({ + enforce = optional(bool) # for boolean policies only. + condition = optional(object({ description = optional(string) expression = optional(string) location = optional(string) title = optional(string) - }) + }), {}) })), []) })) default = {} diff --git a/modules/project/README.md b/modules/project/README.md index 0cf77df7..dcc0643b 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -243,40 +243,46 @@ module "project" { prefix = "foo" org_policies = { "compute.disableGuestAttributesAccess" = { - enforce = true + rules = [{ enforce = true }] } "constraints/compute.skipDefaultNetworkCreation" = { - enforce = true + rules = [{ enforce = true }] } "iam.disableServiceAccountKeyCreation" = { - enforce = true + rules = [{ enforce = true }] } "iam.disableServiceAccountKeyUpload" = { - enforce = false rules = [ { condition = { - expression = "resource.matchTagId(\"tagKeys/1234\", \"tagValues/1234\")" + expression = "resource.matchTagId('tagKeys/1234', 'tagValues/1234')" title = "condition" description = "test condition" location = "somewhere" } enforce = true + }, + { + enforce = false } ] } "constraints/iam.allowedPolicyMemberDomains" = { - allow = { - values = ["C0xxxxxxx", "C0yyyyyyy"] - } + rules = [{ + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + }] } "constraints/compute.trustedImageProjects" = { - allow = { - values = ["projects/my-project"] - } + rules = [{ + allow = { + values = ["projects/my-project"] + } + }] } "constraints/compute.vmExternalIpAccess" = { - deny = { all = true } + rules = [{ deny = { all = true } }] } } } @@ -306,36 +312,42 @@ module "project" { ```yaml # tftest-file id=boolean path=configs/org-policies/boolean.yaml compute.disableGuestAttributesAccess: - enforce: true + rules: + - enforce: true constraints/compute.skipDefaultNetworkCreation: - enforce: true + rules: + - enforce: true iam.disableServiceAccountKeyCreation: - enforce: true + rules: + - enforce: true iam.disableServiceAccountKeyUpload: - enforce: false rules: - condition: description: test condition - expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + expression: resource.matchTagId('tagKeys/1234', 'tagValues/1234') location: somewhere title: condition enforce: true + - enforce: false ``` ```yaml # tftest-file id=list path=configs/org-policies/list.yaml constraints/compute.trustedImageProjects: - allow: - values: - - projects/my-project + rules: + - allow: + values: + - projects/my-project constraints/compute.vmExternalIpAccess: - deny: - all: true + rules: + - deny: + all: true constraints/iam.allowedPolicyMemberDomains: - allow: - values: - - C0xxxxxxx - - C0yyyyyyy + rules: + - allow: + values: + - C0xxxxxxx + - C0yyyyyyy ``` diff --git a/modules/project/organization-policies.tf b/modules/project/organization-policies.tf index 4ff5bb99..37e6f253 100644 --- a/modules/project/organization-policies.tf +++ b/modules/project/organization-policies.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,16 +28,6 @@ locals { k => { inherit_from_parent = try(v.inherit_from_parent, null) reset = try(v.reset, null) - allow = can(v.allow) ? { - all = try(v.allow.all, null) - values = try(v.allow.values, null) - } : null - deny = can(v.deny) ? { - all = try(v.deny.all, null) - values = try(v.deny.values, null) - } : null - enforce = try(v.enforce, true) - rules = [ for r in try(v.rules, []) : { allow = can(r.allow) ? { @@ -48,7 +38,7 @@ locals { all = try(r.deny.all, null) values = try(r.deny.values, null) } : null - enforce = try(r.enforce, true) + enforce = try(r.enforce, null) condition = { description = try(r.condition.description, null) expression = try(r.condition.expression, null) @@ -67,8 +57,9 @@ locals { k => merge(v, { name = "projects/${local.project.project_id}/policies/${k}" parent = "projects/${local.project.project_id}" - - is_boolean_policy = v.allow == null && v.deny == null + is_boolean_policy = ( + alltrue([for r in v.rules : r.allow == null && r.deny == null]) + ) has_values = ( length(coalesce(try(v.allow.values, []), [])) > 0 || length(coalesce(try(v.deny.values, []), [])) > 0 @@ -90,11 +81,9 @@ resource "google_org_policy_policy" "default" { for_each = local.org_policies name = each.value.name parent = each.value.parent - spec { inherit_from_parent = each.value.inherit_from_parent reset = each.value.reset - dynamic "rules" { for_each = each.value.rules iterator = rule @@ -106,11 +95,14 @@ resource "google_org_policy_policy" "default" { ? upper(tostring(rule.value.enforce)) : null ) - condition { - description = rule.value.condition.description - expression = rule.value.condition.expression - location = rule.value.condition.location - title = rule.value.condition.title + dynamic "condition" { + for_each = rule.value.condition.expression != null ? [1] : [] + content { + description = rule.value.condition.description + expression = rule.value.condition.expression + location = rule.value.condition.location + title = rule.value.condition.title + } } dynamic "values" { for_each = rule.value.has_values ? [1] : [] @@ -121,22 +113,5 @@ resource "google_org_policy_policy" "default" { } } } - - rules { - allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null - deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null - enforce = ( - each.value.is_boolean_policy && each.value.enforce != null - ? upper(tostring(each.value.enforce)) - : null - ) - dynamic "values" { - for_each = each.value.has_values ? [1] : [] - content { - allowed_values = try(each.value.allow.values, null) - denied_values = try(each.value.deny.values, null) - } - } - } } } diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 3769a1fb..ede3a8c6 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -147,19 +147,6 @@ variable "org_policies" { type = map(object({ inherit_from_parent = optional(bool) # for list policies only. reset = optional(bool) - - # default (unconditional) values - allow = optional(object({ - all = optional(bool) - values = optional(list(string)) - })) - deny = optional(object({ - all = optional(bool) - values = optional(list(string)) - })) - enforce = optional(bool, true) # for boolean policies only. - - # conditional values rules = optional(list(object({ allow = optional(object({ all = optional(bool) @@ -169,13 +156,13 @@ variable "org_policies" { all = optional(bool) values = optional(list(string)) })) - enforce = optional(bool, true) # for boolean policies only. - condition = object({ + enforce = optional(bool) # for boolean policies only. + condition = optional(object({ description = optional(string) expression = optional(string) location = optional(string) title = optional(string) - }) + }), {}) })), []) })) default = {} diff --git a/tests/modules/folder/examples/org-policies.yaml b/tests/modules/folder/examples/org-policies.yaml index f8bf4187..7d2637ea 100644 --- a/tests/modules/folder/examples/org-policies.yaml +++ b/tests/modules/folder/examples/org-policies.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -91,7 +91,7 @@ values: - allow_all: null condition: - description: test condition - expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + expression: resource.matchTagId('tagKeys/1234', 'tagValues/1234') location: somewhere title: condition deny_all: null diff --git a/tests/modules/folder/org_policies_boolean.tfvars b/tests/modules/folder/org_policies_boolean.tfvars index eceafe6d..cf5047a2 100644 --- a/tests/modules/folder/org_policies_boolean.tfvars +++ b/tests/modules/folder/org_policies_boolean.tfvars @@ -1,9 +1,8 @@ org_policies = { "iam.disableServiceAccountKeyCreation" = { - enforce = true + rules = [{ enforce = true }] } "iam.disableServiceAccountKeyUpload" = { - enforce = false rules = [ { condition = { @@ -13,6 +12,9 @@ org_policies = { location = "xxx" } enforce = true + }, + { + enforce = false } ] } diff --git a/tests/modules/folder/org_policies_list.tfvars b/tests/modules/folder/org_policies_list.tfvars index 73807173..2c83de47 100644 --- a/tests/modules/folder/org_policies_list.tfvars +++ b/tests/modules/folder/org_policies_list.tfvars @@ -1,14 +1,15 @@ org_policies = { "compute.vmExternalIpAccess" = { - deny = { all = true } + rules = [{ deny = { all = true } }] } "iam.allowedPolicyMemberDomains" = { - allow = { - values = ["C0xxxxxxx", "C0yyyyyyy"] - } + rules = [{ + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + }] } "compute.restrictLoadBalancerCreationForTypes" = { - deny = { values = ["in:EXTERNAL"] } rules = [ { condition = { @@ -31,6 +32,9 @@ org_policies = { allow = { all = true } + }, + { + deny = { values = ["in:EXTERNAL"] } } ] } diff --git a/tests/modules/organization/examples/basic.yaml b/tests/modules/organization/examples/basic.yaml index f7b63a1d..a751622d 100644 --- a/tests/modules/organization/examples/basic.yaml +++ b/tests/modules/organization/examples/basic.yaml @@ -71,8 +71,23 @@ values: - inherit_from_parent: null reset: null rules: + - allow_all: 'TRUE' + condition: + - description: Allow external identities when resource has the `allowexternal` + tag set to true. + expression: resource.matchTag('1234567890/allowexternal', 'true') + location: null + title: Allow external identities + deny_all: null + enforce: null + values: [] - allow_all: null - condition: [] + condition: + - description: For any resource without allowexternal=true, only allow identities + from restricted domains. + expression: '!resource.matchTag(''1234567890/allowexternal'', ''true'')' + location: null + title: '' deny_all: null enforce: null values: @@ -102,7 +117,7 @@ values: - allow_all: null condition: - description: test condition - expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + expression: resource.matchTagId('tagKeys/1234', 'tagValues/1234') location: somewhere title: condition deny_all: null @@ -141,6 +156,20 @@ values: member: user:compute@example.org org_id: '1234567890' role: roles/container.viewer + module.org.google_tags_tag_key.default["allowexternal"]: + description: Allow external identities. + parent: organizations/1234567890 + purpose: null + purpose_data: null + short_name: allowexternal + module.org.google_tags_tag_value.default["allowexternal/false"]: + short_name: 'false' + module.org.google_tags_tag_value.default["allowexternal/true"]: + short_name: 'true' + counts: google_org_policy_policy: 8 google_organization_iam_binding: 3 + google_organization_iam_member: 2 + google_tags_tag_key: 1 + google_tags_tag_value: 2 diff --git a/tests/modules/organization/org_policies_boolean.tfvars b/tests/modules/organization/org_policies_boolean.tfvars index eceafe6d..cd0f032c 100644 --- a/tests/modules/organization/org_policies_boolean.tfvars +++ b/tests/modules/organization/org_policies_boolean.tfvars @@ -1,9 +1,9 @@ org_policies = { "iam.disableServiceAccountKeyCreation" = { - enforce = true + rules = [{ enforce = true }] } "iam.disableServiceAccountKeyUpload" = { - enforce = false + rules = [ { condition = { @@ -13,6 +13,9 @@ org_policies = { location = "xxx" } enforce = true + }, + { + enforce = false } ] } diff --git a/tests/modules/organization/org_policies_list.tfvars b/tests/modules/organization/org_policies_list.tfvars index f9de8dba..d03f8530 100644 --- a/tests/modules/organization/org_policies_list.tfvars +++ b/tests/modules/organization/org_policies_list.tfvars @@ -1,15 +1,17 @@ org_policies = { "compute.vmExternalIpAccess" = { - deny = { all = true } + rules = [{ deny = { all = true } }] } "iam.allowedPolicyMemberDomains" = { inherit_from_parent = true - allow = { - values = ["C0xxxxxxx", "C0yyyyyyy"] - } + rules = [{ + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + }] + } "compute.restrictLoadBalancerCreationForTypes" = { - deny = { values = ["in:EXTERNAL"] } rules = [ { condition = { @@ -32,6 +34,9 @@ org_policies = { allow = { all = true } + }, + { + deny = { values = ["in:EXTERNAL"] } } ] } diff --git a/tests/modules/organization/test_plan_org_policies_modules.py b/tests/modules/organization/test_plan_org_policies_modules.py index 1d19ee1e..30881d99 100644 --- a/tests/modules/organization/test_plan_org_policies_modules.py +++ b/tests/modules/organization/test_plan_org_policies_modules.py @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,61 +26,35 @@ def test_policy_implementation(): path = modules_path / module / 'organization-policies.tf' lines[module] = path.open().readlines() - diff1 = difflib.unified_diff(lines['project'], lines['folder']) + diff1 = difflib.unified_diff(lines['project'], lines['folder'], 'project', + 'folder', n=0) assert list(diff1) == [ - '--- \n', - '+++ \n', - '@@ -14,7 +14,7 @@\n', - ' * limitations under the License.\n', - ' */\n', - ' \n', + '--- project\n', + '+++ folder\n', + '@@ -17 +17 @@\n', '-# tfdoc:file:description Project-level organization policies.\n', '+# tfdoc:file:description Folder-level organization policies.\n', - ' \n', - ' locals {\n', - ' _factory_data_raw = merge([\n', - '@@ -65,8 +65,8 @@\n', - ' org_policies = {\n', - ' for k, v in local._org_policies :\n', - ' k => merge(v, {\n', + '@@ -58,2 +58,2 @@\n', '- name = "projects/${local.project.project_id}/policies/${k}"\n', '- parent = "projects/${local.project.project_id}"\n', '+ name = "${local.folder.name}/policies/${k}"\n', '+ parent = local.folder.name\n', - ' \n', - ' is_boolean_policy = v.allow == null && v.deny == null\n', - ' has_values = (\n', ] - diff2 = difflib.unified_diff(lines['folder'], lines['organization']) + diff2 = difflib.unified_diff(lines['folder'], lines['organization'], 'folder', + 'organization', n=0) assert list(diff2) == [ - '--- \n', - '+++ \n', - '@@ -14,7 +14,7 @@\n', - ' * limitations under the License.\n', - ' */\n', - ' \n', + '--- folder\n', + '+++ organization\n', + '@@ -17 +17 @@\n', '-# tfdoc:file:description Folder-level organization policies.\n', '+# tfdoc:file:description Organization-level organization policies.\n', - ' \n', - ' locals {\n', - ' _factory_data_raw = merge([\n', - '@@ -65,8 +65,8 @@\n', - ' org_policies = {\n', - ' for k, v in local._org_policies :\n', - ' k => merge(v, {\n', + '@@ -58,2 +58,2 @@\n', '- name = "${local.folder.name}/policies/${k}"\n', '- parent = local.folder.name\n', '+ name = "${var.organization_id}/policies/${k}"\n', '+ parent = var.organization_id\n', - ' \n', - ' is_boolean_policy = v.allow == null && v.deny == null\n', - ' has_values = (\n', - '@@ -139,4 +139,13 @@\n', - ' }\n', - ' }\n', - ' }\n', - '+\n', + '@@ -116,0 +117,8 @@\n', '+ depends_on = [\n', '+ google_organization_iam_audit_config.config,\n', '+ google_organization_iam_binding.authoritative,\n', @@ -89,5 +63,4 @@ def test_policy_implementation(): '+ google_organization_iam_policy.authoritative,\n', '+ google_org_policy_custom_constraint.constraint,\n', '+ ]\n', - ' }\n', ] diff --git a/tests/modules/project/examples/org-policies.yaml b/tests/modules/project/examples/org-policies.yaml index 8841dede..a426696b 100644 --- a/tests/modules/project/examples/org-policies.yaml +++ b/tests/modules/project/examples/org-policies.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -102,7 +102,7 @@ values: - allow_all: null condition: - description: test condition - expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + expression: resource.matchTagId('tagKeys/1234', 'tagValues/1234') location: somewhere title: condition deny_all: null diff --git a/tests/modules/project/org_policies_boolean.tfvars b/tests/modules/project/org_policies_boolean.tfvars index eceafe6d..cf5047a2 100644 --- a/tests/modules/project/org_policies_boolean.tfvars +++ b/tests/modules/project/org_policies_boolean.tfvars @@ -1,9 +1,8 @@ org_policies = { "iam.disableServiceAccountKeyCreation" = { - enforce = true + rules = [{ enforce = true }] } "iam.disableServiceAccountKeyUpload" = { - enforce = false rules = [ { condition = { @@ -13,6 +12,9 @@ org_policies = { location = "xxx" } enforce = true + }, + { + enforce = false } ] } diff --git a/tests/modules/project/org_policies_list.tfvars b/tests/modules/project/org_policies_list.tfvars index f9de8dba..617c5bf0 100644 --- a/tests/modules/project/org_policies_list.tfvars +++ b/tests/modules/project/org_policies_list.tfvars @@ -1,15 +1,17 @@ org_policies = { "compute.vmExternalIpAccess" = { - deny = { all = true } + rules = [{ deny = { all = true } }] } "iam.allowedPolicyMemberDomains" = { inherit_from_parent = true - allow = { - values = ["C0xxxxxxx", "C0yyyyyyy"] - } + rules = [{ + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + }] } "compute.restrictLoadBalancerCreationForTypes" = { - deny = { values = ["in:EXTERNAL"] } + rules = [ { condition = { @@ -32,6 +34,9 @@ org_policies = { allow = { all = true } + }, + { + deny = { values = ["in:EXTERNAL"] } } ] } From d3bcf625f9384d94ae54f65ff92fed787333015e Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 21 Feb 2023 13:58:08 +0100 Subject: [PATCH 36/47] Update yaml org policies --- .../data/org-policies/compute.yaml | 81 ++++++++++++------- .../data/org-policies/iam.yaml | 9 ++- .../data/org-policies/serverless.yaml | 29 ++++--- .../data/org-policies/sql.yaml | 6 +- .../data/org-policies/storage.yaml | 3 +- .../data/org-policies/compute.yaml | 81 ++++++++++++------- .../data/org-policies/iam.yaml | 9 ++- .../data/org-policies/serverless.yaml | 29 ++++--- .../data/org-policies/storage.yaml | 3 +- .../1-resman/data/org-policies/compute.yaml | 81 ++++++++++++------- .../1-resman/data/org-policies/iam.yaml | 9 ++- .../data/org-policies/serverless.yaml | 29 ++++--- .../1-resman/data/org-policies/sql.yaml | 6 +- .../1-resman/data/org-policies/storage.yaml | 3 +- .../modules/project/org_policies_list.tfvars | 1 - 15 files changed, 233 insertions(+), 146 deletions(-) diff --git a/blueprints/data-solutions/shielded-folder/data/org-policies/compute.yaml b/blueprints/data-solutions/shielded-folder/data/org-policies/compute.yaml index 0d27ac42..a3f96b1b 100644 --- a/blueprints/data-solutions/shielded-folder/data/org-policies/compute.yaml +++ b/blueprints/data-solutions/shielded-folder/data/org-policies/compute.yaml @@ -3,71 +3,90 @@ # sample subset of useful organization policies, edit to suit requirements compute.disableGuestAttributesAccess: - enforce: true + rules: + - enforce: true compute.requireOsLogin: - enforce: true + rules: + - enforce: true compute.restrictLoadBalancerCreationForTypes: - allow: - values: - - in:INTERNAL + rules: + - allow: + values: + - in:INTERNAL compute.skipDefaultNetworkCreation: - enforce: true + rules: + - enforce: true compute.vmExternalIpAccess: - deny: - all: true + rules: + - deny: + all: true # compute.disableInternetNetworkEndpointGroup: -# enforce: true +# rules: +# - enforce: true # compute.disableNestedVirtualization: -# enforce: true +# rules: +# - enforce: true # compute.disableSerialPortAccess: -# enforce: true +# rules: +# - enforce: true # compute.restrictCloudNATUsage: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictDedicatedInterconnectUsage: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictPartnerInterconnectUsage: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictProtocolForwardingCreationForTypes: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictSharedVpcHostProjects: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictSharedVpcSubnetworks: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictVpcPeering: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictVpnPeerIPs: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictXpnProjectLienRemoval: -# enforce: true +# rules: +# - enforce: true # compute.setNewProjectDefaultToZonalDNSOnly: -# enforce: true +# rules: +# - enforce: true # compute.vmCanIpForward: -# deny: -# all: true +# rules: +# - deny: +# all: true diff --git a/blueprints/data-solutions/shielded-folder/data/org-policies/iam.yaml b/blueprints/data-solutions/shielded-folder/data/org-policies/iam.yaml index 4d83f827..58e0032c 100644 --- a/blueprints/data-solutions/shielded-folder/data/org-policies/iam.yaml +++ b/blueprints/data-solutions/shielded-folder/data/org-policies/iam.yaml @@ -3,10 +3,13 @@ # sample subset of useful organization policies, edit to suit requirements iam.automaticIamGrantsForDefaultServiceAccounts: - enforce: true + rules: + - enforce: true iam.disableServiceAccountKeyCreation: - enforce: true + rules: + - enforce: true iam.disableServiceAccountKeyUpload: - enforce: true + rules: + - enforce: true diff --git a/blueprints/data-solutions/shielded-folder/data/org-policies/serverless.yaml b/blueprints/data-solutions/shielded-folder/data/org-policies/serverless.yaml index de62e6c7..3efb23cd 100644 --- a/blueprints/data-solutions/shielded-folder/data/org-policies/serverless.yaml +++ b/blueprints/data-solutions/shielded-folder/data/org-policies/serverless.yaml @@ -3,24 +3,29 @@ # sample subset of useful organization policies, edit to suit requirements run.allowedIngress: - allow: - values: - - is:internal + rules: + - allow: + values: + - is:internal # run.allowedVPCEgress: -# allow: -# values: +# rules: +# - allow: +# values: # - is:private-ranges-only # cloudfunctions.allowedIngressSettings: -# allow: -# values: -# - is:ALLOW_INTERNAL_ONLY +# rules: +# - allow: +# values: +# - is:ALLOW_INTERNAL_ONLY # cloudfunctions.allowedVpcConnectorEgressSettings: -# allow: -# values: -# - is:PRIVATE_RANGES_ONLY +# rules: +# - allow: +# values: +# - is:PRIVATE_RANGES_ONLY # cloudfunctions.requireVPCConnector: -# enforce: true +# rules: +# - enforce: true diff --git a/blueprints/data-solutions/shielded-folder/data/org-policies/sql.yaml b/blueprints/data-solutions/shielded-folder/data/org-policies/sql.yaml index 88b84d9d..0eee8045 100644 --- a/blueprints/data-solutions/shielded-folder/data/org-policies/sql.yaml +++ b/blueprints/data-solutions/shielded-folder/data/org-policies/sql.yaml @@ -3,7 +3,9 @@ # sample subset of useful organization policies, edit to suit requirements sql.restrictAuthorizedNetworks: - enforce: true + rules: + - enforce: true sql.restrictPublicIp: - enforce: true + rules: + - enforce: true diff --git a/blueprints/data-solutions/shielded-folder/data/org-policies/storage.yaml b/blueprints/data-solutions/shielded-folder/data/org-policies/storage.yaml index 6c0a673f..448357b8 100644 --- a/blueprints/data-solutions/shielded-folder/data/org-policies/storage.yaml +++ b/blueprints/data-solutions/shielded-folder/data/org-policies/storage.yaml @@ -3,4 +3,5 @@ # sample subset of useful organization policies, edit to suit requirements storage.uniformBucketLevelAccess: - enforce: true + rules: + - enforce: true diff --git a/fast/stages-multitenant/1-resman-tenant/data/org-policies/compute.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/compute.yaml index 0d27ac42..a3f96b1b 100644 --- a/fast/stages-multitenant/1-resman-tenant/data/org-policies/compute.yaml +++ b/fast/stages-multitenant/1-resman-tenant/data/org-policies/compute.yaml @@ -3,71 +3,90 @@ # sample subset of useful organization policies, edit to suit requirements compute.disableGuestAttributesAccess: - enforce: true + rules: + - enforce: true compute.requireOsLogin: - enforce: true + rules: + - enforce: true compute.restrictLoadBalancerCreationForTypes: - allow: - values: - - in:INTERNAL + rules: + - allow: + values: + - in:INTERNAL compute.skipDefaultNetworkCreation: - enforce: true + rules: + - enforce: true compute.vmExternalIpAccess: - deny: - all: true + rules: + - deny: + all: true # compute.disableInternetNetworkEndpointGroup: -# enforce: true +# rules: +# - enforce: true # compute.disableNestedVirtualization: -# enforce: true +# rules: +# - enforce: true # compute.disableSerialPortAccess: -# enforce: true +# rules: +# - enforce: true # compute.restrictCloudNATUsage: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictDedicatedInterconnectUsage: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictPartnerInterconnectUsage: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictProtocolForwardingCreationForTypes: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictSharedVpcHostProjects: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictSharedVpcSubnetworks: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictVpcPeering: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictVpnPeerIPs: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictXpnProjectLienRemoval: -# enforce: true +# rules: +# - enforce: true # compute.setNewProjectDefaultToZonalDNSOnly: -# enforce: true +# rules: +# - enforce: true # compute.vmCanIpForward: -# deny: -# all: true +# rules: +# - deny: +# all: true diff --git a/fast/stages-multitenant/1-resman-tenant/data/org-policies/iam.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/iam.yaml index 4d83f827..58e0032c 100644 --- a/fast/stages-multitenant/1-resman-tenant/data/org-policies/iam.yaml +++ b/fast/stages-multitenant/1-resman-tenant/data/org-policies/iam.yaml @@ -3,10 +3,13 @@ # sample subset of useful organization policies, edit to suit requirements iam.automaticIamGrantsForDefaultServiceAccounts: - enforce: true + rules: + - enforce: true iam.disableServiceAccountKeyCreation: - enforce: true + rules: + - enforce: true iam.disableServiceAccountKeyUpload: - enforce: true + rules: + - enforce: true diff --git a/fast/stages-multitenant/1-resman-tenant/data/org-policies/serverless.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/serverless.yaml index de62e6c7..3efb23cd 100644 --- a/fast/stages-multitenant/1-resman-tenant/data/org-policies/serverless.yaml +++ b/fast/stages-multitenant/1-resman-tenant/data/org-policies/serverless.yaml @@ -3,24 +3,29 @@ # sample subset of useful organization policies, edit to suit requirements run.allowedIngress: - allow: - values: - - is:internal + rules: + - allow: + values: + - is:internal # run.allowedVPCEgress: -# allow: -# values: +# rules: +# - allow: +# values: # - is:private-ranges-only # cloudfunctions.allowedIngressSettings: -# allow: -# values: -# - is:ALLOW_INTERNAL_ONLY +# rules: +# - allow: +# values: +# - is:ALLOW_INTERNAL_ONLY # cloudfunctions.allowedVpcConnectorEgressSettings: -# allow: -# values: -# - is:PRIVATE_RANGES_ONLY +# rules: +# - allow: +# values: +# - is:PRIVATE_RANGES_ONLY # cloudfunctions.requireVPCConnector: -# enforce: true +# rules: +# - enforce: true diff --git a/fast/stages-multitenant/1-resman-tenant/data/org-policies/storage.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/storage.yaml index 6c0a673f..448357b8 100644 --- a/fast/stages-multitenant/1-resman-tenant/data/org-policies/storage.yaml +++ b/fast/stages-multitenant/1-resman-tenant/data/org-policies/storage.yaml @@ -3,4 +3,5 @@ # sample subset of useful organization policies, edit to suit requirements storage.uniformBucketLevelAccess: - enforce: true + rules: + - enforce: true diff --git a/fast/stages/1-resman/data/org-policies/compute.yaml b/fast/stages/1-resman/data/org-policies/compute.yaml index 0d27ac42..a3f96b1b 100644 --- a/fast/stages/1-resman/data/org-policies/compute.yaml +++ b/fast/stages/1-resman/data/org-policies/compute.yaml @@ -3,71 +3,90 @@ # sample subset of useful organization policies, edit to suit requirements compute.disableGuestAttributesAccess: - enforce: true + rules: + - enforce: true compute.requireOsLogin: - enforce: true + rules: + - enforce: true compute.restrictLoadBalancerCreationForTypes: - allow: - values: - - in:INTERNAL + rules: + - allow: + values: + - in:INTERNAL compute.skipDefaultNetworkCreation: - enforce: true + rules: + - enforce: true compute.vmExternalIpAccess: - deny: - all: true + rules: + - deny: + all: true # compute.disableInternetNetworkEndpointGroup: -# enforce: true +# rules: +# - enforce: true # compute.disableNestedVirtualization: -# enforce: true +# rules: +# - enforce: true # compute.disableSerialPortAccess: -# enforce: true +# rules: +# - enforce: true # compute.restrictCloudNATUsage: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictDedicatedInterconnectUsage: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictPartnerInterconnectUsage: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictProtocolForwardingCreationForTypes: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictSharedVpcHostProjects: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictSharedVpcSubnetworks: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictVpcPeering: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictVpnPeerIPs: -# deny: -# all: true +# rules: +# - deny: +# all: true # compute.restrictXpnProjectLienRemoval: -# enforce: true +# rules: +# - enforce: true # compute.setNewProjectDefaultToZonalDNSOnly: -# enforce: true +# rules: +# - enforce: true # compute.vmCanIpForward: -# deny: -# all: true +# rules: +# - deny: +# all: true diff --git a/fast/stages/1-resman/data/org-policies/iam.yaml b/fast/stages/1-resman/data/org-policies/iam.yaml index 4d83f827..58e0032c 100644 --- a/fast/stages/1-resman/data/org-policies/iam.yaml +++ b/fast/stages/1-resman/data/org-policies/iam.yaml @@ -3,10 +3,13 @@ # sample subset of useful organization policies, edit to suit requirements iam.automaticIamGrantsForDefaultServiceAccounts: - enforce: true + rules: + - enforce: true iam.disableServiceAccountKeyCreation: - enforce: true + rules: + - enforce: true iam.disableServiceAccountKeyUpload: - enforce: true + rules: + - enforce: true diff --git a/fast/stages/1-resman/data/org-policies/serverless.yaml b/fast/stages/1-resman/data/org-policies/serverless.yaml index de62e6c7..3efb23cd 100644 --- a/fast/stages/1-resman/data/org-policies/serverless.yaml +++ b/fast/stages/1-resman/data/org-policies/serverless.yaml @@ -3,24 +3,29 @@ # sample subset of useful organization policies, edit to suit requirements run.allowedIngress: - allow: - values: - - is:internal + rules: + - allow: + values: + - is:internal # run.allowedVPCEgress: -# allow: -# values: +# rules: +# - allow: +# values: # - is:private-ranges-only # cloudfunctions.allowedIngressSettings: -# allow: -# values: -# - is:ALLOW_INTERNAL_ONLY +# rules: +# - allow: +# values: +# - is:ALLOW_INTERNAL_ONLY # cloudfunctions.allowedVpcConnectorEgressSettings: -# allow: -# values: -# - is:PRIVATE_RANGES_ONLY +# rules: +# - allow: +# values: +# - is:PRIVATE_RANGES_ONLY # cloudfunctions.requireVPCConnector: -# enforce: true +# rules: +# - enforce: true diff --git a/fast/stages/1-resman/data/org-policies/sql.yaml b/fast/stages/1-resman/data/org-policies/sql.yaml index 88b84d9d..0eee8045 100644 --- a/fast/stages/1-resman/data/org-policies/sql.yaml +++ b/fast/stages/1-resman/data/org-policies/sql.yaml @@ -3,7 +3,9 @@ # sample subset of useful organization policies, edit to suit requirements sql.restrictAuthorizedNetworks: - enforce: true + rules: + - enforce: true sql.restrictPublicIp: - enforce: true + rules: + - enforce: true diff --git a/fast/stages/1-resman/data/org-policies/storage.yaml b/fast/stages/1-resman/data/org-policies/storage.yaml index 6c0a673f..448357b8 100644 --- a/fast/stages/1-resman/data/org-policies/storage.yaml +++ b/fast/stages/1-resman/data/org-policies/storage.yaml @@ -3,4 +3,5 @@ # sample subset of useful organization policies, edit to suit requirements storage.uniformBucketLevelAccess: - enforce: true + rules: + - enforce: true diff --git a/tests/modules/project/org_policies_list.tfvars b/tests/modules/project/org_policies_list.tfvars index 617c5bf0..4889547d 100644 --- a/tests/modules/project/org_policies_list.tfvars +++ b/tests/modules/project/org_policies_list.tfvars @@ -11,7 +11,6 @@ org_policies = { }] } "compute.restrictLoadBalancerCreationForTypes" = { - rules = [ { condition = { From 62834ca83ad0d6e4d8cba35183d330fe1c2547d2 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 21 Feb 2023 14:01:24 +0100 Subject: [PATCH 37/47] Update READMEs --- modules/folder/README.md | 8 ++++---- modules/organization/README.md | 14 +++++++------- modules/project/README.md | 34 +++++++++++++++++----------------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/modules/folder/README.md b/modules/folder/README.md index fb84ec42..dc6a2dd3 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -346,10 +346,10 @@ module "folder" { | [logging_exclusions](variables.tf#L98) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L105) | Logging sinks to create for the organization. | map(object({…})) | | {} | | [name](variables.tf#L135) | Folder name. | string | | null | -| [org_policies](variables.tf#L141) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | -| [org_policies_data_path](variables.tf#L181) | Path containing org policies in YAML format. | string | | null | -| [parent](variables.tf#L187) | Parent in folders/folder_id or organizations/org_id format. | string | | null | -| [tag_bindings](variables.tf#L197) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [org_policies](variables.tf#L141) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | +| [org_policies_data_path](variables.tf#L168) | Path containing org policies in YAML format. | string | | null | +| [parent](variables.tf#L174) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [tag_bindings](variables.tf#L184) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/organization/README.md b/modules/organization/README.md index 907cf5f4..926f4f07 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -474,7 +474,7 @@ module "org" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L246) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L234) | Organization id in organizations/nnnnnn format. | string | ✓ | | | [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | | [custom_roles](variables.tf#L24) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | | [firewall_policies](variables.tf#L31) | Hierarchical firewall policy rules created in the organization. | map(map(object({…}))) | | {} | @@ -490,12 +490,12 @@ module "org" { | [logging_exclusions](variables.tf#L122) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L129) | Logging sinks to create for the organization. | map(object({…})) | | {} | | [network_tags](variables.tf#L159) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [org_policies](variables.tf#L181) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [org_policies_data_path](variables.tf#L220) | Path containing org policies in YAML format. | string | | null | -| [org_policy_custom_constraints](variables.tf#L226) | Organization policiy custom constraints keyed by constraint name. | map(object({…})) | | {} | -| [org_policy_custom_constraints_data_path](variables.tf#L240) | Path containing org policy custom constraints in YAML format. | string | | null | -| [tag_bindings](variables.tf#L255) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | -| [tags](variables.tf#L261) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [org_policies](variables.tf#L181) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [org_policies_data_path](variables.tf#L208) | Path containing org policies in YAML format. | string | | null | +| [org_policy_custom_constraints](variables.tf#L214) | Organization policiy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [org_policy_custom_constraints_data_path](variables.tf#L228) | Path containing org policy custom constraints in YAML format. | string | | null | +| [tag_bindings](variables.tf#L243) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | +| [tags](variables.tf#L249) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | ## Outputs diff --git a/modules/project/README.md b/modules/project/README.md index dcc0643b..eb91991c 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -534,23 +534,23 @@ output "compute_robot" { | [logging_exclusions](variables.tf#L95) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L102) | Logging sinks to create for this project. | map(object({…})) | | {} | | [metric_scopes](variables.tf#L133) | List of projects that will act as metric scopes for this project. | list(string) | | [] | -| [org_policies](variables.tf#L145) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | -| [org_policies_data_path](variables.tf#L185) | Path containing org policies in YAML format. | string | | null | -| [oslogin](variables.tf#L191) | Enable OS Login. | bool | | false | -| [oslogin_admins](variables.tf#L197) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | -| [oslogin_users](variables.tf#L205) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | -| [parent](variables.tf#L212) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [prefix](variables.tf#L222) | Optional prefix used to generate project id and name. | string | | null | -| [project_create](variables.tf#L232) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | -| [service_config](variables.tf#L238) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L250) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | -| [service_perimeter_bridges](variables.tf#L257) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | -| [service_perimeter_standard](variables.tf#L264) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | -| [services](variables.tf#L270) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L276) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L285) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | -| [skip_delete](variables.tf#L295) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | -| [tag_bindings](variables.tf#L301) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | +| [org_policies](variables.tf#L145) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [org_policies_data_path](variables.tf#L172) | Path containing org policies in YAML format. | string | | null | +| [oslogin](variables.tf#L178) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L184) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L192) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L199) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L209) | Optional prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L219) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L225) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L237) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L244) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L251) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L257) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L263) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L272) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L282) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L288) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | ## Outputs From a5e905cb80f4d1d07b55795b07c5543d82ae4e36 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 21 Feb 2023 14:28:23 +0100 Subject: [PATCH 38/47] Update remaining org policies --- .../data-solutions/data-playground/main.tf | 6 +-- .../data-solutions/vertex-mlops/main.tf | 8 ++-- .../factories/project-factory/README.md | 41 ++++++++++--------- .../sample-data/projects/project.yaml | 19 +++++---- .../factories/project-factory/variables.tf | 14 +------ blueprints/gke/multitenant-fleet/main.tf | 6 +-- blueprints/networking/filtering-proxy/main.tf | 6 +-- .../1-resman-tenant/branch-sandbox.tf | 4 +- .../data/org-policies/sql.yaml | 6 ++- fast/stages/1-resman/branch-sandbox.tf | 6 +-- fast/stages/1-resman/organization.tf | 8 +++- .../dev/data/projects/project.yaml.sample | 19 +++++---- modules/folder/README.md | 8 ++-- modules/organization/README.md | 8 ++-- modules/project/README.md | 16 ++++---- .../fixture/projects/project.yaml | 17 ++++---- .../data/projects/project.yaml | 19 ++++----- .../modules/folder/examples/org-policies.yaml | 8 ++-- .../modules/organization/examples/basic.yaml | 16 ++++---- .../organization/org_policies_boolean.tfvars | 1 - .../project/examples/org-policies.yaml | 16 ++++---- 21 files changed, 126 insertions(+), 126 deletions(-) diff --git a/blueprints/data-solutions/data-playground/main.tf b/blueprints/data-solutions/data-playground/main.tf index 548bee37..a3cfd54e 100644 --- a/blueprints/data-solutions/data-playground/main.tf +++ b/blueprints/data-solutions/data-playground/main.tf @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -83,8 +83,8 @@ module "project" { } org_policies = { - # "constraints/compute.requireOsLogin" = { - # enforce = false + # "compute.requireOsLogin" = { + # rules = [{ enforce = false }] # } # Example of applying a project wide policy, mainly useful for Composer 1 } diff --git a/blueprints/data-solutions/vertex-mlops/main.tf b/blueprints/data-solutions/vertex-mlops/main.tf index 5f7fbc0c..27129298 100644 --- a/blueprints/data-solutions/vertex-mlops/main.tf +++ b/blueprints/data-solutions/vertex-mlops/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -112,7 +112,7 @@ module "gcs-bucket" { encryption_key = try(local.service_encryption_keys.storage, null) } -# Default bucket for Cloud Build to prevent error: "'us' violates constraint ‘constraints/gcp.resourceLocations’" +# Default bucket for Cloud Build to prevent error: "'us' violates constraint ‘gcp.resourceLocations’" # https://stackoverflow.com/questions/53206667/cloud-build-fails-with-resource-location-constraint module "gcs-bucket-cloudbuild" { source = "../../../modules/gcs" @@ -230,8 +230,8 @@ module "project" { org_policies = { # Example of applying a project wide policy - # "constraints/compute.requireOsLogin" = { - # enforce = false + # "compute.requireOsLogin" = { + # rules = [{ enforce = false }] # } } diff --git a/blueprints/factories/project-factory/README.md b/blueprints/factories/project-factory/README.md index 2b8c3874..df8701d2 100644 --- a/blueprints/factories/project-factory/README.md +++ b/blueprints/factories/project-factory/README.md @@ -156,15 +156,18 @@ labels: # [opt] Org policy overrides defined at project level org_policies: - constraints/compute.disableGuestAttributesAccess: - enforce: true - constraints/compute.trustedImageProjects: - allow: - values: + compute.disableGuestAttributesAccess: + rules: + - enforce: true + compute.trustedImageProjects: + rules: + - allow: + values: - projects/fast-dev-iac-core-0 - constraints/compute.vmExternalIpAccess: - deny: - all: true + compute.vmExternalIpAccess: + rules: + - deny: + all: true # [opt] Service account to create for the project and their roles on the project # in name => [roles] format @@ -223,8 +226,8 @@ vpc: | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [billing_account_id](variables.tf#L17) | Billing account id. | string | ✓ | | -| [prefix](variables.tf#L157) | Prefix used for resource names. | string | ✓ | | -| [project_id](variables.tf#L166) | Project id. | string | ✓ | | +| [prefix](variables.tf#L145) | Prefix used for resource names. | string | ✓ | | +| [project_id](variables.tf#L154) | Project id. | string | ✓ | | | [billing_alert](variables.tf#L22) | Billing alert configuration. | object({…}) | | null | | [defaults](variables.tf#L35) | Project factory default values. | object({…}) | | null | | [descriptive_name](variables.tf#L57) | Name of the project name. Used for project name instead of `name` variable. | string | | null | @@ -237,15 +240,15 @@ vpc: | [iam_additive](variables.tf#L99) | Custom additive IAM settings in role => [principal] format. | map(list(string)) | | {} | | [kms_service_agents](variables.tf#L105) | KMS IAM configuration in as service => [key]. | map(list(string)) | | {} | | [labels](variables.tf#L111) | Labels to be assigned at project level. | map(string) | | {} | -| [org_policies](variables.tf#L117) | Org-policy overrides at project level. | map(object({…})) | | {} | -| [service_accounts](variables.tf#L171) | Service accounts to be created, and roles assigned them on the project. | map(list(string)) | | {} | -| [service_accounts_additive](variables.tf#L177) | Service accounts to be created, and roles assigned them on the project additively. | map(list(string)) | | {} | -| [service_accounts_iam](variables.tf#L183) | IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | -| [service_accounts_iam_additive](variables.tf#L190) | IAM additive bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | -| [service_identities_iam](variables.tf#L197) | Custom IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | -| [service_identities_iam_additive](variables.tf#L204) | Custom additive IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | -| [services](variables.tf#L211) | Services to be enabled for the project. | list(string) | | [] | -| [vpc](variables.tf#L218) | VPC configuration for the project. | object({…}) | | null | +| [org_policies](variables.tf#L117) | Org-policy overrides at project level. | map(object({…})) | | {} | +| [service_accounts](variables.tf#L159) | Service accounts to be created, and roles assigned them on the project. | map(list(string)) | | {} | +| [service_accounts_additive](variables.tf#L165) | Service accounts to be created, and roles assigned them on the project additively. | map(list(string)) | | {} | +| [service_accounts_iam](variables.tf#L171) | IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | +| [service_accounts_iam_additive](variables.tf#L178) | IAM additive bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | +| [service_identities_iam](variables.tf#L185) | Custom IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | +| [service_identities_iam_additive](variables.tf#L192) | Custom additive IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | +| [services](variables.tf#L199) | Services to be enabled for the project. | list(string) | | [] | +| [vpc](variables.tf#L206) | VPC configuration for the project. | object({…}) | | null | ## Outputs diff --git a/blueprints/factories/project-factory/sample-data/projects/project.yaml b/blueprints/factories/project-factory/sample-data/projects/project.yaml index 03449913..cd7b1837 100644 --- a/blueprints/factories/project-factory/sample-data/projects/project.yaml +++ b/blueprints/factories/project-factory/sample-data/projects/project.yaml @@ -48,15 +48,18 @@ labels: # [opt] Org policy overrides defined at project level org_policies: - constraints/compute.disableGuestAttributesAccess: - enforce: true - constraints/compute.trustedImageProjects: - allow: - values: + compute.disableGuestAttributesAccess: + rules: + - enforce: true + compute.trustedImageProjects: + rules: + - allow: + values: - projects/fast-dev-iac-core-0 - constraints/compute.vmExternalIpAccess: - deny: - all: true + compute.vmExternalIpAccess: + rules: + - deny: + all: true # [opt] Prefix - overrides default if set prefix: test1 diff --git a/blueprints/factories/project-factory/variables.tf b/blueprints/factories/project-factory/variables.tf index 3aa3fa36..b9a83ae3 100644 --- a/blueprints/factories/project-factory/variables.tf +++ b/blueprints/factories/project-factory/variables.tf @@ -119,18 +119,6 @@ variable "org_policies" { type = map(object({ inherit_from_parent = optional(bool) # for list policies only. reset = optional(bool) - - # default (unconditional) values - allow = optional(object({ - all = optional(bool) - values = optional(list(string)) - })) - deny = optional(object({ - all = optional(bool) - values = optional(list(string)) - })) - enforce = optional(bool, true) # for boolean policies only. - # conditional values rules = optional(list(object({ allow = optional(object({ @@ -141,7 +129,7 @@ variable "org_policies" { all = optional(bool) values = optional(list(string)) })) - enforce = optional(bool, true) # for boolean policies only. + enforce = optional(bool) # for boolean policies only. condition = object({ description = optional(string) expression = optional(string) diff --git a/blueprints/gke/multitenant-fleet/main.tf b/blueprints/gke/multitenant-fleet/main.tf index 588d6c5b..4079db99 100644 --- a/blueprints/gke/multitenant-fleet/main.tf +++ b/blueprints/gke/multitenant-fleet/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,10 +64,10 @@ module "gke-project-0" { } # specify project-level org policies here if you need them # policy_boolean = { - # "constraints/compute.disableGuestAttributesAccess" = true + # "compute.disableGuestAttributesAccess" = true # } # policy_list = { - # "constraints/compute.trustedImageProjects" = { + # "compute.trustedImageProjects" = { # inherit_from_parent = null # suggested_value = null # status = true diff --git a/blueprints/networking/filtering-proxy/main.tf b/blueprints/networking/filtering-proxy/main.tf index 06efa814..b36f0140 100644 --- a/blueprints/networking/filtering-proxy/main.tf +++ b/blueprints/networking/filtering-proxy/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -227,8 +227,8 @@ module "folder-apps" { name = "apps" org_policies = { # prevent VMs with public IPs in the apps folder - "constraints/compute.vmExternalIpAccess" = { - deny = { all = true } + "compute.vmExternalIpAccess" = { + rules = [{ deny = { all = true } }] } } } diff --git a/fast/stages-multitenant/1-resman-tenant/branch-sandbox.tf b/fast/stages-multitenant/1-resman-tenant/branch-sandbox.tf index 6f3d526c..39ab03ed 100644 --- a/fast/stages-multitenant/1-resman-tenant/branch-sandbox.tf +++ b/fast/stages-multitenant/1-resman-tenant/branch-sandbox.tf @@ -28,8 +28,8 @@ module "branch-sandbox-folder" { "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.sandbox] } org_policies = { - "constraints/sql.restrictPublicIp" = { enforce = false } - "constraints/compute.vmExternalIpAccess" = { allow = { all = true } } + "sql.restrictPublicIp" = { rules = [{ enforce = false }] } + "compute.vmExternalIpAccess" = { rules = [{ allow = { all = true } }] } } tag_bindings = { context = var.tags.values["${var.tags.names.context}/sandbox"] diff --git a/fast/stages-multitenant/1-resman-tenant/data/org-policies/sql.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/sql.yaml index 88b84d9d..0eee8045 100644 --- a/fast/stages-multitenant/1-resman-tenant/data/org-policies/sql.yaml +++ b/fast/stages-multitenant/1-resman-tenant/data/org-policies/sql.yaml @@ -3,7 +3,9 @@ # sample subset of useful organization policies, edit to suit requirements sql.restrictAuthorizedNetworks: - enforce: true + rules: + - enforce: true sql.restrictPublicIp: - enforce: true + rules: + - enforce: true diff --git a/fast/stages/1-resman/branch-sandbox.tf b/fast/stages/1-resman/branch-sandbox.tf index 8b54e749..72221bc0 100644 --- a/fast/stages/1-resman/branch-sandbox.tf +++ b/fast/stages/1-resman/branch-sandbox.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,8 +33,8 @@ module "branch-sandbox-folder" { "roles/resourcemanager.projectCreator" = [module.branch-sandbox-sa.0.iam_email] } org_policies = { - "constraints/sql.restrictPublicIp" = { enforce = false } - "constraints/compute.vmExternalIpAccess" = { allow = { all = true } } + "sql.restrictPublicIp" = { rules = [{ enforce = false }] } + "compute.vmExternalIpAccess" = { rules = [{ allow = { all = true } }] } } tag_bindings = { context = try( diff --git a/fast/stages/1-resman/organization.tf b/fast/stages/1-resman/organization.tf index 3d7db46d..a3bc2f0d 100644 --- a/fast/stages/1-resman/organization.tf +++ b/fast/stages/1-resman/organization.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,7 +68,11 @@ module "organization" { # sample subset of useful organization policies, edit to suit requirements org_policies = { - "iam.allowedPolicyMemberDomains" = { allow = { values = local.all_drs_domains } } + "iam.allowedPolicyMemberDomains" = { + rules = [ + { allow = { values = local.all_drs_domains } } + ] + } #"gcp.resourceLocations" = { # allow = { values = local.allowed_regions } diff --git a/fast/stages/3-project-factory/dev/data/projects/project.yaml.sample b/fast/stages/3-project-factory/dev/data/projects/project.yaml.sample index 88ba0bf5..5311019d 100644 --- a/fast/stages/3-project-factory/dev/data/projects/project.yaml.sample +++ b/fast/stages/3-project-factory/dev/data/projects/project.yaml.sample @@ -48,15 +48,18 @@ labels: # [opt] Org policy overrides defined at project level org_policies: - constraints/compute.disableGuestAttributesAccess: - enforce: true - constraints/compute.trustedImageProjects: - allow: - values: + compute.disableGuestAttributesAccess: + rules: + - enforce: true + compute.trustedImageProjects: + rules: + - allow: + values: - projects/fast-dev-iac-core-0 - constraints/compute.vmExternalIpAccess: - deny: - all: true + compute.vmExternalIpAccess: + rules: + - deny: + all: true # [opt] Service account to create for the project and their roles on the project # in name => [roles] format diff --git a/modules/folder/README.md b/modules/folder/README.md index dc6a2dd3..8addd48e 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -44,7 +44,7 @@ module "folder" { "compute.disableGuestAttributesAccess" = { rules = [{ enforce = true }] } - "constraints/compute.skipDefaultNetworkCreation" = { + "compute.skipDefaultNetworkCreation" = { rules = [{ enforce = true }] } "iam.disableServiceAccountKeyCreation" = { @@ -66,21 +66,21 @@ module "folder" { } ] } - "constraints/iam.allowedPolicyMemberDomains" = { + "iam.allowedPolicyMemberDomains" = { rules = [{ allow = { values = ["C0xxxxxxx", "C0yyyyyyy"] } }] } - "constraints/compute.trustedImageProjects" = { + "compute.trustedImageProjects" = { rules = [{ allow = { values = ["projects/my-project"] } }] } - "constraints/compute.vmExternalIpAccess" = { + "compute.vmExternalIpAccess" = { rules = [{ deny = { all = true } }] } } diff --git a/modules/organization/README.md b/modules/organization/README.md index 926f4f07..39b5ff29 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -40,7 +40,7 @@ module "org" { "compute.disableGuestAttributesAccess" = { rules = [{ enforce = true }] } - "constraints/compute.skipDefaultNetworkCreation" = { + "compute.skipDefaultNetworkCreation" = { rules = [{ enforce = true }] } "iam.disableServiceAccountKeyCreation" = { @@ -62,7 +62,7 @@ module "org" { } ] } - "constraints/iam.allowedPolicyMemberDomains" = { + "iam.allowedPolicyMemberDomains" = { rules = [ { allow = { all = true } @@ -83,14 +83,14 @@ module "org" { ] } - "constraints/compute.trustedImageProjects" = { + "compute.trustedImageProjects" = { rules = [{ allow = { values = ["projects/my-project"] } }] } - "constraints/compute.vmExternalIpAccess" = { + "compute.vmExternalIpAccess" = { rules = [{ deny = { all = true } }] } } diff --git a/modules/project/README.md b/modules/project/README.md index eb91991c..730fe190 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -245,7 +245,7 @@ module "project" { "compute.disableGuestAttributesAccess" = { rules = [{ enforce = true }] } - "constraints/compute.skipDefaultNetworkCreation" = { + "compute.skipDefaultNetworkCreation" = { rules = [{ enforce = true }] } "iam.disableServiceAccountKeyCreation" = { @@ -267,21 +267,21 @@ module "project" { } ] } - "constraints/iam.allowedPolicyMemberDomains" = { + "iam.allowedPolicyMemberDomains" = { rules = [{ allow = { values = ["C0xxxxxxx", "C0yyyyyyy"] } }] } - "constraints/compute.trustedImageProjects" = { + "compute.trustedImageProjects" = { rules = [{ allow = { values = ["projects/my-project"] } }] } - "constraints/compute.vmExternalIpAccess" = { + "compute.vmExternalIpAccess" = { rules = [{ deny = { all = true } }] } } @@ -314,7 +314,7 @@ module "project" { compute.disableGuestAttributesAccess: rules: - enforce: true -constraints/compute.skipDefaultNetworkCreation: +compute.skipDefaultNetworkCreation: rules: - enforce: true iam.disableServiceAccountKeyCreation: @@ -333,16 +333,16 @@ iam.disableServiceAccountKeyUpload: ```yaml # tftest-file id=list path=configs/org-policies/list.yaml -constraints/compute.trustedImageProjects: +compute.trustedImageProjects: rules: - allow: values: - projects/my-project -constraints/compute.vmExternalIpAccess: +compute.vmExternalIpAccess: rules: - deny: all: true -constraints/iam.allowedPolicyMemberDomains: +iam.allowedPolicyMemberDomains: rules: - allow: values: diff --git a/tests/blueprints/factories/project_factory/fixture/projects/project.yaml b/tests/blueprints/factories/project_factory/fixture/projects/project.yaml index a1581984..b8d6e663 100644 --- a/tests/blueprints/factories/project_factory/fixture/projects/project.yaml +++ b/tests/blueprints/factories/project_factory/fixture/projects/project.yaml @@ -48,15 +48,14 @@ labels: # [opt] Org policy overrides defined at project level org_policies: - policy_boolean: - constraints/compute.disableGuestAttributesAccess: true - policy_list: - constraints/compute.trustedImageProjects: - inherit_from_parent: null - status: true - suggested_value: null - values: - - projects/fast-prod-iac-core-0 + compute.disableGuestAttributesAccess: + rules: + - enforce: true + compute.trustedImageProjects: + rules: + - allow: + values: + - projects/fast-prod-iac-core-0 # [opt] Prefix - overrides default if set prefix: test1 diff --git a/tests/fast/stages/s3_project_factory/data/projects/project.yaml b/tests/fast/stages/s3_project_factory/data/projects/project.yaml index d988d9d5..90354a2a 100644 --- a/tests/fast/stages/s3_project_factory/data/projects/project.yaml +++ b/tests/fast/stages/s3_project_factory/data/projects/project.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -60,15 +60,14 @@ labels: # [opt] Org policy overrides defined at project level org_policies: - policy_boolean: - constraints/compute.disableGuestAttributesAccess: true - policy_list: - constraints/compute.trustedImageProjects: - inherit_from_parent: null - status: true - suggested_value: null - values: - - projects/fast-prod-iac-core-0 + compute.disableGuestAttributesAccess: + rules: + - enforce: true + compute.trustedImageProjects: + rules: + - allow: + values: + - projects/fast-prod-iac-core-0 # [opt] Service account to create for the project and their roles on the project # in name => [roles] format diff --git a/tests/modules/folder/examples/org-policies.yaml b/tests/modules/folder/examples/org-policies.yaml index 7d2637ea..c7bee123 100644 --- a/tests/modules/folder/examples/org-policies.yaml +++ b/tests/modules/folder/examples/org-policies.yaml @@ -26,7 +26,7 @@ values: deny_all: null enforce: 'TRUE' values: [] - module.folder.google_org_policy_policy.default["constraints/compute.skipDefaultNetworkCreation"]: + module.folder.google_org_policy_policy.default["compute.skipDefaultNetworkCreation"]: spec: - inherit_from_parent: null reset: null @@ -36,7 +36,7 @@ values: deny_all: null enforce: 'TRUE' values: [] - module.folder.google_org_policy_policy.default["constraints/compute.trustedImageProjects"]: + module.folder.google_org_policy_policy.default["compute.trustedImageProjects"]: spec: - inherit_from_parent: null reset: null @@ -49,7 +49,7 @@ values: - allowed_values: - projects/my-project denied_values: null - module.folder.google_org_policy_policy.default["constraints/compute.vmExternalIpAccess"]: + module.folder.google_org_policy_policy.default["compute.vmExternalIpAccess"]: spec: - inherit_from_parent: null reset: null @@ -59,7 +59,7 @@ values: deny_all: 'TRUE' enforce: null values: [] - module.folder.google_org_policy_policy.default["constraints/iam.allowedPolicyMemberDomains"]: + module.folder.google_org_policy_policy.default["iam.allowedPolicyMemberDomains"]: spec: - inherit_from_parent: null reset: null diff --git a/tests/modules/organization/examples/basic.yaml b/tests/modules/organization/examples/basic.yaml index a751622d..9960a712 100644 --- a/tests/modules/organization/examples/basic.yaml +++ b/tests/modules/organization/examples/basic.yaml @@ -25,8 +25,8 @@ values: deny_all: null enforce: 'TRUE' values: [] - module.org.google_org_policy_policy.default["constraints/compute.skipDefaultNetworkCreation"]: - name: organizations/1234567890/policies/constraints/compute.skipDefaultNetworkCreation + module.org.google_org_policy_policy.default["compute.skipDefaultNetworkCreation"]: + name: organizations/1234567890/policies/compute.skipDefaultNetworkCreation parent: organizations/1234567890 spec: - inherit_from_parent: null @@ -37,8 +37,8 @@ values: deny_all: null enforce: 'TRUE' values: [] - module.org.google_org_policy_policy.default["constraints/compute.trustedImageProjects"]: - name: organizations/1234567890/policies/constraints/compute.trustedImageProjects + module.org.google_org_policy_policy.default["compute.trustedImageProjects"]: + name: organizations/1234567890/policies/compute.trustedImageProjects parent: organizations/1234567890 spec: - inherit_from_parent: null @@ -52,8 +52,8 @@ values: - allowed_values: - projects/my-project denied_values: null - module.org.google_org_policy_policy.default["constraints/compute.vmExternalIpAccess"]: - name: organizations/1234567890/policies/constraints/compute.vmExternalIpAccess + module.org.google_org_policy_policy.default["compute.vmExternalIpAccess"]: + name: organizations/1234567890/policies/compute.vmExternalIpAccess parent: organizations/1234567890 spec: - inherit_from_parent: null @@ -64,8 +64,8 @@ values: deny_all: 'TRUE' enforce: null values: [] - module.org.google_org_policy_policy.default["constraints/iam.allowedPolicyMemberDomains"]: - name: organizations/1234567890/policies/constraints/iam.allowedPolicyMemberDomains + module.org.google_org_policy_policy.default["iam.allowedPolicyMemberDomains"]: + name: organizations/1234567890/policies/iam.allowedPolicyMemberDomains parent: organizations/1234567890 spec: - inherit_from_parent: null diff --git a/tests/modules/organization/org_policies_boolean.tfvars b/tests/modules/organization/org_policies_boolean.tfvars index cd0f032c..cf5047a2 100644 --- a/tests/modules/organization/org_policies_boolean.tfvars +++ b/tests/modules/organization/org_policies_boolean.tfvars @@ -3,7 +3,6 @@ org_policies = { rules = [{ enforce = true }] } "iam.disableServiceAccountKeyUpload" = { - rules = [ { condition = { diff --git a/tests/modules/project/examples/org-policies.yaml b/tests/modules/project/examples/org-policies.yaml index a426696b..d4dddc75 100644 --- a/tests/modules/project/examples/org-policies.yaml +++ b/tests/modules/project/examples/org-policies.yaml @@ -25,8 +25,8 @@ values: deny_all: null enforce: 'TRUE' values: [] - module.project.google_org_policy_policy.default["constraints/compute.skipDefaultNetworkCreation"]: - name: projects/foo-project-example/policies/constraints/compute.skipDefaultNetworkCreation + module.project.google_org_policy_policy.default["compute.skipDefaultNetworkCreation"]: + name: projects/foo-project-example/policies/compute.skipDefaultNetworkCreation parent: projects/foo-project-example spec: - inherit_from_parent: null @@ -37,8 +37,8 @@ values: deny_all: null enforce: 'TRUE' values: [] - module.project.google_org_policy_policy.default["constraints/compute.trustedImageProjects"]: - name: projects/foo-project-example/policies/constraints/compute.trustedImageProjects + module.project.google_org_policy_policy.default["compute.trustedImageProjects"]: + name: projects/foo-project-example/policies/compute.trustedImageProjects parent: projects/foo-project-example spec: - inherit_from_parent: null @@ -52,8 +52,8 @@ values: - allowed_values: - projects/my-project denied_values: null - module.project.google_org_policy_policy.default["constraints/compute.vmExternalIpAccess"]: - name: projects/foo-project-example/policies/constraints/compute.vmExternalIpAccess + module.project.google_org_policy_policy.default["compute.vmExternalIpAccess"]: + name: projects/foo-project-example/policies/compute.vmExternalIpAccess parent: projects/foo-project-example spec: - inherit_from_parent: null @@ -64,8 +64,8 @@ values: deny_all: 'TRUE' enforce: null values: [] - module.project.google_org_policy_policy.default["constraints/iam.allowedPolicyMemberDomains"]: - name: projects/foo-project-example/policies/constraints/iam.allowedPolicyMemberDomains + module.project.google_org_policy_policy.default["iam.allowedPolicyMemberDomains"]: + name: projects/foo-project-example/policies/iam.allowedPolicyMemberDomains parent: projects/foo-project-example spec: - inherit_from_parent: null From 1696f70f47dde156e7339aed64f4f75924c8f360 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 21 Feb 2023 15:04:54 +0100 Subject: [PATCH 39/47] Update PF variables for org policies --- .../factories/project-factory/README.md | 22 +++++++++---------- .../factories/project-factory/variables.tf | 5 ++--- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/blueprints/factories/project-factory/README.md b/blueprints/factories/project-factory/README.md index df8701d2..68e2e1d0 100644 --- a/blueprints/factories/project-factory/README.md +++ b/blueprints/factories/project-factory/README.md @@ -226,8 +226,8 @@ vpc: | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [billing_account_id](variables.tf#L17) | Billing account id. | string | ✓ | | -| [prefix](variables.tf#L145) | Prefix used for resource names. | string | ✓ | | -| [project_id](variables.tf#L154) | Project id. | string | ✓ | | +| [prefix](variables.tf#L144) | Prefix used for resource names. | string | ✓ | | +| [project_id](variables.tf#L153) | Project id. | string | ✓ | | | [billing_alert](variables.tf#L22) | Billing alert configuration. | object({…}) | | null | | [defaults](variables.tf#L35) | Project factory default values. | object({…}) | | null | | [descriptive_name](variables.tf#L57) | Name of the project name. Used for project name instead of `name` variable. | string | | null | @@ -240,15 +240,15 @@ vpc: | [iam_additive](variables.tf#L99) | Custom additive IAM settings in role => [principal] format. | map(list(string)) | | {} | | [kms_service_agents](variables.tf#L105) | KMS IAM configuration in as service => [key]. | map(list(string)) | | {} | | [labels](variables.tf#L111) | Labels to be assigned at project level. | map(string) | | {} | -| [org_policies](variables.tf#L117) | Org-policy overrides at project level. | map(object({…})) | | {} | -| [service_accounts](variables.tf#L159) | Service accounts to be created, and roles assigned them on the project. | map(list(string)) | | {} | -| [service_accounts_additive](variables.tf#L165) | Service accounts to be created, and roles assigned them on the project additively. | map(list(string)) | | {} | -| [service_accounts_iam](variables.tf#L171) | IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | -| [service_accounts_iam_additive](variables.tf#L178) | IAM additive bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | -| [service_identities_iam](variables.tf#L185) | Custom IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | -| [service_identities_iam_additive](variables.tf#L192) | Custom additive IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | -| [services](variables.tf#L199) | Services to be enabled for the project. | list(string) | | [] | -| [vpc](variables.tf#L206) | VPC configuration for the project. | object({…}) | | null | +| [org_policies](variables.tf#L117) | Org-policy overrides at project level. | map(object({…})) | | {} | +| [service_accounts](variables.tf#L158) | Service accounts to be created, and roles assigned them on the project. | map(list(string)) | | {} | +| [service_accounts_additive](variables.tf#L164) | Service accounts to be created, and roles assigned them on the project additively. | map(list(string)) | | {} | +| [service_accounts_iam](variables.tf#L170) | IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | +| [service_accounts_iam_additive](variables.tf#L177) | IAM additive bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | +| [service_identities_iam](variables.tf#L184) | Custom IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | +| [service_identities_iam_additive](variables.tf#L191) | Custom additive IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | +| [services](variables.tf#L198) | Services to be enabled for the project. | list(string) | | [] | +| [vpc](variables.tf#L205) | VPC configuration for the project. | object({…}) | | null | ## Outputs diff --git a/blueprints/factories/project-factory/variables.tf b/blueprints/factories/project-factory/variables.tf index b9a83ae3..a2089bcf 100644 --- a/blueprints/factories/project-factory/variables.tf +++ b/blueprints/factories/project-factory/variables.tf @@ -119,7 +119,6 @@ variable "org_policies" { type = map(object({ inherit_from_parent = optional(bool) # for list policies only. reset = optional(bool) - # conditional values rules = optional(list(object({ allow = optional(object({ all = optional(bool) @@ -130,12 +129,12 @@ variable "org_policies" { values = optional(list(string)) })) enforce = optional(bool) # for boolean policies only. - condition = object({ + condition = optional(object({ description = optional(string) expression = optional(string) location = optional(string) title = optional(string) - }) + }), {}) })), []) })) default = {} From 2108b4650deb5814cede7daed89a1548bb1c3117 Mon Sep 17 00:00:00 2001 From: lcaggio Date: Wed, 22 Feb 2023 01:36:01 +0100 Subject: [PATCH 40/47] Fix Tests, rely on iam additive. --- .../data-platform-foundations/01-dropoff.tf | 27 ++++---- .../data-platform-foundations/02-load.tf | 29 ++++---- .../03-orchestration.tf | 41 ++++------- .../04-transformation.tf | 19 ++--- .../05-datawarehouse.tf | 69 ++++++++----------- .../data-platform-foundations/06-common.tf | 29 +++----- .../data-platform-foundations/README.md | 10 +-- .../demo/datapipeline_dc_tags.py | 8 +-- .../demo/delete_table.py | 4 +- fast/stages/3-data-platform/dev/variables.tf | 29 +++++--- .../data_platform_foundations/test_plan.py | 2 +- 11 files changed, 120 insertions(+), 147 deletions(-) diff --git a/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf b/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf index 4c4264d3..46e9a130 100644 --- a/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf +++ b/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf @@ -15,24 +15,22 @@ # tfdoc:file:description drop off project and resources. locals { - group_iam_drp = { - (local.groups.data-engineers) = [ - "roles/bigquery.dataEditor", - "roles/pubsub.editor", - "roles/storage.admin", - ] - } iam_drp = { - "roles/bigquery.dataEditor" = [module.drop-sa-bq-0.iam_email] - "roles/bigquery.user" = [module.load-sa-df-0.iam_email] - "roles/pubsub.publisher" = [module.drop-sa-ps-0.iam_email] + "roles/bigquery.dataEditor" = [ + module.drop-sa-bq-0.iam_email, local.groups_iam.data-engineers + ] + "roles/bigquery.user" = [ + module.load-sa-df-0.iam_email, local.groups_iam.data-engineers + ] + "roles/pubsub.publisher" = [module.drop-sa-ps-0.iam_email] "roles/pubsub.subscriber" = [ module.orch-sa-cmp-0.iam_email, module.load-sa-df-0.iam_email ] - "roles/storage.objectAdmin" = [module.load-sa-df-0.iam_email] "roles/storage.objectCreator" = [module.drop-sa-cs-0.iam_email] "roles/storage.objectViewer" = [module.orch-sa-cmp-0.iam_email] - "roles/storage.admin" = [module.load-sa-df-0.iam_email] + "roles/storage.objectAdmin" = [ + module.load-sa-df-0.iam_email, module.load-sa-df-0.iam_email + ] } } @@ -43,9 +41,8 @@ module "drop-project" { project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.drop : "${var.project_config.project_ids.drop}${local.project_suffix}" - # group_iam = local.group_iam_drp - iam = var.project_config.billing_account_id != null ? local.iam_drp : null - iam_additive = var.project_config.billing_account_id == null ? local.iam_drp : null + iam = var.project_config.billing_account_id != null ? local.iam_drp : null + iam_additive = var.project_config.billing_account_id == null ? local.iam_drp : null services = concat(var.project_services, [ "bigquery.googleapis.com", "bigqueryreservation.googleapis.com", diff --git a/blueprints/data-solutions/data-platform-foundations/02-load.tf b/blueprints/data-solutions/data-platform-foundations/02-load.tf index b547f050..9702fce1 100644 --- a/blueprints/data-solutions/data-platform-foundations/02-load.tf +++ b/blueprints/data-solutions/data-platform-foundations/02-load.tf @@ -15,18 +15,15 @@ # tfdoc:file:description Load project and VPC. locals { - group_iam_load = { - (local.groups.data-engineers) = [ - "roles/compute.viewer", - "roles/dataflow.admin", - "roles/dataflow.developer", - "roles/viewer", - ] - } iam_load = { "roles/bigquery.jobUser" = [module.load-sa-df-0.iam_email] "roles/dataflow.admin" = [ - module.orch-sa-cmp-0.iam_email, module.load-sa-df-0.iam_email + module.orch-sa-cmp-0.iam_email, + module.load-sa-df-0.iam_email, + local.groups_iam.data-engineers + ] + "roles/dataflow.developer" = [ + local.groups_iam.data-engineers ] "roles/dataflow.worker" = [module.load-sa-df-0.iam_email] "roles/storage.objectAdmin" = local.load_service_accounts @@ -56,9 +53,8 @@ module "load-project" { project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.load : "${var.project_config.project_ids.load}${local.project_suffix}" - # group_iam = local.group_iam_load - iam = var.project_config.billing_account_id != null ? local.iam_load : null - iam_additive = var.project_config.billing_account_id == null ? local.iam_load : null + iam = var.project_config.billing_account_id != null ? local.iam_load : null + iam_additive = var.project_config.billing_account_id == null ? local.iam_load : null services = concat(var.project_services, [ "bigquery.googleapis.com", "bigqueryreservation.googleapis.com", @@ -90,8 +86,13 @@ module "load-sa-df-0" { name = "load-df-0" display_name = "Data platform Dataflow load service account" iam = { - "roles/iam.serviceAccountTokenCreator" = [local.groups_iam.data-engineers] - "roles/iam.serviceAccountUser" = [module.orch-sa-cmp-0.iam_email] + "roles/iam.serviceAccountTokenCreator" = [ + local.groups_iam.data-engineers, + module.orch-sa-cmp-0.iam_email + ], + "roles/iam.serviceAccountUser" = [ + module.orch-sa-cmp-0.iam_email + ] } } diff --git a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf index f720fc7f..fc0eda12 100644 --- a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf +++ b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf @@ -15,29 +15,22 @@ # tfdoc:file:description Orchestration project and VPC. locals { - group_iam_orch = { - (local.groups.data-engineers) = [ - "roles/bigquery.dataEditor", - "roles/bigquery.jobUser", - "roles/cloudbuild.builds.editor", - "roles/composer.admin", - "roles/composer.environmentAndStorageObjectAdmin", - "roles/iap.httpsResourceAccessor", - "roles/iam.serviceAccountUser", - "roles/storage.objectAdmin", - "roles/storage.admin", - "roles/artifactregistry.admin", - "roles/serviceusage.serviceUsageConsumer", - ] - } iam_orch = { + "roles/artifactregistry.admin" = [local.groups_iam.data-engineers] + "roles/artifactregistry.reader" = [module.load-sa-df-0.iam_email] "roles/bigquery.dataEditor" = [ module.load-sa-df-0.iam_email, module.transf-sa-df-0.iam_email, + local.groups_iam.data-engineers ] "roles/bigquery.jobUser" = [ module.orch-sa-cmp-0.iam_email, + local.groups_iam.data-engineers ] + "roles/cloudbuild.builds.editor" = [local.groups_iam.data-engineers] + "roles/cloudbuild.serviceAgent" = [module.orch-sa-df-build.iam_email] + "roles/composer.admin" = [local.groups_iam.data-engineers] + "roles/composer.environmentAndStorageObjectAdmin" = [local.groups_iam.data-engineers] "roles/composer.ServiceAgentV2Ext" = [ "serviceAccount:${module.orch-project.service_accounts.robots.composer}" ] @@ -45,19 +38,16 @@ locals { module.orch-sa-cmp-0.iam_email ] "roles/iam.serviceAccountUser" = [ - module.orch-sa-cmp-0.iam_email + module.orch-sa-cmp-0.iam_email, local.groups_iam.data-engineers ] + "roles/iap.httpsResourceAccessor" = [local.groups_iam.data-engineers] + "roles/serviceusage.serviceUsageConsumer" = [local.groups_iam.data-engineers] "roles/storage.objectAdmin" = [ module.orch-sa-cmp-0.iam_email, module.orch-sa-df-build.iam_email, "serviceAccount:${module.orch-project.service_accounts.robots.composer}", "serviceAccount:${module.orch-project.service_accounts.robots.cloudbuild}", - ] - "roles/artifactregistry.reader" = [ - module.load-sa-df-0.iam_email, - ] - "roles/cloudbuild.serviceAgent" = [ - module.orch-sa-df-build.iam_email, + local.groups_iam.data-engineers ] "roles/storage.objectViewer" = [module.load-sa-df-0.iam_email] } @@ -85,10 +75,9 @@ module "orch-project" { project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.orc : "${var.project_config.project_ids.orc}${local.project_suffix}" - # group_iam = local.group_iam_orch - iam = var.project_config.billing_account_id != null ? local.iam_orch : null - iam_additive = var.project_config.billing_account_id == null ? local.iam_orch : null - oslogin = false + iam = var.project_config.billing_account_id != null ? local.iam_orch : null + iam_additive = var.project_config.billing_account_id == null ? local.iam_orch : null + oslogin = false services = concat(var.project_services, [ "artifactregistry.googleapis.com", "bigquery.googleapis.com", diff --git a/blueprints/data-solutions/data-platform-foundations/04-transformation.tf b/blueprints/data-solutions/data-platform-foundations/04-transformation.tf index 63d3f399..394adedf 100644 --- a/blueprints/data-solutions/data-platform-foundations/04-transformation.tf +++ b/blueprints/data-solutions/data-platform-foundations/04-transformation.tf @@ -15,22 +15,14 @@ # tfdoc:file:description Trasformation project and VPC. locals { - group_iam_trf = { - (local.groups.data-engineers) = [ - "roles/bigquery.jobUser", - "roles/dataflow.admin", - ] - } iam_trf = { "roles/bigquery.jobUser" = [ - module.transf-sa-bq-0.iam_email, + module.transf-sa-bq-0.iam_email, local.groups_iam.data-engineers ] "roles/dataflow.admin" = [ - module.orch-sa-cmp-0.iam_email, - ] - "roles/dataflow.worker" = [ - module.transf-sa-df-0.iam_email + module.orch-sa-cmp-0.iam_email, local.groups_iam.data-engineers ] + "roles/dataflow.worker" = [module.transf-sa-df-0.iam_email] "roles/storage.objectAdmin" = [ module.transf-sa-df-0.iam_email, "serviceAccount:${module.transf-project.service_accounts.robots.dataflow}" @@ -55,9 +47,8 @@ module "transf-project" { project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.trf : "${var.project_config.project_ids.trf}${local.project_suffix}" - # group_iam = local.group_iam_trf - iam = var.project_config.billing_account_id != null ? local.iam_orch : null - iam_additive = var.project_config.billing_account_id == null ? local.iam_orch : null + iam = var.project_config.billing_account_id != null ? local.iam_trf : null + iam_additive = var.project_config.billing_account_id == null ? local.iam_trf : null services = concat(var.project_services, [ "bigquery.googleapis.com", "bigqueryreservation.googleapis.com", diff --git a/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf b/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf index d22cf0aa..67c43dae 100644 --- a/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf +++ b/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf @@ -15,54 +15,48 @@ # tfdoc:file:description Data Warehouse projects. locals { - dwh_group_iam = { - (local.groups.data-engineers) = [ - "roles/bigquery.dataEditor", - "roles/storage.admin", - ], - (local.groups.data-analysts) = [ - "roles/bigquery.dataViewer", - "roles/bigquery.jobUser", - "roles/bigquery.metadataViewer", - "roles/bigquery.user", - "roles/datacatalog.viewer", - "roles/datacatalog.tagTemplateViewer", - "roles/storage.objectViewer", - ] - } dwh_lnd_iam = { "roles/bigquery.dataOwner" = [ module.load-sa-df-0.iam_email, + ] + "roles/bigquery.dataViewer" = [ module.transf-sa-df-0.iam_email, module.transf-sa-bq-0.iam_email, + local.groups_iam.data-engineers ] "roles/bigquery.jobUser" = [ - module.load-sa-df-0.iam_email, - ] - "roles/datacatalog.categoryAdmin" = [ - module.transf-sa-bq-0.iam_email - ] - "roles/storage.objectCreator" = [ - module.load-sa-df-0.iam_email, + module.load-sa-df-0.iam_email, local.groups_iam.data-engineers ] + "roles/datacatalog.categoryAdmin" = [module.transf-sa-bq-0.iam_email] + "roles/datacatalog.tagTemplateViewer" = [local.groups_iam.data-engineers] + "roles/datacatalog.viewer" = [local.groups_iam.data-engineers] + "roles/storage.objectCreator" = [module.load-sa-df-0.iam_email] + "roles/storage.objectViewer" = [local.groups_iam.data-engineers] } dwh_iam = { "roles/bigquery.dataOwner" = [ module.transf-sa-df-0.iam_email, module.transf-sa-bq-0.iam_email, ] + "roles/bigquery.dataViewer" = [ + local.groups_iam.data-analysts, + local.groups_iam.data-engineers + ] "roles/bigquery.jobUser" = [ module.transf-sa-bq-0.iam_email, + local.groups_iam.data-analysts, + local.groups_iam.data-engineers ] - "roles/datacatalog.categoryAdmin" = [ - module.load-sa-df-0.iam_email + "roles/datacatalog.tagTemplateViewer" = [ + local.groups_iam.data-analysts, local.groups_iam.data-engineers ] - "roles/storage.objectCreator" = [ - module.transf-sa-df-0.iam_email, + "roles/datacatalog.viewer" = [ + local.groups_iam.data-analysts, local.groups_iam.data-engineers ] "roles/storage.objectViewer" = [ - module.transf-sa-df-0.iam_email, + local.groups_iam.data-analysts, local.groups_iam.data-engineers ] + "roles/storage.objectAdmin" = [module.transf-sa-df-0.iam_email] } dwh_services = concat(var.project_services, [ "bigquery.googleapis.com", @@ -87,10 +81,9 @@ module "dwh-lnd-project" { project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-lnd : "${var.project_config.project_ids.dwh-lnd}${local.project_suffix}" - # group_iam = local.dwh_group_iam - iam = var.project_config.billing_account_id != null ? local.dwh_lnd_iam : {} - iam_additive = var.project_config.billing_account_id == null ? local.dwh_lnd_iam : {} - services = local.dwh_services + iam = var.project_config.billing_account_id != null ? local.dwh_lnd_iam : {} + iam_additive = var.project_config.billing_account_id == null ? local.dwh_lnd_iam : {} + services = local.dwh_services service_encryption_key_ids = { bq = [try(local.service_encryption_keys.bq, null)] storage = [try(local.service_encryption_keys.storage, null)] @@ -104,10 +97,9 @@ module "dwh-cur-project" { project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-cur : "${var.project_config.project_ids.dwh-cur}${local.project_suffix}" - # group_iam = local.dwh_group_iam - iam = var.project_config.billing_account_id != null ? local.dwh_iam : {} - iam_additive = var.project_config.billing_account_id == null ? local.dwh_iam : {} - services = local.dwh_services + iam = var.project_config.billing_account_id != null ? local.dwh_iam : {} + iam_additive = var.project_config.billing_account_id == null ? local.dwh_iam : {} + services = local.dwh_services service_encryption_key_ids = { bq = [try(local.service_encryption_keys.bq, null)] storage = [try(local.service_encryption_keys.storage, null)] @@ -121,10 +113,9 @@ module "dwh-conf-project" { project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-conf : "${var.project_config.project_ids.dwh-conf}${local.project_suffix}" - # group_iam = local.dwh_group_iam - iam = var.project_config.billing_account_id != null ? local.dwh_iam : null - iam_additive = var.project_config.billing_account_id == null ? local.dwh_iam : null - services = local.dwh_services + iam = var.project_config.billing_account_id != null ? local.dwh_iam : null + iam_additive = var.project_config.billing_account_id == null ? local.dwh_iam : null + services = local.dwh_services service_encryption_key_ids = { bq = [try(local.service_encryption_keys.bq, null)] storage = [try(local.service_encryption_keys.storage, null)] diff --git a/blueprints/data-solutions/data-platform-foundations/06-common.tf b/blueprints/data-solutions/data-platform-foundations/06-common.tf index 059d6b5e..5a84ee77 100644 --- a/blueprints/data-solutions/data-platform-foundations/06-common.tf +++ b/blueprints/data-solutions/data-platform-foundations/06-common.tf @@ -15,29 +15,21 @@ # tfdoc:file:description common project. locals { - group_iam_common = { - (local.groups.data-analysts) = [ - "roles/datacatalog.viewer", - ] - (local.groups.data-engineers) = [ - "roles/dlp.reader", - "roles/dlp.user", - "roles/dlp.estimatesAdmin", - ] - (local.groups.data-security) = [ - "roles/dlp.admin", - "roles/datacatalog.admin" - ] - } iam_common = { + "roles/dlp.admin" = [local.groups_iam.data-security] + "roles/dlp.estimatesAdmin" = [local.groups_iam.data-engineers] + "roles/dlp.reader" = [local.groups_iam.data-engineers] "roles/dlp.user" = [ module.load-sa-df-0.iam_email, - module.transf-sa-df-0.iam_email + module.transf-sa-df-0.iam_email, + local.groups_iam.data-engineers ] + "roles/datacatalog.admin" = [local.groups_iam.data-security] "roles/datacatalog.viewer" = [ module.load-sa-df-0.iam_email, module.transf-sa-df-0.iam_email, - module.transf-sa-bq-0.iam_email + module.transf-sa-bq-0.iam_email, + local.groups_iam.data-analysts ] "roles/datacatalog.categoryFineGrainedReader" = [ module.transf-sa-df-0.iam_email, @@ -54,9 +46,8 @@ module "common-project" { project_create = var.project_config.billing_account_id != null prefix = var.project_config.billing_account_id == null ? null : var.prefix name = var.project_config.billing_account_id == null ? var.project_config.project_ids.common : "${var.project_config.project_ids.common}${local.project_suffix}" - # group_iam = local.group_iam_common - iam = var.project_config.billing_account_id != null ? local.iam_common : null - iam_additive = var.project_config.billing_account_id == null ? local.iam_common : null + iam = var.project_config.billing_account_id != null ? local.iam_common : null + iam_additive = var.project_config.billing_account_id == null ? local.iam_common : null services = concat(var.project_services, [ "datacatalog.googleapis.com", "dlp.googleapis.com", diff --git a/blueprints/data-solutions/data-platform-foundations/README.md b/blueprints/data-solutions/data-platform-foundations/README.md index 027c6299..d48ae6cc 100644 --- a/blueprints/data-solutions/data-platform-foundations/README.md +++ b/blueprints/data-solutions/data-platform-foundations/README.md @@ -215,13 +215,13 @@ module "data-platform" { source = "./fabric/blueprints/data-solutions/data-platform-foundations" organization_domain = "example.com" project_config = { - billing_account_id = "123456-123456-123456" - parent = "folders/12345678" - } - prefix = "myprefix" + billing_account_id = "123456-123456-123456" + parent = "folders/12345678" + } + prefix = "myprefix" } -# tftest modules=43 resources=265 +# tftest modules=43 resources=278 ``` ## Customizations diff --git a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags.py b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags.py index 4b15eaab..86b8e5bb 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags.py +++ b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags.py @@ -123,7 +123,7 @@ with models.DAG( task_id="upsert_table_customers", project_id=DWH_LAND_PRJ, dataset_id=DWH_LAND_BQ_DATASET, - impersonation_chain=[TRF_SA_DF], + impersonation_chain=[LOD_SA_DF], table_resource={ "tableReference": {"tableId": "customers"}, }, @@ -133,7 +133,7 @@ with models.DAG( task_id="upsert_table_purchases", project_id=DWH_LAND_PRJ, dataset_id=DWH_LAND_BQ_DATASET, - impersonation_chain=[TRF_SA_BQ], + impersonation_chain=[LOD_SA_DF], table_resource={ "tableReference": {"tableId": "purchases"} }, @@ -167,7 +167,7 @@ with models.DAG( project_id=DWH_LAND_PRJ, dataset_id=DWH_LAND_BQ_DATASET, table_id="customers", - impersonation_chain=[TRF_SA_BQ], + impersonation_chain=[LOD_SA_DF], include_policy_tags=True, schema_fields_updates=[ { "mode": "REQUIRED", "name": "id", "type": "INTEGER", "description": "ID" }, @@ -182,7 +182,7 @@ with models.DAG( project_id=DWH_LAND_PRJ, dataset_id=DWH_LAND_BQ_DATASET, table_id="purchases", - impersonation_chain=[TRF_SA_BQ], + impersonation_chain=[LOD_SA_DF], include_policy_tags=True, schema_fields_updates=[ { "mode": "REQUIRED", "name": "id", "type": "INTEGER", "description": "ID" }, diff --git a/blueprints/data-solutions/data-platform-foundations/demo/delete_table.py b/blueprints/data-solutions/data-platform-foundations/demo/delete_table.py index dc0c954b..bade0388 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/delete_table.py +++ b/blueprints/data-solutions/data-platform-foundations/demo/delete_table.py @@ -122,13 +122,13 @@ with models.DAG( delete_table_customers = BigQueryDeleteTableOperator( task_id="delete_table_customers", deletion_dataset_table=DWH_LAND_PRJ+"."+DWH_LAND_BQ_DATASET+".customers", - impersonation_chain=[TRF_SA_DF] + impersonation_chain=[LOD_SA_DF] ) delete_table_purchases = BigQueryDeleteTableOperator( task_id="delete_table_purchases", deletion_dataset_table=DWH_LAND_PRJ+"."+DWH_LAND_BQ_DATASET+".purchases", - impersonation_chain=[TRF_SA_DF] + impersonation_chain=[LOD_SA_DF] ) delete_table_customer_purchase_curated = BigQueryDeleteTableOperator( diff --git a/fast/stages/3-data-platform/dev/variables.tf b/fast/stages/3-data-platform/dev/variables.tf index 392e2dc9..74a5dbe1 100644 --- a/fast/stages/3-data-platform/dev/variables.tf +++ b/fast/stages/3-data-platform/dev/variables.tf @@ -22,6 +22,19 @@ 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." + type = object({ + id = string + is_org_level = optional(bool, true) + }) + validation { + condition = var.billing_account.is_org_level != null + error_message = "Invalid `null` value for `billing_account.is_org_level`." + } +} + variable "composer_config" { description = "Cloud Composer configuration options." type = object({ @@ -86,6 +99,14 @@ variable "data_force_destroy" { default = false } +variable "folder_ids" { + # tfdoc:variable:source 1-resman + description = "Folder to be used for the networking resources in folders/nnnn format." + type = object({ + data-platform-dev = string + }) +} + variable "groups" { description = "Groups." type = map(string) @@ -148,14 +169,6 @@ variable "prefix" { type = string } -variable "project_config" { - description = "Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format." - type = object({ - billing_account_id = string - parent = string - }) -} - variable "project_services" { description = "List of core services enabled on all projects." type = list(string) diff --git a/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py b/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py index f3ed2ba0..630944f2 100644 --- a/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py +++ b/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py @@ -23,4 +23,4 @@ def test_resources(e2e_plan_runner): modules, resources = e2e_plan_runner(FIXTURES_DIR) assert len(modules) == 42 - assert len(resources) == 264 + assert len(resources) == 277 From ac75cbe71ac2ae57162319d16cc557e468581126 Mon Sep 17 00:00:00 2001 From: lcaggio Date: Wed, 22 Feb 2023 01:38:44 +0100 Subject: [PATCH 41/47] Fix lint. --- fast/stages/3-data-platform/dev/README.md | 33 ++++++++++++----------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/fast/stages/3-data-platform/dev/README.md b/fast/stages/3-data-platform/dev/README.md index f2990310..48d09eaf 100644 --- a/fast/stages/3-data-platform/dev/README.md +++ b/fast/stages/3-data-platform/dev/README.md @@ -185,22 +185,23 @@ You can find examples in the `[demo](../../../../blueprints/data-solutions/data- | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| | [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | -| [host_project_ids](variables.tf#L99) | Shared VPC project ids. | object({…}) | ✓ | | 2-networking | -| [organization](variables.tf#L129) | Organization details. | object({…}) | ✓ | | 00-globals | -| [prefix](variables.tf#L145) | Unique prefix used for resource names. Not used for projects if 'project_create' is null. | string | ✓ | | 00-globals | -| [project_config](variables.tf#L151) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | ✓ | | | -| [composer_config](variables.tf#L25) | Cloud Composer configuration options. | object({…}) | | {…} | | -| [data_catalog_tags](variables.tf#L72) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | | -| [data_force_destroy](variables.tf#L83) | Flag to set 'force_destroy' on data services like BigQery or Cloud Storage. | bool | | false | | -| [groups](variables.tf#L89) | Groups. | map(string) | | {…} | | -| [location](variables.tf#L107) | Location used for multi-regional resources. | string | | "eu" | | -| [network_config_composer](variables.tf#L113) | Network configurations to use for Composer. | object({…}) | | {…} | | -| [outputs_location](variables.tf#L139) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [project_services](variables.tf#L159) | List of core services enabled on all projects. | list(string) | | […] | | -| [region](variables.tf#L170) | Region used for regional resources. | string | | "europe-west1" | | -| [service_encryption_keys](variables.tf#L176) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | -| [subnet_self_links](variables.tf#L188) | Shared VPC subnet self links. | object({…}) | | null | 2-networking | -| [vpc_self_links](variables.tf#L197) | Shared VPC self links. | object({…}) | | null | 2-networking | +| [billing_account](variables.tf#L25) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | +| [folder_ids](variables.tf#L102) | Folder to be used for the networking resources in folders/nnnn format. | object({…}) | ✓ | | 1-resman | +| [host_project_ids](variables.tf#L120) | Shared VPC project ids. | object({…}) | ✓ | | 2-networking | +| [organization](variables.tf#L150) | Organization details. | object({…}) | ✓ | | 00-globals | +| [prefix](variables.tf#L166) | Unique prefix used for resource names. Not used for projects if 'project_create' is null. | string | ✓ | | 00-globals | +| [composer_config](variables.tf#L38) | Cloud Composer configuration options. | object({…}) | | {…} | | +| [data_catalog_tags](variables.tf#L85) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | | +| [data_force_destroy](variables.tf#L96) | Flag to set 'force_destroy' on data services like BigQery or Cloud Storage. | bool | | false | | +| [groups](variables.tf#L110) | Groups. | map(string) | | {…} | | +| [location](variables.tf#L128) | Location used for multi-regional resources. | string | | "eu" | | +| [network_config_composer](variables.tf#L134) | Network configurations to use for Composer. | object({…}) | | {…} | | +| [outputs_location](variables.tf#L160) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| [project_services](variables.tf#L172) | List of core services enabled on all projects. | list(string) | | […] | | +| [region](variables.tf#L183) | Region used for regional resources. | string | | "europe-west1" | | +| [service_encryption_keys](variables.tf#L189) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | +| [subnet_self_links](variables.tf#L201) | Shared VPC subnet self links. | object({…}) | | null | 2-networking | +| [vpc_self_links](variables.tf#L210) | Shared VPC self links. | object({…}) | | null | 2-networking | ## Outputs From ad0840656b3afa695a7a4c8549994f78c1b59515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Wed, 22 Feb 2023 08:37:33 +0000 Subject: [PATCH 42/47] Add documentation about referring modules stored on CSR --- fast/stages/0-bootstrap/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fast/stages/0-bootstrap/README.md b/fast/stages/0-bootstrap/README.md index e1bb2948..88bdceb3 100644 --- a/fast/stages/0-bootstrap/README.md +++ b/fast/stages/0-bootstrap/README.md @@ -452,7 +452,10 @@ The remaining configuration is manual, as it regards the repositories themselves - 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 `source = "git@github.com:my-org/fast-modules.git//$1?ref=v1.0"` + - 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 From e39be7b01d18f19e509cd82acf5fd5c026a031b2 Mon Sep 17 00:00:00 2001 From: lcaggio Date: Wed, 22 Feb 2023 13:02:29 +0100 Subject: [PATCH 43/47] Fix --- tests/fast/stages/s3_data_platform/common.tfvars | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/fast/stages/s3_data_platform/common.tfvars b/tests/fast/stages/s3_data_platform/common.tfvars index 97d8bebc..2ec41d37 100644 --- a/tests/fast/stages/s3_data_platform/common.tfvars +++ b/tests/fast/stages/s3_data_platform/common.tfvars @@ -1,13 +1,11 @@ automation = { outputs_bucket = "test" } -project_config = { - billing_account = { - id = "012345-67890A-BCDEF0", - }, - parent = { - data-platform-dev = "folders/12345678" - } +billing_account = { + id = "012345-67890A-BCDEF0", +} +folder_ids = { + data-platform-dev = "folders/12345678" } host_project_ids = { dev-spoke-0 = "fast-dev-net-spoke-0" From b279c083a0bfdd3abb2b90b717609d32ece9caca Mon Sep 17 00:00:00 2001 From: lcaggio Date: Thu, 23 Feb 2023 11:54:16 +0100 Subject: [PATCH 44/47] Fix README and IAM files --- .../data-platform-foundations/IAM.md | 29 +++++++++---------- .../data-platform-foundations/README.md | 6 +++- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/blueprints/data-solutions/data-platform-foundations/IAM.md b/blueprints/data-solutions/data-platform-foundations/IAM.md index dd898bd7..b982f8c4 100644 --- a/blueprints/data-solutions/data-platform-foundations/IAM.md +++ b/blueprints/data-solutions/data-platform-foundations/IAM.md @@ -17,51 +17,48 @@ Legend: + additive, conditional. | members | roles | |---|---| -|gcp-data-engineers
group|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/pubsub.editor](https://cloud.google.com/iam/docs/understanding-roles#pubsub.editor)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin) | +|gcp-data-engineers
group|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/bigquery.user](https://cloud.google.com/iam/docs/understanding-roles#bigquery.user) | |drp-bq-0
serviceAccount|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor) | |drp-cs-0
serviceAccount|[roles/storage.objectCreator](https://cloud.google.com/iam/docs/understanding-roles#storage.objectCreator) | |drp-ps-0
serviceAccount|[roles/pubsub.publisher](https://cloud.google.com/iam/docs/understanding-roles#pubsub.publisher) | -|load-df-0
serviceAccount|[roles/bigquery.user](https://cloud.google.com/iam/docs/understanding-roles#bigquery.user)
[roles/pubsub.subscriber](https://cloud.google.com/iam/docs/understanding-roles#pubsub.subscriber)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | +|load-df-0
serviceAccount|[roles/bigquery.user](https://cloud.google.com/iam/docs/understanding-roles#bigquery.user)
[roles/pubsub.subscriber](https://cloud.google.com/iam/docs/understanding-roles#pubsub.subscriber)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | |orc-cmp-0
serviceAccount|[roles/pubsub.subscriber](https://cloud.google.com/iam/docs/understanding-roles#pubsub.subscriber)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | ## Project dwh-conf | members | roles | |---|---| -|gcp-data-analysts
group|[roles/bigquery.dataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataViewer)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/bigquery.metadataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.metadataViewer)
[roles/bigquery.user](https://cloud.google.com/iam/docs/understanding-roles#bigquery.user)
[roles/datacatalog.tagTemplateViewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.tagTemplateViewer)
[roles/datacatalog.viewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.viewer)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | -|gcp-data-engineers
group|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin) | +|gcp-data-analysts
group|[roles/bigquery.dataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataViewer)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/datacatalog.tagTemplateViewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.tagTemplateViewer)
[roles/datacatalog.viewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.viewer)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | +|gcp-data-engineers
group|[roles/bigquery.dataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataViewer)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/datacatalog.tagTemplateViewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.tagTemplateViewer)
[roles/datacatalog.viewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.viewer)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | |SERVICE_IDENTITY_service-networking
serviceAccount|[roles/servicenetworking.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#servicenetworking.serviceAgent) +| -|load-df-0
serviceAccount|[roles/datacatalog.categoryAdmin](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.categoryAdmin) | |trf-bq-0
serviceAccount|[roles/bigquery.dataOwner](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataOwner)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser) | -|trf-df-0
serviceAccount|[roles/bigquery.dataOwner](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataOwner)
[roles/storage.objectCreator](https://cloud.google.com/iam/docs/understanding-roles#storage.objectCreator)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | +|trf-df-0
serviceAccount|[roles/bigquery.dataOwner](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataOwner)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | ## Project dwh-cur | members | roles | |---|---| -|gcp-data-analysts
group|[roles/bigquery.dataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataViewer)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/bigquery.metadataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.metadataViewer)
[roles/bigquery.user](https://cloud.google.com/iam/docs/understanding-roles#bigquery.user)
[roles/datacatalog.tagTemplateViewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.tagTemplateViewer)
[roles/datacatalog.viewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.viewer)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | -|gcp-data-engineers
group|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin) | +|gcp-data-analysts
group|[roles/bigquery.dataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataViewer)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/datacatalog.tagTemplateViewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.tagTemplateViewer)
[roles/datacatalog.viewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.viewer)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | +|gcp-data-engineers
group|[roles/bigquery.dataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataViewer)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/datacatalog.tagTemplateViewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.tagTemplateViewer)
[roles/datacatalog.viewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.viewer)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | |SERVICE_IDENTITY_service-networking
serviceAccount|[roles/servicenetworking.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#servicenetworking.serviceAgent) +| -|load-df-0
serviceAccount|[roles/datacatalog.categoryAdmin](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.categoryAdmin) | |trf-bq-0
serviceAccount|[roles/bigquery.dataOwner](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataOwner)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser) | -|trf-df-0
serviceAccount|[roles/bigquery.dataOwner](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataOwner)
[roles/storage.objectCreator](https://cloud.google.com/iam/docs/understanding-roles#storage.objectCreator)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | +|trf-df-0
serviceAccount|[roles/bigquery.dataOwner](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataOwner)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | ## Project dwh-lnd | members | roles | |---|---| -|gcp-data-analysts
group|[roles/bigquery.dataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataViewer)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/bigquery.metadataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.metadataViewer)
[roles/bigquery.user](https://cloud.google.com/iam/docs/understanding-roles#bigquery.user)
[roles/datacatalog.tagTemplateViewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.tagTemplateViewer)
[roles/datacatalog.viewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.viewer)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | -|gcp-data-engineers
group|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin) | +|gcp-data-engineers
group|[roles/bigquery.dataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataViewer)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/datacatalog.tagTemplateViewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.tagTemplateViewer)
[roles/datacatalog.viewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.viewer)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | |SERVICE_IDENTITY_service-networking
serviceAccount|[roles/servicenetworking.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#servicenetworking.serviceAgent) +| |load-df-0
serviceAccount|[roles/bigquery.dataOwner](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataOwner)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/storage.objectCreator](https://cloud.google.com/iam/docs/understanding-roles#storage.objectCreator) | -|trf-bq-0
serviceAccount|[roles/bigquery.dataOwner](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataOwner)
[roles/datacatalog.categoryAdmin](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.categoryAdmin) | -|trf-df-0
serviceAccount|[roles/bigquery.dataOwner](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataOwner) | +|trf-bq-0
serviceAccount|[roles/bigquery.dataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataViewer)
[roles/datacatalog.categoryAdmin](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.categoryAdmin) | +|trf-df-0
serviceAccount|[roles/bigquery.dataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataViewer) | ## Project lod | members | roles | |---|---| -|gcp-data-engineers
group|[roles/compute.viewer](https://cloud.google.com/iam/docs/understanding-roles#compute.viewer)
[roles/dataflow.admin](https://cloud.google.com/iam/docs/understanding-roles#dataflow.admin)
[roles/dataflow.developer](https://cloud.google.com/iam/docs/understanding-roles#dataflow.developer)
[roles/viewer](https://cloud.google.com/iam/docs/understanding-roles#viewer) | +|gcp-data-engineers
group|[roles/dataflow.admin](https://cloud.google.com/iam/docs/understanding-roles#dataflow.admin)
[roles/dataflow.developer](https://cloud.google.com/iam/docs/understanding-roles#dataflow.developer) | |SERVICE_IDENTITY_dataflow-service-producer-prod
serviceAccount|[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | |SERVICE_IDENTITY_service-networking
serviceAccount|[roles/servicenetworking.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#servicenetworking.serviceAgent) +| |load-df-0
serviceAccount|[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/dataflow.admin](https://cloud.google.com/iam/docs/understanding-roles#dataflow.admin)
[roles/dataflow.worker](https://cloud.google.com/iam/docs/understanding-roles#dataflow.worker)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | @@ -71,7 +68,7 @@ Legend: + additive, conditional. | members | roles | |---|---| -|gcp-data-engineers
group|[roles/artifactregistry.admin](https://cloud.google.com/iam/docs/understanding-roles#artifactregistry.admin)
[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/cloudbuild.builds.editor](https://cloud.google.com/iam/docs/understanding-roles#cloudbuild.builds.editor)
[roles/composer.admin](https://cloud.google.com/iam/docs/understanding-roles#composer.admin)
[roles/composer.environmentAndStorageObjectAdmin](https://cloud.google.com/iam/docs/understanding-roles#composer.environmentAndStorageObjectAdmin)
[roles/iam.serviceAccountUser](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountUser)
[roles/iap.httpsResourceAccessor](https://cloud.google.com/iam/docs/understanding-roles#iap.httpsResourceAccessor)
[roles/serviceusage.serviceUsageConsumer](https://cloud.google.com/iam/docs/understanding-roles#serviceusage.serviceUsageConsumer)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | +|gcp-data-engineers
group|[roles/artifactregistry.admin](https://cloud.google.com/iam/docs/understanding-roles#artifactregistry.admin)
[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/cloudbuild.builds.editor](https://cloud.google.com/iam/docs/understanding-roles#cloudbuild.builds.editor)
[roles/composer.admin](https://cloud.google.com/iam/docs/understanding-roles#composer.admin)
[roles/composer.environmentAndStorageObjectAdmin](https://cloud.google.com/iam/docs/understanding-roles#composer.environmentAndStorageObjectAdmin)
[roles/iam.serviceAccountUser](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountUser)
[roles/iap.httpsResourceAccessor](https://cloud.google.com/iam/docs/understanding-roles#iap.httpsResourceAccessor)
[roles/serviceusage.serviceUsageConsumer](https://cloud.google.com/iam/docs/understanding-roles#serviceusage.serviceUsageConsumer)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | |SERVICE_IDENTITY_cloudcomposer-accounts
serviceAccount|[roles/composer.ServiceAgentV2Ext](https://cloud.google.com/iam/docs/understanding-roles#composer.ServiceAgentV2Ext)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | |SERVICE_IDENTITY_gcp-sa-cloudbuild
serviceAccount|[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | |SERVICE_IDENTITY_service-networking
serviceAccount|[roles/servicenetworking.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#servicenetworking.serviceAgent) +| diff --git a/blueprints/data-solutions/data-platform-foundations/README.md b/blueprints/data-solutions/data-platform-foundations/README.md index d48ae6cc..ad087216 100644 --- a/blueprints/data-solutions/data-platform-foundations/README.md +++ b/blueprints/data-solutions/data-platform-foundations/README.md @@ -237,7 +237,11 @@ To do this, you need to remove IAM binging at project-level for the `data-analys ### Project Configuration -The solution can be deployed creating projects on a given parent (organization or folder) or on existing projects. Configure variable `project_config` accordingly. +The solution can be deployed by creating projects on a given parent (organization or folder) or on existing projects. Configure variable `project_config` accordingly. + +When you rely on existing projects, the blueprint is designed to rely on different projects configuring IAM binding with an additive approach. For discovery or experimentation purposes, you may also configure `project_config.project_ids` to point different projects to one project with the granularity you need. For example, deploy resources from the 'load' project with resources in the 'transformation' project. + +Once you have identified the required project granularity for your use case, we suggest adapting the terraform script accordingly and relying on authoritative IAM binding. ## Demo pipeline From 08ba94aebccc4cf4b1f76ac24a22afd82ec5cfb2 Mon Sep 17 00:00:00 2001 From: Julio Diez Date: Fri, 24 Feb 2023 09:55:26 +0100 Subject: [PATCH 45/47] Allow to not use any health check Internet / serverless NEGs (Cloud Run) don't use them and it's an error to add one in their backend services. --- modules/net-ilb-l7/backend-service.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/net-ilb-l7/backend-service.tf b/modules/net-ilb-l7/backend-service.tf index a517bd08..ea758835 100644 --- a/modules/net-ilb-l7/backend-service.tf +++ b/modules/net-ilb-l7/backend-service.tf @@ -46,7 +46,7 @@ resource "google_compute_region_backend_service" "default" { description = var.description affinity_cookie_ttl_sec = each.value.affinity_cookie_ttl_sec connection_draining_timeout_sec = each.value.connection_draining_timeout_sec - health_checks = [ + health_checks = length(each.value.health_checks) == 0 ? null : [ for k in each.value.health_checks : lookup(local.hc_ids, k, k) ] # not for internet / serverless NEGs locality_lb_policy = each.value.locality_lb_policy From ae6080ebf7a1c92a740cad2149c52d94481f9388 Mon Sep 17 00:00:00 2001 From: Miren Esnaola Date: Fri, 24 Feb 2023 09:38:05 +0100 Subject: [PATCH 46/47] Modifications related to autopilot and workload identity. Added workload_identity_pool output to module --- modules/gke-cluster/README.md | 4 ++-- modules/gke-cluster/main.tf | 2 +- modules/gke-cluster/outputs.tf | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/modules/gke-cluster/README.md b/modules/gke-cluster/README.md index 2e09aeb1..2d60e487 100644 --- a/modules/gke-cluster/README.md +++ b/modules/gke-cluster/README.md @@ -91,8 +91,7 @@ module "cluster-autopilot" { master_ipv4_cidr_block = "192.168.0.0/28" } enable_features = { - autopilot = true - workload_identity = false + autopilot = true } } # tftest modules=1 resources=1 inventory=autopilot.yaml @@ -162,5 +161,6 @@ module "cluster-1" { | [name](outputs.tf#L49) | Cluster name. | | | [notifications](outputs.tf#L54) | GKE PubSub notifications topic. | | | [self_link](outputs.tf#L59) | Cluster self link. | ✓ | +| [workload_identity_pool](outputs.tf#L65) | Workload identity pool. | | diff --git a/modules/gke-cluster/main.tf b/modules/gke-cluster/main.tf index 0079dd8d..107d8341 100644 --- a/modules/gke-cluster/main.tf +++ b/modules/gke-cluster/main.tf @@ -379,7 +379,7 @@ resource "google_container_cluster" "cluster" { } dynamic "workload_identity_config" { - for_each = var.enable_features.workload_identity ? [""] : [] + for_each = (var.enable_features.workload_identity && !var.enable_features.autopilot) ? [""] : [] content { workload_pool = "${var.project_id}.svc.id.goog" } diff --git a/modules/gke-cluster/outputs.tf b/modules/gke-cluster/outputs.tf index f98f4f54..c02c9be2 100644 --- a/modules/gke-cluster/outputs.tf +++ b/modules/gke-cluster/outputs.tf @@ -61,3 +61,11 @@ output "self_link" { sensitive = true value = google_container_cluster.cluster.self_link } + +output "workload_identity_pool" { + description = "Workload identity pool." + value = "${var.project_id}.svc.id.goog" + depends_on = [ + google_container_cluster.cluster + ] +} \ No newline at end of file From 3b0223458b415d8ac371628b9d5f377c3887e215 Mon Sep 17 00:00:00 2001 From: Julio Diez Date: Fri, 24 Feb 2023 11:13:55 +0100 Subject: [PATCH 47/47] Don't define nor use health checks with SNEGs SNEGs don't use health checks and it's an error to add one in their backend services. 'terraform plan' doesn't detect it, only 'apply'. --- modules/net-ilb-l7/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/net-ilb-l7/README.md b/modules/net-ilb-l7/README.md index b5862f31..b6436e92 100644 --- a/modules/net-ilb-l7/README.md +++ b/modules/net-ilb-l7/README.md @@ -326,8 +326,10 @@ module "ilb-l7" { group = "my-neg" max_rate = { per_endpoint = 1 } }] + health_checks = [] } } + health_check_configs = {} neg_configs = { my-neg = { cloudrun = { @@ -343,7 +345,7 @@ module "ilb-l7" { subnetwork = var.subnet.self_link } } -# tftest modules=1 resources=6 +# tftest modules=1 resources=5 ``` ### URL Map