diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index e3919451..1610f270 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -16,6 +16,7 @@ name: "Linting" on: pull_request: branches: + - fast-dev - master tags: - ci @@ -51,23 +52,22 @@ jobs: run: | terraform fmt -recursive -check -diff $GITHUB_WORKSPACE - - name: Check documentation - id: documentation + - name: Check documentation (fabric) + id: documentation-fabric run: | - python3 tools/check_documentation.py \ - cloud-operations \ - data-solutions \ - factories \ - foundations \ - modules \ - networking + python3 tools/check_documentation.py examples modules - markdown-link-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - uses: gaurav-nelson/github-action-markdown-link-check@v1 - with: - use-quiet-mode: 'yes' - use-verbose-mode: 'yes' - config-file: '.github/workflows/markdown-link-check.json' + - name: Check documentation (fast) + id: documentation-fast + run: | + python3 tools/check_documentation.py --files --show-extra fast + + # markdown-link-check: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@master + # - uses: gaurav-nelson/github-action-markdown-link-check@v1 + # with: + # use-quiet-mode: "yes" + # use-verbose-mode: "yes" + # config-file: ".github/workflows/markdown-link-check.json" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index db3ad488..ea813657 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,6 +18,7 @@ on: - cron: "45 2 * * *" pull_request: branches: + - fast-dev - master tags: - ci @@ -27,6 +28,38 @@ env: PYTEST_ADDOPTS: "--color=yes" jobs: + doc-examples: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.9" + + - name: Set up Terraform + uses: hashicorp/setup-terraform@v1 + with: + terraform_version: 1.1.3 + terraform_wrapper: false + + - name: Set environment + run: | + echo "TF_PLUGIN_CACHE_DIR=${{ github.workspace }}/.terraform.d/plugin-cache-${GITHUB_JOB}" >> $GITHUB_ENV + echo "GOOGLE_APPLICATION_CREDENTIALS=${GITHUB_WORKSPACE}/.github/workflows/fake-key.json" >> $GITHUB_ENV + mkdir --parents ${{ github.workspace }}/.terraform.d/plugin-cache-${GITHUB_JOB} + terraform -chdir=tests providers lock + + - name: Install dependencies + run: | + pip install -r tests/requirements.txt + + - name: Run tests on documentation examples + id: doc-examples + run: | + pytest -n 4 -vv tests/doc_examples + examples: runs-on: ubuntu-latest steps: @@ -40,12 +73,15 @@ jobs: - name: Set up Terraform uses: hashicorp/setup-terraform@v1 with: - terraform_version: 1.0.9 + terraform_version: 1.1.3 terraform_wrapper: false - name: Set environment run: | + echo "TF_PLUGIN_CACHE_DIR=${{ github.workspace }}/.terraform.d/plugin-cache-${GITHUB_JOB}" >> $GITHUB_ENV echo "GOOGLE_APPLICATION_CREDENTIALS=${GITHUB_WORKSPACE}/.github/workflows/fake-key.json" >> $GITHUB_ENV + mkdir --parents ${{ github.workspace }}/.terraform.d/plugin-cache-${GITHUB_JOB} + terraform -chdir=tests providers lock - name: Install dependencies run: | @@ -56,35 +92,6 @@ jobs: run: | pytest -n 4 -vv tests/examples - module-examples: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: "3.9" - - - name: Set up Terraform - uses: hashicorp/setup-terraform@v1 - with: - terraform_version: 1.0.9 - terraform_wrapper: false - - - name: Set environment - run: | - echo "GOOGLE_APPLICATION_CREDENTIALS=${GITHUB_WORKSPACE}/.github/workflows/fake-key.json" >> $GITHUB_ENV - - - name: Install dependencies - run: | - pip install -r tests/requirements.txt - - - name: Run tests examples - id: test-examples - run: | - pytest -n 4 -vv tests/modules/examples - modules: runs-on: ubuntu-latest steps: @@ -98,12 +105,15 @@ jobs: - name: Set up Terraform uses: hashicorp/setup-terraform@v1 with: - terraform_version: 1.0.9 + terraform_version: 1.1.3 terraform_wrapper: false - name: Set environment run: | + echo "TF_PLUGIN_CACHE_DIR=${{ github.workspace }}/.terraform.d/plugin-cache-${GITHUB_JOB}" >> $GITHUB_ENV echo "GOOGLE_APPLICATION_CREDENTIALS=${GITHUB_WORKSPACE}/.github/workflows/fake-key.json" >> $GITHUB_ENV + mkdir --parents ${{ github.workspace }}/.terraform.d/plugin-cache-${GITHUB_JOB} + terraform -chdir=tests providers lock - name: Install dependencies run: | diff --git a/.gitignore b/.gitignore index 0f0dea4f..d1a5e47c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,8 @@ bundle.zip **/packer_cache **/*.pkrvars.hcl fixture_* +fast/configs +fast/stages/**/providers.tf +fast/stages/**/terraform.tfvars +fast/stages/**/terraform.tfvars.json +fast/stages/**/terraform-*.auto.tfvars.json diff --git a/README.md b/README.md index 16227335..c1fb07bc 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,17 @@ This repository provides **end-to-end examples** and a **suite of Terraform modules** for Google Cloud, which support different use cases: -- starter kits used to bootstrap real-world cloud foundations, and reference examples used to deep dive on network patterns or product features -- composable modules that support quick prototyping and testing -- a comprehensive source of lean modules that lend themselves well to changes +- organization-wide [landing zone blueprint](fast/) used to bootstrap real-world cloud foundations +- reference [examples](./examples/) used to deep dive on network patterns or product features +- a comprehensive source of lean [modules](./modules/dns) that lend themselves well to changes The whole repository is meant to be cloned as a single unit, and then forked into separate owned repositories to seed production usage, or used as-is and periodically updated as a complete toolkit for prototyping. You can read more on this approach in our [manifesto](./MANIFESTO.md). Both the examples and modules require some measure of Terraform skills to be used effectively. If you are looking for a feature-rich black box to manage project or product creation with minimal specific skills, you might be better served by the [Cloud Foundation Toolkit](https://registry.terraform.io/modules/terraform-google-modules) suite of modules. -## End-to-end examples +## Organization blueprint (Fabric FAST) -The [examples](./examples/) in this repository are split in several main sections: **[foundational examples](./examples/foundations/)** that bootstrap the organizational hierarchy and automation prerequisites, **[networking examples](./examples/networking/)** that implement core patterns or features, **[data solutions examples](./examples/data-solutions/)** that demonstrate how to integrate data services in complete scenarios, **[cloud operations examples](./examples/cloud-operations/)** that leverage specific products to meet specific operational needs and **[factories](./examples/factories/)** that implement resource factories for the repetitive creation of specific resources. +Setting up a production-ready GCP organization is often a time-consuming process. Fabric [FAST](fast/) aims to speed up this process via two complementary goals. On the one hand, FAST provides a design of a GCP organization that includes the typical elements required by enterprise customers. Secondly, we provide a reference implementation of the FAST design using Terraform. ## Modules @@ -37,3 +37,7 @@ Currently available modules: - **serverless** - [Cloud Function](./modules/cloud-function), [Cloud Run](./modules/cloud-run) For more information and usage examples see each module's README file. + +## End-to-end examples + +The [examples](./examples/) in this repository are split in several main sections: **[foundational examples](./examples/foundations/)** that bootstrap the organizational hierarchy and automation prerequisites, **[networking examples](./examples/networking/)** that implement core patterns or features, **[data solutions examples](./examples/data-solutions/)** that demonstrate how to integrate data services in complete scenarios, **[cloud operations examples](./examples/cloud-operations/)** that leverage specific products to meet specific operational needs and **[factories](./examples/factories/)** that implement resource factories for the repetitive creation of specific resources. diff --git a/examples/data-solutions/gcs-to-bq-with-dataflow/README.md b/examples/data-solutions/gcs-to-bq-with-dataflow/README.md index aa6a6eb7..055f93ef 100644 --- a/examples/data-solutions/gcs-to-bq-with-dataflow/README.md +++ b/examples/data-solutions/gcs-to-bq-with-dataflow/README.md @@ -113,14 +113,15 @@ You can check data imported into Google BigQuery from the Google Cloud Console U + ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| prefix | Unique prefix used for resource names. Not used for project if 'project_create' is null. | string | ✓ | | | project_id | Project id, references existing project if `project_create` is null. | string | ✓ | | +| prefix | Unique prefix used for resource names. Not used for project if 'project_create' is null. | string | | null | | project_create | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format | object({…}) | | null | | region | The region where resources will be deployed. | string | | "europe-west1" | | vpc_subnet_range | Ip range used for the VPC subnet created for the example. | string | | "10.0.0.0/20" | @@ -139,3 +140,4 @@ You can check data imported into Google BigQuery from the Google Cloud Console U + diff --git a/examples/factories/net-vpc-firewall-yaml/README.md b/examples/factories/net-vpc-firewall-yaml/README.md index 064ee30c..89af153e 100644 --- a/examples/factories/net-vpc-firewall-yaml/README.md +++ b/examples/factories/net-vpc-firewall-yaml/README.md @@ -136,25 +136,27 @@ web-app-a-ingress: ``` + ## Variables -| name | description | type | required | default | -| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------: | :------: | :---------------: | -| config_directories | List of paths to folders where firewall configs are stored in yaml format. Folder may include subfolders with configuration files. Files suffix must be `.yaml` | list(string) | ✓ | | -| network | Name of the network this set of firewall rules applies to. | string | ✓ | | -| project_id | Project Id. | string | ✓ | | -| log_config | Log configuration. Possible values for `metadata` are `EXCLUDE_ALL_METADATA` and `INCLUDE_ALL_METADATA`. Set to `null` for disabling firewall logging. | object({…}) | | null | +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| config_directories | List of paths to folders where firewall configs are stored in yaml format. Folder may include subfolders with configuration files. Files suffix must be `.yaml` | list(string) | ✓ | | +| network | Name of the network this set of firewall rules applies to. | string | ✓ | | +| project_id | Project Id. | string | ✓ | | +| log_config | Log configuration. Possible values for `metadata` are `EXCLUDE_ALL_METADATA` and `INCLUDE_ALL_METADATA`. Set to `null` for disabling firewall logging. | object({…}) | | null | ## Outputs -| name | description | sensitive | -| ------------------- | -------------------------------- | :-------: | -| egress_allow_rules | Egress rules with allow blocks. | | -| egress_deny_rules | Egress rules with allow blocks. | | -| ingress_allow_rules | Ingress rules with allow blocks. | | -| ingress_deny_rules | Ingress rules with deny blocks. | | +| name | description | sensitive | +|---|---|:---:| +| egress_allow_rules | Egress rules with allow blocks. | | +| egress_deny_rules | Egress rules with allow blocks. | | +| ingress_allow_rules | Ingress rules with allow blocks. | | +| ingress_deny_rules | Ingress rules with deny blocks. | | + diff --git a/examples/factories/project-factory/main.tf b/examples/factories/project-factory/main.tf index 21e941c7..a49fb993 100644 --- a/examples/factories/project-factory/main.tf +++ b/examples/factories/project-factory/main.tf @@ -101,7 +101,7 @@ locals { module "billing-alert" { for_each = local.billing_alert == null ? {} : { 1 = 1 } - source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/billing-budget?ref=v12.0.0" + source = "../../../modules/billing-budget" billing_account = local.billing_account_id name = "${module.project.project_id} budget" amount = local.billing_alert.amount @@ -116,7 +116,7 @@ module "billing-alert" { } module "dns" { - source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/dns?ref=v12.0.0" + source = "../../../modules/dns" for_each = toset(var.dns_zones) project_id = module.project.project_id type = "private" @@ -126,7 +126,7 @@ module "dns" { } module "project" { - source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/project?ref=v12.0.0" + source = "../../../modules/project" billing_account = local.billing_account_id name = var.project_id contacts = { for c in local.essential_contacts : c => ["ALL"] } @@ -144,7 +144,7 @@ module "project" { } module "service-accounts" { - source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/iam-service-account?ref=v12.0.0" + source = "../../../modules/iam-service-account" for_each = var.service_accounts name = each.key project_id = module.project.project_id diff --git a/examples/networking/decentralized-firewall/README.md b/examples/networking/decentralized-firewall/README.md index 8bf40135..96c5ac2f 100644 --- a/examples/networking/decentralized-firewall/README.md +++ b/examples/networking/decentralized-firewall/README.md @@ -21,26 +21,28 @@ the two). There is an example of a YAML-based validator using [Yamale](https://g in the [`validator/`](validator/) subdirectory, which can be integrated as part of a CI/CD pipeline. + ## Variables -| name | description | type | required | default | -| ------------------ | --------------------------------------------------------------------------------------------- | :-------------------------------: | :------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -| billing_account_id | Billing account id used as default for new projects. | string | ✓ | | -| prefix | Prefix used for resources that need unique names. | string | ✓ | | -| root_node | Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'. | string | ✓ | | -| ip_ranges | Subnet IP CIDR ranges. | map(string) | | {…} | -| project_services | Service APIs enabled by default in new projects. | list(string) | | […] | -| region | Region used. | string | | "europe-west1" | +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| billing_account_id | Billing account id used as default for new projects. | string | ✓ | | +| prefix | Prefix used for resources that need unique names. | string | ✓ | | +| root_node | Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'. | string | ✓ | | +| ip_ranges | Subnet IP CIDR ranges. | map(string) | | {…} | +| project_services | Service APIs enabled by default in new projects. | list(string) | | […] | +| region | Region used. | string | | "europe-west1" | ## Outputs -| name | description | sensitive | -| -------- | --------------- | :-------: | -| fw_rules | Firewall rules. | | -| projects | Project ids. | | -| vpc | Shared VPCs. | | +| name | description | sensitive | +|---|---|:---:| +| fw_rules | Firewall rules. | | +| projects | Project ids. | | +| vpc | Shared VPCs. | | + diff --git a/fast/README.md b/fast/README.md new file mode 100644 index 00000000..5d990adb --- /dev/null +++ b/fast/README.md @@ -0,0 +1,87 @@ +# Fabric FAST + +Setting up a production-ready GCP organization is often a time-consuming process. Fabric FAST aims to speed up this process via two complementary goals. On the one hand, FAST provides a design of a GCP organization that includes the typical elements required by enterprise customers. Secondly, we provide a reference implementation of the FAST design using Terraform. + +Note that while our implementation is necessarily influenced (and constrained) by the way Terraform works, the design we put forward only refers to GCP constructs and features. In other words, while we use Terraform for our reference implementation, in theory, the FAST design can be implemented using any other tool (e.g., Pulumi, bash scripts, or even calling the relevant APIs directly). + +Fabric FAST comes from engineers in Google Cloud's Professional Services Organization, with a combined experience of decades solving the typical technical problems faced by GCP customers. While every GCP user has specific requirements, many common issues arise repeatedly. Solving those issues correctly from the beginning is key to a robust and scalable GCP setup. It's those common issues and their solutions that Fabric FAST aims to collect and present coherently. + +Fabric FAST was initially conceived to help enterprises quickly set up a GCP organization following battle-tested and widely-used patterns. Despite its origin in enterprise environments, FAST includes many customization points making it an ideal blueprint for organizations of all sizes, ranging from startups to the largest companies. + +## Guiding principles + +### 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. + +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. + +

+ Stages diagram +

