diff --git a/.gitignore b/.gitignore index 79fa83df50..91778178cf 100644 --- a/.gitignore +++ b/.gitignore @@ -21,13 +21,13 @@ bundle.zip **/*.pkrvars.hcl fixture_* fast/configs -fast/stages/**/[0-9]*providers.tf -fast/stages/**/terraform.tfvars -fast/stages/**/terraform.tfvars.json -fast/stages/**/terraform-*.auto.tfvars.json -fast/stages/**/0*.auto.tfvars* +fast/**/[0-9]*providers.tf +fast/**/terraform.tfvars +fast/**/terraform.tfvars.json +fast/**/terraform-*.auto.tfvars.json +fast/**/[0-9]*.auto.tfvars* **/node_modules -fast/stages/**/globals.auto.tfvars.json +fast/**/globals.auto.tfvars.json cloud_sql_proxy examples/cloud-operations/binauthz/tenant-setup.yaml examples/cloud-operations/binauthz/app/app.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index cedd3a40db..c365749440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -480,7 +480,7 @@ All notable changes to this project will be documented in this file. - fix `tag` output on `data-catalog-policy-tag` module - add shared-vpc support on `gcs-to-bq-with-least-privileges` - new `net-ilb-l7` module -- new [02-networking-peering](fast/stages/02-networking-peering) networking stage +- new `02-networking-peering` networking stage - **incompatible change** the variable for PSA ranges in networking stages have changed ## [14.0.0] - 2022-02-25 @@ -499,8 +499,8 @@ All notable changes to this project will be documented in this file. - **incompatible change** removed `ingress_settings` configuration option in the `cloud-functions` module. - new [m4ce VM example](blueprints/cloud-operations/vm-migration/) - Support for resource management tags in the `organization`, `folder`, `project`, `compute-vm`, and `kms` modules -- new [data platform](fast/stages/03-data-platform) stage 3 -- new [02-networking-nva](fast/stages/02-networking-nva) networking stage +- new `data platform` stage 3 +- new `02-networking-nva` networking stage - allow customizing the names of custom roles - added `environment` and `context` resource management tags - use resource management tags to restrict scope of roles/orgpolicy.policyAdmin diff --git a/blueprints/data-solutions/data-platform-foundations/README.md b/blueprints/data-solutions/data-platform-foundations/README.md index b038cfe4b6..a05bbae774 100644 --- a/blueprints/data-solutions/data-platform-foundations/README.md +++ b/blueprints/data-solutions/data-platform-foundations/README.md @@ -21,7 +21,7 @@ The approach adapts to different high-level requirements: - least privilege principle - rely on service account impersonation -The code in this blueprint doesn't address Organization-level configurations (Organization policy, VPC-SC, centralized logs). We expect those elements to be managed by automation stages external to this script like those in [FAST](../../../fast) and this blueprint deployed on top of them as one of the [stages](../../../fast/stages/03-data-platform/dev/README.md). +The code in this blueprint doesn't address Organization-level configurations (Organization policy, VPC-SC, centralized logs). We expect those elements to be managed by automation stages external to this script like those in [FAST](../../../fast) and this blueprint deployed on top of them as one of the [stages](../../../fast/stages/3-data-platform/dev/README.md). ### Project structure diff --git a/blueprints/gke/multitenant-fleet/README.md b/blueprints/gke/multitenant-fleet/README.md index 1e09afaa29..cadcf4109b 100644 --- a/blueprints/gke/multitenant-fleet/README.md +++ b/blueprints/gke/multitenant-fleet/README.md @@ -4,7 +4,7 @@ This blueprint presents an opinionated architecture to handle multiple homogeneo The pattern used in this design is useful, for blueprint, in cases where multiple clusters host/support the same workloads, such as in the case of a multi-regional deployment. Furthermore, combined with Anthos Config Sync and proper RBAC, this architecture can be used to host multiple tenants (e.g. teams, applications) sharing the clusters. -This blueprint is used as part of the [FAST GKE stage](../../../fast/stages/03-gke-multitenant/) but it can also be used independently if desired. +This blueprint is used as part of the [FAST GKE stage](../../../fast/stages/3-gke-multitenant/) but it can also be used independently if desired.

GKE multitenant diff --git a/blueprints/networking/hub-and-spoke-vpn/README.md b/blueprints/networking/hub-and-spoke-vpn/README.md index 5f596142f0..bdf877c737 100644 --- a/blueprints/networking/hub-and-spoke-vpn/README.md +++ b/blueprints/networking/hub-and-spoke-vpn/README.md @@ -7,7 +7,7 @@ A few additional features are also shown: - [custom BGP advertisements](https://cloud.google.com/router/docs/how-to/advertising-overview) to implement transitivity between spokes - [VPC Global Routing](https://cloud.google.com/network-connectivity/docs/router/how-to/configuring-routing-mode) to leverage a regional set of VPN gateways in different regions as next hops (used here for illustrative/study purpose, not usually done in real life) -The blueprint has been purposefully kept simple to show how to use and wire the VPC and VPN-HA modules together, and so that it can be used as a basis for experimentation. For a more complex scenario that better reflects real-life usage, including [Shared VPC](https://cloud.google.com/vpc/docs/shared-vpc) and [DNS cross-project binding](https://cloud.google.com/dns/docs/zones/cross-project-binding) please refer to the [FAST network stage](../../../fast/stages/02-networking-vpn/). +The blueprint has been purposefully kept simple to show how to use and wire the VPC and VPN-HA modules together, and so that it can be used as a basis for experimentation. For a more complex scenario that better reflects real-life usage, including [Shared VPC](https://cloud.google.com/vpc/docs/shared-vpc) and [DNS cross-project binding](https://cloud.google.com/dns/docs/zones/cross-project-binding) please refer to the [FAST network stage](../../../fast/stages/2-networking-b-vpn/). This is the high level diagram of this blueprint: diff --git a/diagram.svg b/diagram.svg new file mode 100644 index 0000000000..689adf24eb --- /dev/null +++ b/diagram.svg @@ -0,0 +1,293 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ + + +
+ +
+
+
+
+ + + +
+ +
+
+
+
+ + + +
+ +
+
+
+
+
+ + + + + + + +
+ +
+
+ +
+ Organization +
+
+ +
+ tag value [tenant] +
+
+ +
+ IAM bindings() +
+
+ +
+ organization policies() +
+
+
+
+ + + + + + +
+ «folder» +
+
+ +
+ Tenant0 +
+
+ +
+ IAM bindings() +
+
+ +
+ organization policies() +
+
+ +
+ tag bindings() +
+
+
+
+ + + + + + +
+ «folder» +
+
+ +
+ Tenant1 +
+
+ +
+ IAM bindings() +
+
+ +
+ organization policies() +
+
+ +
+ tag bindings() +
+
+
+
+ + + + + + +
+ «project» +
+
+ +
+ Tenant0_IaC +
+
+ +
+ service accounts [all stages] +
+
+ +
+ storage buckets [stage 0+1] +
+
+ +
+ optional CI/CD [stage 0+1] +
+
+ +
+ IAM bindings() +
+
+
+
+ + + + + + +
+ «project» +
+
+ +
+ Tenant1_IaC +
+
+ +
+ service accounts [all stages] +
+
+ +
+ storage buckets [stage 0+1] +
+
+ +
+ optional CI/CD [stage 0+1] +
+
+ +
+ IAM bindings() +
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/fast/README.md b/fast/README.md index e35a483728..7459c870ca 100644 --- a/fast/README.md +++ b/fast/README.md @@ -12,7 +12,7 @@ Fabric FAST was initially conceived to help enterprises quickly set up a GCP org ### Contracts and stages -FAST uses the concept of stages, which individually perform precise tasks but, taken together, build a functional, ready-to-use GCP organization. More importantly, stages are modeled around the security boundaries that typically appear in mature organizations. This arrangement allows delegating ownership of each stage to the team responsible for the types of resources it manages. For example, as its name suggests, the networking stage sets up all the networking elements and is usually the responsibility of a dedicated networking team within the organization. +FAST uses the concept of stages, which individually perform precise tasks but taken together build a functional, ready-to-use GCP organization. More importantly, stages are modeled around the security boundaries that typically appear in mature organizations. This arrangement allows delegating ownership of each stage to the team responsible for the types of resources it manages. For example, as its name suggests, the networking stage sets up all the networking elements and is usually the responsibility of a dedicated networking team within the organization. From the perspective of FAST's overall design, stages also work as contacts or interfaces, defining a set of pre-requisites and inputs required to perform their designed task and generating outputs needed by other stages lower in the chain. The diagram below shows the relationships between stages. @@ -20,7 +20,7 @@ From the perspective of FAST's overall design, stages also work as contacts or i Stages diagram

-Please refer to the [stages](./stages/) section for further details on each stage. +Please refer to the [stages](./stages/) section for further details on each stage. For details on tenant-level stages which introduce a deeper level of autonomy via nested FAST setups rooted in a top-level folder, refer to the [multitenant stages](#multitenant-organizations) section below. ### Security-first design @@ -32,11 +32,21 @@ FAST also aims to minimize the number of permissions granted to principals accor A resource factory consumes a simple representation of a resource (e.g., in YAML) and deploys it (e.g., using Terraform). Used correctly, factories can help decrease the management overhead of large-scale infrastructure deployments. See "[Resource Factories: A descriptive approach to Terraform](https://medium.com/google-cloud/resource-factories-a-descriptive-approach-to-terraform-581b3ebb59c)" for more details and the rationale behind factories. -FAST uses YAML-based factories to deploy subnets and firewall rules and, as its name suggests, in the [project factory](./stages/03-project-factory/) stage. +FAST uses YAML-based factories to deploy subnets and firewall rules and, as its name suggests, in the [project factory](./stages/3-project-factory/) stage. ### CI/CD -One of our objectives with FAST is to provide a lightweight reference design for the IaC repositories, and a built-in implementation for running our code in automated pipelines. Our CI/CD approach leverages [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation), and provides sample workflow configurations for several major providers. Refer to the [CI/CD section in the bootstrap stage](stages/00-bootstrap/README.md#cicd) for more details. We also provide separate [optional small stages](./extras/) to help you configure your CI/CD provider. +One of our objectives with FAST is to provide a lightweight reference design for the IaC repositories, and a built-in implementation for running our code in automated pipelines. Our CI/CD approach leverages [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation), and provides sample workflow configurations for several major providers. Refer to the [CI/CD section in the bootstrap stage](./stages/0-bootstrap/README.md#cicd) for more details. We also provide separate [optional small stages](./extras/) to help you configure your CI/CD provider. + +### Multitenant organizations + +FAST has built-in support for complex multitenant organizations, where each tenant has complete control over a separate hierarchy rooted in a top-level folder. This approach is particularly suited for large enterprises or governments, where country-level subsidiaries or government agencies have a wide degree of autonomy within a shared GCP organization managed by a central entity. + +FAST implements multitenancy via [dedicated stages](stages-multitenant) for tenant-level bootstrap and resource management, which configure separate hierarchies within the organization rooted in top-level folders, so that subsequent FAST stages (networking, security, data, etc.) can be used directly for each tenant. The diagram below shows the relationships between organization-level and tenant-level stages. + +

+ Stages diagram +

## Implementation @@ -57,9 +67,9 @@ Those familiar with Python will note that FAST follows many of the maxims in the ## Roadmap -Besides the features already described, FAST roadmap includes: +Besides the features already described, FAST also includes: - Stage to deploy environment-specific multitenant GKE clusters following Google's best practices - Stage to deploy a fully featured data platform -- Reference implementation to use FAST in CI/CD pipelines (in progress) -- Static policy enforcement +- Reference implementation to use FAST in CI/CD pipelines +- Static policy enforcement (planned) diff --git a/fast/extras/0-cicd-github/README.md b/fast/extras/0-cicd-github/README.md new file mode 100644 index 0000000000..58407b5e4e --- /dev/null +++ b/fast/extras/0-cicd-github/README.md @@ -0,0 +1,139 @@ +# FAST GitHub repository management + +This small extra stage allows creating and populating GitHub repositories used to host FAST stage code, including rewriting of module sources and secrets used for private modules repository access. + +It is designed for use in a GitHub organization, and is only meant as a one-shot solution with perishable state especially when used for initial population, as you don't want Terraform to keep overwriting your changes with initial versions of files. + +Initial population is only meant to be used with actual stage, while populating the modules repository should be done by hand to avoid hitting the GitHub hourly limit for their API. + +Once initial population is done, you need to manually push to the repository + +- the `.tfvars` file with custom variable values for your stages +- the workflow configuration file generated by FAST stages + +## GitHub provider credentials + +A [GitHub token](https://github.com/settings/tokens) is needed to authenticate against their API. The token needs organization-level permissions, like shown in this screenshot: + +

+ GitHub token scopes. +

+ +Once a token is available set it in the `GITHUB_TOKEN` environment variable before running Terraform. + +## Variable configuration + +The `organization` required variable sets the GitHub organization where repositories will be created, and is used to configure the Terraform provider. + +### Modules repository and sources + +The `modules_config` variable controls creation and management of the key and secret used to access the private modules repository, and indirectly control population of initial files: if the `modules_config` variable is not specified no module repository is know to the code, so module source paths cannot be replaced, and initial population of files cannot happen. If the variable is specified, an optional `source_ref` attribute can be set to the reference used to pin modules versions. + +This is an example that configures the modules repository name and an optional reference, enabling initial population of repositories where the feature has been turned on: + +```hcl +modules_config = { + repository_name = "GoogleCloudPlatform/cloud-foundation-fabric" + source_ref = "v19.0.0" +} +# 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: + +```hcl +modules_config = { + repository_name = "GoogleCloudPlatform/cloud-foundation-fabric" + key_config = { + create_key = true + create_secrets = true + } +} +# tftest skip +``` + +To use an existing keypair pass the path to the private key, the public key name is assumed to have the same name ending with the `.pub` suffix. This is useful in cases where the access key has already been set in the modules repository, and new repositories need to be created and their corresponding secret set: + +```hcl +modules_config = { + repository_name = "GoogleCloudPlatform/cloud-foundation-fabric" + key_config = { + create_secrets = true + keypair_path = "~/modules-repository-key" + } +} +# tftest skip +``` + +### Repositories + +The `repositories` variable is where you configure which repositories to create and whether initial population of files is desired. + +This is an example that creates repositories for stages 00 and 01, and populates initial files for stages 00, 01, and 02: + +```tfvars +repositories = { + fast_00_bootstrap = { + create_options = { + description = "FAST bootstrap." + features = { + issues = true + } + } + populate_from = "../../stages/0-bootstrap" + } + fast_01_resman = { + create_options = { + description = "FAST resource management." + features = { + issues = true + } + } + populate_from = "../../stages/1-resman" + } + fast_02_networking = { + populate_from = "../../stages/2-networking-peering" + } +} +# tftest skip +``` + +The `create_options` repository attribute controls creation: if the attribute is not present, the repository is assumed to be already existing. + +Initial population depends on a modules repository being configured in the `modules_config` variable described in the preceding section and on the`populate_from` attributes in each repository where population is required, which point to the folder holding the files to be committed. + +### Commit configuration + +Finally, a `commit_config` variable is optional: it can be used to configure author, email and message used in commits for initial population of files, its defaults are probably fine for most use cases. + + + + +## Files + +| name | description | resources | +|---|---|---| +| [cicd-versions.tf](./cicd-versions.tf) | Provider version. | | +| [main.tf](./main.tf) | Module-level locals and resources. | github_actions_secret · github_repository · github_repository_deploy_key · github_repository_file · tls_private_key | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [providers.tf](./providers.tf) | Provider configuration. | | +| [variables.tf](./variables.tf) | Module variables. | | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [organization](variables.tf#L50) | 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({…})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [clone](outputs.tf#L17) | Clone repository commands. | | + + diff --git a/fast/extras/00-cicd-github/cicd-versions.tf b/fast/extras/0-cicd-github/cicd-versions.tf similarity index 96% rename from fast/extras/00-cicd-github/cicd-versions.tf rename to fast/extras/0-cicd-github/cicd-versions.tf index 09f544cba0..830f1e48a3 100644 --- a/fast/extras/00-cicd-github/cicd-versions.tf +++ b/fast/extras/0-cicd-github/cicd-versions.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. diff --git a/fast/extras/00-cicd-github/github_token.png b/fast/extras/0-cicd-github/github_token.png similarity index 100% rename from fast/extras/00-cicd-github/github_token.png rename to fast/extras/0-cicd-github/github_token.png diff --git a/fast/extras/00-cicd-github/main.tf b/fast/extras/0-cicd-github/main.tf similarity index 72% rename from fast/extras/00-cicd-github/main.tf rename to fast/extras/0-cicd-github/main.tf index ac6028c17e..d91ab970c5 100644 --- a/fast/extras/00-cicd-github/main.tf +++ b/fast/extras/0-cicd-github/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,9 +15,6 @@ */ locals { - _modules_repository = [ - for k, v in var.repositories : local.repositories[k] if v.has_modules - ] _repository_files = flatten([ for k, v in var.repositories : [ for f in concat( @@ -30,12 +27,12 @@ locals { } ] if v.populate_from != null ]) - modules_ref = var.modules_ref == null ? "" : "?ref=${var.modules_ref}" - modules_repository = ( - length(local._modules_repository) > 0 - ? local._modules_repository.0 - : null + modules_ref = ( + try(var.modules_config.source_ref, null) == null + ? "" + : "?ref=${var.modules_config.source_ref}" ) + modules_repo = try(var.modules_config.repository_name, null) repositories = { for k, v in var.repositories : k => v.create_options == null ? k : github_repository.default[k].name @@ -56,6 +53,15 @@ locals { name = "templates/providers.tf.tpl" } if v.populate_from != null + }, + { + for k, v in var.repositories : + "${k}/templates/workflow-github.yaml" => { + repository = k + file = "../../assets/templates/workflow-github.yaml" + name = "templates/workflow-github.yaml" + } + if v.populate_from != null } ) } @@ -96,41 +102,49 @@ resource "github_repository" "default" { } resource "tls_private_key" "default" { - count = local.modules_repository != null ? 1 : 0 algorithm = "ED25519" } resource "github_repository_deploy_key" "default" { - count = local.modules_repository == null ? 0 : 1 + count = ( + try(var.modules_config.key_config.create_key, null) == true ? 1 : 0 + ) title = "Modules repository access" - repository = local.modules_repository - key = tls_private_key.default.0.public_key_openssh - read_only = true + repository = local.modules_repo + key = ( + try(var.modules_config.key_config.keypair_path, null) == null + ? tls_private_key.default.public_key_openssh + : file(pathexpand("${var.modules_config.key_config.keypair_path}.pub")) + ) + read_only = true } resource "github_actions_secret" "default" { - for_each = local.modules_repository == null ? {} : { - for k, v in local.repositories : - k => v if k != local.modules_repository - } - repository = each.key - secret_name = "CICD_MODULES_KEY" - plaintext_value = tls_private_key.default.0.private_key_openssh + for_each = ( + try(var.modules_config.key_config.create_secrets, null) == true + ? local.repositories + : {} + ) + repository = each.key + secret_name = "CICD_MODULES_KEY" + plaintext_value = ( + try(var.modules_config.key_config.keypair_path, null) == null + ? tls_private_key.default.private_key_openssh + : file(pathexpand("${var.modules_config.key_config.keypair_path}")) + ) } resource "github_repository_file" "default" { - for_each = ( - local.modules_repository == null ? {} : local.repository_files - ) + for_each = local.modules_repo == null ? {} : local.repository_files repository = local.repositories[each.value.repository] branch = "main" file = each.value.name content = ( - endswith(each.value.name, ".tf") && local.modules_repository != null + endswith(each.value.name, ".tf") && local.modules_repo != null ? replace( file(each.value.file), "/source\\s*=\\s*\"../../../modules/([^/\"]+)\"/", - "source = \"git@github.com:${var.organization}/${local.modules_repository}.git//$1${local.modules_ref}\"" # " + "source = \"git@github.com:${local.modules_repo}.git//$1${local.modules_ref}\"" # " ) : file(each.value.file) ) diff --git a/fast/extras/00-cicd-github/outputs.tf b/fast/extras/0-cicd-github/outputs.tf similarity index 96% rename from fast/extras/00-cicd-github/outputs.tf rename to fast/extras/0-cicd-github/outputs.tf index cb580e1fe2..61b5ffbc77 100644 --- a/fast/extras/00-cicd-github/outputs.tf +++ b/fast/extras/0-cicd-github/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. diff --git a/fast/extras/00-cicd-github/providers.tf b/fast/extras/0-cicd-github/providers.tf similarity index 95% rename from fast/extras/00-cicd-github/providers.tf rename to fast/extras/0-cicd-github/providers.tf index 29be30ae98..a7ccb32d4c 100644 --- a/fast/extras/00-cicd-github/providers.tf +++ b/fast/extras/0-cicd-github/providers.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. diff --git a/fast/extras/00-cicd-github/variables.tf b/fast/extras/0-cicd-github/variables.tf similarity index 75% rename from fast/extras/00-cicd-github/variables.tf rename to fast/extras/0-cicd-github/variables.tf index 0d9cb7fd6d..8e5d0832ff 100644 --- a/fast/extras/00-cicd-github/variables.tf +++ b/fast/extras/0-cicd-github/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. @@ -25,10 +25,26 @@ variable "commmit_config" { nullable = false } -variable "modules_ref" { - description = "Optional git ref used in module sources." - type = string - default = null +variable "modules_config" { + description = "Configure access to repository module via key, and replacement for modules sources in stage repositories." + type = object({ + repository_name = string + source_ref = optional(string) + key_config = optional(object({ + create_key = optional(bool, false) + create_secrets = optional(bool, false) + keypair_path = optional(string) + }), {}) + }) + default = null + validation { + condition = ( + var.modules_config == null + || + try(var.modules_config.repository_name, null) != null + ) + error_message = "Modules configuration requires a modules repository name." + } } variable "organization" { @@ -63,7 +79,6 @@ variable "repositories" { }), {}) visibility = optional(string, "private") })) - has_modules = optional(bool, false) populate_from = optional(string) })) default = {} diff --git a/fast/extras/00-cicd-github/README.md b/fast/extras/00-cicd-github/README.md deleted file mode 100644 index acf249bcfd..0000000000 --- a/fast/extras/00-cicd-github/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# FAST GitHub repository management - -This small extra stage allows creation and management of GitHub repositories used to host FAST stage code, including initial population of files and rewriting of module sources. - -This stage is designed for quick repository creation in a GitHub organization, and is not suited for medium or long-term repository management especially if you enable initial population of files. - -## Initial population caveats - -Initial file population of repositories is controlled via the `populate_from` attribute, and needs a bit of care: - -- never run this stage with the same variables used for population once the repository starts being used, as **Terraform will manage file state and revert any changes at each apply**, which is probably not what you want. -- initial population of the modules repository is discouraged, as the number of resulting files Terraform needs to manage is very close to the GitHub hourly limit for their API, it's much easier to populate modules via regular git commands - -The scenario for which this stage has been designed is one-shot creation and/or population of stage repositories, running it multiple times with different variables and Terraform states if incremental creation is needed for subsequent FAST stages (e.g. GKE, data platform, etc.). - -Once initial population is done, you need to manually push to the repository - -- the `.tfvars` file with custom variable values for your stages -- the workflow configuration file generated by FAST stages - -## GitHub provider credentials - -A [GitHub token](https://github.com/settings/tokens) is needed to authenticate against their API. The token needs organization-level permissions, like shown in this screenshot: - -

- GitHub token scopes. -

- -## Variable configuration - -The `organization` required variable sets the GitHub organization where repositories will be created, and is used to configure the Terraform provider. - -The `repositories` variable is where you configure which repositories to create, whether initial population of files is desired, and which repository is used to host modules. - -This is an example that creates repositories for stages 00 and 01, defines an existing repositories as the source for modules, and populates initial files for stages 00, 01, and 02: - -```tfvars -organization = "ludomagno" -repositories = { - fast_00_bootstrap = { - create_options = { - description = "FAST bootstrap." - features = { - issues = true - } - } - populate_from = "../../stages/00-bootstrap" - } - fast_01_resman = { - create_options = { - description = "FAST resource management." - features = { - issues = true - } - } - populate_from = "../../stages/01-resman" - } - fast_02_networking = { - populate_from = "../../stages/02-networking-peering" - } - fast_modules = { - has_modules = true - } -} -``` - -The `create_options` repository attribute controls creation: if the attribute is not present, the repository is assumed to be already existing. - -Initial population depends on a modules repository being configured, identified by the `has_modules` attribute, and on `populate_from` attributes in each repository where population is required, pointing to the folder holding the files to be committed. - -Finally, a `commit_config` variable is optional: it can be used to configure author, email and message used in commits for initial population of files, its defaults are probably fine for most use cases. - -## Modules secret - -When initial population is configured for a repository, this stage also adds a secret with the private key used to authenticate against the modules repository. This matches the configuration of the GitHub workflow files created for each FAST stage when CI/CD is enabled. - - - - -## Files - -| name | description | resources | -|---|---|---| -| [cicd-versions.tf](./cicd-versions.tf) | Provider version. | | -| [main.tf](./main.tf) | Module-level locals and resources. | github_actions_secret · github_repository · github_repository_deploy_key · github_repository_file · tls_private_key | -| [outputs.tf](./outputs.tf) | Module outputs. | | -| [providers.tf](./providers.tf) | Provider configuration. | | -| [variables.tf](./variables.tf) | Module variables. | | - -## Variables - -| name | description | type | required | default | -|---|---|:---:|:---:|:---:| -| [organization](variables.tf#L34) | GitHub organization. | string | ✓ | | -| [commmit_config](variables.tf#L17) | Configure commit metadata. | object({…}) | | {} | -| [modules_ref](variables.tf#L28) | Optional git ref used in module sources. | string | | null | -| [repositories](variables.tf#L39) | Repositories to create. | map(object({…})) | | {} | - -## Outputs - -| name | description | sensitive | -|---|---|:---:| -| [clone](outputs.tf#L17) | Clone repository commands. | | - - diff --git a/fast/extras/README.md b/fast/extras/README.md index 121fa4b049..9213224cda 100644 --- a/fast/extras/README.md +++ b/fast/extras/README.md @@ -2,4 +2,4 @@ This folder contains additional helper stages for FAST, which can be used to simplify specific operational tasks: -- [GitHub repository management](./00-cicd-github/) +- [GitHub repository management](./0-cicd-github/) diff --git a/fast/stage-links.sh b/fast/stage-links.sh new file mode 100755 index 0000000000..cfa7919d17 --- /dev/null +++ b/fast/stage-links.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# 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. +# 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. + +if [ $# -eq 0 ]; then + echo "Error: no folder or GCS bucket specified. Use -h or --help for usage." + exit 1 +fi + +if [[ "$1" == "-h" || "$1" == "--help" ]]; then + cat < $MESSAGE <---" +fi diff --git a/fast/stages-multitenant/0-bootstrap-tenant/IAM.md b/fast/stages-multitenant/0-bootstrap-tenant/IAM.md new file mode 100644 index 0000000000..543a82ab36 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/IAM.md @@ -0,0 +1,49 @@ +# IAM bindings reference + +Legend: + additive, conditional. + +## Organization [org_id #0] + +| members | roles | +|---|---| +|tn0-admins
group|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +
[roles/resourcemanager.organizationViewer](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.organizationViewer) +| +|tn0-gke-dev-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-gke-prod-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-networking-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-pf-dev-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-pf-prod-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-resman-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-sandbox-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-security-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-teams-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| + +## Folder test tenant 0 [#1] + +| members | roles | +|---|---| +|tn0-admins
group|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | +|tn0-networking-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin) | +|tn0-resman-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Project prod-iac-core-0 + +| members | roles | +|---|---| +|tn0-bootstrap-1
serviceAccount|[roles/logging.logWriter](https://cloud.google.com/iam/docs/understanding-roles#logging.logWriter) +| + +## Project tn0-audit-logs-0 + +| members | roles | +|---|---| +|f260055713332-284719
serviceAccount|[roles/logging.bucketWriter](https://cloud.google.com/iam/docs/understanding-roles#logging.bucketWriter) +| +|prod-resman-0
serviceAccount|[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner) | +|tn0-resman-0
serviceAccount|[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner) | + +## Project tn0-iac-core-0 + +| members | roles | +|---|---| +|tn0-admins
group|[roles/iam.serviceAccountTokenCreator](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountTokenCreator)
[roles/iam.workloadIdentityPoolAdmin](https://cloud.google.com/iam/docs/understanding-roles#iam.workloadIdentityPoolAdmin) | +|SERVICE_IDENTITY_service-networking
serviceAccount|[roles/servicenetworking.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#servicenetworking.serviceAgent) +| +|prod-resman-0
serviceAccount|[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner) | +|tn0-resman-0
serviceAccount|[roles/cloudbuild.builds.editor](https://cloud.google.com/iam/docs/understanding-roles#cloudbuild.builds.editor)
[roles/iam.serviceAccountAdmin](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountAdmin)
[roles/iam.workloadIdentityPoolAdmin](https://cloud.google.com/iam/docs/understanding-roles#iam.workloadIdentityPoolAdmin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/source.admin](https://cloud.google.com/iam/docs/understanding-roles#source.admin)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin) | diff --git a/fast/stages-multitenant/0-bootstrap-tenant/README.md b/fast/stages-multitenant/0-bootstrap-tenant/README.md new file mode 100644 index 0000000000..c1fc9530fb --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/README.md @@ -0,0 +1,210 @@ +# Tenant bootstrap + +The primary purpose of this stage is to decouple a single tenant from centrally managed resources in the organization, so that subsequent management of the tenant's own hierarchy and resources can be implemented with a high degree of autonomy. + +It is logically equivalent to organization-level bootstrap as it's concerned with setting up IAM bindings on a root node and creating supporting projects attached to it, but it depends on the organization-level resource management stage and uses the same service account and permissions since it operates at the hierarchy level (folders, tags, organization policies). + +The resources and policies managed here are: + +- the tag value in the `tenant` key used in IAM conditions +- the billing IAM bindings for the tenant-specific automation service accounts +- the organization-level IAM binding that allows conditional managing of org policies on the tenant folder +- the top-level tenant folder which acts as the root of the tenant's hierarchy +- any organization policy that needs to be set for the tenant on its root folder +- the tenant automation and logging projects +- service accounts for all tenant stages +- GCS buckets for bootstrap and resource management state +- optional CI/CD setup for this and the resource management tenant stages +- tenant-specific Workload Identity Federation pool and providers (planned) + +One notable difference compared to organization-level bootstrap is the creation of service accounts for all tenant stages: this is done here so that Billing and Organization Policy Admin bindings can be set, leveraging permissions of the org-level resman service account which is used to run this stage. Doing this here avoids the need to grant broad scoped permissions on the organization to tenant-level service accounts, and effectively decouples the tenant from the organization. + +The following diagram is a high level reference of what this stage manages, showing one hypothetical tenant (additional tenants require additional instances of this stage being deployed): + +```mermaid +%%{init: {'theme':'base'}}%% +classDiagram + Organization~🏢~ -- Tenant 0~📁~ + Tenant 0~📁~ -- tn0_automation + Tenant 0~📁~ -- tn0_logging + class Organization~🏢~ { + - tag value + - IAM bindings() + - org policies() + } + class Tenant 0~📁~ { + - log sinks + - IAM bindings() + - tag bindings() + } + class tn0_automation { + - GCS buckets + - service accounts + - optional CI/CD + - IAM bindings() + } + class tn0_logging { + - log sink destinations + } +``` + +As most of the features of this stage follow the same design and configurations of the [organization-level bootstrap stage](../../stages/0-bootstrap/), we will only focus on the tenant-specific configuration in this document. + +## Naming + +This stage sets the prefix used to name tenant resources, and passes it downstream to the other tenant stages together with the other globals needed by the tenant. The default is to append the tenant short name (a 3 or 4 letter acronym or abbreviation) to the organization-level prefix, if that is not desired this can be changed by editing local definitions in the `main.tf` file. Just be aware that some resources have name length constraints. + +## How to run this stage + +The tenant bootstrap stage is the effective boundary between organization and tenant-level resources: it uses the same inputs as the organization-level resource management stage, and produces outputs which provide the needed context to all other tenant stages. + +### Output files and cross-stage variables + +As mentioned above, the organization-level set of output files are used here with one exception: the provider file is different since state is specific to this stage. The `stage-links.sh` script can be used to get the commands needed for the provider and output files, just pass a single argument with your FAST output files folder path, or GCS bucket URI: + +```bash +../../stage-links.sh ~/fast-config +``` + +The script output can be copy/pasted to a terminal: + +```bash +# copy and paste the following commands for '0-bootstrap-tenant' + +cp ~/fast-config/providers/0-bootstrap-tenant-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 ./ + +# ---> remember to set the prefix in the provider file <--- +``` + +As shown in the script output above, the provider file is a template used as a source for potentially multiple tenant installations, so it needs to be specifically configured for this tenant by setting the backend `prefix` to a unique string so that the Terraform state file will not overlap with other tenants. Open it in an editor and perform the change before proceeding. + +### Global overrides + +The globals variable file linekd above contains definition which were set for the organization, for example the locations used for log sink destinations. These might not be correct for each tenant, so this stage allows overriding them via the tenant configuration variable described in the next section. + +### Tenant-level configuration + +The tenant configuration resides in the `tenant_config` variable, this is an example configuration for a tenant with comments explaining the different choices that need to be made: + +```hcl +tenant_config = { + # used for the top-level folder name + descriptive_name = "My First Tenant" + # tenant-specific groups, only the admin group is required + # the organization domain is automatically added after the group name + groups = { + gcp-admins = "tn01-admins" + # gcp-devops = "tn01-devops" + # gcp-network-admins = "tn01-networking" + # gcp-security-admins = "tn01-security" + } + # the 3 or 4 letter acronym or abbreviation used in resource names + short_name = "tn01" + # optional CI/CD configuration, refer to the org-level stages for information + # cicd = { + # branch = null + # identity_provider = "foo-provider" + # name = "myorg/tn01-bootstrap" + # type = "github" + # } + # optional group-level IAM bindings to add to the top-level folder + # group_iam = { + # tn01-support = ["roles/viewer"] + # } + # optional IAM bindings to add to the top-level folder + # iam = { + # "roles/logging.admin" = [ + # "serviceAccount:foo@myprj.iam.gserviceaccount.com" + # ] + # } + # optional location overrides to global locations + # locations = { + # bq = null + # gcs = null + # logging = null + # pubsub = null + # } + # optional folder ids for automation and logging project folders, typically + # added in later stages and entered here once created + # project_parent_ids = { + # automation = "folders/012345678" + # logging = "folders/0123456789" + # } +} +# tftest skip +``` + +Configure the tenant variable in a tfvars file for this stage. A few minor points worth noting: + +- the administrator group is the only one required here, specifying other groups only has the effect of populating the output file with group names for reuse in later stages +- the `iam` variable is merged with the IAM bindings for service accounts in the `main.tf` file, which take precedence; if a role specified in the variable is ignored, that's probably the case +- locations can be overridden at the attribute level, there's no need to specify those that are equal to the ones in the organization globals file + +### Running the stage + +Once the configuration is done just go through the usual `init/apply` cycle. On successful apply, a tfvars file specific for this tenant and a set of provider files will be created. + +### TODO + +- [ ] tenant-level Workload Identity Federation pool and providers configuration +- [ ] tenant-level logging project and sinks + + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [automation-sas.tf](./automation-sas.tf) | Tenant automation stage 2 and 3 service accounts. | iam-service-account | google_organization_iam_member | +| [automation.tf](./automation.tf) | Tenant automation project and resources. | gcs · iam-service-account · project | | +| [billing.tf](./billing.tf) | Billing roles for standalone billing accounts. | | google_billing_account_iam_member | +| [cicd.tf](./cicd.tf) | Workload Identity Federation configurations for CI/CD. | iam-service-account · source-repository | | +| [identity-providers.tf](./identity-providers.tf) | Workload Identity Federation provider definitions. | | google_iam_workload_identity_pool · google_iam_workload_identity_pool_provider | +| [log-export.tf](./log-export.tf) | Audit log project and sink. | bigquery-dataset · gcs · logging-bucket · project · pubsub | | +| [main.tf](./main.tf) | Module-level locals and resources. | folder | | +| [organization.tf](./organization.tf) | Organization tag and conditional IAM grant. | organization | google_organization_iam_member · google_tags_tag_value_iam_member | +| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | local_file | +| [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | | google_storage_bucket_object | +| [outputs.tf](./outputs.tf) | Module outputs. | | | +| [variables.tf](./variables.tf) | Module variables. | | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| [automation](variables.tf#L20) | Automation resources created by the organization-level bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | +| [billing_account](variables.tf#L38) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | +| [organization](variables.tf#L193) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L209) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [tag_keys](variables.tf#L232) | Organization tag keys. | object({…}) | ✓ | | 1-resman | +| [tag_names](variables.tf#L243) | Customized names for resource management tags. | object({…}) | ✓ | | 1-resman | +| [tag_values](variables.tf#L254) | Organization resource management tag values. | map(string) | ✓ | | 1-resman | +| [tenant_config](variables.tf#L261) | Tenant configuration. Short name must be 4 characters or less. | object({…}) | ✓ | | | +| [cicd_repositories](variables.tf#L51) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | +| [custom_roles](variables.tf#L97) | Custom roles defined at the organization level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [fast_features](variables.tf#L106) | Selective control for top-level FAST features. | object({…}) | | {} | 0-bootstrap | +| [federated_identity_providers](variables.tf#L120) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | +| [group_iam](variables.tf#L134) | Tenant-level custom group IAM settings in group => [roles] format. | map(list(string)) | | {} | | +| [iam](variables.tf#L140) | Tenant-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | +| [iam_additive](variables.tf#L146) | Tenant-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string)) | | {} | | +| [locations](variables.tf#L152) | Optional locations for GCS, BigQuery, and logging buckets created here. These are the defaults set at the organization level, and can be overridden via the tenant config variable. | object({…}) | | {…} | 0-bootstrap | +| [log_sinks](variables.tf#L172) | Tenant-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | +| [outputs_location](variables.tf#L203) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [project_parent_ids](variables.tf#L219) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the tenant folder as parent. | object({…}) | | {…} | | +| [test_principal](variables.tf#L301) | Used when testing to bypass the data source returning the current identity. | string | | null | | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| [cicd_workflows](outputs.tf#L102) | CI/CD workflows for tenant bootstrap and resource management stages. | ✓ | | +| [federated_identity](outputs.tf#L108) | Workload Identity Federation pool and providers. | | | +| [provider](outputs.tf#L118) | Terraform provider file for tenant resource management stage. | ✓ | stage-01 | +| [tenant_resources](outputs.tf#L125) | Tenant-level resources. | | | +| [tfvars](outputs.tf#L136) | Terraform variable files for the following tenant stages. | ✓ | | + + diff --git a/fast/stages-multitenant/0-bootstrap-tenant/automation-sas.tf b/fast/stages-multitenant/0-bootstrap-tenant/automation-sas.tf new file mode 100644 index 0000000000..c548f8d5fa --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/automation-sas.tf @@ -0,0 +1,127 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Tenant automation stage 2 and 3 service accounts. + +locals { + branch_sas = { + dp-dev = { + condition = join(" && ", [ + "resource.matchTag('${local.tag_keys.context}', 'data')", + "resource.matchTag('${local.tag_keys.environment}', 'development')" + ]) + description = "data platform dev" + flag = "data_platform" + } + dp-prod = { + condition = join(" && ", [ + "resource.matchTag('${local.tag_keys.context}', 'data')", + "resource.matchTag('${local.tag_keys.environment}', 'production')" + ]) + description = "data platform prod" + flag = "data_platform" + } + gke-dev = { + condition = join(" && ", [ + "resource.matchTag('${local.tag_keys.context}', 'gke')", + "resource.matchTag('${local.tag_keys.environment}', 'development')" + ]) + description = "GKE dev" + flag = "gke" + } + gke-prod = { + condition = join(" && ", [ + "resource.matchTag('${local.tag_keys.context}', 'gke')", + "resource.matchTag('${local.tag_keys.environment}', 'production')" + ]) + description = "GKE prod" + flag = "gke" + } + networking = { + condition = "resource.matchTag('${local.tag_keys.context}', 'networking')" + description = "networking" + flag = "-" + } + pf-dev = { + condition = "resource.matchTag('${local.tag_keys.environment}', 'development')" + description = "project factory dev" + flag = "project_factory" + } + pf-prod = { + condition = "resource.matchTag('${local.tag_keys.environment}', 'production')" + description = "project factory prod" + flag = "project_factory" + } + sandbox = { + condition = "resource.matchTag('${local.tag_keys.context}', 'sandbox')" + description = "sandbox" + flag = "sandbox" + } + security = { + condition = "resource.matchTag('${local.tag_keys.context}', 'security')" + description = "security" + flag = "-" + } + teams = { + condition = "resource.matchTag('${local.tag_keys.context}', 'teams')" + description = "teams" + flag = "teams" + } + } +} + +module "automation-tf-resman-sa-stage2-3" { + source = "../../../modules/iam-service-account" + for_each = { + for k, v in local.branch_sas : + k => v if lookup(local.fast_features, v.flag, true) + } + project_id = module.automation-project.project_id + name = "${each.key}-0" + display_name = "Terraform ${each.value.description} service account." + prefix = local.prefix + iam_billing_roles = !var.billing_account.is_org_level ? { + (var.billing_account.id) = [ + "roles/billing.user", "roles/billing.costsManager" + ] + } : {} + iam_organization_roles = var.billing_account.is_org_level ? { + (var.organization.id) = [ + "roles/billing.user", "roles/billing.costsManager" + ] + } : {} +} + +# assign org policy admin with a tag-based condition to stage 2 and 3 SAs + +resource "google_organization_iam_member" "org_policy_admin_stage2_3" { + for_each = { + for k, v in module.automation-tf-resman-sa-stage2-3 : k => v.iam_email + } + org_id = var.organization.id + role = "roles/orgpolicy.policyAdmin" + member = each.value + condition { + title = "org_policy_tag_${var.tenant_config.short_name}_${each.key}_scoped" + description = join("", [ + "Org policy tag scoped grant for tenant ${var.tenant_config.short_name} ", + local.branch_sas[each.key].description + ]) + expression = join(" && ", [ + local.iam_tenant_condition, local.branch_sas[each.key].condition + ]) + } +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/automation.tf b/fast/stages-multitenant/0-bootstrap-tenant/automation.tf new file mode 100644 index 0000000000..9684e7ca3e --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/automation.tf @@ -0,0 +1,141 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Tenant automation project and resources. + +module "automation-project" { + source = "../../../modules/project" + billing_account = var.billing_account.id + name = "iac-core-0" + parent = coalesce( + var.project_parent_ids.automation, + module.tenant-folder.id + ) + prefix = local.prefix + # human (groups) IAM bindings + group_iam = { + (local.groups.gcp-admins) = [ + "roles/iam.serviceAccountAdmin", + "roles/iam.serviceAccountTokenCreator", + ] + (local.groups.gcp-admins) = [ + "roles/iam.serviceAccountTokenCreator", + "roles/iam.workloadIdentityPoolAdmin" + ] + } + # machine (service accounts) IAM bindings + iam = { + "roles/owner" = [ + module.automation-tf-resman-sa.iam_email, + "serviceAccount:${local.resman_sa}" + ] + "roles/cloudbuild.builds.editor" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/iam.serviceAccountAdmin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/iam.workloadIdentityPoolAdmin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/source.admin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/storage.admin" = [ + module.automation-tf-resman-sa.iam_email + ] + } + services = [ + "accesscontextmanager.googleapis.com", + "bigquery.googleapis.com", + "bigqueryreservation.googleapis.com", + "bigquerystorage.googleapis.com", + "billingbudgets.googleapis.com", + "cloudbilling.googleapis.com", + "cloudbuild.googleapis.com", + "cloudkms.googleapis.com", + "cloudresourcemanager.googleapis.com", + "container.googleapis.com", + "compute.googleapis.com", + "container.googleapis.com", + "essentialcontacts.googleapis.com", + "iam.googleapis.com", + "iamcredentials.googleapis.com", + "orgpolicy.googleapis.com", + "pubsub.googleapis.com", + "servicenetworking.googleapis.com", + "serviceusage.googleapis.com", + "sourcerepo.googleapis.com", + "stackdriver.googleapis.com", + "storage-component.googleapis.com", + "storage.googleapis.com", + "sts.googleapis.com" + ] +} + +# output files bucket + +module "automation-tf-output-gcs" { + source = "../../../modules/gcs" + project_id = module.automation-project.project_id + name = "iac-core-outputs-0" + prefix = local.prefix + location = local.locations.gcs + storage_class = local.gcs_storage_class + versioning = true +} + +# resource management stage bucket and service account + +module "automation-tf-resman-gcs" { + source = "../../../modules/gcs" + project_id = module.automation-project.project_id + name = "iac-core-resman-0" + prefix = local.prefix + location = local.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.automation-tf-resman-sa.iam_email] + } +} + +module "automation-tf-resman-sa" { + source = "../../../modules/iam-service-account" + project_id = module.automation-project.project_id + name = "resman-0" + display_name = "Terraform stage 1 resman service account." + prefix = local.prefix + # allow SA used by CI/CD workflow to impersonate this SA + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.automation-tf-cicd-sa-resman["0"].iam_email, null) + ]) + } + iam_billing_roles = !var.billing_account.is_org_level ? { + (var.billing_account.id) = [ + "roles/billing.admin", "roles/billing.costsManager" + ] + } : {} + iam_organization_roles = var.billing_account.is_org_level ? { + (var.organization.id) = [ + "roles/billing.admin", "roles/billing.costsManager" + ] + } : {} + iam_storage_roles = { + (module.automation-tf-output-gcs.name) = ["roles/storage.admin"] + } +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/billing.tf b/fast/stages-multitenant/0-bootstrap-tenant/billing.tf new file mode 100644 index 0000000000..77c26b919c --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/billing.tf @@ -0,0 +1,39 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Billing roles for standalone billing accounts. + +# service account billing roles are in the SA module in automation.tf + +resource "google_billing_account_iam_member" "billing_ext_admin" { + for_each = toset(var.billing_account.is_org_level ? [] : [ + "group:${local.groups.gcp-admins}", + module.automation-tf-resman-sa.iam_email + ]) + billing_account_id = var.billing_account.id + role = "roles/billing.admin" + member = each.key +} + +resource "google_billing_account_iam_member" "billing_ext_cost_manager" { + for_each = toset(var.billing_account.is_org_level ? [] : [ + "group:${local.groups.gcp-admins}", + module.automation-tf-resman-sa.iam_email + ]) + billing_account_id = var.billing_account.id + role = "roles/billing.costsManager" + member = each.key +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/cicd.tf b/fast/stages-multitenant/0-bootstrap-tenant/cicd.tf new file mode 100644 index 0000000000..a25215af20 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/cicd.tf @@ -0,0 +1,223 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Workload Identity Federation configurations for CI/CD. + +locals { + _file_prefix = "tenants/${var.tenant_config.short_name}" + # derive identity pool names from identity providers for easy reference + cicd_identity_pools = { + for k, v in local.cicd_identity_providers : + k => split("/providers/", v.name)[0] + } + # merge org-level and tenant-level identity providers + cicd_identity_providers = merge( + var.automation.federated_identity_providers, + { + for k, v in google_iam_workload_identity_pool_provider.default : + k => { + issuer = local.identity_providers[k].issuer + issuer_uri = local.identity_providers[k].issuer_uri + name = v.name + principal_tpl = local.identity_providers[k].principal_tpl + principalset_tpl = local.identity_providers[k].principalset_tpl + } + }) + # filter CI/CD repositories to only keep valid ones + cicd_repositories = { + for k, v in coalesce(var.cicd_repositories, {}) : k => v + if( + v != null + && + ( + try(v.type, null) == "sourcerepo" + || + contains( + keys(local.cicd_identity_providers), + coalesce(try(v.identity_provider, null), ":") + ) + ) + && + fileexists( + format("${path.module}/templates/workflow-%s.yaml", try(v.type, "")) + ) + ) + } +} + +# tenant bootstrap runs in the org scope and uses top-level automation project + +module "automation-tf-cicd-repo-bootstrap" { + source = "../../../modules/source-repository" + for_each = { + for k, v in local.cicd_repositories : 0 => v + if k == "bootstrap" && try(v.type, null) == "sourcerepo" + } + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = [ + local.resman_sa + ] + "roles/source.reader" = [ + module.automation-tf-cicd-sa-bootstrap["0"].iam_email + ] + } + triggers = { + "fast-${var.tenant_config.short_name}-0-bootstrap" = { + filename = ".cloudbuild/workflow.yaml" + included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] + service_account = module.automation-tf-cicd-sa-bootstrap["0"].id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } +} + +module "automation-tf-cicd-sa-bootstrap" { + source = "../../../modules/iam-service-account" + for_each = { + for k, v in local.cicd_repositories : 0 => v + if k == "bootstrap" && try(v.type, null) != null + } + project_id = var.automation.project_id + name = "bootstrap-1" + display_name = "Terraform CI/CD ${var.tenant_config.short_name} bootstrap." + prefix = local.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? {} + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} + +module "automation-tf-org-resman-sa" { + source = "../../../modules/iam-service-account" + for_each = { + for k, v in local.cicd_repositories : 0 => v + if k == "bootstrap" && try(v.type, null) != null + } + project_id = var.automation.project_id + name = local.resman_sa + service_account_create = false + iam_additive = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.automation-tf-cicd-sa-bootstrap["0"].iam_email, null) + ]) + } +} + +# tenant resman runs in the tenant scope and uses its own automation project + +module "automation-tf-cicd-repo-resman" { + source = "../../../modules/source-repository" + for_each = { + for k, v in local.cicd_repositories : 0 => v + if k == "resman" && try(v.type, null) == "sourcerepo" + } + project_id = module.automation-project.project_id + name = each.value.name + iam = { + "roles/source.admin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/source.reader" = [ + module.automation-tf-cicd-sa-resman["0"].iam_email + ] + } + triggers = { + fast-1-resman = { + filename = ".cloudbuild/workflow.yaml" + included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] + service_account = module.automation-tf-cicd-sa-resman["0"].id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } +} + +module "automation-tf-cicd-sa-resman" { + source = "../../../modules/iam-service-account" + for_each = { + for k, v in local.cicd_repositories : 0 => v + if k == "resman" && try(v.type, null) != null + } + project_id = module.automation-project.project_id + name = "resman-1" + display_name = "Terraform CI/CD resman." + prefix = local.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? {} + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (module.automation-project.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (module.automation-tf-output-gcs.name) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/diagram.svg b/fast/stages-multitenant/0-bootstrap-tenant/diagram.svg new file mode 100644 index 0000000000..4090c7b092 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/diagram.svg @@ -0,0 +1,597 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fast/stages-multitenant/0-bootstrap-tenant/identity-providers.tf b/fast/stages-multitenant/0-bootstrap-tenant/identity-providers.tf new file mode 100644 index 0000000000..3f8499b75c --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/identity-providers.tf @@ -0,0 +1,96 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Workload Identity Federation provider definitions. + +locals { + identity_providers = { + for k, v in var.federated_identity_providers : k => merge( + v, + lookup(local.identity_providers_defs, v.issuer, {}) + ) + } + identity_providers_defs = { + # https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect + github = { + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.actor" = "assertion.actor" + "attribute.repository" = "assertion.repository" + "attribute.repository_owner" = "assertion.repository_owner" + "attribute.ref" = "assertion.ref" + } + issuer_uri = "https://token.actions.githubusercontent.com" + principal_tpl = "principal://iam.googleapis.com/%s/subject/repo:%s:ref:refs/heads/%s" + principalset_tpl = "principalSet://iam.googleapis.com/%s/attribute.repository/%s" + } + # https://docs.gitlab.com/ee/ci/cloud_services/index.html#how-it-works + gitlab = { + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.environment" = "assertion.environment" + "attribute.environment_protected" = "assertion.environment_protected" + "attribute.namespace_id" = "assertion.namespace_id" + "attribute.namespace_path" = "assertion.namespace_path" + "attribute.pipeline_id" = "assertion.pipeline_id" + "attribute.pipeline_source" = "assertion.pipeline_source" + "attribute.project_id" = "assertion.project_id" + "attribute.project_path" = "assertion.project_path" + "attribute.repository" = "assertion.project_path" + "attribute.ref" = "assertion.ref" + "attribute.ref_protected" = "assertion.ref_protected" + "attribute.ref_type" = "assertion.ref_type" + } + allowed_audiences = ["https://gitlab.com"] + issuer_uri = "https://gitlab.com" + principal_tpl = "principalSet://iam.googleapis.com/%s/attribute.sub/project_path:%s:ref_type:branch:ref:%s" + principalset_tpl = "principalSet://iam.googleapis.com/%s/attribute.repository/%s" + } + } +} + +resource "google_iam_workload_identity_pool" "default" { + provider = google-beta + count = length(local.identity_providers) > 0 ? 1 : 0 + project = module.automation-project.project_id + workload_identity_pool_id = "${var.prefix}-bootstrap" +} + +resource "google_iam_workload_identity_pool_provider" "default" { + provider = google-beta + for_each = local.identity_providers + project = module.automation-project.project_id + workload_identity_pool_id = ( + google_iam_workload_identity_pool.default.0.workload_identity_pool_id + ) + workload_identity_pool_provider_id = "${var.prefix}-bootstrap-${each.key}" + attribute_condition = each.value.attribute_condition + attribute_mapping = each.value.attribute_mapping + oidc { + allowed_audiences = ( + try(each.value.custom_settings.allowed_audiences, null) != null + ? each.value.custom_settings.allowed_audiences + : try(each.value.allowed_audiences, null) + ) + issuer_uri = ( + try(each.value.custom_settings.issuer_uri, null) != null + ? each.value.custom_settings.issuer_uri + : try(each.value.issuer_uri, null) + ) + } +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/log-export.tf b/fast/stages-multitenant/0-bootstrap-tenant/log-export.tf new file mode 100644 index 0000000000..b0bf115a25 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/log-export.tf @@ -0,0 +1,94 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Audit log project and sink. + +locals { + log_sink_destinations = merge( + # use the same dataset for all sinks with `bigquery` as destination + { for k, v in var.log_sinks : k => module.log-export-dataset.0 if v.type == "bigquery" }, + # use the same gcs bucket for all sinks with `storage` as destination + { for k, v in var.log_sinks : k => module.log-export-gcs.0 if v.type == "storage" }, + # use separate pubsub topics and logging buckets for sinks with + # destination `pubsub` and `logging` + module.log-export-pubsub, + module.log-export-logbucket + ) + log_types = toset([for k, v in var.log_sinks : v.type]) +} + +module "log-export-project" { + source = "../../../modules/project" + billing_account = var.billing_account.id + name = "audit-logs-0" + parent = coalesce( + var.project_parent_ids.logging, + module.tenant-folder.id + ) + prefix = local.prefix + iam = { + "roles/owner" = [ + module.automation-tf-resman-sa.iam_email, + "serviceAccount:${local.resman_sa}" + ] + } + services = [ + # "cloudresourcemanager.googleapis.com", + # "iam.googleapis.com", + # "serviceusage.googleapis.com", + "bigquery.googleapis.com", + "storage.googleapis.com", + "stackdriver.googleapis.com" + ] +} + +# one log export per type, with conditionals to skip those not needed + +module "log-export-dataset" { + source = "../../../modules/bigquery-dataset" + count = contains(local.log_types, "bigquery") ? 1 : 0 + project_id = module.log-export-project.project_id + id = "audit_export" + friendly_name = "Audit logs export." + location = var.locations.bq +} + +module "log-export-gcs" { + source = "../../../modules/gcs" + count = contains(local.log_types, "storage") ? 1 : 0 + project_id = module.log-export-project.project_id + name = "audit-logs-0" + prefix = local.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class +} + +module "log-export-logbucket" { + source = "../../../modules/logging-bucket" + for_each = toset([for k, v in var.log_sinks : k if v.type == "logging"]) + parent_type = "project" + parent = module.log-export-project.project_id + id = "audit-logs-${each.key}" + location = var.locations.logging +} + +module "log-export-pubsub" { + source = "../../../modules/pubsub" + for_each = toset([for k, v in var.log_sinks : k if v.type == "pubsub"]) + project_id = module.log-export-project.project_id + name = "audit-logs-${each.key}" + regions = var.locations.pubsub +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/main.tf b/fast/stages-multitenant/0-bootstrap-tenant/main.tf new file mode 100644 index 0000000000..3a1505949f --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/main.tf @@ -0,0 +1,100 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + gcs_storage_class = ( + length(split("-", local.locations.gcs)) < 2 + ? "MULTI_REGIONAL" + : "REGIONAL" + ) + groups = { + for k, v in var.tenant_config.groups : + k => v == null ? null : "${v}@${var.organization.domain}" + } + fast_features = { + for k, v in var.tenant_config.fast_features : + k => v == null ? var.fast_features[k] : v + } + locations = { + for k, v in var.tenant_config.locations : + k => v == null || v == [] ? var.locations[k] : v + } + prefix = join("-", compact([var.prefix, var.tenant_config.short_name])) + resman_sa = ( + var.test_principal == null + ? data.google_client_openid_userinfo.resman-sa.0.email + : var.test_principal + ) +} + +data "google_client_openid_userinfo" "resman-sa" { + count = var.test_principal == null ? 1 : 0 +} + +module "tenant-folder" { + source = "../../../modules/folder" + parent = "organizations/${var.organization.id}" + name = var.tenant_config.descriptive_name + logging_sinks = { + for name, attrs in var.log_sinks : name => { + bq_partitioned_table = attrs.type == "bigquery" + destination = local.log_sink_destinations[name].id + filter = attrs.filter + type = attrs.type + } + } + tag_bindings = { + tenant = try( + module.organization.tag_values["${var.tag_names.tenant}/${var.tenant_config.short_name}"].id, + null + ) + } +} + +module "tenant-folder-iam" { + source = "../../../modules/folder" + id = module.tenant-folder.id + folder_create = false + group_iam = merge(var.group_iam, { + (local.groups.gcp-admins) = [ + "roles/logging.admin", + "roles/owner", + "roles/resourcemanager.folderAdmin", + "roles/resourcemanager.projectCreator", + "roles/compute.xpnAdmin" + ] + }) + iam = merge(var.iam, { + "roles/compute.xpnAdmin" = [ + module.automation-tf-resman-sa.iam_email, + module.automation-tf-resman-sa-stage2-3["networking"].iam_email + ] + "roles/logging.admin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/resourcemanager.folderAdmin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/resourcemanager.projectCreator" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/owner" = [ + module.automation-tf-resman-sa.iam_email + ] + }) + iam_additive = var.iam_additive + depends_on = [module.automation-project] +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/organization.tf b/fast/stages-multitenant/0-bootstrap-tenant/organization.tf new file mode 100644 index 0000000000..46f8c0d4f5 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/organization.tf @@ -0,0 +1,84 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Organization tag and conditional IAM grant. + +locals { + iam_tenant_condition = "resource.matchTag('${local.tag_keys.tenant}', '${var.tenant_config.short_name}')" + tag_keys = { + for k, v in var.tag_names : k => "${var.organization.id}/${v}" + } +} + +module "organization" { + source = "../../../modules/organization" + organization_id = "organizations/${var.organization.id}" + iam_additive = merge( + { + "roles/resourcemanager.organizationViewer" = [ + "group:${local.groups.gcp-admins}" + ] + }, + var.billing_account.is_org_level ? { + "roles/billing.admin" = [ + "group:${local.groups.gcp-admins}", + module.automation-tf-resman-sa.iam_email + ] + "roles/billing.costsManager" = ["group:${local.groups.gcp-admins}"] + } : {} + ) + tags = { + tenant = { + id = var.tag_keys.tenant + values = { + (var.tenant_config.short_name) = {} + } + } + } +} + +resource "google_tags_tag_value_iam_member" "resman_tag_user" { + for_each = var.tag_values + tag_value = each.value + role = "roles/resourcemanager.tagUser" + member = module.automation-tf-resman-sa.iam_email +} + +resource "google_tags_tag_value_iam_member" "admins_tag_viewer" { + for_each = var.tag_values + tag_value = each.value + role = "roles/resourcemanager.tagViewer" + member = "group:${local.groups.gcp-admins}" +} + +# assign org policy admin with a tag-based condition to admin group and stage 1 SA + +resource "google_organization_iam_member" "org_policy_admin_stage0" { + for_each = toset([ + "group:${local.groups.gcp-admins}", + module.automation-tf-resman-sa.iam_email + ]) + org_id = var.organization.id + role = "roles/orgpolicy.policyAdmin" + member = each.key + condition { + title = "org_policy_tag_${var.tenant_config.short_name}_scoped" + description = "Org policy tag scoped grant for tenant ${var.tenant_config.short_name}." + expression = local.iam_tenant_condition + } +} + +# tag-based condition for service accounts is in the automation-sa file diff --git a/fast/stages-multitenant/0-bootstrap-tenant/outputs-files.tf b/fast/stages-multitenant/0-bootstrap-tenant/outputs-files.tf new file mode 100644 index 0000000000..28bec32763 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/outputs-files.tf @@ -0,0 +1,46 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Output files persistence to local filesystem. + +locals { + outputs_root = join("/", [ + try(pathexpand(var.outputs_location), ""), + "tenants", + var.tenant_config.short_name + ]) +} + +resource "local_file" "providers" { + count = var.outputs_location == null ? 0 : 1 + file_permission = "0644" + filename = "${local.outputs_root}/providers/1-resman-tenant-providers.tf" + content = try(local.provider, null) +} + +resource "local_file" "tfvars" { + count = var.outputs_location == null ? 0 : 1 + file_permission = "0644" + filename = "${local.outputs_root}/tfvars/0-bootstrap-tenant.auto.tfvars.json" + content = jsonencode(local.tfvars) +} + +resource "local_file" "workflows" { + for_each = var.outputs_location == null ? {} : local.cicd_workflows + file_permission = "0644" + filename = "${local.outputs_root}/workflows/${each.key}-${local.cicd_repositories[each.key].type}.yaml" + content = each.value +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/outputs-gcs.tf b/fast/stages-multitenant/0-bootstrap-tenant/outputs-gcs.tf new file mode 100644 index 0000000000..7c0c7efc37 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/outputs-gcs.tf @@ -0,0 +1,41 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Output files persistence to automation GCS bucket. + +resource "google_storage_bucket_object" "providers" { + bucket = module.automation-tf-output-gcs.name + # provider suffix allows excluding via .gitignore when linked from stages + name = "tenants/${var.tenant_config.short_name}/providers/1-resman-tenant-providers.tf" + content = local.provider +} + +resource "google_storage_bucket_object" "tfvars" { + bucket = module.automation-tf-output-gcs.name + name = "tenants/${var.tenant_config.short_name}/tfvars/0-bootstrap-tenant.auto.tfvars.json" + content = jsonencode(local.tfvars) +} + +resource "google_storage_bucket_object" "workflows" { + for_each = local.cicd_workflows + bucket = ( + each.key == "bootstrap" + ? var.automation.outputs_bucket + : module.automation-tf-output-gcs.name + ) + name = "tenants/${var.tenant_config.short_name}/workflows/${each.key}-${local.cicd_repositories[each.key].type}.yaml" + content = each.value +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/outputs.tf b/fast/stages-multitenant/0-bootstrap-tenant/outputs.tf new file mode 100644 index 0000000000..4f22ff6365 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/outputs.tf @@ -0,0 +1,140 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + cicd_workflows = { + for k, v in local.cicd_repositories : k => templatefile( + "${path.module}/templates/workflow-${v.type}.yaml", ( + k == "bootstrap" + ? { + identity_provider = try( + local.cicd_identity_providers[v["identity_provider"]].name, "" + ) + outputs_bucket = var.automation.outputs_bucket + service_account = try( + module.automation-tf-cicd-sa-bootstrap["0"].email, "" + ) + stage_name = k + tf_providers_file = "" + tf_var_files = [ + "0-bootstrap.auto.tfvars.json", + "1-resman.auto.tfvars.json", + "globals.auto.tfvars.json" + ] + } + : { + identity_provider = try( + local.cicd_identity_providers[v["identity_provider"]].name, "" + ) + outputs_bucket = module.automation-tf-output-gcs.name + service_account = try( + module.automation-tf-cicd-sa-resman["0"].email, "" + ) + stage_name = k + tf_providers_file = ( + "${local._file_prefix}/providers/1-resman-tenant-providers.tf" + ) + tf_var_files = [ + "${local._file_prefix}/tfvars/0-bootstrap-tenant.auto.tfvars.json" + ] + } + ) + ) + } + provider = templatefile( + "${path.module}/templates/providers.tf.tpl", { + bucket = module.automation-tf-resman-gcs.name + name = "resman" + sa = module.automation-tf-resman-sa.email + } + ) + tfvars = { + automation = { + outputs_bucket = module.automation-tf-output-gcs.name + project_id = module.automation-project.project_id + project_number = module.automation-project.number + federated_identity_pools = compact([ + try(google_iam_workload_identity_pool.default.0.name, null), + var.automation.federated_identity_pool, + ]) + federated_identity_providers = local.cicd_identity_providers + service_accounts = merge( + { resman = module.automation-tf-resman-sa.email }, + { + for k, v in local.branch_sas : k => try( + module.automation-tf-resman-sa-stage2-3[k].email, null + ) + } + ) + } + billing_account = var.billing_account + custom_roles = var.custom_roles + fast_features = local.fast_features + groups = var.tenant_config.groups + locations = local.locations + organization = var.organization + prefix = local.prefix + root_node = module.tenant-folder.id + short_name = var.tenant_config.short_name + tags = { + keys = var.tag_keys + names = var.tag_names + values = merge(var.tag_values, { + for k, v in module.organization.tag_values : k => v.id + }) + } + } +} + +output "cicd_workflows" { + description = "CI/CD workflows for tenant bootstrap and resource management stages." + sensitive = true + value = local.cicd_workflows +} + +output "federated_identity" { + description = "Workload Identity Federation pool and providers." + value = { + pool = try( + google_iam_workload_identity_pool.default.0.name, null + ) + providers = local.cicd_identity_providers + } +} + +output "provider" { + # tfdoc:output:consumers stage-01 + description = "Terraform provider file for tenant resource management stage." + sensitive = true + value = local.provider +} + +output "tenant_resources" { + description = "Tenant-level resources." + value = { + bucket = module.automation-tf-resman-gcs.name + folder = module.tenant-folder.id + project_id = module.automation-project.project_id + project_number = module.automation-project.number + service_account = module.automation-tf-resman-sa.email + } +} + +output "tfvars" { + description = "Terraform variable files for the following tenant stages." + sensitive = true + value = local.tfvars +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/templates/providers.tf.tpl b/fast/stages-multitenant/0-bootstrap-tenant/templates/providers.tf.tpl new file mode 100644 index 0000000000..e11a51b800 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/templates/providers.tf.tpl @@ -0,0 +1,30 @@ +/** + * 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. + * 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. + */ + +terraform { + backend "gcs" { + bucket = "${bucket}" + impersonate_service_account = "${sa}" + } +} +provider "google" { + impersonate_service_account = "${sa}" +} +provider "google-beta" { + impersonate_service_account = "${sa}" +} + +# end provider.tf for ${name} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-github.yaml b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-github.yaml new file mode 100644 index 0000000000..5232586808 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-github.yaml @@ -0,0 +1,190 @@ +# 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. + +name: "FAST ${stage_name} stage" + +on: + pull_request: + branches: + - main + types: + - closed + - opened + - synchronize + +env: + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + %{~ if tf_providers_file != "" ~} + TF_PROVIDERS_FILE: ${tf_providers_file} + %{~ endif ~} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + TF_VERSION: 1.3.2 + +jobs: + fast-pr: + permissions: + contents: read + id-token: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@v3 + + # set up SSH key authentication to the modules repository + - id: ssh-config + name: Configure SSH authentication + run: | + ssh-agent -a "$SSH_AUTH_SOCK" > /dev/null + ssh-add - <<< "$${{ secrets.CICD_MODULES_KEY }}" + + # set up authentication via Workload identity Federation + - id: gcp-auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0 + with: + workload_identity_provider: $${{ env.FAST_WIF_PROVIDER }} + service_account: $${{ env.FAST_SERVICE_ACCOUNT }} + access_token_lifetime: 3600s + + - id: gcp-sdk + name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v0 + with: + install_components: alpha + + # copy provider and tfvars files + - id: tf-config + name: Copy Terraform output files + run: | + %{~ if tf_providers_file != "" ~} + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/providers/$${{env.TF_PROVIDERS_FILE}}" ./ + %{~ endif ~} + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/tfvars" ./ + for f in $${{env.TF_VAR_FILES}}; do + ln -s "tfvars/$f" ./ + done + + - id: tf-setup + name: Set up Terraform + uses: hashicorp/setup-terraform@v2.0.3 + with: + terraform_version: $${{ env.TF_VERSION }} + + # run Terraform init/validate/plan + - id: tf-init + name: Terraform init + run: | + terraform init -no-color + + - id: tf-validate + name: Terraform validate + run: terraform validate -no-color + + - id: tf-plan + name: Terraform plan + continue-on-error: true + run: | + terraform plan -input=false -out ../plan.out -no-color + + - id: tf-apply + if: github.event.pull_request.merged == true && success() + name: Terraform apply + continue-on-error: true + run: | + terraform apply -input=false -auto-approve -no-color ../plan.out + + - id: pr-comment + name: Post comment to Pull Request + continue-on-error: true + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + env: + PLAN: $${{ steps.tf-plan.outputs.stdout }}\n$${{ steps.tf-plan.outputs.stderr }} + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + +
Validation Output + + \`\`\`\n + $${{ steps.tf-validate.outputs.stdout }} + \`\`\` + +
+ + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + +
Show Plan + + \`\`\`\n + $${process.env.PLAN.split('\n').filter(l => l.match(/^([A-Z\s].*|)$$/)).join('\n')} + \`\`\` + +
+ + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: pr-short-comment + name: Post comment to Pull Request + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' && steps.pr-comment.outcome != 'success' + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + + Plan output is in the action log. + + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: check-plan + name: Check plan failure + if: steps.tf-plan.outcome != 'success' + run: exit 1 + + - id: check-apply + name: Check apply failure + if: github.event.pull_request.merged == true && steps.tf-apply.outcome != 'success' + run: exit 1 diff --git a/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-gitlab.yaml b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-gitlab.yaml new file mode 100644 index 0000000000..739e748510 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-gitlab.yaml @@ -0,0 +1,124 @@ +# 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. + +default: + before_script: + - echo "$${CI_JOB_JWT_V2}" > token.txt + image: + name: hashicorp/terraform + entrypoint: + - "/usr/bin/env" + - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +variables: + GOOGLE_CREDENTIALS: cicd-sa-credentials.json + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + %{~ if tf_providers_file != "" ~} + TF_PROVIDERS_FILE: ${tf_providers_file} + %{~ endif ~} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + +stages: + - gcp-auth + - tf-files + - tf-plan + - tf-apply + +cache: + key: gcp-auth + paths: + - cicd-sa-credentials.json + - .tf-setup + +gcp-auth: + image: + name: google/cloud-sdk:slim + stage: gcp-auth + script: + - | + gcloud iam workload-identity-pools create-cred-config \ + $${FAST_WIF_PROVIDER} \ + --service-account=$${FAST_SERVICE_ACCOUNT} \ + --service-account-token-lifetime-seconds=3600 \ + --output-file=$${GOOGLE_CREDENTIALS} \ + --credential-source-file=token.txt +tf-files: + dependencies: + - gcp-auth + image: + name: google/cloud-sdk:slim + stage: tf-files + script: + # - gcloud components install -q alpha + - gcloud config set auth/credential_file_override $${GOOGLE_CREDENTIALS} + - mkdir -p .tf-setup + %{~ if tf_providers_file != "" ~} + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/providers/$${TF_PROVIDERS_FILE}" .tf-setup/ + %{~ endif ~} + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/tfvars" .tf-setup/ + +tf-plan: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-plan + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform plan + dependencies: + - tf-files + +tf-apply: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-apply + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform apply -input=false -auto-approve + dependencies: + - tf-files + when: manual + only: + variables: + - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH diff --git a/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-sourcerepo.yaml b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-sourcerepo.yaml new file mode 100644 index 0000000000..e171c45e75 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-sourcerepo.yaml @@ -0,0 +1,100 @@ +# 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. + +steps: + - name: alpine:3 + id: tf-download + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + mkdir -p /builder/home/.local/bin + wget https://releases.hashicorp.com/terraform/$${_TF_VERSION}/terraform_$${_TF_VERSION}_linux_amd64.zip + unzip terraform_$${_TF_VERSION}_linux_amd64.zip -d /builder/home/.local/bin + rm terraform_$${_TF_VERSION}_linux_amd64.zip + chmod 755 /builder/home/.local/bin/terraform + - name: alpine:3 + id: tf-check-format + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform fmt -recursive -check /workspace/ + - name: gcr.io/google.com/cloudsdktool/cloud-sdk:alpine + id: tf-files + entrypoint: bash + args: + - -eEuo + - pipefail + - -c + - |- + %{~ if tf_providers_file != "" ~} + /google-cloud-sdk/bin/gsutil cp \ + gs://$${_FAST_OUTPUTS_BUCKET}/providers/$${_TF_PROVIDERS_FILE} ./ + %{~ endif ~} + /google-cloud-sdk/bin/gsutil cp -r \ + gs://$${_FAST_OUTPUTS_BUCKET}/tfvars ./ + for f in $${_TF_VAR_FILES}; do + ln -s tfvars/$f ./ + done + - name: alpine:3 + id: tf-init + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform init -no-color + - name: alpine:3 + id: tf-check-validate + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform validate -no-color + - name: alpine:3 + id: tf-plan + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform plan -no-color -input=false -out plan.out + # store artifact and ask for approval here if needed + - name: alpine:3 + id: tf-apply + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform apply -no-color -input=false -auto-approve plan.out +options: + env: + - PATH=/usr/local/bin:/usr/bin:/bin:/builder/home/.local/bin + logging: CLOUD_LOGGING_ONLY +substitutions: + _FAST_OUTPUTS_BUCKET: ${outputs_bucket} + _TF_PROVIDERS_FILE: ${tf_providers_file} + _TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + _TF_VERSION: 1.3.2 diff --git a/fast/stages-multitenant/0-bootstrap-tenant/variables.tf b/fast/stages-multitenant/0-bootstrap-tenant/variables.tf new file mode 100644 index 0000000000..1a0134b08e --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/variables.tf @@ -0,0 +1,305 @@ +/** + * 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. + * 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. + */ + +# defaults for variables marked with global tfdoc annotations, can be set via +# the tfvars file generated in stage 00 and stored in its outputs + +variable "automation" { + # tfdoc:variable:source 0-bootstrap + description = "Automation resources created by the organization-level bootstrap stage." + type = object({ + outputs_bucket = string + project_id = string + project_number = string + federated_identity_pool = string + federated_identity_providers = map(object({ + issuer = string + issuer_uri = string + name = string + principal_tpl = string + principalset_tpl = string + })) + }) +} + +variable "billing_account" { + # tfdoc:variable:source 0-bootstrap + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." + 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 "cicd_repositories" { + description = "CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed." + type = object({ + bootstrap = optional(object({ + branch = optional(string) + identity_provider = string + name = string + type = string + })) + resman = optional(object({ + branch = optional(string) + identity_provider = string + name = string + type = string + })) + }) + default = null + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || try(v.name, null) != null + ]) + error_message = "Non-null repositories need a non-null name." + } + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || ( + try(v.identity_provider, null) != null + || + try(v.type, null) == "sourcerepo" + ) + ]) + error_message = "Non-null repositories need a non-null provider unless type is 'sourcerepo'." + } + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || ( + contains(["github", "gitlab", "sourcerepo"], coalesce(try(v.type, null), "null")) + ) + ]) + error_message = "Invalid repository type, supported types: 'github' 'gitlab' or 'sourcerepo'." + } +} + +variable "custom_roles" { + # tfdoc:variable:source 0-bootstrap + description = "Custom roles defined at the organization level, in key => id format." + type = object({ + service_project_network_admin = string + }) + default = null +} + +variable "fast_features" { + # tfdoc:variable:source 0-bootstrap + description = "Selective control for top-level FAST features." + type = object({ + data_platform = optional(bool, true) + gke = optional(bool, true) + project_factory = optional(bool, true) + sandbox = optional(bool, true) + teams = optional(bool, true) + }) + default = {} + nullable = false +} + +variable "federated_identity_providers" { + description = "Workload Identity Federation pools. The `cicd_repositories` variable references keys here." + type = map(object({ + attribute_condition = string + issuer = string + custom_settings = object({ + issuer_uri = string + allowed_audiences = list(string) + }) + })) + default = {} + nullable = false +} + +variable "group_iam" { + description = "Tenant-level custom group IAM settings in group => [roles] format." + type = map(list(string)) + default = {} +} + +variable "iam" { + description = "Tenant-level custom IAM settings in role => [principal] format." + type = map(list(string)) + default = {} +} + +variable "iam_additive" { + description = "Tenant-level custom IAM settings in role => [principal] format for non-authoritative bindings." + type = map(list(string)) + default = {} +} + +variable "locations" { + # tfdoc:variable:source 0-bootstrap + description = "Optional locations for GCS, BigQuery, and logging buckets created here. These are the defaults set at the organization level, and can be overridden via the tenant config variable." + type = object({ + bq = string + gcs = string + logging = string + pubsub = list(string) + }) + default = { + bq = "EU" + gcs = "EU" + logging = "global" + pubsub = [] + } + nullable = false +} + +# See https://cloud.google.com/architecture/exporting-stackdriver-logging-for-security-and-access-analytics +# for additional logging filter examples +variable "log_sinks" { + description = "Tenant-level log sinks, in name => {type, filter} format." + type = map(object({ + filter = string + type = string + })) + default = { + audit-logs = { + filter = "logName:\"/logs/cloudaudit.googleapis.com%2Factivity\" OR logName:\"/logs/cloudaudit.googleapis.com%2Fsystem_event\"" + type = "logging" + } + } + validation { + condition = alltrue([ + for k, v in var.log_sinks : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } +} + +variable "organization" { + # tfdoc:variable:source 0-bootstrap + description = "Organization details." + type = object({ + domain = string + id = number + customer_id = string + }) +} + +variable "outputs_location" { + description = "Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable." + type = string + default = null +} + +variable "prefix" { + # tfdoc:variable:source 0-bootstrap + description = "Prefix used for resources that need unique names. Use 9 characters or less." + type = string + validation { + condition = try(length(var.prefix), 0) < 10 + error_message = "Use a maximum of 9 characters for prefix." + } +} + +variable "project_parent_ids" { + description = "Optional parents for projects created here in folders/nnnnnnn format. Null values will use the tenant folder as parent." + type = object({ + automation = string + logging = string + }) + default = { + automation = null + logging = null + } + nullable = false +} + +variable "tag_keys" { + # tfdoc:variable:source 1-resman + description = "Organization tag keys." + type = object({ + context = string + environment = string + tenant = string + }) + nullable = false +} + +variable "tag_names" { + # tfdoc:variable:source 1-resman + description = "Customized names for resource management tags." + type = object({ + context = string + environment = string + tenant = string + }) + nullable = false +} + +variable "tag_values" { + # tfdoc:variable:source 1-resman + description = "Organization resource management tag values." + type = map(string) + nullable = false +} + +variable "tenant_config" { + description = "Tenant configuration. Short name must be 4 characters or less." + type = object({ + descriptive_name = string + groups = object({ + gcp-admins = string + gcp-devops = optional(string) + gcp-network-admins = optional(string) + gcp-security-admins = optional(string) + }) + short_name = string + fast_features = optional(object({ + data_platform = optional(bool) + gke = optional(bool) + project_factory = optional(bool) + sandbox = optional(bool) + teams = optional(bool) + }), {}) + locations = optional(object({ + bq = optional(string) + gcs = optional(string) + logging = optional(string) + pubsub = optional(list(string)) + }), {}) + }) + nullable = false + validation { + condition = alltrue([ + for a in ["descriptive_name", "groups", "short_name"] : + var.tenant_config[a] != null + ]) + error_message = "Non-optional members must not be null." + } + validation { + condition = length(var.tenant_config.short_name) < 5 + error_message = "Short name must be a string of 4 characters or less." + } +} + + +variable "test_principal" { + description = "Used when testing to bypass the data source returning the current identity." + type = string + default = null +} diff --git a/fast/stages-multitenant/1-resman-tenant/IAM.md b/fast/stages-multitenant/1-resman-tenant/IAM.md new file mode 100644 index 0000000000..16db4a6c5f --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/IAM.md @@ -0,0 +1,60 @@ +# IAM bindings reference + +Legend: + additive, conditional. + +## Folder development [#0] + +| members | roles | +|---|---| +|tn0-gke-dev-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder development [#1] + +| members | roles | +|---|---| +|tn0-gke-dev-0
serviceAccount|organizations/[org_id #0]/roles/serviceProjectNetworkAdmin | +|tn0-pf-dev-0
serviceAccount|organizations/[org_id #0]/roles/serviceProjectNetworkAdmin | + +## Folder networking + +| members | roles | +|---|---| +|tn0-networking-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder production [#0] + +| members | roles | +|---|---| +|tn0-gke-prod-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder production [#1] + +| members | roles | +|---|---| +|tn0-gke-prod-0
serviceAccount|organizations/[org_id #0]/roles/serviceProjectNetworkAdmin | +|tn0-pf-prod-0
serviceAccount|organizations/[org_id #0]/roles/serviceProjectNetworkAdmin | + +## Folder sandbox + +| members | roles | +|---|---| +|tn0-sandbox-0
serviceAccount|[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder security + +| members | roles | +|---|---| +|tn0-security-0
serviceAccount|[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder teams + +| members | roles | +|---|---| +|tn0-teams-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder test tenant 0 + +| members | roles | +|---|---| +|tn0-networking-0
serviceAccount|[roles/compute.orgFirewallPolicyAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.orgFirewallPolicyAdmin) +
[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin) +| +|tn0-security-0
serviceAccount|[roles/accesscontextmanager.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#accesscontextmanager.policyAdmin) +| diff --git a/fast/stages-multitenant/1-resman-tenant/README.md b/fast/stages-multitenant/1-resman-tenant/README.md new file mode 100644 index 0000000000..ae42fc3057 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/README.md @@ -0,0 +1,184 @@ +# Tenant resource management + +This stage is run for a specific tenant after [tenant bootstrap](../0-bootstrap-tenant/) has successfully created initial resources for the tenant, which is then decoupled from the organization. + +It is logically equivalent and almost identical in code to the corresponding [organization resource management stage](../../stages/1-resman/), with a few notable differences: + +- the hierarchy is rooted in the tenant top-level folder instead of the organization +- there's no management of tag values and keys since they organization-level resources (it could be implemented for tenant-specific tags if the need arises) +- automation service accounts for subsequent stages are configured but not created here (tenant-level bootstrap creates them and assigns organization-level permissions) + +The stage runs with a dedicated service account for the tenant, which has no permissions at the organization level except for billing and organization policies, constrained by a condition on the tenant tag. + +The following diagram is a high level reference of what this stage manages, showing one hypothetical tenant (additional tenants require additional instances of this stage being deployed): + +```mermaid +%%{init: {'theme':'base'}}%% +classDiagram + Tenant_root~📁~ -- tn0_automation + Tenant_root~📁~ -- Networking~📁~ + Tenant_root~📁~ -- Security~📁~ + Tenant_root~📁~ -- Data_Platform~📁~ + Data_Platform~📁~ -- DP_Dev~📁~ + Data_Platform~📁~ -- DP_Prod~📁~ + Tenant_root~📁~ -- GKE~📁~ + GKE~📁~ -- GKE_Dev~📁~ + GKE~📁~ -- GKE_Prod~📁~ + Tenant_root~📁~ -- Teams~📁~ + Teams~📁~ -- Team_0~📁~ + Team_0~📁~ -- Team_0_Dev~📁~ + Team_0~📁~ -- Team_0_Prod~📁~ + Tenant_root~📁~ -- Sandbox~📁~ + class Tenant_root~📁~ { + - IAM bindings() + - org policies() + } + class tn0_automation { + - GCS buckets + - IAM bindings() + } + class Data_Platform~📁~ { + - IAM bindings() + - tag bindings() + } + class DP_Dev~📁~ { + - IAM bindings() + - tag bindings() + } + class DP_Prod~📁~ { + - IAM bindings() + - tag bindings() + } + class GKE~📁~ { + - IAM bindings() + - tag bindings() + } + class GKE_Dev~📁~ { + - IAM bindings() + - tag bindings() + } + class GKE_Prod~📁~ { + - IAM bindings() + - tag bindings() + } + class Networking~📁~ { + - IAM bindings() + - tag bindings() + } + class Security~📁~ { + - IAM bindings() + - tag bindings() + } + class Sandbox~📁~ { + - IAM bindings() + - tag bindings() + } + class Teams~📁~ { + - IAM bindings() + - tag bindings() + } + class Team_0~📁~ { + - IAM bindings() + - tag bindings() + } + class Team_0_Dev~📁~ { + - IAM bindings() + - tag bindings() + } + class Team_0_Prod~📁~ { + - IAM bindings() + - tag bindings() + } +``` + +As most of the features of this stage follow the same design and configurations of the [organization-level resource management stage](../../stages/1-resman/), we will only focus on the tenant-specific configuration in this document. + +## How to run this stage + +As mentioned above this stage is decoupled from organization-level stages: it uses a service account and state bucket from the tenant-specific automation project, and its tfvars and provider files are also tenant-specific. + +The `stage-links.sh` script can be used to get the commands needed for the provider and output files, just set the variable for the tenant shortname (the same one specified in the tenant bootstrap stage) and pass a single argument with your FAST output files folder path, or GCS bucket URI: + +```bash +TENANT=tn0 ../../stage-links.sh ~/fast-config +``` + +The script output can be copy/pasted to a terminal: + +```bash +# copy and paste the following commands for '1-resman-tenant' + +ln -s ~/fast-config/tenants/tn0/providers/1-resman-tenant-providers.tf ./ +ln -s ~/fast-config/tenants/tn0/tfvars/0-bootstrap-tenant.auto.tfvars.json ./ +``` + +Once that is done, stage-level configuration variables are the same as the corresponding organization-level stage. + +### Running the stage + +Once the configuration is done just go through the usual `init/apply` cycle. On successful apply, a tfvars file specific for this tenant and a set of provider files will be created. + + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [branch-data-platform.tf](./branch-data-platform.tf) | Data Platform stages resources. | folder · gcs · iam-service-account | | +| [branch-gke.tf](./branch-gke.tf) | GKE multitenant stage resources. | folder · gcs · iam-service-account | | +| [branch-networking.tf](./branch-networking.tf) | Networking stage resources. | folder · gcs · iam-service-account | | +| [branch-project-factory.tf](./branch-project-factory.tf) | Project factory stage resources. | gcs · iam-service-account | | +| [branch-sandbox.tf](./branch-sandbox.tf) | Sandbox stage resources. | folder · gcs | | +| [branch-security.tf](./branch-security.tf) | Security stage resources. | folder · gcs · iam-service-account | | +| [branch-teams.tf](./branch-teams.tf) | Team stage resources. | folder · gcs · iam-service-account | | +| [cicd-data-platform.tf](./cicd-data-platform.tf) | CI/CD resources for the data platform branch. | iam-service-account · source-repository | | +| [cicd-gke.tf](./cicd-gke.tf) | CI/CD resources for the data platform branch. | iam-service-account · source-repository | | +| [cicd-networking.tf](./cicd-networking.tf) | CI/CD resources for the networking branch. | iam-service-account · source-repository | | +| [cicd-project-factory.tf](./cicd-project-factory.tf) | CI/CD resources for the teams branch. | iam-service-account · source-repository | | +| [cicd-security.tf](./cicd-security.tf) | CI/CD resources for the security branch. | iam-service-account · source-repository | | +| [main.tf](./main.tf) | Module-level locals and resources. | | | +| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | local_file | +| [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | | google_storage_bucket_object | +| [outputs.tf](./outputs.tf) | Module outputs. | | | +| [root_node.tf](./root_node.tf) | Tenant root folder configuration. | folder | | +| [variables.tf](./variables.tf) | Module variables. | | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | +| [billing_account](variables.tf#L51) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | +| [organization](variables.tf#L206) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L228) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [root_node](variables.tf#L239) | Root folder node for the tenant, in folders/nnnnnn format. | string | ✓ | | | +| [short_name](variables.tf#L244) | Short name used to identify the tenant. | string | ✓ | | | +| [tags](variables.tf#L249) | Resource management tags. | object({…}) | ✓ | | | +| [cicd_repositories](variables.tf#L64) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | +| [custom_roles](variables.tf#L146) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [data_dir](variables.tf#L155) | Relative path for the folder storing configuration data. | string | | "data" | | +| [fast_features](variables.tf#L161) | Selective control for top-level FAST features. | object({…}) | | {} | 0-0-bootstrap | +| [groups](variables.tf#L175) | Group names to grant organization-level permissions. | object({…}) | | {} | 0-bootstrap | +| [locations](variables.tf#L188) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 0-bootstrap | +| [organization_policy_data_path](variables.tf#L216) | Path for the data folder used by the organization policies factory. | string | | null | | +| [outputs_location](variables.tf#L222) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [team_folders](variables.tf#L267) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | +| [test_skip_data_sources](variables.tf#L277) | Used when testing to bypass data sources. | bool | | false | | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| [cicd_repositories](outputs.tf#L189) | WIF configuration for CI/CD repositories. | | | +| [dataplatform](outputs.tf#L203) | Data for the Data Platform stage. | | | +| [gke_multitenant](outputs.tf#L219) | Data for the GKE multitenant stage. | | 03-gke-multitenant | +| [networking](outputs.tf#L240) | Data for the networking stage. | | | +| [project_factories](outputs.tf#L249) | Data for the project factories stage. | | | +| [providers](outputs.tf#L264) | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking · 02-security · 03-dataplatform · xx-sandbox · xx-teams | +| [sandbox](outputs.tf#L271) | Data for the sandbox stage. | | xx-sandbox | +| [security](outputs.tf#L285) | Data for the networking stage. | | 02-security | +| [teams](outputs.tf#L295) | Data for the teams stage. | | | +| [tfvars](outputs.tf#L307) | Terraform variable files for the following stages. | ✓ | | + + diff --git a/fast/stages-multitenant/1-resman-tenant/branch-data-platform.tf b/fast/stages-multitenant/1-resman-tenant/branch-data-platform.tf new file mode 100644 index 0000000000..3916d63581 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-data-platform.tf @@ -0,0 +1,133 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Data Platform stages resources. + +module "branch-dp-folder" { + source = "../../../modules/folder" + count = var.fast_features.data_platform ? 1 : 0 + parent = module.root-folder.id + name = "Data Platform" + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/data"] + } +} + +module "branch-dp-dev-folder" { + source = "../../../modules/folder" + count = var.fast_features.data_platform ? 1 : 0 + parent = module.branch-dp-folder.0.id + name = "Development" + group_iam = {} + iam = { + (local.custom_roles.service_project_network_admin) = [ + local.automation_sas_iam.dp-dev + ] + # remove owner here and at project level if SA does not manage project resources + "roles/owner" = [local.automation_sas_iam.dp-dev] + "roles/logging.admin" = [local.automation_sas_iam.dp-dev] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.dp-dev] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.dp-dev] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.environment}/development"] + } +} + +module "branch-dp-prod-folder" { + source = "../../../modules/folder" + count = var.fast_features.data_platform ? 1 : 0 + parent = module.branch-dp-folder.0.id + name = "Production" + group_iam = {} + iam = { + (local.custom_roles.service_project_network_admin) = [ + local.automation_sas_iam.dp-prod + ] + # remove owner here and at project level if SA does not manage project resources + "roles/owner" = [local.automation_sas_iam.dp-prod] + "roles/logging.admin" = [local.automation_sas_iam.dp-prod] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.dp-prod] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.dp-prod] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.environment}/production"] + } +} + +# automation service accounts and buckets + +module "branch-dp-dev-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.data_platform ? 1 : 0 + project_id = var.automation.project_id + name = "dp-dev-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-dp-dev-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-dp-prod-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.data_platform ? 1 : 0 + project_id = var.automation.project_id + name = "dp-prod-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-dp-prod-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-dp-dev-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.data_platform ? 1 : 0 + project_id = var.automation.project_id + name = "dev-resman-dp-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.dp-dev] + } +} + +module "branch-dp-prod-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.data_platform ? 1 : 0 + project_id = var.automation.project_id + name = "prod-resman-dp-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.dp-prod] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-gke.tf b/fast/stages-multitenant/1-resman-tenant/branch-gke.tf new file mode 100644 index 0000000000..9ece810bb8 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-gke.tf @@ -0,0 +1,133 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description GKE multitenant stage resources. + +module "branch-gke-folder" { + source = "../../../modules/folder" + count = var.fast_features.gke ? 1 : 0 + parent = module.root-folder.id + name = "GKE" + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/gke"] + } +} + +module "branch-gke-dev-folder" { + source = "../../../modules/folder" + count = var.fast_features.gke ? 1 : 0 + parent = module.branch-gke-folder.0.id + name = "Development" + iam = { + "roles/owner" = [local.automation_sas_iam.gke-dev] + "roles/logging.admin" = [local.automation_sas_iam.gke-dev] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.gke-dev] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.gke-dev] + "roles/compute.xpnAdmin" = [local.automation_sas_iam.gke-dev] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.environment}/development"] + } +} + +module "branch-gke-prod-folder" { + source = "../../../modules/folder" + count = var.fast_features.gke ? 1 : 0 + parent = module.branch-gke-folder.0.id + name = "Production" + iam = { + "roles/owner" = [local.automation_sas_iam.gke-prod] + "roles/logging.admin" = [local.automation_sas_iam.gke-prod] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.gke-prod] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.gke-prod] + "roles/compute.xpnAdmin" = [local.automation_sas_iam.gke-prod] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.environment}/production"] + } +} + +module "branch-gke-dev-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.gke ? 1 : 0 + project_id = var.automation.project_id + name = "gke-dev-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = concat( + ( + local.groups.gcp-devops == null + ? [] + : ["group:${local.groups.gcp-devops}"] + ), + compact([ + try(module.branch-gke-dev-sa-cicd.0.iam_email, null) + ]) + ) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-gke-prod-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.gke ? 1 : 0 + project_id = var.automation.project_id + name = "gke-prod-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = concat( + ( + local.groups.gcp-devops == null + ? [] + : ["group:${local.groups.gcp-devops}"] + ), + compact([ + try(module.branch-gke-prod-sa-cicd.0.iam_email, null) + ]) + ) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-gke-dev-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.gke ? 1 : 0 + project_id = var.automation.project_id + name = "dev-resman-gke-0" + prefix = var.prefix + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.gke-dev] + } +} + +module "branch-gke-prod-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.gke ? 1 : 0 + project_id = var.automation.project_id + name = "prod-resman-gke-0" + prefix = var.prefix + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.gke-prod] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-networking.tf b/fast/stages-multitenant/1-resman-tenant/branch-networking.tf new file mode 100644 index 0000000000..85490baf03 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-networking.tf @@ -0,0 +1,107 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Networking stage resources. + +module "branch-network-folder" { + source = "../../../modules/folder" + parent = module.root-folder.id + name = "Networking" + group_iam = local.groups.gcp-network-admins == null ? {} : { + (local.groups.gcp-network-admins) = [ + # add any needed roles for resources/services not managed via Terraform, + # or replace editor with ~viewer if no broad resource management needed + # e.g. + # "roles/compute.networkAdmin", + # "roles/dns.admin", + # "roles/compute.securityAdmin", + "roles/editor", + ] + } + iam = { + "roles/logging.admin" = [local.automation_sas_iam.networking] + "roles/owner" = [local.automation_sas_iam.networking] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.networking] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.networking] + "roles/compute.xpnAdmin" = [local.automation_sas_iam.networking] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/networking"] + } +} + +module "branch-network-prod-folder" { + source = "../../../modules/folder" + parent = module.branch-network-folder.id + name = "Production" + iam = { + (local.custom_roles.service_project_network_admin) = concat( + local.branch_optional_sa_lists.dp-prod, + local.branch_optional_sa_lists.gke-prod, + local.branch_optional_sa_lists.pf-prod, + ) + } + tag_bindings = { + environment = var.tags.values["${var.tags.names.environment}/production"] + } +} + +module "branch-network-dev-folder" { + source = "../../../modules/folder" + parent = module.branch-network-folder.id + name = "Development" + iam = { + (local.custom_roles.service_project_network_admin) = concat( + local.branch_optional_sa_lists.dp-dev, + local.branch_optional_sa_lists.gke-dev, + local.branch_optional_sa_lists.pf-dev, + ) + } + tag_bindings = { + environment = var.tags.values["${var.tags.names.environment}/development"] + } +} + +# automation service account and bucket + +module "branch-network-sa" { + source = "../../../modules/iam-service-account" + project_id = var.automation.project_id + name = "networking-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-network-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-network-gcs" { + source = "../../../modules/gcs" + project_id = var.automation.project_id + name = "prod-resman-net-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.networking] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-project-factory.tf b/fast/stages-multitenant/1-resman-tenant/branch-project-factory.tf new file mode 100644 index 0000000000..2fa64bbc52 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-project-factory.tf @@ -0,0 +1,79 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Project factory stage resources. + +module "branch-pf-dev-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.project_factory ? 1 : 0 + project_id = var.automation.project_id + name = "pf-dev-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-pf-dev-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-pf-prod-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.project_factory ? 1 : 0 + project_id = var.automation.project_id + name = "pf-prod-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-pf-prod-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-pf-dev-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.project_factory ? 1 : 0 + project_id = var.automation.project_id + name = "dev-resman-pf-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.pf-dev] + } +} + +module "branch-pf-prod-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.project_factory ? 1 : 0 + project_id = var.automation.project_id + name = "prod-resman-pf-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.pf-prod] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-sandbox.tf b/fast/stages-multitenant/1-resman-tenant/branch-sandbox.tf new file mode 100644 index 0000000000..6f3d526c85 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-sandbox.tf @@ -0,0 +1,51 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Sandbox stage resources. + +module "branch-sandbox-folder" { + source = "../../../modules/folder" + count = var.fast_features.sandbox ? 1 : 0 + parent = module.root-folder.id + name = "Sandbox" + iam = { + "roles/logging.admin" = [local.automation_sas_iam.sandbox] + "roles/owner" = [local.automation_sas_iam.sandbox] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.sandbox] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.sandbox] + } + org_policies = { + "constraints/sql.restrictPublicIp" = { enforce = false } + "constraints/compute.vmExternalIpAccess" = { allow = { all = true } } + } + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/sandbox"] + } +} + +module "branch-sandbox-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.sandbox ? 1 : 0 + project_id = var.automation.project_id + name = "dev-resman-sbox-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.sandbox] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-security.tf b/fast/stages-multitenant/1-resman-tenant/branch-security.tf new file mode 100644 index 0000000000..d7253cce1a --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-security.tf @@ -0,0 +1,76 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Security stage resources. + +module "branch-security-folder" { + source = "../../../modules/folder" + parent = module.root-folder.id + name = "Security" + group_iam = local.groups.gcp-security-admins == null ? {} : { + (local.groups.gcp-security-admins) = [ + # add any needed roles for resources/services not managed via Terraform, + # e.g. + # "roles/bigquery.admin", + # "roles/cloudasset.owner", + # "roles/cloudkms.admin", + # "roles/logging.admin", + # "roles/secretmanager.admin", + # "roles/storage.admin", + "roles/viewer" + ] + } + iam = { + "roles/logging.admin" = [local.automation_sas_iam.security] + "roles/owner" = [local.automation_sas_iam.security] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.security] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.security] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/security"] + } +} + +# automation service account and bucket + +module "branch-security-sa" { + source = "../../../modules/iam-service-account" + project_id = var.automation.project_id + name = "security-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-security-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-security-gcs" { + source = "../../../modules/gcs" + project_id = var.automation.project_id + name = "prod-resman-sec-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.security] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-teams.tf b/fast/stages-multitenant/1-resman-tenant/branch-teams.tf new file mode 100644 index 0000000000..57f221104f --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-teams.tf @@ -0,0 +1,163 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Team stage resources. + +# TODO(ludo): add support for CI/CD + +############### top-level Teams branch and automation resources ############### + +module "branch-teams-folder" { + source = "../../../modules/folder" + count = var.fast_features.teams ? 1 : 0 + parent = module.root-folder.id + name = "Teams" + iam = { + "roles/logging.admin" = [local.automation_sas_iam.teams] + "roles/owner" = [local.automation_sas_iam.teams] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.teams] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.teams] + "roles/compute.xpnAdmin" = [local.automation_sas_iam.teams] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/teams"] + } +} + +module "branch-teams-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.teams ? 1 : 0 + project_id = var.automation.project_id + name = "teams-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-teams-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.teams ? 1 : 0 + project_id = var.automation.project_id + name = "prod-resman-teams-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.branch-teams-sa.0.iam_email] + } +} + +################## per-team folders and automation resources ################## + +module "branch-teams-team-folder" { + source = "../../../modules/folder" + for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} + parent = module.branch-teams-folder.0.id + name = each.value.descriptive_name + iam = { + "roles/logging.admin" = [module.branch-teams-team-sa[each.key].iam_email] + "roles/owner" = [module.branch-teams-team-sa[each.key].iam_email] + "roles/resourcemanager.folderAdmin" = [module.branch-teams-team-sa[each.key].iam_email] + "roles/resourcemanager.projectCreator" = [module.branch-teams-team-sa[each.key].iam_email] + "roles/compute.xpnAdmin" = [module.branch-teams-team-sa[each.key].iam_email] + } + group_iam = each.value.group_iam == null ? {} : each.value.group_iam +} + +module "branch-teams-team-sa" { + source = "../../../modules/iam-service-account" + for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} + project_id = var.automation.project_id + name = "prod-teams-${each.key}-0" + display_name = "Terraform team ${each.key} service account." + prefix = var.prefix + iam = { + "roles/iam.serviceAccountTokenCreator" = ( + each.value.impersonation_groups == null + ? [] + : [for g in each.value.impersonation_groups : "group:${g}"] + ) + } +} + +module "branch-teams-team-gcs" { + source = "../../../modules/gcs" + for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} + project_id = var.automation.project_id + name = "prod-teams-${each.key}-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.branch-teams-team-sa[each.key].iam_email] + } +} + +# per-team environment folders where project factory SAs can create projects + +module "branch-teams-team-dev-folder" { + source = "../../../modules/folder" + for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} + parent = module.branch-teams-team-folder[each.key].id + # naming: environment descriptive name + name = "Development" + # environment-wide human permissions on the whole teams environment + group_iam = {} + iam = { + (local.custom_roles.service_project_network_admin) = ( + local.branch_optional_sa_lists.pf-dev + ) + # remove owner here and at project level if SA does not manage project resources + "roles/owner" = local.branch_optional_sa_lists.pf-dev + "roles/logging.admin" = local.branch_optional_sa_lists.pf-dev + "roles/resourcemanager.folderAdmin" = local.branch_optional_sa_lists.pf-dev + "roles/resourcemanager.projectCreator" = local.branch_optional_sa_lists.pf-dev + } + tag_bindings = { + environment = try( + var.tags.values["${var.tags.names.environment}/development"], null + ) + } +} + +module "branch-teams-team-prod-folder" { + source = "../../../modules/folder" + for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} + parent = module.branch-teams-team-folder[each.key].id + # naming: environment descriptive name + name = "Production" + # environment-wide human permissions on the whole teams environment + group_iam = {} + iam = { + (local.custom_roles.service_project_network_admin) = ( + local.branch_optional_sa_lists.pf-prod + ) + # remove owner here and at project level if SA does not manage project resources + "roles/owner" = local.branch_optional_sa_lists.pf-prod + "roles/logging.admin" = local.branch_optional_sa_lists.pf-prod + "roles/resourcemanager.folderAdmin" = local.branch_optional_sa_lists.pf-prod + "roles/resourcemanager.projectCreator" = local.branch_optional_sa_lists.pf-prod + } + tag_bindings = { + environment = try( + var.tags.values["${var.tags.names.environment}/production"], null + ) + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/cicd-data-platform.tf b/fast/stages-multitenant/1-resman-tenant/cicd-data-platform.tf new file mode 100644 index 0000000000..704f45d78b --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/cicd-data-platform.tf @@ -0,0 +1,173 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description CI/CD resources for the data platform branch. + +# source repositories + +module "branch-dp-dev-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.data_platform_dev.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.data_platform_dev } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = local.branch_optional_sa_lists.dp-dev + "roles/source.reader" = compact([ + try(module.branch-dp-dev-sa-cicd.0.iam_email, "") + ]) + } + triggers = { + fast-03-dp-dev = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-dp-dev-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-dp-dev-sa-cicd] +} + +module "branch-dp-prod-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.data_platform_prod.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.data_platform_prod } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = local.branch_optional_sa_lists.dp-prod + "roles/source.reader" = [module.branch-dp-prod-sa-cicd.0.iam_email] + } + triggers = { + fast-03-dp-prod = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-dp-prod-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-dp-prod-sa-cicd] +} + +# SAs used by CI/CD workflows to impersonate automation SAs + +module "branch-dp-dev-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.data_platform_dev.name, null) != null + ? { 0 = local.cicd_repositories.data_platform_dev } + : {} + ) + project_id = var.automation.project_id + name = "dev-resman-dp-1" + display_name = "Terraform CI/CD data platform development service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} + +module "branch-dp-prod-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.data_platform_prod.name, null) != null + ? { 0 = local.cicd_repositories.data_platform_prod } + : {} + ) + project_id = var.automation.project_id + name = "prod-resman-dp-1" + display_name = "Terraform CI/CD data platform production service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/cicd-gke.tf b/fast/stages-multitenant/1-resman-tenant/cicd-gke.tf new file mode 100644 index 0000000000..dfd035a51b --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/cicd-gke.tf @@ -0,0 +1,175 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description CI/CD resources for the data platform branch. + +# source repositories + +module "branch-gke-dev-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.gke_dev.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.gke_dev } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = compact([ + try(module.branch-gke-dev-sa.0.iam_email, "") + ]) + "roles/source.reader" = compact([ + try(module.branch-gke-dev-sa-cicd.0.iam_email, "") + ]) + } + triggers = { + fast-03-gke-dev = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-gke-dev-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-gke-dev-sa-cicd] +} + +module "branch-gke-prod-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.gke_prod.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.gke_prod } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = [module.branch-gke-prod-sa.0.iam_email] + "roles/source.reader" = [module.branch-gke-prod-sa-cicd.0.iam_email] + } + triggers = { + fast-03-gke-prod = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-gke-prod-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-gke-prod-sa-cicd] +} + +# SAs used by CI/CD workflows to impersonate automation SAs + +module "branch-gke-dev-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.gke_dev.name, null) != null + ? { 0 = local.cicd_repositories.gke_dev } + : {} + ) + project_id = var.automation.project_id + name = "dev-resman-gke-1" + display_name = "Terraform CI/CD GKE development service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} + +module "branch-gke-prod-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.gke_prod.name, null) != null + ? { 0 = local.cicd_repositories.gke_prod } + : {} + ) + project_id = var.automation.project_id + name = "prod-resman-gke-1" + display_name = "Terraform CI/CD GKE production service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/cicd-networking.tf b/fast/stages-multitenant/1-resman-tenant/cicd-networking.tf new file mode 100644 index 0000000000..dbaf587d63 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/cicd-networking.tf @@ -0,0 +1,94 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description CI/CD resources for the networking branch. + +# source repository + +module "branch-network-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.networking.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.networking } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = [module.branch-network-sa.iam_email] + "roles/source.reader" = [module.branch-network-sa-cicd.0.iam_email] + } + triggers = { + fast-02-networking = { + filename = ".cloudbuild/workflow.yaml" + included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] + service_account = module.branch-network-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-network-sa-cicd] +} + +# SA used by CI/CD workflows to impersonate automation SAs + +module "branch-network-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.networking.name, null) != null + ? { 0 = local.cicd_repositories.networking } + : {} + ) + project_id = var.automation.project_id + name = "prod-resman-net-1" + display_name = "Terraform CI/CD stage 2 networking service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/cicd-project-factory.tf b/fast/stages-multitenant/1-resman-tenant/cicd-project-factory.tf new file mode 100644 index 0000000000..4c46d8585e --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/cicd-project-factory.tf @@ -0,0 +1,191 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description CI/CD resources for the teams branch. + +# source repositories + +moved { + from = module.branch-teams-dev-pf-cicd-repo + to = module.branch-pf-dev-cicd-repo +} + +module "branch-pf-dev-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.project_factory_dev.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.project_factory_dev } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = local.branch_optional_sa_lists.pf-dev + "roles/source.reader" = [module.branch-pf-dev-sa-cicd.0.iam_email] + } + triggers = { + fast-03-pf-dev = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-pf-dev-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-pf-dev-sa-cicd] +} + +moved { + from = module.branch-teams-prod-pf-cicd-repo + to = module.branch-pf-prod-cicd-repo +} + +module "branch-pf-prod-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.project_factory_prod.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.project_factory_prod } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = local.branch_optional_sa_lists.pf-prod + "roles/source.reader" = [module.branch-pf-prod-sa-cicd.0.iam_email] + } + triggers = { + fast-03-pf-prod = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-pf-prod-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-pf-prod-sa-cicd] +} + +# SAs used by CI/CD workflows to impersonate automation SAs + +moved { + from = module.branch-teams-dev-pf-sa-cicd + to = module.branch-pf-dev-sa-cicd +} + +module "branch-pf-dev-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.project_factory_dev.name, null) != null + ? { 0 = local.cicd_repositories.project_factory_dev } + : {} + ) + project_id = var.automation.project_id + name = "dev-pf-resman-pf-1" + display_name = "Terraform CI/CD project factory development service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} + +moved { + from = module.branch-teams-prod-pf-sa-cicd + to = module.branch-pf-prod-sa-cicd +} + +module "branch-pf-prod-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.project_factory_prod.name, null) != null + ? { 0 = local.cicd_repositories.project_factory_prod } + : {} + ) + project_id = var.automation.project_id + name = "prod-pf-resman-pf-1" + display_name = "Terraform CI/CD project factory production service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + var.automation.federated_identity_pool, + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/cicd-security.tf b/fast/stages-multitenant/1-resman-tenant/cicd-security.tf new file mode 100644 index 0000000000..5cb1581cfa --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/cicd-security.tf @@ -0,0 +1,94 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description CI/CD resources for the security branch. + +# source repository + +module "branch-security-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.security.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.security } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = [module.branch-security-sa.iam_email] + "roles/source.reader" = [module.branch-security-sa-cicd.0.iam_email] + } + triggers = { + fast-02-security = { + filename = ".cloudbuild/workflow.yaml" + included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] + service_account = module.branch-security-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-security-sa-cicd] +} + +# SA used by CI/CD workflows to impersonate automation SAs + +module "branch-security-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.security.name, null) != null + ? { 0 = local.cicd_repositories.security } + : {} + ) + project_id = var.automation.project_id + name = "prod-resman-sec-1" + display_name = "Terraform CI/CD stage 2 security service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages/01-resman/data/org-policies/compute.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/compute.yaml similarity index 100% rename from fast/stages/01-resman/data/org-policies/compute.yaml rename to fast/stages-multitenant/1-resman-tenant/data/org-policies/compute.yaml diff --git a/fast/stages/01-resman/data/org-policies/iam.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/iam.yaml similarity index 100% rename from fast/stages/01-resman/data/org-policies/iam.yaml rename to fast/stages-multitenant/1-resman-tenant/data/org-policies/iam.yaml diff --git a/fast/stages/01-resman/data/org-policies/serverless.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/serverless.yaml similarity index 100% rename from fast/stages/01-resman/data/org-policies/serverless.yaml rename to fast/stages-multitenant/1-resman-tenant/data/org-policies/serverless.yaml diff --git a/fast/stages/01-resman/data/org-policies/sql.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/sql.yaml similarity index 100% rename from fast/stages/01-resman/data/org-policies/sql.yaml rename to fast/stages-multitenant/1-resman-tenant/data/org-policies/sql.yaml diff --git a/fast/stages/01-resman/data/org-policies/storage.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/storage.yaml similarity index 100% rename from fast/stages/01-resman/data/org-policies/storage.yaml rename to fast/stages-multitenant/1-resman-tenant/data/org-policies/storage.yaml diff --git a/fast/stages/01-resman/diagram.png b/fast/stages-multitenant/1-resman-tenant/diagram.png similarity index 100% rename from fast/stages/01-resman/diagram.png rename to fast/stages-multitenant/1-resman-tenant/diagram.png diff --git a/fast/stages/01-resman/diagram.svg b/fast/stages-multitenant/1-resman-tenant/diagram.svg similarity index 100% rename from fast/stages/01-resman/diagram.svg rename to fast/stages-multitenant/1-resman-tenant/diagram.svg diff --git a/fast/stages-multitenant/1-resman-tenant/main.tf b/fast/stages-multitenant/1-resman-tenant/main.tf new file mode 100644 index 0000000000..76c046396b --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/main.tf @@ -0,0 +1,79 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + automation_resman_sa_iam = [ + "serviceAccount:${var.automation.service_accounts.resman}" + ] + automation_sas_iam = { + for k, v in var.automation.service_accounts : + k => v == null ? null : "serviceAccount:${v}" + } + branch_optional_sa_lists = { + dp-dev = compact([local.automation_sas_iam.dp-dev]) + dp-prod = compact([local.automation_sas_iam.dp-prod]) + gke-dev = compact([local.automation_sas_iam.gke-dev]) + gke-prod = compact([local.automation_sas_iam.gke-prod]) + pf-dev = compact([local.automation_sas_iam.pf-dev]) + pf-prod = compact([local.automation_sas_iam.pf-prod]) + } + # derive identity pool names from identity providers for easy reference + cicd_identity_pools = { + for k, v in local.cicd_identity_providers : + k => split("/providers/", v.name)[0] + } + cicd_identity_providers = coalesce( + try(var.automation.federated_identity_providers, null), {} + ) + cicd_repositories = { + for k, v in coalesce(var.cicd_repositories, {}) : k => v + if( + v != null && + ( + try(v.type, null) == "sourcerepo" + || + contains( + keys(local.cicd_identity_providers), + coalesce(try(v.identity_provider, null), ":") + ) + ) && + fileexists("${path.module}/templates/workflow-${try(v.type, "")}.yaml") + ) + } + cicd_workflow_var_files = { + stage_2 = [ + "0-bootstrap-tenant.auto.tfvars.json", + ] + stage_3 = [ + "0-bootstrap-tenant.auto.tfvars.json", + "2-networking.auto.tfvars.json", + "2-security.auto.tfvars.json" + ] + } + custom_roles = coalesce(var.custom_roles, {}) + gcs_storage_class = ( + length(split("-", var.locations.gcs)) < 2 + ? "MULTI_REGIONAL" + : "REGIONAL" + ) + groups = { + for k, v in var.groups : + k => v == null ? null : "${v}@${var.organization.domain}" + } + groups_iam = { + for k, v in local.groups : k => v != null ? "group:${v}" : null + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/outputs-files.tf b/fast/stages-multitenant/1-resman-tenant/outputs-files.tf new file mode 100644 index 0000000000..29d5ed4606 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/outputs-files.tf @@ -0,0 +1,46 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Output files persistence to local filesystem. + +locals { + outputs_root = join("/", [ + try(pathexpand(var.outputs_location), ""), + "tenants", + var.short_name + ]) +} + +resource "local_file" "providers" { + for_each = var.outputs_location == null ? {} : local.providers + file_permission = "0644" + filename = "${local.outputs_root}/providers/${each.key}-providers.tf" + content = try(each.value, null) +} + +resource "local_file" "tfvars" { + count = var.outputs_location == null ? 0 : 1 + file_permission = "0644" + filename = "${local.outputs_root}/tfvars/1-resman.auto.tfvars.json" + content = jsonencode(local.tfvars) +} + +resource "local_file" "workflows" { + for_each = var.outputs_location == null ? {} : local.cicd_workflows + file_permission = "0644" + filename = "${local.outputs_root}/workflows/${replace(each.key, "_", "-")}-workflow.yaml" + content = try(each.value, null) +} diff --git a/fast/stages-multitenant/1-resman-tenant/outputs-gcs.tf b/fast/stages-multitenant/1-resman-tenant/outputs-gcs.tf new file mode 100644 index 0000000000..6b0fc89cb0 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/outputs-gcs.tf @@ -0,0 +1,37 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Output files persistence to automation GCS bucket. + +resource "google_storage_bucket_object" "providers" { + for_each = local.providers + bucket = var.automation.outputs_bucket + name = "providers/${each.key}-providers.tf" + content = each.value +} + +resource "google_storage_bucket_object" "tfvars" { + bucket = var.automation.outputs_bucket + name = "tfvars/1-resman.auto.tfvars.json" + content = jsonencode(local.tfvars) +} + +resource "google_storage_bucket_object" "workflows" { + for_each = local.cicd_workflows + bucket = var.automation.outputs_bucket + name = "workflows/${replace(each.key, "_", "-")}-workflow.yaml" + content = each.value +} diff --git a/fast/stages-multitenant/1-resman-tenant/outputs.tf b/fast/stages-multitenant/1-resman-tenant/outputs.tf new file mode 100644 index 0000000000..ad2d8dcfd3 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/outputs.tf @@ -0,0 +1,311 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + _tpl_providers = "${path.module}/templates/providers.tf.tpl" + cicd_workflow_attrs = { + data_platform_dev = { + service_account = try(module.branch-dp-dev-sa-cicd.0.email, null) + tf_providers_file = "3-data-platform-dev-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + data_platform_prod = { + service_account = try(module.branch-dp-prod-sa-cicd.0.email, null) + tf_providers_file = "3-data-platform-prod-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + gke_dev = { + service_account = try(module.branch-gke-dev-sa-cicd.0.email, null) + tf_providers_file = "3-gke-dev-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + gke_prod = { + service_account = try(module.branch-gke-prod-sa-cicd.0.email, null) + tf_providers_file = "3-gke-prod-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + networking = { + service_account = try(module.branch-network-sa-cicd.0.email, null) + tf_providers_file = "2-networking-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_2 + } + project_factory_dev = { + service_account = try(module.branch-pf-dev-sa-cicd.0.email, null) + tf_providers_file = "3-project-factory-dev-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + project_factory_prod = { + service_account = try(module.branch-pf-prod-sa-cicd.0.email, null) + tf_providers_file = "3-project-factory-prod-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + security = { + service_account = try(module.branch-security-sa-cicd.0.email, null) + tf_providers_file = "2-security-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_2 + } + } + cicd_workflows = { + for k, v in local.cicd_repositories : k => templatefile( + "${path.module}/templates/workflow-${v.type}.yaml", + merge(local.cicd_workflow_attrs[k], { + identity_provider = try( + local.cicd_identity_providers[v.identity_provider].name, null + ) + outputs_bucket = var.automation.outputs_bucket + stage_name = k + }) + ) + } + folder_ids = merge( + { + data-platform-dev = try(module.branch-dp-dev-folder.0.id, null) + data-platform-prod = try(module.branch-dp-prod-folder.0.id, null) + gke-dev = try(module.branch-gke-dev-folder.0.id, null) + gke-prod = try(module.branch-gke-prod-folder.0.id, null) + networking = module.branch-network-folder.id + networking-dev = module.branch-network-dev-folder.id + networking-prod = module.branch-network-prod-folder.id + sandbox = try(module.branch-sandbox-folder.0.id, null) + security = module.branch-security-folder.id + teams = try(module.branch-teams-folder.0.id, null) + }, + { + for k, v in module.branch-teams-team-folder : + "team-${k}" => v.id + }, + { + for k, v in module.branch-teams-team-dev-folder : + "team-${k}-dev" => v.id + }, + { + for k, v in module.branch-teams-team-prod-folder : + "team-${k}-prod" => v.id + } + ) + providers = merge( + { + "2-0-networking" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-network-gcs.name + name = "networking" + sa = module.branch-network-sa.email + }) + "2-0-security" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-security-gcs.name + name = "security" + sa = module.branch-security-sa.email + }) + }, + !var.fast_features.data_platform ? {} : { + "3-0-data-platform-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-dp-dev-gcs.0.name + name = "dp-dev" + sa = module.branch-dp-dev-sa.0.email + }) + "3-0-data-platform-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-dp-prod-gcs.0.name + name = "dp-prod" + sa = module.branch-dp-prod-sa.0.email + }) + }, + !var.fast_features.gke ? {} : { + "3-0-gke-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-gke-dev-gcs.0.name + name = "gke-dev" + sa = module.branch-gke-dev-sa.0.email + }) + "3-0-gke-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-gke-prod-gcs.0.name + name = "gke-prod" + sa = module.branch-gke-prod-sa.0.email + }) + }, + !var.fast_features.project_factory ? {} : { + "3-0-project-factory-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-pf-dev-gcs.0.name + name = "team-dev" + sa = var.automation.service_accounts.pf-dev + }) + "3-0-project-factory-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-pf-prod-gcs.0.name + name = "team-prod" + sa = var.automation.service_accounts.pf-prod + }) + }, + !var.fast_features.sandbox ? {} : { + "9-0-sandbox" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-sandbox-gcs.0.name + name = "sandbox" + sa = var.automation.service_accounts.sandbox + }) + }, + !var.fast_features.teams ? {} : merge( + { + "3-teams" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-teams-gcs.0.name + name = "teams" + sa = module.branch-teams-sa.0.email + }) + }, + { + for k, v in module.branch-teams-team-sa : + "3-teams-${k}" => templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-teams-team-gcs[k].name + name = "teams" + sa = v.email + }) + } + ) + ) + tfvars = { + folder_ids = local.folder_ids + } +} + +output "cicd_repositories" { + description = "WIF configuration for CI/CD repositories." + value = { + for k, v in local.cicd_repositories : k => { + branch = v.branch + name = v.name + provider = try( + local.cicd_identity_providers[v.identity_provider].name, null + ) + service_account = local.cicd_workflow_attrs[k].service_account + } if v != null + } +} + +output "dataplatform" { + description = "Data for the Data Platform stage." + value = !var.fast_features.data_platform ? {} : { + dev = { + folder = module.branch-dp-dev-folder.0.id + gcs_bucket = module.branch-dp-dev-gcs.0.name + service_account = module.branch-dp-dev-sa.0.email + } + prod = { + folder = module.branch-dp-prod-folder.0.id + gcs_bucket = module.branch-dp-prod-gcs.0.name + service_account = module.branch-dp-prod-sa.0.email + } + } +} + +output "gke_multitenant" { + # tfdoc:output:consumers 03-gke-multitenant + description = "Data for the GKE multitenant stage." + value = ( + var.fast_features.gke + ? { + "dev" = { + folder = module.branch-gke-dev-folder.0.id + gcs_bucket = module.branch-gke-dev-gcs.0.name + service_account = module.branch-gke-dev-sa.0.email + } + "prod" = { + folder = module.branch-gke-prod-folder.0.id + gcs_bucket = module.branch-gke-prod-gcs.0.name + service_account = module.branch-gke-prod-sa.0.email + } + } + : {} + ) +} + +output "networking" { + description = "Data for the networking stage." + value = { + folder = module.branch-network-folder.id + gcs_bucket = module.branch-network-gcs.name + service_account = module.branch-network-sa.iam_email + } +} + +output "project_factories" { + description = "Data for the project factories stage." + value = !var.fast_features.project_factory ? {} : { + dev = { + bucket = module.branch-pf-dev-gcs.0.name + sa = var.automation.service_accounts.pf-dev + } + prod = { + bucket = module.branch-pf-prod-gcs.0.name + sa = var.automation.service_accounts.pf-prod + } + } +} + +# ready to use provider configurations for subsequent stages +output "providers" { + # tfdoc:output:consumers 02-networking 02-security 03-dataplatform xx-sandbox xx-teams + description = "Terraform provider files for this stage and dependent stages." + sensitive = true + value = local.providers +} + +output "sandbox" { + # tfdoc:output:consumers xx-sandbox + description = "Data for the sandbox stage." + value = ( + var.fast_features.sandbox + ? { + folder = module.branch-sandbox-folder.0.id + gcs_bucket = module.branch-sandbox-gcs.0.name + service_account = var.automation.service_accounts.sandbox + } + : null + ) +} + +output "security" { + # tfdoc:output:consumers 02-security + description = "Data for the networking stage." + value = { + folder = module.branch-security-folder.id + gcs_bucket = module.branch-security-gcs.name + service_account = module.branch-security-sa.iam_email + } +} + +output "teams" { + description = "Data for the teams stage." + value = { + for k, v in module.branch-teams-team-folder : k => { + folder = v.id + gcs_bucket = module.branch-teams-team-gcs[k].name + service_account = module.branch-teams-team-sa[k].email + } + } +} + +# ready to use variable values for subsequent stages +output "tfvars" { + description = "Terraform variable files for the following stages." + sensitive = true + value = local.tfvars +} diff --git a/fast/stages-multitenant/1-resman-tenant/root_node.tf b/fast/stages-multitenant/1-resman-tenant/root_node.tf new file mode 100644 index 0000000000..5b83d2dd22 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/root_node.tf @@ -0,0 +1,41 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Tenant root folder configuration. + +module "root-folder" { + source = "../../../modules/folder" + id = var.root_node + folder_create = var.test_skip_data_sources + # start test attributes + parent = ( + var.test_skip_data_sources ? "organizations/${var.organization.id}" : null + ) + name = var.test_skip_data_sources ? "Test" : null + # end test attributes + iam_additive = { + "roles/accesscontextmanager.policyAdmin" = [ + local.automation_sas_iam.security + ] + "roles/compute.orgFirewallPolicyAdmin" = [ + local.automation_sas_iam.networking + ] + "roles/compute.xpnAdmin" = [ + local.automation_sas_iam.networking + ] + } + org_policies_data_path = var.organization_policy_data_path +} diff --git a/fast/stages-multitenant/1-resman-tenant/templates/providers.tf.tpl b/fast/stages-multitenant/1-resman-tenant/templates/providers.tf.tpl new file mode 100644 index 0000000000..993c78ca41 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/templates/providers.tf.tpl @@ -0,0 +1,33 @@ +/** + * 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. + * 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. + */ + +terraform { + backend "gcs" { + bucket = "${bucket}" + impersonate_service_account = "${sa}" + %{~ if backend_extra != null ~} + ${indent(4, backend_extra)} + %{~ endif ~} + } +} +provider "google" { + impersonate_service_account = "${sa}" +} +provider "google-beta" { + impersonate_service_account = "${sa}" +} + +# end provider.tf for ${name} diff --git a/fast/stages-multitenant/1-resman-tenant/templates/workflow-github.yaml b/fast/stages-multitenant/1-resman-tenant/templates/workflow-github.yaml new file mode 100644 index 0000000000..8a946d84c8 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/templates/workflow-github.yaml @@ -0,0 +1,186 @@ +# 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. + +name: "FAST ${stage_name} stage" + +on: + pull_request: + branches: + - main + types: + - closed + - opened + - synchronize + +env: + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + TF_VERSION: 1.3.2 + +jobs: + fast-pr: + permissions: + contents: read + id-token: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@v3 + + # set up SSH key authentication to the modules repository + - id: ssh-config + name: Configure SSH authentication + run: | + ssh-agent -a "$SSH_AUTH_SOCK" > /dev/null + ssh-add - <<< "$${{ secrets.CICD_MODULES_KEY }}" + + # set up authentication via Workload identity Federation + - id: gcp-auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0 + with: + workload_identity_provider: $${{ env.FAST_WIF_PROVIDER }} + service_account: $${{ env.FAST_SERVICE_ACCOUNT }} + access_token_lifetime: 3600s + + - id: gcp-sdk + name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v0 + with: + install_components: alpha + + # copy provider and tfvars files + - id: tf-config + name: Copy Terraform output files + run: | + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/providers/$${{env.TF_PROVIDERS_FILE}}" ./ + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/tfvars" ./ + for f in $${{env.TF_VAR_FILES}}; do + ln -s "tfvars/$f" ./ + done + + - id: tf-setup + name: Set up Terraform + uses: hashicorp/setup-terraform@v2.0.3 + with: + terraform_version: $${{ env.TF_VERSION }} + + # run Terraform init/validate/plan + - id: tf-init + name: Terraform init + run: | + terraform init -no-color + + - id: tf-validate + name: Terraform validate + run: terraform validate -no-color + + - id: tf-plan + name: Terraform plan + continue-on-error: true + run: | + terraform plan -input=false -out ../plan.out -no-color + + - id: tf-apply + if: github.event.pull_request.merged == true && success() + name: Terraform apply + continue-on-error: true + run: | + terraform apply -input=false -auto-approve -no-color ../plan.out + + - id: pr-comment + name: Post comment to Pull Request + continue-on-error: true + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + env: + PLAN: $${{ steps.tf-plan.outputs.stdout }}\n$${{ steps.tf-plan.outputs.stderr }} + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + +
Validation Output + + \`\`\`\n + $${{ steps.tf-validate.outputs.stdout }} + \`\`\` + +
+ + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + +
Show Plan + + \`\`\`\n + $${process.env.PLAN.split('\n').filter(l => l.match(/^([A-Z\s].*|)$$/)).join('\n')} + \`\`\` + +
+ + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: pr-short-comment + name: Post comment to Pull Request + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' && steps.pr-comment.outcome != 'success' + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + + Plan output is in the action log. + + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: check-plan + name: Check plan failure + if: steps.tf-plan.outcome != 'success' + run: exit 1 + + - id: check-apply + name: Check apply failure + if: github.event.pull_request.merged == true && steps.tf-apply.outcome != 'success' + run: exit 1 diff --git a/fast/stages-multitenant/1-resman-tenant/templates/workflow-gitlab.yaml b/fast/stages-multitenant/1-resman-tenant/templates/workflow-gitlab.yaml new file mode 100644 index 0000000000..8981e70b3c --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/templates/workflow-gitlab.yaml @@ -0,0 +1,120 @@ +# 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. + +default: + before_script: + - echo "$${CI_JOB_JWT_V2}" > token.txt + image: + name: hashicorp/terraform + entrypoint: + - "/usr/bin/env" + - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +variables: + GOOGLE_CREDENTIALS: cicd-sa-credentials.json + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + +stages: + - gcp-auth + - tf-files + - tf-plan + - tf-apply + +cache: + key: gcp-auth + paths: + - cicd-sa-credentials.json + - .tf-setup + +gcp-auth: + image: + name: google/cloud-sdk:slim + stage: gcp-auth + script: + - | + gcloud iam workload-identity-pools create-cred-config \ + $${FAST_WIF_PROVIDER} \ + --service-account=$${FAST_SERVICE_ACCOUNT} \ + --service-account-token-lifetime-seconds=3600 \ + --output-file=$${GOOGLE_CREDENTIALS} \ + --credential-source-file=token.txt +tf-files: + dependencies: + - gcp-auth + image: + name: google/cloud-sdk:slim + stage: tf-files + script: + # - gcloud components install -q alpha + - gcloud config set auth/credential_file_override $${GOOGLE_CREDENTIALS} + - mkdir -p .tf-setup + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/providers/$${TF_PROVIDERS_FILE}" .tf-setup/ + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/tfvars" .tf-setup/ + +tf-plan: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-plan + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform plan + dependencies: + - tf-files + +tf-apply: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-apply + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform apply -input=false -auto-approve + dependencies: + - tf-files + when: manual + only: + variables: + - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH diff --git a/fast/stages-multitenant/1-resman-tenant/templates/workflow-sourcerepo.yaml b/fast/stages-multitenant/1-resman-tenant/templates/workflow-sourcerepo.yaml new file mode 100644 index 0000000000..446c9c9605 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/templates/workflow-sourcerepo.yaml @@ -0,0 +1,98 @@ +# 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. + +steps: + - name: alpine:3 + id: tf-download + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + mkdir -p /builder/home/.local/bin + wget https://releases.hashicorp.com/terraform/$${_TF_VERSION}/terraform_$${_TF_VERSION}_linux_amd64.zip + unzip terraform_$${_TF_VERSION}_linux_amd64.zip -d /builder/home/.local/bin + rm terraform_$${_TF_VERSION}_linux_amd64.zip + chmod 755 /builder/home/.local/bin/terraform + - name: alpine:3 + id: tf-check-format + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform fmt -recursive -check /workspace/ + - name: gcr.io/google.com/cloudsdktool/cloud-sdk:alpine + id: tf-files + entrypoint: bash + args: + - -eEuo + - pipefail + - -c + - |- + /google-cloud-sdk/bin/gsutil cp \ + gs://$${_FAST_OUTPUTS_BUCKET}/providers/$${_TF_PROVIDERS_FILE} ./ + /google-cloud-sdk/bin/gsutil cp -r \ + gs://$${_FAST_OUTPUTS_BUCKET}/tfvars ./ + for f in $${_TF_VAR_FILES}; do + ln -s tfvars/$f ./ + done + - name: alpine:3 + id: tf-init + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform init -no-color + - name: alpine:3 + id: tf-check-validate + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform validate -no-color + - name: alpine:3 + id: tf-plan + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform plan -no-color -input=false -out plan.out + # store artifact and ask for approval here if needed + - name: alpine:3 + id: tf-apply + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform apply -no-color -input=false -auto-approve plan.out +options: + env: + - PATH=/usr/local/bin:/usr/bin:/bin:/builder/home/.local/bin + logging: CLOUD_LOGGING_ONLY +substitutions: + _FAST_OUTPUTS_BUCKET: ${outputs_bucket} + _TF_PROVIDERS_FILE: ${tf_providers_file} + _TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + _TF_VERSION: 1.3.2 diff --git a/fast/stages-multitenant/1-resman-tenant/variables.tf b/fast/stages-multitenant/1-resman-tenant/variables.tf new file mode 100644 index 0000000000..0229dd78f2 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/variables.tf @@ -0,0 +1,281 @@ +/** + * 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. + * 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. + */ + +# defaults for variables marked with global tfdoc annotations, can be set via +# the tfvars file generated in stage 00 and stored in its outputs + +variable "automation" { + # tfdoc:variable:source 0-bootstrap + description = "Automation resources created by the bootstrap stage." + type = object({ + outputs_bucket = string + project_id = string + project_number = string + federated_identity_pools = list(string) + federated_identity_providers = map(object({ + issuer = string + issuer_uri = string + name = string + principal_tpl = string + principalset_tpl = string + })) + service_accounts = object({ + networking = string + resman = string + security = string + dp-dev = optional(string) + dp-prod = optional(string) + gke-dev = optional(string) + gke-prod = optional(string) + pf-dev = optional(string) + pf-prod = optional(string) + sandbox = optional(string) + teams = optional(string) + }) + }) +} + +variable "billing_account" { + # tfdoc:variable:source 0-bootstrap + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." + 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 "cicd_repositories" { + description = "CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed." + type = object({ + data_platform_dev = object({ + branch = string + identity_provider = string + name = string + type = string + }) + data_platform_prod = object({ + branch = string + identity_provider = string + name = string + type = string + }) + gke_dev = object({ + branch = string + identity_provider = string + name = string + type = string + }) + gke_prod = object({ + branch = string + identity_provider = string + name = string + type = string + }) + networking = object({ + branch = string + identity_provider = string + name = string + type = string + }) + project_factory_dev = object({ + branch = string + identity_provider = string + name = string + type = string + }) + project_factory_prod = object({ + branch = string + identity_provider = string + name = string + type = string + }) + security = object({ + branch = string + identity_provider = string + name = string + type = string + }) + }) + default = null + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || try(v.name, null) != null + ]) + error_message = "Non-null repositories need a non-null name." + } + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || ( + try(v.identity_provider, null) != null + || + try(v.type, null) == "sourcerepo" + ) + ]) + error_message = "Non-null repositories need a non-null provider unless type is 'sourcerepo'." + } + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || ( + contains(["github", "gitlab", "sourcerepo"], coalesce(try(v.type, null), "null")) + ) + ]) + error_message = "Invalid repository type, supported types: 'github' 'gitlab' or 'sourcerepo'." + } +} + +variable "custom_roles" { + # tfdoc:variable:source 0-bootstrap + description = "Custom roles defined at the org level, in key => id format." + type = object({ + service_project_network_admin = string + }) + default = null +} + +variable "data_dir" { + description = "Relative path for the folder storing configuration data." + type = string + default = "data" +} + +variable "fast_features" { + # tfdoc:variable:source 0-0-bootstrap + description = "Selective control for top-level FAST features." + type = object({ + data_platform = optional(bool, false) + gke = optional(bool, false) + project_factory = optional(bool, false) + sandbox = optional(bool, false) + teams = optional(bool, false) + }) + default = {} + nullable = false +} + +variable "groups" { + # tfdoc:variable:source 0-bootstrap + # https://cloud.google.com/docs/enterprise/setup-checklist + description = "Group names to grant organization-level permissions." + type = object({ + gcp-devops = optional(string) + gcp-network-admins = optional(string) + gcp-security-admins = optional(string) + }) + default = {} + nullable = false +} + +variable "locations" { + # tfdoc:variable:source 0-bootstrap + description = "Optional locations for GCS, BigQuery, and logging buckets created here." + type = object({ + bq = string + gcs = string + logging = string + pubsub = list(string) + }) + default = { + bq = "EU" + gcs = "EU" + logging = "global" + pubsub = [] + } + nullable = false +} + +variable "organization" { + # tfdoc:variable:source 0-bootstrap + description = "Organization details." + type = object({ + domain = string + id = number + customer_id = string + }) +} + +variable "organization_policy_data_path" { + description = "Path for the data folder used by the organization policies factory." + type = string + default = null +} + +variable "outputs_location" { + description = "Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable." + type = string + default = null +} + +variable "prefix" { + # tfdoc:variable:source 0-bootstrap + description = "Prefix used for resources that need unique names. Use 9 characters or less." + type = string + + validation { + condition = try(length(var.prefix), 0) < 10 + error_message = "Use a maximum of 9 characters for prefix." + } +} + +variable "root_node" { + description = "Root folder node for the tenant, in folders/nnnnnn format." + type = string +} + +variable "short_name" { + description = "Short name used to identify the tenant." + type = string +} + +variable "tags" { + description = "Resource management tags." + type = object({ + keys = object({ + context = string + environment = string + tenant = string + }) + names = object({ + context = string + environment = string + tenant = string + }) + values = map(string) + }) + nullable = false +} + +variable "team_folders" { + description = "Team folders to be created. Format is described in a code comment." + type = map(object({ + descriptive_name = string + group_iam = map(list(string)) + impersonation_groups = list(string) + })) + default = null +} + +variable "test_skip_data_sources" { + description = "Used when testing to bypass data sources." + type = bool + default = false +} diff --git a/fast/stages-multitenant/MULTITENANT-TODO.md b/fast/stages-multitenant/MULTITENANT-TODO.md new file mode 100644 index 0000000000..a14d64ab5b --- /dev/null +++ b/fast/stages-multitenant/MULTITENANT-TODO.md @@ -0,0 +1,8 @@ +# Things to add before sending the PR + +- [x] multitenant resman stage +- [ ] identity providers for tenant bootstrap stages +- [ ] edit FAST top-level and stages READMEs for multitenant +- [ ] multitenant stages READMEs +- [ ] add test to ensure templates folder is the same in every stage +- [ ] add test to ensure identity providers code is the same in bootstrap and mt bootstrap diff --git a/fast/stages-multitenant/README.md b/fast/stages-multitenant/README.md new file mode 100644 index 0000000000..27ce17dd6c --- /dev/null +++ b/fast/stages-multitenant/README.md @@ -0,0 +1,27 @@ +# FAST multitenant stages + +The stages in this folder set up separate resource hierarchies inside the same organization that are fully FAST-compliant, and allow each tenant to run and manage their own networking, security, or application-level stages. They are designed to be used where a high degree of autonomy is needed for each tenant, for example individual subsidiaries of a large corporation all sharing the same GCP organization. + +The multitenant stages have the following characteristics: + +- they support one tenant at a time, so one copy of both stages is needed for each tenant +- they have the organization-level bootstrap and resource management stages as prerequisite +- they are logically equivalent to the respective organization-level stages but behave slightly differently, as they actively minimize access and changes to organization or shared resources + +Once both tenant-level stages are run, a hierarchy and a set of resources is available for the new tenant, including a separate automation project, service accounts for subsequent stages, etc. + +The tenant-level stages require that organization-level stage 0 (bootstrap) and 1 (resource management) have been applied. Their position and role in the FAST stage flow is shown in the following diagram: + +

+ Stages diagram +

+ +## Tenant bootstrap (0) + +This stage creates the top-level root folder and tag for the tenant, and the tenant-level automation project and automation service accounts. It also sets up billing and organization-level roles for the tenant administrators group and the automation service accounts. As in the organizational-level stages, it can optionally set up CI/CD for itself and the tenant resource management stage. + +This stage is run with the organization-level resource management service account as it leverages its permissions, and is the bridge between the organization-level stages and the tenant stages which are effectively decoupled from the rest of the organization. + +## Tenant resource management (1) + +This stage populates the resource hierarchy rooted in the top-level tenant folder, assigns roles to the tenant automation service accounts, and optionally sets up CI/CD for the following stages. It is functionally equivalent to the organization-level resource management stage, but runs with a tenant-specific service account and has no control over resources outside of the tenant context. diff --git a/fast/stages-multitenant/diagram.png b/fast/stages-multitenant/diagram.png new file mode 100644 index 0000000000..940fa3c36e Binary files /dev/null and b/fast/stages-multitenant/diagram.png differ diff --git a/fast/stages-multitenant/stages.png b/fast/stages-multitenant/stages.png new file mode 100644 index 0000000000..dc2acc0a28 Binary files /dev/null and b/fast/stages-multitenant/stages.png differ diff --git a/fast/stages-multitenant/stages.svg b/fast/stages-multitenant/stages.svg new file mode 100644 index 0000000000..157453f0dc --- /dev/null +++ b/fast/stages-multitenant/stages.svg @@ -0,0 +1,1278 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages.png b/fast/stages.png index 238ad5a048..d6c3e386a5 100644 Binary files a/fast/stages.png and b/fast/stages.png differ diff --git a/fast/stages.svg b/fast/stages.svg index f952c88448..b6855676f7 100644 --- a/fast/stages.svg +++ b/fast/stages.svg @@ -1,1063 +1,929 @@ + inkscape:version="1.0.2 (e86c870879, 2021-01-15)"> + + + + image/svg+xml + + + + + id="defs273" /> + inkscape:window-maximized="1" + inkscape:current-layer="svg269" /> + id="g1d4f14c2bd7_0_215.0"> + id="path2" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages/00-bootstrap/IAM.md b/fast/stages/0-bootstrap/IAM.md similarity index 100% rename from fast/stages/00-bootstrap/IAM.md rename to fast/stages/0-bootstrap/IAM.md diff --git a/fast/stages/00-bootstrap/README.md b/fast/stages/0-bootstrap/README.md similarity index 88% rename from fast/stages/00-bootstrap/README.md rename to fast/stages/0-bootstrap/README.md index 889b0bcea8..9e6512cf7f 100644 --- a/fast/stages/00-bootstrap/README.md +++ b/fast/stages/0-bootstrap/README.md @@ -18,7 +18,7 @@ Use the following diagram as a simple high level reference for the following sec As mentioned above, this stage only does the bare minimum required to bootstrap automation, and ensure that base audit and billing exports are in place from the start to provide some measure of accountability, even before the security configurations are applied in a later stage. -It also sets up organization-level IAM bindings so the Organization Administrator role is only used here, trading off some design freedom for ease of auditing and troubleshooting, and reducing the risk of costly security mistakes down the line. The only exception to this rule is for the [Resource Management stage](../01-resman) service account, described below. +It also sets up organization-level IAM bindings so the Organization Administrator role is only used here, trading off some design freedom for ease of auditing and troubleshooting, and reducing the risk of costly security mistakes down the line. The only exception to this rule is for the [Resource Management stage](../1-resman) service account, described below. ### User groups @@ -28,7 +28,7 @@ We have standardized the initial set of groups on those outlined in the [GCP Ent ### Organization-level IAM -The service account used in the [Resource Management stage](../01-resman) needs to be able to grant specific permissions at the organizational level, to enable specific functionality for subsequent stages that deal with network or security resources, or billing-related activities. +The service account used in the [Resource Management stage](../1-resman) needs to be able to grant specific permissions at the organizational level, to enable specific functionality for subsequent stages that deal with network or security resources, or billing-related activities. In order to be able to assign those roles without having the full authority of the Organization Admin role, this stage defines a custom role that only allows setting IAM policies on the organization, and grants it via a [delegated role grant](https://cloud.google.com/iam/docs/setting-limits-on-granting-roles) that only allows it to be used to grant a limited subset of roles. @@ -89,7 +89,7 @@ This stage also implements initial support for two interrelated features Workload Identity Federation support allows configuring external providers independently from CI/CD, and offers predefined attributes for a few well known ones (more can be easily added by editing the `identity-providers.tf` file). Once providers have been configured their names are passed to the following stages via interface outputs, and can be leveraged to set up access or impersonation in IAM bindings. -CI/CD support is fully implemented for GitHub, Gitlab, and Cloud Source Repositories / Cloud Build. For GitHub, we also offer a [separate supporting setup](../../extras/00-cicd-github/) to quickly create / configure repositories. +CI/CD support is fully implemented for GitHub, Gitlab, and Cloud Source Repositories / Cloud Build. For GitHub, we also offer a [separate supporting setup](../../extras/0-cicd-github/) to quickly create / configure repositories. @@ -419,7 +419,7 @@ The `type` attribute can be set to one of the supported repository types: `githu Once the stage is applied the generated output files will contain pre-configured workflow files for each repository, that will use Workload Identity Federation via a dedicated service account for each repository to impersonate the automation service account for the stage. -You can use Terraform to automate creation of the repositories using the extra stage defined in [fast/extras/00-cicd-github](../../extras/00-cicd-github/) (only for Github for now). +You can use Terraform to automate creation of the repositories using the extra stage defined in [fast/extras/0-cicd-github](../../extras/0-cicd-github/) (only for Github for now). The remaining configuration is manual, as it regards the repositories themselves: @@ -453,7 +453,7 @@ The remaining configuration is manual, as it regards the repositories themselves | name | description | modules | resources | |---|---|---|---| | [automation.tf](./automation.tf) | Automation project and resources. | gcs · iam-service-account · project | | -| [billing.tf](./billing.tf) | Billing export project and dataset. | bigquery-dataset · organization · project | google_billing_account_iam_member · google_organization_iam_binding | +| [billing.tf](./billing.tf) | Billing export project and dataset. | bigquery-dataset · project | google_billing_account_iam_member | | [cicd.tf](./cicd.tf) | Workload Identity Federation configurations for CI/CD. | iam-service-account · source-repository | | | [identity-providers.tf](./identity-providers.tf) | Workload Identity Federation provider definitions. | | google_iam_workload_identity_pool · google_iam_workload_identity_pool_provider | | [log-export.tf](./log-export.tf) | Audit log project and sink. | bigquery-dataset · gcs · logging-bucket · project · pubsub | | @@ -468,35 +468,35 @@ The remaining configuration is manual, as it regards the repositories themselves | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [billing_account](variables.tf#L17) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | | -| [organization](variables.tf#L202) | Organization details. | object({…}) | ✓ | | | -| [prefix](variables.tf#L217) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | | -| [bootstrap_user](variables.tf#L25) | Email of the nominal user running this stage for the first time. | string | | null | | -| [cicd_repositories](variables.tf#L31) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | -| [custom_role_names](variables.tf#L83) | Names of custom roles defined at the org level. | object({…}) | | {…} | | -| [fast_features](variables.tf#L95) | Selective control for top-level FAST features. | object({…}) | | {…} | | -| [federated_identity_providers](variables.tf#L114) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | -| [groups](variables.tf#L128) | Group names to grant organization-level permissions. | map(string) | | {…} | | -| [iam](variables.tf#L146) | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | -| [iam_additive](variables.tf#L152) | Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string)) | | {} | | -| [locations](variables.tf#L158) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | | -| [log_sinks](variables.tf#L177) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | -| [outputs_location](variables.tf#L211) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [project_parent_ids](variables.tf#L227) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…}) | | {…} | | +| [billing_account](variables.tf#L17) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | | +| [organization](variables.tf#L194) | Organization details. | object({…}) | ✓ | | | +| [prefix](variables.tf#L209) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | | +| [bootstrap_user](variables.tf#L29) | Email of the nominal user running this stage for the first time. | string | | null | | +| [cicd_repositories](variables.tf#L35) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | +| [custom_role_names](variables.tf#L81) | Names of custom roles defined at the org level. | object({…}) | | {…} | | +| [fast_features](variables.tf#L93) | Selective control for top-level FAST features. | object({…}) | | {} | | +| [federated_identity_providers](variables.tf#L106) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | +| [groups](variables.tf#L120) | Group names to grant organization-level permissions. | map(string) | | {…} | | +| [iam](variables.tf#L138) | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | +| [iam_additive](variables.tf#L144) | Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string)) | | {} | | +| [locations](variables.tf#L150) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | | +| [log_sinks](variables.tf#L169) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | +| [outputs_location](variables.tf#L203) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [project_parent_ids](variables.tf#L219) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…}) | | {…} | | ## Outputs | name | description | sensitive | consumers | |---|---|:---:|---| -| [automation](outputs.tf#L89) | Automation resources. | | | -| [billing_dataset](outputs.tf#L94) | BigQuery dataset prepared for billing export. | | | -| [cicd_repositories](outputs.tf#L99) | CI/CD repository configurations. | | | -| [custom_roles](outputs.tf#L111) | Organization-level custom roles. | | | -| [federated_identity](outputs.tf#L116) | Workload Identity Federation pool and providers. | | | -| [outputs_bucket](outputs.tf#L126) | GCS bucket where generated output files are stored. | | | -| [project_ids](outputs.tf#L131) | Projects created by this stage. | | | -| [providers](outputs.tf#L141) | Terraform provider files for this stage and dependent stages. | ✓ | stage-01 | -| [service_accounts](outputs.tf#L148) | Automation service accounts created by this stage. | | | -| [tfvars](outputs.tf#L158) | Terraform variable files for the following stages. | ✓ | | +| [automation](outputs.tf#L86) | Automation resources. | | | +| [billing_dataset](outputs.tf#L91) | BigQuery dataset prepared for billing export. | | | +| [cicd_repositories](outputs.tf#L96) | CI/CD repository configurations. | | | +| [custom_roles](outputs.tf#L108) | Organization-level custom roles. | | | +| [federated_identity](outputs.tf#L113) | Workload Identity Federation pool and providers. | | | +| [outputs_bucket](outputs.tf#L123) | GCS bucket where generated output files are stored. | | | +| [project_ids](outputs.tf#L128) | Projects created by this stage. | | | +| [providers](outputs.tf#L138) | Terraform provider files for this stage and dependent stages. | ✓ | stage-01 | +| [service_accounts](outputs.tf#L145) | Automation service accounts created by this stage. | | | +| [tfvars](outputs.tf#L154) | Terraform variable files for the following stages. | ✓ | | diff --git a/fast/stages/00-bootstrap/automation.tf b/fast/stages/0-bootstrap/automation.tf similarity index 82% rename from fast/stages/00-bootstrap/automation.tf rename to fast/stages/0-bootstrap/automation.tf index 1475c811c9..90c14a81d3 100644 --- a/fast/stages/00-bootstrap/automation.tf +++ b/fast/stages/0-bootstrap/automation.tf @@ -127,39 +127,6 @@ module "automation-tf-bootstrap-sa" { } } -# cicd stage's bucket and service account - -module "automation-tf-cicd-gcs" { - source = "../../../modules/gcs" - project_id = module.automation-project.project_id - name = "iac-core-cicd-0" - prefix = local.prefix - location = var.locations.gcs - storage_class = local.gcs_storage_class - versioning = true - iam = { - "roles/storage.objectAdmin" = [module.automation-tf-cicd-provisioning-sa.iam_email] - } - depends_on = [module.organization] -} - -module "automation-tf-cicd-provisioning-sa" { - source = "../../../modules/iam-service-account" - project_id = module.automation-project.project_id - name = "cicd-0" - display_name = "Terraform stage 1 CICD service account." - prefix = local.prefix - # allow SA used by CI/CD workflow to impersonate this SA - iam = { - "roles/iam.serviceAccountTokenCreator" = compact([ - try(module.automation-tf-cicd-sa["cicd"].iam_email, null) - ]) - } - iam_storage_roles = { - (module.automation-tf-output-gcs.name) = ["roles/storage.admin"] - } -} - # resource hierarchy stage's bucket and service account module "automation-tf-resman-gcs" { @@ -183,7 +150,8 @@ module "automation-tf-resman-sa" { display_name = "Terraform stage 1 resman service account." prefix = local.prefix # allow SA used by CI/CD workflow to impersonate this SA - iam = { + # we use additive IAM to allow tenant CI/CD SAs to impersonate it + iam_additive = { "roles/iam.serviceAccountTokenCreator" = compact([ try(module.automation-tf-cicd-sa["resman"].iam_email, null) ]) diff --git a/fast/stages/00-bootstrap/billing.tf b/fast/stages/0-bootstrap/billing.tf similarity index 60% rename from fast/stages/00-bootstrap/billing.tf rename to fast/stages/0-bootstrap/billing.tf index df10e8f085..aee033bd8a 100644 --- a/fast/stages/00-bootstrap/billing.tf +++ b/fast/stages/0-bootstrap/billing.tf @@ -30,7 +30,7 @@ locals { module "billing-export-project" { source = "../../../modules/project" - count = local.billing_org ? 1 : 0 + count = var.billing_account.is_org_level ? 1 : 0 billing_account = var.billing_account.id name = "billing-exp-0" parent = coalesce( @@ -52,56 +52,18 @@ module "billing-export-project" { module "billing-export-dataset" { source = "../../../modules/bigquery-dataset" - count = local.billing_org ? 1 : 0 + count = var.billing_account.is_org_level ? 1 : 0 project_id = module.billing-export-project.0.project_id id = "billing_export" friendly_name = "Billing export." location = var.locations.bq } -# billing account in a different org - -module "billing-organization-ext" { - source = "../../../modules/organization" - count = local.billing_org_ext ? 1 : 0 - organization_id = "organizations/${var.billing_account.organization_id}" - iam_additive = { - "roles/billing.admin" = local.billing_ext_admins - } -} - - -resource "google_organization_iam_binding" "billing_org_ext_admin_delegated" { - # refer to organization.tf for the explanation of how this binding works - count = local.billing_org_ext ? 1 : 0 - org_id = var.billing_account.organization_id - # if the billing org does not have our custom role, user the predefined one - # role = "roles/resourcemanager.organizationAdmin" - role = join("", [ - "organizations/${var.billing_account.organization_id}/", - "roles/${var.custom_role_names.organization_iam_admin}" - ]) - members = [module.automation-tf-resman-sa.iam_email] - condition { - title = "automation_sa_delegated_grants" - description = "Automation service account delegated grants." - expression = format( - "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", - join(",", formatlist("'%s'", [ - "roles/billing.costsManager", - "roles/billing.user", - ] - )) - ) - } - depends_on = [module.billing-organization-ext] -} - # standalone billing account resource "google_billing_account_iam_member" "billing_ext_admin" { for_each = toset( - local.billing_ext ? local.billing_ext_admins : [] + !var.billing_account.is_org_level ? local.billing_ext_admins : [] ) billing_account_id = var.billing_account.id role = "roles/billing.admin" @@ -110,7 +72,7 @@ resource "google_billing_account_iam_member" "billing_ext_admin" { resource "google_billing_account_iam_member" "billing_ext_cost_manager" { for_each = toset( - local.billing_ext ? local.billing_ext_admins : [] + !var.billing_account.is_org_level ? local.billing_ext_admins : [] ) billing_account_id = var.billing_account.id role = "roles/billing.costsManager" diff --git a/fast/stages/00-bootstrap/cicd.tf b/fast/stages/0-bootstrap/cicd.tf similarity index 86% rename from fast/stages/00-bootstrap/cicd.tf rename to fast/stages/0-bootstrap/cicd.tf index 7cdae41c98..2b2a3df95f 100644 --- a/fast/stages/00-bootstrap/cicd.tf +++ b/fast/stages/0-bootstrap/cicd.tf @@ -17,6 +17,16 @@ # tfdoc:file:description Workload Identity Federation configurations for CI/CD. locals { + cicd_providers = { + for k, v in google_iam_workload_identity_pool_provider.default : + k => { + issuer = local.identity_providers[k].issuer + issuer_uri = local.identity_providers[k].issuer_uri + name = v.name + principal_tpl = local.identity_providers[k].principal_tpl + principalset_tpl = local.identity_providers[k].principalset_tpl + } + } cicd_repositories = { for k, v in coalesce(var.cicd_repositories, {}) : k => v if( @@ -32,18 +42,13 @@ locals { ) } cicd_workflow_providers = { - bootstrap = "00-bootstrap-providers.tf" - cicd = "00-cicd-providers.tf" - resman = "01-resman-providers.tf" + bootstrap = "0-bootstrap-providers.tf" + resman = "1-resman-providers.tf" } cicd_workflow_var_files = { bootstrap = [] - cicd = [ - "00-bootstrap.auto.tfvars.json", - "globals.auto.tfvars.json" - ] resman = [ - "00-bootstrap.auto.tfvars.json", + "0-bootstrap.auto.tfvars.json", "globals.auto.tfvars.json" ] } @@ -69,7 +74,7 @@ module "automation-tf-cicd-repo" { ] } triggers = { - "fast-00-${each.key}" = { + "fast-0-${each.key}" = { filename = ".cloudbuild/workflow.yaml" included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] service_account = module.automation-tf-cicd-sa[each.key].id diff --git a/fast/stages/00-bootstrap/diagram.png b/fast/stages/0-bootstrap/diagram.png similarity index 100% rename from fast/stages/00-bootstrap/diagram.png rename to fast/stages/0-bootstrap/diagram.png diff --git a/fast/stages/00-bootstrap/diagram.svg b/fast/stages/0-bootstrap/diagram.svg similarity index 100% rename from fast/stages/00-bootstrap/diagram.svg rename to fast/stages/0-bootstrap/diagram.svg diff --git a/fast/stages/00-bootstrap/groups.gif b/fast/stages/0-bootstrap/groups.gif similarity index 100% rename from fast/stages/00-bootstrap/groups.gif rename to fast/stages/0-bootstrap/groups.gif diff --git a/fast/stages/00-bootstrap/identity-providers.tf b/fast/stages/0-bootstrap/identity-providers.tf similarity index 100% rename from fast/stages/00-bootstrap/identity-providers.tf rename to fast/stages/0-bootstrap/identity-providers.tf diff --git a/fast/stages/00-bootstrap/log-export.tf b/fast/stages/0-bootstrap/log-export.tf similarity index 82% rename from fast/stages/00-bootstrap/log-export.tf rename to fast/stages/0-bootstrap/log-export.tf index 1c9f5a87a3..b9b5da42fa 100644 --- a/fast/stages/00-bootstrap/log-export.tf +++ b/fast/stages/0-bootstrap/log-export.tf @@ -17,6 +17,16 @@ # tfdoc:file:description Audit log project and sink. locals { + log_sink_destinations = merge( + # use the same dataset for all sinks with `bigquery` as destination + { for k, v in var.log_sinks : k => module.log-export-dataset.0 if v.type == "bigquery" }, + # use the same gcs bucket for all sinks with `storage` as destination + { for k, v in var.log_sinks : k => module.log-export-gcs.0 if v.type == "storage" }, + # use separate pubsub topics and logging buckets for sinks with + # destination `pubsub` and `logging` + module.log-export-pubsub, + module.log-export-logbucket + ) log_types = toset([for k, v in var.log_sinks : v.type]) } diff --git a/fast/stages/00-bootstrap/main.tf b/fast/stages/0-bootstrap/main.tf similarity index 78% rename from fast/stages/00-bootstrap/main.tf rename to fast/stages/0-bootstrap/main.tf index 839019f3cf..dba2ed089c 100644 --- a/fast/stages/00-bootstrap/main.tf +++ b/fast/stages/0-bootstrap/main.tf @@ -28,10 +28,6 @@ locals { for k, v in local.groups : k => "group:${v}" } - # convenience flags that express where billing account resides - billing_ext = var.billing_account.organization_id == null - billing_org = var.billing_account.organization_id == var.organization.id - billing_org_ext = !local.billing_ext && !local.billing_org # naming: environment used in most resource names prefix = join("-", compact([var.prefix, "prod"])) } diff --git a/fast/stages/00-bootstrap/organization.tf b/fast/stages/0-bootstrap/organization.tf similarity index 89% rename from fast/stages/00-bootstrap/organization.tf rename to fast/stages/0-bootstrap/organization.tf index 33b87820d3..36b2ac344c 100644 --- a/fast/stages/00-bootstrap/organization.tf +++ b/fast/stages/0-bootstrap/organization.tf @@ -24,7 +24,10 @@ locals { "domain:${var.organization.domain}" ] "roles/logging.admin" = concat( - [module.automation-tf-bootstrap-sa.iam_email], + [ + module.automation-tf-bootstrap-sa.iam_email, + module.automation-tf-resman-sa.iam_email + ], local._iam_bootstrap_user ) "roles/owner" = local._iam_bootstrap_user @@ -35,12 +38,11 @@ locals { [module.automation-tf-bootstrap-sa.iam_email], local._iam_bootstrap_user ) - # the following is useful if roles/browser is not desirable - # "roles/resourcemanager.organizationViewer" = [ - # "domain:${var.organization.domain}" - # ] "roles/resourcemanager.projectCreator" = concat( - [module.automation-tf-bootstrap-sa.iam_email], + [ + module.automation-tf-bootstrap-sa.iam_email, + module.automation-tf-resman-sa.iam_email + ], local._iam_bootstrap_user ) "roles/resourcemanager.projectMover" = [ @@ -78,8 +80,12 @@ locals { local.groups_iam.gcp-security-admins, module.automation-tf-resman-sa.iam_email ] + # the following is useful if roles/browser is not desirable + # "roles/resourcemanager.organizationViewer" = [ + # "domain:${var.organization.domain}" + # ] }, - local.billing_org ? { + var.billing_account.is_org_level ? { "roles/billing.admin" = [ local.groups_iam.gcp-billing-admins, local.groups_iam.gcp-organization-admins, @@ -115,16 +121,6 @@ locals { iam_roles_additive = distinct(concat( keys(local._iam_additive), keys(var.iam_additive) )) - log_sink_destinations = merge( - # use the same dataset for all sinks with `bigquery` as destination - { for k, v in var.log_sinks : k => module.log-export-dataset.0 if v.type == "bigquery" }, - # use the same gcs bucket for all sinks with `storage` as destination - { for k, v in var.log_sinks : k => module.log-export-gcs.0 if v.type == "storage" }, - # use separate pubsub topics and logging buckets for sinks with - # destination `pubsub` and `logging` - module.log-export-pubsub, - module.log-export-logbucket - ) } module "organization" { @@ -220,8 +216,9 @@ resource "google_organization_iam_binding" "org_admin_delegated" { "roles/compute.orgFirewallPolicyAdmin", "roles/compute.xpnAdmin", "roles/orgpolicy.policyAdmin", + "roles/resourcemanager.organizationViewer", ], - local.billing_org ? [ + var.billing_account.is_org_level ? [ "roles/billing.admin", "roles/billing.costsManager", "roles/billing.user", diff --git a/fast/stages/00-bootstrap/outputs-files.tf b/fast/stages/0-bootstrap/outputs-files.tf similarity index 97% rename from fast/stages/00-bootstrap/outputs-files.tf rename to fast/stages/0-bootstrap/outputs-files.tf index ded88cd56d..90e195b53f 100644 --- a/fast/stages/00-bootstrap/outputs-files.tf +++ b/fast/stages/0-bootstrap/outputs-files.tf @@ -26,7 +26,7 @@ resource "local_file" "providers" { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/00-bootstrap.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/0-bootstrap.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/00-bootstrap/outputs-gcs.tf b/fast/stages/0-bootstrap/outputs-gcs.tf similarity index 96% rename from fast/stages/00-bootstrap/outputs-gcs.tf rename to fast/stages/0-bootstrap/outputs-gcs.tf index 2c281d4ccb..0aded986a9 100644 --- a/fast/stages/00-bootstrap/outputs-gcs.tf +++ b/fast/stages/0-bootstrap/outputs-gcs.tf @@ -26,7 +26,7 @@ resource "google_storage_bucket_object" "providers" { resource "google_storage_bucket_object" "tfvars" { bucket = module.automation-tf-output-gcs.name - name = "tfvars/00-bootstrap.auto.tfvars.json" + name = "tfvars/0-bootstrap.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/00-bootstrap/outputs.tf b/fast/stages/0-bootstrap/outputs.tf similarity index 77% rename from fast/stages/00-bootstrap/outputs.tf rename to fast/stages/0-bootstrap/outputs.tf index 73dd64f4e9..364abd6840 100644 --- a/fast/stages/00-bootstrap/outputs.tf +++ b/fast/stages/0-bootstrap/outputs.tf @@ -21,7 +21,7 @@ locals { for k, v in local.cicd_repositories : k => templatefile( "${path.module}/templates/workflow-${v.type}.yaml", { identity_provider = try( - local.wif_providers[v["identity_provider"]].name, "" + local.cicd_providers[v["identity_provider"]].name, "" ) outputs_bucket = module.automation-tf-output-gcs.name service_account = try( @@ -38,19 +38,26 @@ locals { k => try(module.organization.custom_role_id[v], null) } providers = { - "00-bootstrap" = templatefile(local._tpl_providers, { - bucket = module.automation-tf-bootstrap-gcs.name - name = "bootstrap" - sa = module.automation-tf-bootstrap-sa.email + "0-bootstrap" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.automation-tf-bootstrap-gcs.name + name = "bootstrap" + sa = module.automation-tf-bootstrap-sa.email }) - "00-cicd" = templatefile(local._tpl_providers, { - bucket = module.automation-tf-cicd-gcs.name - name = "cicd" - sa = module.automation-tf-cicd-provisioning-sa.email + "1-resman" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.automation-tf-resman-gcs.name + name = "resman" + sa = module.automation-tf-resman-sa.email }) - "01-resman" = templatefile(local._tpl_providers, { + "0-bootstrap-tenant" = templatefile(local._tpl_providers, { + backend_extra = join("\n", [ + "# remove the newline between quotes and set the tenant name as prefix", + "prefix = \"", + "\"" + ]) bucket = module.automation-tf-resman-gcs.name - name = "resman" + name = "bootstrap-tenant" sa = module.automation-tf-resman-sa.email }) } @@ -59,7 +66,7 @@ locals { federated_identity_pool = try( google_iam_workload_identity_pool.default.0.name, null ) - federated_identity_providers = local.wif_providers + federated_identity_providers = local.cicd_providers outputs_bucket = module.automation-tf-output-gcs.name project_id = module.automation-project.project_id project_number = module.automation-project.number @@ -74,16 +81,6 @@ locals { organization = var.organization prefix = var.prefix } - wif_providers = { - for k, v in google_iam_workload_identity_pool_provider.default : - k => { - issuer = local.identity_providers[k].issuer - issuer_uri = local.identity_providers[k].issuer_uri - name = v.name - principal_tpl = local.identity_providers[k].principal_tpl - principalset_tpl = local.identity_providers[k].principalset_tpl - } - } } output "automation" { @@ -102,7 +99,7 @@ output "cicd_repositories" { for k, v in local.cicd_repositories : k => { branch = v.branch name = v.name - provider = try(local.wif_providers[v.identity_provider].name, null) + provider = try(local.cicd_providers[v.identity_provider].name, null) service_account = try(module.automation-tf-cicd-sa[k].email, null) } } @@ -119,7 +116,7 @@ output "federated_identity" { pool = try( google_iam_workload_identity_pool.default.0.name, null ) - providers = local.wif_providers + providers = local.cicd_providers } } @@ -149,7 +146,6 @@ output "service_accounts" { description = "Automation service accounts created by this stage." value = { bootstrap = module.automation-tf-bootstrap-sa.email - cicd = module.automation-tf-cicd-provisioning-sa.email resman = module.automation-tf-resman-sa.email } } diff --git a/fast/stages/0-bootstrap/templates/providers.tf.tpl b/fast/stages/0-bootstrap/templates/providers.tf.tpl new file mode 100644 index 0000000000..d1c224c5c1 --- /dev/null +++ b/fast/stages/0-bootstrap/templates/providers.tf.tpl @@ -0,0 +1,33 @@ +/** + * 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. + */ + +terraform { + backend "gcs" { + bucket = "${bucket}" + impersonate_service_account = "${sa}" + %{~ if backend_extra != null ~} + ${indent(4, backend_extra)} + %{~ endif ~} + } +} +provider "google" { + impersonate_service_account = "${sa}" +} +provider "google-beta" { + impersonate_service_account = "${sa}" +} + +# end provider.tf for ${name} diff --git a/fast/stages/0-bootstrap/templates/workflow-github.yaml b/fast/stages/0-bootstrap/templates/workflow-github.yaml new file mode 100644 index 0000000000..8a946d84c8 --- /dev/null +++ b/fast/stages/0-bootstrap/templates/workflow-github.yaml @@ -0,0 +1,186 @@ +# 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. + +name: "FAST ${stage_name} stage" + +on: + pull_request: + branches: + - main + types: + - closed + - opened + - synchronize + +env: + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + TF_VERSION: 1.3.2 + +jobs: + fast-pr: + permissions: + contents: read + id-token: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@v3 + + # set up SSH key authentication to the modules repository + - id: ssh-config + name: Configure SSH authentication + run: | + ssh-agent -a "$SSH_AUTH_SOCK" > /dev/null + ssh-add - <<< "$${{ secrets.CICD_MODULES_KEY }}" + + # set up authentication via Workload identity Federation + - id: gcp-auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0 + with: + workload_identity_provider: $${{ env.FAST_WIF_PROVIDER }} + service_account: $${{ env.FAST_SERVICE_ACCOUNT }} + access_token_lifetime: 3600s + + - id: gcp-sdk + name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v0 + with: + install_components: alpha + + # copy provider and tfvars files + - id: tf-config + name: Copy Terraform output files + run: | + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/providers/$${{env.TF_PROVIDERS_FILE}}" ./ + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/tfvars" ./ + for f in $${{env.TF_VAR_FILES}}; do + ln -s "tfvars/$f" ./ + done + + - id: tf-setup + name: Set up Terraform + uses: hashicorp/setup-terraform@v2.0.3 + with: + terraform_version: $${{ env.TF_VERSION }} + + # run Terraform init/validate/plan + - id: tf-init + name: Terraform init + run: | + terraform init -no-color + + - id: tf-validate + name: Terraform validate + run: terraform validate -no-color + + - id: tf-plan + name: Terraform plan + continue-on-error: true + run: | + terraform plan -input=false -out ../plan.out -no-color + + - id: tf-apply + if: github.event.pull_request.merged == true && success() + name: Terraform apply + continue-on-error: true + run: | + terraform apply -input=false -auto-approve -no-color ../plan.out + + - id: pr-comment + name: Post comment to Pull Request + continue-on-error: true + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + env: + PLAN: $${{ steps.tf-plan.outputs.stdout }}\n$${{ steps.tf-plan.outputs.stderr }} + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + +
Validation Output + + \`\`\`\n + $${{ steps.tf-validate.outputs.stdout }} + \`\`\` + +
+ + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + +
Show Plan + + \`\`\`\n + $${process.env.PLAN.split('\n').filter(l => l.match(/^([A-Z\s].*|)$$/)).join('\n')} + \`\`\` + +
+ + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: pr-short-comment + name: Post comment to Pull Request + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' && steps.pr-comment.outcome != 'success' + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + + Plan output is in the action log. + + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: check-plan + name: Check plan failure + if: steps.tf-plan.outcome != 'success' + run: exit 1 + + - id: check-apply + name: Check apply failure + if: github.event.pull_request.merged == true && steps.tf-apply.outcome != 'success' + run: exit 1 diff --git a/fast/stages/0-bootstrap/templates/workflow-gitlab.yaml b/fast/stages/0-bootstrap/templates/workflow-gitlab.yaml new file mode 100644 index 0000000000..8981e70b3c --- /dev/null +++ b/fast/stages/0-bootstrap/templates/workflow-gitlab.yaml @@ -0,0 +1,120 @@ +# 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. + +default: + before_script: + - echo "$${CI_JOB_JWT_V2}" > token.txt + image: + name: hashicorp/terraform + entrypoint: + - "/usr/bin/env" + - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +variables: + GOOGLE_CREDENTIALS: cicd-sa-credentials.json + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + +stages: + - gcp-auth + - tf-files + - tf-plan + - tf-apply + +cache: + key: gcp-auth + paths: + - cicd-sa-credentials.json + - .tf-setup + +gcp-auth: + image: + name: google/cloud-sdk:slim + stage: gcp-auth + script: + - | + gcloud iam workload-identity-pools create-cred-config \ + $${FAST_WIF_PROVIDER} \ + --service-account=$${FAST_SERVICE_ACCOUNT} \ + --service-account-token-lifetime-seconds=3600 \ + --output-file=$${GOOGLE_CREDENTIALS} \ + --credential-source-file=token.txt +tf-files: + dependencies: + - gcp-auth + image: + name: google/cloud-sdk:slim + stage: tf-files + script: + # - gcloud components install -q alpha + - gcloud config set auth/credential_file_override $${GOOGLE_CREDENTIALS} + - mkdir -p .tf-setup + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/providers/$${TF_PROVIDERS_FILE}" .tf-setup/ + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/tfvars" .tf-setup/ + +tf-plan: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-plan + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform plan + dependencies: + - tf-files + +tf-apply: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-apply + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform apply -input=false -auto-approve + dependencies: + - tf-files + when: manual + only: + variables: + - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH diff --git a/fast/stages/0-bootstrap/templates/workflow-sourcerepo.yaml b/fast/stages/0-bootstrap/templates/workflow-sourcerepo.yaml new file mode 100644 index 0000000000..446c9c9605 --- /dev/null +++ b/fast/stages/0-bootstrap/templates/workflow-sourcerepo.yaml @@ -0,0 +1,98 @@ +# 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. + +steps: + - name: alpine:3 + id: tf-download + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + mkdir -p /builder/home/.local/bin + wget https://releases.hashicorp.com/terraform/$${_TF_VERSION}/terraform_$${_TF_VERSION}_linux_amd64.zip + unzip terraform_$${_TF_VERSION}_linux_amd64.zip -d /builder/home/.local/bin + rm terraform_$${_TF_VERSION}_linux_amd64.zip + chmod 755 /builder/home/.local/bin/terraform + - name: alpine:3 + id: tf-check-format + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform fmt -recursive -check /workspace/ + - name: gcr.io/google.com/cloudsdktool/cloud-sdk:alpine + id: tf-files + entrypoint: bash + args: + - -eEuo + - pipefail + - -c + - |- + /google-cloud-sdk/bin/gsutil cp \ + gs://$${_FAST_OUTPUTS_BUCKET}/providers/$${_TF_PROVIDERS_FILE} ./ + /google-cloud-sdk/bin/gsutil cp -r \ + gs://$${_FAST_OUTPUTS_BUCKET}/tfvars ./ + for f in $${_TF_VAR_FILES}; do + ln -s tfvars/$f ./ + done + - name: alpine:3 + id: tf-init + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform init -no-color + - name: alpine:3 + id: tf-check-validate + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform validate -no-color + - name: alpine:3 + id: tf-plan + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform plan -no-color -input=false -out plan.out + # store artifact and ask for approval here if needed + - name: alpine:3 + id: tf-apply + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform apply -no-color -input=false -auto-approve plan.out +options: + env: + - PATH=/usr/local/bin:/usr/bin:/bin:/builder/home/.local/bin + logging: CLOUD_LOGGING_ONLY +substitutions: + _FAST_OUTPUTS_BUCKET: ${outputs_bucket} + _TF_PROVIDERS_FILE: ${tf_providers_file} + _TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + _TF_VERSION: 1.3.2 diff --git a/fast/stages/00-bootstrap/terraform.tfvars.sample b/fast/stages/0-bootstrap/terraform.tfvars.sample similarity index 100% rename from fast/stages/00-bootstrap/terraform.tfvars.sample rename to fast/stages/0-bootstrap/terraform.tfvars.sample diff --git a/fast/stages/00-bootstrap/variables.tf b/fast/stages/0-bootstrap/variables.tf similarity index 90% rename from fast/stages/00-bootstrap/variables.tf rename to fast/stages/0-bootstrap/variables.tf index 0b9f37c20c..c4b236695f 100644 --- a/fast/stages/00-bootstrap/variables.tf +++ b/fast/stages/0-bootstrap/variables.tf @@ -15,11 +15,15 @@ */ variable "billing_account" { - description = "Billing account id and organization id ('nnnnnnnn' or null)." + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." type = object({ - id = string - organization_id = number + 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 "bootstrap_user" { @@ -31,24 +35,18 @@ variable "bootstrap_user" { variable "cicd_repositories" { description = "CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed." type = object({ - bootstrap = object({ - branch = string - identity_provider = string - name = string - type = string - }) - cicd = object({ + bootstrap = optional(object({ branch = string identity_provider = string name = string type = string - }) - resman = object({ + })) + resman = optional(object({ branch = string identity_provider = string name = string type = string - }) + })) }) default = null validation { @@ -95,19 +93,13 @@ variable "custom_role_names" { variable "fast_features" { description = "Selective control for top-level FAST features." type = object({ - data_platform = bool - gke = bool - project_factory = bool - sandbox = bool - teams = bool + data_platform = optional(bool, false) + gke = optional(bool, false) + project_factory = optional(bool, false) + sandbox = optional(bool, false) + teams = optional(bool, false) }) - default = { - data_platform = true - gke = true - project_factory = true - sandbox = true - teams = true - } + default = {} nullable = false } @@ -183,11 +175,11 @@ variable "log_sinks" { default = { audit-logs = { filter = "logName:\"/logs/cloudaudit.googleapis.com%2Factivity\" OR logName:\"/logs/cloudaudit.googleapis.com%2Fsystem_event\"" - type = "bigquery" + type = "logging" } vpc-sc = { filter = "protoPayload.metadata.@type=\"type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata\"" - type = "bigquery" + type = "logging" } } validation { diff --git a/fast/stages/00-bootstrap/templates b/fast/stages/00-bootstrap/templates deleted file mode 120000 index bcb6967bec..0000000000 --- a/fast/stages/00-bootstrap/templates +++ /dev/null @@ -1 +0,0 @@ -../../assets/templates \ No newline at end of file diff --git a/fast/stages/01-resman/templates b/fast/stages/01-resman/templates deleted file mode 120000 index bcb6967bec..0000000000 --- a/fast/stages/01-resman/templates +++ /dev/null @@ -1 +0,0 @@ -../../assets/templates \ No newline at end of file diff --git a/fast/stages/01-resman/IAM.md b/fast/stages/1-resman/IAM.md similarity index 100% rename from fast/stages/01-resman/IAM.md rename to fast/stages/1-resman/IAM.md diff --git a/fast/stages/01-resman/README.md b/fast/stages/1-resman/README.md similarity index 68% rename from fast/stages/01-resman/README.md rename to fast/stages/1-resman/README.md index d36563523d..c2091eb50a 100644 --- a/fast/stages/01-resman/README.md +++ b/fast/stages/1-resman/README.md @@ -34,17 +34,21 @@ Additionally, a few critical benefits are directly provided by this design: - grouping application resources and services using teams or business logic is a flexible approach, which maps well to typical operational or budget requirements - automation stages (e.g. Networking) can be segregated in a simple and effective way, by creating the required service accounts and buckets for each stage here, and applying a handful of IAM roles to the relevant folder -For a discussion on naming, please refer to the [Bootstrap stage documentation](../00-bootstrap/README.md#naming), as the same approach is shared by all stages. +For a discussion on naming, please refer to the [Bootstrap stage documentation](../0-bootstrap/README.md#naming), as the same approach is shared by all stages. + +### Multitenancy + +Fully multitenant hierarchies inside the same organization are implemented via [separate additional stages](../../stages-multitenant/) that need to be run once for each tenant, and require this stage as a prerequisite. ### Workload Identity Federation and CI/CD This stage also implements optional support for CI/CD, much in the same way as the bootstrap stage. The only difference is on Workload Identity Federation, which is only configured in bootstrap and made available here via stage interface variables (the automatically generated `.tfvars` files). -For details on how to configure CI/CD please refer to the [relevant section in the bootstrap stage documentation](../00-bootstrap/README.md#cicd-repositories). +For details on how to configure CI/CD please refer to the [relevant section in the bootstrap stage documentation](../0-bootstrap/README.md#cicd-repositories). ## How to run this stage -This stage is meant to be executed after the [bootstrap](../00-bootstrap) stage has run, as it leverages the automation service account and bucket created there. The relevant user groups must also exist, but that's one of the requirements for the previous stage too, so if you ran that successfully, you're good to go. +This stage is meant to be executed after the [bootstrap](../0-bootstrap) stage has run, as it leverages the automation service account and bucket created there. The relevant user groups must also exist, but that's one of the requirements for the previous stage too, so if you ran that successfully, you're good to go. 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 bootstrap stage for the actual roles needed. @@ -56,7 +60,7 @@ The default way of making sure you have the right permissions, is to use the ide 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](../00-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: +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: ```bash # `outputs_location` is set to `~/fast-config` @@ -66,12 +70,12 @@ ln -s ~/fast-config/providers/01-resman-providers.tf . If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: ```bash -cd ../00-bootstrap +cd ../0-bootstrap terraform output -json providers | jq -r '.["01-resman"]' \ - > ../01-resman/providers.tf + > ../1-resman/providers.tf ``` -If you want to continue to rely on `outputs_location` logic, create a `terraform.tfvars` file and configure it as described [here](../00-bootstrap/#output-files-and-cross-stage-variables). +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). ### Variable configuration @@ -82,17 +86,17 @@ There are two broad sets of variables you will need to fill in: 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. -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 `00-bootstrap.auto.tfvars.json` containing values derived from resources managed by the bootstrap stage: +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: ```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/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](../00-bootstrap/README.md#billing-account) as the `billing_account` variable is identical across all stages. +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: @@ -141,7 +145,7 @@ For policies where additional data is needed, a root-level `organization_policy_ ### IAM -IAM roles can be easily edited in the relevant `branch-xxx.tf` file, following the best practice outlined in the [bootstrap stage](../00-bootstrap#customizations) documentation of separating user-level and service-account level IAM policies in modules' `iam_groups`, `iam`, and `iam_additive` variables. +IAM roles can be easily edited in the relevant `branch-xxx.tf` file, following the best practice outlined in the [bootstrap stage](../0-bootstrap#customizations) documentation of separating user-level and service-account level IAM policies in modules' `iam_groups`, `iam`, and `iam_additive` variables. A full reference of IAM roles managed by this stage [is available here](./IAM.md). @@ -156,11 +160,11 @@ Due to its simplicity, this stage lends itself easily to customizations: adding | name | description | modules | resources | |---|---|---|---| -| [billing.tf](./billing.tf) | Billing resources for external billing use cases. | organization | google_billing_account_iam_member | -| [branch-data-platform.tf](./branch-data-platform.tf) | Data Platform stages resources. | folder · gcs · iam-service-account | | +| [billing.tf](./billing.tf) | Billing resources for external billing use cases. | | google_billing_account_iam_member | +| [branch-data-platform.tf](./branch-data-platform.tf) | Data Platform stages resources. | folder · gcs · iam-service-account | google_organization_iam_member | | [branch-gke.tf](./branch-gke.tf) | GKE multitenant stage resources. | folder · gcs · iam-service-account | | | [branch-networking.tf](./branch-networking.tf) | Networking stage resources. | folder · gcs · iam-service-account | | -| [branch-project-factory.tf](./branch-project-factory.tf) | Project factory stage resources. | gcs · iam-service-account | | +| [branch-project-factory.tf](./branch-project-factory.tf) | Project factory stage resources. | gcs · iam-service-account | google_organization_iam_member | | [branch-sandbox.tf](./branch-sandbox.tf) | Sandbox stage resources. | folder · gcs · iam-service-account | | | [branch-security.tf](./branch-security.tf) | Security stage resources. | folder · gcs · iam-service-account | | | [branch-teams.tf](./branch-teams.tf) | Team stage resources. | folder · gcs · iam-service-account | | @@ -170,7 +174,7 @@ Due to its simplicity, this stage lends itself easily to customizations: adding | [cicd-project-factory.tf](./cicd-project-factory.tf) | CI/CD resources for the teams branch. | iam-service-account · source-repository | | | [cicd-security.tf](./cicd-security.tf) | CI/CD resources for the security branch. | iam-service-account · source-repository | | | [main.tf](./main.tf) | Module-level locals and resources. | | | -| [organization.tf](./organization.tf) | Organization policies. | organization | google_organization_iam_member | +| [organization.tf](./organization.tf) | Organization policies. | organization | | | [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | local_file | | [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | | google_storage_bucket_object | | [outputs.tf](./outputs.tf) | Module outputs. | | | @@ -180,34 +184,34 @@ Due to its simplicity, this stage lends itself easily to customizations: adding | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L38) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [organization](variables.tf#L197) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L221) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [cicd_repositories](variables.tf#L47) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | -| [custom_roles](variables.tf#L129) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [data_dir](variables.tf#L138) | Relative path for the folder storing configuration data. | string | | "data" | | -| [fast_features](variables.tf#L144) | Selective control for top-level FAST features. | object({…}) | | {…} | 00-bootstrap | -| [groups](variables.tf#L164) | Group names to grant organization-level permissions. | map(string) | | {…} | 00-bootstrap | -| [locations](variables.tf#L179) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 00-bootstrap | -| [organization_policy_configs](variables.tf#L207) | Organization policies customization. | object({…}) | | null | | -| [outputs_location](variables.tf#L215) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [tag_names](variables.tf#L232) | Customized names for resource management tags. | object({…}) | | {…} | | -| [team_folders](variables.tf#L249) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | +| [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | +| [billing_account](variables.tf#L38) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | +| [organization](variables.tf#L193) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L217) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [cicd_repositories](variables.tf#L51) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | +| [custom_roles](variables.tf#L133) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [data_dir](variables.tf#L142) | Relative path for the folder storing configuration data. | string | | "data" | | +| [fast_features](variables.tf#L148) | Selective control for top-level FAST features. | object({…}) | | {} | 0-0-bootstrap | +| [groups](variables.tf#L162) | Group names to grant organization-level permissions. | object({…}) | | {} | 0-bootstrap | +| [locations](variables.tf#L175) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 0-bootstrap | +| [organization_policy_configs](variables.tf#L203) | Organization policies customization. | object({…}) | | null | | +| [outputs_location](variables.tf#L211) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [tag_names](variables.tf#L228) | Customized names for resource management tags. | object({…}) | | {…} | | +| [team_folders](variables.tf#L247) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | ## Outputs | name | description | sensitive | consumers | |---|---|:---:|---| -| [cicd_repositories](outputs.tf#L197) | WIF configuration for CI/CD repositories. | | | -| [dataplatform](outputs.tf#L211) | Data for the Data Platform stage. | | | -| [gke_multitenant](outputs.tf#L227) | Data for the GKE multitenant stage. | | 03-gke-multitenant | -| [networking](outputs.tf#L248) | Data for the networking stage. | | | -| [project_factories](outputs.tf#L257) | Data for the project factories stage. | | | -| [providers](outputs.tf#L272) | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking · 02-security · 03-dataplatform · xx-sandbox · xx-teams | -| [sandbox](outputs.tf#L279) | Data for the sandbox stage. | | xx-sandbox | -| [security](outputs.tf#L293) | Data for the networking stage. | | 02-security | -| [teams](outputs.tf#L303) | Data for the teams stage. | | | -| [tfvars](outputs.tf#L315) | Terraform variable files for the following stages. | ✓ | | +| [cicd_repositories](outputs.tf#L210) | WIF configuration for CI/CD repositories. | | | +| [dataplatform](outputs.tf#L224) | Data for the Data Platform stage. | | | +| [gke_multitenant](outputs.tf#L240) | Data for the GKE multitenant stage. | | 03-gke-multitenant | +| [networking](outputs.tf#L261) | Data for the networking stage. | | | +| [project_factories](outputs.tf#L270) | Data for the project factories stage. | | | +| [providers](outputs.tf#L285) | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking · 02-security · 03-dataplatform · xx-sandbox · xx-teams | +| [sandbox](outputs.tf#L292) | Data for the sandbox stage. | | xx-sandbox | +| [security](outputs.tf#L306) | Data for the networking stage. | | 02-security | +| [teams](outputs.tf#L316) | Data for the teams stage. | | | +| [tfvars](outputs.tf#L328) | Terraform variable files for the following stages. | ✓ | | diff --git a/fast/stages/01-resman/billing.tf b/fast/stages/1-resman/billing.tf similarity index 77% rename from fast/stages/01-resman/billing.tf rename to fast/stages/1-resman/billing.tf index fe497c7c34..ba20ab0534 100644 --- a/fast/stages/01-resman/billing.tf +++ b/fast/stages/1-resman/billing.tf @@ -34,23 +34,11 @@ locals { # billing account in same org (resources is in the organization.tf file) -# billing account in a different org - -module "billing-organization-ext" { - source = "../../../modules/organization" - count = local.billing_org_ext ? 1 : 0 - organization_id = "organizations/${var.billing_account.organization_id}" - iam_additive = { - "roles/billing.user" = local.billing_ext_users - "roles/billing.costsManager" = local.billing_ext_users - } -} - # standalone billing account resource "google_billing_account_iam_member" "billing_ext_admin" { for_each = toset( - local.billing_ext ? local.billing_ext_users : [] + !var.billing_account.is_org_level ? local.billing_ext_users : [] ) billing_account_id = var.billing_account.id role = "roles/billing.user" @@ -59,7 +47,7 @@ resource "google_billing_account_iam_member" "billing_ext_admin" { resource "google_billing_account_iam_member" "billing_ext_costsmanager" { for_each = toset( - local.billing_ext ? local.billing_ext_users : [] + !var.billing_account.is_org_level ? local.billing_ext_users : [] ) billing_account_id = var.billing_account.id role = "roles/billing.costsManager" diff --git a/fast/stages/01-resman/branch-data-platform.tf b/fast/stages/1-resman/branch-data-platform.tf similarity index 86% rename from fast/stages/01-resman/branch-data-platform.tf rename to fast/stages/1-resman/branch-data-platform.tf index 66cc9fbb08..7a93e7a54b 100644 --- a/fast/stages/01-resman/branch-data-platform.tf +++ b/fast/stages/1-resman/branch-data-platform.tf @@ -137,3 +137,22 @@ module "branch-dp-prod-gcs" { "roles/storage.objectAdmin" = [module.branch-dp-prod-sa.0.iam_email] } } + +resource "google_organization_iam_member" "org_policy_admin_dp" { + for_each = !var.fast_features.data_platform ? {} : { + data-dev = ["data", "development", module.branch-dp-dev-sa.0.iam_email] + data-prod = ["data", "production", module.branch-dp-prod-sa.0.iam_email] + } + org_id = var.organization.id + role = "roles/orgpolicy.policyAdmin" + member = each.value.2 + condition { + title = "org_policy_tag_dp_scoped" + description = "Org policy tag scoped grant for ${each.value.0}/${each.value.1}." + expression = <<-END + resource.matchTag('${var.organization.id}/${var.tag_names.context}', '${each.value.0}') + && + resource.matchTag('${var.organization.id}/${var.tag_names.environment}', '${each.value.1}') + END + } +} diff --git a/fast/stages/01-resman/branch-gke.tf b/fast/stages/1-resman/branch-gke.tf similarity index 94% rename from fast/stages/01-resman/branch-gke.tf rename to fast/stages/1-resman/branch-gke.tf index 84ca41ed59..76777d8fca 100644 --- a/fast/stages/01-resman/branch-gke.tf +++ b/fast/stages/1-resman/branch-gke.tf @@ -77,7 +77,11 @@ module "branch-gke-dev-sa" { prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = concat( - ["group:${local.groups.gcp-devops}"], + ( + local.groups.gcp-devops == null + ? [] + : ["group:${local.groups.gcp-devops}"] + ), compact([ try(module.branch-gke-dev-sa-cicd.0.iam_email, null) ]) @@ -97,7 +101,11 @@ module "branch-gke-prod-sa" { prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = concat( - ["group:${local.groups.gcp-devops}"], + ( + local.groups.gcp-devops == null + ? [] + : ["group:${local.groups.gcp-devops}"] + ), compact([ try(module.branch-gke-prod-sa-cicd.0.iam_email, null) ]) diff --git a/fast/stages/01-resman/branch-networking.tf b/fast/stages/1-resman/branch-networking.tf similarity index 98% rename from fast/stages/01-resman/branch-networking.tf rename to fast/stages/1-resman/branch-networking.tf index 530cf6b09f..1fd7a6b3d9 100644 --- a/fast/stages/01-resman/branch-networking.tf +++ b/fast/stages/1-resman/branch-networking.tf @@ -20,7 +20,7 @@ module "branch-network-folder" { source = "../../../modules/folder" parent = "organizations/${var.organization.id}" name = "Networking" - group_iam = { + group_iam = local.groups.gcp-network-admins == null ? {} : { (local.groups.gcp-network-admins) = [ # add any needed roles for resources/services not managed via Terraform, # or replace editor with ~viewer if no broad resource management needed diff --git a/fast/stages/01-resman/branch-project-factory.tf b/fast/stages/1-resman/branch-project-factory.tf similarity index 78% rename from fast/stages/01-resman/branch-project-factory.tf rename to fast/stages/1-resman/branch-project-factory.tf index 41651a28c3..d74a8acb10 100644 --- a/fast/stages/01-resman/branch-project-factory.tf +++ b/fast/stages/1-resman/branch-project-factory.tf @@ -79,3 +79,22 @@ module "branch-pf-prod-gcs" { "roles/storage.objectAdmin" = [module.branch-pf-prod-sa.0.iam_email] } } + +resource "google_organization_iam_member" "org_policy_admin_pf" { + for_each = !var.fast_features.project_factory ? {} : { + pf-dev = ["teams", "development", module.branch-pf-dev-sa.0.iam_email] + pf-prod = ["teams", "production", module.branch-pf-prod-sa.0.iam_email] + } + org_id = var.organization.id + role = "roles/orgpolicy.policyAdmin" + member = each.value.2 + condition { + title = "org_policy_tag_pf_scoped" + description = "Org policy tag scoped grant for ${each.value.0}/${each.value.1}." + expression = <<-END + resource.matchTag('${var.organization.id}/${var.tag_names.context}', '${each.value.0}') + && + resource.matchTag('${var.organization.id}/${var.tag_names.environment}', '${each.value.1}') + END + } +} diff --git a/fast/stages/01-resman/branch-sandbox.tf b/fast/stages/1-resman/branch-sandbox.tf similarity index 100% rename from fast/stages/01-resman/branch-sandbox.tf rename to fast/stages/1-resman/branch-sandbox.tf diff --git a/fast/stages/01-resman/branch-security.tf b/fast/stages/1-resman/branch-security.tf similarity index 96% rename from fast/stages/01-resman/branch-security.tf rename to fast/stages/1-resman/branch-security.tf index c7b4fc9708..4b0c0fb131 100644 --- a/fast/stages/01-resman/branch-security.tf +++ b/fast/stages/1-resman/branch-security.tf @@ -20,7 +20,7 @@ module "branch-security-folder" { source = "../../../modules/folder" parent = "organizations/${var.organization.id}" name = "Security" - group_iam = { + group_iam = local.groups.gcp-security-admins == null ? {} : { (local.groups.gcp-security-admins) = [ # add any needed roles for resources/services not managed via Terraform, # e.g. @@ -51,7 +51,7 @@ module "branch-security-folder" { module "branch-security-sa" { source = "../../../modules/iam-service-account" project_id = var.automation.project_id - name = "prod-resman-sec-0" + name = "security-0" display_name = "Terraform resman security service account." prefix = var.prefix iam = { diff --git a/fast/stages/01-resman/branch-teams.tf b/fast/stages/1-resman/branch-teams.tf similarity index 100% rename from fast/stages/01-resman/branch-teams.tf rename to fast/stages/1-resman/branch-teams.tf diff --git a/fast/stages/01-resman/cicd-data-platform.tf b/fast/stages/1-resman/cicd-data-platform.tf similarity index 99% rename from fast/stages/01-resman/cicd-data-platform.tf rename to fast/stages/1-resman/cicd-data-platform.tf index 5b07883c44..e69fd5bc1e 100644 --- a/fast/stages/01-resman/cicd-data-platform.tf +++ b/fast/stages/1-resman/cicd-data-platform.tf @@ -103,7 +103,7 @@ module "branch-dp-dev-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { @@ -146,7 +146,7 @@ module "branch-dp-prod-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { diff --git a/fast/stages/01-resman/cicd-gke.tf b/fast/stages/1-resman/cicd-gke.tf similarity index 99% rename from fast/stages/01-resman/cicd-gke.tf rename to fast/stages/1-resman/cicd-gke.tf index fa4f8767ca..4388a3ac55 100644 --- a/fast/stages/01-resman/cicd-gke.tf +++ b/fast/stages/1-resman/cicd-gke.tf @@ -103,7 +103,7 @@ module "branch-gke-dev-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { @@ -146,7 +146,7 @@ module "branch-gke-prod-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { diff --git a/fast/stages/01-resman/cicd-networking.tf b/fast/stages/1-resman/cicd-networking.tf similarity index 99% rename from fast/stages/01-resman/cicd-networking.tf rename to fast/stages/1-resman/cicd-networking.tf index 894348ff3b..245d5ed025 100644 --- a/fast/stages/01-resman/cicd-networking.tf +++ b/fast/stages/1-resman/cicd-networking.tf @@ -65,7 +65,7 @@ module "branch-network-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { diff --git a/fast/stages/01-resman/cicd-project-factory.tf b/fast/stages/1-resman/cicd-project-factory.tf similarity index 99% rename from fast/stages/01-resman/cicd-project-factory.tf rename to fast/stages/1-resman/cicd-project-factory.tf index 8f357ce6c0..1e2b456531 100644 --- a/fast/stages/01-resman/cicd-project-factory.tf +++ b/fast/stages/1-resman/cicd-project-factory.tf @@ -114,7 +114,7 @@ module "branch-pf-dev-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { @@ -162,7 +162,7 @@ module "branch-pf-prod-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { diff --git a/fast/stages/01-resman/cicd-security.tf b/fast/stages/1-resman/cicd-security.tf similarity index 99% rename from fast/stages/01-resman/cicd-security.tf rename to fast/stages/1-resman/cicd-security.tf index dd27a47331..c35bfbfbb6 100644 --- a/fast/stages/01-resman/cicd-security.tf +++ b/fast/stages/1-resman/cicd-security.tf @@ -65,7 +65,7 @@ module "branch-security-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { diff --git a/fast/stages/1-resman/data/org-policies/compute.yaml b/fast/stages/1-resman/data/org-policies/compute.yaml new file mode 100644 index 0000000000..0d27ac426d --- /dev/null +++ b/fast/stages/1-resman/data/org-policies/compute.yaml @@ -0,0 +1,73 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +compute.disableGuestAttributesAccess: + enforce: true + +compute.requireOsLogin: + enforce: true + +compute.restrictLoadBalancerCreationForTypes: + allow: + values: + - in:INTERNAL + +compute.skipDefaultNetworkCreation: + enforce: true + +compute.vmExternalIpAccess: + deny: + all: true + + +# compute.disableInternetNetworkEndpointGroup: +# enforce: true + +# compute.disableNestedVirtualization: +# enforce: true + +# compute.disableSerialPortAccess: +# enforce: true + +# compute.restrictCloudNATUsage: +# deny: +# all: true + +# compute.restrictDedicatedInterconnectUsage: +# deny: +# all: true + +# compute.restrictPartnerInterconnectUsage: +# deny: +# all: true + +# compute.restrictProtocolForwardingCreationForTypes: +# deny: +# all: true + +# compute.restrictSharedVpcHostProjects: +# deny: +# all: true + +# compute.restrictSharedVpcSubnetworks: +# deny: +# all: true + +# compute.restrictVpcPeering: +# deny: +# all: true + +# compute.restrictVpnPeerIPs: +# deny: +# all: true + +# compute.restrictXpnProjectLienRemoval: +# enforce: true + +# compute.setNewProjectDefaultToZonalDNSOnly: +# enforce: true + +# compute.vmCanIpForward: +# 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 new file mode 100644 index 0000000000..4d83f827fe --- /dev/null +++ b/fast/stages/1-resman/data/org-policies/iam.yaml @@ -0,0 +1,12 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +iam.automaticIamGrantsForDefaultServiceAccounts: + enforce: true + +iam.disableServiceAccountKeyCreation: + enforce: true + +iam.disableServiceAccountKeyUpload: + enforce: true diff --git a/fast/stages/1-resman/data/org-policies/serverless.yaml b/fast/stages/1-resman/data/org-policies/serverless.yaml new file mode 100644 index 0000000000..de62e6c702 --- /dev/null +++ b/fast/stages/1-resman/data/org-policies/serverless.yaml @@ -0,0 +1,26 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +run.allowedIngress: + allow: + values: + - is:internal + +# run.allowedVPCEgress: +# allow: +# values: +# - is:private-ranges-only + +# cloudfunctions.allowedIngressSettings: +# allow: +# values: +# - is:ALLOW_INTERNAL_ONLY + +# cloudfunctions.allowedVpcConnectorEgressSettings: +# allow: +# values: +# - is:PRIVATE_RANGES_ONLY + +# cloudfunctions.requireVPCConnector: +# enforce: true diff --git a/fast/stages/1-resman/data/org-policies/sql.yaml b/fast/stages/1-resman/data/org-policies/sql.yaml new file mode 100644 index 0000000000..88b84d9d50 --- /dev/null +++ b/fast/stages/1-resman/data/org-policies/sql.yaml @@ -0,0 +1,9 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +sql.restrictAuthorizedNetworks: + enforce: true + +sql.restrictPublicIp: + enforce: true diff --git a/fast/stages/1-resman/data/org-policies/storage.yaml b/fast/stages/1-resman/data/org-policies/storage.yaml new file mode 100644 index 0000000000..6c0a673f3a --- /dev/null +++ b/fast/stages/1-resman/data/org-policies/storage.yaml @@ -0,0 +1,6 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +storage.uniformBucketLevelAccess: + enforce: true diff --git a/fast/stages/1-resman/diagram.png b/fast/stages/1-resman/diagram.png new file mode 100644 index 0000000000..d1026318b4 Binary files /dev/null and b/fast/stages/1-resman/diagram.png differ diff --git a/fast/stages/1-resman/diagram.svg b/fast/stages/1-resman/diagram.svg new file mode 100644 index 0000000000..541db3f4b8 --- /dev/null +++ b/fast/stages/1-resman/diagram.svg @@ -0,0 +1,1340 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages/01-resman/main.tf b/fast/stages/1-resman/main.tf similarity index 78% rename from fast/stages/01-resman/main.tf rename to fast/stages/1-resman/main.tf index 0651ee3fac..ff08b8c58d 100644 --- a/fast/stages/01-resman/main.tf +++ b/fast/stages/1-resman/main.tf @@ -17,15 +17,13 @@ locals { # convenience flags that express where billing account resides automation_resman_sa = try( - [format( - "serviceAccount:%s", - data.google_client_openid_userinfo.provider_identity.0.email - )], - [] + data.google_client_openid_userinfo.provider_identity.0.email, null + ) + automation_resman_sa_iam = ( + local.automation_resman_sa == null + ? [] + : ["serviceAccount:${local.automation_resman_sa}"] ) - billing_ext = var.billing_account.organization_id == null - billing_org = var.billing_account.organization_id == var.organization.id - billing_org_ext = !local.billing_ext && !local.billing_org branch_optional_sa_lists = { dp-dev = compact([try(module.branch-dp-dev-sa.0.iam_email, "")]) dp-prod = compact([try(module.branch-dp-prod-sa.0.iam_email, "")]) @@ -51,16 +49,16 @@ locals { } cicd_workflow_var_files = { stage_2 = [ - "00-bootstrap.auto.tfvars.json", - "01-resman.auto.tfvars.json", + "0-bootstrap.auto.tfvars.json", + "1-resman.auto.tfvars.json", "globals.auto.tfvars.json" ] stage_3 = [ - "00-bootstrap.auto.tfvars.json", - "01-resman.auto.tfvars.json", + "0-bootstrap.auto.tfvars.json", + "1-resman.auto.tfvars.json", "globals.auto.tfvars.json", - "02-networking.auto.tfvars.json", - "02-security.auto.tfvars.json" + "2-networking.auto.tfvars.json", + "2-security.auto.tfvars.json" ] } custom_roles = coalesce(var.custom_roles, {}) @@ -74,8 +72,7 @@ locals { k => "${v}@${var.organization.domain}" } groups_iam = { - for k, v in local.groups : - k => "group:${v}" + for k, v in local.groups : k => v != null ? "group:${v}" : null } identity_providers = coalesce( try(var.automation.federated_identity_providers, null), {} diff --git a/fast/stages/01-resman/organization.tf b/fast/stages/1-resman/organization.tf similarity index 66% rename from fast/stages/01-resman/organization.tf rename to fast/stages/1-resman/organization.tf index 7ecf795232..3d7db46dbc 100644 --- a/fast/stages/01-resman/organization.tf +++ b/fast/stages/1-resman/organization.tf @@ -16,7 +16,6 @@ # tfdoc:file:description Organization policies. - locals { all_drs_domains = concat( [var.organization.customer_id], @@ -47,7 +46,7 @@ module "organization" { module.branch-network-sa.iam_email ] }, - local.billing_org ? { + var.billing_account.is_org_level ? { "roles/billing.costsManager" = concat( local.branch_optional_sa_lists.pf-dev, local.branch_optional_sa_lists.pf-prod @@ -85,6 +84,9 @@ module "organization" { } org_policies_data_path = "${var.data_dir}/org-policies" + # do not assign tagViewer or tagUser roles here on tag keys and values as + # they are managed authoritatively and will break multitenant stages + tags = { (var.tag_names.context) = { description = "Resource management context." @@ -106,45 +108,10 @@ module "organization" { production = null } } + (var.tag_names.tenant) = { + description = "Organization tenant." + } } } -# organization policy admin role assigned with a condition on tags - -resource "google_organization_iam_member" "org_policy_admin_dp" { - for_each = !var.fast_features.data_platform ? {} : { - data-dev = ["data", "development", module.branch-dp-dev-sa.0.iam_email] - data-prod = ["data", "production", module.branch-dp-prod-sa.0.iam_email] - } - org_id = var.organization.id - role = "roles/orgpolicy.policyAdmin" - member = each.value.2 - condition { - title = "org_policy_tag_dp_scoped" - description = "Org policy tag scoped grant for ${each.value.0}/${each.value.1}." - expression = <<-END - resource.matchTag('${var.organization.id}/${var.tag_names.context}', '${each.value.0}') - && - resource.matchTag('${var.organization.id}/${var.tag_names.environment}', '${each.value.1}') - END - } -} - -resource "google_organization_iam_member" "org_policy_admin_pf" { - for_each = !var.fast_features.project_factory ? {} : { - pf-dev = ["teams", "development", module.branch-pf-dev-sa.0.iam_email] - pf-prod = ["teams", "production", module.branch-pf-prod-sa.0.iam_email] - } - org_id = var.organization.id - role = "roles/orgpolicy.policyAdmin" - member = each.value.2 - condition { - title = "org_policy_tag_pf_scoped" - description = "Org policy tag scoped grant for ${each.value.0}/${each.value.1}." - expression = <<-END - resource.matchTag('${var.organization.id}/${var.tag_names.context}', '${each.value.0}') - && - resource.matchTag('${var.organization.id}/${var.tag_names.environment}', '${each.value.1}') - END - } -} +# organization policy conditional roles are in the relevant branch files diff --git a/fast/stages/01-resman/outputs-files.tf b/fast/stages/1-resman/outputs-files.tf similarity index 94% rename from fast/stages/01-resman/outputs-files.tf rename to fast/stages/1-resman/outputs-files.tf index bd281d451b..f7f080dd9c 100644 --- a/fast/stages/01-resman/outputs-files.tf +++ b/fast/stages/1-resman/outputs-files.tf @@ -30,7 +30,7 @@ resource "local_file" "providers" { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${local.outputs_location}/tfvars/01-resman.auto.tfvars.json" + filename = "${local.outputs_location}/tfvars/1-resman.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/01-resman/outputs-gcs.tf b/fast/stages/1-resman/outputs-gcs.tf similarity index 96% rename from fast/stages/01-resman/outputs-gcs.tf rename to fast/stages/1-resman/outputs-gcs.tf index f1db11ef5d..5b9f5d8518 100644 --- a/fast/stages/01-resman/outputs-gcs.tf +++ b/fast/stages/1-resman/outputs-gcs.tf @@ -25,7 +25,7 @@ resource "google_storage_bucket_object" "providers" { resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/01-resman.auto.tfvars.json" + name = "tfvars/1-resman.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/01-resman/outputs.tf b/fast/stages/1-resman/outputs.tf similarity index 72% rename from fast/stages/01-resman/outputs.tf rename to fast/stages/1-resman/outputs.tf index e07df577c2..62525399e8 100644 --- a/fast/stages/01-resman/outputs.tf +++ b/fast/stages/1-resman/outputs.tf @@ -19,42 +19,42 @@ locals { cicd_workflow_attrs = { data_platform_dev = { service_account = try(module.branch-dp-dev-sa-cicd.0.email, null) - tf_providers_file = "03-data-platform-dev-providers.tf" + tf_providers_file = "3-data-platform-dev-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } data_platform_prod = { service_account = try(module.branch-dp-prod-sa-cicd.0.email, null) - tf_providers_file = "03-data-platform-prod-providers.tf" + tf_providers_file = "3-data-platform-prod-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } gke_dev = { service_account = try(module.branch-gke-dev-sa-cicd.0.email, null) - tf_providers_file = "03-gke-dev-providers.tf" + tf_providers_file = "3-gke-dev-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } gke_prod = { service_account = try(module.branch-gke-prod-sa-cicd.0.email, null) - tf_providers_file = "03-gke-prod-providers.tf" + tf_providers_file = "3-gke-prod-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } networking = { service_account = try(module.branch-network-sa-cicd.0.email, null) - tf_providers_file = "02-networking-providers.tf" + tf_providers_file = "2-networking-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_2 } project_factory_dev = { service_account = try(module.branch-pf-dev-sa-cicd.0.email, null) - tf_providers_file = "03-project-factory-dev-providers.tf" + tf_providers_file = "3-project-factory-dev-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } project_factory_prod = { service_account = try(module.branch-pf-prod-sa-cicd.0.email, null) - tf_providers_file = "03-project-factory-prod-providers.tf" + tf_providers_file = "3-project-factory-prod-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } security = { service_account = try(module.branch-security-sa-cicd.0.email, null) - tf_providers_file = "02-security-providers.tf" + tf_providers_file = "2-security-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_2 } } @@ -98,74 +98,85 @@ locals { ) providers = merge( { - "02-networking" = templatefile(local._tpl_providers, { - bucket = module.branch-network-gcs.name - name = "networking" - sa = module.branch-network-sa.email + "2-0-networking" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-network-gcs.name + name = "networking" + sa = module.branch-network-sa.email }) - "02-security" = templatefile(local._tpl_providers, { - bucket = module.branch-security-gcs.name - name = "security" - sa = module.branch-security-sa.email + "2-0-security" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-security-gcs.name + name = "security" + sa = module.branch-security-sa.email }) }, !var.fast_features.data_platform ? {} : { - "03-data-platform-dev" = templatefile(local._tpl_providers, { - bucket = module.branch-dp-dev-gcs.0.name - name = "dp-dev" - sa = module.branch-dp-dev-sa.0.email + "3-0-data-platform-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-dp-dev-gcs.0.name + name = "dp-dev" + sa = module.branch-dp-dev-sa.0.email }) - "03-data-platform-prod" = templatefile(local._tpl_providers, { - bucket = module.branch-dp-prod-gcs.0.name - name = "dp-prod" - sa = module.branch-dp-prod-sa.0.email + "3-0-data-platform-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-dp-prod-gcs.0.name + name = "dp-prod" + sa = module.branch-dp-prod-sa.0.email }) }, !var.fast_features.gke ? {} : { - "03-gke-dev" = templatefile(local._tpl_providers, { - bucket = module.branch-gke-dev-gcs.0.name - name = "gke-dev" - sa = module.branch-gke-dev-sa.0.email + "3-0-gke-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-gke-dev-gcs.0.name + name = "gke-dev" + sa = module.branch-gke-dev-sa.0.email }) - "03-gke-prod" = templatefile(local._tpl_providers, { - bucket = module.branch-gke-prod-gcs.0.name - name = "gke-prod" - sa = module.branch-gke-prod-sa.0.email + "3-0-gke-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-gke-prod-gcs.0.name + name = "gke-prod" + sa = module.branch-gke-prod-sa.0.email }) }, !var.fast_features.project_factory ? {} : { - "03-project-factory-dev" = templatefile(local._tpl_providers, { - bucket = module.branch-pf-dev-gcs.0.name - name = "team-dev" - sa = module.branch-pf-dev-sa.0.email + "3-0-project-factory-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-pf-dev-gcs.0.name + name = "team-dev" + sa = module.branch-pf-dev-sa.0.email }) - "03-project-factory-prod" = templatefile(local._tpl_providers, { - bucket = module.branch-pf-prod-gcs.0.name - name = "team-prod" - sa = module.branch-pf-prod-sa.0.email + "3-0-project-factory-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-pf-prod-gcs.0.name + name = "team-prod" + sa = module.branch-pf-prod-sa.0.email }) }, !var.fast_features.sandbox ? {} : { - "99-sandbox" = templatefile(local._tpl_providers, { - bucket = module.branch-sandbox-gcs.0.name - name = "sandbox" - sa = module.branch-sandbox-sa.0.email + "9-0-sandbox" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-sandbox-gcs.0.name + name = "sandbox" + sa = module.branch-sandbox-sa.0.email }) }, !var.fast_features.teams ? {} : merge( { - "03-teams" = templatefile(local._tpl_providers, { - bucket = module.branch-teams-gcs.0.name - name = "teams" - sa = module.branch-teams-sa.0.email + "3-teams" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-teams-gcs.0.name + name = "teams" + sa = module.branch-teams-sa.0.email }) }, { for k, v in module.branch-teams-team-sa : - "03-teams-${k}" => templatefile(local._tpl_providers, { - bucket = module.branch-teams-team-gcs[k].name - name = "teams" - sa = v.email + "3-teams-${k}" => templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-teams-team-gcs[k].name + name = "teams" + sa = v.email }) } ) @@ -190,7 +201,9 @@ locals { tfvars = { folder_ids = local.folder_ids service_accounts = local.service_accounts + tag_keys = { for k, v in module.organization.tag_keys : k => v.id } tag_names = var.tag_names + tag_values = { for k, v in module.organization.tag_values : k => v.id } } } diff --git a/fast/stages/1-resman/templates/providers.tf.tpl b/fast/stages/1-resman/templates/providers.tf.tpl new file mode 100644 index 0000000000..d1c224c5c1 --- /dev/null +++ b/fast/stages/1-resman/templates/providers.tf.tpl @@ -0,0 +1,33 @@ +/** + * 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. + */ + +terraform { + backend "gcs" { + bucket = "${bucket}" + impersonate_service_account = "${sa}" + %{~ if backend_extra != null ~} + ${indent(4, backend_extra)} + %{~ endif ~} + } +} +provider "google" { + impersonate_service_account = "${sa}" +} +provider "google-beta" { + impersonate_service_account = "${sa}" +} + +# end provider.tf for ${name} diff --git a/fast/stages/1-resman/templates/workflow-github.yaml b/fast/stages/1-resman/templates/workflow-github.yaml new file mode 100644 index 0000000000..8a946d84c8 --- /dev/null +++ b/fast/stages/1-resman/templates/workflow-github.yaml @@ -0,0 +1,186 @@ +# 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. + +name: "FAST ${stage_name} stage" + +on: + pull_request: + branches: + - main + types: + - closed + - opened + - synchronize + +env: + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + TF_VERSION: 1.3.2 + +jobs: + fast-pr: + permissions: + contents: read + id-token: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@v3 + + # set up SSH key authentication to the modules repository + - id: ssh-config + name: Configure SSH authentication + run: | + ssh-agent -a "$SSH_AUTH_SOCK" > /dev/null + ssh-add - <<< "$${{ secrets.CICD_MODULES_KEY }}" + + # set up authentication via Workload identity Federation + - id: gcp-auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0 + with: + workload_identity_provider: $${{ env.FAST_WIF_PROVIDER }} + service_account: $${{ env.FAST_SERVICE_ACCOUNT }} + access_token_lifetime: 3600s + + - id: gcp-sdk + name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v0 + with: + install_components: alpha + + # copy provider and tfvars files + - id: tf-config + name: Copy Terraform output files + run: | + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/providers/$${{env.TF_PROVIDERS_FILE}}" ./ + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/tfvars" ./ + for f in $${{env.TF_VAR_FILES}}; do + ln -s "tfvars/$f" ./ + done + + - id: tf-setup + name: Set up Terraform + uses: hashicorp/setup-terraform@v2.0.3 + with: + terraform_version: $${{ env.TF_VERSION }} + + # run Terraform init/validate/plan + - id: tf-init + name: Terraform init + run: | + terraform init -no-color + + - id: tf-validate + name: Terraform validate + run: terraform validate -no-color + + - id: tf-plan + name: Terraform plan + continue-on-error: true + run: | + terraform plan -input=false -out ../plan.out -no-color + + - id: tf-apply + if: github.event.pull_request.merged == true && success() + name: Terraform apply + continue-on-error: true + run: | + terraform apply -input=false -auto-approve -no-color ../plan.out + + - id: pr-comment + name: Post comment to Pull Request + continue-on-error: true + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + env: + PLAN: $${{ steps.tf-plan.outputs.stdout }}\n$${{ steps.tf-plan.outputs.stderr }} + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + +
Validation Output + + \`\`\`\n + $${{ steps.tf-validate.outputs.stdout }} + \`\`\` + +
+ + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + +
Show Plan + + \`\`\`\n + $${process.env.PLAN.split('\n').filter(l => l.match(/^([A-Z\s].*|)$$/)).join('\n')} + \`\`\` + +
+ + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: pr-short-comment + name: Post comment to Pull Request + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' && steps.pr-comment.outcome != 'success' + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + + Plan output is in the action log. + + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: check-plan + name: Check plan failure + if: steps.tf-plan.outcome != 'success' + run: exit 1 + + - id: check-apply + name: Check apply failure + if: github.event.pull_request.merged == true && steps.tf-apply.outcome != 'success' + run: exit 1 diff --git a/fast/stages/1-resman/templates/workflow-gitlab.yaml b/fast/stages/1-resman/templates/workflow-gitlab.yaml new file mode 100644 index 0000000000..8981e70b3c --- /dev/null +++ b/fast/stages/1-resman/templates/workflow-gitlab.yaml @@ -0,0 +1,120 @@ +# 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. + +default: + before_script: + - echo "$${CI_JOB_JWT_V2}" > token.txt + image: + name: hashicorp/terraform + entrypoint: + - "/usr/bin/env" + - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +variables: + GOOGLE_CREDENTIALS: cicd-sa-credentials.json + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + +stages: + - gcp-auth + - tf-files + - tf-plan + - tf-apply + +cache: + key: gcp-auth + paths: + - cicd-sa-credentials.json + - .tf-setup + +gcp-auth: + image: + name: google/cloud-sdk:slim + stage: gcp-auth + script: + - | + gcloud iam workload-identity-pools create-cred-config \ + $${FAST_WIF_PROVIDER} \ + --service-account=$${FAST_SERVICE_ACCOUNT} \ + --service-account-token-lifetime-seconds=3600 \ + --output-file=$${GOOGLE_CREDENTIALS} \ + --credential-source-file=token.txt +tf-files: + dependencies: + - gcp-auth + image: + name: google/cloud-sdk:slim + stage: tf-files + script: + # - gcloud components install -q alpha + - gcloud config set auth/credential_file_override $${GOOGLE_CREDENTIALS} + - mkdir -p .tf-setup + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/providers/$${TF_PROVIDERS_FILE}" .tf-setup/ + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/tfvars" .tf-setup/ + +tf-plan: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-plan + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform plan + dependencies: + - tf-files + +tf-apply: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-apply + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform apply -input=false -auto-approve + dependencies: + - tf-files + when: manual + only: + variables: + - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH diff --git a/fast/stages/1-resman/templates/workflow-sourcerepo.yaml b/fast/stages/1-resman/templates/workflow-sourcerepo.yaml new file mode 100644 index 0000000000..446c9c9605 --- /dev/null +++ b/fast/stages/1-resman/templates/workflow-sourcerepo.yaml @@ -0,0 +1,98 @@ +# 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. + +steps: + - name: alpine:3 + id: tf-download + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + mkdir -p /builder/home/.local/bin + wget https://releases.hashicorp.com/terraform/$${_TF_VERSION}/terraform_$${_TF_VERSION}_linux_amd64.zip + unzip terraform_$${_TF_VERSION}_linux_amd64.zip -d /builder/home/.local/bin + rm terraform_$${_TF_VERSION}_linux_amd64.zip + chmod 755 /builder/home/.local/bin/terraform + - name: alpine:3 + id: tf-check-format + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform fmt -recursive -check /workspace/ + - name: gcr.io/google.com/cloudsdktool/cloud-sdk:alpine + id: tf-files + entrypoint: bash + args: + - -eEuo + - pipefail + - -c + - |- + /google-cloud-sdk/bin/gsutil cp \ + gs://$${_FAST_OUTPUTS_BUCKET}/providers/$${_TF_PROVIDERS_FILE} ./ + /google-cloud-sdk/bin/gsutil cp -r \ + gs://$${_FAST_OUTPUTS_BUCKET}/tfvars ./ + for f in $${_TF_VAR_FILES}; do + ln -s tfvars/$f ./ + done + - name: alpine:3 + id: tf-init + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform init -no-color + - name: alpine:3 + id: tf-check-validate + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform validate -no-color + - name: alpine:3 + id: tf-plan + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform plan -no-color -input=false -out plan.out + # store artifact and ask for approval here if needed + - name: alpine:3 + id: tf-apply + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform apply -no-color -input=false -auto-approve plan.out +options: + env: + - PATH=/usr/local/bin:/usr/bin:/bin:/builder/home/.local/bin + logging: CLOUD_LOGGING_ONLY +substitutions: + _FAST_OUTPUTS_BUCKET: ${outputs_bucket} + _TF_PROVIDERS_FILE: ${tf_providers_file} + _TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + _TF_VERSION: 1.3.2 diff --git a/fast/stages/01-resman/variables.tf b/fast/stages/1-resman/variables.tf similarity index 79% rename from fast/stages/01-resman/variables.tf rename to fast/stages/1-resman/variables.tf index 8b6f866bda..16c65ee83e 100644 --- a/fast/stages/01-resman/variables.tf +++ b/fast/stages/1-resman/variables.tf @@ -18,7 +18,7 @@ # the tfvars file generated in stage 00 and stored in its outputs variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -36,65 +36,69 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # 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 - organization_id = number + 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 "cicd_repositories" { description = "CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed." type = object({ - data_platform_dev = object({ + data_platform_dev = optional(object({ branch = string identity_provider = string name = string type = string - }) - data_platform_prod = object({ + })) + data_platform_prod = optional(object({ branch = string identity_provider = string name = string type = string - }) - gke_dev = object({ + })) + gke_dev = optional(object({ branch = string identity_provider = string name = string type = string - }) - gke_prod = object({ + })) + gke_prod = optional(object({ branch = string identity_provider = string name = string type = string - }) - networking = object({ + })) + networking = optional(object({ branch = string identity_provider = string name = string type = string - }) - project_factory_dev = object({ + })) + project_factory_dev = optional(object({ branch = string identity_provider = string name = string type = string - }) - project_factory_prod = object({ + })) + project_factory_prod = optional(object({ branch = string identity_provider = string name = string type = string - }) - security = object({ + })) + security = optional(object({ branch = string identity_provider = string name = string type = string - }) + })) }) default = null validation { @@ -127,7 +131,7 @@ variable "cicd_repositories" { } variable "custom_roles" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Custom roles defined at the org level, in key => id format." type = object({ service_project_network_admin = string @@ -142,42 +146,34 @@ variable "data_dir" { } variable "fast_features" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-0-bootstrap description = "Selective control for top-level FAST features." type = object({ - data_platform = bool - gke = bool - project_factory = bool - sandbox = bool - teams = bool + data_platform = optional(bool, false) + gke = optional(bool, false) + project_factory = optional(bool, false) + sandbox = optional(bool, false) + teams = optional(bool, false) }) - default = { - data_platform = true - gke = true - project_factory = true - sandbox = true - teams = true - } - # nullable = false + default = {} + nullable = false } variable "groups" { - # tfdoc:variable:source 00-bootstrap - description = "Group names to grant organization-level permissions." - type = map(string) + # tfdoc:variable:source 0-bootstrap # https://cloud.google.com/docs/enterprise/setup-checklist - default = { - gcp-billing-admins = "gcp-billing-admins", - gcp-devops = "gcp-devops", - gcp-network-admins = "gcp-network-admins" - gcp-organization-admins = "gcp-organization-admins" - gcp-security-admins = "gcp-security-admins" - gcp-support = "gcp-support" - } + description = "Group names to grant organization-level permissions." + type = object({ + gcp-devops = optional(string) + gcp-network-admins = optional(string) + gcp-security-admins = optional(string) + }) + default = {} + nullable = false } variable "locations" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Optional locations for GCS, BigQuery, and logging buckets created here." type = object({ bq = string @@ -195,7 +191,7 @@ variable "locations" { } variable "organization" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Organization details." type = object({ domain = string @@ -219,7 +215,7 @@ variable "outputs_location" { } variable "prefix" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string @@ -234,10 +230,12 @@ variable "tag_names" { type = object({ context = string environment = string + tenant = string }) default = { context = "context" environment = "environment" + tenant = "tenant" } nullable = false validation { diff --git a/fast/stages/02-networking-peering/.gitignore b/fast/stages/2-networking-a-peering/.gitignore similarity index 100% rename from fast/stages/02-networking-peering/.gitignore rename to fast/stages/2-networking-a-peering/.gitignore diff --git a/fast/stages/02-networking-peering/IAM.md b/fast/stages/2-networking-a-peering/IAM.md similarity index 100% rename from fast/stages/02-networking-peering/IAM.md rename to fast/stages/2-networking-a-peering/IAM.md diff --git a/fast/stages/02-networking-peering/README.md b/fast/stages/2-networking-a-peering/README.md similarity index 93% rename from fast/stages/02-networking-peering/README.md rename to fast/stages/2-networking-a-peering/README.md index c7829f0fb5..a29f6c7752 100644 --- a/fast/stages/02-networking-peering/README.md +++ b/fast/stages/2-networking-a-peering/README.md @@ -44,13 +44,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](../02-networking-vpn/)) +- [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) (implemented by [02-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](../02-networking-nva/)) +- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [02-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 @@ -122,7 +122,7 @@ This configuration is battle-tested, and flexible enough to lend itself to simpl ## How to run this stage -This stage is meant to be executed after the [resman](../01-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../00-bootstrap) 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. @@ -130,7 +130,7 @@ Before running this stage, you need to make sure you have the correct credential ### 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](../01-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). +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. @@ -144,7 +144,7 @@ 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 ../01-resman +cd ../1-resman terraform output -json providers | jq -r '.["02-networking"]' \ > ../02-networking/providers.tf ``` @@ -295,23 +295,23 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L74) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | -| [organization](variables.tf#L102) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L118) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [custom_adv](variables.tf#L34) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | -| [custom_roles](variables.tf#L51) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [data_dir](variables.tf#L60) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | -| [dns](variables.tf#L66) | Onprem DNS resolvers. | map(list(string)) | | {…} | | -| [l7ilb_subnets](variables.tf#L84) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | -| [outputs_location](variables.tf#L112) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | +| [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#L78) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | +| [organization](variables.tf#L106) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L122) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [custom_adv](variables.tf#L38) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | +| [custom_roles](variables.tf#L55) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [data_dir](variables.tf#L64) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | +| [dns](variables.tf#L70) | Onprem DNS resolvers. | map(list(string)) | | {…} | | +| [l7ilb_subnets](variables.tf#L88) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | +| [outputs_location](variables.tf#L116) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | | [peering_configs](variables-peerings.tf#L19) | Peering configurations. | map(object({…})) | | {…} | | -| [psa_ranges](variables.tf#L129) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | -| [region_trigram](variables.tf#L166) | Short names for GCP regions. | map(string) | | {…} | | -| [router_onprem_configs](variables.tf#L175) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | -| [service_accounts](variables.tf#L193) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L207) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [psa_ranges](variables.tf#L133) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | +| [region_trigram](variables.tf#L170) | Short names for GCP regions. | map(string) | | {…} | | +| [router_onprem_configs](variables.tf#L179) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | +| [service_accounts](variables.tf#L197) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_configs](variables.tf#L211) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-nva/data/cidrs.yaml b/fast/stages/2-networking-a-peering/data/cidrs.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/cidrs.yaml rename to fast/stages/2-networking-a-peering/data/cidrs.yaml diff --git a/fast/stages/02-networking-peering/data/dashboards/firewall_insights.json b/fast/stages/2-networking-a-peering/data/dashboards/firewall_insights.json similarity index 100% rename from fast/stages/02-networking-peering/data/dashboards/firewall_insights.json rename to fast/stages/2-networking-a-peering/data/dashboards/firewall_insights.json diff --git a/fast/stages/02-networking-peering/data/dashboards/vpn.json b/fast/stages/2-networking-a-peering/data/dashboards/vpn.json similarity index 100% rename from fast/stages/02-networking-peering/data/dashboards/vpn.json rename to fast/stages/2-networking-a-peering/data/dashboards/vpn.json diff --git a/fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml b/fast/stages/2-networking-a-peering/data/firewall-rules/dev/rules.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml rename to fast/stages/2-networking-a-peering/data/firewall-rules/dev/rules.yaml diff --git a/fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml b/fast/stages/2-networking-a-peering/data/firewall-rules/landing/rules.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml rename to fast/stages/2-networking-a-peering/data/firewall-rules/landing/rules.yaml diff --git a/fast/stages/02-networking-nva/data/hierarchical-policy-rules.yaml b/fast/stages/2-networking-a-peering/data/hierarchical-policy-rules.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/hierarchical-policy-rules.yaml rename to fast/stages/2-networking-a-peering/data/hierarchical-policy-rules.yaml diff --git a/fast/stages/02-networking-peering/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/dev/dev-dataplatform-ew1.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/subnets/dev/dev-dataplatform-ew1.yaml rename to fast/stages/2-networking-a-peering/data/subnets/dev/dev-dataplatform-ew1.yaml diff --git a/fast/stages/02-networking-peering/data/subnets/dev/dev-default-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/dev/dev-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/subnets/dev/dev-default-ew1.yaml rename to fast/stages/2-networking-a-peering/data/subnets/dev/dev-default-ew1.yaml diff --git a/fast/stages/02-networking-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml rename to fast/stages/2-networking-a-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml diff --git a/fast/stages/02-networking-peering/data/subnets/landing/landing-default-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/landing/landing-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/subnets/landing/landing-default-ew1.yaml rename to fast/stages/2-networking-a-peering/data/subnets/landing/landing-default-ew1.yaml diff --git a/fast/stages/02-networking-peering/data/subnets/prod/prod-default-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/prod/prod-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/subnets/prod/prod-default-ew1.yaml rename to fast/stages/2-networking-a-peering/data/subnets/prod/prod-default-ew1.yaml diff --git a/fast/stages/02-networking-peering/diagram.png b/fast/stages/2-networking-a-peering/diagram.png similarity index 100% rename from fast/stages/02-networking-peering/diagram.png rename to fast/stages/2-networking-a-peering/diagram.png diff --git a/fast/stages/02-networking-peering/diagram.svg b/fast/stages/2-networking-a-peering/diagram.svg similarity index 100% rename from fast/stages/02-networking-peering/diagram.svg rename to fast/stages/2-networking-a-peering/diagram.svg diff --git a/fast/stages/02-networking-peering/dns-dev.tf b/fast/stages/2-networking-a-peering/dns-dev.tf similarity index 100% rename from fast/stages/02-networking-peering/dns-dev.tf rename to fast/stages/2-networking-a-peering/dns-dev.tf diff --git a/fast/stages/02-networking-peering/dns-landing.tf b/fast/stages/2-networking-a-peering/dns-landing.tf similarity index 100% rename from fast/stages/02-networking-peering/dns-landing.tf rename to fast/stages/2-networking-a-peering/dns-landing.tf diff --git a/fast/stages/02-networking-peering/dns-prod.tf b/fast/stages/2-networking-a-peering/dns-prod.tf similarity index 100% rename from fast/stages/02-networking-peering/dns-prod.tf rename to fast/stages/2-networking-a-peering/dns-prod.tf diff --git a/fast/stages/02-networking-peering/landing.tf b/fast/stages/2-networking-a-peering/landing.tf similarity index 100% rename from fast/stages/02-networking-peering/landing.tf rename to fast/stages/2-networking-a-peering/landing.tf diff --git a/fast/stages/02-networking-peering/main.tf b/fast/stages/2-networking-a-peering/main.tf similarity index 100% rename from fast/stages/02-networking-peering/main.tf rename to fast/stages/2-networking-a-peering/main.tf diff --git a/fast/stages/02-networking-nva/monitoring.tf b/fast/stages/2-networking-a-peering/monitoring.tf similarity index 100% rename from fast/stages/02-networking-nva/monitoring.tf rename to fast/stages/2-networking-a-peering/monitoring.tf diff --git a/fast/stages/02-networking-vpn/outputs.tf b/fast/stages/2-networking-a-peering/outputs.tf similarity index 96% rename from fast/stages/02-networking-vpn/outputs.tf rename to fast/stages/2-networking-a-peering/outputs.tf index 3b97b7f254..0e101f4123 100644 --- a/fast/stages/02-networking-vpn/outputs.tf +++ b/fast/stages/2-networking-a-peering/outputs.tf @@ -48,13 +48,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/02-networking.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/02-networking.auto.tfvars.json" + name = "tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/02-networking-peering/peerings.tf b/fast/stages/2-networking-a-peering/peerings.tf similarity index 100% rename from fast/stages/02-networking-peering/peerings.tf rename to fast/stages/2-networking-a-peering/peerings.tf diff --git a/fast/stages/02-networking-peering/spoke-dev.tf b/fast/stages/2-networking-a-peering/spoke-dev.tf similarity index 100% rename from fast/stages/02-networking-peering/spoke-dev.tf rename to fast/stages/2-networking-a-peering/spoke-dev.tf diff --git a/fast/stages/02-networking-peering/spoke-prod.tf b/fast/stages/2-networking-a-peering/spoke-prod.tf similarity index 100% rename from fast/stages/02-networking-peering/spoke-prod.tf rename to fast/stages/2-networking-a-peering/spoke-prod.tf diff --git a/fast/stages/02-networking-peering/test-resources.tf b/fast/stages/2-networking-a-peering/test-resources.tf similarity index 100% rename from fast/stages/02-networking-peering/test-resources.tf rename to fast/stages/2-networking-a-peering/test-resources.tf diff --git a/fast/stages/02-networking-peering/variables-peerings.tf b/fast/stages/2-networking-a-peering/variables-peerings.tf similarity index 100% rename from fast/stages/02-networking-peering/variables-peerings.tf rename to fast/stages/2-networking-a-peering/variables-peerings.tf diff --git a/fast/stages/02-networking-vpn/variables.tf b/fast/stages/2-networking-a-peering/variables.tf similarity index 91% rename from fast/stages/02-networking-vpn/variables.tf rename to fast/stages/2-networking-a-peering/variables.tf index faa9169877..fb2fd8745b 100644 --- a/fast/stages/02-networking-vpn/variables.tf +++ b/fast/stages/2-networking-a-peering/variables.tf @@ -15,7 +15,7 @@ */ variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -23,12 +23,16 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # 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 - organization_id = number + 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 "custom_adv" { @@ -49,7 +53,7 @@ variable "custom_adv" { } variable "custom_roles" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Custom roles defined at the org level, in key => id format." type = object({ service_project_network_admin = string @@ -72,7 +76,7 @@ variable "dns" { } variable "folder_ids" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created." type = object({ networking = string @@ -100,7 +104,7 @@ variable "l7ilb_subnets" { } variable "organization" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Organization details." type = object({ domain = string @@ -116,7 +120,7 @@ variable "outputs_location" { } variable "prefix" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string @@ -191,7 +195,7 @@ variable "router_onprem_configs" { } variable "service_accounts" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Automation service accounts in name => email format." type = object({ data-platform-dev = string diff --git a/fast/stages/02-networking-peering/vpn-onprem.tf b/fast/stages/2-networking-a-peering/vpn-onprem.tf similarity index 100% rename from fast/stages/02-networking-peering/vpn-onprem.tf rename to fast/stages/2-networking-a-peering/vpn-onprem.tf diff --git a/fast/stages/02-networking-separate-envs/.gitignore b/fast/stages/2-networking-b-vpn/.gitignore similarity index 100% rename from fast/stages/02-networking-separate-envs/.gitignore rename to fast/stages/2-networking-b-vpn/.gitignore diff --git a/fast/stages/02-networking-separate-envs/IAM.md b/fast/stages/2-networking-b-vpn/IAM.md similarity index 100% rename from fast/stages/02-networking-separate-envs/IAM.md rename to fast/stages/2-networking-b-vpn/IAM.md diff --git a/fast/stages/02-networking-vpn/README.md b/fast/stages/2-networking-b-vpn/README.md similarity index 94% rename from fast/stages/02-networking-vpn/README.md rename to fast/stages/2-networking-b-vpn/README.md index 047a1189ce..8dbea716cc 100644 --- a/fast/stages/02-networking-vpn/README.md +++ b/fast/stages/2-networking-b-vpn/README.md @@ -45,10 +45,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](../02-networking-peering/)) +- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (implemented by [02-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](../02-networking-nva/)) +- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [02-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 @@ -128,7 +128,7 @@ This configuration is battle-tested, and flexible enough to lend itself to simpl ## How to run this stage -This stage is meant to be executed after the [resman](../01-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../00-bootstrap) 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. @@ -136,7 +136,7 @@ Before running this stage, you need to make sure you have the correct credential ### 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](../01-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). +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. @@ -150,7 +150,7 @@ 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 ../01-resman +cd ../1-resman terraform output -json providers | jq -r '.["02-networking"]' \ > ../02-networking/providers.tf ``` @@ -320,23 +320,23 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L74) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | -| [organization](variables.tf#L102) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L118) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [custom_adv](variables.tf#L34) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | -| [custom_roles](variables.tf#L51) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [data_dir](variables.tf#L60) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | -| [dns](variables.tf#L66) | Onprem DNS resolvers. | map(list(string)) | | {…} | | -| [l7ilb_subnets](variables.tf#L84) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | -| [outputs_location](variables.tf#L112) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | -| [psa_ranges](variables.tf#L129) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | -| [region_trigram](variables.tf#L166) | Short names for GCP regions. | map(string) | | {…} | | -| [router_onprem_configs](variables.tf#L175) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | +| [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#L78) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | +| [organization](variables.tf#L106) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L122) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [custom_adv](variables.tf#L38) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | +| [custom_roles](variables.tf#L55) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [data_dir](variables.tf#L64) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | +| [dns](variables.tf#L70) | Onprem DNS resolvers. | map(list(string)) | | {…} | | +| [l7ilb_subnets](variables.tf#L88) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | +| [outputs_location](variables.tf#L116) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | +| [psa_ranges](variables.tf#L133) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | +| [region_trigram](variables.tf#L170) | Short names for GCP regions. | map(string) | | {…} | | +| [router_onprem_configs](variables.tf#L179) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | | [router_spoke_configs](variables-vpn.tf#L18) | Configurations for routers used for internal connectivity. | map(object({…})) | | {…} | | -| [service_accounts](variables.tf#L193) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L207) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [service_accounts](variables.tf#L197) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_configs](variables.tf#L211) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | | [vpn_spoke_configs](variables-vpn.tf#L37) | VPN gateway configuration for spokes. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-peering/data/cidrs.yaml b/fast/stages/2-networking-b-vpn/data/cidrs.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/cidrs.yaml rename to fast/stages/2-networking-b-vpn/data/cidrs.yaml diff --git a/fast/stages/02-networking-separate-envs/data/dashboards/firewall_insights.json b/fast/stages/2-networking-b-vpn/data/dashboards/firewall_insights.json similarity index 100% rename from fast/stages/02-networking-separate-envs/data/dashboards/firewall_insights.json rename to fast/stages/2-networking-b-vpn/data/dashboards/firewall_insights.json diff --git a/fast/stages/02-networking-separate-envs/data/dashboards/vpn.json b/fast/stages/2-networking-b-vpn/data/dashboards/vpn.json similarity index 100% rename from fast/stages/02-networking-separate-envs/data/dashboards/vpn.json rename to fast/stages/2-networking-b-vpn/data/dashboards/vpn.json diff --git a/fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml b/fast/stages/2-networking-b-vpn/data/firewall-rules/dev/rules.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml rename to fast/stages/2-networking-b-vpn/data/firewall-rules/dev/rules.yaml diff --git a/fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml b/fast/stages/2-networking-b-vpn/data/firewall-rules/landing/rules.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml rename to fast/stages/2-networking-b-vpn/data/firewall-rules/landing/rules.yaml diff --git a/fast/stages/02-networking-peering/data/hierarchical-policy-rules.yaml b/fast/stages/2-networking-b-vpn/data/hierarchical-policy-rules.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/hierarchical-policy-rules.yaml rename to fast/stages/2-networking-b-vpn/data/hierarchical-policy-rules.yaml diff --git a/fast/stages/02-networking-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml rename to fast/stages/2-networking-b-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml diff --git a/fast/stages/02-networking-separate-envs/data/subnets/dev/dev-default-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/subnets/dev/dev-default-ew1.yaml rename to fast/stages/2-networking-b-vpn/data/subnets/dev/dev-default-ew1.yaml diff --git a/fast/stages/02-networking-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml rename to fast/stages/2-networking-b-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml diff --git a/fast/stages/02-networking-vpn/data/subnets/landing/landing-default-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/landing/landing-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/subnets/landing/landing-default-ew1.yaml rename to fast/stages/2-networking-b-vpn/data/subnets/landing/landing-default-ew1.yaml diff --git a/fast/stages/02-networking-separate-envs/data/subnets/prod/prod-default-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/prod/prod-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/subnets/prod/prod-default-ew1.yaml rename to fast/stages/2-networking-b-vpn/data/subnets/prod/prod-default-ew1.yaml diff --git a/fast/stages/02-networking-vpn/diagram.png b/fast/stages/2-networking-b-vpn/diagram.png similarity index 100% rename from fast/stages/02-networking-vpn/diagram.png rename to fast/stages/2-networking-b-vpn/diagram.png diff --git a/fast/stages/02-networking-vpn/diagram.svg b/fast/stages/2-networking-b-vpn/diagram.svg similarity index 100% rename from fast/stages/02-networking-vpn/diagram.svg rename to fast/stages/2-networking-b-vpn/diagram.svg diff --git a/fast/stages/02-networking-vpn/dns-dev.tf b/fast/stages/2-networking-b-vpn/dns-dev.tf similarity index 100% rename from fast/stages/02-networking-vpn/dns-dev.tf rename to fast/stages/2-networking-b-vpn/dns-dev.tf diff --git a/fast/stages/02-networking-vpn/dns-landing.tf b/fast/stages/2-networking-b-vpn/dns-landing.tf similarity index 100% rename from fast/stages/02-networking-vpn/dns-landing.tf rename to fast/stages/2-networking-b-vpn/dns-landing.tf diff --git a/fast/stages/02-networking-vpn/dns-prod.tf b/fast/stages/2-networking-b-vpn/dns-prod.tf similarity index 100% rename from fast/stages/02-networking-vpn/dns-prod.tf rename to fast/stages/2-networking-b-vpn/dns-prod.tf diff --git a/fast/stages/02-networking-vpn/landing.tf b/fast/stages/2-networking-b-vpn/landing.tf similarity index 100% rename from fast/stages/02-networking-vpn/landing.tf rename to fast/stages/2-networking-b-vpn/landing.tf diff --git a/fast/stages/02-networking-vpn/main.tf b/fast/stages/2-networking-b-vpn/main.tf similarity index 100% rename from fast/stages/02-networking-vpn/main.tf rename to fast/stages/2-networking-b-vpn/main.tf diff --git a/fast/stages/02-networking-peering/monitoring.tf b/fast/stages/2-networking-b-vpn/monitoring.tf similarity index 100% rename from fast/stages/02-networking-peering/monitoring.tf rename to fast/stages/2-networking-b-vpn/monitoring.tf diff --git a/fast/stages/02-networking-peering/outputs.tf b/fast/stages/2-networking-b-vpn/outputs.tf similarity index 96% rename from fast/stages/02-networking-peering/outputs.tf rename to fast/stages/2-networking-b-vpn/outputs.tf index 3b97b7f254..0e101f4123 100644 --- a/fast/stages/02-networking-peering/outputs.tf +++ b/fast/stages/2-networking-b-vpn/outputs.tf @@ -48,13 +48,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/02-networking.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/02-networking.auto.tfvars.json" + name = "tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/02-networking-vpn/spoke-dev.tf b/fast/stages/2-networking-b-vpn/spoke-dev.tf similarity index 100% rename from fast/stages/02-networking-vpn/spoke-dev.tf rename to fast/stages/2-networking-b-vpn/spoke-dev.tf diff --git a/fast/stages/02-networking-vpn/spoke-prod.tf b/fast/stages/2-networking-b-vpn/spoke-prod.tf similarity index 100% rename from fast/stages/02-networking-vpn/spoke-prod.tf rename to fast/stages/2-networking-b-vpn/spoke-prod.tf diff --git a/fast/stages/02-networking-vpn/test-resources.tf b/fast/stages/2-networking-b-vpn/test-resources.tf similarity index 100% rename from fast/stages/02-networking-vpn/test-resources.tf rename to fast/stages/2-networking-b-vpn/test-resources.tf diff --git a/fast/stages/02-networking-vpn/variables-vpn.tf b/fast/stages/2-networking-b-vpn/variables-vpn.tf similarity index 100% rename from fast/stages/02-networking-vpn/variables-vpn.tf rename to fast/stages/2-networking-b-vpn/variables-vpn.tf diff --git a/fast/stages/02-networking-peering/variables.tf b/fast/stages/2-networking-b-vpn/variables.tf similarity index 91% rename from fast/stages/02-networking-peering/variables.tf rename to fast/stages/2-networking-b-vpn/variables.tf index faa9169877..fb2fd8745b 100644 --- a/fast/stages/02-networking-peering/variables.tf +++ b/fast/stages/2-networking-b-vpn/variables.tf @@ -15,7 +15,7 @@ */ variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -23,12 +23,16 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # 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 - organization_id = number + 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 "custom_adv" { @@ -49,7 +53,7 @@ variable "custom_adv" { } variable "custom_roles" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Custom roles defined at the org level, in key => id format." type = object({ service_project_network_admin = string @@ -72,7 +76,7 @@ variable "dns" { } variable "folder_ids" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created." type = object({ networking = string @@ -100,7 +104,7 @@ variable "l7ilb_subnets" { } variable "organization" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Organization details." type = object({ domain = string @@ -116,7 +120,7 @@ variable "outputs_location" { } variable "prefix" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string @@ -191,7 +195,7 @@ variable "router_onprem_configs" { } variable "service_accounts" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Automation service accounts in name => email format." type = object({ data-platform-dev = string diff --git a/fast/stages/02-networking-vpn/vpn-onprem.tf b/fast/stages/2-networking-b-vpn/vpn-onprem.tf similarity index 100% rename from fast/stages/02-networking-vpn/vpn-onprem.tf rename to fast/stages/2-networking-b-vpn/vpn-onprem.tf diff --git a/fast/stages/02-networking-vpn/vpn-spoke-dev.tf b/fast/stages/2-networking-b-vpn/vpn-spoke-dev.tf similarity index 100% rename from fast/stages/02-networking-vpn/vpn-spoke-dev.tf rename to fast/stages/2-networking-b-vpn/vpn-spoke-dev.tf diff --git a/fast/stages/02-networking-vpn/vpn-spoke-prod-ew1.tf b/fast/stages/2-networking-b-vpn/vpn-spoke-prod-ew1.tf similarity index 100% rename from fast/stages/02-networking-vpn/vpn-spoke-prod-ew1.tf rename to fast/stages/2-networking-b-vpn/vpn-spoke-prod-ew1.tf diff --git a/fast/stages/02-networking-vpn/vpn-spoke-prod-ew4.tf b/fast/stages/2-networking-b-vpn/vpn-spoke-prod-ew4.tf similarity index 100% rename from fast/stages/02-networking-vpn/vpn-spoke-prod-ew4.tf rename to fast/stages/2-networking-b-vpn/vpn-spoke-prod-ew4.tf diff --git a/fast/stages/02-networking-nva/README.md b/fast/stages/2-networking-c-nva/README.md similarity index 94% rename from fast/stages/02-networking-nva/README.md rename to fast/stages/2-networking-c-nva/README.md index 79c27e8de6..260d320f24 100644 --- a/fast/stages/02-networking-nva/README.md +++ b/fast/stages/2-networking-c-nva/README.md @@ -69,7 +69,7 @@ Internal connectivity (e.g. between the trusted landing VPC and the spokes) is r This is an options summary: -- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (used here to connect the trusted landing VPC with the spokes, also used by [02-networking-vpn](../02-networking-vpn/)) +- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (used here to connect the trusted landing VPC with the spokes, also used by [02-networking-vpn](../2-networking-b-vpn/)) - 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) (used here to connect the trusted landing and untrusted VPCs) @@ -192,7 +192,7 @@ This configuration is battle-tested, and flexible enough to lend itself to simpl ## How to run this stage -This stage is meant to be executed after the [resman](../01-resman) stage has run. It leverages the automation service account and the storage bucket created there, and additional resources configured in the [bootstrap](../00-bootstrap) 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. @@ -200,7 +200,7 @@ Before running this stage, you need to make sure you have the correct credential ### 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](../01-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). +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. @@ -214,9 +214,9 @@ 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 ../01-resman +cd ../1-resman terraform output -json providers | jq -r '.["02-networking"]' \ - > ../02-networking-nva/providers.tf + > ../2-networking-c-nva/providers.tf ``` ### Variable configuration @@ -371,23 +371,23 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L79) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | -| [organization](variables.tf#L115) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L131) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [custom_adv](variables.tf#L34) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | -| [custom_roles](variables.tf#L56) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [data_dir](variables.tf#L65) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | -| [dns](variables.tf#L71) | Onprem DNS resolvers. | map(list(string)) | | {…} | | -| [l7ilb_subnets](variables.tf#L89) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | -| [onprem_cidr](variables.tf#L107) | Onprem addresses in name => range format. | map(string) | | {…} | | -| [outputs_location](variables.tf#L125) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | -| [psa_ranges](variables.tf#L142) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | -| [region_trigram](variables.tf#L183) | Short names for GCP regions. | map(string) | | {…} | | -| [router_configs](variables.tf#L192) | Configurations for CRs and onprem routers. | map(object({…})) | | {…} | | -| [service_accounts](variables.tf#L215) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L229) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [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#L83) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | +| [organization](variables.tf#L119) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L135) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [custom_adv](variables.tf#L38) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | +| [custom_roles](variables.tf#L60) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [data_dir](variables.tf#L69) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | +| [dns](variables.tf#L75) | Onprem DNS resolvers. | map(list(string)) | | {…} | | +| [l7ilb_subnets](variables.tf#L93) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | +| [onprem_cidr](variables.tf#L111) | Onprem addresses in name => range format. | map(string) | | {…} | | +| [outputs_location](variables.tf#L129) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | +| [psa_ranges](variables.tf#L146) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | +| [region_trigram](variables.tf#L187) | Short names for GCP regions. | map(string) | | {…} | | +| [router_configs](variables.tf#L196) | Configurations for CRs and onprem routers. | map(object({…})) | | {…} | | +| [service_accounts](variables.tf#L219) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_configs](variables.tf#L233) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-separate-envs/data/cidrs.yaml b/fast/stages/2-networking-c-nva/data/cidrs.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/cidrs.yaml rename to fast/stages/2-networking-c-nva/data/cidrs.yaml diff --git a/fast/stages/02-networking-nva/data/dashboards/firewall_insights.json b/fast/stages/2-networking-c-nva/data/dashboards/firewall_insights.json similarity index 100% rename from fast/stages/02-networking-nva/data/dashboards/firewall_insights.json rename to fast/stages/2-networking-c-nva/data/dashboards/firewall_insights.json diff --git a/fast/stages/02-networking-nva/data/dashboards/vpn.json b/fast/stages/2-networking-c-nva/data/dashboards/vpn.json similarity index 100% rename from fast/stages/02-networking-nva/data/dashboards/vpn.json rename to fast/stages/2-networking-c-nva/data/dashboards/vpn.json diff --git a/fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml b/fast/stages/2-networking-c-nva/data/firewall-rules/dev/rules.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml rename to fast/stages/2-networking-c-nva/data/firewall-rules/dev/rules.yaml diff --git a/fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml b/fast/stages/2-networking-c-nva/data/firewall-rules/landing-trusted/rules.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml rename to fast/stages/2-networking-c-nva/data/firewall-rules/landing-trusted/rules.yaml diff --git a/fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml b/fast/stages/2-networking-c-nva/data/firewall-rules/landing-untrusted/rules.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml rename to fast/stages/2-networking-c-nva/data/firewall-rules/landing-untrusted/rules.yaml diff --git a/fast/stages/02-networking-separate-envs/data/hierarchical-policy-rules.yaml b/fast/stages/2-networking-c-nva/data/hierarchical-policy-rules.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/hierarchical-policy-rules.yaml rename to fast/stages/2-networking-c-nva/data/hierarchical-policy-rules.yaml diff --git a/fast/stages/02-networking-nva/data/nva-startup-script.tftpl b/fast/stages/2-networking-c-nva/data/nva-startup-script.tftpl similarity index 100% rename from fast/stages/02-networking-nva/data/nva-startup-script.tftpl rename to fast/stages/2-networking-c-nva/data/nva-startup-script.tftpl diff --git a/fast/stages/02-networking-nva/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-c-nva/data/subnets/dev/dev-dataplatform-ew1.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/dev/dev-dataplatform-ew1.yaml rename to fast/stages/2-networking-c-nva/data/subnets/dev/dev-dataplatform-ew1.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/dev/dev-default-ew1.yaml b/fast/stages/2-networking-c-nva/data/subnets/dev/dev-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/dev/dev-default-ew1.yaml rename to fast/stages/2-networking-c-nva/data/subnets/dev/dev-default-ew1.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/dev/dev-default-ew4.yaml b/fast/stages/2-networking-c-nva/data/subnets/dev/dev-default-ew4.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/dev/dev-default-ew4.yaml rename to fast/stages/2-networking-c-nva/data/subnets/dev/dev-default-ew4.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml b/fast/stages/2-networking-c-nva/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml rename to fast/stages/2-networking-c-nva/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml b/fast/stages/2-networking-c-nva/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml rename to fast/stages/2-networking-c-nva/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml b/fast/stages/2-networking-c-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml rename to fast/stages/2-networking-c-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml b/fast/stages/2-networking-c-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml rename to fast/stages/2-networking-c-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/prod/prod-default-ew1.yaml b/fast/stages/2-networking-c-nva/data/subnets/prod/prod-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/prod/prod-default-ew1.yaml rename to fast/stages/2-networking-c-nva/data/subnets/prod/prod-default-ew1.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/prod/prod-default-ew4.yaml b/fast/stages/2-networking-c-nva/data/subnets/prod/prod-default-ew4.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/prod/prod-default-ew4.yaml rename to fast/stages/2-networking-c-nva/data/subnets/prod/prod-default-ew4.yaml diff --git a/fast/stages/02-networking-nva/diagram.png b/fast/stages/2-networking-c-nva/diagram.png similarity index 100% rename from fast/stages/02-networking-nva/diagram.png rename to fast/stages/2-networking-c-nva/diagram.png diff --git a/fast/stages/02-networking-nva/diagram.svg b/fast/stages/2-networking-c-nva/diagram.svg similarity index 100% rename from fast/stages/02-networking-nva/diagram.svg rename to fast/stages/2-networking-c-nva/diagram.svg diff --git a/fast/stages/02-networking-nva/dns-dev.tf b/fast/stages/2-networking-c-nva/dns-dev.tf similarity index 100% rename from fast/stages/02-networking-nva/dns-dev.tf rename to fast/stages/2-networking-c-nva/dns-dev.tf diff --git a/fast/stages/02-networking-nva/dns-landing.tf b/fast/stages/2-networking-c-nva/dns-landing.tf similarity index 100% rename from fast/stages/02-networking-nva/dns-landing.tf rename to fast/stages/2-networking-c-nva/dns-landing.tf diff --git a/fast/stages/02-networking-nva/dns-prod.tf b/fast/stages/2-networking-c-nva/dns-prod.tf similarity index 100% rename from fast/stages/02-networking-nva/dns-prod.tf rename to fast/stages/2-networking-c-nva/dns-prod.tf diff --git a/fast/stages/02-networking-nva/landing.tf b/fast/stages/2-networking-c-nva/landing.tf similarity index 100% rename from fast/stages/02-networking-nva/landing.tf rename to fast/stages/2-networking-c-nva/landing.tf diff --git a/fast/stages/02-networking-nva/main.tf b/fast/stages/2-networking-c-nva/main.tf similarity index 100% rename from fast/stages/02-networking-nva/main.tf rename to fast/stages/2-networking-c-nva/main.tf diff --git a/fast/stages/02-networking-vpn/monitoring.tf b/fast/stages/2-networking-c-nva/monitoring.tf similarity index 100% rename from fast/stages/02-networking-vpn/monitoring.tf rename to fast/stages/2-networking-c-nva/monitoring.tf diff --git a/fast/stages/02-networking-nva/nva.tf b/fast/stages/2-networking-c-nva/nva.tf similarity index 100% rename from fast/stages/02-networking-nva/nva.tf rename to fast/stages/2-networking-c-nva/nva.tf diff --git a/fast/stages/02-networking-nva/outputs.tf b/fast/stages/2-networking-c-nva/outputs.tf similarity index 96% rename from fast/stages/02-networking-nva/outputs.tf rename to fast/stages/2-networking-c-nva/outputs.tf index df324570dd..97db168ee2 100644 --- a/fast/stages/02-networking-nva/outputs.tf +++ b/fast/stages/2-networking-c-nva/outputs.tf @@ -43,13 +43,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/02-networking.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/02-networking.auto.tfvars.json" + name = "tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/02-networking-nva/spoke-dev.tf b/fast/stages/2-networking-c-nva/spoke-dev.tf similarity index 100% rename from fast/stages/02-networking-nva/spoke-dev.tf rename to fast/stages/2-networking-c-nva/spoke-dev.tf diff --git a/fast/stages/02-networking-nva/spoke-prod.tf b/fast/stages/2-networking-c-nva/spoke-prod.tf similarity index 100% rename from fast/stages/02-networking-nva/spoke-prod.tf rename to fast/stages/2-networking-c-nva/spoke-prod.tf diff --git a/fast/stages/02-networking-nva/test-resources.tf b/fast/stages/2-networking-c-nva/test-resources.tf similarity index 100% rename from fast/stages/02-networking-nva/test-resources.tf rename to fast/stages/2-networking-c-nva/test-resources.tf diff --git a/fast/stages/02-networking-nva/variables.tf b/fast/stages/2-networking-c-nva/variables.tf similarity index 93% rename from fast/stages/02-networking-nva/variables.tf rename to fast/stages/2-networking-c-nva/variables.tf index ea6c0bf0d7..c6516314ba 100644 --- a/fast/stages/02-networking-nva/variables.tf +++ b/fast/stages/2-networking-c-nva/variables.tf @@ -15,7 +15,7 @@ */ variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -23,12 +23,16 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # 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 - organization_id = number + 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 "custom_adv" { @@ -54,7 +58,7 @@ variable "custom_adv" { } variable "custom_roles" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Custom roles defined at the org level, in key => id format." type = object({ service_project_network_admin = string @@ -77,7 +81,7 @@ variable "dns" { } variable "folder_ids" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created." type = object({ networking = string @@ -113,7 +117,7 @@ variable "onprem_cidr" { } variable "organization" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Organization details." type = object({ domain = string @@ -129,7 +133,7 @@ variable "outputs_location" { } variable "prefix" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string @@ -213,7 +217,7 @@ variable "router_configs" { } variable "service_accounts" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Automation service accounts in name => email format." type = object({ data-platform-dev = string diff --git a/fast/stages/02-networking-nva/vpn-onprem.tf b/fast/stages/2-networking-c-nva/vpn-onprem.tf similarity index 100% rename from fast/stages/02-networking-nva/vpn-onprem.tf rename to fast/stages/2-networking-c-nva/vpn-onprem.tf diff --git a/fast/stages/02-networking-vpn/.gitignore b/fast/stages/2-networking-d-separate-envs/.gitignore similarity index 100% rename from fast/stages/02-networking-vpn/.gitignore rename to fast/stages/2-networking-d-separate-envs/.gitignore diff --git a/fast/stages/02-networking-vpn/IAM.md b/fast/stages/2-networking-d-separate-envs/IAM.md similarity index 100% rename from fast/stages/02-networking-vpn/IAM.md rename to fast/stages/2-networking-d-separate-envs/IAM.md diff --git a/fast/stages/02-networking-separate-envs/README.md b/fast/stages/2-networking-d-separate-envs/README.md similarity index 92% rename from fast/stages/02-networking-separate-envs/README.md rename to fast/stages/2-networking-d-separate-envs/README.md index 66b31646ec..ad36ca3a1b 100644 --- a/fast/stages/02-networking-separate-envs/README.md +++ b/fast/stages/2-networking-d-separate-envs/README.md @@ -89,7 +89,7 @@ This configuration is battle-tested, and flexible enough to lend itself to simpl ## How to run this stage -This stage is meant to be executed after the [resman](../01-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../00-bootstrap) 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. @@ -97,7 +97,7 @@ Before running this stage, you need to make sure you have the correct credential ### 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](../01-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). +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. @@ -111,7 +111,7 @@ 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 ../01-resman +cd ../1-resman terraform output -json providers | jq -r '.["02-networking"]' \ > ../02-networking/providers.tf ``` @@ -132,7 +132,7 @@ If you have set a valid value for `outputs_location` in the bootstrap and in the 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 ../00-bootstrap/terraform.tfvars . +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. @@ -238,21 +238,21 @@ You're now ready to run `terraform init` and `apply`. | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L74) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | -| [organization](variables.tf#L102) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L118) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [custom_adv](variables.tf#L34) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | -| [custom_roles](variables.tf#L50) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [data_dir](variables.tf#L59) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | -| [dns](variables.tf#L65) | Onprem DNS resolvers. | map(list(string)) | | {…} | | -| [l7ilb_subnets](variables.tf#L84) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | -| [outputs_location](variables.tf#L112) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | -| [psa_ranges](variables.tf#L129) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | -| [router_onprem_configs](variables.tf#L166) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | -| [service_accounts](variables.tf#L189) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L201) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [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#L78) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | +| [organization](variables.tf#L106) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L122) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [custom_adv](variables.tf#L38) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | +| [custom_roles](variables.tf#L54) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [data_dir](variables.tf#L63) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | +| [dns](variables.tf#L69) | Onprem DNS resolvers. | map(list(string)) | | {…} | | +| [l7ilb_subnets](variables.tf#L88) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | +| [outputs_location](variables.tf#L116) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | +| [psa_ranges](variables.tf#L133) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | +| [router_onprem_configs](variables.tf#L170) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | +| [service_accounts](variables.tf#L193) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_configs](variables.tf#L205) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-vpn/data/cidrs.yaml b/fast/stages/2-networking-d-separate-envs/data/cidrs.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/cidrs.yaml rename to fast/stages/2-networking-d-separate-envs/data/cidrs.yaml diff --git a/fast/stages/02-networking-vpn/data/dashboards/firewall_insights.json b/fast/stages/2-networking-d-separate-envs/data/dashboards/firewall_insights.json similarity index 100% rename from fast/stages/02-networking-vpn/data/dashboards/firewall_insights.json rename to fast/stages/2-networking-d-separate-envs/data/dashboards/firewall_insights.json diff --git a/fast/stages/02-networking-vpn/data/dashboards/vpn.json b/fast/stages/2-networking-d-separate-envs/data/dashboards/vpn.json similarity index 100% rename from fast/stages/02-networking-vpn/data/dashboards/vpn.json rename to fast/stages/2-networking-d-separate-envs/data/dashboards/vpn.json diff --git a/fast/stages/02-networking-separate-envs/data/firewall-rules/dev/rules.yaml b/fast/stages/2-networking-d-separate-envs/data/firewall-rules/dev/rules.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/firewall-rules/dev/rules.yaml rename to fast/stages/2-networking-d-separate-envs/data/firewall-rules/dev/rules.yaml diff --git a/fast/stages/02-networking-vpn/data/hierarchical-policy-rules.yaml b/fast/stages/2-networking-d-separate-envs/data/hierarchical-policy-rules.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/hierarchical-policy-rules.yaml rename to fast/stages/2-networking-d-separate-envs/data/hierarchical-policy-rules.yaml diff --git a/fast/stages/02-networking-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml rename to fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml diff --git a/fast/stages/02-networking-vpn/data/subnets/dev/dev-default-ew1.yaml b/fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/subnets/dev/dev-default-ew1.yaml rename to fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-default-ew1.yaml diff --git a/fast/stages/02-networking-vpn/data/subnets/prod/prod-default-ew1.yaml b/fast/stages/2-networking-d-separate-envs/data/subnets/prod/prod-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/subnets/prod/prod-default-ew1.yaml rename to fast/stages/2-networking-d-separate-envs/data/subnets/prod/prod-default-ew1.yaml diff --git a/fast/stages/02-networking-separate-envs/diagram.png b/fast/stages/2-networking-d-separate-envs/diagram.png similarity index 100% rename from fast/stages/02-networking-separate-envs/diagram.png rename to fast/stages/2-networking-d-separate-envs/diagram.png diff --git a/fast/stages/02-networking-separate-envs/diagram.svg b/fast/stages/2-networking-d-separate-envs/diagram.svg similarity index 100% rename from fast/stages/02-networking-separate-envs/diagram.svg rename to fast/stages/2-networking-d-separate-envs/diagram.svg diff --git a/fast/stages/02-networking-separate-envs/dns-dev.tf b/fast/stages/2-networking-d-separate-envs/dns-dev.tf similarity index 100% rename from fast/stages/02-networking-separate-envs/dns-dev.tf rename to fast/stages/2-networking-d-separate-envs/dns-dev.tf diff --git a/fast/stages/02-networking-separate-envs/dns-prod.tf b/fast/stages/2-networking-d-separate-envs/dns-prod.tf similarity index 100% rename from fast/stages/02-networking-separate-envs/dns-prod.tf rename to fast/stages/2-networking-d-separate-envs/dns-prod.tf diff --git a/fast/stages/02-networking-separate-envs/main.tf b/fast/stages/2-networking-d-separate-envs/main.tf similarity index 100% rename from fast/stages/02-networking-separate-envs/main.tf rename to fast/stages/2-networking-d-separate-envs/main.tf diff --git a/fast/stages/02-networking-separate-envs/monitoring.tf b/fast/stages/2-networking-d-separate-envs/monitoring.tf similarity index 100% rename from fast/stages/02-networking-separate-envs/monitoring.tf rename to fast/stages/2-networking-d-separate-envs/monitoring.tf diff --git a/fast/stages/02-networking-separate-envs/outputs.tf b/fast/stages/2-networking-d-separate-envs/outputs.tf similarity index 97% rename from fast/stages/02-networking-separate-envs/outputs.tf rename to fast/stages/2-networking-d-separate-envs/outputs.tf index d06d499d63..97b0dc059e 100644 --- a/fast/stages/02-networking-separate-envs/outputs.tf +++ b/fast/stages/2-networking-d-separate-envs/outputs.tf @@ -44,13 +44,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/02-networking.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/02-networking.auto.tfvars.json" + name = "tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/02-networking-separate-envs/spoke-dev.tf b/fast/stages/2-networking-d-separate-envs/spoke-dev.tf similarity index 100% rename from fast/stages/02-networking-separate-envs/spoke-dev.tf rename to fast/stages/2-networking-d-separate-envs/spoke-dev.tf diff --git a/fast/stages/02-networking-separate-envs/spoke-prod.tf b/fast/stages/2-networking-d-separate-envs/spoke-prod.tf similarity index 100% rename from fast/stages/02-networking-separate-envs/spoke-prod.tf rename to fast/stages/2-networking-d-separate-envs/spoke-prod.tf diff --git a/fast/stages/02-networking-separate-envs/test-resources.tf b/fast/stages/2-networking-d-separate-envs/test-resources.tf similarity index 100% rename from fast/stages/02-networking-separate-envs/test-resources.tf rename to fast/stages/2-networking-d-separate-envs/test-resources.tf diff --git a/fast/stages/02-networking-separate-envs/variables.tf b/fast/stages/2-networking-d-separate-envs/variables.tf similarity index 92% rename from fast/stages/02-networking-separate-envs/variables.tf rename to fast/stages/2-networking-d-separate-envs/variables.tf index 019d0b2ed4..61262a4215 100644 --- a/fast/stages/02-networking-separate-envs/variables.tf +++ b/fast/stages/2-networking-d-separate-envs/variables.tf @@ -15,7 +15,7 @@ */ variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -23,12 +23,16 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # 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 - organization_id = number + 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 "custom_adv" { @@ -48,7 +52,7 @@ variable "custom_adv" { } variable "custom_roles" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Custom roles defined at the org level, in key => id format." type = object({ service_project_network_admin = string @@ -72,7 +76,7 @@ variable "dns" { } variable "folder_ids" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created." type = object({ networking = string @@ -100,7 +104,7 @@ variable "l7ilb_subnets" { } variable "organization" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Organization details." type = object({ domain = string @@ -116,7 +120,7 @@ variable "outputs_location" { } variable "prefix" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string @@ -187,7 +191,7 @@ variable "router_onprem_configs" { } variable "service_accounts" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Automation service accounts in name => email format." type = object({ data-platform-dev = string diff --git a/fast/stages/02-networking-separate-envs/vpn-onprem-dev.tf b/fast/stages/2-networking-d-separate-envs/vpn-onprem-dev.tf similarity index 100% rename from fast/stages/02-networking-separate-envs/vpn-onprem-dev.tf rename to fast/stages/2-networking-d-separate-envs/vpn-onprem-dev.tf diff --git a/fast/stages/02-networking-separate-envs/vpn-onprem-prod.tf b/fast/stages/2-networking-d-separate-envs/vpn-onprem-prod.tf similarity index 100% rename from fast/stages/02-networking-separate-envs/vpn-onprem-prod.tf rename to fast/stages/2-networking-d-separate-envs/vpn-onprem-prod.tf diff --git a/fast/stages/02-security/IAM.md b/fast/stages/2-security/IAM.md similarity index 100% rename from fast/stages/02-security/IAM.md rename to fast/stages/2-security/IAM.md diff --git a/fast/stages/02-security/README.md b/fast/stages/2-security/README.md similarity index 92% rename from fast/stages/02-security/README.md rename to fast/stages/2-security/README.md index 024ababbfe..a609cd8144 100644 --- a/fast/stages/02-security/README.md +++ b/fast/stages/2-security/README.md @@ -42,7 +42,7 @@ 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](../01-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 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. 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. @@ -64,7 +64,7 @@ ln -s ~/fast-config/providers/02-security-providers.tf . If you have not configured `outputs_location` in resource management, you can derive the providers file from that stage's outputs: ```bash -cd ../01-resman +cd ../1-resman terraform output -json providers | jq -r '.["02-security"]' \ > ../02-security/providers.tf ``` @@ -85,7 +85,7 @@ If you configured a valid path for `outputs_location` in the previous stages, si 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 ../00-bootstrap/terraform.tfvars . +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. @@ -262,20 +262,20 @@ Some references that might be useful in setting up this stage: | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L34) | Folder name => id mappings, the 'security' folder name must exist. | object({…}) | ✓ | | 01-resman | -| [organization](variables.tf#L80) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L96) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [service_accounts](variables.tf#L107) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | object({…}) | ✓ | | 01-resman | -| [groups](variables.tf#L42) | Group names to grant organization-level permissions. | map(string) | | {…} | 00-bootstrap | -| [kms_defaults](variables.tf#L57) | Defaults used for KMS keys. | object({…}) | | {…} | | -| [kms_keys](variables.tf#L69) | KMS keys to create, keyed by name. Null attributes will be interpolated with defaults. | map(object({…})) | | {} | | -| [outputs_location](variables.tf#L90) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [vpc_sc_access_levels](variables.tf#L118) | VPC SC access level definitions. | map(object({…})) | | {} | | -| [vpc_sc_egress_policies](variables.tf#L147) | VPC SC egress policy defnitions. | map(object({…})) | | {} | | -| [vpc_sc_ingress_policies](variables.tf#L167) | VPC SC ingress policy defnitions. | map(object({…})) | | {} | | -| [vpc_sc_perimeters](variables.tf#L188) | VPC SC regular perimeter definitions. | object({…}) | | {} | | +| [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#L38) | Folder name => id mappings, the 'security' folder name must exist. | object({…}) | ✓ | | 1-resman | +| [organization](variables.tf#L84) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L100) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [service_accounts](variables.tf#L111) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | object({…}) | ✓ | | 1-resman | +| [groups](variables.tf#L46) | Group names to grant organization-level permissions. | map(string) | | {…} | 0-bootstrap | +| [kms_defaults](variables.tf#L61) | Defaults used for KMS keys. | object({…}) | | {…} | | +| [kms_keys](variables.tf#L73) | KMS keys to create, keyed by name. Null attributes will be interpolated with defaults. | map(object({…})) | | {} | | +| [outputs_location](variables.tf#L94) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| [vpc_sc_access_levels](variables.tf#L122) | VPC SC access level definitions. | map(object({…})) | | {} | | +| [vpc_sc_egress_policies](variables.tf#L151) | VPC SC egress policy defnitions. | map(object({…})) | | {} | | +| [vpc_sc_ingress_policies](variables.tf#L171) | VPC SC ingress policy defnitions. | map(object({…})) | | {} | | +| [vpc_sc_perimeters](variables.tf#L192) | VPC SC regular perimeter definitions. | object({…}) | | {} | | ## Outputs diff --git a/fast/stages/02-security/core-dev.tf b/fast/stages/2-security/core-dev.tf similarity index 100% rename from fast/stages/02-security/core-dev.tf rename to fast/stages/2-security/core-dev.tf diff --git a/fast/stages/02-security/core-prod.tf b/fast/stages/2-security/core-prod.tf similarity index 100% rename from fast/stages/02-security/core-prod.tf rename to fast/stages/2-security/core-prod.tf diff --git a/fast/stages/02-security/diagram.png b/fast/stages/2-security/diagram.png similarity index 100% rename from fast/stages/02-security/diagram.png rename to fast/stages/2-security/diagram.png diff --git a/fast/stages/02-security/diagram.svg b/fast/stages/2-security/diagram.svg similarity index 100% rename from fast/stages/02-security/diagram.svg rename to fast/stages/2-security/diagram.svg diff --git a/fast/stages/02-security/main.tf b/fast/stages/2-security/main.tf similarity index 100% rename from fast/stages/02-security/main.tf rename to fast/stages/2-security/main.tf diff --git a/fast/stages/02-security/outputs.tf b/fast/stages/2-security/outputs.tf similarity index 96% rename from fast/stages/02-security/outputs.tf rename to fast/stages/2-security/outputs.tf index b7e42e4923..ff0c13eda8 100644 --- a/fast/stages/02-security/outputs.tf +++ b/fast/stages/2-security/outputs.tf @@ -44,13 +44,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/tfvars/02-security.auto.tfvars.json" + filename = "${pathexpand(var.outputs_location)}/tfvars/2-security.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/02-security.auto.tfvars.json" + name = "tfvars/2-security.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/02-security/variables.tf b/fast/stages/2-security/variables.tf similarity index 91% rename from fast/stages/02-security/variables.tf rename to fast/stages/2-security/variables.tf index 349589c964..e14d637635 100644 --- a/fast/stages/02-security/variables.tf +++ b/fast/stages/2-security/variables.tf @@ -15,7 +15,7 @@ */ variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -23,16 +23,20 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # 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 - organization_id = number + 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 "folder_ids" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Folder name => id mappings, the 'security' folder name must exist." type = object({ security = string @@ -40,7 +44,7 @@ variable "folder_ids" { } variable "groups" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Group names to grant organization-level permissions." type = map(string) # https://cloud.google.com/docs/enterprise/setup-checklist @@ -78,7 +82,7 @@ variable "kms_keys" { } variable "organization" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Organization details." type = object({ domain = string @@ -94,7 +98,7 @@ variable "outputs_location" { } variable "prefix" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string @@ -105,7 +109,7 @@ variable "prefix" { } variable "service_accounts" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Automation service accounts that can assign the encrypt/decrypt roles on keys." type = object({ data-platform-dev = string diff --git a/fast/stages/02-security/vpc-sc-restricted-services.yaml b/fast/stages/2-security/vpc-sc-restricted-services.yaml similarity index 100% rename from fast/stages/02-security/vpc-sc-restricted-services.yaml rename to fast/stages/2-security/vpc-sc-restricted-services.yaml diff --git a/fast/stages/02-security/vpc-sc.tf b/fast/stages/2-security/vpc-sc.tf similarity index 100% rename from fast/stages/02-security/vpc-sc.tf rename to fast/stages/2-security/vpc-sc.tf diff --git a/fast/stages/03-data-platform/README.md b/fast/stages/3-data-platform/README.md similarity index 100% rename from fast/stages/03-data-platform/README.md rename to fast/stages/3-data-platform/README.md diff --git a/fast/stages/03-data-platform/dev/IAM.md b/fast/stages/3-data-platform/dev/IAM.md similarity index 100% rename from fast/stages/03-data-platform/dev/IAM.md rename to fast/stages/3-data-platform/dev/IAM.md diff --git a/fast/stages/03-data-platform/dev/README.md b/fast/stages/3-data-platform/dev/README.md similarity index 82% rename from fast/stages/03-data-platform/dev/README.md rename to fast/stages/3-data-platform/dev/README.md index 35f9bf1c51..615dbde8b0 100644 --- a/fast/stages/03-data-platform/dev/README.md +++ b/fast/stages/3-data-platform/dev/README.md @@ -42,11 +42,11 @@ As per our GCP best practices the Data Platform relies on user groups to assign ### Network -A Shared VPC is used here, either from one of the FAST networking stages (e.g. [hub and spoke via VPN](../../02-networking-vpn)) or from an external source. +A Shared VPC is used here, either from one of the FAST networking stages (e.g. [hub and spoke via VPN](../../2-networking-b-vpn)) or from an external source. ### Encryption -Cloud KMS crypto keys can be configured wither from the [FAST security stage](../../02-security) or from an external source. This step is optional and depends on customer policies and security best practices. +Cloud KMS crypto keys can be configured wither from the [FAST security stage](../../2-security) or from an external source. This step is optional and depends on customer policies and security best practices. To configure the use of Cloud KMS on resources, you have to specify the key id on the `service_encryption_keys` variable. Key locations should match resource locations. @@ -55,19 +55,20 @@ To configure the use of Cloud KMS on resources, you have to specify the key id o [Data Catalog](https://cloud.google.com/data-catalog) helps you to document your data entry at scale. Data Catalog relies on [tags](https://cloud.google.com/data-catalog/docs/tags-and-tag-templates#tags) and [tag template](https://cloud.google.com/data-catalog/docs/tags-and-tag-templates#tag-templates) to manage metadata for all data entries in a unified and centralized service. To implement [column-level security](https://cloud.google.com/bigquery/docs/column-level-security-intro) on BigQuery, we suggest to use `Tags` and `Tag templates`. The default configuration will implement 3 tags: - - `3_Confidential`: policy tag for columns that include very sensitive information, such as credit card numbers. - - `2_Private`: policy tag for columns that include sensitive personal identifiable information (PII) information, such as a person's first name. - - `1_Sensitive`: policy tag for columns that include data that cannot be made public, such as the credit limit. + +- `3_Confidential`: policy tag for columns that include very sensitive information, such as credit card numbers. +- `2_Private`: policy tag for columns that include sensitive personal identifiable information (PII) information, such as a person's first name. +- `1_Sensitive`: policy tag for columns that include data that cannot be made public, such as the credit limit. Anything that is not tagged is available to all users who have access to the data warehouse. -You can configure your tags and roles associated by configuring the `data_catalog_tags` variable. We suggest useing the "[Best practices for using policy tags in BigQuery](https://cloud.google.com/bigquery/docs/best-practices-policy-tags)" article as a guide to designing your tags structure and access pattern. By default, no groups has access to tagged data. +You can configure your tags and roles associated by configuring the `data_catalog_tags` variable. We suggest useing the "[Best practices for using policy tags in BigQuery](https://cloud.google.com/bigquery/docs/best-practices-policy-tags)" article as a guide to designing your tags structure and access pattern. By default, no groups has access to tagged data. ### VPC-SC -As is often the case in real-world configurations, [VPC-SC](https://cloud.google.com/vpc-service-controls) is needed to mitigate data exfiltration. VPC-SC can be configured from the [FAST security stage](../../02-security). This step is optional, but highly recomended, and depends on customer policies and security best practices. +As is often the case in real-world configurations, [VPC-SC](https://cloud.google.com/vpc-service-controls) is needed to mitigate data exfiltration. VPC-SC can be configured from the [FAST security stage](../../2-security). This step is optional, but highly recomended, and depends on customer policies and security best practices. -To configure the use of VPC-SC on the data platform, you have to specify the data platform project numbers on the `vpc_sc_perimeter_projects.dev` variable on [FAST security stage](../../02-security#perimeter-resources). +To configure the use of VPC-SC on the data platform, you have to specify the data platform project numbers on the `vpc_sc_perimeter_projects.dev` variable on [FAST security stage](../../2-security#perimeter-resources). In the case your Data Warehouse need to handle confidential data and you have the requirement to separate them deeply from other data and IAM is not enough, the suggested configuration is to keep the confidential project in a separate VPC-SC perimeter with the adequate ingress/egress rules needed for the load and tranformation service account. Below you can find an high level diagram describing the configuration. @@ -77,7 +78,7 @@ 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`](../../00-bootstrap), [`01-resman`](../../01-resman), [`02-networking`](../../02-networking-vpn) and [`02-security`](../../02-security)). +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)). When running in isolation, the following roles are needed on the principal used to apply Terraform: @@ -111,9 +112,9 @@ 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 ../../01-resman +cd ../../1-resman terraform output -json providers | jq -r '.["03-data-platform-dev"]' \ - > ../03-data-platform/dev/providers.tf + > ../3-data-platform/dev/providers.tf ``` ### Variable configuration @@ -133,7 +134,7 @@ 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 ../../00-bootstrap/terraform.tfvars . +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. @@ -147,7 +148,7 @@ 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. +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. You can find examples in the `[demo](../../../../blueprints/data-solutions/data-platform-foundations/demo)` folder. @@ -166,24 +167,24 @@ 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({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-globals | -| [folder_ids](variables.tf#L98) | Folder to be used for the networking resources in folders/nnnn format. | object({…}) | ✓ | | 01-resman | -| [host_project_ids](variables.tf#L116) | Shared VPC project ids. | object({…}) | ✓ | | 02-networking | -| [organization](variables.tf#L146) | Organization details. | object({…}) | ✓ | | 00-globals | -| [prefix](variables.tf#L162) | Unique prefix used for resource names. Not used for projects if 'project_create' is null. | string | ✓ | | 00-globals | -| [composer_config](variables.tf#L34) | Cloud Composer configuration options. | object({…}) | | {…} | | -| [data_catalog_tags](variables.tf#L81) | 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#L92) | Flag to set 'force_destroy' on data services like BigQery or Cloud Storage. | bool | | false | | -| [groups](variables.tf#L106) | Groups. | map(string) | | {…} | | -| [location](variables.tf#L124) | Location used for multi-regional resources. | string | | "eu" | | -| [network_config_composer](variables.tf#L130) | Network configurations to use for Composer. | object({…}) | | {…} | | -| [outputs_location](variables.tf#L156) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [project_services](variables.tf#L168) | List of core services enabled on all projects. | list(string) | | […] | | -| [region](variables.tf#L179) | Region used for regional resources. | string | | "europe-west1" | | -| [service_encryption_keys](variables.tf#L185) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | -| [subnet_self_links](variables.tf#L197) | Shared VPC subnet self links. | object({…}) | | null | 02-networking | -| [vpc_self_links](variables.tf#L206) | Shared VPC self links. | object({…}) | | null | 02-networking | +| [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 | +| [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 diff --git a/fast/stages/03-data-platform/dev/demo b/fast/stages/3-data-platform/dev/demo similarity index 100% rename from fast/stages/03-data-platform/dev/demo rename to fast/stages/3-data-platform/dev/demo diff --git a/fast/stages/03-data-platform/dev/diagram.png b/fast/stages/3-data-platform/dev/diagram.png similarity index 100% rename from fast/stages/03-data-platform/dev/diagram.png rename to fast/stages/3-data-platform/dev/diagram.png diff --git a/fast/stages/03-data-platform/dev/diagram_vpcsc.png b/fast/stages/3-data-platform/dev/diagram_vpcsc.png similarity index 100% rename from fast/stages/03-data-platform/dev/diagram_vpcsc.png rename to fast/stages/3-data-platform/dev/diagram_vpcsc.png diff --git a/fast/stages/03-data-platform/dev/main.tf b/fast/stages/3-data-platform/dev/main.tf similarity index 100% rename from fast/stages/03-data-platform/dev/main.tf rename to fast/stages/3-data-platform/dev/main.tf diff --git a/fast/stages/03-data-platform/dev/outputs.tf b/fast/stages/3-data-platform/dev/outputs.tf similarity index 95% rename from fast/stages/03-data-platform/dev/outputs.tf rename to fast/stages/3-data-platform/dev/outputs.tf index d0f79358cb..2eb813b4d1 100644 --- a/fast/stages/03-data-platform/dev/outputs.tf +++ b/fast/stages/3-data-platform/dev/outputs.tf @@ -27,13 +27,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/tfvars/03-data-platform-dev.auto.tfvars.json" + filename = "${pathexpand(var.outputs_location)}/tfvars/3-data-platform-dev.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/03-data-platform-dev.auto.tfvars.json" + name = "tfvars/3-data-platform-dev.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/03-data-platform/dev/variables.tf b/fast/stages/3-data-platform/dev/variables.tf similarity index 90% rename from fast/stages/03-data-platform/dev/variables.tf rename to fast/stages/3-data-platform/dev/variables.tf index 29dd1e45a4..74a5dbe11d 100644 --- a/fast/stages/03-data-platform/dev/variables.tf +++ b/fast/stages/3-data-platform/dev/variables.tf @@ -15,7 +15,7 @@ # tfdoc:file:description Terraform Variables. variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -23,12 +23,16 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-globals - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # 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 - organization_id = number + 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" { @@ -96,7 +100,7 @@ variable "data_force_destroy" { } variable "folder_ids" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Folder to be used for the networking resources in folders/nnnn format." type = object({ data-platform-dev = string @@ -114,7 +118,7 @@ variable "groups" { } variable "host_project_ids" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Shared VPC project ids." type = object({ dev-spoke-0 = string @@ -195,7 +199,7 @@ variable "service_encryption_keys" { } variable "subnet_self_links" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Shared VPC subnet self links." type = object({ dev-spoke-0 = map(string) @@ -204,7 +208,7 @@ variable "subnet_self_links" { } variable "vpc_self_links" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Shared VPC self links." type = object({ dev-spoke-0 = string diff --git a/fast/stages/03-gke-multitenant/README.md b/fast/stages/3-gke-multitenant/README.md similarity index 71% rename from fast/stages/03-gke-multitenant/README.md rename to fast/stages/3-gke-multitenant/README.md index f08910c834..9f9d9498e8 100644 --- a/fast/stages/03-gke-multitenant/README.md +++ b/fast/stages/3-gke-multitenant/README.md @@ -2,7 +2,7 @@ This directory contains a stage that can be used to centralize management of GKE multinenant clusters. -The Terraform code follows the same general approach used for the [project factory](../03-project-factory/) and [data platform](../03-data-platform/) stages, where a "fat module" contains the stage code and is used by thin code wrappers that localize it for each environment or specialized configuration: +The Terraform code follows the same general approach used for the [project factory](../3-project-factory/) and [data platform](../3-data-platform/) stages, where a "fat module" contains the stage code and is used by thin code wrappers that localize it for each environment or specialized configuration: The [`dev` folder](./dev/) contains an example setup for a generic development environment, and can be used as-is or cloned to implement other environments, or more specialized setups diff --git a/fast/stages/03-gke-multitenant/dev/README.md b/fast/stages/3-gke-multitenant/dev/README.md similarity index 90% rename from fast/stages/03-gke-multitenant/dev/README.md rename to fast/stages/3-gke-multitenant/dev/README.md index c446fbcb4a..4accf8e1aa 100644 --- a/fast/stages/03-gke-multitenant/dev/README.md +++ b/fast/stages/3-gke-multitenant/dev/README.md @@ -39,7 +39,7 @@ 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`](../../00-bootstrap), [`01-resman`](../../01-resman), 02-networking (either [VPN](../../02-networking-vpn) or [NVA](../../02-networking-nva)) and [`02-security`](../../02-security)) have been run. +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. 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: @@ -140,23 +140,23 @@ terraform apply | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L21) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L29) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L149) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | -| [host_project_ids](variables.tf#L164) | Host project for the shared VPC. | object({…}) | ✓ | | 02-networking | -| [prefix](variables.tf#L213) | Prefix used for resources that need unique names. | string | ✓ | | | -| [vpc_self_links](variables.tf#L225) | Self link for the shared VPC. | object({…}) | ✓ | | 02-networking | -| [clusters](variables.tf#L38) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} | | -| [fleet_configmanagement_clusters](variables.tf#L86) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | | -| [fleet_configmanagement_templates](variables.tf#L94) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | | -| [fleet_features](variables.tf#L129) | Enable and configue fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | | -| [fleet_workload_identity](variables.tf#L142) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | | -| [group_iam](variables.tf#L157) | Project-level authoritative IAM bindings for groups in {GROUP_EMAIL => [ROLES]} format. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | | -| [iam](variables.tf#L172) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | -| [labels](variables.tf#L179) | Project-level labels. | map(string) | | {} | | -| [nodepools](variables.tf#L185) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | | -| [outputs_location](variables.tf#L207) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [project_services](variables.tf#L218) | Additional project services to enable. | list(string) | | [] | | +| [automation](variables.tf#L21) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | +| [billing_account](variables.tf#L29) | 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#L153) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | +| [host_project_ids](variables.tf#L168) | Host project for the shared VPC. | object({…}) | ✓ | | 2-networking | +| [prefix](variables.tf#L217) | Prefix used for resources that need unique names. | string | ✓ | | | +| [vpc_self_links](variables.tf#L229) | Self link for the shared VPC. | object({…}) | ✓ | | 2-networking | +| [clusters](variables.tf#L42) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} | | +| [fleet_configmanagement_clusters](variables.tf#L90) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | | +| [fleet_configmanagement_templates](variables.tf#L98) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | | +| [fleet_features](variables.tf#L133) | Enable and configue fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | | +| [fleet_workload_identity](variables.tf#L146) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | | +| [group_iam](variables.tf#L161) | Project-level authoritative IAM bindings for groups in {GROUP_EMAIL => [ROLES]} format. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | | +| [iam](variables.tf#L176) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | +| [labels](variables.tf#L183) | Project-level labels. | map(string) | | {} | | +| [nodepools](variables.tf#L189) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | | +| [outputs_location](variables.tf#L211) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| [project_services](variables.tf#L222) | Additional project services to enable. | list(string) | | [] | | ## Outputs diff --git a/fast/stages/03-gke-multitenant/dev/diagram.png b/fast/stages/3-gke-multitenant/dev/diagram.png similarity index 100% rename from fast/stages/03-gke-multitenant/dev/diagram.png rename to fast/stages/3-gke-multitenant/dev/diagram.png diff --git a/fast/stages/03-gke-multitenant/dev/main.tf b/fast/stages/3-gke-multitenant/dev/main.tf similarity index 100% rename from fast/stages/03-gke-multitenant/dev/main.tf rename to fast/stages/3-gke-multitenant/dev/main.tf diff --git a/fast/stages/03-gke-multitenant/dev/outputs.tf b/fast/stages/3-gke-multitenant/dev/outputs.tf similarity index 96% rename from fast/stages/03-gke-multitenant/dev/outputs.tf rename to fast/stages/3-gke-multitenant/dev/outputs.tf index 87b0ca737c..3f231c6820 100644 --- a/fast/stages/03-gke-multitenant/dev/outputs.tf +++ b/fast/stages/3-gke-multitenant/dev/outputs.tf @@ -42,13 +42,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/tfvars/03-gke-dev.auto.tfvars.json" + filename = "${pathexpand(var.outputs_location)}/tfvars/3-gke-dev.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/03-gke-dev.auto.tfvars.json" + name = "tfvars/3-gke-dev.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/03-gke-multitenant/dev/variables.tf b/fast/stages/3-gke-multitenant/dev/variables.tf similarity index 92% rename from fast/stages/03-gke-multitenant/dev/variables.tf rename to fast/stages/3-gke-multitenant/dev/variables.tf index 6be89126a5..2dbf5a6ea5 100644 --- a/fast/stages/03-gke-multitenant/dev/variables.tf +++ b/fast/stages/3-gke-multitenant/dev/variables.tf @@ -19,7 +19,7 @@ # cloud dns for gke? variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -27,12 +27,16 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # 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 - organization_id = number + 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 "clusters" { @@ -147,7 +151,7 @@ variable "fleet_workload_identity" { } variable "folder_ids" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created." type = object({ gke-dev = string @@ -162,7 +166,7 @@ variable "group_iam" { } variable "host_project_ids" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Host project for the shared VPC." type = object({ dev-spoke-0 = string @@ -223,7 +227,7 @@ variable "project_services" { } variable "vpc_self_links" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Self link for the shared VPC." type = object({ dev-spoke-0 = string diff --git a/fast/stages/03-project-factory/README.md b/fast/stages/3-project-factory/README.md similarity index 100% rename from fast/stages/03-project-factory/README.md rename to fast/stages/3-project-factory/README.md diff --git a/fast/stages/03-project-factory/dev/README.md b/fast/stages/3-project-factory/dev/README.md similarity index 85% rename from fast/stages/03-project-factory/dev/README.md rename to fast/stages/3-project-factory/dev/README.md index 8fe213cee9..2d95f918d7 100644 --- a/fast/stages/03-project-factory/dev/README.md +++ b/fast/stages/3-project-factory/dev/README.md @@ -28,7 +28,7 @@ The project factory takes care of the following activities: ## How to run this stage -This stage is meant to be executed after "foundational stages" (i.e., stages [`00-bootstrap`](../../00-bootstrap), [`01-resman`](../../01-resman), 02-networking (either [VPN](../../02-networking-vpn) or [NVA](../../02-networking-nva)) and [`02-security`](../../02-security)) have been run. +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. 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: @@ -108,13 +108,13 @@ terraform apply | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [billing_account](variables.tf#L19) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L56) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [data_dir](variables.tf#L28) | Relative path for the folder storing configuration data. | string | | "data/projects" | | -| [defaults_file](variables.tf#L34) | Relative path for the file storing the project factory configuration. | string | | "data/defaults.yaml" | | -| [environment_dns_zone](variables.tf#L40) | DNS zone suffix for environment. | string | | null | 02-networking | -| [host_project_ids](variables.tf#L47) | Host project for the shared VPC. | object({…}) | | null | 02-networking | -| [vpc_self_links](variables.tf#L67) | Self link for the shared VPC. | object({…}) | | null | 02-networking | +| [billing_account](variables.tf#L19) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L60) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [data_dir](variables.tf#L32) | Relative path for the folder storing configuration data. | string | | "data/projects" | | +| [defaults_file](variables.tf#L38) | Relative path for the file storing the project factory configuration. | string | | "data/defaults.yaml" | | +| [environment_dns_zone](variables.tf#L44) | DNS zone suffix for environment. | string | | null | 2-networking | +| [host_project_ids](variables.tf#L51) | Host project for the shared VPC. | object({…}) | | null | 2-networking | +| [vpc_self_links](variables.tf#L71) | Self link for the shared VPC. | object({…}) | | null | 2-networking | ## Outputs diff --git a/fast/stages/03-project-factory/dev/data/defaults.yaml b/fast/stages/3-project-factory/dev/data/defaults.yaml similarity index 100% rename from fast/stages/03-project-factory/dev/data/defaults.yaml rename to fast/stages/3-project-factory/dev/data/defaults.yaml diff --git a/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample b/fast/stages/3-project-factory/dev/data/projects/project.yaml.sample similarity index 100% rename from fast/stages/03-project-factory/dev/data/projects/project.yaml.sample rename to fast/stages/3-project-factory/dev/data/projects/project.yaml.sample diff --git a/fast/stages/03-project-factory/dev/diagram.png b/fast/stages/3-project-factory/dev/diagram.png similarity index 100% rename from fast/stages/03-project-factory/dev/diagram.png rename to fast/stages/3-project-factory/dev/diagram.png diff --git a/fast/stages/03-project-factory/dev/diagram.svg b/fast/stages/3-project-factory/dev/diagram.svg similarity index 100% rename from fast/stages/03-project-factory/dev/diagram.svg rename to fast/stages/3-project-factory/dev/diagram.svg diff --git a/fast/stages/03-project-factory/dev/main.tf b/fast/stages/3-project-factory/dev/main.tf similarity index 100% rename from fast/stages/03-project-factory/dev/main.tf rename to fast/stages/3-project-factory/dev/main.tf diff --git a/fast/stages/03-project-factory/dev/outputs.tf b/fast/stages/3-project-factory/dev/outputs.tf similarity index 100% rename from fast/stages/03-project-factory/dev/outputs.tf rename to fast/stages/3-project-factory/dev/outputs.tf diff --git a/fast/stages/03-project-factory/dev/variables.tf b/fast/stages/3-project-factory/dev/variables.tf similarity index 76% rename from fast/stages/03-project-factory/dev/variables.tf rename to fast/stages/3-project-factory/dev/variables.tf index 2993bfba7b..5ad49f7720 100644 --- a/fast/stages/03-project-factory/dev/variables.tf +++ b/fast/stages/3-project-factory/dev/variables.tf @@ -17,12 +17,16 @@ #TODO: tfdoc annotations variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # 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 - organization_id = number + 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 "data_dir" { @@ -38,14 +42,14 @@ variable "defaults_file" { } variable "environment_dns_zone" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "DNS zone suffix for environment." type = string default = null } variable "host_project_ids" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Host project for the shared VPC." type = object({ dev-spoke-0 = string @@ -54,7 +58,7 @@ variable "host_project_ids" { } variable "prefix" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string @@ -65,7 +69,7 @@ variable "prefix" { } variable "vpc_self_links" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Self link for the shared VPC." type = object({ dev-spoke-0 = string diff --git a/fast/stages/CLEANUP.md b/fast/stages/CLEANUP.md index e5f1418f75..3bc581f92a 100644 --- a/fast/stages/CLEANUP.md +++ b/fast/stages/CLEANUP.md @@ -1,4 +1,5 @@ # FAST deployment clean up + If you want to destroy a previous FAST deployment in your organization, follow these steps. Destruction must be done in reverse order, from stage 3 to stage 0 @@ -11,6 +12,7 @@ terraform destroy ``` ## Stage 3 (GKE) + Terraform refuses to delete non-empty GCS buckets and BigQuery datasets, so they need to be removed manually from the state. ```bash @@ -24,14 +26,15 @@ done terraform destroy ``` - ## Stage 2 (Security) + ```bash cd $FAST_PWD/02-security/ terraform destroy ``` ## Stage 2 (Networking) + ```bash cd $FAST_PWD/02-networking-XXX/ terraform destroy @@ -43,7 +46,6 @@ 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/ @@ -110,5 +112,6 @@ rm -i terraform.tfstate* ``` In case you want to deploy FAST stages again, the make sure to: -* Modify the [prefix](00-bootstrap/variables.tf) variable to allow the deployment of resources that need unique names (eg, projects). -* Modify the [custom_roles](00-bootstrap/variables.tf) variable to allow recently deleted custom roles to be created again. + +* Modify the [prefix](0-bootstrap/variables.tf) variable to allow the deployment of resources that need unique names (eg, projects). +* Modify the [custom_roles](0-bootstrap/variables.tf) variable to allow recently deleted custom roles to be created again. diff --git a/fast/stages/COMPANION.md b/fast/stages/COMPANION.md index e0f6ec6215..d5d7752f2e 100644 --- a/fast/stages/COMPANION.md +++ b/fast/stages/COMPANION.md @@ -1,32 +1,42 @@ # FAST deployment companion guide -To deploy a GCP Landing Zone using FAST, your organization needs to meet a few prerequisites before starting. This guide serves as quick guide to prepare your GCP organization and also as cheat sheet with the commands and minimal configuration required to deploy FAST. +To deploy a GCP Landing Zone using FAST, your organization needs to meet a few prerequisites before starting. This guide serves as quick guide to prepare your GCP organization and also as cheat sheet with the commands and minimal configuration required to deploy FAST. The detailed explanation of each stage, their configuration, possible modifications and adaptations are included in the README of stage. This document only outlines the minimal configuration to get from an empty organization to a working FAST deployment. **Warning! Executing FAST sets organization policies and authoritative role bindings in your GCP Organization. We recommend using FAST on a clean organization, or to fork and adapt FAST to support your existing Organization needs.** ## 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: - - `gcp-billing-admins@` - - `gcp-devops@` - - `gcp-network-admins@` - - `gcp-organization-admins@` - - `gcp-security-admins@` - - `gcp-support@` + +- `gcp-billing-admins@` +- `gcp-devops@` +- `gcp-network-admins@` +- `gcp-organization-admins@` +- `gcp-security-admins@` +- `gcp-support@` + 2. If you already executed FAST in your organization, make you [clean it up](CLEANUP.md) before continuing with the rest of this guide. + 3. Grant your user “Organization Administrator” role in your organization and add it to the `gcp-organization-admins@` group. + 4. Login with your user using gcloud. + ```bash gcloud auth login gcloud auth application-default login ``` + 5. Clone the Fabric repository. + ```bash git clone https://github.com/GoogleCloudPlatform/cloud-foundation-fabric.git cd cloud-foundation-fabric ``` + 6. Grant required roles to your user. + ```bash # set a variable to the fast folder export FAST_PWD="$(pwd)/fast/stages" @@ -49,9 +59,11 @@ gcloud organizations add-iam-policy-binding $FAST_ORG_ID \ --member user:$FAST_BU --role $role done ``` -7. Configure Billing Account permissions. + +7. Configure Billing Account permissions. If you are using a standalone billing account, the user applying this stage for the first time needs to be a Billing Administrator. + ```bash # find your billing account id with gcloud beta billing accounts list # replace with your billing id! @@ -60,9 +72,11 @@ export FAST_BA_ID=XXXXXX-YYYYYY-ZZZZZZ gcloud beta billing accounts add-iam-policy-binding $FAST_BA_ID \ --member user:$FAST_BU --role roles/billing.admin ``` -If you are using a billing account in a different organization, please follow [these steps](00-bootstrap#billing-account-in-a-different-organization) instead. + +If you are using a billing account in a different organization, please follow [these steps](0-bootstrap#billing-account-in-a-different-organization) instead. ## Stage 0 (Bootstrap) + This initial stage will create common projects for IaC, Logging & Billing, and bootstrap IAM policies. ```bash @@ -73,7 +87,9 @@ cd $FAST_PWD/00-bootstrap # then edit to match your environment! edit terraform.tfvars.sample ``` + Here you have a terraform.tfvars example: + ```hcl # fetch the required id by running `gcloud beta billing accounts list` billing_account={ @@ -99,7 +115,7 @@ terraform init terraform apply -var bootstrap_user=$FAST_BU # link the generated provider file -ln -s ~/fast-config/providers/00-bootstrap* . +ln -s ~/fast-config/providers/0-0-bootstrap* . # re-run init and apply to remove user-level IAM terraform init -migrate-state @@ -108,22 +124,27 @@ terraform apply ``` ## Stage 1 (Resource Management) + This stage performs two important tasks: + - Create the top-level hierarchy of folders, and the associated resources used later on to automate each part of the hierarchy (eg. Networking). - Set organization policies on the organization, and any exception required on specific folders. + ```bash # move to the 01-resman directory cd $FAST_PWD/01-resman # Link providers and variables from previous stages -ln -s ~/fast-config/providers/01-resman-providers.tf . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . +ln -s ~/fast-config/providers/1-0-resman-providers.tf . +ln -s ~/fast-config/tfvars/0-0-bootstrap.auto.tfvars.json . ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . # Edit your terraform.tfvars to append Teams configuration (optional) edit terraform.tfvars ``` + In the following terraform.tfvars it is shown an example of configuration for teams provisioning: + ```hcl outputs_location = "~/fast-config" @@ -140,6 +161,7 @@ team_folders = { } } ``` + ```bash # run init and apply terraform init @@ -147,28 +169,34 @@ terraform apply ``` ## Stage 2 (Networking) + In this stage, we will deploy one of the 3 available Hub&Spoke networking topologies: + 1. VPC Peering 2. HA VPN 3. Multi-NIC appliances (NVA) + ```bash # move to the 02-networking-XXX directory (where XXX should be one of vpn|peering|nva) cd $FAST_PWD/02-networking-XXX # setup providers and variables from previous stages -ln -s ~/fast-config/providers/02-networking-providers.tf . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . +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/tfvars/globals.auto.tfvars.json . # Create terraform.tfvars. output_location variable is required to generate networking stage output file edit terraform.tfvars ``` + In the following terraform.tfvars we configure output_location variable to generate networking stage output file: + ```hcl # path for automatic generation of configs outputs_location = "~/fast-config" ``` + ```bash # run init and apply terraform init @@ -176,21 +204,25 @@ terraform apply ``` ## Stage 2 (Security) + This stage sets up security resources (KMS and VPC-SC) and configurations which impact the whole organization, or are shared across the hierarchy to other projects and teams. + ```bash # move to the 02-security directory cd $FAST_PWD/02-security # link providers and variables from previous stages -ln -s ~/fast-config/providers/02-security-providers.tf . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . +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/tfvars/globals.auto.tfvars.json . # Edit terraform.tfvars to include KMS and/or VPC-SC configuration edit terraform.tfvars ``` -Some examples of terraform.tfvars configurations for KMS and VPC-SC can be found [here](02-security#customizations) + +Some examples of terraform.tfvars configurations for KMS and VPC-SC can be found [here](2-security#customizations) + ```bash # run init and apply terraform init @@ -198,15 +230,17 @@ terraform apply ``` ## Stage 3 (Project Factory) + 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/03-project-factory/ENVIRONMENT -ln -s ~/fast-config/providers/03-project-factory-ENVIRONMENT-providers.tf . +cd $FAST_PWD/3-0-project-factory/ENVIRONMENT +ln -s ~/fast-config/providers/3-0-project-factory-ENVIRONMENT-providers.tf . -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 . +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/globals.auto.tfvars.json . # Define your environment default values (eg for billing alerts and labels) diff --git a/fast/stages/README.md b/fast/stages/README.md index 9b41bf1cae..acb14be6c4 100644 --- a/fast/stages/README.md +++ b/fast/stages/README.md @@ -1,4 +1,4 @@ -# Fast stages +# FAST stages Each of the folders contained here is a separate "stage", or Terraform root module. @@ -9,7 +9,7 @@ When combined together, each stage is designed to leverage the previous stage's This has two important consequences - any stage can be swapped out and replaced by different code as long as it respects the contract by providing a predefined set of outputs and optionally accepting a predefined set of variables -- data flow between stages can be partially automated (see [stage 00 documentation on output files](./00-bootstrap/README.md#output-files-and-cross-stage-variables)), reducing the effort and pain required to compile variables by hand +- data flow between stages can be partially automated (see [stage 00 documentation on output files](./0-bootstrap/README.md#output-files-and-cross-stage-variables)), reducing the effort and pain required to compile variables by hand One important assumption is that the flow of data is always forward looking, so no stage needs to depend on outputs generated further down the chain. This greatly simplifies both the logic and the implementation, and allows stages to be effectively independent. @@ -19,28 +19,32 @@ Refer to each stage's documentation for a detailed description of its purpose, t To destroy a previous FAST deployment follow the instructions detailed in [cleanup](CLEANUP.md). -## Organizational level (00-01) +## Organization (0 and 1) -- [Bootstrap](00-bootstrap/README.md) +- [Bootstrap](0-bootstrap/README.md) Enables critical organization-level functionality that depends on broad permissions. It has two primary purposes. The first is to bootstrap the resources needed for automation of this and the following stages (service accounts, GCS buckets). And secondly, it applies the minimum amount of configuration needed at the organization level, to avoid the need of broad permissions later on, and to implement a minimum of security features like sinks and exports from the start.\ Exports: automation variables, organization-level custom roles -- [Resource Management](01-resman/README.md) +- [Resource Management](1-resman/README.md) Creates the base resource hierarchy (folders) and the automation resources required later to delegate deployment of each part of the hierarchy to separate stages. This stage also configures organization-level policies and any exceptions needed by different branches of the resource hierarchy.\ Exports: folder ids, automation service account emails -## Shared resources (02) +## Multitenancy -- [Security](02-security/README.md) +Implemented via separate stages that configure separate FAST-enabled hierarchies for each tenant, check the [multitenant stages folder](../stages-multitenant/). + +## Shared resources (2) + +- [Security](2-security/README.md) Manages centralized security configurations in a separate stage, and is typically owned by the security team. This stage implements VPC Security Controls via separate perimeters for environments and central services, and creates projects to host centralized KMS keys used by the whole organization. It's meant to be easily extended to include other security-related resources which are required, like Secret Manager.\ Exports: KMS key ids -- Networking ([VPN](02-networking-vpn/README.md)/[NVA](02-networking-nva/README.md)/[Peering](02-networking-separate-envs/README.md)/[Separate environments](02-networking-separate-envs/README.md)) - Manages centralized network resources in a separate stage, and is typically owned by the networking team. This stage implements a hub-and-spoke design, and includes connectivity via VPN to on-premises, and YAML-based factories for firewall rules (hierarchical and VPC-level) and subnets. It's currently available in four flavors: [spokes connected via VPN](02-networking-vpn/README.md), [and spokes connected via appliances](02-networking-nva/README.md), [spokes connected via VPC peering](02-networking-peering/README.md), and [separated network environments](02-networking-separate-envs/README.md).\ +- Networking ([Peering](2-networking-a-peering/README.md)/[VPN](2-networking-b-vpn/README.md)/[NVA](2-networking-c-nva/README.md)/[Separate environments](2-networking-d-separate-envs/README.md)) + Manages centralized network resources in a separate stage, and is typically owned by the networking team. This stage implements a hub-and-spoke design, and includes connectivity via VPN to on-premises, and YAML-based factories for firewall rules (hierarchical and VPC-level) and subnets. It's currently available in four flavors: [spokes connected via VPC peering](2-networking-a-peering/README.md), [spokes connected via VPN](2-networking-b-vpn/README.md), [and spokes connected via appliances](2-networking-c-nva/README.md), and [separated network environments](2-networking-d-separate-envs/README.md).\ Exports: host project ids and numbers, vpc self links -## Environment-level resources (03) +## Environment-level resources (3) -- [Project Factory](03-project-factory/dev/) +- [Project Factory](3-project-factory/dev/) YAML-based fatory to create and configure application or team-level projects. Configuration includes VPC-level settings for Shared VPC, service-level configuration for CMEK encryption via centralized keys, and service account creation for workloads and applications. This stage is meant to be used once per environment. -- [Data Platform](03-data-platform/dev/) -- [GKE Multitenant](03-gke-multitenant/dev/) +- [Data Platform](3-data-platform/dev/) +- [GKE Multitenant](3-gke-multitenant/dev/) - GCE Migration (in development) diff --git a/modules/organization/README.md b/modules/organization/README.md index da44046a9d..b6caa3cd03 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -462,13 +462,13 @@ module "org" { | [iam_bindings_authoritative](variables.tf#L116) | IAM authoritative bindings, in {ROLE => [MEMBERS]} format. Roles and members not explicitly listed will be cleared. Bindings should also be authoritative when using authoritative audit config. Use with caution. | map(list(string)) | | null | | [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. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [org_policies](variables.tf#L180) | Organization policies applied to this organization keyed by policy name. | 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. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [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({…})) | | {} | ## Outputs @@ -480,9 +480,9 @@ module "org" { | [firewall_policy_id](outputs.tf#L40) | Map of firewall policy ids created in the organization. | | | [network_tag_keys](outputs.tf#L45) | Tag key resources. | | | [network_tag_values](outputs.tf#L54) | Tag value resources. | | -| [organization_id](outputs.tf#L65) | Organization id dependent on module resources. | | -| [sink_writer_identities](outputs.tf#L82) | Writer identities created for each sink. | | -| [tag_keys](outputs.tf#L90) | Tag key resources. | | -| [tag_values](outputs.tf#L99) | Tag value resources. | | +| [organization_id](outputs.tf#L62) | Organization id dependent on module resources. | | +| [sink_writer_identities](outputs.tf#L79) | Writer identities created for each sink. | | +| [tag_keys](outputs.tf#L87) | Tag key resources. | | +| [tag_values](outputs.tf#L96) | Tag value resources. | | diff --git a/modules/organization/outputs.tf b/modules/organization/outputs.tf index 40d84b473a..2e594ee665 100644 --- a/modules/organization/outputs.tf +++ b/modules/organization/outputs.tf @@ -54,11 +54,8 @@ output "network_tag_keys" { output "network_tag_values" { description = "Tag value resources." value = { - for k, v in google_tags_tag_value.default - : k => v if( - google_tags_tag_key.default[split("/", k)[0]].purpose != null && - google_tags_tag_key.default[split("/", k)[0]].purpose != "" - ) + for k, v in google_tags_tag_value.default : + k => v if local.tag_values[k].tag_network } } @@ -99,10 +96,7 @@ output "tag_keys" { output "tag_values" { description = "Tag value resources." value = { - for k, v in google_tags_tag_value.default - : k => v if( - google_tags_tag_key.default[split("/", k)[0]].purpose == null || - google_tags_tag_key.default[split("/", k)[0]].purpose == "" - ) + for k, v in google_tags_tag_value.default : + k => v if !local.tag_values[k].tag_network } } diff --git a/modules/organization/tags.tf b/modules/organization/tags.tf index 544b8989fc..b579479edc 100644 --- a/modules/organization/tags.tf +++ b/modules/organization/tags.tf @@ -23,17 +23,21 @@ locals { "Managed by the Terraform organization module." ) key = "${tag}/${value}" + id = try(value_attrs.id, null) name = value roles = keys(coalesce( value_attrs == null ? null : value_attrs.iam, {} )) - tag = tag + tag = tag + tag_id = attrs.id + tag_network = try(attrs.network, null) != null } ] ]) _tag_values_iam = flatten([ for key, value_attrs in local.tag_values : [ for role in value_attrs.roles : { + id = value_attrs.id key = value_attrs.key name = value_attrs.name role = role @@ -44,8 +48,9 @@ locals { _tags_iam = flatten([ for tag, attrs in local.tags : [ for role in keys(coalesce(attrs.iam, {})) : { - role = role - tag = tag + role = role + tag = tag + tag_id = attrs.id } ] ]) @@ -64,7 +69,7 @@ locals { # keys resource "google_tags_tag_key" "default" { - for_each = local.tags + for_each = { for k, v in local.tags : k => v if v.id == null } parent = var.organization_id purpose = ( lookup(each.value, "network", null) == null ? null : "GCE_FIREWALL" @@ -83,8 +88,12 @@ resource "google_tags_tag_key" "default" { resource "google_tags_tag_key_iam_binding" "default" { for_each = local.tags_iam - tag_key = google_tags_tag_key.default[each.value.tag].id - role = each.value.role + tag_key = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) + role = each.value.role members = coalesce( local.tags[each.value.tag]["iam"][each.value.role], [] ) @@ -93,16 +102,24 @@ resource "google_tags_tag_key_iam_binding" "default" { # values resource "google_tags_tag_value" "default" { - for_each = local.tag_values - parent = google_tags_tag_key.default[each.value.tag].id + for_each = { for k, v in local.tag_values : k => v if v.id == null } + parent = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) short_name = each.value.name description = each.value.description } resource "google_tags_tag_value_iam_binding" "default" { - for_each = local.tag_values_iam - tag_value = google_tags_tag_value.default[each.value.key].id - role = each.value.role + for_each = local.tag_values_iam + tag_value = ( + each.value.id == null + ? google_tags_tag_value.default[each.value.key].id + : each.value.id + ) + role = each.value.role members = coalesce( local.tags[each.value.tag]["values"][each.value.name]["iam"][each.value.role], [] diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 84c81ff5b5..ced5cad3d2 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -157,10 +157,11 @@ variable "logging_sinks" { } variable "network_tags" { - description = "Network tags by key name. The `iam` attribute behaves like the similarly named one at module level." + description = "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." type = map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) + id = optional(string) network = string # project_id/vpc_name values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") @@ -193,7 +194,6 @@ variable "org_policies" { values = optional(list(string)) })) enforce = optional(bool, true) # for boolean policies only. - # conditional values rules = optional(list(object({ allow = optional(object({ @@ -259,13 +259,15 @@ variable "tag_bindings" { } variable "tags" { - description = "Tags by key name. The `iam` attribute behaves like the similarly named one at module level." + description = "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." type = map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) + id = optional(string) values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) + id = optional(string) })), {}) })) nullable = false diff --git a/tests/fast/stages/s03_data_platform/test_plan.py b/tests/fast/stages/s03_data_platform/test_plan.py deleted file mode 100644 index 0bb333e711..0000000000 --- a/tests/fast/stages/s03_data_platform/test_plan.py +++ /dev/null @@ -1,21 +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. - - -def test_counts(plan_summary): - "Test stage." - summary = plan_summary("fast/stages/03-data-platform/dev/", - tf_var_files=["common.tfvars"]) - assert summary.counts["modules"] > 0 - assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s03_gke_multitenant/test_plan.py b/tests/fast/stages/s03_gke_multitenant/test_plan.py deleted file mode 100644 index 2d196ec46b..0000000000 --- a/tests/fast/stages/s03_gke_multitenant/test_plan.py +++ /dev/null @@ -1,21 +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. - - -def test_counts(plan_summary): - "Test stage." - summary = plan_summary("fast/stages/03-gke-multitenant/dev/", - tf_var_files=["common.tfvars"]) - assert summary.counts["modules"] > 0 - assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s03_project_factory/common.tfvars b/tests/fast/stages/s03_project_factory/common.tfvars deleted file mode 100644 index b65956b6b8..0000000000 --- a/tests/fast/stages/s03_project_factory/common.tfvars +++ /dev/null @@ -1,11 +0,0 @@ -data_dir = "../../../../tests/fast/stages/s03_project_factory/data/projects/" -defaults_file = "../../../../tests/fast/stages/s03_project_factory/data/defaults.yaml" -prefix = "test" -environment_dns_zone = "dev" -billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 -} -vpc_self_links = { - dev-spoke-0 = "link" -} diff --git a/tests/fast/stages/s03_project_factory/test_plan.py b/tests/fast/stages/s03_project_factory/test_plan.py deleted file mode 100644 index 3b284abbfc..0000000000 --- a/tests/fast/stages/s03_project_factory/test_plan.py +++ /dev/null @@ -1,21 +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. - - -def test_counts(plan_summary): - "Test stage." - summary = plan_summary("fast/stages/03-project-factory/dev", - tf_var_files=["common.tfvars"]) - assert summary.counts["modules"] > 0 - assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s00_bootstrap/__init__.py b/tests/fast/stages/s0_bootstrap/__init__.py similarity index 100% rename from tests/fast/stages/s00_bootstrap/__init__.py rename to tests/fast/stages/s0_bootstrap/__init__.py diff --git a/tests/fast/stages/s00_bootstrap/simple.tfvars b/tests/fast/stages/s0_bootstrap/simple.tfvars similarity index 71% rename from tests/fast/stages/s00_bootstrap/simple.tfvars rename to tests/fast/stages/s0_bootstrap/simple.tfvars index f8ef5735bd..5c389f53ad 100644 --- a/tests/fast/stages/s00_bootstrap/simple.tfvars +++ b/tests/fast/stages/s0_bootstrap/simple.tfvars @@ -4,8 +4,7 @@ organization = { customer_id = "C00000000" } billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 + id = "000000-111111-222222" } prefix = "fast" outputs_location = "/fast-config" diff --git a/tests/fast/stages/s00_bootstrap/simple.yaml b/tests/fast/stages/s0_bootstrap/simple.yaml similarity index 81% rename from tests/fast/stages/s00_bootstrap/simple.yaml rename to tests/fast/stages/s0_bootstrap/simple.yaml index ed0d773808..4b65e836d5 100644 --- a/tests/fast/stages/s00_bootstrap/simple.yaml +++ b/tests/fast/stages/s0_bootstrap/simple.yaml @@ -13,8 +13,7 @@ # limitations under the License. counts: - google_bigquery_dataset: 2 - google_bigquery_dataset_iam_member: 2 + google_bigquery_dataset: 1 google_bigquery_default_service_account: 3 google_logging_organization_sink: 2 google_organization_iam_binding: 19 @@ -22,14 +21,14 @@ counts: google_organization_iam_member: 16 google_project: 3 google_project_iam_binding: 9 - google_project_iam_member: 1 + google_project_iam_member: 3 google_project_service: 29 google_project_service_identity: 3 - google_service_account: 3 - google_service_account_iam_binding: 3 - google_storage_bucket: 4 - google_storage_bucket_iam_binding: 2 - google_storage_bucket_iam_member: 3 + google_service_account: 2 + google_service_account_iam_binding: 1 + google_storage_bucket: 3 + google_storage_bucket_iam_binding: 1 + google_storage_bucket_iam_member: 2 google_storage_bucket_object: 5 google_storage_project_service_account: 3 local_file: 5 @@ -45,5 +44,4 @@ outputs: log-export: fast-prod-audit-logs-0 service_accounts: bootstrap: fast-prod-bootstrap-0@fast-prod-iac-core-0.iam.gserviceaccount.com - cicd: fast-prod-cicd-0@fast-prod-iac-core-0.iam.gserviceaccount.com resman: fast-prod-resman-0@fast-prod-iac-core-0.iam.gserviceaccount.com diff --git a/tests/fast/stages/s00_bootstrap/simple_projects.yaml b/tests/fast/stages/s0_bootstrap/simple_projects.yaml similarity index 100% rename from tests/fast/stages/s00_bootstrap/simple_projects.yaml rename to tests/fast/stages/s0_bootstrap/simple_projects.yaml diff --git a/tests/fast/stages/s00_bootstrap/simple_sas.yaml b/tests/fast/stages/s0_bootstrap/simple_sas.yaml similarity index 82% rename from tests/fast/stages/s00_bootstrap/simple_sas.yaml rename to tests/fast/stages/s0_bootstrap/simple_sas.yaml index ba84948d86..0424e5983f 100644 --- a/tests/fast/stages/s00_bootstrap/simple_sas.yaml +++ b/tests/fast/stages/s0_bootstrap/simple_sas.yaml @@ -17,10 +17,6 @@ values: account_id: fast-prod-bootstrap-0 display_name: Terraform organization bootstrap service account. project: fast-prod-iac-core-0 - module.automation-tf-cicd-provisioning-sa.google_service_account.service_account[0]: - account_id: fast-prod-cicd-0 - display_name: Terraform stage 1 CICD service account. - project: fast-prod-iac-core-0 module.automation-tf-resman-sa.google_service_account.service_account[0]: account_id: fast-prod-resman-0 display_name: Terraform stage 1 resman service account. diff --git a/tests/fast/stages/s00_bootstrap/tftest.yaml b/tests/fast/stages/s0_bootstrap/tftest.yaml similarity index 83% rename from tests/fast/stages/s00_bootstrap/tftest.yaml rename to tests/fast/stages/s0_bootstrap/tftest.yaml index 4656859bc2..b53749adcc 100644 --- a/tests/fast/stages/s00_bootstrap/tftest.yaml +++ b/tests/fast/stages/s0_bootstrap/tftest.yaml @@ -1,6 +1,6 @@ # skip boilerplate check -module: fast/stages/00-bootstrap +module: fast/stages/0-bootstrap tests: simple: diff --git a/tests/fast/stages/s01_resman/__init__.py b/tests/fast/stages/s1_resman/__init__.py similarity index 100% rename from tests/fast/stages/s01_resman/__init__.py rename to tests/fast/stages/s1_resman/__init__.py diff --git a/tests/fast/stages/s01_resman/common.tfvars b/tests/fast/stages/s1_resman/common.tfvars similarity index 91% rename from tests/fast/stages/s01_resman/common.tfvars rename to tests/fast/stages/s1_resman/common.tfvars index f6d1d5acf4..34c61351e6 100644 --- a/tests/fast/stages/s01_resman/common.tfvars +++ b/tests/fast/stages/s1_resman/common.tfvars @@ -6,8 +6,7 @@ automation = { outputs_bucket = "test" } billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 + id = "000000-111111-222222" } custom_roles = { # organization_iam_admin = "organizations/123456789012/roles/organizationIamAdmin", diff --git a/tests/fast/stages/s01_resman/test_plan.py b/tests/fast/stages/s1_resman/test_plan.py similarity index 93% rename from tests/fast/stages/s01_resman/test_plan.py rename to tests/fast/stages/s1_resman/test_plan.py index c8dce75082..39bdfc11ee 100644 --- a/tests/fast/stages/s01_resman/test_plan.py +++ b/tests/fast/stages/s1_resman/test_plan.py @@ -15,7 +15,7 @@ def test_counts(plan_summary): "Test stage." - summary = plan_summary("fast/stages/01-resman", + summary = plan_summary("fast/stages/1-resman", tf_var_files=["common.tfvars"]) assert summary.counts["modules"] > 0 assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s02_networking_nva/__init__.py b/tests/fast/stages/s2_networking_a_peering/__init__.py similarity index 100% rename from tests/fast/stages/s02_networking_nva/__init__.py rename to tests/fast/stages/s2_networking_a_peering/__init__.py diff --git a/tests/fast/stages/s02_networking_peering/common.tfvars b/tests/fast/stages/s2_networking_a_peering/common.tfvars similarity index 83% rename from tests/fast/stages/s02_networking_peering/common.tfvars rename to tests/fast/stages/s2_networking_a_peering/common.tfvars index 11b49d7c04..d3e0779fc3 100644 --- a/tests/fast/stages/s02_networking_peering/common.tfvars +++ b/tests/fast/stages/s2_networking_a_peering/common.tfvars @@ -1,10 +1,9 @@ -data_dir = "../../../fast/stages/02-networking-peering/data/" +data_dir = "../../../fast/stages/2-networking-a-peering/data/" automation = { outputs_bucket = "test" } billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 + id = "000000-111111-222222" } custom_roles = { service_project_network_admin = "organizations/123456789012/roles/foo" diff --git a/tests/fast/stages/s02_networking_peering/test_plan.py b/tests/fast/stages/s2_networking_a_peering/test_plan.py similarity index 87% rename from tests/fast/stages/s02_networking_peering/test_plan.py rename to tests/fast/stages/s2_networking_a_peering/test_plan.py index dff0fd47b6..09590d361d 100644 --- a/tests/fast/stages/s02_networking_peering/test_plan.py +++ b/tests/fast/stages/s2_networking_a_peering/test_plan.py @@ -20,16 +20,16 @@ BASEDIR = Path(__file__).parent FIXTURE_PEERING = BASEDIR / 'fixture' -FIXTURE_VPN = BASEDIR.parent / 's02_networking_vpn/fixture' +FIXTURE_VPN = BASEDIR.parent / 's2_networking_b_vpn/fixture' STAGES = Path(__file__).parents[4] / 'fast/stages' -STAGE_PEERING = STAGES / '02-networking-peering' -STAGE_VPN = STAGES / '02-networking-vpn' +STAGE_PEERING = STAGES / '2-networking-a-peering' +STAGE_VPN = STAGES / '2-networking-b-vpn' def test_counts(plan_summary): "Test stage." - summary = plan_summary("fast/stages/02-networking-peering", + summary = plan_summary("fast/stages/2-networking-a-peering", tf_var_files=["common.tfvars"]) assert summary.counts["modules"] > 0 assert summary.counts["resources"] > 0 @@ -38,9 +38,9 @@ def test_counts(plan_summary): def test_vpn_peering_parity(plan_summary): '''Ensure VPN- and peering-based networking stages are identical except for VPN and VPC peering resources''' - summary_peering = plan_summary("fast/stages/02-networking-peering", + summary_peering = plan_summary("fast/stages/2-networking-a-peering", tf_var_files=["common.tfvars"]) - summary_vpn = plan_summary("fast/stages/02-networking-vpn", + summary_vpn = plan_summary("fast/stages/2-networking-b-vpn", tf_var_files=["common.tfvars"]) ddiff = DeepDiff(summary_vpn.values, summary_peering.values, diff --git a/tests/fast/stages/s02_networking_peering/__init__.py b/tests/fast/stages/s2_networking_b_vpn/__init__.py similarity index 100% rename from tests/fast/stages/s02_networking_peering/__init__.py rename to tests/fast/stages/s2_networking_b_vpn/__init__.py diff --git a/tests/fast/stages/s02_networking_vpn/common.tfvars b/tests/fast/stages/s2_networking_b_vpn/common.tfvars similarity index 83% rename from tests/fast/stages/s02_networking_vpn/common.tfvars rename to tests/fast/stages/s2_networking_b_vpn/common.tfvars index 7241594d1c..66a7a60909 100644 --- a/tests/fast/stages/s02_networking_vpn/common.tfvars +++ b/tests/fast/stages/s2_networking_b_vpn/common.tfvars @@ -1,10 +1,9 @@ -data_dir = "../../../../../fast/stages/02-networking-vpn/data/" +data_dir = "../../../../../fast/stages/2-networking-b-vpn/data/" automation = { outputs_bucket = "test" } billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 + id = "000000-111111-222222" } custom_roles = { service_project_network_admin = "organizations/123456789012/roles/foo" diff --git a/tests/fast/stages/s02_networking_vpn/fixture/main.tf b/tests/fast/stages/s2_networking_b_vpn/fixture/main.tf similarity index 100% rename from tests/fast/stages/s02_networking_vpn/fixture/main.tf rename to tests/fast/stages/s2_networking_b_vpn/fixture/main.tf diff --git a/tests/fast/stages/s02_networking_nva/test_plan.py b/tests/fast/stages/s2_networking_b_vpn/test_plan.py similarity index 92% rename from tests/fast/stages/s02_networking_nva/test_plan.py rename to tests/fast/stages/s2_networking_b_vpn/test_plan.py index 24964f7abe..8ac1bade10 100644 --- a/tests/fast/stages/s02_networking_nva/test_plan.py +++ b/tests/fast/stages/s2_networking_b_vpn/test_plan.py @@ -15,7 +15,7 @@ def test_counts(plan_summary): "Test stage." - summary = plan_summary("fast/stages/02-networking-nva", + summary = plan_summary("fast/stages/2-networking-b-vpn", tf_var_files=["common.tfvars"]) assert summary.counts["modules"] > 0 assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s02_networking_separate_envs/__init__.py b/tests/fast/stages/s2_networking_c_nva/__init__.py similarity index 100% rename from tests/fast/stages/s02_networking_separate_envs/__init__.py rename to tests/fast/stages/s2_networking_c_nva/__init__.py diff --git a/tests/fast/stages/s02_networking_nva/common.tfvars b/tests/fast/stages/s2_networking_c_nva/common.tfvars similarity index 81% rename from tests/fast/stages/s02_networking_nva/common.tfvars rename to tests/fast/stages/s2_networking_c_nva/common.tfvars index acfc641f33..ad12b8d339 100644 --- a/tests/fast/stages/s02_networking_nva/common.tfvars +++ b/tests/fast/stages/s2_networking_c_nva/common.tfvars @@ -1,10 +1,9 @@ -data_dir = "../../../fast/stages/02-networking-nva/data/" +data_dir = "../../../fast/stages/2-networking-c-nva/data/" automation = { outputs_bucket = "test" } billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 + id = "000000-111111-222222" } custom_roles = { service_project_network_admin = "organizations/123456789012/roles/foo" diff --git a/tests/fast/stages/s02_networking_vpn/test_plan.py b/tests/fast/stages/s2_networking_c_nva/test_plan.py similarity index 92% rename from tests/fast/stages/s02_networking_vpn/test_plan.py rename to tests/fast/stages/s2_networking_c_nva/test_plan.py index cc62dbf736..70c37bc388 100644 --- a/tests/fast/stages/s02_networking_vpn/test_plan.py +++ b/tests/fast/stages/s2_networking_c_nva/test_plan.py @@ -15,7 +15,7 @@ def test_counts(plan_summary): "Test stage." - summary = plan_summary("fast/stages/02-networking-vpn", + summary = plan_summary("fast/stages/2-networking-c-nva", tf_var_files=["common.tfvars"]) assert summary.counts["modules"] > 0 assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s02_networking_vpn/__init__.py b/tests/fast/stages/s2_networking_d_separate_envs/__init__.py similarity index 100% rename from tests/fast/stages/s02_networking_vpn/__init__.py rename to tests/fast/stages/s2_networking_d_separate_envs/__init__.py diff --git a/tests/fast/stages/s02_networking_separate_envs/common.tfvars b/tests/fast/stages/s2_networking_d_separate_envs/common.tfvars similarity index 78% rename from tests/fast/stages/s02_networking_separate_envs/common.tfvars rename to tests/fast/stages/s2_networking_d_separate_envs/common.tfvars index c6b793fd1d..3ff0020ac2 100644 --- a/tests/fast/stages/s02_networking_separate_envs/common.tfvars +++ b/tests/fast/stages/s2_networking_d_separate_envs/common.tfvars @@ -1,10 +1,9 @@ -data_dir = "../../../../../fast/stages/02-networking-separate-envs/data/" +data_dir = "../../../../../fast/stages/2-networking-d-separate-envs/data/" automation = { outputs_bucket = "test" } billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 + id = "000000-111111-222222" } custom_roles = { service_project_network_admin = "organizations/123456789012/roles/foo" diff --git a/tests/fast/stages/s02_networking_separate_envs/test_plan.py b/tests/fast/stages/s2_networking_d_separate_envs/test_plan.py similarity index 91% rename from tests/fast/stages/s02_networking_separate_envs/test_plan.py rename to tests/fast/stages/s2_networking_d_separate_envs/test_plan.py index 89f6e25743..51a9257af8 100644 --- a/tests/fast/stages/s02_networking_separate_envs/test_plan.py +++ b/tests/fast/stages/s2_networking_d_separate_envs/test_plan.py @@ -15,7 +15,7 @@ def test_counts(plan_summary): "Test stage." - summary = plan_summary("fast/stages/02-networking-separate-envs", + summary = plan_summary("fast/stages/2-networking-d-separate-envs", tf_var_files=["common.tfvars"]) assert summary.counts["modules"] > 0 assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s02_security/__init__.py b/tests/fast/stages/s2_security/__init__.py similarity index 100% rename from tests/fast/stages/s02_security/__init__.py rename to tests/fast/stages/s2_security/__init__.py diff --git a/tests/fast/stages/s02_security/common.tfvars b/tests/fast/stages/s2_security/common.tfvars similarity index 91% rename from tests/fast/stages/s02_security/common.tfvars rename to tests/fast/stages/s2_security/common.tfvars index b480a67320..6fbb60b645 100644 --- a/tests/fast/stages/s02_security/common.tfvars +++ b/tests/fast/stages/s2_security/common.tfvars @@ -2,16 +2,15 @@ automation = { outputs_bucket = "test" } billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 + id = "000000-111111-222222" } folder_ids = { security = null } organization = { - domain = "gcp-pso-italy.net" - id = 856933387836 - customer_id = "C01lmug8b" + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" } prefix = "fast" kms_keys = { diff --git a/tests/fast/stages/s02_security/test_plan.py b/tests/fast/stages/s2_security/test_plan.py similarity index 93% rename from tests/fast/stages/s02_security/test_plan.py rename to tests/fast/stages/s2_security/test_plan.py index 004f6b8368..edf5622e7d 100644 --- a/tests/fast/stages/s02_security/test_plan.py +++ b/tests/fast/stages/s2_security/test_plan.py @@ -15,7 +15,7 @@ def test_counts(plan_summary): "Test stage." - summary = plan_summary("fast/stages/02-security", + summary = plan_summary("fast/stages/2-security", tf_var_files=["common.tfvars"]) assert summary.counts["modules"] > 0 assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s03_data_platform/__init__.py b/tests/fast/stages/s3_data_platform/__init__.py similarity index 100% rename from tests/fast/stages/s03_data_platform/__init__.py rename to tests/fast/stages/s3_data_platform/__init__.py diff --git a/tests/fast/stages/s03_data_platform/common.tfvars b/tests/fast/stages/s3_data_platform/common.tfvars similarity index 85% rename from tests/fast/stages/s03_data_platform/common.tfvars rename to tests/fast/stages/s3_data_platform/common.tfvars index f5aada165d..2ec41d37ad 100644 --- a/tests/fast/stages/s03_data_platform/common.tfvars +++ b/tests/fast/stages/s3_data_platform/common.tfvars @@ -2,8 +2,7 @@ automation = { outputs_bucket = "test" } billing_account = { - id = "012345-67890A-BCDEF0", - organization_id = 123456 + id = "012345-67890A-BCDEF0", } folder_ids = { data-platform-dev = "folders/12345678" @@ -12,9 +11,9 @@ host_project_ids = { dev-spoke-0 = "fast-dev-net-spoke-0" } organization = { - domain = "example.com" + domain = "fast.example.com" id = 123456789012 - customer_id = "A11aaaaa1" + customer_id = "C00000000" } prefix = "fast" subnet_self_links = { diff --git a/tests/fast/stages/s3_data_platform/test_plan.py b/tests/fast/stages/s3_data_platform/test_plan.py new file mode 100644 index 0000000000..ad7fa3d28a --- /dev/null +++ b/tests/fast/stages/s3_data_platform/test_plan.py @@ -0,0 +1,21 @@ +# 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. + + +def test_counts(plan_summary): + "Test stage." + summary = plan_summary("fast/stages/3-data-platform/dev/", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s03_gke_multitenant/__init__.py b/tests/fast/stages/s3_gke_multitenant/__init__.py similarity index 100% rename from tests/fast/stages/s03_gke_multitenant/__init__.py rename to tests/fast/stages/s3_gke_multitenant/__init__.py diff --git a/tests/fast/stages/s03_gke_multitenant/common.tfvars b/tests/fast/stages/s3_gke_multitenant/common.tfvars similarity index 92% rename from tests/fast/stages/s03_gke_multitenant/common.tfvars rename to tests/fast/stages/s3_gke_multitenant/common.tfvars index d22db1d265..1cafdd9aba 100644 --- a/tests/fast/stages/s03_gke_multitenant/common.tfvars +++ b/tests/fast/stages/s3_gke_multitenant/common.tfvars @@ -2,8 +2,7 @@ automation = { outputs_bucket = "test" } billing_account = { - id = "012345-67890A-BCDEF0", - organization_id = 123456 + id = "012345-67890A-BCDEF0", } clusters = { mycluster = { diff --git a/tests/fast/stages/s3_gke_multitenant/test_plan.py b/tests/fast/stages/s3_gke_multitenant/test_plan.py new file mode 100644 index 0000000000..c517cb9338 --- /dev/null +++ b/tests/fast/stages/s3_gke_multitenant/test_plan.py @@ -0,0 +1,21 @@ +# 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. + + +def test_counts(plan_summary): + "Test stage." + summary = plan_summary("fast/stages/3-gke-multitenant/dev/", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s03_project_factory/__init__.py b/tests/fast/stages/s3_project_factory/__init__.py similarity index 100% rename from tests/fast/stages/s03_project_factory/__init__.py rename to tests/fast/stages/s3_project_factory/__init__.py diff --git a/tests/fast/stages/s3_project_factory/common.tfvars b/tests/fast/stages/s3_project_factory/common.tfvars new file mode 100644 index 0000000000..d3f8c6f9ad --- /dev/null +++ b/tests/fast/stages/s3_project_factory/common.tfvars @@ -0,0 +1,10 @@ +data_dir = "../../../../tests/fast/stages/s3_project_factory/data/projects/" +defaults_file = "../../../../tests/fast/stages/s3_project_factory/data/defaults.yaml" +prefix = "test" +environment_dns_zone = "dev" +billing_account = { + id = "000000-111111-222222" +} +vpc_self_links = { + dev-spoke-0 = "link" +} diff --git a/tests/fast/stages/s03_project_factory/data/defaults.yaml b/tests/fast/stages/s3_project_factory/data/defaults.yaml similarity index 100% rename from tests/fast/stages/s03_project_factory/data/defaults.yaml rename to tests/fast/stages/s3_project_factory/data/defaults.yaml diff --git a/tests/fast/stages/s03_project_factory/data/projects/project.yaml b/tests/fast/stages/s3_project_factory/data/projects/project.yaml similarity index 100% rename from tests/fast/stages/s03_project_factory/data/projects/project.yaml rename to tests/fast/stages/s3_project_factory/data/projects/project.yaml diff --git a/tests/fast/stages/s3_project_factory/test_plan.py b/tests/fast/stages/s3_project_factory/test_plan.py new file mode 100644 index 0000000000..fa293da849 --- /dev/null +++ b/tests/fast/stages/s3_project_factory/test_plan.py @@ -0,0 +1,21 @@ +# 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. + + +def test_counts(plan_summary): + "Test stage." + summary = plan_summary("fast/stages/3-project-factory/dev", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages_multitenant/__init__.py b/tests/fast/stages_multitenant/__init__.py new file mode 100644 index 0000000000..6d6d1266c3 --- /dev/null +++ b/tests/fast/stages_multitenant/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/tests/fast/stages_multitenant/s0_bootstrap_tenant/__init__.py b/tests/fast/stages_multitenant/s0_bootstrap_tenant/__init__.py new file mode 100644 index 0000000000..6d6d1266c3 --- /dev/null +++ b/tests/fast/stages_multitenant/s0_bootstrap_tenant/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.tfvars b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.tfvars new file mode 100644 index 0000000000..f77e541152 --- /dev/null +++ b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.tfvars @@ -0,0 +1,60 @@ +automation = { + federated_identity_pool = null + federated_identity_providers = null + project_id = "fast-prod-automation" + project_number = 123456 + outputs_bucket = "test" +} +billing_account = { + id = "000000-111111-222222" +} +custom_roles = { + # organization_iam_admin = "organizations/123456789012/roles/organizationIamAdmin", + service_project_network_admin = "organizations/123456789012/roles/xpnServiceAdmin" +} +groups = { + gcp-billing-admins = "gcp-billing-admins", + gcp-devops = "gcp-devops", + gcp-network-admins = "gcp-network-admins", + gcp-organization-admins = "gcp-organization-admins", + gcp-security-admins = "gcp-security-admins", + gcp-support = "gcp-support" +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "fast2" +tag_keys = { + context = "tagKeys/1234567890" + environment = "tagKeys/4567890123" + tenant = "tagKeys/7890123456" +} +tag_names = { + context = "context" + environment = "environment" + tenant = "tenant" +} +tag_values = { + "context/data" : "tagValues/1234567890", + "context/gke" : "tagValues/1234567890", + "context/networking" : "tagValues/1234567890", + "context/sandbox" : "tagValues/1234567890", + "context/security" : "tagValues/1234567890", + "context/teams" : "tagValues/1234567890", + "environment/development" : "tagValues/1234567890", + "environment/production" : "tagValues/1234567890" +} +tenant_config = { + groups = { + gcp-admins = "gcp-tn01-admins" + } + descriptive_name = "Tenant 01" + locations = { + gcs = "europe-west8" + logging = "europe-west8" + } + short_name = "tn01" +} +test_principal = "foo-prod-resman-0@foo-prod-iac-core-0.iam.gserviceaccount.com" diff --git a/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.yaml b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.yaml new file mode 100644 index 0000000000..7e132128c1 --- /dev/null +++ b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.yaml @@ -0,0 +1,33 @@ +# 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. + +counts: + google_bigquery_default_service_account: 2 + google_folder: 2 + google_folder_iam_binding: 5 + google_organization_iam_member: 38 + google_project: 2 + google_project_iam_binding: 8 + google_project_service: 26 + google_project_service_identity: 3 + google_service_account: 11 + google_storage_bucket: 2 + google_storage_bucket_iam_binding: 1 + google_storage_bucket_iam_member: 1 + google_storage_bucket_object: 2 + google_storage_project_service_account: 2 + google_tags_tag_binding: 1 + google_tags_tag_value: 1 + modules: 19 + resources: 128 diff --git a/tests/fast/stages_multitenant/s0_bootstrap_tenant/tftest.yaml b/tests/fast/stages_multitenant/s0_bootstrap_tenant/tftest.yaml new file mode 100644 index 0000000000..c2fa9fa8e9 --- /dev/null +++ b/tests/fast/stages_multitenant/s0_bootstrap_tenant/tftest.yaml @@ -0,0 +1,10 @@ +# skip boilerplate check + +module: fast/stages-multitenant/0-bootstrap-tenant + +tests: + simple: + tfvars: + - simple.tfvars + inventory: + - simple.yaml diff --git a/tests/fast/stages_multitenant/s1_resman_tenant/__init__.py b/tests/fast/stages_multitenant/s1_resman_tenant/__init__.py new file mode 100644 index 0000000000..6d6d1266c3 --- /dev/null +++ b/tests/fast/stages_multitenant/s1_resman_tenant/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/tests/fast/stages_multitenant/s1_resman_tenant/simple.tfvars b/tests/fast/stages_multitenant/s1_resman_tenant/simple.tfvars new file mode 100644 index 0000000000..33cf461989 --- /dev/null +++ b/tests/fast/stages_multitenant/s1_resman_tenant/simple.tfvars @@ -0,0 +1,70 @@ +automation = { + federated_identity_pools = null + federated_identity_providers = null + project_id = "tn0-prod-automation-0" + project_number = 123456 + outputs_bucket = "tn0-prod-automation-0" + service_accounts = { + networking = "foo-tn0-net-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + resman = "foo-tn0-resman-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + security = "foo-tn0-sec-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + dp-dev = "foo-tn0-dp-dev-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + dp-prod = "foo-tn0-dp-prod-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + gke-dev = "foo-tn0-gke-dev-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + gke-prod = "foo-tn0-gke-prod-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + pf-dev = "foo-tn0-pf-dev-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + pf-prod = "foo-tn0-pf-prod-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + sandbox = "foo-tn0-sandbox-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + teams = "foo-tn0-teams-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + } +} +billing_account = { + id = "000000-111111-222222" +} +custom_roles = { + # organization_iam_admin = "organizations/123456789012/roles/organizationIamAdmin", + service_project_network_admin = "organizations/123456789012/roles/xpnServiceAdmin" +} +fast_features = { + data_platform = true + gke = true + project_factory = true + sandbox = true + teams = true +} +groups = { + gcp-devops = "gcp-devops", + gcp-network-admins = "gcp-network-admins", + gcp-security-admins = "gcp-security-admins", +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "foo-tn0" +root_node = "folders/1234567890" +short_name = "tn0" +tags = { + keys = { + context = "tagKeys/1234567890" + environment = "tagKeys/4567890123" + tenant = "tagKeys/7890123456" + } + names = { + context = "context" + environment = "environment" + tenant = "tenant" + } + values = { + "context/data" : "tagValues/1234567890", + "context/gke" : "tagValues/1234567890", + "context/networking" : "tagValues/1234567890", + "context/sandbox" : "tagValues/1234567890", + "context/security" : "tagValues/1234567890", + "context/teams" : "tagValues/1234567890", + "environment/development" : "tagValues/1234567890", + "environment/production" : "tagValues/1234567890" + } +} +test_skip_data_sources = true diff --git a/tests/fast/stages_multitenant/s1_resman_tenant/simple.yaml b/tests/fast/stages_multitenant/s1_resman_tenant/simple.yaml new file mode 100644 index 0000000000..44c07c62fb --- /dev/null +++ b/tests/fast/stages_multitenant/s1_resman_tenant/simple.yaml @@ -0,0 +1,28 @@ +# 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. + +counts: + google_folder: 13 + google_folder_iam_binding: 42 + google_folder_iam_member: 3 + google_org_policy_policy: 2 + google_service_account: 9 + google_service_account_iam_binding: 8 + google_storage_bucket: 10 + google_storage_bucket_iam_binding: 10 + google_storage_bucket_iam_member: 9 + google_storage_bucket_object: 11 + google_tags_tag_binding: 12 + modules: 32 + resources: 129 diff --git a/tests/fast/stages_multitenant/s1_resman_tenant/tftest.yaml b/tests/fast/stages_multitenant/s1_resman_tenant/tftest.yaml new file mode 100644 index 0000000000..2e107e08d2 --- /dev/null +++ b/tests/fast/stages_multitenant/s1_resman_tenant/tftest.yaml @@ -0,0 +1,10 @@ +# skip boilerplate check + +module: fast/stages-multitenant/1-resman-tenant + +tests: + simple: + tfvars: + - simple.tfvars + inventory: + - simple.yaml diff --git a/tests/fixtures.py b/tests/fixtures.py index 788f81786f..11a397de7e 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -31,7 +31,7 @@ @contextlib.contextmanager def _prepare_root_module(path): """Context manager to prepare a terraform module to be tested. - + If the TFTEST_COPY environment variable is set, `path` is copied to a temporary directory and a few terraform files (e.g. terraform.tfvars) are delete to ensure a clean test environment. @@ -49,6 +49,7 @@ def _prepare_root_module(path): # deployment with links to configs) ignore_patterns = shutil.ignore_patterns('*.auto.tfvars', '*.auto.tfvars.json', + '[0-9]-*-providers.tf', 'terraform.tfstate*', 'terraform.tfvars', '.terraform') @@ -180,19 +181,19 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None, expected_values = inventory['values'] for address, expected_value in expected_values.items(): assert address in summary.values, \ - f'{address} is not a valid address in the plan' + f'{address} is not a valid address in the plan' for k, v in expected_value.items(): assert k in summary.values[address], \ - f'{k} not found at {address}' + f'{k} not found at {address}' plan_value = summary.values[address][k] assert plan_value == v, \ - f'{k} at {address} failed. Got `{plan_value}`, expected `{v}`' + f'{k} at {address} failed. Got `{plan_value}`, expected `{v}`' if 'counts' in inventory: expected_counts = inventory['counts'] for type_, expected_count in expected_counts.items(): assert type_ in summary.counts, \ - f'module does not create any resources of type `{type_}`' + f'module does not create any resources of type `{type_}`' plan_count = summary.counts[type_] assert plan_count == expected_count, \ f'count of {type_} resources failed. Got {plan_count}, expected {expected_count}' @@ -201,7 +202,7 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None, expected_outputs = inventory['outputs'] for output_name, expected_output in expected_outputs.items(): assert output_name in summary.outputs, \ - f'module does not output `{output_name}`' + f'module does not output `{output_name}`' output = summary.outputs[output_name] # assert 'value' in output, \ # f'output `{output_name}` does not have a value (is it sensitive or dynamic?)' diff --git a/tests/modules/organization/tags.tfvars b/tests/modules/organization/tags.tfvars new file mode 100644 index 0000000000..2a4dcb42f1 --- /dev/null +++ b/tests/modules/organization/tags.tfvars @@ -0,0 +1,59 @@ +network_tags = { + net_environment = { + network = "foobar" + } +} +tags = { + foo = {} + bar = { + description = null + iam = null + values = null + } + baz = { + id = "tagKeys/1234567890" + values = { + one = null + two = null + } + } + foobar = { + description = "Foobar tag." + iam = { + "roles/resourcemanager.tagAdmin" = [ + "user:user1@example.com", "user:user2@example.com" + ] + } + values = { + one = null + two = { + description = "Foobar 2." + iam = { + "roles/resourcemanager.tagViewer" = [ + "user:user3@example.com" + ] + } + } + three = { + description = "Foobar 3." + iam = { + "roles/resourcemanager.tagViewer" = [ + "user:user3@example.com" + ] + "roles/resourcemanager.tagAdmin" = [ + "user:user4@example.com" + ] + } + } + four = { + description = "Foobar 4." + id = "tagValues/1234567890" + iam = { + "roles/resourcemanager.tagViewer" = [ + "user:user4@example.com" + ] + } + } + } + } +} diff --git a/tests/modules/organization/tags.yaml b/tests/modules/organization/tags.yaml new file mode 100644 index 0000000000..3e5524d473 --- /dev/null +++ b/tests/modules/organization/tags.yaml @@ -0,0 +1,76 @@ +# 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. + +values: + google_tags_tag_key.default["bar"]: + description: Managed by the Terraform organization module. + parent: organizations/1234567890 + purpose: null + purpose_data: null + short_name: bar + google_tags_tag_key.default["foo"]: + description: Managed by the Terraform organization module. + parent: organizations/1234567890 + purpose: null + purpose_data: null + short_name: foo + google_tags_tag_key.default["foobar"]: + description: Foobar tag. + parent: organizations/1234567890 + purpose: null + purpose_data: null + short_name: foobar + google_tags_tag_key.default["net_environment"]: + description: Managed by the Terraform organization module. + parent: organizations/1234567890 + purpose: GCE_FIREWALL + purpose_data: + network: foobar + short_name: net_environment + ? google_tags_tag_key_iam_binding.default["foobar:roles/resourcemanager.tagAdmin"] + : condition: [] + members: + - user:user1@example.com + - user:user2@example.com + role: roles/resourcemanager.tagAdmin + google_tags_tag_value.default["foobar/one"]: + description: Managed by the Terraform organization module. + short_name: one + google_tags_tag_value.default["foobar/three"]: + description: Foobar 3. + short_name: three + google_tags_tag_value.default["foobar/two"]: + description: Foobar 2. + short_name: two + ? google_tags_tag_value_iam_binding.default["foobar/three:roles/resourcemanager.tagAdmin"] + : condition: [] + members: + - user:user4@example.com + role: roles/resourcemanager.tagAdmin + ? google_tags_tag_value_iam_binding.default["foobar/three:roles/resourcemanager.tagViewer"] + : condition: [] + members: + - user:user3@example.com + role: roles/resourcemanager.tagViewer + ? google_tags_tag_value_iam_binding.default["foobar/two:roles/resourcemanager.tagViewer"] + : condition: [] + members: + - user:user3@example.com + role: roles/resourcemanager.tagViewer + +counts: + google_tags_tag_key: 4 + google_tags_tag_key_iam_binding: 1 + google_tags_tag_value: 5 + google_tags_tag_value_iam_binding: 4 diff --git a/tests/modules/organization/tftest.yaml b/tests/modules/organization/tftest.yaml index 7466614ef3..c49a139a6f 100644 --- a/tests/modules/organization/tftest.yaml +++ b/tests/modules/organization/tftest.yaml @@ -23,3 +23,4 @@ tests: org_policies_boolean: org_policies_custom_constraints: firewall_policies_factory_combined: + tags: diff --git a/tools/check_links.py b/tools/check_links.py index 77dc617392..1e2759dfb7 100755 --- a/tools/check_links.py +++ b/tools/check_links.py @@ -86,7 +86,7 @@ def main(dirs, external): state = '✓' if all(l.valid for l in doc.links) else '✗' print(f'[{state}] {doc.relpath} ({len(doc.links)})') if state == '✗': - error = [f'{dir_name}{doc.relpath}'] + error = [f'{dir_name}/{doc.relpath}'] for l in doc.links: if not l.valid: error.append(f' - {l.dest}') diff --git a/tools/plan_summary.py b/tools/plan_summary.py index 78c5f939f6..ae52c86cf2 100755 --- a/tools/plan_summary.py +++ b/tools/plan_summary.py @@ -15,16 +15,19 @@ # limitations under the License. import click +import os import sys import tempfile import yaml from pathlib import Path -BASEDIR = Path(__file__).parents[1] -sys.path.append(str(BASEDIR / 'tests')) - -import fixtures +try: + import fixtures +except ImportError: + BASEDIR = Path(__file__).parents[1] + sys.path.append(str(BASEDIR / 'tests')) + import fixtures @click.command()