+ +### Security-first design + +Security was, from the beginning, one of the most critical elements in the design of Fabric FAST. Many of FAST's design decisions aim to build the foundations of a secure organization. In fact, the first two stages deal mainly with the organization-wide security setup. + +FAST also aims to minimize the number of permissions granted to principals according to the security-first approach previously mentioned. We achieve this through the meticulous use of groups, service accounts, custom roles, and [Cloud IAM Conditions](https://cloud.google.com/iam/docs/conditions-overview), among other things. + +### Extensive use of factories + +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. + +## High level design + +As mentioned before, fast relies on multiple stages to progressively bring up your GCP organization(s). In this section we briefly describe each stage. + +### Organizational level (00-01) + +- [Bootstrap](stages/00-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 to automate 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 for broad permissions later on, and to implement a minimum of security features like sinks and exports from the start. +- [Resource Management](stages/01-resman/README.md)
+ Creates the base resource hierarchy (folders) and the automation resources required to delegate 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. + +### Shared resources (02) + +- [Security](stages/02-security/README.md)
+ Manages centralized security configurations in a separate stage, 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 intentionally easy to extend to include other security-related resources, like Secret Manager. +- [Networking](stages/02-networking/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, includes connectivity via VPN to on-premises, and YAML-based factories for firewall rules (hierarchical and VPC-level) and subnets. + +### Environment-level resources (03) + +- [Project Factory](03-projectfactory/prod/README.md)
+ YAML-based factory 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 (in development) +- GKE Multitenant (in development) +- GCE Migration (in development) + +Please refer to the READMEs of each stage for further details. + +## Implementation + +There are many decisions and tasks required to convert an empty GCP organization to one that can host production environments safely. Arguably, FAST could expose those decisions as configuration options to allow for different outcomes. However, supporting all the possible combinations is almost impossible and leads to code which is hard to maintain efficiently. + +Instead, FAST aims to leverage different reference architectures as “pluggable modules”, and then have a small set of variables covering only the essential options of each stage. While we could expose every option of the underlying resources as stage-level variables, we prefer to provide the basic implementation and encourage users to modify the codebase if additional (or different) behavior is needed. + +Since we expect users to customize FAST to their specific needs, we strive to make its code easy to understand and modify. Root-level modules (i.e., stages) should be low in complexity, which among other things, means: + +- Code should avoid magic and be as explicit as possible. +- We hide advanced features and complexity behind modules. +- We prefer as little indirection as possible. +- We favor flat over nested. + +We also recognize that FAST users don't need all of its features. Therefore, you don't need to use our project factory or our GKE implementation if you don't want to. Instead, remove those stages or pieces of code and keep what suits you. + +Those familiar with Python will note that FAST follows many of the maxims in the [Zen of Python](https://www.python.org/dev/peps/pep-0020/#id2). + +## Roadmap + +Besides the features already described, FAST roadmap 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 +- Static policy enforcement diff --git a/fast/TODO.md b/fast/TODO.md new file mode 100644 index 00000000..0ea1f822 --- /dev/null +++ b/fast/TODO.md @@ -0,0 +1,23 @@ +TODO before merging + +- [x] fix tests +- [x] fix linting errors +- [x] fast-specific .gitignore +- [x] YAML samples thingy +- [ ] stages README +- [ ] fabric top-level README +- [x] proper docstring on new tools + - [x] validate_schema.py + - [x] tfutils.py (deleted, it's an empty shell) + - [x] check_boilerplate.py + - [x] check_documentation.py +- [x] remove GKE branch from resman and update diagram +- [x] remove GKE branch from resman and update diagram +- [x] modify github actions for different fast tfdoc usage +- [x] add roadmap to top-level fast README +- [x] update modules references to local paths + - [x] stage 00 (ludo) + - [x] stage 01 (julio) + - [x] stage 02-net (simo) + - [x] stage 02-sec (ludo) + - [x] stage 03-pf (simo) diff --git a/fast/assets/schemas/firewall_rules.schema.yaml b/fast/assets/schemas/firewall_rules.schema.yaml new file mode 100644 index 00000000..1fd96caf --- /dev/null +++ b/fast/assets/schemas/firewall_rules.schema.yaml @@ -0,0 +1,29 @@ +# 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. + +map(include('firewall_rule')) +--- +firewall_rule: + description: str() + direction: enum("INGRESS", "EGRESS") + action: enum("allow", "deny") + sources: list(str()) + ranges: list(str()) + targets: list(str()) + use_service_accounts: bool() + rules: list(include('rule')) +--- +rule: + protocol: enum("tcp", "udp", "all") + ports: list(num()) diff --git a/fast/assets/schemas/hierarchical_rules.schema.yaml b/fast/assets/schemas/hierarchical_rules.schema.yaml new file mode 100644 index 00000000..0e0f7b66 --- /dev/null +++ b/fast/assets/schemas/hierarchical_rules.schema.yaml @@ -0,0 +1,25 @@ +# 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. + +map(include('hierarchical_rule')) +--- +hierarchical_rule: + description: str() + direction: enum("INGRESS", "EGRESS") + action: enum("allow", "deny") + priority: int() + ranges: list(str()) + target_resources: any(null(), list(str())) + ports: map(list(str(), required=False)) + enable_logging: bool() diff --git a/fast/assets/schemas/project.schema.yaml b/fast/assets/schemas/project.schema.yaml new file mode 100644 index 00000000..f7f89730 --- /dev/null +++ b/fast/assets/schemas/project.schema.yaml @@ -0,0 +1,57 @@ +# 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. + +billing_account_id: str(matches='[A-F0-9]{6}-[A-F0-9]{6}-[A-F0-9]{6}', required=False) +billing_alert: any(include('billing_alert'), null(), required=False) # If set to null, use defaults +dns_zones: list(str(), required=False) +essential_contacts: list(str(), required=False) # Also used for billing alerts +folder_id: str(matches='(organizations/|folders/)[0-9]*$') +group_iam: map(list(str()), key=str(), required=False) +iam: map(list(str()), key=str(), required=False) +kms_service_agents: map(list(str()), key=str(), required=False) +labels: map(str(), key=str(), required=False) +org_policies: include('org_policies', required=False) +secrets: map(list(str()), key=str(), required=False) +service_accounts: map(list(str()), required=False) +services: list(str(matches='^[a-z-]*\.googleapis\.com$'), required=False) +services_iam: map(list(str()), key=str(), required=False) +vpc: include('vpc', required=False) +--- +billing_alert: + amount: int() + thresholds: include('billing_alert_thresholds') + credit_treatment: enum("INCLUDE_ALL_CREDITS", "EXCLUDE_ALL_CREDITS") +--- +billing_alert_thresholds: + current: list(num(min=0, max=1)) + forecasted: list(num(min=0, max=1)) +--- +gke_setup: + enable_security_admin: bool(required=False) + enable_host_service_agent: bool(required=False) +--- +org_policies: + policy_boolean: map(bool(), key=str(matches='^constraints/[A-z\.]*$'), required=False) + policy_list: map(include('policy_list'), key=str(matches='^constraints/[A-z\.]*$'), required=False) +--- +policy_list: + inherit_from_parent: any(bool(), null()) + suggested_value: any(str(), null()) + status: any(bool(), null()) + values: list(str()) +--- +vpc: + host_project: str(matches='[a-z]([-a-z0-9]*[a-z0-9])?', min=6, max=30) + gke_setup: include('gke_setup', required=False) + subnets_iam: map(list(str()), key=str(matches='^[a-z0-9-]*/[a-z0-9-]*$'), required=False) diff --git a/fast/assets/schemas/project_defaults.schema.yaml b/fast/assets/schemas/project_defaults.schema.yaml new file mode 100644 index 00000000..113fe26b --- /dev/null +++ b/fast/assets/schemas/project_defaults.schema.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. + +billing_account_id: str(matches='[A-F0-9]{6}-[A-F0-9]{6}-[A-F0-9]{6}', required=False) +billing_alert: any(include('billing_alert'), null(), required=False) +essential_contacts: list(str(), required=False) +labels: map(str(), key=str(), required=False) +notification_channels: list(str(), required=False) +--- +billing_alert: + amount: int() + thresholds: include('billing_alert_thresholds') + credit_treatment: enum('INCLUDE_ALL_CREDITS', 'EXCLUDE_ALL_CREDITS') +--- +billing_alert_thresholds: + current: list(num(min=0, max=1)) + forecasted: list(num(min=0, max=1)) diff --git a/fast/assets/schemas/subnet.schema.yaml b/fast/assets/schemas/subnet.schema.yaml new file mode 100644 index 00000000..add0d74b --- /dev/null +++ b/fast/assets/schemas/subnet.schema.yaml @@ -0,0 +1,29 @@ +# 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. + +region: str() +description: str() +ip_cidr_range: str() +# optional attributes +private_ip_google_access: bool(required=False) # defaults to true +iam_users: list(str(), required=False) +iam_groups: list(str(), required=False) +iam_service_accounts: list(str(), required=False) +secondary_ip_ranges: list(map(str()), key=str(), required=False) +flow_logs: any(include('flow_logs'), required=False) +--- +flow_logs: + - aggregation_interval: enum('INTERVAL_5_SEC', 'INTERVAL_30_SEC', 'INTERVAL_1_MIN', 'INTERVAL_5_MIN', 'INTERVAL_10_MIN', 'INTERVAL_15_MIN', required=False) + - flow_sampling: num(min=0, max=1, required=False) + - metadata: enum('EXCLUDE_ALL_METADATA', 'INCLUDE_ALL_METADATA', 'CUSTOM_METADATA', required=False) diff --git a/fast/assets/templates/providers.tpl b/fast/assets/templates/providers.tpl new file mode 100644 index 00000000..7f0ce142 --- /dev/null +++ b/fast/assets/templates/providers.tpl @@ -0,0 +1,30 @@ +/** + * 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}" + } +} +provider "google" { + impersonate_service_account = "${sa}" +} +provider "google-beta" { + impersonate_service_account = "${sa}" +} + +# end provider.tf for ${name} diff --git a/fast/stages.png b/fast/stages.png new file mode 100644 index 00000000..6d1335bb Binary files /dev/null and b/fast/stages.png differ diff --git a/fast/stages.svg b/fast/stages.svg new file mode 100644 index 00000000..15e8195b --- /dev/null +++ b/fast/stages.svg @@ -0,0 +1,1074 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages/00-bootstrap/README.md b/fast/stages/00-bootstrap/README.md new file mode 100644 index 00000000..621b42b5 --- /dev/null +++ b/fast/stages/00-bootstrap/README.md @@ -0,0 +1,310 @@ +# Organization bootstrap + +The primary purpose of this stage is to enable critical organization-level functionality that depends on broad administrative permissions, and prepare the prerequisites needed to enable automation in this and future stages. + +It is intentionally simple, to minimize usage of administrative-level permissions and enable simple auditing and troubleshooting, and only deals with three sets of resources: + +- project, service accounts, and GCS buckets for automation +- projects, BQ datasets, and sinks for audit log and billing exports +- IAM bindings on the organization + +Use the following diagram as a simple high level reference for the following sections, which describe the stage and its possible customizations in detail. + +

+ Organization-level diagram +

+ +## Design overview and choices + +As mentioned above, this stage only does the bare minimum required to bootstrap automation, and ensure that base audit and billing exports are in place from the start to provide some measure of accountability, even before the security configurations are applied in a later stage. + +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. + +### User groups + +User groups are important, not only here but throughout the whole automation process. They provide a stable frame of reference that allows decoupling the final set of permissions for each group, from the stage where entities and resources are created and their IAM bindings defined. For example, the final set of roles for the networking group is contributed by this stage at the organization level (XPN Admin, Cloud Asset Viewer, etc.), and by the Resource Management stage at the folder level. + +We have standardized the initial set of groups on those outlined in the [GCP Enterprise Setup Checklist](https://cloud.google.com/docs/enterprise/setup-checklist) to simplify adoption. They provide a comprehensive and flexible starting point that can suit most users. Adding new groups, or deviating from the initial setup is possible and reasonably simple, and it's briefly outlined in the customization section below. + +### Organization-level IAM + +The service account used in the [Resource Management stage](../01-resman) needs to be able to grant specific roles at the organizational level (`roles/billing.user`, `roles/compute.xpnAdmin`, etc.), 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. + +In this way, the Resource Management service account can effectively act as an Organization Admin, but only to grant the roles it effectively needs to control. + +One consequence of the above setup, is the need to configure IAM bindings as non-authoritative for the roles included in the IAM condition, since those same roles are effectively under the control of two stages: this one and Resource Management. Using authoritative bindings for these roles (instead of non-authoritative ones) would generate potential conflicts, where each stage could try to overwrite and negate the bindings applied by the other at each `apply` cycle. + +### Automation project and resources + +One other design choice worth mentioning here is using a single automation project for all foundational stages. We trade off some complexity on the API side (single source for usage quota, multiple service activation) for increased flexibility and simpler operations, while still effectively providing the same degree of separation via resource-level IAM. + +### Billing account + +We support three use cases in regards to billing: + +- the billing account is part of this same organization, IAM bindings will be set at the organization level +- the billing account is part of a different organization, billing IAM bindings will be set at the organization level in the billing account owning organization +- the billing account is not considered part of an organization (even though it might be), billing IAM bindings are set on the billing account itself + +For same-organization billing, we configure a custom organization role that can set IAM bindings, via a delegated role grant to limit its scope to the relevant roles. + +For details on configuring the different billing account modes, refer to the [How to run this stage](#how-to-run-this-stage) section below. + +### Naming + +We are intentionally not supporting random prefix/suffixes for names, as that is an antipattern typically only used in development. It does not map to our customer's actual production usage, where they always adopt a fixed naming convention. + +What is implemented here is a fairly common convention, composed of tokens ordered by relative importance: + +- a static prefix (e.g. `myco` or `myco-gcp`) +- an environment identifier (e.g. `prod`) +- a team/owner identifier (e.g. `sec` for Security) +- a context identifier (e.g. `core` or `kms`) +- an arbitrary identifier used to distinguish similar resources (e.g. `0`, `1`) + +Tokens are joined by a `-` character, making it easy to separate the individual tokens visually, and to programmatically split them in billing exports to derive initial high-level groupings for cost attribution. + +The convention is used in its full form only for specific resources with globally unique names (projects, GCS buckets). Other resources adopt a shorter version for legibility, as the full context can always be derived from their project. + +The [Customizations](#names-and-naming-convention) section on names below explains how to configure tokens, or implement a different naming convention. + +## How to run this stage + +This stage has straightforward initial requirements, as it is designed to work on newly created GCP organizations. Four steps are needed to bring up this stage: + +- an Organization Admin self-assigns the required roles listed below +- the same administrator runs the first `init/apply` sequence passing a special variable to `apply` +- the providers configuration file is derived from the Terraform output or linked from the generated file +- a second `init` is run to migrate state, and from then on, the stage is run via impersonation + +### Prerequisites + +The roles that the Organization Admin used in the first `apply` needs to self-grant are: + +- Billing Account Administrator (`roles/billing.admin`) + either on the organization or the billing account (see the following section for details) +- Logging Admin (`roles/logging.admin`) +- Organization Role Administrator (`roles/iam.organizationRoleAdmin`) +- Organization Administrator (`roles/resourcemanager.organizationAdmin`) +- Project Creator (`roles/resourcemanager.projectCreator`) + +To quickly self-grant the above roles, run the following code snippet as the initial Organization Admin: + +```bash +export BOOTSTRAP_ORG_ID=123456 +export BOOTSTRAP_USER=$(gcloud config list --format 'value(core.account)') +export BOOTSTRAP_ROLES=(roles/billing.admin roles/logging.admin roles/iam.organizationRoleAdmin roles/resourcemanager.projectCreator) +for role in $BOOTSTRAP_ROLES; do + gcloud organizations add-iam-policy-binding $BOOTSTRAP_ORG_ID \ + --member user:$BOOTSTRAP_USER --role $role +done +``` + +#### Billing account in a different organization + +If you are using a billing account belonging to a different organization (e.g. in multiple organization setups), some initial configurations are needed to ensure the identities running this stage can assign billing-related roles. + +If the billing organization is managed by another version of this stage, we leverage the `organizationIamAdmin` role created there, to allow restricted granting of billing roles at the organization level. + +If that's not the case, an equivalent role needs to exist, or the predefined `resourcemanager.organizationAdmin` role can be used if not managed authoritatively. The role name then needs to be manually changed in the `billing.tf` file, in the `google_organization_iam_binding` resource. + +The identity applying this stage for the first time also needs two roles in billing organization, they can be removed after the first `apply` completes successfully: + +```bash +export BILLING_ORG_ID=789012 +export BILLING_ROLES=(roles/billing.admin roles/resourcemanager.organizationAdmin) +for role in $BILLING_ROLES; do + gcloud organizations add-iam-policy-binding $BILLING_ORG_ID \ + --member user:$BOOTSTRAP_USER --role $role +done +``` + +#### Standalone billing account + +If you are using a standalone billing account, the identity applying this stage for the first time needs to be a billing account administrator: + +```bash +export BILLING_ACCOUNT_ID=ABCD-01234-ABCD +gcloud beta billing accounts add-iam-policy-binding $BILLING_ACCOUNT \ + --member user:$BOOTSTRAP_USER --role roles/billing.admin +``` + +#### Groups + +Before the first run, the following IAM groups must exist to allow IAM bindings to be created (actual names are flexible, see the [Customization](#customizations) section): + +- gcp-billing-admins +- gcp-devops +- gcp-network-admins +- gcp-organization-admins +- gcp-security-admins +- gcp-support + +#### Configure variables + +Then make sure you have configured the correct values for the following variables by editing providing a `terraform.tfvars` file: + +- `billing_account` + an object containing the id of your billing account, derived from the Cloud Console UI or by running `gcloud beta billing accounts list`, and the id of the organization owning it, or `null` to use the billing account in isolation +- `groups` + the name mappings for your groups, if you're following the default convention you can leave this to the provided default +- `organization.id`, `organization.domain`, `organization.customer_id` + the id, domain and customer id of your organization, derived from the Cloud Console UI or by running `gcloud organizations list` +- `prefix` + the fixed prefix used in your naming convention + +### Output files and cross-stage variables + +At any time during the life of this stage, you can configure it to automatically generate provider configurations and variable files for the following, to simplify exchanging inputs and outputs between stages and avoid having to edit files manually. + +Automatic generation of files is disabled by default. To enable the mechanism, set the `outputs_location` variable to a valid path on a local filesystem, e.g. + +```hcl +outputs_location = "../../configs" +``` + +Once the variable is set, `apply` will generate and manage providers and variables files, including the initial one used for this stage after the first run. You can then link these files in the relevant stages, instead of manually transfering outputs from one stage, to Terraform variables in another. + +Below is the outline of the output files generated by this stage: + +```bash +[path specified in outputs_location] +├── 00-bootstrap +│   ├── providers.tf +├── 01-resman +│   ├── providers.tf +│   ├── terraform-bootstrap.auto.tfvars.json +├── 02-networking +│   ├── providers.tf +│   ├── terraform-bootstrap.auto.tfvars.json +├── 02-security +│   ├── providers.tf +│   ├── terraform-bootstrap.auto.tfvars.json +├── 03-project-factory-dev +│   └── terraform-bootstrap.auto.tfvars.json +├── 03-project-factory-prod +│   └── terraform-bootstrap.auto.tfvars.json +``` + +### Running the stage + +The first `apply` run as a user needs a special runtime variable, so that the user roles are preserved when setting IAM bindings: + +```bash +terraform init +terraform apply \ + -var bootstrap_user=$(gcloud config list --format 'value(core.account)') +``` + +Once the initial `apply` completes successfully, configure a remote backend using the new GCS bucket, and impersonation on the automation service account for this stage. To do this, you can use the generated `providers.tf` file if you have configured output files as described above, or extract its contents from Terraform's output, then migrate state with `terraform init`: + +```bash +# if using output files via the outputs_location variable +ln -s [path set in outputs_location]/00-bootstrap/* ./ +# or from outputs if not using output files +terraform output -json providers | jq -r '.["00-bootstrap"]' \ + > providers.tf +# migrate state to GCS bucket configured in providers file +terraform init -migrate-state +``` + +## Customizations + +Most variables (e.g. `billing_account` and `organization`) are only used to input actual values and should be self-explanatory. The only meaningful customizations that apply here are groups, and IAM roles. + +### Group names + +As we mentioned above, groups reflect the convention used in the [GCP Enterprise Setup Checklist](https://cloud.google.com/docs/enterprise/setup-checklist), with an added level of indirection: the `groups` variable maps logical names to actual names, so that you don't need to delve into the code if your group names do not comply with the checklist convention. + +For example, if your network admins team is called `net-rockstars@example.com`, simply set that name in the variable, minus the domain which is interpolated internally with the organization domain: + +```hcl +variable "groups" { + description = "Group names to grant organization-level permissions." + type = map(string) + default = { + gcp-network-admins = "net-rockstars" + # [...] + } +} +``` + +If your groups layout differs substantially from the checklist, define all relevant groups in the `groups` variable, then rearrange IAM roles in the code to match your setup. + +### IAM + +One other area where we directly support customizations is IAM. The code here, as in all stages, follows a simple pattern derived from best practices: + +- operational roles for humans are assigned to groups +- any other principal is a service account + +In code, the distinction above reflects on how IAM bindings are specified in the underlying module variables: + +- group roles "for humans" always use `iam_groups` variables +- service account roles always use `iam` variables + +This makes it easy to tweak user roles by adding mappings to the `iam_groups` variables of the relevant resources, without having to understand and deal with the details of service account roles. + +In those cases where roles need to be assigned to end-user service accounts (e.g. an application or pipeline service account), we offer a stage-level `iam` variable that allows pinpointing individual role/members pairs, without having to touch the code internals, to avoid the risk of breaking a critical role for a robot account. The variable can also be used to assign roles to specific users or to groups external to the organization, e.g. to support external suppliers. + +The one exception to this convention is for roles which are part of the delegated grant condition described above, and which can then be assigned from other stages. In this case, use the `iam_additive` variable as they are implemented with non-authoritative resources. Using non-authoritative bindings ensure that re-executing this stage will not override any bindings set in downstream stages. + +### Names and naming convention + +Configuring the individual tokens for the naming convention described above, has varying degrees of complexity: + +- the static prefix can be set via the `prefix` variable once +- the environment identifier is set to `prod` as resources here influence production and are considered as such, and can be changed in `main.tf` locals + +All other tokens are set directly in resource names, as providing abstractions to manage them would have added too much complexity to the code, making it less readable and more fragile. + +If a different convention is needed, identify names via search/grep (e.g. with `^\s+name\s+=\s+"`) and change them in an editor: it should take a couple of minutes at most, as there's just a handful of modules and resources to change. + +Names used in internal references (e.g. `module.foo-prod.id`) are only used by Terraform and do not influence resource naming, so they are best left untouched to avoid having to debug complex errors. + + + + +## Files + +| 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 | +| [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. | | | +| [organization.tf](./organization.tf) | Organization-level IAM and org policies. | organization | google_organization_iam_binding | +| [outputs.tf](./outputs.tf) | Module outputs. | | local_file | +| [variables.tf](./variables.tf) | Module variables. | | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| billing_account | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | | +| organization | Organization details. | object({…}) | ✓ | | | +| prefix | Prefix used for resources that need unique names. | string | ✓ | | | +| bootstrap_user | Email of the nominal user running this stage for the first time. | string | | null | | +| groups | Group names to grant organization-level permissions. | map(string) | | {…} | | +| iam | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | +| iam_additive | Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string)) | | {} | | +| log_sinks | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | +| outputs_location | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| billing_dataset | BigQuery dataset prepared for billing export. | | | +| project_ids | Projects created by this stage. | | | +| providers | Terraform provider files for this stage and dependent stages. | ✓ | stage-01 | +| tfvars | Terraform variable files for the following stages. | ✓ | | + + + + + + + diff --git a/fast/stages/00-bootstrap/automation.tf b/fast/stages/00-bootstrap/automation.tf new file mode 100644 index 00000000..f55cfdc5 --- /dev/null +++ b/fast/stages/00-bootstrap/automation.tf @@ -0,0 +1,107 @@ +/** + * 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. + */ + +# tfdoc:file:description Automation project and resources. + +module "automation-project" { + source = "../../../modules/project" + billing_account = var.billing_account.id + name = "iac-core-0" + parent = "organizations/${var.organization.id}" + prefix = local.prefix + # human (groups) IAM bindings + group_iam = { + (local.groups.gcp-devops) = [ + "roles/iam.serviceAccountAdmin", + "roles/iam.serviceAccountTokenCreator", + ] + (local.groups.gcp-organization-admins) = [ + "roles/iam.serviceAccountTokenCreator", + ] + } + # machine (service accounts) IAM bindings + iam = { + "roles/owner" = [module.automation-tf-bootstrap-sa.iam_email] + "roles/iam.serviceAccountAdmin" = [ + module.automation-tf-bootstrap-sa.iam_email, + module.automation-tf-resman-sa.iam_email + ] + "roles/storage.admin" = [ + module.automation-tf-bootstrap-sa.iam_email, + 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", + "cloudkms.googleapis.com", + "cloudresourcemanager.googleapis.com", + "compute.googleapis.com", + "essentialcontacts.googleapis.com", + "iam.googleapis.com", + "pubsub.googleapis.com", + "servicenetworking.googleapis.com", + "serviceusage.googleapis.com", + "stackdriver.googleapis.com", + "storage-component.googleapis.com", + "storage.googleapis.com", + ] +} + +# this stage's bucket and service account + +module "automation-tf-bootstrap-gcs" { + source = "../../../modules/gcs" + project_id = module.automation-project.project_id + name = "iac-core-bootstrap-0" + prefix = local.prefix + versioning = true + depends_on = [module.organization] +} + +module "automation-tf-bootstrap-sa" { + source = "../../../modules/iam-service-account" + project_id = module.automation-project.project_id + name = "bootstrap-0" + description = "Terraform organization bootstrap service account." + prefix = local.prefix +} + +# resource hierarchy stage's 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 + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.automation-tf-resman-sa.iam_email] + } + depends_on = [module.organization] +} + +module "automation-tf-resman-sa" { + source = "../../../modules/iam-service-account" + project_id = module.automation-project.project_id + name = "resman-0" + description = "Terraform organization bootstrap service account." + prefix = local.prefix +} diff --git a/fast/stages/00-bootstrap/billing.tf b/fast/stages/00-bootstrap/billing.tf new file mode 100644 index 00000000..6f163ad2 --- /dev/null +++ b/fast/stages/00-bootstrap/billing.tf @@ -0,0 +1,102 @@ +/** + * 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. + */ + +# tfdoc:file:description Billing export project and dataset. + +locals { + # used here for convenience, in organization.tf members are explicit + billing_ext_admins = [ + local.groups_iam.gcp-organization-admins, + module.automation-tf-bootstrap-sa.iam_email, + module.automation-tf-resman-sa.iam_email + ] +} + +# billing account in same org (IAM is in the organization.tf file) + +module "billing-export-project" { + source = "../../../modules/project" + count = local.billing_org ? 1 : 0 + billing_account = var.billing_account.id + name = "billing-export-0" + parent = "organizations/${var.organization.id}" + prefix = local.prefix + iam = { + "roles/owner" = [module.automation-tf-bootstrap-sa.iam_email] + } + services = [ + # "cloudresourcemanager.googleapis.com", + # "iam.googleapis.com", + # "serviceusage.googleapis.com", + "bigquery.googleapis.com", + "bigquerydatatransfer.googleapis.com", + "storage.googleapis.com" + ] +} + +module "billing-export-dataset" { + source = "../../../modules/bigquery-dataset" + count = local.billing_org ? 1 : 0 + project_id = module.billing-export-project.0.project_id + id = "billing_export" + friendly_name = "Billing export." +} + +# 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 = "organizations/${var.billing_account.organization_id}/roles/organizationIamAdmin" + 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 : [] + ) + billing_account_id = var.billing_account.id + role = "roles/billing.admin" + member = each.key +} diff --git a/fast/stages/00-bootstrap/diagram.png b/fast/stages/00-bootstrap/diagram.png new file mode 100644 index 00000000..932dfa8d Binary files /dev/null and b/fast/stages/00-bootstrap/diagram.png differ diff --git a/fast/stages/00-bootstrap/diagram.svg b/fast/stages/00-bootstrap/diagram.svg new file mode 100644 index 00000000..06fbe800 --- /dev/null +++ b/fast/stages/00-bootstrap/diagram.svg @@ -0,0 +1,807 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages/00-bootstrap/log-export.tf b/fast/stages/00-bootstrap/log-export.tf new file mode 100644 index 00000000..682d473d --- /dev/null +++ b/fast/stages/00-bootstrap/log-export.tf @@ -0,0 +1,73 @@ +/** + * 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. + */ + +# tfdoc:file:description Audit log project and sink. + +locals { + log_types = toset([for k, v in var.log_sinks : v.type]) +} + +module "log-export-project" { + source = "../../../modules/project" + name = "audit-logs-0" + parent = "organizations/${var.organization.id}" + prefix = local.prefix + billing_account = var.billing_account.id + iam = { + "roles/owner" = [module.automation-tf-bootstrap-sa.iam_email] + } + 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." +} + +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 +} + +module "log-export-logbucket" { + source = "../../../modules/logging-bucket" + count = contains(local.log_types, "logging") ? 1 : 0 + parent_type = "project" + parent = module.log-export-project.project_id + id = "audit-logs-0" +} + +module "log-export-pubsub" { + source = "../../../modules/pubsub" + for_each = toset([for k, v in var.log_sinks : k if v == "pubsub"]) + project_id = module.log-export-project.project_id + name = "audit-logs-${each.key}" +} diff --git a/fast/stages/00-bootstrap/main.tf b/fast/stages/00-bootstrap/main.tf new file mode 100644 index 00000000..49fa9140 --- /dev/null +++ b/fast/stages/00-bootstrap/main.tf @@ -0,0 +1,32 @@ +/** + * 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. + */ + +locals { + groups = { + for k, v in var.groups : + k => "${v}@${var.organization.domain}" + } + groups_iam = { + 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/00-bootstrap/organization.tf new file mode 100644 index 00000000..04be206a --- /dev/null +++ b/fast/stages/00-bootstrap/organization.tf @@ -0,0 +1,205 @@ +/** + * 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. + */ + +# tfdoc:file:description Organization-level IAM and org policies. + +locals { + # organization authoritative IAM bindings, in an easy to edit format before + # they are combined with var.iam a bit further in locals + _iam = { + "roles/browser" = [ + "domain:${var.organization.domain}" + ] + "roles/logging.admin" = [ + module.automation-tf-bootstrap-sa.iam_email + ] + "roles/owner" = local._iam_bootstrap_user + "roles/resourcemanager.folderAdmin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/resourcemanager.organizationAdmin" = concat( + [module.automation-tf-bootstrap-sa.iam_email], + local._iam_bootstrap_user + ) + "roles/resourcemanager.organizationViewer" = [ + "domain:${var.organization.domain}" + ] + } + # organization additive IAM bindings, in an easy to edit format before + # they are combined with var.iam_additive a bit further in locals + _iam_additive = merge( + { + "roles/accesscontextmanager.policyAdmin" = [ + local.groups_iam.gcp-security-admins + ] + "roles/compute.orgFirewallPolicyAdmin" = [ + local.groups_iam.gcp-network-admins + ] + "roles/compute.xpnAdmin" = [ + local.groups_iam.gcp-network-admins + ] + # use additive to support cross-org roles for billing + "roles/iam.organizationRoleAdmin" = [ + local.groups_iam.gcp-security-admins, + module.automation-tf-bootstrap-sa.iam_email + ] + "roles/orgpolicy.policyAdmin" = [ + module.automation-tf-resman-sa.iam_email, + local.groups_iam.gcp-security-admins + ] + }, + local.billing_org ? { + "roles/billing.admin" = [ + local.groups_iam.gcp-organization-admins, + module.automation-tf-bootstrap-sa.iam_email, + module.automation-tf-resman-sa.iam_email + ] + } : {} + ) + _iam_bootstrap_user = ( + var.bootstrap_user == null ? [] : ["user:${var.bootstrap_user}"] + ) + _log_sink_destinations = { + bigquery = try(module.log-export-dataset.0.id, null), + logging = try(module.log-export-logbucket.0.id, null), + storage = try(module.log-export-gcs.0.name, null) + } + iam = { + for role in local.iam_roles : role => distinct(concat( + try(sort(local._iam[role]), []), + try(sort(var.iam[role]), []) + )) + } + iam_additive = { + for role in local.iam_roles_additive : role => distinct(concat( + try(sort(local._iam_additive[role]), []), + try(sort(var.iam_additive[role]), []) + )) + } + iam_roles = distinct(concat( + keys(local._iam), keys(var.iam) + )) + iam_roles_additive = distinct(concat( + keys(local._iam_additive), keys(var.iam_additive) + )) + log_sink_destinations = { + for k, v in var.log_sinks : k => ( + v.type == "pubsub" + ? module.log-export-pubsub[k] + : local._log_sink_destinations[v.type] + ) + } +} + +module "organization" { + source = "../../../modules/organization" + organization_id = "organizations/${var.organization.id}" + # human (groups) IAM bindings + group_iam = { + (local.groups.gcp-organization-admins) = [ + "roles/cloudasset.owner", + "roles/cloudsupport.admin", + "roles/compute.osAdminLogin", + "roles/compute.osLoginExternalUser", + "roles/owner", + "roles/resourcemanager.folderAdmin", + "roles/resourcemanager.organizationAdmin", + "roles/resourcemanager.projectCreator", + ], + (local.groups.gcp-network-admins) = [ + "roles/cloudasset.owner", + "roles/cloudsupport.techSupportEditor", + ] + (local.groups.gcp-security-admins) = [ + "roles/cloudasset.owner", + "roles/cloudsupport.techSupportEditor", + "roles/iam.securityReviewer", + "roles/logging.admin", + "roles/securitycenter.admin", + ], + (local.groups.gcp-support) = [ + "roles/cloudsupport.techSupportEditor", + "roles/logging.viewer", + "roles/monitoring.viewer", + ] + } + # machine (service accounts) IAM bindings + iam = local.iam + # additive bindings, used for roles co-managed by different stages + iam_additive = local.iam_additive + custom_roles = { + # this is needed for use in additive IAM bindings, to avoid conflicts + "organizationIamAdmin" = [ + "resourcemanager.organizations.get", + "resourcemanager.organizations.getIamPolicy", + "resourcemanager.organizations.setIamPolicy" + ] + "xpnServiceAdmin" = [ + "compute.globalOperations.get", + "compute.organizations.disableXpnResource", + "compute.organizations.enableXpnResource", + "compute.projects.get", + "compute.subnetworks.getIamPolicy", + "compute.subnetworks.setIamPolicy", + "dns.networks.bindPrivateDNSZone", + "resourcemanager.projects.get", + ] + } + logging_sinks = { + for name, attrs in var.log_sinks : name => { + bq_partitioned_table = attrs.type == "bigquery" + destination = local.log_sink_destinations[name] + exclusions = {} + filter = attrs.filter + iam = true + include_children = true + type = attrs.type + } + } +} + +# assign the custom restricted Organization Admin role to the relevant service +# accounts, with a condition that only enables granting specific roles; +# these roles use additive bindings everywhere to avoid conflicts / permadiffs + +resource "google_organization_iam_binding" "org_admin_delegated" { + org_id = var.organization.id + count = local.billing_org ? 1 : 0 + role = module.organization.custom_role_id.organizationIamAdmin + 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'", concat( + [ + "roles/accesscontextmanager.policyAdmin", + "roles/compute.orgFirewallPolicyAdmin", + "roles/compute.xpnAdmin", + "roles/orgpolicy.policyAdmin", + module.organization.custom_role_id.xpnServiceAdmin + ], + local.billing_org ? [ + "roles/billing.admin", + "roles/billing.costsManager", + "roles/billing.user", + ] : [] + ))) + ) + } + depends_on = [module.organization] +} diff --git a/fast/stages/00-bootstrap/outputs.tf b/fast/stages/00-bootstrap/outputs.tf new file mode 100644 index 00000000..c25b9013 --- /dev/null +++ b/fast/stages/00-bootstrap/outputs.tf @@ -0,0 +1,113 @@ +/** + * 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. + */ + +locals { + providers = { + "00-bootstrap" = templatefile("${path.module}/../../assets/templates/providers.tpl", { + bucket = module.automation-tf-bootstrap-gcs.name + name = "bootstrap" + sa = module.automation-tf-bootstrap-sa.email + }) + "01-resman" = templatefile("${path.module}/../../assets/templates/providers.tpl", { + bucket = module.automation-tf-resman-gcs.name + name = "resman" + sa = module.automation-tf-resman-sa.email + }) + } + tfvars = { + "01-resman" = jsonencode({ + automation_project_id = module.automation-project.project_id + billing_account = var.billing_account + custom_roles = module.organization.custom_role_id + groups = var.groups + organization = var.organization + prefix = var.prefix + }) + "02-networking" = jsonencode({ + billing_account_id = var.billing_account.id + organization = var.organization + prefix = var.prefix + }) + "02-security" = jsonencode({ + billing_account_id = var.billing_account.id + organization = var.organization + prefix = var.prefix + }) + "03-gke-multitenant-dev" = jsonencode({ + billing_account_id = var.billing_account.id + prefix = var.prefix + }) + "03-gke-multitenant-prod" = jsonencode({ + billing_account_id = var.billing_account.id + prefix = var.prefix + }) + "03-project-factory-dev" = jsonencode({ + billing_account_id = var.billing_account.id + prefix = var.prefix + }) + "03-project-factory-prod" = jsonencode({ + billing_account_id = var.billing_account.id + prefix = var.prefix + }) + } +} + +# optionally generate providers and tfvars files for subsequent stages + +resource "local_file" "providers" { + for_each = var.outputs_location == null ? {} : local.providers + filename = "${var.outputs_location}/${each.key}/providers.tf" + content = each.value +} + +resource "local_file" "tfvars" { + for_each = var.outputs_location == null ? {} : local.tfvars + filename = "${var.outputs_location}/${each.key}/terraform-bootstrap.auto.tfvars.json" + content = each.value +} + +# outputs + +output "billing_dataset" { + description = "BigQuery dataset prepared for billing export." + value = try(module.billing-export-dataset.0.id, null) +} + +output "project_ids" { + description = "Projects created by this stage." + value = { + automation = module.automation-project.project_id + billing-export = try(module.billing-export-project.0.project_id, null) + log-export = module.log-export-project.project_id + } +} + +# ready to use provider configurations for subsequent stages when not using files + +output "providers" { + # tfdoc:output:consumers stage-01 + description = "Terraform provider files for this stage and dependent stages." + sensitive = true + value = local.providers +} + +# 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/00-bootstrap/variables.tf b/fast/stages/00-bootstrap/variables.tf new file mode 100644 index 00000000..9f102c77 --- /dev/null +++ b/fast/stages/00-bootstrap/variables.tf @@ -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. + */ + +variable "billing_account" { + description = "Billing account id and organization id ('nnnnnnnn' or null)." + type = object({ + id = string + organization_id = number + }) +} + +variable "bootstrap_user" { + description = "Email of the nominal user running this stage for the first time." + type = string + default = null +} + +variable "groups" { + # https://cloud.google.com/docs/enterprise/setup-checklist + description = "Group names to grant organization-level permissions." + type = map(string) + 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" + } +} + +variable "iam" { + description = "Organization-level custom IAM settings in role => [principal] format." + type = map(list(string)) + default = {} +} + +variable "iam_additive" { + description = "Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings." + type = map(list(string)) + default = {} +} + +variable "log_sinks" { + description = "Org-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 = "bigquery" + } + vpc-sc = { + filter = "protoPayload.metadata.@type=\"type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata\"" + type = "bigquery" + } + } + 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" { + description = "Organization details." + type = object({ + domain = string + id = number + customer_id = string + }) +} + +variable "outputs_location" { + description = "Path where providers and tfvars files for the following stages are written. Leave empty to disable." + type = string + default = null +} + +variable "prefix" { + description = "Prefix used for resources that need unique names." + type = string +} diff --git a/fast/stages/01-resman/README.md b/fast/stages/01-resman/README.md new file mode 100644 index 00000000..f5e69cb9 --- /dev/null +++ b/fast/stages/01-resman/README.md @@ -0,0 +1,196 @@ +# Resource hierarchy + +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 + +The code is intentionally simple, as it's intended to provide a generic initial setup (Networking, Security, etc.), and then allow easy customizations to complete the implementation of the intended hierarchy design. + +The following diagram is a high level reference of the resources created and managed here: + +

+ Resource-management diagram +

+ +## Design overview and choices + +Despite its simplicity, this stage implements the basics of a design that we've seen working well for a variety of customers, where the hierarchy is laid out following two conceptually different approaches: + +- core or shared resources are grouped in hierarchy branches that map to their type or purpose (e.g. Networking) +- team or application resources are grouped in lower level hierarchy branches that map to management or operational considerations (e.g. which team manages a set of applications, or owns a subset of company data, etc.) + +This split approach usually represents well functional and operational patterns, where core resources are centrally managed by individual teams (e.g. networking, security, fleets of similar VMS, etc.), while teams need more granularity to access managed services used by the applications they maintain. + +The approach also adapts to different high level requirements: + +- it can be used either for single organizations containing multiple environments, or with multiple organizations dedicated to specific environments (e.g. prod/nonprod), as the environment split is implemented at the project or lower folder level +- it adapts to complex scenarios, with different countries or corporate entities using the same GCP organization, as core services are typically shared, and/or an extra layer on top can be used as a drop-in to implement the country/entity separation + +Additionally, a few critical benefits are directly provided by this design: + +- core services are clearly separated, with very few touchpoints where IAM and security policies need to be applied (typically their top-level folder) +- adding a new set of core services (e.g. shared GKE clusters) is a trivial operation that does not break the existing 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. + +## 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. + +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. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### Providers configuration + +The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during bootstrap, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). + +To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. + +If you have set a valid value for `outputs_location` in the bootstrap stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified: + +```bash +# `outputs_location` is set to `../../configs/example` +ln -s ../../configs/example/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 +terraform output -json providers | jq -r '.["01-resman"]' \ + > ../01-resman/providers.tf +``` + +### Variable configuration + +There are two broad sets of variables you will need to fill in: + +- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) +- variables specific to resources managed by this stage + +To avoid the tedious job of filling in the first group of 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 `terraform-*.auto.tfvars.json` files from this stage's outputs folder (under the path you specified), where the `*` above is set to the name of the stage that produced it. For this stage, a single `.tfvars` file is avalaible: + +```bash +# `outputs_location` is set to `../../configs/example` +ln -s ../../configs/example/01-resman/terraform-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. + +Once done, you can run this stage: + +```bash +terraform init +terraform apply +``` + +## Customizations + +### Team folders + +This stage provides a single built-in customization that offers a minimal (but usable) implementation of the "application" or "business" grouping for resources discussed above. The `team_folders` variable allows you to specify a map of team name and groups, that will result in folders, automation service accounts, and IAM policies applied. + +Consider the following example + +```hcl +team_folders = { + team-a = { + descriptive_name = "Team A" + group_iam = { + "team-a@gcp-pso-italy.net" = [ + "roles/viewer" + ] + } + impersonation_groups = ["team-a-admins@gcp-pso-italy.net"] + } +} +``` + +This will result in + +- a "Team A" folder under the "Teams" folder +- one GCS bucket in the automation project +- one service account in the automation project with the correct IAM policies on the folder and bucket +- a IAM policy on the folder that assigns `roles/viewer` to the `team-a` group +- a IAM policy on the service account that allows `team-a` to impersonate it + +This allows to centralize the minimum set of resources to delegate control of each team's folder to a pipeline, and/or to the team group. This can be used as a starting point for scenarios that implement more complex requirements (e.g. environment folders per team, etc.). + +### Organization policies + +Organization policies are laid out in an explicit manner in the `organization.tf` file, so it's fairly easy to add or remove specific policies. + +For policies where additional data is needed, a root-level `organization_policy_configs` variable allows passing in specific data. Its built-in use to add additional organizations to the [Domain Restricted Sharing](https://cloud.google.com/resource-manager/docs/organization-policy/restricting-domains) policy, can be taken as an example on how to leverage it for additional customizations. + +### 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. + +### Additional folders + +Due to its simplicity, this stage lends itself easily to customizations: adding a new top-level branch (e.g. for shared GKE clusters) is as easy as cloning one of the `branch-xxx.tf` files, and changing names. + + + + + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [billing.tf](./billing.tf) | Billing resources for external billing use cases. | organization | google_billing_account_iam_member | +| [branch-networking.tf](./branch-networking.tf) | Networking stage resources. | folder · gcs · iam-service-account | | +| [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 stages resources. | folder · gcs · iam-service-account | | +| [main.tf](./main.tf) | Module-level locals and resources. | | | +| [organization.tf](./organization.tf) | Organization policies. | organization | | +| [outputs.tf](./outputs.tf) | Module outputs. | | local_file | +| [variables.tf](./variables.tf) | Module variables. | | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| automation_project_id | Project id for the automation project created by the bootstrap stage. | string | ✓ | | 00-bootstrap | +| billing_account | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | +| organization | Organization details. | object({…}) | ✓ | | 00-bootstrap | +| prefix | Prefix used for resources that need unique names. | string | ✓ | | 00-bootstrap | +| custom_roles | Custom roles defined at the org level, in key => id format. | map(string) | | {} | 00-bootstrap | +| groups | Group names to grant organization-level permissions. | map(string) | | {…} | 00-bootstrap | +| organization_policy_configs | Organization policies customization. | object({…}) | | null | | +| outputs_location | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | +| team_folders | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| networking | Data for the networking stage. | | 02-networking | +| project_factories | Data for the project factories stage. | | xx-teams | +| providers | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking · 02-security · xx-sandbox · xx-teams | +| sandbox | Data for the sandbox stage. | | xx-sandbox | +| security | Data for the networking stage. | | 02-security | +| teams | Data for the teams stage. | | | +| tfvars | Terraform variable files for the following stages. | ✓ | | + + + + + + + + + + + diff --git a/fast/stages/01-resman/billing.tf b/fast/stages/01-resman/billing.tf new file mode 100644 index 00000000..5fcb39f4 --- /dev/null +++ b/fast/stages/01-resman/billing.tf @@ -0,0 +1,56 @@ +/** + * 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. + */ + +# tfdoc:file:description Billing resources for external billing use cases. + +locals { + # used here for convenience, in organization.tf members are explicit + billing_ext_users = concat( + [ + module.branch-network-sa.iam_email, + module.branch-security-sa.iam_email, + ], + # enable if individual teams can create their own projects + # [ + # for k, v in module.branch-teams-team-sa : v.iam_email + # ], + local.branch_teams_pf_sa_iam_emails + ) +} + +# 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 + } +} + +# standalone billing account + +resource "google_billing_account_iam_member" "billing_ext_admin" { + for_each = toset( + local.billing_ext ? local.billing_ext_users : [] + ) + billing_account_id = var.billing_account.id + role = "roles/billing.user" + member = each.key +} diff --git a/fast/stages/01-resman/branch-networking.tf b/fast/stages/01-resman/branch-networking.tf new file mode 100644 index 00000000..3b291197 --- /dev/null +++ b/fast/stages/01-resman/branch-networking.tf @@ -0,0 +1,59 @@ +/** + * 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. + */ + +# tfdoc:file:description Networking stage resources. + +module "branch-network-folder" { + source = "../../../modules/folder" + parent = "organizations/${var.organization.id}" + name = "Networking" + group_iam = { + (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" = [module.branch-network-sa.iam_email] + "roles/owner" = [module.branch-network-sa.iam_email] + "roles/resourcemanager.folderAdmin" = [module.branch-network-sa.iam_email] + "roles/resourcemanager.projectCreator" = [module.branch-network-sa.iam_email] + } +} + +module "branch-network-sa" { + source = "../../../modules/iam-service-account" + project_id = var.automation_project_id + name = "resman-networking-0" + description = "Terraform resman networking service account." + prefix = local.prefixes.prod +} + +module "branch-network-gcs" { + source = "../../../modules/gcs" + project_id = var.automation_project_id + name = "resman-networking-0" + prefix = local.prefixes.prod + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.branch-network-sa.iam_email] + } +} diff --git a/fast/stages/01-resman/branch-sandbox.tf b/fast/stages/01-resman/branch-sandbox.tf new file mode 100644 index 00000000..e40aa3fe --- /dev/null +++ b/fast/stages/01-resman/branch-sandbox.tf @@ -0,0 +1,59 @@ +/** + * 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. + */ + +# tfdoc:file:description Sandbox stage resources. + +module "branch-sandbox-folder" { + source = "../../../modules/folder" + parent = "organizations/${var.organization.id}" + name = "Sandbox" + iam = { + "roles/logging.admin" = [module.branch-sandbox-sa.iam_email] + "roles/owner" = [module.branch-sandbox-sa.iam_email] + "roles/resourcemanager.folderAdmin" = [module.branch-sandbox-sa.iam_email] + "roles/resourcemanager.projectCreator" = [module.branch-sandbox-sa.iam_email] + } + policy_boolean = { + "constraints/sql.restrictPublicIp" = false + } + policy_list = { + "constraints/compute.vmExternalIpAccess" = { + inherit_from_parent = false + suggested_value = null + status = true + values = [] + } + } +} + +module "branch-sandbox-gcs" { + source = "../../../modules/gcs" + project_id = var.automation_project_id + name = "resman-sandbox-0" + prefix = local.prefixes.dev + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.branch-sandbox-sa.iam_email] + } +} + +module "branch-sandbox-sa" { + source = "../../../modules/iam-service-account" + project_id = var.automation_project_id + name = "resman-sandbox-0" + description = "Terraform resman sandbox service account." + prefix = local.prefixes.dev +} diff --git a/fast/stages/01-resman/branch-security.tf b/fast/stages/01-resman/branch-security.tf new file mode 100644 index 00000000..94f68ecd --- /dev/null +++ b/fast/stages/01-resman/branch-security.tf @@ -0,0 +1,61 @@ +/** + * 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. + */ + +# tfdoc:file:description Security stage resources. + +module "branch-security-folder" { + source = "../../../modules/folder" + parent = "organizations/${var.organization.id}" + name = "Security" + group_iam = { + (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" = [module.branch-security-sa.iam_email] + "roles/owner" = [module.branch-security-sa.iam_email] + "roles/resourcemanager.folderAdmin" = [module.branch-security-sa.iam_email] + "roles/resourcemanager.projectCreator" = [module.branch-security-sa.iam_email] + } +} + +module "branch-security-sa" { + source = "../../../modules/iam-service-account" + project_id = var.automation_project_id + name = "resman-security-0" + description = "Terraform resman security service account." + prefix = local.prefixes.prod +} + +module "branch-security-gcs" { + source = "../../../modules/gcs" + project_id = var.automation_project_id + name = "resman-security-0" + prefix = local.prefixes.prod + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.branch-security-sa.iam_email] + } +} diff --git a/fast/stages/01-resman/branch-teams.tf b/fast/stages/01-resman/branch-teams.tf new file mode 100644 index 00000000..7967fc9b --- /dev/null +++ b/fast/stages/01-resman/branch-teams.tf @@ -0,0 +1,165 @@ +/** + * 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. + */ + +# tfdoc:file:description Team stages resources. + +# top-level teams folder and service account + +module "branch-teams-folder" { + source = "../../../modules/folder" + parent = "organizations/${var.organization.id}" + name = "Teams" +} + +module "branch-teams-prod-sa" { + source = "../../../modules/iam-service-account" + project_id = var.automation_project_id + name = "resman-teams-0" + description = "Terraform resman production service account." + prefix = local.prefixes.prod +} + +# Team-level folders, service accounts and buckets for each individual team + +module "branch-teams-team-folder" { + source = "../../../modules/folder" + for_each = coalesce(var.team_folders, {}) + parent = module.branch-teams-folder.id + name = each.value.descriptive_name + group_iam = each.value.group_iam == null ? {} : each.value.group_iam +} + +module "branch-teams-team-sa" { + source = "../../../modules/iam-service-account" + for_each = coalesce(var.team_folders, {}) + project_id = var.automation_project_id + name = "teams-${each.key}-0" + description = "Terraform team ${each.key} service account." + prefix = local.prefixes.prod + 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 = coalesce(var.team_folders, {}) + project_id = var.automation_project_id + name = "teams-${each.key}-0" + prefix = local.prefixes.prod + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.branch-teams-team-sa[each.key].iam_email] + } +} + +# environment: development folder and project factory automation resources + +module "branch-teams-team-dev-folder" { + source = "../../../modules/folder" + for_each = coalesce(var.team_folders, {}) + parent = module.branch-teams-team-folder[each.key].id + # naming: environment descriptive name + name = "${module.branch-teams-team-folder[each.key].name} - Development" + # environment-wide human permissions on the whole teams environment + group_iam = {} + iam = { + # remove owner here and at project level if SA does not manage project resources + "roles/owner" = [ + module.branch-teams-dev-projectfactory-sa.iam_email + ] + "roles/logging.admin" = [ + module.branch-teams-dev-projectfactory-sa.iam_email + ] + "roles/resourcemanager.folderAdmin" = [ + module.branch-teams-dev-projectfactory-sa.iam_email + ] + "roles/resourcemanager.projectCreator" = [ + module.branch-teams-dev-projectfactory-sa.iam_email + ] + } +} + +module "branch-teams-dev-projectfactory-sa" { + source = "../../../modules/iam-service-account" + project_id = var.automation_project_id + name = "resman-pf-0" + # naming: environment in description + description = "Terraform project factory development service account." + prefix = local.prefixes.dev +} + +module "branch-teams-dev-projectfactory-gcs" { + source = "../../../modules/gcs" + project_id = var.automation_project_id + name = "resman-pf-0" + prefix = local.prefixes.dev + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.branch-teams-dev-projectfactory-sa.iam_email] + } +} + +# environment: production folder and project factory automation resources + +module "branch-teams-team-prod-folder" { + source = "../../../modules/folder" + for_each = coalesce(var.team_folders, {}) + parent = module.branch-teams-team-folder[each.key].id + # naming: environment descriptive name + name = "${module.branch-teams-team-folder[each.key].name} - Production" + # environment-wide human permissions on the whole teams environment + group_iam = {} + iam = { + # remove owner here and at project level if SA does not manage project resources + "roles/owner" = [ + module.branch-teams-prod-projectfactory-sa.iam_email + ] + "roles/logging.admin" = [ + module.branch-teams-prod-projectfactory-sa.iam_email + ] + "roles/resourcemanager.folderAdmin" = [ + module.branch-teams-prod-projectfactory-sa.iam_email + ] + "roles/resourcemanager.projectCreator" = [ + module.branch-teams-prod-projectfactory-sa.iam_email + ] + } +} + +module "branch-teams-prod-projectfactory-sa" { + source = "../../../modules/iam-service-account" + project_id = var.automation_project_id + name = "resman-pf-0" + # naming: environment in description + description = "Terraform project factory production service account." + prefix = local.prefixes.prod +} + +module "branch-teams-prod-projectfactory-gcs" { + source = "../../../modules/gcs" + project_id = var.automation_project_id + name = "resman-pf-0" + prefix = local.prefixes.prod + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.branch-teams-prod-projectfactory-sa.iam_email] + } +} diff --git a/fast/stages/01-resman/diagram.png b/fast/stages/01-resman/diagram.png new file mode 100644 index 00000000..d1026318 Binary files /dev/null and b/fast/stages/01-resman/diagram.png differ diff --git a/fast/stages/01-resman/diagram.svg b/fast/stages/01-resman/diagram.svg new file mode 100644 index 00000000..541db3f4 --- /dev/null +++ b/fast/stages/01-resman/diagram.svg @@ -0,0 +1,1340 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages/01-resman/main.tf b/fast/stages/01-resman/main.tf new file mode 100644 index 00000000..2aedb7ce --- /dev/null +++ b/fast/stages/01-resman/main.tf @@ -0,0 +1,35 @@ +/** + * 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. + */ + +locals { + # 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 + groups = { + for k, v in var.groups : + k => "${v}@${var.organization.domain}" + } + groups_iam = { + for k, v in local.groups : + k => "group:${v}" + } + # naming: environment names + prefixes = { + dev = "${var.prefix}-dev" + prod = "${var.prefix}-prod" + } +} diff --git a/fast/stages/01-resman/organization.tf b/fast/stages/01-resman/organization.tf new file mode 100644 index 00000000..f96ad16c --- /dev/null +++ b/fast/stages/01-resman/organization.tf @@ -0,0 +1,136 @@ +/** + * 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. + */ + +# tfdoc:file:description Organization policies. + + +locals { + # set to the empty list if you remove the teams branch + branch_teams_pf_sa_iam_emails = [ + module.branch-teams-dev-projectfactory-sa.iam_email, + module.branch-teams-prod-projectfactory-sa.iam_email + ] + list_deny = { + inherit_from_parent = false + suggested_value = null + status = false + values = [] + } + policy_configs = ( + var.organization_policy_configs == null + ? {} + : var.organization_policy_configs + ) +} + +module "organization" { + source = "../../../modules/organization" + organization_id = "organizations/${var.organization.id}" + # IAM additive bindings, granted via the restricted Organization Admin custom + # role assigned in stage 00; they need to be additive to avoid conflicts + iam_additive = merge( + { + (var.custom_roles.xpnServiceAdmin) = concat( + local.branch_teams_pf_sa_iam_emails + ) + "roles/accesscontextmanager.policyAdmin" = [ + module.branch-security-sa.iam_email + ] + "roles/billing.costsManager" = concat( + local.branch_teams_pf_sa_iam_emails + ), + "roles/compute.orgFirewallPolicyAdmin" = [ + module.branch-network-sa.iam_email + ] + "roles/compute.xpnAdmin" = [ + module.branch-network-sa.iam_email + ] + "roles/orgpolicy.policyAdmin" = local.branch_teams_pf_sa_iam_emails + }, + local.billing_org ? { + "roles/billing.user" = concat( + [ + module.branch-network-sa.iam_email, + module.branch-security-sa.iam_email, + ], + # enable if individual teams can create their own projects + # [ + # for k, v in module.branch-teams-team-sa : v.iam_email + # ], + local.branch_teams_pf_sa_iam_emails + ) + } : {} + ) + # sample subset of useful organization policies, edit to suit requirements + policy_boolean = { + "constraints/cloudfunctions.requireVPCConnector" = true + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.disableInternetNetworkEndpointGroup" = true + "constraints/compute.disableNestedVirtualization" = true + "constraints/compute.disableSerialPortAccess" = true + "constraints/compute.requireOsLogin" = true + "constraints/compute.restrictXpnProjectLienRemoval" = true + "constraints/compute.skipDefaultNetworkCreation" = true + "constraints/iam.automaticIamGrantsForDefaultServiceAccounts" = true + "constraints/iam.disableServiceAccountKeyCreation" = true + "constraints/iam.disableServiceAccountKeyUpload" = true + "constraints/sql.restrictPublicIp" = true + "constraints/sql.restrictAuthorizedNetworks" = true + "constraints/storage.uniformBucketLevelAccess" = true + } + policy_list = { + "constraints/cloudfunctions.allowedIngressSettings" = merge( + local.list_deny, { values = ["ALLOW_INTERNAL_ONLY"] } + ) + "constraints/cloudfunctions.allowedVpcConnectorEgressSettings" = merge( + local.list_deny, { values = ["PRIVATE_RANGES_ONLY"] } + ) + "constraints/compute.restrictLoadBalancerCreationForTypes" = merge( + local.list_deny, { values = ["in:INTERNAL"] } + ) + "constraints/compute.vmExternalIpAccess" = local.list_deny + "constraints/iam.allowedPolicyMemberDomains" = { + inherit_from_parent = false + suggested_value = null + status = true + values = concat( + [var.organization.customer_id], + try(local.policy_configs.allowed_policy_member_domains, []) + ) + } + "constraints/run.allowedIngress" = merge( + local.list_deny, { values = ["internal"] } + ) + "constraints/run.allowedVPCEgress" = merge( + local.list_deny, { values = ["private-ranges-only"] } + ) + # "constraints/compute.restrictCloudNATUsage" = local.list_deny + # "constraints/compute.restrictDedicatedInterconnectUsage" = local.list_deny + # "constraints/compute.restrictPartnerInterconnectUsage" = local.list_deny + # "constraints/compute.restrictProtocolForwardingCreationForTypes" = local.list_deny + # "constraints/compute.restrictSharedVpcHostProjects" = local.list_deny + # "constraints/compute.restrictSharedVpcSubnetworks" = local.list_deny + # "constraints/compute.restrictVpcPeering" = local.list_deny + # "constraints/compute.restrictVpnPeerIPs" = local.list_deny + # "constraints/compute.vmCanIpForward" = local.list_deny + # "constraints/gcp.resourceLocations" = { + # inherit_from_parent = false + # suggested_value = null + # status = true + # values = local.allowed_regions + # } + } +} diff --git a/fast/stages/01-resman/outputs.tf b/fast/stages/01-resman/outputs.tf new file mode 100644 index 00000000..67fce0bc --- /dev/null +++ b/fast/stages/01-resman/outputs.tf @@ -0,0 +1,150 @@ +/** + * 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. + */ + +locals { + _project_factory_sas = { + dev = module.branch-teams-dev-projectfactory-sa.iam_email + prod = module.branch-teams-prod-projectfactory-sa.iam_email + } + providers = { + "02-networking" = templatefile("${path.module}/../../assets/templates/providers.tpl", { + bucket = module.branch-network-gcs.name + name = "networking" + sa = module.branch-network-sa.email + }) + "02-security" = templatefile("${path.module}/../../assets/templates/providers.tpl", { + bucket = module.branch-security-gcs.name + name = "security" + sa = module.branch-security-sa.email + }) + "99-sandbox" = templatefile("${path.module}/../../assets/templates/providers.tpl", { + bucket = module.branch-sandbox-gcs.name + name = "sandbox" + sa = module.branch-sandbox-sa.email + }) + "03-project-factory-dev" = templatefile("${path.module}/../../assets/templates/providers.tpl", { + bucket = module.branch-teams-dev-projectfactory-gcs.name + name = "team-dev" + sa = module.branch-teams-dev-projectfactory-sa.email + }) + "03-project-factory-prod" = templatefile("${path.module}/../../assets/templates/providers.tpl", { + bucket = module.branch-teams-prod-projectfactory-gcs.name + name = "team-prod" + sa = module.branch-teams-prod-projectfactory-sa.email + }) + } + tfvars = { + "02-networking" = jsonencode({ + folder_id = module.branch-network-folder.id + project_factory_sa = local._project_factory_sas + }) + "02-security" = jsonencode({ + folder_id = module.branch-security-folder.id + kms_restricted_admins = { + for k, v in local._project_factory_sas : k => [v] + } + }) + } +} + +# optionally generate providers and tfvars files for subsequent stages + +resource "local_file" "providers" { + for_each = var.outputs_location == null ? {} : local.providers + filename = "${var.outputs_location}/${each.key}/providers.tf" + content = each.value +} + +resource "local_file" "tfvars" { + for_each = var.outputs_location == null ? {} : local.tfvars + filename = "${var.outputs_location}/${each.key}/terraform-resman.auto.tfvars.json" + content = each.value +} + +# outputs + +output "networking" { + # tfdoc:output:consumers 02-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" { + # tfdoc:output:consumers xx-teams + description = "Data for the project factories stage." + value = { + dev = { + bucket = module.branch-teams-dev-projectfactory-gcs.name + sa = module.branch-teams-dev-projectfactory-sa.email + } + prod = { + bucket = module.branch-teams-prod-projectfactory-gcs.name + sa = module.branch-teams-prod-projectfactory-sa.email + } + } +} + +# ready to use provider configurations for subsequent stages + +output "providers" { + # tfdoc:output:consumers 02-networking 02-security 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 = { + folder = module.branch-sandbox-folder.id + gcs_bucket = module.branch-sandbox-gcs.name + service_account = module.branch-sandbox-sa.email + } +} + +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/01-resman/variables.tf b/fast/stages/01-resman/variables.tf new file mode 100644 index 00000000..c1d63c86 --- /dev/null +++ b/fast/stages/01-resman/variables.tf @@ -0,0 +1,104 @@ +/** + * 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. + */ + +# 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 "billing_account" { + # tfdoc:variable:source 00-bootstrap + description = "Billing account id and organization id ('nnnnnnnn' or null)." + type = object({ + id = string + organization_id = number + }) +} + +variable "automation_project_id" { + # tfdoc:variable:source 00-bootstrap + description = "Project id for the automation project created by the bootstrap stage." + type = string +} + +variable "custom_roles" { + # tfdoc:variable:source 00-bootstrap + description = "Custom roles defined at the org level, in key => id format." + type = map(string) + default = {} +} + +variable "groups" { + # tfdoc:variable:source 00-bootstrap + description = "Group names to grant organization-level permissions." + type = map(string) + # 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" + } +} + +variable "organization" { + # tfdoc:variable:source 00-bootstrap + description = "Organization details." + type = object({ + domain = string + id = number + customer_id = string + }) +} + +variable "organization_policy_configs" { + description = "Organization policies customization." + type = object({ + allowed_policy_member_domains = list(string) + }) + default = null +} + +variable "outputs_location" { + description = "Path where providers and tfvars files for the following stages are written. Leave empty to disable." + type = string + default = null +} + +variable "prefix" { + # tfdoc:variable:source 00-bootstrap + description = "Prefix used for resources that need unique names." + type = string +} + +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 + # default = { + # team-a = { + # descriptive_name = "Team A" + # group_iam = { + # team-a-group = [roles/owner, roles/projectCreator] + # } + # impersonation_groups = ["team-a-admins@example.com"] + # } + # } +} diff --git a/fast/stages/02-networking/README.md b/fast/stages/02-networking/README.md new file mode 100644 index 00000000..5d7b539b --- /dev/null +++ b/fast/stages/02-networking/README.md @@ -0,0 +1,338 @@ +# Networking + +This stage sets up the shared network infrastructure for the whole organization. It adopts the common “hub and spoke” reference design, which is well suited to multiple scenarios, and offers several advantages versus other designs: + +- the “hub” VPC centralizes external connectivity to on-prem or other cloud environments, and is ready to host cross-environment services like CI/CD, code repositories, and monitoring probes +- the “spoke” VPCs allow partitioning workloads (e.g. by environment like in this setup), while still retaining controlled access to central connectivity and services +- Shared VPC in both hub and spokes splits management of network resources in specific (host) projects, while still allowing them to be consumed from workload (service) projects +- the design also lends itself to easy DNS centralization, both from on-prem to cloud and from cloud to on-prem + +Connectivity between hub and spokes is established here via [VPN HA](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) tunnels, which offer easy interoperability with some key GCP features (GKE, services leveraging Service Networking like Cloud SQL, etc.), allowing clear partitioning of quota and limits between environments, and fine-grained control of routing. Different ways of implementing connectivity, and their respective pros and cons, are discussed below. + +The following diagram illustrates the high-level design, and should be used as a reference for the following sections. The final number of subnets, and their IP addressing design will of course depend on customer-specific requirements, and can be easily changed via variables or external data files without having to edit the actual code. + +

+ Networking diagram +

+ +## Design overview and choices + +### VPC design + +The hub/landing VPC hosts external connectivity and shared services for spoke VPCs, which are connected to it via VPN HA tunnels. Spokes are used here to partition environments, which is a fairly common pattern: + +- one spoke VPC for the production environment +- one spoke VPC for the development environment + +Each VPC is created into its own project, and each project is configured as a Shared VPC host, so that network-related resources and access configurations via IAM are kept separate for each VPC. + +The design easily lends itself to implementing additional environments, or adopting a different logical mapping for spokes (e.g. one spoke for each company entity, etc.). Adding spokes is a trivial operation, does not increase the design complexity, and is explained in operational terms in the following sections. + +In multi-organization scenarios, where production and non-production resources use different Cloud Identity and GCP organizations, the hub/landing VPC is usually part of the production organization, and establishes connections with production spokes in its same organization, and non-production spokes in a different organization. + +An additional VPC is also deployed by default with the provided code, disconnected from the other VPCs and hosting a single VM that emulates on-prem for testing purposes, via a Docker network and containers for VPN, DNS, HTTP, etc. + +### External connectivity + +External connectivity to on-prem is implemented here via VPN HA (two tunnels per region), as this is the minimum common denominator often used directly, or as a stop-gap solution to validate routing and transfer data, while waiting for [interconnects](https://cloud.google.com/network-connectivity/docs/interconnect) to be provisioned. + +Connectivity to additional on-prem sites or other cloud providers should be implemented in a similar fashion, via VPN tunnels or interconnects in the landing VPC sharing the same regional router. + +### Internal connectivity + +As mentioned initially, there are of course other ways to implement internal connectivity other than VPN HA. These can be easily retrofitted with minimal code changes, but introduce additional considerations for service interoperability, quotas and management. + +This is a summary of the main options: + +- [VPN HA](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) + - 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) + - 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 + +### IP ranges, subnetting, routing + +Minimizing the number of routes (and subnets) in use on the cloud environment is an important consideration, as it simplifies management and avoids hitting [Cloud Router](https://cloud.google.com/network-connectivity/docs/router/quotas) and [VPC](https://cloud.google.com/vpc/docs/quota) quotas and limits. For this reason, we recommend careful planning of the IP space used in your cloud environment, to be able to use large IP CIDR blocks in routes whenever possible. + +This stage uses a dedicated /16 block (which should of course be sized to your needs) for each region in each VPC, and subnets created in each VPC derive their ranges from the relevant block. + +Spoke VPCs also define and reserve two "special" CIDR ranges dedicated to [PSA (Private Service Access)](https://cloud.google.com/vpc/docs/private-services-access) and [Internal HTTPs Load Balancers (L7ILB)](https://cloud.google.com/load-balancing/docs/l7-internal). + +Routes in GCP are either automatically created for VPC subnets, manually created via static routes, or dynamically programmed by [Cloud Routers](https://cloud.google.com/network-connectivity/docs/router#docs) via BGP sessions, which can be configured to advertise VPC ranges, and/or custom ranges via custom advertisements. + +In this setup, the Cloud Routers are configured so as to exclude the default advertisement of VPC ranges, and they only advertise their respective aggregate ranges via custom advertisements. This greatly simplifies the routing configuration, and more importantly it allows to avoid quota or limit issues by keeping the number of routes small, instead of making it proportional to the subnets and secondary ranges in the VPCs. + +The high-level routing plan implemented in this architecture is as follows: + +| source | target | advertisement | +| ----------- | ----------- | ------------------------------ | +| VPC landing | onprem | GCP aggregate | +| VPC landing | onprem | Cloud DNS forwarders | +| VPC landing | onprem | Google private/restricted APIs | +| VPC landing | spokes | RFC1918 | +| VPC spoke | VPC landing | spoke aggregate | +| onprem | VC landing | onprem aggregates | + +As is evident from the table above, the hub/landing VPC acts as the route concentrator for the whole GCP network, implementing a full line of sight between environments, and between GCP and on-prem. While advertisements can be adjusted to selectively exchange routes (e.g. to isolate the production and the development environment), we recommend using [Firewall](#firewall) policies or rules to achieve the desired isolation. + +### Internet egress + +The path of least resistance for Internet egress is using Cloud NAT, and that is what's implemented in this setup, with a NAT gateway configured for each VPC. + +Several other scenarios are possible of course, with varying degrees of complexity: + +- a forward proxy, with optional URL filters +- a default route to on-prem to leverage existing egress infrastructure +- a full-fledged perimeter firewall to control egress and implement additional security features like IPS + +Future pluggable modules will allow to easily experiment, or deploy the above scenarios. + +### VPC and Hierarchical Firewall + +The GCP Firewall is a stateful, distributed feature that allows the creation of L4 policies, either via VPC-level rules or more recently via hierarchical policies applied on the resource hierarchy (organization, folders). + +The current setup adopts both firewall types, and uses hierarchical rules on the Networking folder for common ingress rules (egress is open by default), e.g. from health check or IAP forwarders ranges, and VPC rules for the environment or workload-level ingress. + +Rules and policies are defined in simple YAML files, described below. + +### DNS + +DNS often goes hand in hand with networking, especially on GCP where Cloud DNS zones and policies are associated at the VPC level. This setup implements both DNS flows: + +- on-prem to cloud via private zones for cloud-managed domains, and an [inbound policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) used as forwarding target or via delegation (requires some extra configuration) from on-prem DNS resolvers +- cloud to on-prem via forwarding zones for the on-prem managed domains + +DNS configuration is further centralized by leveraging peering zones, so that + +- the hub/landing Cloud DNS hosts configurations for on-prem forwarding and Google API domains, with the spokes consuming them via DNS peering zones +- the spokes Cloud DNS host configurations for the environment-specific domains, with the hub/landing VPC acting as consumer via DNS peering + +To complete the configuration, the 35.199.192.0/19 range should be routed on the VPN tunnels from on-prem, and the following names configured for DNS forwarding to cloud: + +- `private.googleapis.com` +- `restricted.googleapis.com` +- `gcp.example.com` (used as a placeholder) + +From cloud, the `example.com` domain (used as a placeholder) is forwarded to on-prem. + +This configuration is battle-tested, and flexible enough to lend itself to simple modifications without subverting its design, for example by forwarding and peering root zones to bypass Cloud DNS external resolution. + +## How to run this stage + +This stage is meant to be executed after the [resman](../01-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../00-boostrap) stage. + +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### Providers configuration + +The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during the [resource management](./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`). + +To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. + +If you have set a valid value for `outputs_location` in the bootstrap stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified: + +```bash +# `outputs_location` is set to `../../configs/example` +ln -s ../../configs/example/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 ../00-bootstrap +terraform output -json providers | jq -r '.["02-networking"]' \ + > ../02-networking/providers.tf +``` + +### Variable configuration + +There are two broad sets of variables you will need to fill in: + +- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) +- variables specific to resources managed by this stage + +To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. + +If you have set a valid value for `outputs_location` in the bootstrap and in the resman stage, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's folder in the path you specified, where the `*` above is set to the name of the stage that produced it. For this stage, a single `.tfvars` file is available: + +```bash +# `outputs_location` is set to `../../configs/example` +ln -s ../../configs/example/02-networking/terraform-bootstrap.auto.tfvars.json +ln -s ../../configs/example/02-networking/terraform-resman.auto.tfvars.json +``` + +Please refer to the [Variables](#variables) table below for a map of the variable origins, and to the sections below on how to adapt this stage to your networking configuration. + +### VPCs + +VPCs are defined in separate files, one for `landing` and one for each of `prod` and `dev`. +Each file contains the same resources, described in the following paragraphs. + +The **project** ([`project`](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/project)) contains the VPC, and enables the required APIs and sets itself as a "[host project](https://cloud.google.com/vpc/docs/shared-vpc)". + +The **VPC** ([`net-vpc`](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/net-vpc)) manages the DNS inbound policy (for Landing), explicit routes for `{private,restricted}.googleapis.com`, and its **subnets**. Subnets are created leveraging a "resource factory" paradigm, where the configuration is separated from the module that implements it, and stored in a well-structured file. To add a new subnet, simply create a new file in the `data_folder` directory defined in the module, following the examples found in the [Fabric `net-vpc` documentation](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/net-vpc#subnet-factory). Sample subnets are shipped in [data/subnets](./data/subnets), and can be easily customised to fit your needs. + +Subnets for [L7 ILBs](https://cloud.google.com/load-balancing/docs/l7-internal/proxy-only-subnets) are handled differently, and defined in variable `l7ilb_subnets`, while ranges for [PSA](https://cloud.google.com/vpc/docs/configure-private-services-access#allocating-range) are configured by variable `psa_ranges` - such variables are consumed by spoke VPCs. + +**Cloud NAT** ([`net-cloudnat`](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/net-cloudnat)) manages the networking infrastructure required to enable internet egress. + +### VPNs + +#### External + +Connectivity to on-prem is implemented with VPN HA ([`net-vpn`](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/net-vpn)) and defined in [`vpn-onprem.tf`](./vpn-onprem.tf). The file provisionally implements a single logical connection between onprem and landing at `europe-west1`, and the relevant parameters for its configuration are found in variable `vpn_onprem_configs`. + +#### Internal + +VPNs ([`net-vpn`](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/net-vpn)) used to interconnect landing and spokes are managed by `vpn-spoke-*.tf` files, each implementing both sides of the VPN connection. Per-gateway configurations (e.g. BGP advertisements and session ranges) are controlled by variable `vpn_onprem_configs`. VPN gateways and IKE secrets are automatically generated and configured. + +### Routing and BGP + +Each VPC network ([`net-vpc`](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/net-vpc)) manages a separate routing table, which can define static routes (e.g. to private.googleapis.com) and receives dynamic routes from BGP sessions established with neighbor networks (e.g. landing receives routes from onprem and spokes, and spokes receive RFC1918 from landing). + +Static routes are defined in `vpc-*.tf` files, in the `routes` section of each `net-vpc` module. + +BGP sessions for landing-spoke are configured through variable `vpn_spoke_configs`, while the ones for landing-onprem use variable `vpn_onprem_configs` + +### Firewall + +**VPC firewall rules** ([`net-vpc-firewall`](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/net-vpc-firewall)) are defined per-vpc on each `vpc-*.tf` file and leverage a resource factory to massively create rules. +To add a new firewall rule, create a new file or edit an existing one in the `data_folder` directory defined in the module `net-vpc-firewall`, following the examples of the "[Rules factory](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/net-vpc-firewall#rules-factory)" section of the module documentation. Sample firewall rules are shipped in [data/firewall-rules/landing](./data/firewall-rules/landing) and can be easily customised. + +**Hierarchical firewall policies** ([`folder`](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/folder)) are defined in `main.tf`, and managed through a policy factory implemented by the `folder` module, which applies the defined hierarchical to the `Networking` folder, which contains all the core networking infrastructure. Policies are defined in the `rules_file` file - to define a new one simply use the instructions found on "[Firewall policy factory](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/organization#firewall-policy-factory)". Sample hierarchical firewall policies are shipped in [data/hierarchical-policy-rules.yaml](./data/hierarchical-policy-rules.yaml) and can be easily customised. + +### DNS architecture + +The DNS ([`dns`](https://github.com/terraform-google-modules/cloud-foundation-fabric/tree/master/modules/dns)) infrastructure is defined in [`dns.tf`](dns.tf). + +Cloud DNS manages onprem forwarding, the main GCP zone (in this example `gcp.example.com`) and is peered to environment-specific zones (i.e. `dev.gcp.example.com` and `prod.gcp.example.com`). + +#### Cloud environment + +Per the section above Landing acts as the source of truth for DNS within the Cloud environment. Resources defined in the spoke VPCs consume the Landing DNS infrastructure through DNS peering (e.g. `prod-landing-root-dns-peering`). +Spokes can optionally define private zones (e.g. `prod-dns-private-zone`) - granting visibility to the Landing VPC ensures that the whole cloud environment can query such zones. + +#### Cloud to on-prem + +Leveraging the forwarding zones defined on Landing (e.g. `onprem-example-dns-forwarding` and `reverse-10-dns-forwarding`), the cloud environment can resolve `in-addr.arpa.` and `onprem.example.com.` using the on-premises DNS infrastructure. Onprem resolvers IPs are set in variable `dns.onprem`. + +DNS queries sent to the on-premises infrastructure come from the `35.199.192.0/19` source range, which is only accessible from within a VPC or networks connected to one. + +#### On-prem to cloud + +The [Inbound DNS Policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) defined in module `landing-vpc` ([`landing.tf`](./landing.tf)) automatically reserves the first available IP address on each created subnet (typically the third one in a CIDR) to expose the Cloud DNS service so that it can be consumed from outside of GCP. + +### Private Google Access + +[Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) enables VMs and on-prem systems to consume Google APIs from within the Google network, and is already fully configured on this environment. + +For PGA to work: + +- Private Google Access should be enabled on the subnet. \ +Subnets created by the `net-vpc` module are PGA-enabled by default. + +- 199.36.153.4/30 (`restricted.googleapis.com`) and 199.36.153.8/30 (`private.googleapis.com`) should be routed from on-prem to VPC, and from there to the `default-internet-gateway`. \ +Per variable `vpn_onprem_configs` such ranges are advertised to onprem - furthermore every VPC (e.g. see `landing-vpc` in [`landing.tf`](./landing.tf)) has explicit routes set in case the `0.0.0.0/0` route is changed. + +- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in [`dns.tf`](./dns.tf) + +### Preliminar activities + +Before running `terraform apply` on this stage, make sure to adapt all of `variables.tf` to your needs, to update all reference to regions (e.g. `europe-west1` or `ew1`) in the whole directory to match your preferences. + +If you're not using FAST, you'll also need to create a `providers.tf` file to configure the GCS backend and the service account to use to run the deployment. + +You're now ready to run `terraform init` and `apply`. + +### Post-deployment activities + +- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. +- On-prem routers should accept BGP sessions from their cloud peers. +- On-prem DNS servers should have forward zones for GCP-managed ones. + +## Customizations + +### Adding an environment + +To create a new environment (e.g. `staging`), a few changes are required. + +Create a `vpc-spoke-staging.tf` file by copying `vpc-spoke-prod.tf` file, +and adapt the new file by replacing the value "prod" with the value "staging". +Running `diff vpc-spoke-dev.tf vpc-spoke-prod.tf` can help to see how environment files differ. + +The new VPC requires a set of dedicated CIDRs, one per region, added to variable `custom_adv` (for example as `spoke_staging_ew1` and `spoke_staging_ew4`). +>`custom_adv` is a map that "resolves" CIDR names to actual addresses, and will be used later to configure routing. +> +Variables managing L7 Interal Load Balancers (`l7ilb_subnets`) and Private Service Access (`psa_ranges`) should also be adapted, and subnets and firewall rules for the new spoke should be added as described above. + +VPN HA connectivity (see also [VPNs](#vpns)) to `landing` is managed by the `vpn-spoke-*.tf` files. +Copy `vpn-spoke-prod.tf` to `vpn-spoke-staging.tf` - replace "prod" with "staging" where relevant. + +VPN configuration also controls BGP advertisements, which requires the following variable changes: + +- `router_configs` to configure the new routers (one per region) created for the `staging` VPC +- `vpn_onprem_configs` to configure the new advertisments to on-premises for the new CIDRs +- `vpn_spoke_configs` to configure the new advertisements to `landing` for the new VPC - new keys (one per region) should be added, such as e.g. `staging-ew1` and `staging-ew4` + +DNS configurations are centralised in the `dns.tf` file. Spokes delegate DNS resolution to Landing through DNS peering, and optionally define a private zone (e.g. `staging.gcp.example.com`) which the landing peers to. To configure DNS for a new environment, copy all the `prod-*` modules in the `dns.tf` file to `staging-*`, and update their content accordingly. Don't forget to add a peering zone from Landing to the newly created environment private zone. + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [dns-dev.tf](./dns-dev.tf) | Development spoke DNS zones and peerings setup. | dns | | +| [dns-landing.tf](./dns-landing.tf) | Landing DNS zones and peerings setup. | dns | | +| [dns-prod.tf](./dns-prod.tf) | Production spoke DNS zones and peerings setup. | dns | | +| [main.tf](./main.tf) | Networking folder and hierarchical policy. | folder | | +| [monitoring.tf](./monitoring.tf) | Network monitoring dashboards. | | google_monitoring_dashboard | +| [outputs.tf](./outputs.tf) | Module outputs. | | local_file | +| [test-resources.tf](./test-resources.tf) | temporary instances for testing | compute-vm | | +| [variables.tf](./variables.tf) | Module variables. | | | +| [vpc-landing.tf](./vpc-landing.tf) | Landing VPC and related resources. | net-cloudnat · net-vpc · net-vpc-firewall · project | | +| [vpc-spoke-dev.tf](./vpc-spoke-dev.tf) | Dev spoke VPC and related resources. | net-address · net-cloudnat · net-vpc · net-vpc-firewall · project | | +| [vpc-spoke-prod.tf](./vpc-spoke-prod.tf) | Production spoke VPC and related resources. | net-address · net-cloudnat · net-vpc · net-vpc-firewall · project | | +| [vpn-onprem.tf](./vpn-onprem.tf) | VPN between landing and onprem. | net-vpn-ha | | +| [vpn-spoke-dev.tf](./vpn-spoke-dev.tf) | VPN between landing and development spoke. | net-vpn-ha | | +| [vpn-spoke-prod.tf](./vpn-spoke-prod.tf) | VPN between landing and production spoke. | net-vpn-ha | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| billing_account_id | Billing account id. | string | ✓ | | 00-bootstrap | +| organization | Organization details. | object({…}) | ✓ | | 00-bootstrap | +| prefix | Prefix used for resources that need unique names. | string | ✓ | | 00-bootstrap | +| custom_adv | Custom advertisement definitions in name => range format. | map(string) | | {…} | | +| data_dir | Relative path for the folder storing configuration data for network resources. | string | | "data" | | +| dns | Onprem DNS resolvers | map(list(string)) | | {…} | | +| folder_id | Folder to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | string | | null | 01-resman | +| gke | | map(object({…})) | | {} | 01-resman | +| l7ilb_subnets | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | +| outputs_location | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | +| project_factory_sa | IAM emails for project factory service accounts | map(string) | | {} | 01-resman | +| psa_ranges | IP ranges used for Private Service Access (e.g. CloudSQL). | map(map(string)) | | {…} | | +| router_configs | Configurations for CRs and onprem routers. | map(object({…})) | | {…} | | +| vpn_onprem_configs | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| vpn_spoke_configs | VPN gateway configuration for spokes. | map(object({…})) | | {…} | | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| cloud_dns_inbound_policy | IP Addresses for Cloud DNS inbound policy. | | | +| project_ids | Network project ids. | | | +| project_numbers | Network project numbers. | | | +| shared_vpc_host_projects | Shared VPC host projects. | | | +| shared_vpc_self_links | Shared VPC host projects. | | | +| tfvars | Network-related variables used in other stages. | ✓ | | +| vpn_gateway_endpoints | External IP Addresses for the GCP VPN gateways. | | | + + diff --git a/fast/stages/02-networking/data/cidrs.yaml b/fast/stages/02-networking/data/cidrs.yaml new file mode 100644 index 00000000..5f453d8d --- /dev/null +++ b/fast/stages/02-networking/data/cidrs.yaml @@ -0,0 +1,15 @@ +# skip boilerplate check + +healthchecks: + - 35.191.0.0/16 + - 130.211.0.0/22 + - 209.85.152.0/22 + - 209.85.204.0/22 + +rfc1918: + - 10.0.0.0/8 + - 172.16.0.0/16 + - 192.168.0.0/16 + +onprem_probes: + - 10.255.255.254/32 diff --git a/fast/stages/02-networking/data/dashboards/firewall_insights.json b/fast/stages/02-networking/data/dashboards/firewall_insights.json new file mode 100644 index 00000000..e829091c --- /dev/null +++ b/fast/stages/02-networking/data/dashboards/firewall_insights.json @@ -0,0 +1,68 @@ +{ + "displayName": "Firewall Insights Monitoring", + "gridLayout": { + "columns": "2", + "widgets": [ + { + "title": "Subnet Firewall Hit Counts", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"firewallinsights.googleapis.com/subnet/firewall_hit_count\" resource.type=\"gce_subnetwork\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "VM Firewall Hit Counts", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"firewallinsights.googleapis.com/vm/firewall_hit_count\" resource.type=\"gce_instance\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + } + ] + } +} \ No newline at end of file diff --git a/fast/stages/02-networking/data/dashboards/vpn.json b/fast/stages/02-networking/data/dashboards/vpn.json new file mode 100644 index 00000000..4396cc00 --- /dev/null +++ b/fast/stages/02-networking/data/dashboards/vpn.json @@ -0,0 +1,248 @@ +{ + "displayName": "VPN Monitoring", + "gridLayout": { + "columns": "2", + "widgets": [ + { + "title": "Number of connections", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_MEAN" + }, + "filter": "metric.type=\"vpn.googleapis.com/gateway/connections\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Tunnel established", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_MEAN" + }, + "filter": "metric.type=\"vpn.googleapis.com/tunnel_established\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Cloud VPN Gateway - Received bytes", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/received_bytes_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "By" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Cloud VPN Gateway - Sent bytes", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/sent_bytes_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "By" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Cloud VPN Gateway - Received packets", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/received_packets_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "{packets}" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Cloud VPN Gateway - Sent packets", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/sent_packets_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "{packets}" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Incoming packets dropped", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/dropped_received_packets_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Outgoing packets dropped", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/dropped_sent_packets_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + } + ] + } +} \ No newline at end of file diff --git a/fast/stages/02-networking/data/firewall-rules/landing/rules.yaml b/fast/stages/02-networking/data/firewall-rules/landing/rules.yaml new file mode 100644 index 00000000..e72b7c9c --- /dev/null +++ b/fast/stages/02-networking/data/firewall-rules/landing/rules.yaml @@ -0,0 +1,15 @@ +# skip boilerplate check + +allow-onprem-probes-example: + description: "Allow traffic from onprem probes" + direction: INGRESS + action: allow + sources: [] + ranges: + - $onprem_probes + targets: [] + use_service_accounts: false + rules: + - protocol: tcp + ports: + - 12345 diff --git a/fast/stages/02-networking/data/hierarchical-policy-rules.yaml b/fast/stages/02-networking/data/hierarchical-policy-rules.yaml new file mode 100644 index 00000000..0172a309 --- /dev/null +++ b/fast/stages/02-networking/data/hierarchical-policy-rules.yaml @@ -0,0 +1,49 @@ +# skip boilerplate check + +allow-admins: + description: Access from the admin subnet to all subnets + direction: INGRESS + action: allow + priority: 1000 + ranges: + - $rfc1918 + ports: + all: [] + target_resources: null + enable_logging: false + +allow-healthchecks: + description: Enable HTTP and HTTPS healthchecks + direction: INGRESS + action: allow + priority: 1001 + ranges: + - $healthchecks + ports: + tcp: ["80", "443"] + target_resources: null + enable_logging: false + +allow-ssh-from-iap: + description: Enable SSH from IAP + direction: INGRESS + action: allow + priority: 1002 + ranges: + - 35.235.240.0/20 + ports: + tcp: ["22"] + target_resources: null + enable_logging: false + +allow-icmp: + description: Enable ICMP + direction: INGRESS + action: allow + priority: 1003 + ranges: + - 0.0.0.0/0 + ports: + icmp: [] + target_resources: null + enable_logging: false diff --git a/fast/stages/02-networking/data/subnets/dev/dev-default-ew1.yaml b/fast/stages/02-networking/data/subnets/dev/dev-default-ew1.yaml new file mode 100644 index 00000000..37c28f03 --- /dev/null +++ b/fast/stages/02-networking/data/subnets/dev/dev-default-ew1.yaml @@ -0,0 +1,5 @@ +# skip boilerplate check + +region: europe-west1 +ip_cidr_range: 10.144.0.0/24 +description: Default subnet for dev diff --git a/fast/stages/02-networking/data/subnets/landing/landing-default-ew1.yaml b/fast/stages/02-networking/data/subnets/landing/landing-default-ew1.yaml new file mode 100644 index 00000000..5af68db6 --- /dev/null +++ b/fast/stages/02-networking/data/subnets/landing/landing-default-ew1.yaml @@ -0,0 +1,5 @@ +# skip boilerplate check + +region: europe-west1 +ip_cidr_range: 10.128.0.0/24 +description: Default subnet for landing diff --git a/fast/stages/02-networking/data/subnets/prod/prod-default-ew1.yaml b/fast/stages/02-networking/data/subnets/prod/prod-default-ew1.yaml new file mode 100644 index 00000000..7a77f309 --- /dev/null +++ b/fast/stages/02-networking/data/subnets/prod/prod-default-ew1.yaml @@ -0,0 +1,5 @@ +# skip boilerplate check + +region: europe-west1 +ip_cidr_range: 10.136.0.0/24 +description: Default subnet for prod diff --git a/fast/stages/02-networking/diagram.png b/fast/stages/02-networking/diagram.png new file mode 100644 index 00000000..c071ccf1 Binary files /dev/null and b/fast/stages/02-networking/diagram.png differ diff --git a/fast/stages/02-networking/diagram.svg b/fast/stages/02-networking/diagram.svg new file mode 100644 index 00000000..52f424f7 --- /dev/null +++ b/fast/stages/02-networking/diagram.svg @@ -0,0 +1,2788 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages/02-networking/dns-dev.tf b/fast/stages/02-networking/dns-dev.tf new file mode 100644 index 00000000..3c81a93f --- /dev/null +++ b/fast/stages/02-networking/dns-dev.tf @@ -0,0 +1,53 @@ +/** + * 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. + */ + +# tfdoc:file:description Development spoke DNS zones and peerings setup. + +# GCP-specific environment zone + +module "dev-dns-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "dev-gcp-example-com" + domain = "dev.gcp.example.com." + client_networks = [module.dev-spoke-vpc.self_link] + recordsets = { + "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] } + } +} + +# root zone peering to landing to centralize configuration; remove if unneeded + +module "dev-landing-root-dns-peering" { + source = "../../../modules/dns" + project_id = module.dev-spoke-project.project_id + type = "peering" + name = "dev-root-dns-peering" + domain = "." + client_networks = [module.dev-spoke-vpc.self_link] + peer_network = module.landing-vpc.self_link +} + +module "dev-reverse-10-dns-peering" { + source = "../../../modules/dns" + project_id = module.dev-spoke-project.project_id + type = "peering" + name = "dev-reverse-10-dns-peering" + domain = "10.in-addr.arpa." + client_networks = [module.dev-spoke-vpc.self_link] + peer_network = module.landing-vpc.self_link +} diff --git a/fast/stages/02-networking/dns-landing.tf b/fast/stages/02-networking/dns-landing.tf new file mode 100644 index 00000000..611410b5 --- /dev/null +++ b/fast/stages/02-networking/dns-landing.tf @@ -0,0 +1,93 @@ +/** + * 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. + */ + +# tfdoc:file:description Landing DNS zones and peerings setup. + +# forwarding to on-prem DNS resolvers + +module "onprem-example-dns-forwarding" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "forwarding" + name = "example-com" + domain = "onprem.example.com." + client_networks = [module.landing-vpc.self_link] + forwarders = { for ip in var.dns.onprem : ip => null } +} + +module "reverse-10-dns-forwarding" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "forwarding" + name = "root-reverse-10" + domain = "10.in-addr.arpa." + client_networks = [module.landing-vpc.self_link] + forwarders = { for ip in var.dns.onprem : ip => null } +} + +module "gcp-example-dns-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "gcp-example-com" + domain = "gcp.example.com." + client_networks = [module.landing-vpc.self_link] + recordsets = { + "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] } + } +} + +# GCP-specific DNS zones peered to the environment spoke that holds the config + +module "prod-gcp-example-dns-peering" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "peering" + name = "prod-root-dns-peering" + domain = "prod.gcp.example.com." + client_networks = [module.landing-vpc.self_link] + peer_network = module.prod-spoke-vpc.self_link +} + +module "dev-gcp-example-dns-peering" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "peering" + name = "dev-root-dns-peering" + domain = "dev.gcp.example.com." + client_networks = [module.landing-vpc.self_link] + peer_network = module.dev-spoke-vpc.self_link +} + +# Google API zone to trigger Private Access + +module "googleapis-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "googleapis-com" + domain = "googleapis.com." + client_networks = [module.landing-vpc.self_link] + recordsets = { + "A private" = { type = "A", ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "A restricted" = { type = "A", ttl = 300, records = [ + "199.36.153.4", "199.36.153.5", "199.36.153.6", "199.36.153.7" + ] } + "CNAME *" = { type = "CNAME", ttl = 300, records = ["private.googleapis.com."] } + } +} diff --git a/fast/stages/02-networking/dns-prod.tf b/fast/stages/02-networking/dns-prod.tf new file mode 100644 index 00000000..22977348 --- /dev/null +++ b/fast/stages/02-networking/dns-prod.tf @@ -0,0 +1,53 @@ +/** + * 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. + */ + +# tfdoc:file:description Production spoke DNS zones and peerings setup. + +# GCP-specific environment zone + +module "prod-dns-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "prod-gcp-example-com" + domain = "prod.gcp.example.com." + client_networks = [module.prod-spoke-vpc.self_link] + recordsets = { + "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] } + } +} + +# root zone peering to landing to centralize configuration; remove if unneeded + +module "prod-landing-root-dns-peering" { + source = "../../../modules/dns" + project_id = module.prod-spoke-project.project_id + type = "peering" + name = "prod-root-dns-peering" + domain = "." + client_networks = [module.prod-spoke-vpc.self_link] + peer_network = module.landing-vpc.self_link +} + +module "prod-reverse-10-dns-peering" { + source = "../../../modules/dns" + project_id = module.prod-spoke-project.project_id + type = "peering" + name = "prod-reverse-10-dns-peering" + domain = "10.in-addr.arpa." + client_networks = [module.prod-spoke-vpc.self_link] + peer_network = module.landing-vpc.self_link +} diff --git a/fast/stages/02-networking/main.tf b/fast/stages/02-networking/main.tf new file mode 100644 index 00000000..fd03c287 --- /dev/null +++ b/fast/stages/02-networking/main.tf @@ -0,0 +1,72 @@ +/** + * 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. + */ + +# tfdoc:file:description Networking folder and hierarchical policy. + +locals { + # define the structures used for BGP peers in the VPN resources + bgp_peer_options = { + for k, v in var.vpn_spoke_configs : + k => var.vpn_spoke_configs[k].adv == null ? null : { + advertise_groups = [] + advertise_ip_ranges = { + for adv in(var.vpn_spoke_configs[k].adv == null ? [] : var.vpn_spoke_configs[k].adv.custom) : + var.custom_adv[adv] => adv + } + advertise_mode = try(var.vpn_spoke_configs[k].adv.default, false) ? "DEFAULT" : "CUSTOM" + route_priority = null + } + } + bgp_peer_options_onprem = { + for k, v in var.vpn_onprem_configs : + k => var.vpn_onprem_configs[k].adv == null ? null : { + advertise_groups = [] + advertise_ip_ranges = { + for adv in(var.vpn_onprem_configs[k].adv == null ? [] : var.vpn_onprem_configs[k].adv.custom) : + var.custom_adv[adv] => adv + } + advertise_mode = try(var.vpn_onprem_configs[k].adv.default, false) ? "DEFAULT" : "CUSTOM" + route_priority = null + } + } + l7ilb_subnets = { for env, v in var.l7ilb_subnets : env => [ + for s in v : merge(s, { + active = true + name = "${env}-l7ilb-${s.region}" + })] + } + region_trigram = { + europe-west1 = "ew1" + europe-west3 = "ew3" + } +} + +module "folder" { + source = "../../../modules/folder" + parent = "organizations/${var.organization.id}" + name = "Networking" + folder_create = var.folder_id == null + id = var.folder_id + firewall_policy_factory = { + cidr_file = "${var.data_dir}/cidrs.yaml" + policy_name = null + rules_file = "${var.data_dir}/hierarchical-policy-rules.yaml" + } + firewall_policy_association = { + factory-policy = "factory" + } +} + diff --git a/fast/stages/02-networking/monitoring.tf b/fast/stages/02-networking/monitoring.tf new file mode 100644 index 00000000..7b8b70c5 --- /dev/null +++ b/fast/stages/02-networking/monitoring.tf @@ -0,0 +1,32 @@ +/** + * 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. + */ + +# tfdoc:file:description Network monitoring dashboards. + +locals { + dashboard_path = "${var.data_dir}/dashboards" + dashboard_files = fileset(local.dashboard_path, "*.json") + dashboards = { + for filename in local.dashboard_files : + filename => "${local.dashboard_path}/${filename}" + } +} + +resource "google_monitoring_dashboard" "dashboard" { + for_each = local.dashboards + project = module.landing-project.project_id + dashboard_json = file(each.value) +} diff --git a/fast/stages/02-networking/outputs.tf b/fast/stages/02-networking/outputs.tf new file mode 100644 index 00000000..4efe9bc6 --- /dev/null +++ b/fast/stages/02-networking/outputs.tf @@ -0,0 +1,95 @@ +/** + * 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. + */ +# optionally generate providers and tfvars files for subsequent stages + +locals { + tfvars = { + "03-project-factory-dev" = jsonencode({ + environment_dns_zone = module.dev-dns-private-zone.domain + shared_vpc_self_link = module.dev-spoke-vpc.self_link + vpc_host_project = module.dev-spoke-project.project_id + }) + "03-project-factory-prod" = jsonencode({ + environment_dns_zone = module.prod-dns-private-zone.domain + shared_vpc_self_link = module.prod-spoke-vpc.self_link + vpc_host_project = module.prod-spoke-project.project_id + }) + } +} + +resource "local_file" "tfvars" { + for_each = var.outputs_location == null ? {} : local.tfvars + filename = "${var.outputs_location}/${each.key}/terraform-networking.auto.tfvars.json" + content = each.value +} + +# outputs + +output "cloud_dns_inbound_policy" { + description = "IP Addresses for Cloud DNS inbound policy." + value = [for s in module.landing-vpc.subnets : cidrhost(s.ip_cidr_range, 2)] +} + +output "project_ids" { + description = "Network project ids." + value = { + dev = module.dev-spoke-project.project_id + landing = module.landing-project.project_id + prod = module.prod-spoke-project.project_id + } +} + +output "project_numbers" { + description = "Network project numbers." + value = { + dev = "projects/${module.dev-spoke-project.number}" + landing = "projects/${module.landing-project.number}" + prod = "projects/${module.prod-spoke-project.number}" + } +} + +output "shared_vpc_host_projects" { + description = "Shared VPC host projects." + value = { + landing = module.landing-project.project_id + dev = module.dev-spoke-project.project_id + prod = module.prod-spoke-project.project_id + } +} + + +output "shared_vpc_self_links" { + description = "Shared VPC host projects." + value = { + landing = module.landing-vpc.self_link + dev = module.dev-spoke-vpc.self_link + prod = module.prod-spoke-vpc.self_link + } +} + + +output "vpn_gateway_endpoints" { + description = "External IP Addresses for the GCP VPN gateways." + value = { + onprem-ew1 = { for v in module.landing-to-onprem-ew1-vpn.gateway.vpn_interfaces : v.id => v.ip_address } + } +} + +output "tfvars" { + description = "Network-related variables used in other stages." + sensitive = true + value = local.tfvars +} diff --git a/fast/stages/02-networking/test-resources.tf b/fast/stages/02-networking/test-resources.tf new file mode 100644 index 00000000..8e54717d --- /dev/null +++ b/fast/stages/02-networking/test-resources.tf @@ -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. + */ + +# tfdoc:file:description temporary instances for testing + +module "test-vm-landing-0" { + source = "../../../modules/compute-vm" + project_id = module.landing-project.project_id + zone = "europe-west1-b" + name = "test-vm-1" + network_interfaces = [{ + network = module.landing-vpc.self_link + subnetwork = module.landing-vpc.subnet_self_links["europe-west1/landing-default-ew1"] + alias_ips = {} + nat = false + addresses = null + }] + tags = ["ssh"] + service_account_create = true + boot_disk = { + image = "projects/debian-cloud/global/images/family/debian-10" + type = "pd-balanced" + size = 10 + } + metadata = { + startup-script = < { + address = cidrhost(v, 0) + network = module.dev-spoke-vpc.self_link + prefix_length = split("/", v)[1] + } + } +} diff --git a/fast/stages/02-networking/vpc-spoke-prod.tf b/fast/stages/02-networking/vpc-spoke-prod.tf new file mode 100644 index 00000000..574af757 --- /dev/null +++ b/fast/stages/02-networking/vpc-spoke-prod.tf @@ -0,0 +1,105 @@ +/** + * 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. + */ + +# tfdoc:file:description Production spoke VPC and related resources. + +module "prod-spoke-project" { + source = "../../../modules/project" + billing_account = var.billing_account_id + name = "prod-net-spoke-0" + parent = var.folder_id + prefix = var.prefix + service_config = { + disable_on_destroy = false + disable_dependent_services = false + } + services = [ + "compute.googleapis.com", + "dns.googleapis.com", + "iap.googleapis.com", + "networkmanagement.googleapis.com", + "servicenetworking.googleapis.com", + ] + shared_vpc_host_config = { + enabled = true + service_projects = [] + } + metric_scopes = [module.landing-project.project_id] + iam = { + "roles/dns.admin" = [var.project_factory_sa.prod] + } +} + +module "prod-spoke-vpc" { + source = "../../../modules/net-vpc" + project_id = module.prod-spoke-project.project_id + name = "prod-spoke-0" + mtu = 1500 + data_folder = "${var.data_dir}/subnets/prod" + subnets_l7ilb = local.l7ilb_subnets.prod + # set explicit routes for googleapis in case the default route is deleted + routes = { + private-googleapis = { + dest_range = "199.36.153.8/30" + priority = 1000 + tags = [] + next_hop_type = "gateway" + next_hop = "default-internet-gateway" + } + restricted-googleapis = { + dest_range = "199.36.153.4/30" + priority = 1000 + tags = [] + next_hop_type = "gateway" + next_hop = "default-internet-gateway" + } + } +} + +module "prod-spoke-firewall" { + source = "../../../modules/net-vpc-firewall" + project_id = module.prod-spoke-project.project_id + network = module.prod-spoke-vpc.name + admin_ranges = [] + http_source_ranges = [] + https_source_ranges = [] + ssh_source_ranges = [] + data_folder = "${var.data_dir}/firewall-rules/prod" + cidr_template_file = "${var.data_dir}/cidrs.yaml" +} + +module "prod-spoke-cloudnat" { + for_each = toset(values(module.prod-spoke-vpc.subnet_regions)) + source = "../../../modules/net-cloudnat" + project_id = module.prod-spoke-project.project_id + region = each.value + name = "prod-nat-${local.region_trigram[each.value]}" + router_create = true + router_network = module.prod-spoke-vpc.name + router_asn = 4200001024 + logging_filter = "ERRORS_ONLY" +} + +module "prod-spoke-psa-addresses" { + source = "../../../modules/net-address" + project_id = module.prod-spoke-project.project_id + psa_addresses = { for r, v in var.psa_ranges.prod : r => { + address = cidrhost(v, 0) + network = module.prod-spoke-vpc.self_link + prefix_length = split("/", v)[1] + } + } +} diff --git a/fast/stages/02-networking/vpn-onprem.tf b/fast/stages/02-networking/vpn-onprem.tf new file mode 100644 index 00000000..d06a0cdb --- /dev/null +++ b/fast/stages/02-networking/vpn-onprem.tf @@ -0,0 +1,50 @@ +/** + * 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. + */ + +# tfdoc:file:description VPN between landing and onprem. + +module "landing-to-onprem-ew1-vpn" { + source = "../../../modules/net-vpn-ha" + project_id = module.landing-project.project_id + network = module.landing-vpc.self_link + region = "europe-west1" + name = "vpn-to-onprem-ew1" + router_create = true + router_name = "dev-spoke-vpn-ew1" + router_asn = var.router_configs.landing-ew1.asn + peer_external_gateway = { + redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" + interfaces = [{ + id = 0 + # on-prem router ip address + ip_address = var.vpn_onprem_configs.landing-ew1.peer.address + }] + } + tunnels = { for t in range(2) : "remote-${t}" => { + bgp_peer = { + address = cidrhost(var.vpn_onprem_configs.landing-ew1.session_range, 1 + (t * 4)) + asn = var.vpn_onprem_configs.landing-ew1.peer.asn + } + bgp_peer_options = local.bgp_peer_options_onprem["landing-ew1"] + bgp_session_range = "${cidrhost(var.vpn_onprem_configs.landing-ew1.session_range, 2 + (t * 4))}/30" + ike_version = 2 + peer_external_gateway_interface = 0 + router = null + shared_secret = var.vpn_onprem_configs.landing-ew1.peer.secret_id + vpn_gateway_interface = t + } + } +} diff --git a/fast/stages/02-networking/vpn-spoke-dev.tf b/fast/stages/02-networking/vpn-spoke-dev.tf new file mode 100644 index 00000000..edfe4000 --- /dev/null +++ b/fast/stages/02-networking/vpn-spoke-dev.tf @@ -0,0 +1,73 @@ +/** + * 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. + */ + +# tfdoc:file:description VPN between landing and development spoke. + +module "landing-to-dev-ew1-vpn" { + source = "../../../modules/net-vpn-ha" + project_id = module.landing-project.project_id + network = module.landing-vpc.self_link + region = "europe-west1" + name = "vpn-to-dev-ew1" + # The router used for this VPN is managed in vpn-prod.tf + router_create = false + router_name = "landing-vpn-ew1" + router_asn = var.router_configs.landing-ew1.asn + peer_gcp_gateway = module.dev-to-landing-ew1-vpn.self_link + tunnels = { for t in range(2) : "tunnel-${t}" => { + bgp_peer = { + address = cidrhost(var.vpn_spoke_configs.dev-ew1.session_range, 1 + (t * 4)) + asn = var.router_configs.spoke-dev-ew1.asn + } + bgp_peer_options = local.bgp_peer_options["landing-ew1"] + bgp_session_range = "${cidrhost(var.vpn_spoke_configs.dev-ew1.session_range, 2 + (t * 4))}/30" + ike_version = 2 + peer_external_gateway_interface = null + router = null + shared_secret = null + vpn_gateway_interface = t + } + } + depends_on = [ + module.landing-to-prod-ew1-vpn.router + ] +} + +module "dev-to-landing-ew1-vpn" { + source = "../../../modules/net-vpn-ha" + project_id = module.dev-spoke-project.project_id + network = module.dev-spoke-vpc.self_link + region = "europe-west1" + name = "vpn-to-landing-ew1" + router_create = true + router_name = "dev-spoke-vpn-ew1" + router_asn = var.router_configs.spoke-dev-ew1.asn + peer_gcp_gateway = module.landing-to-dev-ew1-vpn.self_link + tunnels = { for t in range(2) : "tunnel-${t}" => { + bgp_peer = { + address = cidrhost(var.vpn_spoke_configs.dev-ew1.session_range, 2 + (t * 4)) + asn = var.router_configs.landing-ew1.asn + } + bgp_peer_options = local.bgp_peer_options["dev-ew1"] + bgp_session_range = "${cidrhost(var.vpn_spoke_configs.dev-ew1.session_range, 1 + (t * 4))}/30" + ike_version = 2 + peer_external_gateway_interface = null + router = null + shared_secret = module.landing-to-dev-ew1-vpn.random_secret + vpn_gateway_interface = t + } + } +} diff --git a/fast/stages/02-networking/vpn-spoke-prod.tf b/fast/stages/02-networking/vpn-spoke-prod.tf new file mode 100644 index 00000000..a6d1ea16 --- /dev/null +++ b/fast/stages/02-networking/vpn-spoke-prod.tf @@ -0,0 +1,121 @@ +/** + * 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. + */ + +# tfdoc:file:description VPN between landing and production spoke. + +module "landing-to-prod-ew1-vpn" { + source = "../../../modules/net-vpn-ha" + project_id = module.landing-project.project_id + network = module.landing-vpc.self_link + region = "europe-west1" + name = "vpn-to-prod-ew1" + router_create = true + router_name = "landing-vpn-ew1" + router_asn = var.router_configs.landing-ew1.asn + peer_gcp_gateway = module.prod-to-landing-ew1-vpn.self_link + tunnels = { for t in range(2) : "tunnel-${t}" => { + bgp_peer = { + address = cidrhost(var.vpn_spoke_configs.prod-ew1.session_range, 1 + (t * 4)) + asn = var.router_configs.spoke-prod-ew1.asn + } + bgp_peer_options = local.bgp_peer_options["landing-ew1"] + bgp_session_range = "${cidrhost(var.vpn_spoke_configs.prod-ew1.session_range, 2 + (t * 4))}/30" + ike_version = 2 + peer_external_gateway_interface = null + router = null + shared_secret = null + vpn_gateway_interface = t + } + } +} + +module "prod-to-landing-ew1-vpn" { + source = "../../../modules/net-vpn-ha" + project_id = module.prod-spoke-project.project_id + network = module.prod-spoke-vpc.self_link + region = "europe-west1" + name = "vpn-to-landing-ew1" + router_create = true + router_name = "prod-spoke-vpn-ew1" + router_asn = var.router_configs.spoke-prod-ew1.asn + peer_gcp_gateway = module.landing-to-prod-ew1-vpn.self_link + tunnels = { for t in range(2) : "tunnel-${t}" => { + bgp_peer = { + address = cidrhost(var.vpn_spoke_configs.prod-ew1.session_range, 2 + (t * 4)) + asn = var.router_configs.landing-ew1.asn + } + bgp_peer_options = local.bgp_peer_options["prod-ew1"] + bgp_session_range = "${cidrhost(var.vpn_spoke_configs.prod-ew1.session_range, 1 + (t * 4))}/30" + ike_version = 2 + peer_external_gateway_interface = null + router = null + shared_secret = module.landing-to-prod-ew1-vpn.random_secret + vpn_gateway_interface = t + } + } +} + +module "landing-to-prod-ew4-vpn" { + source = "../../../modules/net-vpn-ha" + project_id = module.landing-project.project_id + network = module.landing-vpc.self_link + region = "europe-west1" + name = "vpn-to-prod-ew4" + router_create = true + router_name = "landing-vpn-ew4" + router_asn = var.router_configs.landing-ew4.asn + peer_gcp_gateway = module.prod-to-landing-ew4-vpn.self_link + tunnels = { for t in range(2) : "tunnel-${t}" => { + bgp_peer = { + address = cidrhost(var.vpn_spoke_configs.prod-ew4.session_range, 1 + (t * 4)) + asn = var.router_configs.spoke-prod-ew4.asn + } + bgp_peer_options = local.bgp_peer_options["landing-ew4"] + bgp_session_range = "${cidrhost(var.vpn_spoke_configs.prod-ew4.session_range, 2 + (t * 4))}/30" + ike_version = 2 + peer_external_gateway_interface = null + router = null + shared_secret = null + vpn_gateway_interface = t + } + } +} + +module "prod-to-landing-ew4-vpn" { + source = "../../../modules/net-vpn-ha" + project_id = module.prod-spoke-project.project_id + network = module.prod-spoke-vpc.self_link + region = "europe-west1" + name = "vpn-to-landing-ew4" + router_create = true + router_name = "prod-spoke-vpn-ew4" + router_asn = var.router_configs.spoke-prod-ew4.asn + peer_gcp_gateway = module.landing-to-prod-ew4-vpn.self_link + tunnels = { for t in range(2) : "tunnel-${t}" => { + bgp_peer = { + address = cidrhost(var.vpn_spoke_configs.prod-ew4.session_range, 2 + (t * 4)) + asn = var.router_configs.landing-ew4.asn + } + bgp_peer_options = local.bgp_peer_options["prod-ew4"] + bgp_session_range = "${cidrhost(var.vpn_spoke_configs.prod-ew4.session_range, 1 + (t * 4))}/30" + ike_version = 2 + peer_external_gateway_interface = null + router = null + shared_secret = module.landing-to-prod-ew4-vpn.random_secret + vpn_gateway_interface = t + } + } +} diff --git a/fast/stages/02-security/README.md b/fast/stages/02-security/README.md new file mode 100644 index 00000000..4ce5019a --- /dev/null +++ b/fast/stages/02-security/README.md @@ -0,0 +1,323 @@ +# Shared security resources + +This stage sets up security resources and configurations which impact the whole organization, or are shared across the hierarchy to other projects and teams. + +The design of this stage is fairly general, and provides a reference example for [Cloud KMS](https://cloud.google.com/security-key-management) and a [VPC Service Controls](https://cloud.google.com/vpc-service-controls) configuration that sets up three perimeters (landing, development, production), their related bridge perimeters, and provides variables to configure their resources, access levels, and directional policies. + +Expanding this stage to include other security-related services like Secret Manager, is fairly simple by using the provided implementation for Cloud KMS, and leveraging the broad permissions on the top-level Security folder of the automation service account used. + +The following diagram illustrates the high-level design of created resources and a schema of the VPC SC design, which can be adapted to specific requirements via variables: + +

+ Security diagram +

+ +## Design overview and choices + +Project-level security resources are grouped into two separate projects, one per environment. This setup matches requirements we frequently observe in real life and provides enough separation without needlessly complicating operations. + +Cloud KMS is configured and designed mainly to encrypt GCP resources with a [Customer-managed encryption key](https://cloud.google.com/kms/docs/cmek) but it may be used to create cryptokeys used to [encrypt application data](https://cloud.google.com/kms/docs/encrypting-application-data) too. + +IAM for management-related operations is already assigned at the folder level to the security team by the previous stage, but more granularity can be added here at the project level, to grant control of separate services across environments to different actors. + +### Cloud KMS + +A reference Cloud KMS implementation is part of this stage, to provide a simple way of managing centralized keys, that are then shared and consumed widely across the organization to enable customer-managed encryption. The implementation is also easy to clone and modify to support other services like Secret Manager. + +The Cloud KMS configuration allows defining keys by name (typically matching the downstream service that uses them) in different locations, either based on a common default or a per-key setting. It then takes care internally of provisioning the relevant keyrings and creating keys in the appropriate location. + +IAM roles on keys can be configured at the logical level for all locations where a logical key is created. Their management can also be delegated via [delegated role grants](https://cloud.google.com/iam/docs/setting-limits-on-granting-roles) exposed through a simple variable, to allow other identities to set IAM policies on keys. This is particularly useful in setups like project factories, making it possible to configure IAM bindings during project creation for team groups or service agent accounts (compute, storage, etc.). + +### VPC Service Controls + +This stage also provisions the VPC Service Controls configuration on demand for the whole organization, implementing the straightforward design illustrated above: + +- one perimeter for each environment +- one perimeter for centralized services and the landing VPC +- bridge perimeters to connect the landing perimeter to each environment + +The VPC SC configuration is set to dry-run mode, but switching to enforced mode is a simple operation involving modifying a few lines of code highlighted by ad-hoc comments. Variables are designed to enable easy centralized management of VPC Service Controls, including access levels and [ingress/egress rules](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules) as described below. + +Some care needs to be taken with project membership in perimeters, which can only be implemented here instead of being delegated (all or partially) to different stages, until the [Google Provider feature request](https://github.com/hashicorp/terraform-provider-google/issues/7270) allowing using project-level association for both enforced and dry-run modes is implemented. + +## 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. + +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. + +Before running this stage, you need to ensure you have the correct credentials and permissions, and customize variables by assigning values that match your configuration. + +### Providers configuration + +The default way of making sure you have the correct permissions is to use the identity of the service account pre-created for this stage during bootstrap, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). + +To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. + +If you have set a valid value for `outputs_location` in the resource management stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified: + +```bash +# `outputs_location` is set to `../../configs/example` +ln -s ../../configs/example/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 +terraform output -json providers | jq -r '.["02-security"]' \ + > ../02-security/providers.tf +``` + +### Variable configuration + +There are two broad sets of variables you will need to fill in: + +- variables shared by other stages (organization id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) +- variables specific to resources managed by this stage + +To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. + +If you configured a valid path for `outputs_location` in the previous stages, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's output folder (under the path you specified), where the `*` above is set to the name of the stage that produced it. For this stage, two `.tfvars` files are available: + +```bash +# `outputs_location` is set to `../../configs/example` +ln -s ../../configs/example/02-security/terraform-bootstrap.auto.tfvars.json +ln -s ../../configs/example/02-security/terraform-resman.auto.tfvars.json +``` + +A second set of optional variables is specific to this stage. 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. + +Once done, you can run this stage: + +```bash +terraform init +terraform apply +``` + +## Customizations + +### KMS keys + +Cloud KMS configuration is split in two variables: + +- `kms_defaults` configures the locations and rotation period, used for keys that don't specifically configure them +- `kms_keys` configures the actual keys to create, and also allows configuring their IAM bindings and labels, and overriding locations and rotation period. When configuring locations for a key, please consider the limitations each cloud product may have. + +The additional `kms_restricted_admins` variable allows granting `roles/cloudkms.admin` to specified principals, restricted via [delegated role grants](https://cloud.google.com/iam/docs/setting-limits-on-granting-roles) so that it only allows granting the roles needed for encryption/decryption on keys. This allows safe delegation of key management to subsequent Terraform stages like the Project Factory, for example to grant usage access on relevant keys to the service agent accounts for compute, storage, etc. + +To support these scenarios, key IAM bindings are configured by default to be additive, to enable other stages or Terraform configuration to safely co-manage bindings on the same keys. If this is not desired, follow the comments in the `core-dev.tf` and `core-prod.tf` files to switch to authoritative bindings on keys. + +An example of how to configure keys: + +```hcl +# terraform.tfvars + +kms_defaults = { + locations = ["europe-west1", "europe-west3", "global"] + rotation_period = "7776000s" +} +kms_keys = { + compute = { + iam = { + "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ + "user:user1@example.com" + ] + } + labels = { service = "compute" } + locations = null + rotation_period = null + } + storage = { + iam = null + labels = { service = "compute" } + locations = ["europe"] + rotation_period = null + } +} +``` + +The script will create one keyring for each specified location and keys on each keyring. + +### VPC Service Controls configuration + +A set of variables allows configuring the VPC SC perimeters described above: + +- `vpc_sc_perimeter_projects` configures project membership in the three regular perimeters +- `vpc_sc_access_levels` configures access levels, which can then be associated to perimeters by key using the `vpc_sc_perimeter_access_levels` +- `vpc_sc_egress_policies` configures directional egress policies, which can then be associated to perimeters by key using the `vpc_sc_perimeter_egress_policies` +- `vpc_sc_ingress_policies` configures directional ingress policies, which can then be associated to perimeters by key using the `vpc_sc_perimeter_ingress_policies` + +This allows configuring VPC SC in a fairly flexible and concise way, without repeating similar definitions. Bridges perimeters configuration will be computed automatically to allow communication between regular perimeters: `landing <-> prod` and `landing <-> dev`. + +#### Dry-run vs. enforced + +The VPC SC configuration is set up by default in dry-run mode to allow easy experimentation, and detecting violations before enforcement. Once everything is set up correctly, switching to enforced mode needs to be done in code, by swapping the contents of the `spec` and `status` attributes for perimeters in the `vpc-sc.tf` file. The effort involved is minimal (2 lines of code per perimeter), and comments help identify the correct lines. + +#### Perimeter resources + +Projects are added to perimeters via the `vpc_sc_perimeter_projects`, and that's currently the only way of doing it without generating permadiffs or conflicts, because of the way the Terraform provider is implemented. + +Once the Google Terraform Provider [implements support for dry-run mode in the additive resource](https://github.com/hashicorp/terraform-provider-google/issues/7270), it will be possible to concurrently manage perimeter resources both here and in subsequent Terraform configurations, for example to allow the Project Factory to add a project to a perimeter during the creation process. + +Bridge perimeters are auto-populated with all projects configured for the connected regular perimeters. + +An example of adding projects to perimeters using project numbers: + +```hcl +# terraform.tfvars + +vpc_sc_perimeter_projects = { + dev = ["projects/12345678", "projects/12345679"] + landing = ["projects/12345670"] + prod = ["projects/12345674", "projects/12345675"] +} +``` + +#### Access levels + +Below an example for an access level that allows unconditional ingress from a set of IP CIDR ranges can be configured once, and enabled on selected perimeters: + +```hcl +# terraform.tfvars + +vpc_sc_access_levels = { + on-prem = { + conditions = [{ + ip_subnetworks = ["10.0.0.0/24", "10.0.0.1/24"], + combining_function = null, members = null, negate = null, + regions = null, required_access_levels = null + }] + } +} +vpc_sc_perimeter_access_levels = { + dev = null + landing = ["on-prem"] + prod = ["on-prem"] +} +``` + +#### Ingress and Egress policies + +The same applies to Ingress and Egress policies, as shown in the examples below referencing the automation service account for this stage. + +Below you can find an ingress policy configuration that allows applying Terraform from outside the perimeter, useful when bringing up this stage to avoid generating violations: + +```hcl +# terraform.tfvars + +vpc_sc_ingress_policies = { + iac = { + ingress_from = { + identities = [ + "serviceAccount:xxx-prod-resman-security-0@xxx-prod-iac-core-0.iam.gserviceaccount.com" + ] + source_access_levels = ["*"] + identity_type = null + source_resources = null + } + ingress_to = { + operations = [{ method_selectors = [], service_name = "*" }] + resources = ["*"] + } + } +} +vpc_sc_perimeter_ingress_policies = { + dev = ["iac"] + landing = ["iac"] + prod = ["iac"] +} +``` + +Below you can find an egress policy that allows writing Terraform state to the automation bucket, useful once Terraform starts running inside the perimeter in a pipeline: + +```hcl +# terraform.tfvars + +vpc_sc_egress_policies = { + iac-gcs = { + egress_from = { + identity_type = null + identities = [ + "serviceAccount:xxx-prod-resman-security-0@xxx-prod-iac-core-0.iam.gserviceaccount.com" + ] + } + egress_to = { + operations = [{ + method_selectors = ["*"], service_name = "storage.googleapis.com" + }] + resources = ["projects/123456782"] + } + } +} +vpc_sc_perimeter_ingress_policies = { + dev = ["iac-gcs"] + landing = ["iac-gcs"] + prod = ["iac-gcs"] +} +``` + +## Notes + +Some references that might be useful in setting up this stage: + +- [VPC SC CSCC requirements](https://cloud.google.com/security-command-center/docs/troubleshooting). + + + + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [core-dev.tf](./core-dev.tf) | None | kms · project | google_project_iam_member | +| [core-prod.tf](./core-prod.tf) | None | kms · project | google_project_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | | | +| [outputs.tf](./outputs.tf) | Module outputs. | | local_file | +| [variables.tf](./variables.tf) | Module variables. | | | +| [vpc-sc.tf](./vpc-sc.tf) | None | vpc-sc | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| billing_account_id | Billing account id. | string | ✓ | | bootstrap | +| folder_id | Folder to be used for the networking resources in folders/nnnn format. | string | ✓ | | resman | +| organization | Organization details. | object({…}) | ✓ | | bootstrap | +| prefix | Prefix used for resources that need unique names. | string | ✓ | | | +| groups | Group names to grant organization-level permissions. | map(string) | | {…} | bootstrap | +| kms_defaults | Defaults used for KMS keys. | object({…}) | | {…} | | +| kms_keys | KMS keys to create, keyed by name. Null attributes will be interpolated with defaults. | map(object({…})) | | {} | | +| kms_restricted_admins | Map of environment => [identities] who can assign the encrypt/decrypt roles on keys. | map(list(string)) | | {} | | +| outputs_location | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| vpc_sc_access_levels | VPC SC access level definitions. | map(object({…})) | | {} | | +| vpc_sc_egress_policies | VPC SC egress policy defnitions. | map(object({…})) | | {} | | +| vpc_sc_ingress_policies | VPC SC ingress policy defnitions. | map(object({…})) | | {} | | +| vpc_sc_perimeter_access_levels | VPC SC perimeter access_levels. | object({…}) | | null | | +| vpc_sc_perimeter_egress_policies | VPC SC egress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable. | object({…}) | | null | | +| vpc_sc_perimeter_ingress_policies | VPC SC ingress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable. | object({…}) | | null | | +| vpc_sc_perimeter_projects | VPC SC perimeter resources. | object({…}) | | null | | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| stage_perimeter_projects | Security project numbers. They can be added to perimeter resources. | | | + + + + + + + + + + + + + + diff --git a/fast/stages/02-security/core-dev.tf b/fast/stages/02-security/core-dev.tf new file mode 100644 index 00000000..4862ef52 --- /dev/null +++ b/fast/stages/02-security/core-dev.tf @@ -0,0 +1,64 @@ +/** + * 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. + */ + +module "dev-sec-project" { + source = "../../../modules/project" + name = "dev-sec-core-0" + parent = var.folder_id + prefix = var.prefix + billing_account = var.billing_account_id + iam = { + "roles/cloudkms.viewer" = try(var.kms_restricted_admins.dev, []) + } + labels = { environment = "dev", team = "security" } + services = local.project_services +} + +module "dev-sec-kms" { + for_each = toset(local.kms_locations) + source = "../../../modules/kms" + project_id = module.dev-sec-project.project_id + keyring = { + location = each.key + name = "dev-${each.key}" + } + # rename to `key_iam` to switch to authoritative bindings + key_iam_additive = { + for k, v in local.kms_locations_keys[each.key] : k => v.iam + } + keys = local.kms_locations_keys[each.key] +} + +# TODO(ludo): add support for conditions to Fabric modules + +resource "google_project_iam_member" "dev_key_admin_delegated" { + for_each = toset(try(var.kms_restricted_admins.dev, [])) + project = module.dev-sec-project.project_id + role = "roles/cloudkms.admin" + member = each.key + condition { + title = "kms_sa_delegated_grants" + description = "Automation service account delegated grants" + expression = format( + "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", + join(",", formatlist("'%s'", [ + "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation" + ])) + ) + } + depends_on = [module.dev-sec-project] +} diff --git a/fast/stages/02-security/core-prod.tf b/fast/stages/02-security/core-prod.tf new file mode 100644 index 00000000..f259a488 --- /dev/null +++ b/fast/stages/02-security/core-prod.tf @@ -0,0 +1,64 @@ +/** + * 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. + */ + +module "prod-sec-project" { + source = "../../../modules/project" + name = "prod-sec-core-0" + parent = var.folder_id + prefix = var.prefix + billing_account = var.billing_account_id + iam = { + "roles/cloudkms.viewer" = try(var.kms_restricted_admins.prod, []) + } + labels = { environment = "prod", team = "security" } + services = local.project_services +} + +module "prod-sec-kms" { + for_each = toset(local.kms_locations) + source = "../../../modules/kms" + project_id = module.prod-sec-project.project_id + keyring = { + location = each.key + name = "prod-${each.key}" + } + # rename to `key_iam` to switch to authoritative bindings + key_iam_additive = { + for k, v in local.kms_locations_keys[each.key] : k => v.iam + } + keys = local.kms_locations_keys[each.key] +} + +# TODO(ludo): add support for conditions to Fabric modules + +resource "google_project_iam_member" "prod_key_admin_delegated" { + for_each = toset(try(var.kms_restricted_admins.prod, [])) + project = module.prod-sec-project.project_id + role = "roles/cloudkms.admin" + member = each.key + condition { + title = "kms_sa_delegated_grants" + description = "Automation service account delegated grants" + expression = format( + "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", + join(",", formatlist("'%s'", [ + "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation" + ])) + ) + } + depends_on = [module.prod-sec-project] +} diff --git a/fast/stages/02-security/diagram.png b/fast/stages/02-security/diagram.png new file mode 100644 index 00000000..779c9039 Binary files /dev/null and b/fast/stages/02-security/diagram.png differ diff --git a/fast/stages/02-security/diagram.svg b/fast/stages/02-security/diagram.svg new file mode 100644 index 00000000..7dc82d45 --- /dev/null +++ b/fast/stages/02-security/diagram.svg @@ -0,0 +1,1157 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages/02-security/main.tf b/fast/stages/02-security/main.tf new file mode 100644 index 00000000..13078d12 --- /dev/null +++ b/fast/stages/02-security/main.tf @@ -0,0 +1,47 @@ +/** + * 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. + */ + +locals { + kms_keys = { + for k, v in var.kms_keys : k => { + iam = coalesce(v.iam, {}) + labels = coalesce(v.labels, {}) + locations = ( + v.locations == null + ? var.kms_defaults.locations + : v.locations + ) + rotation_period = ( + v.rotation_period == null + ? var.kms_defaults.rotation_period + : v.rotation_period + ) + } + } + kms_locations = distinct(flatten([ + for k, v in local.kms_keys : v.locations + ])) + kms_locations_keys = { + for loc in local.kms_locations : loc => { + for k, v in local.kms_keys : k => v if contains(v.locations, loc) + } + } + project_services = [ + "cloudkms.googleapis.com", + "secretmanager.googleapis.com", + "stackdriver.googleapis.com" + ] +} diff --git a/fast/stages/02-security/outputs.tf b/fast/stages/02-security/outputs.tf new file mode 100644 index 00000000..8f296d86 --- /dev/null +++ b/fast/stages/02-security/outputs.tf @@ -0,0 +1,43 @@ +/** + * 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. + */ + +# optionally generate files for subsequent stages + +resource "local_file" "dev_sec_kms" { + for_each = var.outputs_location == null ? {} : { 1 = 1 } + filename = "${var.outputs_location}/yamls/02-security-kms-dev-keys.yaml" + content = yamlencode({ + for k, m in module.dev-sec-kms : k => m.key_ids + }) +} + +resource "local_file" "prod_sec_kms" { + for_each = var.outputs_location == null ? {} : { 1 = 1 } + filename = "${var.outputs_location}/yamls/02-security-kms-prod-keys.yaml" + content = yamlencode({ + for k, m in module.prod-sec-kms : k => m.key_ids + }) +} + +# outputs + +output "stage_perimeter_projects" { + description = "Security project numbers. They can be added to perimeter resources." + value = { + dev = ["projects/${module.dev-sec-project.number}"] + prod = ["projects/${module.prod-sec-project.number}"] + } +} diff --git a/fast/stages/02-security/variables.tf b/fast/stages/02-security/variables.tf new file mode 100644 index 00000000..0829f098 --- /dev/null +++ b/fast/stages/02-security/variables.tf @@ -0,0 +1,185 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "billing_account_id" { + # tfdoc:variable:source bootstrap + description = "Billing account id." + type = string +} + +variable "folder_id" { + # tfdoc:variable:source resman + description = "Folder to be used for the networking resources in folders/nnnn format." + type = string +} + +variable "groups" { + # tfdoc:variable:source bootstrap + description = "Group names to grant organization-level permissions." + type = map(string) + # 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" + } +} + +variable "kms_defaults" { + description = "Defaults used for KMS keys." + type = object({ + locations = list(string) + rotation_period = string + }) + default = { + locations = ["europe", "europe-west1", "europe-west3", "global"] + rotation_period = "7776000s" + } +} + +variable "kms_keys" { + description = "KMS keys to create, keyed by name. Null attributes will be interpolated with defaults." + type = map(object({ + iam = map(list(string)) + labels = map(string) + locations = list(string) + rotation_period = string + })) + default = {} +} + +variable "kms_restricted_admins" { + description = "Map of environment => [identities] who can assign the encrypt/decrypt roles on keys." + type = map(list(string)) + default = {} +} + +variable "organization" { + # tfdoc:variable:source bootstrap + description = "Organization details." + type = object({ + domain = string + id = number + customer_id = string + }) +} + +variable "outputs_location" { + description = "Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable." + type = string + default = null +} + +variable "prefix" { + description = "Prefix used for resources that need unique names." + type = string +} + +variable "vpc_sc_access_levels" { + description = "VPC SC access level definitions." + type = map(object({ + combining_function = string + conditions = list(object({ + ip_subnetworks = list(string) + members = list(string) + negate = bool + regions = list(string) + required_access_levels = list(string) + })) + })) + default = {} +} + +variable "vpc_sc_egress_policies" { + description = "VPC SC egress policy defnitions." + type = map(object({ + egress_from = object({ + identity_type = string + identities = list(string) + }) + egress_to = object({ + operations = list(object({ + method_selectors = list(string) + service_name = string + })) + resources = list(string) + }) + })) + default = {} +} + +variable "vpc_sc_ingress_policies" { + description = "VPC SC ingress policy defnitions." + type = map(object({ + ingress_from = object({ + identity_type = string + identities = list(string) + source_access_levels = list(string) + source_resources = list(string) + }) + ingress_to = object({ + operations = list(object({ + method_selectors = list(string) + service_name = string + })) + resources = list(string) + }) + })) + default = {} +} + +variable "vpc_sc_perimeter_access_levels" { + description = "VPC SC perimeter access_levels." + type = object({ + dev = list(string) + landing = list(string) + prod = list(string) + }) + default = null +} + +variable "vpc_sc_perimeter_egress_policies" { + description = "VPC SC egress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable." + type = object({ + dev = list(string) + landing = list(string) + prod = list(string) + }) + default = null +} + +variable "vpc_sc_perimeter_ingress_policies" { + description = "VPC SC ingress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable." + type = object({ + dev = list(string) + landing = list(string) + prod = list(string) + }) + default = null +} + +variable "vpc_sc_perimeter_projects" { + description = "VPC SC perimeter resources." + type = object({ + dev = list(string) + landing = list(string) + prod = list(string) + }) + default = null +} diff --git a/fast/stages/02-security/vpc-sc-restricted-services.yaml b/fast/stages/02-security/vpc-sc-restricted-services.yaml new file mode 100644 index 00000000..89844cd2 --- /dev/null +++ b/fast/stages/02-security/vpc-sc-restricted-services.yaml @@ -0,0 +1,88 @@ +# skip boilerplate check +- accessapproval.googleapis.com +- adsdatahub.googleapis.com +- aiplatform.googleapis.com +- alpha-documentai.googleapis.com +- apigee.googleapis.com +- apigeeconnect.googleapis.com +- artifactregistry.googleapis.com +- assuredworkloads.googleapis.com +- automl.googleapis.com +- bigquery.googleapis.com +- bigquerydatatransfer.googleapis.com +- bigtable.googleapis.com +- binaryauthorization.googleapis.com +- cloudasset.googleapis.com +- cloudbuild.googleapis.com +- cloudfunctions.googleapis.com +- cloudkms.googleapis.com +- cloudprofiler.googleapis.com +- cloudresourcemanager.googleapis.com +- cloudsearch.googleapis.com +- cloudtrace.googleapis.com +- composer.googleapis.com +- compute.googleapis.com +- connectgateway.googleapis.com +- contactcenterinsights.googleapis.com +- container.googleapis.com +- containeranalysis.googleapis.com +- containerregistry.googleapis.com +- containerthreatdetection.googleapis.com +- datacatalog.googleapis.com +- dataflow.googleapis.com +- datafusion.googleapis.com +- dataproc.googleapis.com +- datastream.googleapis.com +- dialogflow.googleapis.com +- dlp.googleapis.com +- dns.googleapis.com +- documentai.googleapis.com +- eventarc.googleapis.com +- file.googleapis.com +- gameservices.googleapis.com +- gkeconnect.googleapis.com +- gkehub.googleapis.com +- healthcare.googleapis.com +- iam.googleapis.com +- iaptunnel.googleapis.com +- language.googleapis.com +- lifesciences.googleapis.com +- logging.googleapis.com +- managedidentities.googleapis.com +- memcache.googleapis.com +- meshca.googleapis.com +- metastore.googleapis.com +- ml.googleapis.com +- monitoring.googleapis.com +- networkconnectivity.googleapis.com +- networkmanagement.googleapis.com +- networksecurity.googleapis.com +- networkservices.googleapis.com +- notebooks.googleapis.com +- opsconfigmonitoring.googleapis.com +- osconfig.googleapis.com +- oslogin.googleapis.com +- privateca.googleapis.com +- pubsub.googleapis.com +- pubsublite.googleapis.com +- recaptchaenterprise.googleapis.com +- recommender.googleapis.com +- redis.googleapis.com +- run.googleapis.com +- secretmanager.googleapis.com +- servicecontrol.googleapis.com +- servicedirectory.googleapis.com +- spanner.googleapis.com +- speakerid.googleapis.com +- speech.googleapis.com +- sqladmin.googleapis.com +- storage.googleapis.com +- storagetransfer.googleapis.com +- texttospeech.googleapis.com +- tpu.googleapis.com +- trafficdirector.googleapis.com +- transcoder.googleapis.com +- translate.googleapis.com +- videointelligence.googleapis.com +- vision.googleapis.com +- vpcaccess.googleapis.com diff --git a/fast/stages/02-security/vpc-sc.tf b/fast/stages/02-security/vpc-sc.tf new file mode 100644 index 00000000..855dc1dd --- /dev/null +++ b/fast/stages/02-security/vpc-sc.tf @@ -0,0 +1,167 @@ +/** + * 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. + */ + +locals { + # compute the number of projects in each perimeter to detect which to create + vpc_sc_counts = { + for k in ["dev", "landing", "prod"] : k => length( + coalesce(try(var.vpc_sc_perimeter_projects[k], null), []) + ) + } + # dereference perimeter egress policy names to the actual objects + vpc_sc_perimeter_egress_policies = { + for k, v in coalesce(var.vpc_sc_perimeter_egress_policies, {}) : + k => [ + for i in coalesce(v, []) : var.vpc_sc_egress_policies[i] + if lookup(var.vpc_sc_egress_policies, i, null) != null + ] + } + # dereference perimeter ingress policy names to the actual objects + vpc_sc_perimeter_ingress_policies = { + for k, v in coalesce(var.vpc_sc_perimeter_ingress_policies, {}) : + k => [ + for i in coalesce(v, []) : var.vpc_sc_ingress_policies[i] + if lookup(var.vpc_sc_ingress_policies, i, null) != null + ] + } + # get the list of restricted services from the yaml file + vpcsc_restricted_services = yamldecode( + file("${path.module}/vpc-sc-restricted-services.yaml") + ) +} + +module "vpc-sc" { + source = "../../../modules/vpc-sc" + # only enable if we have projects defined for perimeters + count = anytrue([for k, v in local.vpc_sc_counts : v > 0]) ? 1 : 0 + access_policy = null + access_policy_create = { + parent = "organizations/${var.organization.id}" + title = "default" + } + access_levels = coalesce(try(var.vpc_sc_access_levels, null), {}) + # bridge type perimeters + service_perimeters_bridge = merge( + # landing to dev, only we have projects in landing and dev perimeters + local.vpc_sc_counts.landing * local.vpc_sc_counts.dev == 0 ? {} : { + landing_to_dev = { + status_resources = null + spec_resources = concat( + var.vpc_sc_perimeter_projects.landing, + var.vpc_sc_perimeter_projects.dev + ) + use_explicit_dry_run_spec = true + } + }, + # landing to prod, only we have projects in landing and prod perimeters + local.vpc_sc_counts.landing * local.vpc_sc_counts.prod == 0 ? {} : { + landing_to_prod = { + status_resources = null + spec_resources = concat( + var.vpc_sc_perimeter_projects.landing, + var.vpc_sc_perimeter_projects.prod + ) + # set to null and switch spec and status above to enforce + use_explicit_dry_run_spec = true + } + } + ) + # regular type perimeters + service_perimeters_regular = merge( + # dev if we have projects in var.vpc_sc_perimeter_projects.dev + local.vpc_sc_counts.dev == 0 ? {} : { + dev = { + spec = { + access_levels = coalesce( + try(var.vpc_sc_perimeter_access_levels.dev, null), [] + ) + resources = var.vpc_sc_perimeter_projects.dev + restricted_services = local.vpcsc_restricted_services + egress_policies = try( + local.vpc_sc_perimeter_egress_policies.dev, null + ) + ingress_policies = try( + local.vpc_sc_perimeter_ingress_policies.dev, null + ) + # replace with commented block to enable vpc restrictions + vpc_accessible_services = null + # vpc_accessible_services = { + # allowed_services = ["RESTRICTED-SERVICES"] + # enable_restriction = true + # } + } + status = null + # set to null and switch spec and status above to enforce + use_explicit_dry_run_spec = true + } + }, + # prod if we have projects in var.vpc_sc_perimeter_projects.prod + local.vpc_sc_counts.prod == 0 ? {} : { + prod = { + spec = { + access_levels = coalesce( + try(var.vpc_sc_perimeter_access_levels.prod, null), [] + ) + # combine the security project, and any specified in the variable + resources = var.vpc_sc_perimeter_projects.prod + restricted_services = local.vpcsc_restricted_services + egress_policies = try( + local.vpc_sc_perimeter_egress_policies.prod, null + ) + ingress_policies = try( + local.vpc_sc_perimeter_ingress_policies.prod, null + ) + # replace with commented block to enable vpc restrictions + vpc_accessible_services = null + # vpc_accessible_services = { + # allowed_services = ["RESTRICTED-SERVICES"] + # enable_restriction = true + # } + } + status = null + # set to null and switch spec and status above to enforce + use_explicit_dry_run_spec = true + } + }, + # prod if we have projects in var.vpc_sc_perimeter_projects.prod + local.vpc_sc_counts.landing == 0 ? {} : { + landing = { + spec = { + access_levels = coalesce( + try(var.vpc_sc_perimeter_access_levels.landing, null), [] + ) + resources = var.vpc_sc_perimeter_projects.landing + restricted_services = local.vpcsc_restricted_services + egress_policies = try( + local.vpc_sc_perimeter_egress_policies.landing, null + ) + ingress_policies = try( + local.vpc_sc_perimeter_ingress_policies.landing, null + ) + # replace with commented block to enable vpc restrictions + vpc_accessible_services = null + # vpc_accessible_services = { + # allowed_services = ["RESTRICTED-SERVICES"] + # enable_restriction = true + # } + } + status = null + # set to null and switch spec and status above to enforce + use_explicit_dry_run_spec = true + } + } + ) +} diff --git a/fast/stages/03-project-factory/README.md b/fast/stages/03-project-factory/README.md new file mode 100644 index 00000000..2be41b95 --- /dev/null +++ b/fast/stages/03-project-factory/README.md @@ -0,0 +1,6 @@ +# Project factory + +The Project Factory (PF) 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](https://medium.com/google-cloud/resource-factories-a-descriptive-approach-to-terraform-581b3ebb59c). + +This directory contains a single project factory ([`prod/`](./prod/)) as an example - to implement multiple environments (e.g. "prod" and "dev") you'll need to copy the `prod` folder into one folder per environment, then customize each one following the instructions found in [`prod/README.md`](./prod/README.md). \ No newline at end of file diff --git a/fast/stages/03-project-factory/prod/README.md b/fast/stages/03-project-factory/prod/README.md new file mode 100644 index 00000000..b938f788 --- /dev/null +++ b/fast/stages/03-project-factory/prod/README.md @@ -0,0 +1,131 @@ +# Project factory + +The Project Factory (or PF) 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](https://medium.com/google-cloud/resource-factories-a-descriptive-approach-to-terraform-581b3ebb59c). + +## Design overview and choices + +

+ Project factory diagram +

+ +A single factory creates projects in a well-defined context, according to your resource management structure. For example, in the diagram above, each Team is structured to have specific folders projects for a given environment, such as Production and Development, per the resource management structure configured in stage `01-resman`. + +Projects for each environment across different teams are created by dedicated service accounts, as exemplified in the diagram above. While there's no intrinsic limitation regarding where the project factory can create a projects, the IAM bindings for the service account effectively enforce boundaries (e.g., the production service account shouldn't be able to create or have any access to the development projects, and vice versa). + +The project factory takes care of the following activities: + +* Project creation +* API/Services enablement +* Service accounts creation +* IAM roles assignment for groups and service accounts +* KMS keys roles assignment +* Shared VPC attachment and subnets IAM binding +* DNS zones creation and visibility configuration +* Project-level org policies definition +* Billing setup (billing account attachment and budget configuration) +* Essential contacts definition (for [budget alerts](https://cloud.google.com/billing/docs/how-to/budgets) and [important notifications](https://cloud.google.com/resource-manager/docs/managing-notification-contacts?hl=en)) + + +## 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`](../../02-networking) and [`02-security`](../../02-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: + +* One service account per environment, each with appropriate permissions + * at the organization level a custom role for networking operations including the following permissions + * `"compute.organizations.enableXpnResource"`, + * `"compute.organizations.disableXpnResource"`, + * `"compute.subnetworks.setIamPolicy"`, + * `"dns.networks.bindPrivateDNSZone"` + * and role `"roles/orgpolicy.policyAdmin"` + * on each folder where projects are created + * `"roles/logging.admin"` + * `"roles/owner"` + * `"roles/resourcemanager.folderAdmin"` + * `"roles/resourcemanager.projectCreator"` + * on the host project for the Shared VPC + * `"roles/browser"` + * `"roles/compute.viewer"` + * `"roles/dns.admin"` +* If networking is used (e.g., for VMs, GKE Clusters or AppEngine flex), VPC Host projects and their subnets should exist when creating projects +* If per-environment DNS sub-zones are required, one "root" zone per environment should exist when creating projects (e.g., prod.gcp.example.com.) + +### Providers configuration + +If you're running this on top of Fast, you should run the following commands to create the providers file, and populate the required variables from the previous stage. + +```bash +# Variable `outputs_location` is set to `../../configs/example` in stage 01-resman +$ cd fabric-fast/stages/03-project-factory/prod +ln -s ../../../configs/example/03-project-factory-prod/providers.tf +``` + +### Variable configuration + +There are two broad sets of variables you will need to fill in: + +- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) +- variables specific to resources managed by this stage + +To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. + +If you configured a valid path for `outputs_location` in the bootstrap and networking stage, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's outputs folder (under the path you specified), where the `*` above is set to the name of the stage that produced it. For this stage, a single `.tfvars` file is available: + +```bash +# Variable `outputs_location` is set to `../../configs/example` in stages 01-bootstrap and 02-networking +ln -s ../../../configs/example/03-project-factory-prod/terraform-bootstrap.auto.tfvars.json +ln -s ../../../configs/example/03-project-factory-prod/terraform-networking.auto.tfvars.json +``` + +If you're not using Fast, refer to the [Variables](#variables) table at the bottom of this document for a full list of variables, their origin (e.g., a stage or specific to this one), and descriptions explaining their meaning. + +Besides the values above, a project factory takes 2 additional inputs: + +* `data/defaults.yaml`, manually configured by adapting the [`prod/data/defaults.yaml.sample`](./prod/data/defaults.yaml.sample), which defines per-environment default values e.g., for billing alerts and labels. + +* `data/projects/*.yaml`, one file per project (optionally grouped in folders), which configures each project. A [`prod/data/projects/project.yaml.sample`](./prod/data/projects/project.yaml.sample) is provided as reference and documentation for the schema. Projects will be named after the filename, e.g., `fast-prod-lab0.yaml` will create project `fast-prod-lab0`. + +Once the configuration is complete, run the project factory by running + +```bash +terraform init +terraform apply +``` + + + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [main.tf](./main.tf) | Project factory. | project-factory | | +| [outputs.tf](./outputs.tf) | Module outputs. | | | +| [variables.tf](./variables.tf) | Module variables. | | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| billing_account_id | Billing account id. | string | ✓ | | 00-bootstrap | +| shared_vpc_self_link | Self link for the shared VPC. | string | ✓ | | 02-networking | +| vpc_host_project | Host project for the shared VPC. | string | ✓ | | 02-networking | +| data_dir | Relative path for the folder storing configuration data. | string | | "data/projects" | | +| defaults_file | Relative path for the file storing the project factory configuration. | string | | "data/defaults.yaml" | | +| environment_dns_zone | DNS zone suffix for environment. | string | | null | 02-networking | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| projects | Created projects and service accounts. | | | + + + + + + + diff --git a/fast/stages/03-project-factory/prod/data/defaults.yaml b/fast/stages/03-project-factory/prod/data/defaults.yaml new file mode 100644 index 00000000..dc5b1616 --- /dev/null +++ b/fast/stages/03-project-factory/prod/data/defaults.yaml @@ -0,0 +1,24 @@ +# skip boilerplate check + +billing_account_id: 012345-67890A-BCDEF0 + +# [opt] Setup for billing alerts +billing_alert: + amount: 1000 + thresholds: + current: [0.5, 0.8] + forecasted: [0.5, 0.8] + credit_treatment: INCLUDE_ALL_CREDITS + +# [opt] Contacts for billing alerts and important notifications +essential_contacts: ["team-contacts@example.com"] + +# [opt] Labels set for all projects +labels: + environment: prod + department: accounting + application: example-app + foo: bar + +# [opt] Additional notification channels for billing +notification_channels: [] diff --git a/fast/stages/03-project-factory/prod/data/projects/project.yaml b/fast/stages/03-project-factory/prod/data/projects/project.yaml new file mode 100644 index 00000000..244f6955 --- /dev/null +++ b/fast/stages/03-project-factory/prod/data/projects/project.yaml @@ -0,0 +1,100 @@ +# skip boilerplate check + +# [opt] Billing account id - overrides default if set +billing_account_id: 012345-67890A-BCDEF0 + +# [opt] Billing alerts config - overrides default if set +billing_alert: + amount: 10 + thresholds: + current: + - 0.5 + - 0.8 + forecasted: [] + credit_treatment: INCLUDE_ALL_CREDITS + +# [opt] DNS zones to be created as children of the environment_dns_zone defined in defaults +dns_zones: + - lorem + - ipsum + +# [opt] Contacts for billing alerts and important notifications +essential_contacts: + - team-a-contacts@example.com + +# Folder the project will be created as children of +folder_id: folders/012345678901 + +# [opt] Authoritative IAM bindings in group => [roles] format +group_iam: + test-team-foobar@fast-lab-0.gcp-pso-italy.net: + - roles/compute.admin + +# [opt] Authoritative IAM bindings in role => [principals] format +# Generally used to grant roles to service accounts external to the project +iam: + roles/compute.admin: + - serviceAccount:service-account + +# [opt] Service robots and keys they will be assigned as cryptoKeyEncrypterDecrypter +# in service => [keys] format +kms_service_agents: + compute: [key1, key2] + storage: [key1, key2] + +# [opt] Labels for the project - merged with the ones defined in defaults +labels: + environment: prod + +# [opt] Org policy overrides defined at project level +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + policy_list: + constraints/compute.trustedImageProjects: + inherit_from_parent: null + status: true + suggested_value: null + values: + - projects/fast-prod-iac-core-0 + +# [opt] Service account to create for the project and their roles on the project +# in name => [roles] format +service_accounts: + another-service-account: + - roles/compute.admin + my-service-account: + - roles/compute.admin + +# [opt] APIs to enable on the project. +services: + - storage.googleapis.com + - stackdriver.googleapis.com + - compute.googleapis.com + +# [opt] Roles to assign to the robots service accounts in robot => [roles] format +services_iam: + compute: + - roles/storage.objectViewer + + # [opt] VPC setup. + # If set enables the `compute.googleapis.com` service and configures + # service project attachment +vpc: + # [opt] If set, enables the container API + gke_setup: + # Grants "roles/container.hostServiceAgentUser" to the container robot if set + enable_host_service_agent: false + + # Grants "roles/compute.securityAdmin" to the container robot if set + enable_security_admin: true + + # Host project the project will be service project of + host_project: fast-prod-net-spoke-0 + + # [opt] Subnets in the host project where principals will be granted networkUser + # in region/subnet-name => [principals] + subnets_iam: + europe-west1/prod-default-ew1: + - user:foobar@example.com + - serviceAccount:service-account1 diff --git a/fast/stages/03-project-factory/prod/diagram.png b/fast/stages/03-project-factory/prod/diagram.png new file mode 100644 index 00000000..b942ea47 Binary files /dev/null and b/fast/stages/03-project-factory/prod/diagram.png differ diff --git a/fast/stages/03-project-factory/prod/diagram.svg b/fast/stages/03-project-factory/prod/diagram.svg new file mode 100644 index 00000000..d7821c60 --- /dev/null +++ b/fast/stages/03-project-factory/prod/diagram.svg @@ -0,0 +1,1530 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages/03-project-factory/prod/main.tf b/fast/stages/03-project-factory/prod/main.tf new file mode 100644 index 00000000..a6636b01 --- /dev/null +++ b/fast/stages/03-project-factory/prod/main.tf @@ -0,0 +1,56 @@ +/** + * 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. + */ + +# tfdoc:file:description Project factory. + + +locals { + _defaults = yamldecode(file(var.defaults_file)) + _defaults_net = { + billing_account_id = var.billing_account_id + environment_dns_zone = var.environment_dns_zone + shared_vpc_self_link = var.shared_vpc_self_link + vpc_host_project = var.vpc_host_project + } + defaults = merge(local._defaults, local._defaults_net) + projects = { + for f in fileset("${var.data_dir}", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("${var.data_dir}/${f}")) + } +} + +module "projects" { + source = "../../../../examples/factories/project-factory" + for_each = local.projects + defaults = local.defaults + project_id = each.key + billing_account_id = try(each.value.billing_account_id, null) + billing_alert = try(each.value.billing_alert, null) + dns_zones = try(each.value.dns_zones, []) + essential_contacts = try(each.value.essential_contacts, []) + folder_id = each.value.folder_id + group_iam = try(each.value.group_iam, {}) + iam = try(each.value.iam, {}) + kms_service_agents = try(each.value.kms, {}) + labels = try(each.value.labels, {}) + org_policies = try(each.value.org_policies, null) + service_accounts = try(each.value.service_accounts, {}) + services = try(each.value.services, []) + services_iam = try(each.value.services_iam, {}) + vpc = try(each.value.vpc, null) +} + + diff --git a/fast/stages/03-project-factory/prod/outputs.tf b/fast/stages/03-project-factory/prod/outputs.tf new file mode 100644 index 00000000..59ecff95 --- /dev/null +++ b/fast/stages/03-project-factory/prod/outputs.tf @@ -0,0 +1,20 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "projects" { + description = "Created projects and service accounts." + value = module.projects +} diff --git a/fast/stages/03-project-factory/prod/variables.tf b/fast/stages/03-project-factory/prod/variables.tf new file mode 100644 index 00000000..8bb9f035 --- /dev/null +++ b/fast/stages/03-project-factory/prod/variables.tf @@ -0,0 +1,54 @@ +/** + * 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. + */ + +#TODO: tfdoc annotations + +variable "billing_account_id" { + # tfdoc:variable:source 00-bootstrap + description = "Billing account id." + type = string +} + +variable "data_dir" { + description = "Relative path for the folder storing configuration data." + type = string + default = "data/projects" +} + +variable "environment_dns_zone" { + # tfdoc:variable:source 02-networking + description = "DNS zone suffix for environment." + type = string + default = null +} + +variable "defaults_file" { + description = "Relative path for the file storing the project factory configuration." + type = string + default = "data/defaults.yaml" +} + +variable "shared_vpc_self_link" { + # tfdoc:variable:source 02-networking + description = "Self link for the shared VPC." + type = string +} + +variable "vpc_host_project" { + # tfdoc:variable:source 02-networking + description = "Host project for the shared VPC." + type = string +} diff --git a/fast/stages/README.md b/fast/stages/README.md new file mode 100644 index 00000000..7cc02931 --- /dev/null +++ b/fast/stages/README.md @@ -0,0 +1,29 @@ +# Fast stages + +Each of the folders contained here is a separate "stage", or Terraform root module. + +They are designed to be combined together, each stage leveraging the previous stage's resources and providing outputs to the following stages, but they can also be run in isolation if their specific functionality is all that is needed (e.g. only bring up a hub and spoke VPC in an existing environment). + +Refer to each stage's documentation for a detailed description of its purpose, the architectural choices made in its design, and how it can be configured and wired together to terraform a whole GCP organization. The following is a brief overview of each stage. + +## Organizational level (00-01) + +- [Bootstrap](stages/00-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. +- [Resource Management](stages/01-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. + +## Shared resources (02) + +- [Security](stages/02-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. +- [Networking](stages/02-security/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. + +## Environment-level resources (03) + +- [Project Factory](stages/03-project-factory/README.md) + 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 (in development) +- GKE Multitenant (in development) +- GCE Migration (in development) diff --git a/stages.png b/stages.png new file mode 100644 index 00000000..83f3c7e8 Binary files /dev/null and b/stages.png differ diff --git a/tests/conftest.py b/tests/conftest.py index 98f275e2..cb404a9c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,12 +38,15 @@ def _plan_runner(): fixture_parent = os.path.dirname(fixture_path) fixture_prefix = os.path.basename(fixture_path) + "_" - + tf_lock = os.path.join(BASEDIR, 'tests/.terraform.lock.hcl') + tf_lock_use = os.path.isfile(tf_lock) with tempfile.TemporaryDirectory(prefix=fixture_prefix, dir=fixture_parent) as tmp_path: # copy fixture to a temporary directory so we can execute # multiple tests in parallel shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True) + if tf_lock_use: + os.symlink(tf_lock, os.path.join(tmp_path, '.terraform.lock.hcl')) tf = tftest.TerraformTest(tmp_path, BASEDIR, os.environ.get('TERRAFORM', 'terraform')) tf.setup(upgrade=True) @@ -52,7 +55,7 @@ def _plan_runner(): return run_plan -@pytest.fixture(scope='session') +@ pytest.fixture(scope='session') def plan_runner(_plan_runner): "Returns a function to run Terraform plan on a module fixture." @@ -66,7 +69,7 @@ def plan_runner(_plan_runner): return run_plan -@pytest.fixture(scope='session') +@ pytest.fixture(scope='session') def e2e_plan_runner(_plan_runner): "Returns a function to run Terraform plan on an end-to-end fixture." @@ -88,7 +91,7 @@ def e2e_plan_runner(_plan_runner): return run_plan -@pytest.fixture(scope='session') +@ pytest.fixture(scope='session') def example_plan_runner(_plan_runner): "Returns a function to run Terraform plan on documentation examples." @@ -104,7 +107,7 @@ def example_plan_runner(_plan_runner): return run_plan -@pytest.fixture(scope='session') +@ pytest.fixture(scope='session') def apply_runner(): "Returns a function to run Terraform apply on a fixture." diff --git a/tests/modules/examples/conftest.py b/tests/doc_examples/conftest.py similarity index 96% rename from tests/modules/examples/conftest.py rename to tests/doc_examples/conftest.py index be07a74e..f36191dc 100644 --- a/tests/modules/examples/conftest.py +++ b/tests/doc_examples/conftest.py @@ -17,7 +17,7 @@ from pathlib import Path import marko -MODULES_PATH = Path(__file__).parents[3] / 'modules/' +MODULES_PATH = Path(__file__).parents[2] / 'modules/' print(MODULES_PATH) diff --git a/tests/modules/examples/test_plan.py b/tests/doc_examples/test_plan.py similarity index 80% rename from tests/modules/examples/test_plan.py rename to tests/doc_examples/test_plan.py index b1112a10..6c8d186a 100644 --- a/tests/modules/examples/test_plan.py +++ b/tests/doc_examples/test_plan.py @@ -12,21 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import tftest import re -import tempfile from pathlib import Path -import marko -MODULES_PATH = Path(__file__, '../../../../modules/').resolve() -VARIABLES_PATH = Path(__file__, '../variables.tf').resolve() +BASE_PATH = Path(__file__).parent EXPECTED_RESOURCES_RE = re.compile(r'# tftest:modules=(\d+):resources=(\d+)') def test_example(example_plan_runner, tmp_path, example): - (tmp_path / 'modules').symlink_to(MODULES_PATH) - (tmp_path / 'variables.tf').symlink_to(VARIABLES_PATH) + (tmp_path / 'modules').symlink_to( + Path(BASE_PATH, '../../modules/').resolve()) + (tmp_path / 'variables.tf').symlink_to( + Path(BASE_PATH, 'variables.tf').resolve()) (tmp_path / 'main.tf').write_text(example) match = EXPECTED_RESOURCES_RE.search(example) diff --git a/tests/modules/examples/variables.tf b/tests/doc_examples/variables.tf similarity index 100% rename from tests/modules/examples/variables.tf rename to tests/doc_examples/variables.tf diff --git a/tests/examples/factories/net-vpc-firewall-yaml/__init__.py b/tests/examples/factories/net_vpc_firewall_yaml/__init__.py similarity index 100% rename from tests/examples/factories/net-vpc-firewall-yaml/__init__.py rename to tests/examples/factories/net_vpc_firewall_yaml/__init__.py diff --git a/tests/examples/factories/net-vpc-firewall-yaml/fixture/main.tf b/tests/examples/factories/net_vpc_firewall_yaml/fixture/main.tf similarity index 100% rename from tests/examples/factories/net-vpc-firewall-yaml/fixture/main.tf rename to tests/examples/factories/net_vpc_firewall_yaml/fixture/main.tf diff --git a/tests/examples/factories/net-vpc-firewall-yaml/fixture/rules/common.yaml b/tests/examples/factories/net_vpc_firewall_yaml/fixture/rules/common.yaml similarity index 100% rename from tests/examples/factories/net-vpc-firewall-yaml/fixture/rules/common.yaml rename to tests/examples/factories/net_vpc_firewall_yaml/fixture/rules/common.yaml diff --git a/tests/examples/factories/net-vpc-firewall-yaml/fixture/variables.tf b/tests/examples/factories/net_vpc_firewall_yaml/fixture/variables.tf similarity index 100% rename from tests/examples/factories/net-vpc-firewall-yaml/fixture/variables.tf rename to tests/examples/factories/net_vpc_firewall_yaml/fixture/variables.tf diff --git a/tests/examples/factories/net-vpc-firewall-yaml/test_plan.py b/tests/examples/factories/net_vpc_firewall_yaml/test_plan.py similarity index 100% rename from tests/examples/factories/net-vpc-firewall-yaml/test_plan.py rename to tests/examples/factories/net_vpc_firewall_yaml/test_plan.py diff --git a/tests/fast/README.md b/tests/fast/README.md new file mode 100644 index 00000000..01ea3e69 --- /dev/null +++ b/tests/fast/README.md @@ -0,0 +1,34 @@ +# Fabric FAST + +Setting up a production-ready GCP organization is often a time-consuming process. Fabric FAST aims to speed up this process via two complementary goals. On the one hand, FAST provides a design of a GCP organization that includes the typical elements required by enterprise customers. Secondly, we provide a reference implementation of the FAST design using Terraform. + +Note that while our implementation is necessarily influenced (and constrained) by the way Terraform works, the design we put forward only refers to GCP constructs and features. In other words, while we use Terraform for our reference implementation, in theory, the FAST design can be implemented using any other tool (e.g., Pulumi, bash scripts, or even calling the relevant APIs directly). + +Fabric FAST comes from engineers in Google Cloud's Professional Services Organization, with a combined experience of decades solving the typical technical problems faced by GCP customers. While every GCP user has specific requirements, many common issues arise repeatedly. Solving those issues correctly from the beginning is key to a robust and scalable GCP setup. It's those common issues and their solutions that Fabric FAST aims to collect and present coherently. + +Fabric FAST was initially conceived to help enterprises quickly set up a GCP organization following battle-tested and widely-used patterns. Despite its origin in enterprise environments, FAST includes many customization points making it an ideal blueprint for organizations of all sizes, ranging from startups to the largest companies. + + +## Guiding principles +### 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. + +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. + +### Security-first design +Security was, from the beginning, one of the most critical elements in the design of Fabric FAST. Many of FAST's design decisions aim to build the foundations of a secure organization. In fact, the first two stages deal mainly with the organization-wide security setup. + +FAST also aims to minimize the number of permissions granted to principals according to the security-first approach previously mentioned. We achieve this through the meticulous use of groups, service accounts, custom roles, and [Cloud IAM Conditions](https://cloud.google.com/iam/docs/conditions-overview), among other things. + +### Extensive use of factories +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. + +## High level design + +TBD + +## Implementation + +TBD diff --git a/tests/fast/__init__.py b/tests/fast/__init__.py new file mode 100644 index 00000000..6d6d1266 --- /dev/null +++ b/tests/fast/__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/conftest.py b/tests/fast/conftest.py new file mode 100644 index 00000000..d96af5dc --- /dev/null +++ b/tests/fast/conftest.py @@ -0,0 +1,48 @@ +# 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. + +"Shared fixtures" + +import inspect +import os +import types + +import pytest +import tftest + + +BASEDIR = os.path.dirname(os.path.dirname(__file__)) + + +@pytest.fixture(scope='session') +def fast_e2e_plan_runner(_plan_runner): + "Plan runner for end-to-end root module, returns modules and resources." + def run_plan(fixture_path=None, targets=None, refresh=True, + include_bare_resources=False, compute_sums=True, **tf_vars): + "Runs Terraform plan on a root module using defaults, returns data." + plan = _plan_runner(fixture_path, targets=targets, refresh=refresh, + **tf_vars) + root_module = plan.root_module['child_modules'][0] + modules = { + m['address'].removeprefix(root_module['address'])[1:]: m['resources'] + for m in root_module['child_modules'] + } + resources = [r for m in modules.values() for r in m] + if include_bare_resources: + bare_resources = root_module['resources'] + resources.extend(bare_resources) + if compute_sums: + return len(modules), len(resources), {k: len(v) for k, v in modules.items()} + return modules, resources + return run_plan diff --git a/tests/fast/stages/__init__.py b/tests/fast/stages/__init__.py new file mode 100644 index 00000000..6d6d1266 --- /dev/null +++ b/tests/fast/stages/__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/s00_bootstrap/__init__.py b/tests/fast/stages/s00_bootstrap/__init__.py new file mode 100644 index 00000000..6d6d1266 --- /dev/null +++ b/tests/fast/stages/s00_bootstrap/__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/s00_bootstrap/fixture/main.tf b/tests/fast/stages/s00_bootstrap/fixture/main.tf new file mode 100644 index 00000000..1f07048a --- /dev/null +++ b/tests/fast/stages/s00_bootstrap/fixture/main.tf @@ -0,0 +1,29 @@ +/** + * 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. + */ + +module "stage" { + source = "../../../../../fast/stages/00-bootstrap" + prefix = "fast" + organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" + } + billing_account = { + id = "000000-111111-222222" + organization_id = 123456789012 + } +} diff --git a/tests/fast/stages/s00_bootstrap/test_plan.py b/tests/fast/stages/s00_bootstrap/test_plan.py new file mode 100644 index 00000000..2201cfcc --- /dev/null +++ b/tests/fast/stages/s00_bootstrap/test_plan.py @@ -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. + +# _RESOURCE_COUNT = { +# 'module.organization': 28, +# 'module.automation-project': 23, +# 'module.automation-tf-bootstrap-gcs': 1, +# 'module.automation-tf-bootstrap-sa': 1, +# 'module.automation-tf-resman-gcs': 2, +# 'module.automation-tf-resman-sa': 1, +# 'module.billing-export-dataset': 1, +# 'module.billing-export-project': 7, +# 'module.log-export-dataset': 1, +# 'module.log-export-project': 7, +# } + + +def test_counts(fast_e2e_plan_runner): + "Test stage." + # TODO: to re-enable per-module resource count check print _, then test + num_modules, num_resources, _ = fast_e2e_plan_runner() + assert num_modules > 0 and num_resources > 0 diff --git a/tests/fast/stages/s01_resman/__init__.py b/tests/fast/stages/s01_resman/__init__.py new file mode 100644 index 00000000..6d6d1266 --- /dev/null +++ b/tests/fast/stages/s01_resman/__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/s01_resman/fixture/main.tf b/tests/fast/stages/s01_resman/fixture/main.tf new file mode 100644 index 00000000..2509f4d5 --- /dev/null +++ b/tests/fast/stages/s01_resman/fixture/main.tf @@ -0,0 +1,42 @@ +/** + * 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. + */ + +module "stage" { + source = "../../../../../fast/stages/01-resman" + automation_project_id = "fast-prod-automation" + billing_account = { + id = "000000-111111-222222" + organization_id = 123456789012 + } + custom_roles = { + "organizationIamAdmin" : "organizations/123456789012/roles/organizationIamAdmin", + "xpnServiceAdmin" : "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" +} diff --git a/tests/fast/stages/s01_resman/test_plan.py b/tests/fast/stages/s01_resman/test_plan.py new file mode 100644 index 00000000..6189f62e --- /dev/null +++ b/tests/fast/stages/s01_resman/test_plan.py @@ -0,0 +1,20 @@ +# 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(fast_e2e_plan_runner): + "Test stage." + num_modules, num_resources, _ = fast_e2e_plan_runner() + # TODO: to re-enable per-module resource count check print _, then test + assert num_modules > 0 and num_resources > 0 diff --git a/tests/fast/stages/s02_networking/__init__.py b/tests/fast/stages/s02_networking/__init__.py new file mode 100644 index 00000000..6d6d1266 --- /dev/null +++ b/tests/fast/stages/s02_networking/__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/s02_networking/fixture/main.tf b/tests/fast/stages/s02_networking/fixture/main.tf new file mode 100644 index 00000000..fe1cfbf5 --- /dev/null +++ b/tests/fast/stages/s02_networking/fixture/main.tf @@ -0,0 +1,31 @@ +/** + * 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. + */ + +module "stage" { + source = "../../../../../fast/stages/02-networking" + billing_account_id = "000000-111111-222222" + organization = { + domain = "gcp-pso-italy.net" + id = 856933387836 + customer_id = "C01lmug8b" + } + prefix = "fast" + project_factory_sa = { + dev = "foo@iam" + prod = "bar@iam" + } + data_dir = "../../../../../fast/stages/02-networking/data/" +} diff --git a/tests/fast/stages/s02_networking/test_plan.py b/tests/fast/stages/s02_networking/test_plan.py new file mode 100644 index 00000000..6189f62e --- /dev/null +++ b/tests/fast/stages/s02_networking/test_plan.py @@ -0,0 +1,20 @@ +# 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(fast_e2e_plan_runner): + "Test stage." + num_modules, num_resources, _ = fast_e2e_plan_runner() + # TODO: to re-enable per-module resource count check print _, then test + assert num_modules > 0 and num_resources > 0 diff --git a/tests/fast/stages/s02_security/__init__.py b/tests/fast/stages/s02_security/__init__.py new file mode 100644 index 00000000..6d6d1266 --- /dev/null +++ b/tests/fast/stages/s02_security/__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/s02_security/fixture/main.tf b/tests/fast/stages/s02_security/fixture/main.tf new file mode 100644 index 00000000..20608b28 --- /dev/null +++ b/tests/fast/stages/s02_security/fixture/main.tf @@ -0,0 +1,109 @@ +/** + * 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. + */ + +module "stage" { + source = "../../../../../fast/stages/02-security" + billing_account_id = "000000-111111-222222" + folder_id = "folders/12345678" + organization = { + domain = "gcp-pso-italy.net" + id = 856933387836 + customer_id = "C01lmug8b" + } + prefix = "fast" + kms_restricted_admins = { + "dev" : [ + "serviceAccount:fast-dev-resman-pf-0@fast-prod-iac-core-0.iam.gserviceaccount.com" + ], + "prod" : [ + "serviceAccount:fast-prod-resman-pf-0@fast-prod-iac-core-0.iam.gserviceaccount.com" + ] + } + kms_keys = { + compute = { + iam = { + "roles/cloudkms.admin" = ["user:user1@example.com"] + } + labels = { service = "compute" } + locations = null + rotation_period = null + } + } + vpc_sc_ingress_policies = { + iac = { + ingress_from = { + identities = [ + "serviceAccount:fast-prod-resman-security-0@fast-prod-iac-core-0.iam.gserviceaccount.com" + ], + source_access_levels = ["*"], identity_type = null, source_resources = null + } + ingress_to = { + operations = [{ method_selectors = [], service_name = "*" }] + resources = ["*"] + } + } + } + vpc_sc_perimeter_ingress_policies = { + dev = ["iac"] + landing = null + prod = ["iac"] + } + vpc_sc_perimeter_projects = { + dev = [ + "projects/345678912", # ludo-dev-sec-core-0 + ] + landing = [] + prod = [ + "projects/234567891", # ludo-prod-sec-core-0 + ] + } + + vpc_sc_access_levels = { + all = { + combining_function = null + conditions = [{ + members = [ + "serviceAccount:quota-monitor@foobar.iam.gserviceaccount.com", + ], + ip_subnetworks = null, negate = null, regions = null, + required_access_levels = null + }] + } + } + + vpc_sc_perimeter_access_levels = { + dev = ["all"] + landing = null + prod = ["all"] + } + + vpc_sc_egress_policies = { + iac-gcs = { + egress_from = { + identity_type = null + identities = [ + "serviceAccount:fast-prod-resman-security-0@fast-prod-iac-core-0.iam.gserviceaccount.com" + ] + } + egress_to = { + operations = [{ + method_selectors = ["*"], service_name = "storage.googleapis.com" + }] + resources = ["projects/123456789"] + } + } + } +} diff --git a/tests/fast/stages/s02_security/test_plan.py b/tests/fast/stages/s02_security/test_plan.py new file mode 100644 index 00000000..6189f62e --- /dev/null +++ b/tests/fast/stages/s02_security/test_plan.py @@ -0,0 +1,20 @@ +# 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(fast_e2e_plan_runner): + "Test stage." + num_modules, num_resources, _ = fast_e2e_plan_runner() + # TODO: to re-enable per-module resource count check print _, then test + assert num_modules > 0 and num_resources > 0 diff --git a/tests/fast/stages/s03_project_factory/__init__.py b/tests/fast/stages/s03_project_factory/__init__.py new file mode 100644 index 00000000..6d6d1266 --- /dev/null +++ b/tests/fast/stages/s03_project_factory/__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/tools/tfutils.py b/tests/fast/stages/s03_project_factory/fixture/data/defaults.yaml old mode 100755 new mode 100644 similarity index 51% rename from tools/tfutils.py rename to tests/fast/stages/s03_project_factory/fixture/data/defaults.yaml index 053e0664..b050583f --- a/tools/tfutils.py +++ b/tests/fast/stages/s03_project_factory/fixture/data/defaults.yaml @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,32 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pathlib +billing_account_id: 012345-67890A-BCDEF0 -import click +# [opt] Setup for billing alerts +billing_alert: + amount: 1000 + thresholds: + current: [0.5, 0.8] + forecasted: [0.5, 0.8] + credit_treatment: INCLUDE_ALL_CREDITS +# [opt] Contacts for billing alerts and important notifications +essential_contacts: ["team-contacts@example.com"] -def main(**kw): - pass +# [opt] Labels set for all projects +labels: + environment: prod + department: accounting + application: example-app + foo: bar - -@click.group() -@click.option('--dry-run', is_flag=True, default=False) -def cli(**kwargs): - basedir = pathlib.Path(source or '.') - for f in basedir.glob('**/*.tf'): - if '.terraform' in f: - continue - print(f) - - -@cli.command() -@click.argument('source', nargs=-1) -@click.option('--from-static/--to-static', default=True) -def mod_source(source=None, from_static=True): - print('mod_source') - print(source, from_static) - - -if __name__ == '__main__': - cli() +# [opt] Additional notification channels for billing +notification_channels: [] diff --git a/tests/fast/stages/s03_project_factory/fixture/data/projects/project.yaml b/tests/fast/stages/s03_project_factory/fixture/data/projects/project.yaml new file mode 100644 index 00000000..d988d9d5 --- /dev/null +++ b/tests/fast/stages/s03_project_factory/fixture/data/projects/project.yaml @@ -0,0 +1,112 @@ +# 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. + +# [opt] Billing account id - overrides default if set +billing_account_id: 012345-67890A-BCDEF0 + +# [opt] Billing alerts config - overrides default if set +billing_alert: + amount: 10 + thresholds: + current: + - 0.5 + - 0.8 + forecasted: [] + credit_treatment: INCLUDE_ALL_CREDITS + +# [opt] DNS zones to be created as children of the environment_dns_zone defined in defaults +dns_zones: + - lorem + - ipsum + +# [opt] Contacts for billing alerts and important notifications +essential_contacts: + - team-a-contacts@example.com + +# Folder the project will be created as children of +folder_id: folders/012345678901 + +# [opt] Authoritative IAM bindings in group => [roles] format +group_iam: + test-team-foobar@fast-lab-0.gcp-pso-italy.net: + - roles/compute.admin + +# [opt] Authoritative IAM bindings in role => [principals] format +# Generally used to grant roles to service accounts external to the project +iam: + roles/compute.admin: + - serviceAccount:service-account + +# [opt] Service robots and keys they will be assigned as cryptoKeyEncrypterDecrypter +# in service => [keys] format +kms_service_agents: + compute: [key1, key2] + storage: [key1, key2] + +# [opt] Labels for the project - merged with the ones defined in defaults +labels: + environment: prod + +# [opt] Org policy overrides defined at project level +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + policy_list: + constraints/compute.trustedImageProjects: + inherit_from_parent: null + status: true + suggested_value: null + values: + - projects/fast-prod-iac-core-0 + +# [opt] Service account to create for the project and their roles on the project +# in name => [roles] format +service_accounts: + another-service-account: + - roles/compute.admin + my-service-account: + - roles/compute.admin + +# [opt] APIs to enable on the project. +services: + - storage.googleapis.com + - stackdriver.googleapis.com + - compute.googleapis.com + +# [opt] Roles to assign to the robots service accounts in robot => [roles] format +services_iam: + compute: + - roles/storage.objectViewer + + # [opt] VPC setup. + # If set enables the `compute.googleapis.com` service and configures + # service project attachment +vpc: + # [opt] If set, enables the container API + gke_setup: + # Grants "roles/container.hostServiceAgentUser" to the container robot if set + enable_host_service_agent: false + + # Grants "roles/compute.securityAdmin" to the container robot if set + enable_security_admin: true + + # Host project the project will be service project of + host_project: fast-prod-net-spoke-0 + + # [opt] Subnets in the host project where principals will be granted networkUser + # in region/subnet-name => [principals] + subnets_iam: + europe-west1/prod-default-ew1: + - user:foobar@example.com + - serviceAccount:service-account1 diff --git a/tests/fast/stages/s03_project_factory/fixture/main.tf b/tests/fast/stages/s03_project_factory/fixture/main.tf new file mode 100644 index 00000000..8f5f8c4d --- /dev/null +++ b/tests/fast/stages/s03_project_factory/fixture/main.tf @@ -0,0 +1,57 @@ +/** + * 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. + */ + +# tfdoc:file:description Project factory. + + +locals { + _defaults = yamldecode(file(var.defaults_file)) + _defaults_net = { + billing_account_id = var.billing_account_id + environment_dns_zone = var.environment_dns_zone + shared_vpc_self_link = var.shared_vpc_self_link + vpc_host_project = var.vpc_host_project + } + defaults = merge(local._defaults, local._defaults_net) + projects = { + for f in fileset("${var.data_dir}", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("${var.data_dir}/${f}")) + } +} + +module "projects" { + #TODO(sruffilli): Pin to release + source = "../../../../../examples/factories/project-factory" + for_each = local.projects + defaults = local.defaults + project_id = each.key + billing_account_id = try(each.value.billing_account_id, null) + billing_alert = try(each.value.billing_alert, null) + dns_zones = try(each.value.dns_zones, []) + essential_contacts = try(each.value.essential_contacts, []) + folder_id = each.value.folder_id + group_iam = try(each.value.group_iam, {}) + iam = try(each.value.iam, {}) + kms_service_agents = try(each.value.kms, {}) + labels = try(each.value.labels, {}) + org_policies = try(each.value.org_policies, null) + service_accounts = try(each.value.service_accounts, {}) + services = try(each.value.services, []) + services_iam = try(each.value.services_iam, {}) + vpc = try(each.value.vpc, null) +} + + diff --git a/tests/fast/stages/s03_project_factory/fixture/terraform-bootstrap.auto.tfvars.json b/tests/fast/stages/s03_project_factory/fixture/terraform-bootstrap.auto.tfvars.json new file mode 100644 index 00000000..d446d643 --- /dev/null +++ b/tests/fast/stages/s03_project_factory/fixture/terraform-bootstrap.auto.tfvars.json @@ -0,0 +1,4 @@ +{ + "billing_account_id": "012345-67890A-BCDEF0", + "prefix": "fast" +} \ No newline at end of file diff --git a/tests/fast/stages/s03_project_factory/fixture/terraform-networking.auto.tfvars.json b/tests/fast/stages/s03_project_factory/fixture/terraform-networking.auto.tfvars.json new file mode 100644 index 00000000..56cfa3de --- /dev/null +++ b/tests/fast/stages/s03_project_factory/fixture/terraform-networking.auto.tfvars.json @@ -0,0 +1,5 @@ +{ + "environment_dns_zone": "prod.gcp.example.com.", + "shared_vpc_self_link": "https://www.googleapis.com/compute/v1/projects/fast-example/global/networks/prod-spoke-0", + "vpc_host_project": "fast-example" +} \ No newline at end of file diff --git a/tests/fast/stages/s03_project_factory/fixture/variables.tf b/tests/fast/stages/s03_project_factory/fixture/variables.tf new file mode 100644 index 00000000..b52ebd6c --- /dev/null +++ b/tests/fast/stages/s03_project_factory/fixture/variables.tf @@ -0,0 +1,61 @@ +/** + * 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. + */ + +#TODO: tfdoc annotations + +variable "billing_account_id" { + # tfdoc:variable:source 00-bootstrap + description = "Billing account id." + type = string +} + +variable "data_dir" { + description = "Relative path for the folder storing configuration data." + type = string + default = "data/projects" +} + +variable "environment_dns_zone" { + # tfdoc:variable:source 02-networking + description = "DNS zone suffix for environment." + type = string + default = null +} + +variable "defaults_file" { + description = "Relative path for the file storing the project factory configuration." + type = string + default = "data/defaults.yaml" +} + +#TODO(sruffilli): is this really required? +variable "environment" { + description = "Environment where projects will be created (e.g. prod, dev, ...)." + type = string + default = "prod" +} + +variable "shared_vpc_self_link" { + # tfdoc:variable:source 02-networking + description = "Self link for the shared VPC." + type = string +} + +variable "vpc_host_project" { + # tfdoc:variable:source 02-networking + description = "Host project for the shared VPC." + type = string +} diff --git a/tests/fast/stages/s03_project_factory/test_plan.py b/tests/fast/stages/s03_project_factory/test_plan.py new file mode 100644 index 00000000..6189f62e --- /dev/null +++ b/tests/fast/stages/s03_project_factory/test_plan.py @@ -0,0 +1,20 @@ +# 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(fast_e2e_plan_runner): + "Test stage." + num_modules, num_resources, _ = fast_e2e_plan_runner() + # TODO: to re-enable per-module resource count check print _, then test + assert num_modules > 0 and num_resources > 0 diff --git a/tests/versions.tf b/tests/versions.tf new file mode 100644 index 00000000..5ac151cb --- /dev/null +++ b/tests/versions.tf @@ -0,0 +1,43 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_version = ">= 1.0.0" + required_providers { + archive = { + source = "registry.terraform.io/hashicorp/archive" + version = ">= 2.2.0" + } + google = { + source = "hashicorp/google" + version = ">= 4.6.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.6.0" + } + local = { + source = "registry.terraform.io/hashicorp/local" + version = ">= 2.1.0" + } + random = { + source = "registry.terraform.io/hashicorp/random" + version = ">= 3.1.0" + } + time = { + source = "registry.terraform.io/hashicorp/time" + version = ">= 0.7.2" + } + } +} diff --git a/tools/REQUIREMENTS.txt b/tools/REQUIREMENTS.txt index dca9a909..f53a35cc 100644 --- a/tools/REQUIREMENTS.txt +++ b/tools/REQUIREMENTS.txt @@ -1 +1,2 @@ click +yamale diff --git a/tools/check_boilerplate.py b/tools/check_boilerplate.py index 6bbd4cb2..2095e048 100755 --- a/tools/check_boilerplate.py +++ b/tools/check_boilerplate.py @@ -14,6 +14,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +'''Check that boilerplate is present in relevant files. + +This tools offers a simple way of ensuring that the required boilerplate header +is present in files with specific extensions. Files can be excluded by using a +special comment anywhere in the file. + +The interface is purposefully simple and only supports passing one or more +folder paths as arguments, as this tool is designed to be run in CI pipelines +triggered by pull requests. +''' + import glob import os import re @@ -33,22 +44,23 @@ _MATCH_STRING = ( _MATCH_RE = re.compile(_MATCH_STRING, re.M) -def main(dir): - "Cycle through files in dir and check for the Apache 2.0 boilerplate." +def main(base_dirs): + "Cycle through files in base_dirs and check for the Apache 2.0 boilerplate." errors, warnings = [], [] - for root, dirs, files in os.walk(dir): - dirs[:] = [d for d in dirs if d not in _EXCLUDE_DIRS] - for fname in files: - if fname in _MATCH_FILES or os.path.splitext(fname)[1] in _MATCH_FILES: - fpath = os.path.abspath(os.path.join(root, fname)) - content = open(fpath).read() - if _EXCLUDE_RE.search(content): - continue - try: - if not _MATCH_RE.search(content): - errors.append(fpath) - except (IOError, OSError): - warnings.append(fpath) + for dir in base_dirs: + for root, dirs, files in os.walk(dir): + dirs[:] = [d for d in dirs if d not in _EXCLUDE_DIRS] + for fname in files: + if fname in _MATCH_FILES or os.path.splitext(fname)[1] in _MATCH_FILES: + fpath = os.path.abspath(os.path.join(root, fname)) + content = open(fpath).read() + if _EXCLUDE_RE.search(content): + continue + try: + if not _MATCH_RE.search(content): + errors.append(fpath) + except (IOError, OSError): + warnings.append(fpath) if warnings: print('The following files cannot be accessed:') print('\n'.join(' - {}'.format(s) for s in warnings)) @@ -59,6 +71,6 @@ def main(dir): if __name__ == '__main__': - if len(sys.argv) != 2: - raise SystemExit('No directory passed.') - main(sys.argv[1]) + if len(sys.argv) < 2: + raise SystemExit('No directory to check.') + main(sys.argv[1:]) diff --git a/tools/check_documentation.py b/tools/check_documentation.py index 1968f3b4..f3ed7106 100755 --- a/tools/check_documentation.py +++ b/tools/check_documentation.py @@ -14,6 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +'''Recursively check freshness of tfdoc's generated tables in README files. + +This tool recursively checks that the embedded variables and outputs tables in +README files, match what is generated at runtime by tfdoc based on current +sources. As such, it accepts pretty much the same options as tfdoc does. Its +main use is in CI pipelines triggered by pull requests. +''' + +import difflib import enum import pathlib @@ -28,10 +37,12 @@ State = enum.Enum('State', 'OK FAIL SKIP') def _check_dir(dir_name, files=False, show_extra=False): + 'Invoke tfdoc on folder, using the relevant options.' dir_path = BASEDIR / dir_name for readme_path in dir_path.glob('**/README.md'): if '.terraform' in str(readme_path): continue + diff = None readme = readme_path.read_text() mod_name = str(readme_path.relative_to(dir_path).parent) result = tfdoc.get_doc(readme) @@ -44,24 +55,36 @@ def _check_dir(dir_name, files=False, show_extra=False): except SystemExit: state = state.SKIP else: - state = State.OK if new_doc == result['doc'] else State.FAIL - yield mod_name, state + if new_doc == result['doc']: + state = State.OK + else: + state = State.FAIL + diff = '\n'.join( + [f'----- {mod_name} diff -----\n'] + + list(difflib.ndiff( + result['doc'].split('\n'), new_doc.split('\n') + ))) + yield mod_name, state, diff @click.command() @click.argument('dirs', type=str, nargs=-1) -@ click.option('--show-extra/--no-show-extra', default=False) @ click.option('--files/--no-files', default=False) -def main(dirs, files=False, show_extra=False): +@ click.option('--show-diffs/--no-show-diffs', default=False) +@ click.option('--show-extra/--no-show-extra', default=False) +def main(dirs, files=False, show_diffs=False, show_extra=False): 'Cycle through modules and ensure READMEs are up-to-date.' - errors = 0 + errors = [] state_labels = {State.FAIL: '✗', State.OK: '✓', State.SKIP: '?'} for dir_name in dirs: print(f'----- {dir_name} -----') - for mod_name, state in _check_dir(dir_name, files, show_extra): - errors += 1 if state == State.FAIL else 0 + for mod_name, state, diff in _check_dir(dir_name, files, show_extra): + if state == State.FAIL: + errors.append(diff) print(f'[{state_labels[state]}] {mod_name}') if errors: + if show_diffs: + print('\n'.join(errors)) raise SystemExit('Errors found.') diff --git a/tools/validate_schema.py b/tools/validate_schema.py new file mode 100755 index 00000000..b1a9020f --- /dev/null +++ b/tools/validate_schema.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +'''Validate YaML document against yamale schemas. +Fast includes YaML driven resource factories, along with their schemas which +are available at `fast/assets/schemas`. + +An arbitrary number of files and directories can be validated against a given +schema via options (--file and --directory, optionally --recursive). +''' + +import glob +import os + +import click +import yamale + + +@ click.command() +@ click.argument('schema', type=click.Path(exists=True)) +@ click.option('--directory', multiple=True, type=click.Path(exists=True, file_okay=False, dir_okay=True)) +@ click.option('--file', multiple=True, type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@ click.option('--recursive', is_flag=True, default=False) +@ click.option('--quiet', is_flag=True, default=False) +def main(directory=None, file=None, schema=None, recursive=False, quiet=False): + 'Program entry point.' + + yamale_schema = yamale.make_schema(schema) + search = "**/*.yaml" if recursive else "*.yaml" + has_errors = [] + + files = list(file) + for d in directory: + files = files + glob.glob(os.path.join(d, search), recursive=recursive) + + for document in files: + yamale_data = yamale.make_data(document) + try: + yamale.validate(yamale_schema, yamale_data) + if quiet: + pass + else: + print(f'✅ {document} -> {os.path.basename(schema)}') + except ValueError as e: + has_errors.append(document) + print(e) + print(f'❌ {document} -> {os.path.basename(schema)}') + + if len(has_errors) > 0: + raise SystemExit(f"❌ Errors found in {len(has_errors)} documents.") + + +if __name__ == '__main__': + main()