From 68f57190360d98c6bd2c961c4c89e0a219fbd1d8 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Tue, 25 Oct 2022 07:23:38 +0200 Subject: [PATCH 01/27] gke module datapath for autopilot --- modules/gke-cluster/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/gke-cluster/main.tf b/modules/gke-cluster/main.tf index 9981d9b4..3c041541 100644 --- a/modules/gke-cluster/main.tf +++ b/modules/gke-cluster/main.tf @@ -41,7 +41,7 @@ resource "google_container_cluster" "cluster" { initial_node_count = 1 remove_default_node_pool = var.enable_features.autopilot ? null : true datapath_provider = ( - var.enable_features.dataplane_v2 + var.enable_features.dataplane_v2 || var.enable_features.autopilot ? "ADVANCED_DATAPATH" : "DATAPATH_PROVIDER_UNSPECIFIED" ) From 991cd1324d3ed4dd5997303867a5fa930a1cc2b5 Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Tue, 25 Oct 2022 13:04:27 +0200 Subject: [PATCH 02/27] feat: TFE OIDC with GCP WIF blueprint added. --- blueprints/cloud-operations/README.md | 5 + .../terraform-enterprise-wif/README.md | 115 ++++++++++++++++++ .../terraform-enterprise-wif/diagram.png | Bin 0 -> 29084 bytes .../gcp-workload-identity-provider/README.md | 33 +++++ .../gcp-workload-identity-provider/main.tf | 83 +++++++++++++ .../gcp-workload-identity-provider/outputs.tf | 34 ++++++ .../terraform.auto.tfvars.template | 20 +++ .../variables.tf | 69 +++++++++++ .../tfc-workflow-using-wif/README.md | 19 +++ .../backend.tf.template | 29 +++++ .../tfc-workflow-using-wif/main.tf | 25 ++++ .../tfc-workflow-using-wif/provider.tf | 25 ++++ .../terraform.auto.tfvars.template | 17 +++ .../tfc-workflow-using-wif/tfc-oidc/README.md | 40 ++++++ .../tfc-workflow-using-wif/tfc-oidc/main.tf | 23 ++++ .../tfc-oidc/outputs.tf | 26 ++++ .../tfc-oidc/variables.tf | 31 +++++ .../tfc-oidc/versions.tf | 17 +++ .../tfc-oidc/write_token.sh | 23 ++++ .../tfc-workflow-using-wif/variables.tf | 29 +++++ .../__init__.py | 13 ++ .../fixture/main.tf | 28 +++++ .../fixture/variables.tf | 68 +++++++++++ .../test_plan.py | 19 +++ 24 files changed, 791 insertions(+) create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/README.md create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/diagram.png create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/main.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf create mode 100644 tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/__init__.py create mode 100644 tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/main.tf create mode 100644 tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf create mode 100644 tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py diff --git a/blueprints/cloud-operations/README.md b/blueprints/cloud-operations/README.md index 36e4c41b..88d55d4e 100644 --- a/blueprints/cloud-operations/README.md +++ b/blueprints/cloud-operations/README.md @@ -62,3 +62,8 @@ This [blueprint](./onprem-sa-key-management) shows how to manage IAM Service Acc This [blueprint](./unmanaged-instances-healthcheck) shows how to leverage [Serverless VPC Access](https://cloud.google.com/vpc/docs/configure-serverless-vpc-access) and Cloud Functions to organize a highly performant TCP healtheck for unmanaged GCE instances.
+ +## Workload identity federation for Terraform Enterprise workflow + This [blueprint](./terraform-enterprise-wif) shows how to configure [Wokload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between [Terraform Cloud/Enterprise](https://developer.hashicorp.com/terraform/enterprise) instance and Google Cloud. + +
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/README.md new file mode 100644 index 00000000..4bb282c5 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/README.md @@ -0,0 +1,115 @@ +# Configuring workload identity federation for Terraform Cloud/Enterprise workflow + +The most common way to use Terraform Cloud for GCP deployments is to store a GCP Service Account Key as a part of TFE Workflow configuration, as we all know there are security risks due to the fact that keys are long term credentials that could be compromised. + +Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. + +This blueprint shows how to set up [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between [Terraform Cloud/Enterprise](https://developer.hashicorp.com/terraform/enterprise) instance and Google Cloud. This will be possible by configuring workload identity federation to trust oidc tokens generated for a specific workflow in a Terraform Enterprise organization. + +The following diagram illustrates how the VM will get a short-lived access token and use it to access a resource: + + ![Sequence diagram](diagram.png) + +## Running the blueprint + +### Create Terraform Enterprise Workflow +If you don't have an existing Terraform Enterprise organization you can sign up for a [free trial](https://app.terraform.io/public/signup/account) account. + +Create a new Workspace for a `CLI-driven workflow` (Identity Federation will work for any workflow type, but for simplicity of the blueprint we use CLI driven workflow). + +Note workspace name and id (id starts with `ws-`), we will use them on a later stage. + +Go to the organization settings and note the org name and id (id starts with `org-`). + +### Deploy GCP Workload Identity Pool Provider for Terraform Enterprise + +> **_NOTE:_** This is a preparation part and should be executed on behalf of a user with enough permissions. + +Required permissions when new project is created: + - Project Creator on the parent folder/org. + + Required permissions when an existing project is used: + - Workload Identity Admin on the project level + - Project IAM Admin on the project level + +Fill out required variables, use TFE Org and Workspace IDs from the previous steps (IDs are not the names). +```bash +cd gcp-workload-identity-provider + +mv terraform.auto.tfvars.template terraform.auto.tfvars + +vi terraform.auto.tfvars +``` + +Authenticate using application default credentials, execute terraform code and deploy resources +``` +gcloud auth application-default login + +terraform init + +terraform apply +``` + +As a result a set of outputs will be provided (your values will be different), note the output since we will use it on the next steps. + +``` +impersonate_service_account_email = "sa-tfe@fe-test-oidc.iam.gserviceaccount.com" +project_id = "tfe-test-oidc" +workload_identity_audience = "//iam.googleapis.com/projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +workload_identity_pool_provider_id = "projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +``` + +### Configure OIDC provider for your TFE Workflow + +To enable OIDC for a TFE workflow it's enough to setup an environment variable `TFC_WORKLOAD_IDENTITY_AUDIENCE`. + +Go the the Workflow -> Variables and add a new variable `TFC_WORKLOAD_IDENTITY_AUDIENCE` equal to the value of `workload_identity_audience` output, in our example it's: + +``` +TFC_WORKLOAD_IDENTITY_AUDIENCE = "//iam.googleapis.com/projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +``` + +At that point we setup GCP Identity Federation to trust TFE generated OIDC tokens, so the TFE workflow can use the token to impersonate a GCP Service Account. + +## Testing the blueprint + +In order to test the setup we will deploy a GCS bucket from TFE Workflow using OIDC token for Service Account Impersonation. + +### Configure backend and variables + +First, we need to configure TFE Remote backend for our testing terraform code, use TFE Organization name and workspace name (names are not the same as ids) + +``` +cd ../tfc-workflow-using-wif + +mv backend.tf.template backend.tf + + +vi backend.tf + +``` + +Fill out variables based on the output from the preparation steps: + +``` +mv terraform.auto.tfvars.template terraform.auto.tfvars + +vi terraform.auto.tfvars + +``` + +### Authenticate terraform for triggering CLI-driven workflow + +Follow this [documentation](https://learn.hashicorp.com/tutorials/terraform/cloud-login) to login ti terraform cloud from the CLI. + +### Trigger the workflow + +``` +terraform init + +terraform apply +``` + +As a result we have a successfully deployed GCS bucket from Terraform Enterprise workflow using Workload Identity Federation. + +Once done testing, you can clean up resources by running `terraform destroy` first in the `tfc-workflow-using-wif` and then `gcp-workload-identity-provider` folders. diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/diagram.png b/blueprints/cloud-operations/terraform-enterprise-wif/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e6f82e9d0bfbc41c85ed9e3e5b4aaa5b72ac25 GIT binary patch literal 29084 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

w%=&~_s@(isigS^wDyk9P9`wMxNtgc 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 + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [billing_account](variables.tf#L16) | Billing account id used as default for new projects. | string | ✓ | | +| [project_id](variables.tf#L38) | Existing project id. | string | ✓ | | +| [tfe_organization_id](variables.tf#L43) | | | ✓ | | +| [tfe_workspace_id](variables.tf#L48) | | | ✓ | | +| [issuer_uri](variables.tf#L65) | Terraform Enterprise uri. Replace the uri if a self hosted instance is used. | string | | "https://app.terraform.io/" | +| [parent](variables.tf#L27) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [project_create](variables.tf#L21) | Create project instead of using an existing one. | bool | | true | +| [workload_identity_pool_id](variables.tf#L53) | Workload identity pool id. | string | | "tfe-pool" | +| [workload_identity_pool_provider_id](variables.tf#L59) | Workload identity pool provider id. | string | | "tfe-provider" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [impersonate_service_account_email](outputs.tf#L31) | | | +| [project_id](outputs.tf#L16) | | | +| [workload_identity_audience](outputs.tf#L26) | | | +| [workload_identity_pool_provider_id](outputs.tf#L21) | GCP workload identity pool provider ID. | | + + diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf new file mode 100644 index 00000000..6ca48696 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf @@ -0,0 +1,83 @@ +# 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. + + +############################################################################### +# GCP PROJECT # +############################################################################### + +module "project" { + source = "../../../../modules/project" + name = var.project_id + project_create = var.project_create + parent = var.parent + billing_account = var.billing_account + services = [ + "iam.googleapis.com", + "cloudresourcemanager.googleapis.com", + "iamcredentials.googleapis.com", + "sts.googleapis.com", + "storage.googleapis.com" + ] +} + +############################################################################### +# Workload Identity Pool and Provider # +############################################################################### + +resource "google_iam_workload_identity_pool" "tfe-pool" { + project = module.project.project_id + workload_identity_pool_id = var.workload_identity_pool_id + display_name = "TFE Pool" + description = "Identity pool for Terraform Enterprise OIDC integration" +} + +resource "google_iam_workload_identity_pool_provider" "tfe-pool-provider" { + project = module.project.project_id + workload_identity_pool_id = google_iam_workload_identity_pool.tfe-pool.workload_identity_pool_id + workload_identity_pool_provider_id = var.workload_identity_pool_provider_id + display_name = "TFE Pool Provider" + description = "OIDC identity pool provider for TFE Integration" + # Use condition to make sure only token generated for a specific TFE Org and workspace can be used + attribute_condition = "attribute.terraform_workspace_id == \"${var.tfe_workspace_id}\" && attribute.terraform_organization_id == \"${var.tfe_organization_id}\"" + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.terraform_organization_id" = "assertion.terraform_organization_id" + "attribute.terraform_workspace_id" = "assertion.terraform_workspace_id" + } + oidc { + # Should be different if self hosted TFE instance is used + issuer_uri = var.issuer_uri + } +} + +############################################################################### +# Service Account and IAM bindings # +############################################################################### + +module "sa-tfe" { + source = "../../../../modules/iam-service-account" + project_id = module.project.project_id + name = "sa-tfe" + + iam = { + "roles/iam.workloadIdentityUser" = ["principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.tfe-pool.name}/*"] + } + + iam_project_roles = { + "${module.project.project_id}" = [ + "roles/storage.admin" + ] + } +} \ No newline at end of file diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf new file mode 100644 index 00000000..f28dc3a9 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf @@ -0,0 +1,34 @@ +# 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. + + +output "project_id" { + description = "GCP Project ID." + value = module.project.project_id +} + +output "workload_identity_pool_provider_id" { + description = "GCP workload identity pool provider ID." + value = google_iam_workload_identity_pool_provider.tfe-pool-provider.name +} + +output "workload_identity_audience" { + description = "TFC Workload Identity Audience." + value = "//iam.googleapis.com/${google_iam_workload_identity_pool_provider.tfe-pool-provider.name}" +} + +output "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity." + value = module.sa-tfe.email +} \ No newline at end of file diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template new file mode 100644 index 00000000..645eea0b --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template @@ -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 +# +# 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. + + +parent = "folders/437102807785" +project_id = "my-project-id" +tfe_organization_id = "org-W3bz9neazHrZz99U" +tfe_workspace_id = "ws-DFxEE3NmeMdaAvoK" +billing_account = "015617-1B8CBC-AF10D9" diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf new file mode 100644 index 00000000..6df8dbcb --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf @@ -0,0 +1,69 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +variable "billing_account" { + description = "Billing account id used as default for new projects." + type = string +} + +variable "project_create" { + description = "Create project instead of using an existing one." + type = bool + default = true +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + + +variable "project_id" { + description = "Existing project id." + type = string +} + +variable "tfe_organization_id" { + description = "TFE organization id." + type = string +} + +variable "tfe_workspace_id" { + description = "TFE workspace id." + type = string +} + +variable "workload_identity_pool_id" { + description = "Workload identity pool id." + type = string + default = "tfe-pool" +} + +variable "workload_identity_pool_provider_id" { + description = "Workload identity pool provider id." + type = string + default = "tfe-provider" +} + +variable "issuer_uri" { + description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." + type = string + default = "https://app.terraform.io/" +} \ No newline at end of file diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md new file mode 100644 index 00000000..5226dd64 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md @@ -0,0 +1,19 @@ +# GCP Workload Identity Provider for Terraform Enterprise + +This terraform code is a part of [GCP Workload Identity Federation for Terraform Enterprise](../) blueprint. For instructions please refer to the blueprint [readme](../README.md). + +The codebase provisions the following list of resources: + +- GCS Bucket + + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [impersonate_service_account_email](variables.tf#L26) | | | ✓ | | +| [project_id](variables.tf#L16) | | | ✓ | | +| [workload_identity_pool_provider_id](variables.tf#L21) | GCP workload identity pool provider ID. | string | ✓ | | + + diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template new file mode 100644 index 00000000..87d4737d --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template @@ -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 +# +# 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. + + +# The block below configures Terraform to use the 'remote' backend with Terraform Cloud. +# For more information, see https://www.terraform.io/docs/backends/types/remote.html + +terraform { + backend "remote" { + organization = "" + + workspaces { + name = "" + } + } + + required_version = ">= 0.14.0" +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/main.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/main.tf new file mode 100644 index 00000000..5e03ada5 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/main.tf @@ -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 +# +# 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. + + +############################################################################### +# TEST RESOURCE TO VALIDATE WIF # +############################################################################### + +resource "google_storage_bucket" "test-bucket" { + project = var.project_id + name = "${var.project_id}-tfe-oidc-test-bucket" + location = "US" + force_destroy = true +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf new file mode 100644 index 00000000..47f24620 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf @@ -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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +module "tfe_oidc" { + source = "./tfc-oidc" + + workload_identity_pool_provider_id = var.workload_identity_pool_provider_id + impersonate_service_account_email = var.impersonate_service_account_email +} + +provider "google" { + credentials = module.tfe_oidc.credentials +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template new file mode 100644 index 00000000..efea4cc9 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template @@ -0,0 +1,17 @@ +# 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. + +project_id = "tfe-oidc-workflow" +workload_identity_pool_provider_id = "projects/683987109094/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +impersonate_service_account_email = "sa-tfe@tfe-oidc-workflow2.iam.gserviceaccount.com" diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md new file mode 100644 index 00000000..bb8d7983 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md @@ -0,0 +1,40 @@ +# Terraform Enterprise OIDC Credential for GCP Workload Identity Federation + +This is a helper module to prepare GCP Credentials from Terraform Enterprise workload identity token. For more information see [Terraform Enterprise Workload Identity Federation](../) blueprint. + +## Example +```hcl +module "tfe_oidc" { + source = "./tfe_oidc" + + workload_identity_pool_provider_id = "projects/683987109094/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" + impersonate_service_account_email = "tfe-test@tfe-test-wif.iam.gserviceaccount.com" +} + +provider "google" { + credentials = module.tfe_oidc.credentials +} + +provider "google-beta" { + credentials = module.tfe_oidc.credentials +} + +# tftest skip +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [impersonate_service_account_email](variables.tf#L22) | Service account to be impersonated by workload identity federation. | string | ✓ | | +| [workload_identity_pool_provider_id](variables.tf#L17) | GCP workload identity pool provider ID. | string | ✓ | | +| [tmp_oidc_token_path](variables.tf#L27) | Name of the temporary file where TFC OIDC token will be stored to authentificate terraform provider google. | string | | ".oidc_token" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [credentials](outputs.tf#L17) | | | + + diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf new file mode 100644 index 00000000..2c510a6a --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf @@ -0,0 +1,23 @@ +/** + * 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 { + audience = "//iam.googleapis.com/${var.workload_identity_pool_provider_id}" +} + +data "external" "oidc_token_file" { + program = ["bash", "${path.module}/write_token.sh", "${var.tmp_oidc_token_path}"] +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf new file mode 100644 index 00000000..fbcea8c2 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf @@ -0,0 +1,26 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "credentials" { + value = jsonencode({ + "type" : "external_account", + "audience" : "${local.audience}", + "subject_token_type" : "urn:ietf:params:oauth:token-type:jwt", + "token_url" : "https://sts.googleapis.com/v1/token", + "credential_source" : data.external.oidc_token_file.result + "service_account_impersonation_url" : "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${var.impersonate_service_account_email}:generateAccessToken" + }) +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.tf new file mode 100644 index 00000000..06f310da --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.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. + */ + +variable "workload_identity_pool_provider_id" { + description = "GCP workload identity pool provider ID." + type = string +} + +variable "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity federation." + type = string +} + +variable "tmp_oidc_token_path" { + description = "Name of the temporary file where TFC OIDC token will be stored to authentificate terraform provider google." + type = string + default = ".oidc_token" +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf new file mode 100644 index 00000000..a079e99c --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf @@ -0,0 +1,17 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_version = ">= 1.3.1" +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh new file mode 100644 index 00000000..2f7e30a2 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# 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. + +# Exit if any of the intermediate steps fail +set -e + +FILENAME=$@ + +echo $TFC_WORKLOAD_IDENTITY_TOKEN > $FILENAME + +echo -n "{\"file\":\"${FILENAME}\"}" diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf new file mode 100644 index 00000000..55a8c691 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +variable "project_id" { + description = "GCP project ID." + type = string +} + +variable "workload_identity_pool_provider_id" { + description = "GCP workload identity pool provider ID." + type = string +} + +variable "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity." + type = string +} \ No newline at end of file diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/__init__.py b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/__init__.py new file mode 100644 index 00000000..6d6d1266 --- /dev/null +++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/__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/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/main.tf b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/main.tf new file mode 100644 index 00000000..3552740c --- /dev/null +++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/main.tf @@ -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. + */ + +module "test" { + source = "../../../../../../blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider" + billing_account = var.billing_account + project_create = var.project_create + project_id = var.project_id + parent = var.parent + tfe_organization_id = var.tfe_organization_id + tfe_workspace_id = var.tfe_workspace_id + workload_identity_pool_id = var.workload_identity_pool_id + workload_identity_pool_provider_id = var.workload_identity_pool_provider_id + issuer_uri = var.issuer_uri +} diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf new file mode 100644 index 00000000..134e4aff --- /dev/null +++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf @@ -0,0 +1,68 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "billing_account" { + type = string + default = "1234-ABCD-1234" +} + +variable "project_create" { + type = bool + default = true +} + +variable "project_id" { + type = string + default = "project-1" +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "tfe_organization_id" { + description = "TFE organization id." + type = string + default = "org-123" +} + +variable "tfe_workspace_id" { + description = "TFE workspace id." + type = string + default = "ws-123" +} + +variable "workload_identity_pool_id" { + description = "Workload identity pool id." + type = string + default = "tfe-pool" +} + +variable "workload_identity_pool_provider_id" { + description = "Workload identity pool provider id." + type = string + default = "tfe-provider" +} + +variable "issuer_uri" { + description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." + type = string + default = "https://app.terraform.io/" +} \ No newline at end of file diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py new file mode 100644 index 00000000..e6a8bde4 --- /dev/null +++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py @@ -0,0 +1,19 @@ +# 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_resources(e2e_plan_runner): + "Test that plan works and the numbers of resources is as expected." + modules, resources = e2e_plan_runner() + assert len(modules) == 2 + assert len(resources) == 13 From cadaba8cacebf49e71970938ba5c2d4ff2607703 Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Tue, 25 Oct 2022 13:07:05 +0200 Subject: [PATCH 03/27] Add missing newlines --- .../gcp-workload-identity-provider/main.tf | 2 +- .../gcp-workload-identity-provider/outputs.tf | 2 +- .../gcp-workload-identity-provider/variables.tf | 2 +- .../tfc-workflow-using-wif/variables.tf | 2 +- .../gcp-workload-identity-provider/fixture/variables.tf | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf index 6ca48696..543e9d72 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf @@ -80,4 +80,4 @@ module "sa-tfe" { "roles/storage.admin" ] } -} \ No newline at end of file +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf index f28dc3a9..79cea39a 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf @@ -31,4 +31,4 @@ output "workload_identity_audience" { output "impersonate_service_account_email" { description = "Service account to be impersonated by workload identity." value = module.sa-tfe.email -} \ No newline at end of file +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf index 6df8dbcb..62163d17 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf @@ -66,4 +66,4 @@ variable "issuer_uri" { description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." type = string default = "https://app.terraform.io/" -} \ No newline at end of file +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf index 55a8c691..3f36c2ca 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf @@ -26,4 +26,4 @@ variable "workload_identity_pool_provider_id" { variable "impersonate_service_account_email" { description = "Service account to be impersonated by workload identity." type = string -} \ No newline at end of file +} diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf index 134e4aff..d99981c0 100644 --- a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf +++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf @@ -65,4 +65,4 @@ variable "issuer_uri" { description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." type = string default = "https://app.terraform.io/" -} \ No newline at end of file +} From a837e4361ab50959294a25aab4bca31e33f6e982 Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Tue, 25 Oct 2022 13:20:56 +0200 Subject: [PATCH 04/27] Fix tests --- .../gcp-workload-identity-provider/test_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py index e6a8bde4..228e51df 100644 --- a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py +++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py @@ -16,4 +16,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner() assert len(modules) == 2 - assert len(resources) == 13 + assert len(resources) == 10 From 7b2a82a7d6d59fa1494e60de08ff305833e82baa Mon Sep 17 00:00:00 2001 From: Simone Ruffilli Date: Tue, 25 Oct 2022 14:28:28 +0200 Subject: [PATCH 05/27] PGA DNS records (#911) Additional PGA DNS records --- fast/stages/02-networking-nva/README.md | 7 ++ fast/stages/02-networking-nva/dns-landing.tf | 74 ++++++++++++++++++- fast/stages/02-networking-peering/README.md | 7 ++ .../02-networking-peering/dns-landing.tf | 62 +++++++++++++++- .../02-networking-separate-envs/README.md | 7 ++ .../02-networking-separate-envs/dns-dev.tf | 62 ++++++++++++++++ .../02-networking-separate-envs/dns-prod.tf | 61 +++++++++++++++ fast/stages/02-networking-vpn/README.md | 7 ++ fast/stages/02-networking-vpn/dns-landing.tf | 62 +++++++++++++++- 9 files changed, 346 insertions(+), 3 deletions(-) diff --git a/fast/stages/02-networking-nva/README.md b/fast/stages/02-networking-nva/README.md index 84c236cf..cddfddaa 100644 --- a/fast/stages/02-networking-nva/README.md +++ b/fast/stages/02-networking-nva/README.md @@ -172,6 +172,13 @@ DNS configuration is further centralized by leveraging peering zones, so that - the hub/landing Cloud DNS hosts configurations for on-prem forwarding, Google API domains, and the top-level private zone/s (e.g. gcp.example.com) - the spokes Cloud DNS host configurations for the environment-specific domains (e.g. prod.gcp.example.com), which are bound to the hub/landing leveraging [cross-project binding](https://cloud.google.com/dns/docs/zones/zones-overview#cross-project_binding); a peering zone for the `.` (root) zone is then created on each spoke, delegating all DNS resolution to hub/landing. +- Private Google Access is enabled for a selection of the [supported domains](https://cloud.google.com/vpc/docs/configure-private-google-access#domain-options), namely + - `private.googleapis.com` + - `restricted.googleapis.com` + - `gcr.io` + - `packages.cloud.google.com` + - `pkg.dev` + - `pki.goog` To complete the configuration, the 35.199.192.0/19 range should be routed to the VPN tunnels from on-premises, and the following names should be configured for DNS forwarding to cloud: diff --git a/fast/stages/02-networking-nva/dns-landing.tf b/fast/stages/02-networking-nva/dns-landing.tf index e7834405..40090279 100644 --- a/fast/stages/02-networking-nva/dns-landing.tf +++ b/fast/stages/02-networking-nva/dns-landing.tf @@ -59,7 +59,7 @@ module "gcp-example-dns-private-zone" { } } -# Google API zone to trigger Private Access +# Google APIs module "googleapis-private-zone" { source = "../../../modules/dns" @@ -81,3 +81,75 @@ module "googleapis-private-zone" { "CNAME *" = { records = ["private.googleapis.com."] } } } + +module "gcrio-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "gcr-io" + domain = "gcr.io." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + recordsets = { + "A gcr.io." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "packages-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "packages-cloud" + domain = "packages.cloud.google.com." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + recordsets = { + "A packages.cloud.google.com." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "pkgdev-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "pkg-dev" + domain = "pkg.dev." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + recordsets = { + "A pkg.dev." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "pkigoog-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "pki-goog" + domain = "pki.goog." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + recordsets = { + "A pki.goog." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} diff --git a/fast/stages/02-networking-peering/README.md b/fast/stages/02-networking-peering/README.md index 0e5c72a7..1dfdb9a5 100644 --- a/fast/stages/02-networking-peering/README.md +++ b/fast/stages/02-networking-peering/README.md @@ -102,6 +102,13 @@ DNS configuration is further centralized by leveraging peering zones, so that - the hub/landing Cloud DNS hosts configurations for on-prem forwarding, Google API domains, and the top-level private zone/s (e.g. gcp.example.com) - the spokes Cloud DNS host configurations for the environment-specific domains (e.g. prod.gcp.example.com), which are bound to the hub/landing leveraging [cross-project binding](https://cloud.google.com/dns/docs/zones/zones-overview#cross-project_binding); a peering zone for the `.` (root) zone is then created on each spoke, delegating all DNS resolution to hub/landing. +- Private Google Access is enabled for a selection of the [supported domains](https://cloud.google.com/vpc/docs/configure-private-google-access#domain-options), namely + - `private.googleapis.com` + - `restricted.googleapis.com` + - `gcr.io` + - `packages.cloud.google.com` + - `pkg.dev` + - `pki.goog` 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: diff --git a/fast/stages/02-networking-peering/dns-landing.tf b/fast/stages/02-networking-peering/dns-landing.tf index e9a5da33..7b97a8cf 100644 --- a/fast/stages/02-networking-peering/dns-landing.tf +++ b/fast/stages/02-networking-peering/dns-landing.tf @@ -50,7 +50,7 @@ module "gcp-example-dns-private-zone" { } } -# Google API zone to trigger Private Access +# Google APIs module "googleapis-private-zone" { source = "../../../modules/dns" @@ -69,3 +69,63 @@ module "googleapis-private-zone" { "CNAME *" = { records = ["private.googleapis.com."] } } } + +module "gcrio-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "gcr-io" + domain = "gcr.io." + client_networks = [module.landing-vpc.self_link] + recordsets = { + "A gcr.io." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "packages-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "packages-cloud" + domain = "packages.cloud.google.com." + client_networks = [module.landing-vpc.self_link] + recordsets = { + "A packages.cloud.google.com." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "pkgdev-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "pkg-dev" + domain = "pkg.dev." + client_networks = [module.landing-vpc.self_link] + recordsets = { + "A pkg.dev." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "pkigoog-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "pki-goog" + domain = "pki.goog." + client_networks = [module.landing-vpc.self_link] + recordsets = { + "A pki.goog." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} diff --git a/fast/stages/02-networking-separate-envs/README.md b/fast/stages/02-networking-separate-envs/README.md index 2329aad4..6fdb00cf 100644 --- a/fast/stages/02-networking-separate-envs/README.md +++ b/fast/stages/02-networking-separate-envs/README.md @@ -69,6 +69,13 @@ DNS often goes hand in hand with networking, especially on GCP where Cloud DNS z - 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 +- Private Google Access is enabled for a selection of the [supported domains](https://cloud.google.com/vpc/docs/configure-private-google-access#domain-options), namely + - `private.googleapis.com` + - `restricted.googleapis.com` + - `gcr.io` + - `packages.cloud.google.com` + - `pkg.dev` + - `pki.goog` 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: diff --git a/fast/stages/02-networking-separate-envs/dns-dev.tf b/fast/stages/02-networking-separate-envs/dns-dev.tf index 5811c255..25adab5e 100644 --- a/fast/stages/02-networking-separate-envs/dns-dev.tf +++ b/fast/stages/02-networking-separate-envs/dns-dev.tf @@ -50,6 +50,8 @@ module "dev-reverse-10-dns-forwarding" { forwarders = { for ip in var.dns.dev : ip => null } } +# Google APIs + module "dev-googleapis-private-zone" { source = "../../../modules/dns" project_id = module.dev-spoke-project.project_id @@ -67,3 +69,63 @@ module "dev-googleapis-private-zone" { "CNAME *" = { records = ["private.googleapis.com."] } } } + +module "dev-gcrio-private-zone" { + source = "../../../modules/dns" + project_id = module.dev-spoke-project.project_id + type = "private" + name = "gcr-io" + domain = "gcr.io." + client_networks = [module.dev-spoke-vpc.self_link] + recordsets = { + "A gcr.io." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "dev-packages-private-zone" { + source = "../../../modules/dns" + project_id = module.dev-spoke-project.project_id + type = "private" + name = "packages-cloud" + domain = "packages.cloud.google.com." + client_networks = [module.dev-spoke-vpc.self_link] + recordsets = { + "A packages.cloud.google.com." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "dev-pkgdev-private-zone" { + source = "../../../modules/dns" + project_id = module.dev-spoke-project.project_id + type = "private" + name = "pkg-dev" + domain = "pkg.dev." + client_networks = [module.dev-spoke-vpc.self_link] + recordsets = { + "A pkg.dev." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "dev-pkigoog-private-zone" { + source = "../../../modules/dns" + project_id = module.dev-spoke-project.project_id + type = "private" + name = "pki-goog" + domain = "pki.goog." + client_networks = [module.dev-spoke-vpc.self_link] + recordsets = { + "A pki.goog." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} diff --git a/fast/stages/02-networking-separate-envs/dns-prod.tf b/fast/stages/02-networking-separate-envs/dns-prod.tf index db38064e..47c8cdca 100644 --- a/fast/stages/02-networking-separate-envs/dns-prod.tf +++ b/fast/stages/02-networking-separate-envs/dns-prod.tf @@ -50,6 +50,7 @@ module "prod-reverse-10-dns-forwarding" { forwarders = { for ip in var.dns.prod : ip => null } } +# Google APIs module "prod-googleapis-private-zone" { source = "../../../modules/dns" @@ -68,3 +69,63 @@ module "prod-googleapis-private-zone" { "CNAME *" = { records = ["private.googleapis.com."] } } } + +module "prod-gcrio-private-zone" { + source = "../../../modules/dns" + project_id = module.prod-spoke-project.project_id + type = "private" + name = "gcr-io" + domain = "gcr.io." + client_networks = [module.prod-spoke-vpc.self_link] + recordsets = { + "A gcr.io." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "prod-packages-private-zone" { + source = "../../../modules/dns" + project_id = module.prod-spoke-project.project_id + type = "private" + name = "packages-cloud" + domain = "packages.cloud.google.com." + client_networks = [module.prod-spoke-vpc.self_link] + recordsets = { + "A packages.cloud.google.com." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "prod-pkgdev-private-zone" { + source = "../../../modules/dns" + project_id = module.prod-spoke-project.project_id + type = "private" + name = "pkg-dev" + domain = "pkg.dev." + client_networks = [module.prod-spoke-vpc.self_link] + recordsets = { + "A pkg.dev." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "prod-pkigoog-private-zone" { + source = "../../../modules/dns" + project_id = module.prod-spoke-project.project_id + type = "private" + name = "pki-goog" + domain = "pki.goog." + client_networks = [module.prod-spoke-vpc.self_link] + recordsets = { + "A pki.goog." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} diff --git a/fast/stages/02-networking-vpn/README.md b/fast/stages/02-networking-vpn/README.md index 010b2246..783b11fb 100644 --- a/fast/stages/02-networking-vpn/README.md +++ b/fast/stages/02-networking-vpn/README.md @@ -108,6 +108,13 @@ DNS configuration is further centralized by leveraging peering zones, so that - the hub/landing Cloud DNS hosts configurations for on-prem forwarding, Google API domains, and the top-level private zone/s (e.g. gcp.example.com) - the spokes Cloud DNS host configurations for the environment-specific domains (e.g. prod.gcp.example.com), which are bound to the hub/landing leveraging [cross-project binding](https://cloud.google.com/dns/docs/zones/zones-overview#cross-project_binding); a peering zone for the `.` (root) zone is then created on each spoke, delegating all DNS resolution to hub/landing. +- Private Google Access is enabled for a selection of the [supported domains](https://cloud.google.com/vpc/docs/configure-private-google-access#domain-options), namely + - `private.googleapis.com` + - `restricted.googleapis.com` + - `gcr.io` + - `packages.cloud.google.com` + - `pkg.dev` + - `pki.goog` 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: diff --git a/fast/stages/02-networking-vpn/dns-landing.tf b/fast/stages/02-networking-vpn/dns-landing.tf index e9a5da33..7b97a8cf 100644 --- a/fast/stages/02-networking-vpn/dns-landing.tf +++ b/fast/stages/02-networking-vpn/dns-landing.tf @@ -50,7 +50,7 @@ module "gcp-example-dns-private-zone" { } } -# Google API zone to trigger Private Access +# Google APIs module "googleapis-private-zone" { source = "../../../modules/dns" @@ -69,3 +69,63 @@ module "googleapis-private-zone" { "CNAME *" = { records = ["private.googleapis.com."] } } } + +module "gcrio-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "gcr-io" + domain = "gcr.io." + client_networks = [module.landing-vpc.self_link] + recordsets = { + "A gcr.io." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "packages-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "packages-cloud" + domain = "packages.cloud.google.com." + client_networks = [module.landing-vpc.self_link] + recordsets = { + "A packages.cloud.google.com." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "pkgdev-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "pkg-dev" + domain = "pkg.dev." + client_networks = [module.landing-vpc.self_link] + recordsets = { + "A pkg.dev." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "pkigoog-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "pki-goog" + domain = "pki.goog." + client_networks = [module.landing-vpc.self_link] + recordsets = { + "A pki.goog." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} From c4d599f3210e97dd456fc686c8bab8a4eb80686b Mon Sep 17 00:00:00 2001 From: Simone Ruffilli Date: Tue, 25 Oct 2022 15:04:38 +0200 Subject: [PATCH 06/27] Fix race condition (#918) The cloud-init runcmd had a race condition where the script could run before the network interfaces were ready. Changed the script to a systemd unit and added a dependency on network ready. --- .../simple-nva/cloud-config.yaml | 38 ++++++++++++++----- .../simple-nva/files/policy_based_routing.sh | 23 ++++++----- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/modules/cloud-config-container/simple-nva/cloud-config.yaml b/modules/cloud-config-container/simple-nva/cloud-config.yaml index 8d18a356..f1d71e82 100644 --- a/modules/cloud-config-container/simple-nva/cloud-config.yaml +++ b/modules/cloud-config-container/simple-nva/cloud-config.yaml @@ -22,17 +22,37 @@ write_files: content: | ${indent(6, data.content)} %{ endfor } + - path: /etc/systemd/system/routing.service + permissions: 0644 + owner: root + content: | + [Install] + WantedBy=multi-user.target + [Unit] + Description=Start routing + After=network-online.target + Wants=network-online.target + [Service] + ExecStart=/bin/sh -c "/var/run/nva/start-routing.sh" + - path: /var/run/nva/start-routing.sh + permissions: 0744 + owner: root + content: | + iptables --policy FORWARD ACCEPT +%{ for interface in network_interfaces ~} +%{ if enable_health_checks ~} + /var/run/nva/policy_based_routing.sh ${interface.name} +%{ endif ~} +%{ for route in interface.routes ~} + ip route add ${route} via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/${interface.number}/gateway -H "Metadata-Flavor:Google"` dev ${interface.name} +%{ endfor ~} +%{ endfor ~} bootcmd: - systemctl start node-problem-detector runcmd: - - iptables --policy FORWARD ACCEPT -%{ for interface in network_interfaces ~} -%{ if enable_health_checks ~} - - /var/run/nva/policy_based_routing.sh ${interface.name} -%{ endif ~} -%{ for route in interface.routes ~} - - ip route add ${route} via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/${interface.number}/gateway -H "Metadata-Flavor:Google"` dev ${interface.name} -%{ endfor ~} -%{ endfor ~} + - systemctl daemon-reload + - systemctl enable routing + - systemctl start routing + diff --git a/modules/cloud-config-container/simple-nva/files/policy_based_routing.sh b/modules/cloud-config-container/simple-nva/files/policy_based_routing.sh index 42ed0dcb..2e1eb152 100644 --- a/modules/cloud-config-container/simple-nva/files/policy_based_routing.sh +++ b/modules/cloud-config-container/simple-nva/files/policy_based_routing.sh @@ -15,13 +15,18 @@ # limitations under the License. IF_NAME=$1 -IF_NUMBER=$(echo $1 | sed -e s/eth//) -IF_GW=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/gateway -H "Metadata-Flavor: Google") -IF_IP=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/ip -H "Metadata-Flavor: Google") -IF_NETMASK=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/subnetmask -H "Metadata-Flavor: Google") -IF_IP_PREFIX=$(/var/run/nva/ipprefix_by_netmask.sh $IF_NETMASK) IP_LB=$(ip r show table local | grep "$IF_NAME proto 66" | cut -f 2 -d " ") -grep -qxF "$((200 + $IF_NUMBER)) hc-$IF_NAME" /etc/iproute2/rt_tables || echo "$((200 + $IF_NUMBER)) hc-$IF_NAME" >>/etc/iproute2/rt_tables -ip route add $IF_GW src $IF_IP dev $IF_NAME table hc-$IF_NAME -ip route add default via $IF_GW dev $IF_NAME table hc-$IF_NAME -ip rule add from $IP_LB/32 table hc-$IF_NAME + +# If there's a load balancer for this IF... +if [ ! -z $IP_LB ] +then + IF_NUMBER=$(echo $IF_NAME | sed -e s/eth//) + IF_GW=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/gateway -H "Metadata-Flavor: Google") + IF_IP=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/ip -H "Metadata-Flavor: Google") + IF_NETMASK=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/subnetmask -H "Metadata-Flavor: Google") + IF_IP_PREFIX=$(/var/run/nva/ipprefix_by_netmask.sh $IF_NETMASK) + grep -qxF "$((200 + $IF_NUMBER)) hc-$IF_NAME" /etc/iproute2/rt_tables || echo "$((200 + $IF_NUMBER)) hc-$IF_NAME" >>/etc/iproute2/rt_tables + ip route add $IF_GW src $IF_IP dev $IF_NAME table hc-$IF_NAME + ip route add default via $IF_GW dev $IF_NAME table hc-$IF_NAME + ip rule add from $IP_LB/32 table hc-$IF_NAME +fi From 8bacd8f5d5b7babae87d65bb5892d02a5383bb43 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 25 Oct 2022 13:38:46 +0200 Subject: [PATCH 07/27] Add support for DNS routing policies --- modules/dns/README.md | 56 ++++++++++++++++++----- modules/dns/main.tf | 98 +++++++++++++++++++++++++++++++++++++++- modules/dns/variables.tf | 26 +++++++++-- 3 files changed, 165 insertions(+), 15 deletions(-) diff --git a/modules/dns/README.md b/modules/dns/README.md index ebd200ab..c9232371 100644 --- a/modules/dns/README.md +++ b/modules/dns/README.md @@ -53,25 +53,59 @@ module "private-dns" { } # tftest modules=1 resources=1 ``` + +### Routing Policies + +```hcl +module "private-dns" { + source = "./fabric/modules/dns" + project_id = "myproject" + type = "private" + name = "test-example" + domain = "test.example." + client_networks = [var.vpc.self_link] + recordsets = { + "A regular" = { records = ["10.20.0.1"] } + "A geo" = { + geo_routing = [ + { location = "europe-west1", records = ["10.0.0.1"] }, + { location = "europe-west2", records = ["10.0.0.2"] }, + { location = "europe-west3", records = ["10.0.0.3"] } + ] + } + + "A wrr" = { + ttl = 600 + wrr_routing = [ + { weight = 0.6, records = ["10.10.0.1"] }, + { weight = 0.2, records = ["10.10.0.2"] }, + { weight = 0.2, records = ["10.10.0.3"] } + ] + } + } +} +# tftest modules=1 resources=4 +``` + ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [domain](variables.tf#L51) | Zone domain, must end with a period. | string | ✓ | | -| [name](variables.tf#L69) | Zone name, must be unique within the project. | string | ✓ | | -| [project_id](variables.tf#L80) | Project id for the zone. | string | ✓ | | +| [domain](variables.tf#L53) | Zone domain, must end with a period. | string | ✓ | | +| [name](variables.tf#L71) | Zone name, must be unique within the project. | string | ✓ | | +| [project_id](variables.tf#L82) | Project id for the zone. | string | ✓ | | | [client_networks](variables.tf#L21) | List of VPC self links that can see this zone. | list(string) | | [] | | [description](variables.tf#L28) | Domain description. | string | | "Terraform managed." | -| [dnssec_config](variables.tf#L34) | DNSSEC configuration for this zone. | object({…}) | | null | -| [enable_logging](variables.tf#L62) | Enable query logging for this zone. Only valid for public zones. | bool | | false | -| [forwarders](variables.tf#L56) | Map of {IPV4_ADDRESS => FORWARDING_PATH} for 'forwarding' zone types. Path can be 'default', 'private', or null for provider default. | map(string) | | {} | -| [peer_network](variables.tf#L74) | Peering network self link, only valid for 'peering' zone types. | string | | null | -| [recordsets](variables.tf#L85) | Map of DNS recordsets in \"type name\" => {ttl, [records]} format. | map(object({…})) | | {} | -| [service_directory_namespace](variables.tf#L102) | Service directory namespace id (URL), only valid for 'service-directory' zone types. | string | | null | -| [type](variables.tf#L108) | Type of zone to create, valid values are 'public', 'private', 'forwarding', 'peering', 'service-directory'. | string | | "private" | -| [zone_create](variables.tf#L118) | Create zone. When set to false, uses a data source to reference existing zone. | bool | | true | +| [dnssec_config](variables.tf#L34) | DNSSEC configuration for this zone. | object({…}) | | {…} | +| [enable_logging](variables.tf#L64) | Enable query logging for this zone. Only valid for public zones. | bool | | false | +| [forwarders](variables.tf#L58) | Map of {IPV4_ADDRESS => FORWARDING_PATH} for 'forwarding' zone types. Path can be 'default', 'private', or null for provider default. | map(string) | | {} | +| [peer_network](variables.tf#L76) | Peering network self link, only valid for 'peering' zone types. | string | | null | +| [recordsets](variables.tf#L87) | Map of DNS recordsets in \"type name\" => {ttl, [records]} format. | map(object({…})) | | {} | +| [service_directory_namespace](variables.tf#L122) | Service directory namespace id (URL), only valid for 'service-directory' zone types. | string | | null | +| [type](variables.tf#L128) | Type of zone to create, valid values are 'public', 'private', 'forwarding', 'peering', 'service-directory'. | string | | "private" | +| [zone_create](variables.tf#L138) | Create zone. When set to false, uses a data source to reference existing zone. | bool | | true | ## Outputs diff --git a/modules/dns/main.tf b/modules/dns/main.tf index ed687d97..55b9301b 100644 --- a/modules/dns/main.tf +++ b/modules/dns/main.tf @@ -15,10 +15,25 @@ */ locals { - recordsets = { + _recordsets = { for key, attrs in var.recordsets : key => merge(attrs, zipmap(["type", "name"], split(" ", key))) } + geo_recordsets = { + for k, v in local._recordsets : + k => v + if v.geo_routing != null + } + recordsets = { + for k, v in local._recordsets : + k => v + if v.records != null + } + wrr_recordsets = { + for k, v in local._recordsets : + k => v + if v.wrr_routing != null + } zone = ( var.zone_create ? try( @@ -166,6 +181,87 @@ resource "google_dns_record_set" "cloud-static-records" { type = each.value.type ttl = each.value.ttl rrdatas = each.value.records + + depends_on = [ + google_dns_managed_zone.non-public, google_dns_managed_zone.public + ] +} + +resource "google_dns_record_set" "cloud-geo-records" { + for_each = ( + var.type == "public" || var.type == "private" + ? local.geo_recordsets + : {} + ) + project = var.project_id + managed_zone = var.name + name = ( + each.value.name == "" + ? var.domain + : ( + substr(each.value.name, -1, 1) == "." + ? each.value.name + : "${each.value.name}.${var.domain}" + ) + ) + type = each.value.type + ttl = each.value.ttl + + dynamic "routing_policy" { + for_each = each.value.geo_routing != null ? [1] : [0] + iterator = unused + content { + dynamic "geo" { + for_each = each.value.geo_routing + iterator = policy + content { + location = policy.value.location + rrdatas = policy.value.records + } + } + } + } + + depends_on = [ + google_dns_managed_zone.non-public, google_dns_managed_zone.public + ] +} + +resource "google_dns_record_set" "cloud-wrr-records" { + for_each = ( + var.type == "public" || var.type == "private" + ? local.wrr_recordsets + : {} + ) + project = var.project_id + managed_zone = var.name + name = ( + each.value.name == "" + ? var.domain + : ( + substr(each.value.name, -1, 1) == "." + ? each.value.name + : "${each.value.name}.${var.domain}" + ) + ) + type = each.value.type + ttl = each.value.ttl + + dynamic "routing_policy" { + for_each = each.value.wrr_routing != null ? [1] : [0] + iterator = unused + content { + dynamic "wrr" { + for_each = each.value.wrr_routing + iterator = policy + content { + weight = policy.value.weight + rrdatas = policy.value.records + } + } + } + } + depends_on = [ google_dns_managed_zone.non-public, google_dns_managed_zone.public ] diff --git a/modules/dns/variables.tf b/modules/dns/variables.tf index 749bbdd5..644b8395 100644 --- a/modules/dns/variables.tf +++ b/modules/dns/variables.tf @@ -45,7 +45,9 @@ variable "dnssec_config" { { algorithm = "rsasha256", key_length = 1024 } ) }) - default = null + default = { + state = "off" + } } variable "domain" { @@ -86,17 +88,35 @@ variable "recordsets" { description = "Map of DNS recordsets in \"type name\" => {ttl, [records]} format." type = map(object({ ttl = optional(number, 300) - records = list(string) + records = optional(list(string)) + geo_routing = optional(list(object({ + location = string + records = list(string) + }))) + wrr_routing = optional(list(object({ + weight = number + records = list(string) + }))) })) default = {} nullable = false validation { condition = alltrue([ - for k, v in var.recordsets == null ? {} : var.recordsets : + for k, v in coalesce(var.recordsets, {}) : length(split(" ", k)) == 2 ]) error_message = "Recordsets must have keys in the format \"type name\"." } + validation { + condition = alltrue([ + for k, v in coalesce(var.recordsets, {}) : ( + (v.records != null && v.wrr_routing == null && v.geo_routing == null) || + (v.records == null && v.wrr_routing != null && v.geo_routing == null) || + (v.records == null && v.wrr_routing == null && v.geo_routing != null) + ) + ]) + error_message = "Only one of records, wrr_routing or geo_routing can be defined for each recordset." + } } variable "service_directory_namespace" { From 6340286fa48849eff3e2b0ffcb5789c968154327 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 25 Oct 2022 15:05:29 +0200 Subject: [PATCH 08/27] DRY up resource name for recordsets --- modules/dns/main.tf | 63 ++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/modules/dns/main.tf b/modules/dns/main.tf index 55b9301b..25286a8a 100644 --- a/modules/dns/main.tf +++ b/modules/dns/main.tf @@ -15,10 +15,27 @@ */ locals { - _recordsets = { + # split record name and type and set as keys in a map + _recordsets_0 = { for key, attrs in var.recordsets : key => merge(attrs, zipmap(["type", "name"], split(" ", key))) } + # compute the final resource name for the recordset + _recordsets = { + for key, attrs in local._recordsets_0 : + key => merge(attrs, { + resource_name = ( + attrs.name == "" + ? var.domain + : ( + substr(attrs.name, -1, 1) == "." + ? attrs.name + : "${attrs.name}.${var.domain}." + ) + ) + }) + } + # split recordsets between regular, geo and wrr geo_recordsets = { for k, v in local._recordsets : k => v @@ -169,18 +186,10 @@ resource "google_dns_record_set" "cloud-static-records" { ) project = var.project_id managed_zone = var.name - name = ( - each.value.name == "" - ? var.domain - : ( - substr(each.value.name, -1, 1) == "." - ? each.value.name - : "${each.value.name}.${var.domain}" - ) - ) - type = each.value.type - ttl = each.value.ttl - rrdatas = each.value.records + name = each.value.resource_name + type = each.value.type + ttl = each.value.ttl + rrdatas = each.value.records depends_on = [ google_dns_managed_zone.non-public, google_dns_managed_zone.public @@ -195,17 +204,9 @@ resource "google_dns_record_set" "cloud-geo-records" { ) project = var.project_id managed_zone = var.name - name = ( - each.value.name == "" - ? var.domain - : ( - substr(each.value.name, -1, 1) == "." - ? each.value.name - : "${each.value.name}.${var.domain}" - ) - ) - type = each.value.type - ttl = each.value.ttl + name = each.value.resource_name + type = each.value.type + ttl = each.value.ttl dynamic "routing_policy" { for_each = each.value.geo_routing != null ? [1] : [0] @@ -235,17 +236,9 @@ resource "google_dns_record_set" "cloud-wrr-records" { ) project = var.project_id managed_zone = var.name - name = ( - each.value.name == "" - ? var.domain - : ( - substr(each.value.name, -1, 1) == "." - ? each.value.name - : "${each.value.name}.${var.domain}" - ) - ) - type = each.value.type - ttl = each.value.ttl + name = each.value.resource_name + type = each.value.type + ttl = each.value.ttl dynamic "routing_policy" { for_each = each.value.wrr_routing != null ? [1] : [0] From b5cee10dcaddc3f25d734e074171742cb4279b24 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 25 Oct 2022 15:07:07 +0200 Subject: [PATCH 09/27] Remove useless dynamic blocks --- modules/dns/main.tf | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/modules/dns/main.tf b/modules/dns/main.tf index 25286a8a..5c7113e9 100644 --- a/modules/dns/main.tf +++ b/modules/dns/main.tf @@ -30,7 +30,7 @@ locals { : ( substr(attrs.name, -1, 1) == "." ? attrs.name - : "${attrs.name}.${var.domain}." + : "${attrs.name}.${var.domain}" ) ) }) @@ -208,17 +208,13 @@ resource "google_dns_record_set" "cloud-geo-records" { type = each.value.type ttl = each.value.ttl - dynamic "routing_policy" { - for_each = each.value.geo_routing != null ? [1] : [0] - iterator = unused - content { - dynamic "geo" { - for_each = each.value.geo_routing - iterator = policy - content { - location = policy.value.location - rrdatas = policy.value.records - } + routing_policy { + dynamic "geo" { + for_each = each.value.geo_routing + iterator = policy + content { + location = policy.value.location + rrdatas = policy.value.records } } } @@ -240,17 +236,13 @@ resource "google_dns_record_set" "cloud-wrr-records" { type = each.value.type ttl = each.value.ttl - dynamic "routing_policy" { - for_each = each.value.wrr_routing != null ? [1] : [0] - iterator = unused - content { - dynamic "wrr" { - for_each = each.value.wrr_routing - iterator = policy - content { - weight = policy.value.weight - rrdatas = policy.value.records - } + routing_policy { + dynamic "wrr" { + for_each = each.value.wrr_routing + iterator = policy + content { + weight = policy.value.weight + rrdatas = policy.value.records } } } From 442f87e60e965c344b883254654ea8983259ee3c Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 25 Oct 2022 15:08:55 +0200 Subject: [PATCH 10/27] Rename local for consistency --- modules/dns/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/dns/main.tf b/modules/dns/main.tf index 5c7113e9..c1687761 100644 --- a/modules/dns/main.tf +++ b/modules/dns/main.tf @@ -41,7 +41,7 @@ locals { k => v if v.geo_routing != null } - recordsets = { + regular_recordsets = { for k, v in local._recordsets : k => v if v.records != null @@ -181,7 +181,7 @@ data "google_dns_keys" "dns_keys" { resource "google_dns_record_set" "cloud-static-records" { for_each = ( var.type == "public" || var.type == "private" - ? local.recordsets + ? local.regular_recordsets : {} ) project = var.project_id From 19db2739638804e4a370338fd4e7a71fee1db233 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 25 Oct 2022 15:35:29 +0200 Subject: [PATCH 11/27] Add tests for dns routing policies --- tests/modules/dns/fixture/variables.tf | 20 +++++-- tests/modules/dns/test_plan.py | 82 ++++++++++++++++++++------ 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/tests/modules/dns/fixture/variables.tf b/tests/modules/dns/fixture/variables.tf index 522b238a..0fc6871a 100644 --- a/tests/modules/dns/fixture/variables.tf +++ b/tests/modules/dns/fixture/variables.tf @@ -32,15 +32,27 @@ variable "peer_network" { } variable "recordsets" { - type = map(object({ - ttl = number - records = list(string) - })) + type = any default = { "A localhost" = { ttl = 300, records = ["127.0.0.1"] } "A local-host.test.example." = { ttl = 300, records = ["127.0.0.2"] } "CNAME *" = { ttl = 300, records = ["localhost.example.org."] } "A " = { ttl = 300, records = ["127.0.0.3"] } + "A geo" = { + geo_routing = [ + { location = "europe-west1", records = ["127.0.0.4"] }, + { location = "europe-west2", records = ["127.0.0.5"] }, + { location = "europe-west3", records = ["127.0.0.6"] } + ] + } + "A wrr" = { + ttl = 600 + wrr_routing = [ + { weight = 0.6, records = ["127.0.0.7"] }, + { weight = 0.2, records = ["127.0.0.8"] }, + { weight = 0.2, records = ["10.10.0.9"] } + ] + } } } diff --git a/tests/modules/dns/test_plan.py b/tests/modules/dns/test_plan.py index 184ffe5d..a5f7407b 100644 --- a/tests/modules/dns/test_plan.py +++ b/tests/modules/dns/test_plan.py @@ -12,13 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. + def test_private(plan_runner): "Test private zone with three recordsets." _, resources = plan_runner() - assert len(resources) == 5 - assert set(r['type'] for r in resources) == set([ - 'google_dns_record_set', 'google_dns_managed_zone' - ]) + assert len(resources) == 7 + assert set(r['type'] for r in resources) == set( + ['google_dns_record_set', 'google_dns_managed_zone']) for r in resources: if r['type'] != 'google_dns_managed_zone': continue @@ -29,15 +29,54 @@ def test_private(plan_runner): def test_private_recordsets(plan_runner): "Test recordsets in private zone." _, resources = plan_runner() - recordsets = [r['values'] - for r in resources if r['type'] == 'google_dns_record_set'] + recordsets = [ + r['values'] for r in resources if r['type'] == 'google_dns_record_set' + ] + assert set(r['name'] for r in recordsets) == set([ - 'localhost.test.example.', - 'local-host.test.example.', - '*.test.example.', - "test.example." + 'localhost.test.example.', 'local-host.test.example.', '*.test.example.', + "test.example.", "geo.test.example.", "wrr.test.example." ]) + for r in recordsets: + if r['name'] not in ['wrr.test.example.', 'geo.test.example.']: + assert r['routing_policy'] == [] + assert r['rrdatas'] != [] + + geo_zone = [ + r['values'] for r in resources if r['address'] == + 'module.test.google_dns_record_set.cloud-geo-records["A geo"]' + ][0] + assert geo_zone['name'] == 'geo.test.example.' + assert geo_zone['routing_policy'][0]['wrr'] == [] + assert geo_zone['routing_policy'][0]['geo'] == [{ + 'location': 'europe-west1', + 'rrdatas': ['127.0.0.4'] + }, { + 'location': 'europe-west2', + 'rrdatas': ['127.0.0.5'] + }, { + 'location': 'europe-west3', + 'rrdatas': ['127.0.0.6'] + }] + + wrr_zone = [ + r['values'] for r in resources if r['address'] == + 'module.test.google_dns_record_set.cloud-wrr-records["A wrr"]' + ][0] + assert wrr_zone['name'] == 'wrr.test.example.' + assert wrr_zone['routing_policy'][0]['wrr'] == [{ + 'rrdatas': ['127.0.0.7'], + 'weight': 0.6 + }, { + 'rrdatas': ['127.0.0.8'], + 'weight': 0.2 + }, { + 'rrdatas': ['10.10.0.9'], + 'weight': 0.2 + }] + assert wrr_zone['routing_policy'][0]['geo'] == [] + def test_private_no_networks(plan_runner): "Test private zone not exposed to any network." @@ -60,26 +99,31 @@ def test_forwarding_recordsets_null_forwarders(plan_runner): def test_forwarding(plan_runner): "Test forwarding zone with single forwarder." - _, resources = plan_runner( - type='forwarding', recordsets='null', - forwarders='{ "1.2.3.4" = null }') + _, resources = plan_runner(type='forwarding', recordsets='null', + forwarders='{ "1.2.3.4" = null }') assert len(resources) == 1 resource = resources[0] assert resource['type'] == 'google_dns_managed_zone' - assert resource['values']['forwarding_config'] == [{'target_name_servers': [ - {'forwarding_path': '', 'ipv4_address': '1.2.3.4'}]}] + assert resource['values']['forwarding_config'] == [{ + 'target_name_servers': [{ + 'forwarding_path': '', + 'ipv4_address': '1.2.3.4' + }] + }] def test_peering(plan_runner): "Test peering zone." - _, resources = plan_runner(type='peering', - recordsets='null', + _, resources = plan_runner(type='peering', recordsets='null', peer_network='dummy-vpc-self-link') assert len(resources) == 1 resource = resources[0] assert resource['type'] == 'google_dns_managed_zone' - assert resource['values']['peering_config'] == [ - {'target_network': [{'network_url': 'dummy-vpc-self-link'}]}] + assert resource['values']['peering_config'] == [{ + 'target_network': [{ + 'network_url': 'dummy-vpc-self-link' + }] + }] def test_public(plan_runner): From d759ac2ff132490a4faae2a44d6dafc9e32ca310 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 25 Oct 2022 15:47:18 +0200 Subject: [PATCH 12/27] Make dnssec not nullable --- modules/dns/variables.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/dns/variables.tf b/modules/dns/variables.tf index 644b8395..aafe6a1d 100644 --- a/modules/dns/variables.tf +++ b/modules/dns/variables.tf @@ -48,6 +48,7 @@ variable "dnssec_config" { default = { state = "off" } + nullable = false } variable "domain" { From 9e03ddbf6efaa2fce38c7c675e29f089f5828220 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 25 Oct 2022 16:08:17 +0200 Subject: [PATCH 13/27] Update README --- modules/dns/README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/modules/dns/README.md b/modules/dns/README.md index c9232371..62b38efc 100644 --- a/modules/dns/README.md +++ b/modules/dns/README.md @@ -86,26 +86,25 @@ module "private-dns" { } # tftest modules=1 resources=4 ``` - ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [domain](variables.tf#L53) | Zone domain, must end with a period. | string | ✓ | | -| [name](variables.tf#L71) | Zone name, must be unique within the project. | string | ✓ | | -| [project_id](variables.tf#L82) | Project id for the zone. | string | ✓ | | +| [domain](variables.tf#L54) | Zone domain, must end with a period. | string | ✓ | | +| [name](variables.tf#L72) | Zone name, must be unique within the project. | string | ✓ | | +| [project_id](variables.tf#L83) | Project id for the zone. | string | ✓ | | | [client_networks](variables.tf#L21) | List of VPC self links that can see this zone. | list(string) | | [] | | [description](variables.tf#L28) | Domain description. | string | | "Terraform managed." | | [dnssec_config](variables.tf#L34) | DNSSEC configuration for this zone. | object({…}) | | {…} | -| [enable_logging](variables.tf#L64) | Enable query logging for this zone. Only valid for public zones. | bool | | false | -| [forwarders](variables.tf#L58) | Map of {IPV4_ADDRESS => FORWARDING_PATH} for 'forwarding' zone types. Path can be 'default', 'private', or null for provider default. | map(string) | | {} | -| [peer_network](variables.tf#L76) | Peering network self link, only valid for 'peering' zone types. | string | | null | -| [recordsets](variables.tf#L87) | Map of DNS recordsets in \"type name\" => {ttl, [records]} format. | map(object({…})) | | {} | -| [service_directory_namespace](variables.tf#L122) | Service directory namespace id (URL), only valid for 'service-directory' zone types. | string | | null | -| [type](variables.tf#L128) | Type of zone to create, valid values are 'public', 'private', 'forwarding', 'peering', 'service-directory'. | string | | "private" | -| [zone_create](variables.tf#L138) | Create zone. When set to false, uses a data source to reference existing zone. | bool | | true | +| [enable_logging](variables.tf#L65) | Enable query logging for this zone. Only valid for public zones. | bool | | false | +| [forwarders](variables.tf#L59) | Map of {IPV4_ADDRESS => FORWARDING_PATH} for 'forwarding' zone types. Path can be 'default', 'private', or null for provider default. | map(string) | | {} | +| [peer_network](variables.tf#L77) | Peering network self link, only valid for 'peering' zone types. | string | | null | +| [recordsets](variables.tf#L88) | Map of DNS recordsets in \"type name\" => {ttl, [records]} format. | map(object({…})) | | {} | +| [service_directory_namespace](variables.tf#L123) | Service directory namespace id (URL), only valid for 'service-directory' zone types. | string | | null | +| [type](variables.tf#L129) | Type of zone to create, valid values are 'public', 'private', 'forwarding', 'peering', 'service-directory'. | string | | "private" | +| [zone_create](variables.tf#L139) | Create zone. When set to false, uses a data source to reference existing zone. | bool | | true | ## Outputs From 1df961966cd426b4708ae108c779371e14a36486 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 25 Oct 2022 16:58:06 +0200 Subject: [PATCH 14/27] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f751674..808fd9ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file. ### BLUEPRINTS +- [[#899](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/899)] Static routes monitoring metrics added to network dashboard BP ([maunope](https://github.com/maunope)) +- [[#909](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/909)] GCS2BQ: Move images and templates in sub-folders ([lcaggio](https://github.com/lcaggio)) +- [[#907](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/907)] Fix CloudSQL blueprint ([lcaggio](https://github.com/lcaggio)) - [[#897](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/897)] Project-factory: allow folder_id to be defined in defaults_file ([Malet](https://github.com/Malet)) - [[#900](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/900)] Improve net dashboard variables ([juliocc](https://github.com/juliocc)) - [[#896](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/896)] Network Dashboard: CFv2 and performance improvements ([aurelienlegrand](https://github.com/aurelienlegrand)) @@ -44,6 +47,7 @@ All notable changes to this project will be documented in this file. ### FAST +- [[#911](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/911)] FAST: Additional PGA DNS records ([sruffilli](https://github.com/sruffilli)) - [[#903](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/903)] Initial replacement for CI/CD stage ([ludoo](https://github.com/ludoo)) - [[#898](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/898)] Update FAST bootstrap README.md ([juliocc](https://github.com/juliocc)) - [[#880](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/880)] **incompatible change:** Refactor net-vpc module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) @@ -63,6 +67,9 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#916](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/916)] Add support for DNS routing policies ([juliocc](https://github.com/juliocc)) +- [[#918](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/918)] Fix race condition in SimpleNVA ([sruffilli](https://github.com/sruffilli)) +- [[#914](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/914)] **incompatible change:** Update DNS module ([juliocc](https://github.com/juliocc)) - [[#904](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/904)] Add missing description field ([dsbutler101](https://github.com/dsbutler101)) - [[#891](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/891)] Add internal_ips output to compute-vm module ([LucaPrete](https://github.com/LucaPrete)) - [[#890](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/890)] Add auto_delete and instance_redistribution_type to compute-vm and compute-mig modules. ([giovannibaratta](https://github.com/giovannibaratta)) From 15d21eba39d8e620056d0ff606ea8590994ab46e Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 25 Oct 2022 16:58:37 +0200 Subject: [PATCH 15/27] Rename workflow names --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a903525..3fc3fe56 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ env: TF_VERSION: 1.3.2 jobs: - doc-examples: + examples: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -68,7 +68,7 @@ jobs: pip install -r tests/requirements.txt pytest -vv tests/examples - examples: + blueprints: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 From 0611d66be7eceab3e4ea2fbce917576933b2f5c3 Mon Sep 17 00:00:00 2001 From: Simone Ruffilli Date: Tue, 25 Oct 2022 18:07:38 +0200 Subject: [PATCH 16/27] SimpleNVA: add the option to create additional files --- modules/cloud-config-container/simple-nva/main.tf | 10 ++++++++-- modules/cloud-config-container/simple-nva/variables.tf | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/modules/cloud-config-container/simple-nva/main.tf b/modules/cloud-config-container/simple-nva/main.tf index 5b9663bd..4ff0afe2 100644 --- a/modules/cloud-config-container/simple-nva/main.tf +++ b/modules/cloud-config-container/simple-nva/main.tf @@ -21,7 +21,7 @@ locals { network_interfaces = local.network_interfaces })) - files = { + files = merge({ "/var/run/nva/ipprefix_by_netmask.sh" = { content = file("${path.module}/files/ipprefix_by_netmask.sh") owner = "root" @@ -32,7 +32,13 @@ locals { owner = "root" permissions = "0744" } - } + }, { + for path, attrs in var.files : path => { + content = attrs.content, + owner = attrs.owner, + permissions = attrs.permissions + } + }) network_interfaces = [ for index, interface in var.network_interfaces : { diff --git a/modules/cloud-config-container/simple-nva/variables.tf b/modules/cloud-config-container/simple-nva/variables.tf index 9307ddac..3c2ebfcb 100644 --- a/modules/cloud-config-container/simple-nva/variables.tf +++ b/modules/cloud-config-container/simple-nva/variables.tf @@ -20,6 +20,16 @@ variable "cloud_config" { default = null } +variable "files" { + description = "Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null." + type = map(object({ + content = string + owner = string + permissions = string + })) + default = {} +} + variable "enable_health_checks" { description = "Configures routing to enable responses to health check probes." type = bool From 384756a8a7f2c2e4038979343dbb4c9341841827 Mon Sep 17 00:00:00 2001 From: Simone Ruffilli Date: Tue, 25 Oct 2022 18:09:31 +0200 Subject: [PATCH 17/27] SimpleNVA: updated example --- modules/cloud-config-container/simple-nva/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/cloud-config-container/simple-nva/README.md b/modules/cloud-config-container/simple-nva/README.md index 5014e9a3..3f5b0553 100644 --- a/modules/cloud-config-container/simple-nva/README.md +++ b/modules/cloud-config-container/simple-nva/README.md @@ -35,6 +35,13 @@ module "nva-cloud-config" { source = "../../../cloud-foundation-fabric/modules/cloud-config-container/simple-nva" enable_health_checks = true network_interfaces = local.network_interfaces + files = { + "/var/lib/cloud/scripts/per-boot/firewall-rules.sh" = { + content = file("./your_path/to/firewall-rules.sh") + owner = "root" + permissions = 0700 + } + } } # COS VM @@ -63,9 +70,10 @@ module "nva" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [network_interfaces](variables.tf#L29) | Network interfaces configuration. | list(object({…})) | ✓ | | +| [network_interfaces](variables.tf#L39) | Network interfaces configuration. | list(object({…})) | ✓ | | | [cloud_config](variables.tf#L17) | Cloud config template path. If null default will be used. | string | | null | -| [enable_health_checks](variables.tf#L23) | Configures routing to enable responses to health check probes. | bool | | false | +| [enable_health_checks](variables.tf#L33) | Configures routing to enable responses to health check probes. | bool | | false | +| [files](variables.tf#L23) | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({…})) | | {} | | [test_instance](variables-instance.tf#L17) | Test/development instance attributes, leave null to skip creation. | object({…}) | | null | | [test_instance_defaults](variables-instance.tf#L30) | Test/development instance defaults used for optional configuration. If image is null, COS stable will be used. | object({…}) | | {…} | From 77614191bb056d4453171a56e84b96e2df49b17b Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Tue, 25 Oct 2022 20:53:14 +0200 Subject: [PATCH 18/27] Added more attribute mappings, updated attribute condition and IAM binding for WIF --- .../gcp-workload-identity-provider/main.tf | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf index 543e9d72..5ced2e3c 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf @@ -49,12 +49,18 @@ resource "google_iam_workload_identity_pool_provider" "tfe-pool-provider" { workload_identity_pool_provider_id = var.workload_identity_pool_provider_id display_name = "TFE Pool Provider" description = "OIDC identity pool provider for TFE Integration" - # Use condition to make sure only token generated for a specific TFE Org and workspace can be used - attribute_condition = "attribute.terraform_workspace_id == \"${var.tfe_workspace_id}\" && attribute.terraform_organization_id == \"${var.tfe_organization_id}\"" + # Use condition to make sure only token generated for a specific TFE Org can be used across org workspaces + attribute_condition = "attribute.terraform_organization_id == \"${var.tfe_organization_id}\"" attribute_mapping = { - "google.subject" = "assertion.sub" - "attribute.terraform_organization_id" = "assertion.terraform_organization_id" - "attribute.terraform_workspace_id" = "assertion.terraform_workspace_id" + "google.subject" = "assertion.sub" + "attribute.aud" = "assertion.aud" + "attribute.terraform_run_phase" = "assertion.terraform_run_phase" + "attribute.terraform_workspace_id" = "assertion.terraform_workspace_id" + "attribute.terraform_workspace_name" = "assertion.terraform_workspace_name" + "attribute.terraform_organization_id" = "assertion.terraform_organization_id" + "attribute.terraform_organization_name" = "assertion.terraform_organization_name" + "attribute.terraform_run_id" = "assertion.terraform_run_id" + "attribute.terraform_full_workspace" = "assertion.terraform_full_workspace" } oidc { # Should be different if self hosted TFE instance is used @@ -72,7 +78,9 @@ module "sa-tfe" { name = "sa-tfe" iam = { - "roles/iam.workloadIdentityUser" = ["principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.tfe-pool.name}/*"] + # We allow only tokens generated by a specific TFE workspace impersonation of the service account, + # that way one identity pool can be used for a TFE Organization, but every workspace will be able to impersonate only a specifc SA + "roles/iam.workloadIdentityUser" = ["principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.tfe-pool.name}/attribute.terraform_workspace_id/${var.tfe_workspace_id}"] } iam_project_roles = { From d9ccf042219f0ea3c1948ab60c58158fb6f855f3 Mon Sep 17 00:00:00 2001 From: apichick Date: Tue, 25 Oct 2022 23:18:50 +0200 Subject: [PATCH 19/27] Added managed_prometheus to features (#906) * Added managed_prometheus monitoring_config * fix module Co-authored-by: Ludovico Magnocavallo --- blueprints/gke/multitenant-fleet/README.md | 28 ++++++++--------- blueprints/gke/multitenant-fleet/variables.tf | 9 ++++-- fast/stages/03-gke-multitenant/dev/README.md | 30 +++++++++---------- .../03-gke-multitenant/dev/variables.tf | 9 ++++-- modules/gke-cluster/README.md | 14 ++++----- modules/gke-cluster/main.tf | 10 ++++++- modules/gke-cluster/variables.tf | 9 ++++-- .../modules/gke_cluster/fixture/variables.tf | 7 +++++ 8 files changed, 71 insertions(+), 45 deletions(-) diff --git a/blueprints/gke/multitenant-fleet/README.md b/blueprints/gke/multitenant-fleet/README.md index ab8c6247..bd6df945 100644 --- a/blueprints/gke/multitenant-fleet/README.md +++ b/blueprints/gke/multitenant-fleet/README.md @@ -246,20 +246,20 @@ module "gke" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [billing_account_id](variables.tf#L17) | Billing account id. | string | ✓ | | -| [folder_id](variables.tf#L129) | Folder used for the GKE project in folders/nnnnnnnnnnn format. | string | ✓ | | -| [prefix](variables.tf#L176) | Prefix used for resources that need unique names. | string | ✓ | | -| [project_id](variables.tf#L181) | ID of the project that will contain all the clusters. | string | ✓ | | -| [vpc_config](variables.tf#L193) | Shared VPC project and VPC details. | object({…}) | ✓ | | -| [clusters](variables.tf#L22) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} | -| [fleet_configmanagement_clusters](variables.tf#L67) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | -| [fleet_configmanagement_templates](variables.tf#L74) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | -| [fleet_features](variables.tf#L109) | Enable and configue fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | -| [fleet_workload_identity](variables.tf#L122) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | -| [group_iam](variables.tf#L134) | Project-level IAM bindings for groups. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | -| [iam](variables.tf#L141) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [labels](variables.tf#L148) | Project-level labels. | map(string) | | {} | -| [nodepools](variables.tf#L154) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | -| [project_services](variables.tf#L186) | Additional project services to enable. | list(string) | | [] | +| [folder_id](variables.tf#L132) | Folder used for the GKE project in folders/nnnnnnnnnnn format. | string | ✓ | | +| [prefix](variables.tf#L179) | Prefix used for resources that need unique names. | string | ✓ | | +| [project_id](variables.tf#L184) | ID of the project that will contain all the clusters. | string | ✓ | | +| [vpc_config](variables.tf#L196) | Shared VPC project and VPC details. | object({…}) | ✓ | | +| [clusters](variables.tf#L22) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} | +| [fleet_configmanagement_clusters](variables.tf#L70) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | +| [fleet_configmanagement_templates](variables.tf#L77) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | +| [fleet_features](variables.tf#L112) | Enable and configue fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | +| [fleet_workload_identity](variables.tf#L125) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | +| [group_iam](variables.tf#L137) | Project-level IAM bindings for groups. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | +| [iam](variables.tf#L144) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [labels](variables.tf#L151) | Project-level labels. | map(string) | | {} | +| [nodepools](variables.tf#L157) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | +| [project_services](variables.tf#L189) | Additional project services to enable. | list(string) | | [] | ## Outputs diff --git a/blueprints/gke/multitenant-fleet/variables.tf b/blueprints/gke/multitenant-fleet/variables.tf index d0464298..8d6c69ae 100644 --- a/blueprints/gke/multitenant-fleet/variables.tf +++ b/blueprints/gke/multitenant-fleet/variables.tf @@ -39,9 +39,12 @@ variable "clusters" { recurring_window = null maintenance_exclusion = [] }) - max_pods_per_node = optional(number, 110) - min_master_version = optional(string) - monitoring_config = optional(list(string), ["SYSTEM_COMPONENTS"]) + max_pods_per_node = optional(number, 110) + min_master_version = optional(string) + monitoring_config = optional(object({ + enable_components = optional(list(string), ["SYSTEM_COMPONENTS"]) + managed_prometheus = optional(bool) + })) node_locations = optional(list(string)) private_cluster_config = optional(any) release_channel = optional(string) diff --git a/fast/stages/03-gke-multitenant/dev/README.md b/fast/stages/03-gke-multitenant/dev/README.md index ac4e03d3..f3abf494 100644 --- a/fast/stages/03-gke-multitenant/dev/README.md +++ b/fast/stages/03-gke-multitenant/dev/README.md @@ -142,21 +142,21 @@ terraform apply |---|---|:---:|:---:|:---:|:---:| | [automation](variables.tf#L21) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | | [billing_account](variables.tf#L29) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L146) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | -| [host_project_ids](variables.tf#L168) | Host project for the shared VPC. | object({…}) | ✓ | | 02-networking | -| [prefix](variables.tf#L210) | Prefix used for resources that need unique names. | string | ✓ | | | -| [vpc_self_links](variables.tf#L222) | Self link for the shared VPC. | object({…}) | ✓ | | 02-networking | -| [clusters](variables.tf#L38) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} | | -| [fleet_configmanagement_clusters](variables.tf#L83) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | | -| [fleet_configmanagement_templates](variables.tf#L91) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | | -| [fleet_features](variables.tf#L126) | Enable and configue fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | | -| [fleet_workload_identity](variables.tf#L139) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | | -| [group_iam](variables.tf#L154) | Project-level authoritative IAM bindings for groups in {GROUP_EMAIL => [ROLES]} format. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | | -| [iam](variables.tf#L161) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | -| [labels](variables.tf#L176) | Project-level labels. | map(string) | | {} | | -| [nodepools](variables.tf#L182) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | | -| [outputs_location](variables.tf#L204) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [project_services](variables.tf#L215) | Additional project services to enable. | list(string) | | [] | | +| [folder_ids](variables.tf#L149) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | +| [host_project_ids](variables.tf#L171) | Host project for the shared VPC. | object({…}) | ✓ | | 02-networking | +| [prefix](variables.tf#L213) | Prefix used for resources that need unique names. | string | ✓ | | | +| [vpc_self_links](variables.tf#L225) | Self link for the shared VPC. | object({…}) | ✓ | | 02-networking | +| [clusters](variables.tf#L38) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} | | +| [fleet_configmanagement_clusters](variables.tf#L86) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | | +| [fleet_configmanagement_templates](variables.tf#L94) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | | +| [fleet_features](variables.tf#L129) | Enable and configue fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | | +| [fleet_workload_identity](variables.tf#L142) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | | +| [group_iam](variables.tf#L157) | Project-level authoritative IAM bindings for groups in {GROUP_EMAIL => [ROLES]} format. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | | +| [iam](variables.tf#L164) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | +| [labels](variables.tf#L179) | Project-level labels. | map(string) | | {} | | +| [nodepools](variables.tf#L185) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | | +| [outputs_location](variables.tf#L207) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| [project_services](variables.tf#L218) | Additional project services to enable. | list(string) | | [] | | ## Outputs diff --git a/fast/stages/03-gke-multitenant/dev/variables.tf b/fast/stages/03-gke-multitenant/dev/variables.tf index 1a17da4b..9c5a1d38 100644 --- a/fast/stages/03-gke-multitenant/dev/variables.tf +++ b/fast/stages/03-gke-multitenant/dev/variables.tf @@ -55,9 +55,12 @@ variable "clusters" { recurring_window = null maintenance_exclusion = [] }) - max_pods_per_node = optional(number, 110) - min_master_version = optional(string) - monitoring_config = optional(list(string), ["SYSTEM_COMPONENTS"]) + max_pods_per_node = optional(number, 110) + min_master_version = optional(string) + monitoring_config = optional(object({ + enable_components = optional(list(string), ["SYSTEM_COMPONENTS"]) + managed_prometheus = optional(bool) + })) node_locations = optional(list(string)) private_cluster_config = optional(any) release_channel = optional(string) diff --git a/modules/gke-cluster/README.md b/modules/gke-cluster/README.md index be0a9f62..55b594c6 100644 --- a/modules/gke-cluster/README.md +++ b/modules/gke-cluster/README.md @@ -77,9 +77,9 @@ module "cluster-1" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [location](variables.tf#L117) | Cluster zone or region. | string | ✓ | | -| [name](variables.tf#L169) | Cluster name. | string | ✓ | | -| [project_id](variables.tf#L195) | Cluster project id. | string | ✓ | | -| [vpc_config](variables.tf#L206) | VPC-level configuration. | object({…}) | ✓ | | +| [name](variables.tf#L174) | Cluster name. | string | ✓ | | +| [project_id](variables.tf#L200) | Cluster project id. | string | ✓ | | +| [vpc_config](variables.tf#L211) | VPC-level configuration. | object({…}) | ✓ | | | [cluster_autoscaling](variables.tf#L17) | Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler. | object({…}) | | null | | [description](variables.tf#L38) | Cluster description. | string | | null | | [enable_addons](variables.tf#L44) | Addons enabled in the cluster (true means enabled). | object({…}) | | {…} | @@ -90,10 +90,10 @@ module "cluster-1" { | [maintenance_config](variables.tf#L128) | Maintenance window configuration. | object({…}) | | {…} | | [max_pods_per_node](variables.tf#L151) | Maximum number of pods per node in this cluster. | number | | 110 | | [min_master_version](variables.tf#L157) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | -| [monitoring_config](variables.tf#L163) | Monitoring components. | list(string) | | ["SYSTEM_COMPONENTS"] | -| [node_locations](variables.tf#L174) | Zones in which the cluster's nodes are located. | list(string) | | [] | -| [private_cluster_config](variables.tf#L181) | Private cluster configuration. | object({…}) | | null | -| [release_channel](variables.tf#L200) | Release channel for GKE upgrades. | string | | null | +| [monitoring_config](variables.tf#L163) | Monitoring components. | object({…}) | | {…} | +| [node_locations](variables.tf#L179) | Zones in which the cluster's nodes are located. | list(string) | | [] | +| [private_cluster_config](variables.tf#L186) | Private cluster configuration. | object({…}) | | null | +| [release_channel](variables.tf#L205) | Release channel for GKE upgrades. | string | | null | ## Outputs diff --git a/modules/gke-cluster/main.tf b/modules/gke-cluster/main.tf index 9981d9b4..c6b179ff 100644 --- a/modules/gke-cluster/main.tf +++ b/modules/gke-cluster/main.tf @@ -240,7 +240,15 @@ resource "google_container_cluster" "cluster" { dynamic "monitoring_config" { for_each = var.monitoring_config != null && !var.enable_features.autopilot ? [""] : [] content { - enable_components = var.monitoring_config + enable_components = var.monitoring_config.enable_components + dynamic "managed_prometheus" { + for_each = ( + try(var.monitoring_config.managed_prometheus, null) == true ? [""] : [] + ) + content { + enabled = true + } + } } } diff --git a/modules/gke-cluster/variables.tf b/modules/gke-cluster/variables.tf index a227d5c7..f9a3b69e 100644 --- a/modules/gke-cluster/variables.tf +++ b/modules/gke-cluster/variables.tf @@ -162,8 +162,13 @@ variable "min_master_version" { variable "monitoring_config" { description = "Monitoring components." - type = list(string) - default = ["SYSTEM_COMPONENTS"] + type = object({ + enable_components = optional(list(string)) + managed_prometheus = optional(bool) + }) + default = { + enable_components = ["SYSTEM_COMPONENTS"] + } } variable "name" { diff --git a/tests/modules/gke_cluster/fixture/variables.tf b/tests/modules/gke_cluster/fixture/variables.tf index 1b539d20..97fc6a63 100644 --- a/tests/modules/gke_cluster/fixture/variables.tf +++ b/tests/modules/gke_cluster/fixture/variables.tf @@ -28,3 +28,10 @@ variable "enable_features" { workload_identity = true } } + +variable "monitoring_config" { + type = any + default = { + managed_prometheus = true + } +} From ed9fd6b08de690a90fd99e938e78ee97cd9e60c2 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Wed, 26 Oct 2022 14:31:04 +0200 Subject: [PATCH 20/27] Align documentation, move glb blueprint (#921) * aling documentation, move glb blueprint * modules README, sort modules * reorder modules * fix bp links * fix moved blueprint test * use a single cloud shell image in the repo --- CHANGELOG.md | 4 ++ README.md | 16 ++--- .../images/cloud-shell-button.png | Bin blueprints/README.md | 14 ++-- blueprints/cloud-operations/README.md | 62 ++++++++++------ blueprints/cloud-operations/adfs/README.md | 12 ++-- .../workload-identity-federation/README.md | 17 +++-- blueprints/data-solutions/README.md | 43 +++++++----- .../cloudsql-multiregion/README.md | 15 ++-- .../cloudsql-multiregion/images/button.png | Bin 10762 -> 0 bytes .../gcs-to-bq-with-least-privileges/README.md | 3 +- .../images/cloud_shell.png | Bin 147763 -> 0 bytes blueprints/gke/README.md | 23 +++--- blueprints/networking/README.md | 66 ++++++++++++------ .../glb-and-armor}/README.md | 4 +- .../glb-and-armor}/architecture.png | Bin .../glb-and-armor}/cloud_shell.png | Bin .../glb-and-armor}/main.tf | 0 .../glb-and-armor}/outputs.tf | 0 .../glb-and-armor}/variables.tf | 0 .../nginx-reverse-proxy-cluster/README.md | 13 ++-- blueprints/third-party-solutions/README.md | 8 +++ .../wordpress/cloudrun/README.md | 12 +++- .../wordpress/cloudrun/images/button.png | Bin 10762 -> 0 bytes modules/README.md | 58 ++++++++------- .../glb_and_armor/__init__.py | 0 .../glb_and_armor/fixture/main.tf | 2 +- .../glb_and_armor/fixture/variables.tf | 0 .../glb_and_armor/test_plan.py | 0 29 files changed, 224 insertions(+), 148 deletions(-) rename blueprints/cloud-operations/glb_and_armor/shell_button.png => assets/images/cloud-shell-button.png (100%) delete mode 100644 blueprints/data-solutions/cloudsql-multiregion/images/button.png delete mode 100644 blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/cloud_shell.png rename blueprints/{cloud-operations/glb_and_armor => networking/glb-and-armor}/README.md (96%) rename blueprints/{cloud-operations/glb_and_armor => networking/glb-and-armor}/architecture.png (100%) rename blueprints/{cloud-operations/glb_and_armor => networking/glb-and-armor}/cloud_shell.png (100%) rename blueprints/{cloud-operations/glb_and_armor => networking/glb-and-armor}/main.tf (100%) rename blueprints/{cloud-operations/glb_and_armor => networking/glb-and-armor}/outputs.tf (100%) rename blueprints/{cloud-operations/glb_and_armor => networking/glb-and-armor}/variables.tf (100%) delete mode 100644 blueprints/third-party-solutions/wordpress/cloudrun/images/button.png rename tests/blueprints/{cloud_operations => networking}/glb_and_armor/__init__.py (100%) rename tests/blueprints/{cloud_operations => networking}/glb_and_armor/fixture/main.tf (89%) rename tests/blueprints/{cloud_operations => networking}/glb_and_armor/fixture/variables.tf (100%) rename tests/blueprints/{cloud_operations => networking}/glb_and_armor/test_plan.py (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 808fd9ef..c43d3fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. ### BLUEPRINTS +- [[#915](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/915)] TFE OIDC with GCP WIF blueprint added ([averbuks](https://github.com/averbuks)) - [[#899](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/899)] Static routes monitoring metrics added to network dashboard BP ([maunope](https://github.com/maunope)) - [[#909](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/909)] GCS2BQ: Move images and templates in sub-folders ([lcaggio](https://github.com/lcaggio)) - [[#907](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/907)] Fix CloudSQL blueprint ([lcaggio](https://github.com/lcaggio)) @@ -67,6 +68,8 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#908](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/908)] GKE module: autopilot fixes ([ludoo](https://github.com/ludoo)) +- [[#906](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/906)] GKE module: add managed_prometheus to features ([apichick](https://github.com/apichick)) - [[#916](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/916)] Add support for DNS routing policies ([juliocc](https://github.com/juliocc)) - [[#918](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/918)] Fix race condition in SimpleNVA ([sruffilli](https://github.com/sruffilli)) - [[#914](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/914)] **incompatible change:** Update DNS module ([juliocc](https://github.com/juliocc)) @@ -102,6 +105,7 @@ All notable changes to this project will be documented in this file. ### TOOLS +- [[#919](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/919)] Rename workflow names ([juliocc](https://github.com/juliocc)) - [[#902](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/902)] Bring back sorted variables check ([juliocc](https://github.com/juliocc)) - [[#887](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/887)] Disable parallel execution of tests and plugin cache ([ludoo](https://github.com/ludoo)) - [[#886](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/886)] Revert "Improve handling of tf plugin cache in tests" ([ludoo](https://github.com/ludoo)) diff --git a/README.md b/README.md index 70d5d666..6aa292d7 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This repository provides **end-to-end blueprints** and a **suite of Terraform mo - organization-wide [landing zone blueprint](fast/) used to bootstrap real-world cloud foundations - reference [blueprints](./blueprints/) used to deep dive on network patterns or product features -- a comprehensive source of lean [modules](./modules/dns) that lend themselves well to changes +- a comprehensive source of lean [modules](./modules/) 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 [contributing guide](./CONTRIBUTING.md), and a comparison against similar toolkits [here](./FABRIC-AND-CFT.md). @@ -29,16 +29,16 @@ The current list of modules supports most of the core foundational and networkin Currently available modules: -- **foundational** - [folder](./modules/folder), [organization](./modules/organization), [project](./modules/project), [service accounts](./modules/iam-service-account), [logging bucket](./modules/logging-bucket), [billing budget](./modules/billing-budget), [projects-data-source](./modules/projects-data-source), [organization-policy](./modules/organization-policy) -- **networking** - [VPC](./modules/net-vpc), [VPC firewall](./modules/net-vpc-firewall), [VPC peering](./modules/net-vpc-peering), [VPN static](./modules/net-vpn-static), [VPN dynamic](./modules/net-vpn-dynamic), [HA VPN](./modules/net-vpn-ha), [NAT](./modules/net-cloudnat), [address reservation](./modules/net-address), [DNS](./modules/dns), [L4 ILB](./modules/net-ilb), [L7 ILB](./modules/net-ilb-l7), [Service Directory](./modules/service-directory), [Cloud Endpoints](./modules/endpoints) -- **compute** - [VM/VM group](./modules/compute-vm), [MIG](./modules/compute-mig), [GKE cluster](./modules/gke-cluster), [GKE nodepool](./modules/gke-nodepool), [GKE hub](./modules/gke-hub), [COS container](./modules/cloud-config-container/cos-generic-metadata/) (coredns, mysql, onprem, squid) -- **data** - [GCS](./modules/gcs), [BigQuery dataset](./modules/bigquery-dataset), [Pub/Sub](./modules/pubsub), [Datafusion](./modules/datafusion), [Bigtable instance](./modules/bigtable-instance), [Cloud SQL instance](./modules/cloudsql-instance), [Data Catalog Policy Tag](./modules/data-catalog-policy-tag) -- **development** - [Cloud Source Repository](./modules/source-repository), [Container Registry](./modules/container-registry), [Artifact Registry](./modules/artifact-registry), [Apigee Organization](./modules/apigee-organization), [Apigee X Instance](./modules/apigee-x-instance), [API Gateway](./modules/api-gateway) -- **security** - [KMS](./modules/kms), [SecretManager](./modules/secret-manager), [VPC Service Control](./modules/vpc-sc) +- **foundational** - [billing budget](./modules/billing-budget), [Cloud Identity group](./modules/cloud-identity-group/), [folder](./modules/folder), [service accounts](./modules/iam-service-account), [logging bucket](./modules/logging-bucket), [organization](./modules/organization), [organization-policy](./modules/organization-policy), [project](./modules/project), [projects-data-source](./modules/projects-data-source) +- **networking** - [DNS](./modules/dns), [Cloud Endpoints](./modules/endpoints), [address reservation](./modules/net-address), [NAT](./modules/net-cloudnat), [Global Load Balancer (classic)](./modules/net-glb/), [L4 ILB](./modules/net-ilb), [L7 ILB](./modules/net-ilb-l7), [VPC](./modules/net-vpc), [VPC firewall](./modules/net-vpc-firewall), [VPC peering](./modules/net-vpc-peering), [VPN dynamic](./modules/net-vpn-dynamic), [HA VPN](./modules/net-vpn-ha), [VPN static](./modules/net-vpn-static), [Service Directory](./modules/service-directory) +- **compute** - [VM/VM group](./modules/compute-vm), [MIG](./modules/compute-mig), [COS container](./modules/cloud-config-container/cos-generic-metadata/) (coredns, mysql, onprem, squid), [GKE cluster](./modules/gke-cluster), [GKE hub](./modules/gke-hub), [GKE nodepool](./modules/gke-nodepool) +- **data** - [BigQuery dataset](./modules/bigquery-dataset), [Bigtable instance](./modules/bigtable-instance), [Cloud SQL instance](./modules/cloudsql-instance), [Data Catalog Policy Tag](./modules/data-catalog-policy-tag), [Datafusion](./modules/datafusion), [GCS](./modules/gcs), [Pub/Sub](./modules/pubsub) +- **development** - [API Gateway](./modules/api-gateway), [Apigee Organization](./modules/apigee-organization), [Apigee X Instance](./modules/apigee-x-instance), [Artifact Registry](./modules/artifact-registry), [Container Registry](./modules/container-registry), [Cloud Source Repository](./modules/source-repository) +- **security** - [Binauthz](./modules/binauthz/), [KMS](./modules/kms), [SecretManager](./modules/secret-manager), [VPC Service Control](./modules/vpc-sc) - **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 blueprints -The [blueprints](./blueprints/) in this repository are split in several main sections: **[networking blueprints](./blueprints/networking/)** that implement core patterns or features, **[data solutions blueprints](./blueprints/data-solutions/)** that demonstrate how to integrate data services in complete scenarios, **[cloud operations blueprints](./blueprints/cloud-operations/)** that leverage specific products to meet specific operational needs and **[factories](./blueprints/factories/)** that implement resource factories for the repetitive creation of specific resources, and finally **[GKE](./blueprints/gke)** and **[serverless](./blueprints/serverless)** design blueprints. +The [blueprints](./blueprints/) in this repository are split in several main sections: **[networking blueprints](./blueprints/networking/)** that implement core patterns or features, **[data solutions blueprints](./blueprints/data-solutions/)** that demonstrate how to integrate data services in complete scenarios, **[cloud operations blueprints](./blueprints/cloud-operations/)** that leverage specific products to meet specific operational needs and **[factories](./blueprints/factories/)** that implement resource factories for the repetitive creation of specific resources, and finally **[GKE](./blueprints/gke)**, **[serverless](./blueprints/serverless)**, and **[third-party solutions](./blueprints/third-party-solutions/)** design blueprints. diff --git a/blueprints/cloud-operations/glb_and_armor/shell_button.png b/assets/images/cloud-shell-button.png similarity index 100% rename from blueprints/cloud-operations/glb_and_armor/shell_button.png rename to assets/images/cloud-shell-button.png diff --git a/blueprints/README.md b/blueprints/README.md index aad7cb08..77e13906 100644 --- a/blueprints/README.md +++ b/blueprints/README.md @@ -4,12 +4,12 @@ This section **[networking blueprints](./networking/)** that implement core patt Currently available blueprints: -- **cloud operations** - [Resource tracking and remediation via Cloud Asset feeds](./cloud-operations/asset-inventory-feed-remediation), [Granular Cloud DNS IAM via Service Directory](./cloud-operations/dns-fine-grained-iam), [Granular Cloud DNS IAM for Shared VPC](./cloud-operations/dns-shared-vpc), [Compute Engine quota monitoring](./cloud-operations/quota-monitoring), [Scheduled Cloud Asset Inventory Export to Bigquery](./cloud-operations/scheduled-asset-inventory-export-bq), [Packer image builder](./cloud-operations/packer-image-builder), [On-prem SA key management](./cloud-operations/onprem-sa-key-management), [TCP healthcheck for unmanaged GCE instances](./cloud-operations/unmanaged-instances-healthcheck), [HTTP Load Balancer with Cloud Armor](./cloud-operations/glb_and_armor) -- **data solutions** - [GCE/GCS CMEK via centralized Cloud KMS](./data-solutions/cmek-via-centralized-kms/), [Cloud Storage to Bigquery with Cloud Dataflow with least privileges](./data-solutions/gcs-to-bq-with-least-privileges/), [Data Platform Foundations](./data-solutions/data-platform-foundations/), [SQL Server AlwaysOn availability groups blueprint](./data-solutions/sqlserver-alwayson), [Cloud SQL instance with multi-region read replicas](./data-solutions/cloudsql-multiregion/), [Cloud Composer version 2 private instance, supporting Shared VPC and external CMEK key](./data-solutions/composer-2/) -- **factories** - [The why and the how of resource factories](./factories/README.md) -- **GKE** - [GKE multitenant fleet](./gke/multitenant-fleet/), [Shared VPC with GKE support](./networking/shared-vpc-gke/), [Binary Authorization Pipeline](./gke/binauthz/), [Multi-cluster mesh on GKE (fleet API)](./gke/multi-cluster-mesh-gke-fleet-api/) -- **networking** - [hub and spoke via peering](./networking/hub-and-spoke-peering/), [hub and spoke via VPN](./networking/hub-and-spoke-vpn/), [DNS and Google Private Access for on-premises](./networking/onprem-google-access-dns/), [Shared VPC with GKE support](./networking/shared-vpc-gke/), [ILB as next hop](./networking/ilb-next-hop), [Connecting to on-premise services leveraging PSC and hybrid NEGs](./networking/psc-hybrid/), [decentralized firewall](./networking/decentralized-firewall) -- **serverless** - [Multi-region deployments for API Gateway](./serverless/api-gateway/) -- **third party solutions** - [OpenShift cluster on Shared VPC](./third-party-solutions/openshift) +- **cloud operations** - [Active Directory Federation Services](./cloud-operations/adfs), [Cloud Asset Inventory feeds for resource change tracking and remediation](./cloud-operations/asset-inventory-feed-remediation), [Fine-grained Cloud DNS IAM via Service Directory](./cloud-operations/dns-fine-grained-iam), [Cloud DNS & Shared VPC design](./cloud-operations/dns-shared-vpc), [Delegated Role Grants](./cloud-operations/iam-delegated-role-grants), [Networking Dashboard](./cloud-operations/network-dashboard), [Managing on-prem service account keys by uploading public keys](./cloud-operations/onprem-sa-key-management), [Compute Image builder with Hashicorp Packer](./cloud-operations/packer-image-builder), [Packer example](./cloud-operations/packer-image-builder/packer), [Compute Engine quota monitoring](./cloud-operations/quota-monitoring), [Scheduled Cloud Asset Inventory Export to Bigquery](./cloud-operations/scheduled-asset-inventory-export-bq), [Configuring workload identity federation for Terraform Cloud/Enterprise workflow](./cloud-operations/terraform-enterprise-wif), [TCP healthcheck and restart for unmanaged GCE instances](./cloud-operations/unmanaged-instances-healthcheck), [Migrate for Compute Engine (v5) blueprints](./cloud-operations/vm-migration), [Configuring workload identity federation to access Google Cloud resources from apps running on Azure](./cloud-operations/workload-identity-federation) +- **data solutions** - [GCE and GCS CMEK via centralized Cloud KMS](./data-solutions/cmek-via-centralized-kms), [Cloud Composer version 2 private instance, supporting Shared VPC and external CMEK key](./data-solutions/composer-2), [Cloud SQL instance with multi-region read replicas](./data-solutions/cloudsql-multiregion), [Data Platform](./data-solutions/data-platform-foundations), [Spinning up a foundation data pipeline on Google Cloud using Cloud Storage, Dataflow and BigQuery](./data-solutions/gcs-to-bq-with-least-privileges), [#SQL Server Always On Groups blueprint](./data-solutions/sqlserver-alwayson), [Data Playground](./data-solutions/data-playground) +- **factories** - [[The why and the how of Resource Factories](./factories), [Google Cloud Identity Group Factory](./factories/cloud-identity-group-factory), [Google Cloud BQ Factory](./factories/bigquery-factory), [Google Cloud VPC Firewall Factory](./factories/net-vpc-firewall-yaml), [Minimal Project Factory](./factories/project-factory) +- **GKE** - [Binary Authorization Pipeline Blueprint](./gke/binauthz), [Storage API](./gke/binauthz/image), [Multi-cluster mesh on GKE (fleet API)](./gke/multi-cluster-mesh-gke-fleet-api), [GKE Multitenant Blueprint](./gke/multitenant-fleet), [Shared VPC with GKE support](./networking/shared-vpc-gke/) +- **networking** - [Decentralized firewall management](./networking/decentralized-firewall), [Decentralized firewall validator](./networking/decentralized-firewall/validator), [Network filtering with Squid](./networking/filtering-proxy), [HTTP Load Balancer with Cloud Armor](./networking/glb-and-armor), [Hub and Spoke via VPN](./networking/hub-and-spoke-vpn), [Hub and Spoke via VPC Peering](./networking/hub-and-spoke-peering), [Internal Load Balancer as Next Hop](./networking/ilb-next-hop), [Nginx-based reverse proxy cluster](./networking/nginx-reverse-proxy-cluster), [On-prem DNS and Google Private Access](./networking/onprem-google-access-dns), [Calling a private Cloud Function from On-premises](./networking/private-cloud-function-from-onprem), [Hybrid connectivity to on-premise services through PSC](./networking/psc-hybrid), [PSC Producer](./networking/psc-hybrid/psc-producer), [PSC Consumer](./networking/psc-hybrid/psc-consumer), [Shared VPC with optional GKE cluster](./networking/shared-vpc-gke) +- **serverless** - [Creating multi-region deployments for API Gateway](./serverless/api-gateway) +- **third party solutions** - [OpenShift on GCP user-provisioned infrastructure](./third-party-solutions/openshift), [Wordpress deployment on Cloud Run](./third-party-solutions/wordpress/cloudrun) For more information see the individual README files in each section. diff --git a/blueprints/cloud-operations/README.md b/blueprints/cloud-operations/README.md index 88d55d4e..863aee58 100644 --- a/blueprints/cloud-operations/README.md +++ b/blueprints/cloud-operations/README.md @@ -2,6 +2,12 @@ The blueprints in this folder show how to wire together different Google Cloud services to simplify operations, and are meant for testing, or as minimal but sufficiently complete starting points for actual use. +## Active Directory Federation Services + + This [blueprint](./adfs/) Sets up managed AD, creates a server where AD FS will be installed which will also act as admin workstation for AD, and exposes ADFS using GLB. It can also optionally set up a GCP project and VPC if needed + +
+ ## Resource tracking and remediation via Cloud Asset feeds This [blueprint](./asset-inventory-feed-remediation) shows how to leverage [Cloud Asset Inventory feeds](https://cloud.google.com/asset-inventory/docs/monitoring-asset-changes) to stream resource changes in real time, and how to programmatically use the feed change notifications for alerting or remediation, via a Cloud Function wired to the feed PubSub queue. @@ -10,12 +16,6 @@ The blueprint's feed tracks changes to Google Compute instances, and the Cloud F
-## Scheduled Cloud Asset Inventory Export to Bigquery - - This [blueprint](./scheduled-asset-inventory-export-bq) shows how to leverage the [Cloud Asset Inventory Exporting to Bigquery](https://cloud.google.com/asset-inventory/docs/exporting-to-bigquery) feature, to keep track of your organization's assets over time storing information in Bigquery. Data stored in Bigquery can then be used for different purposes like dashboarding or analysis. - -
- ## Granular Cloud DNS IAM via Service Directory This [blueprint](./dns-fine-grained-iam) shows how to leverage [Service Directory](https://cloud.google.com/blog/products/networking/introducing-service-directory) and Cloud DNS Service Directory private zones, to implement fine-grained IAM controls on DNS. The blueprint creates a Service Directory namespace, a Cloud DNS private zone that uses it as its authoritative source, service accounts with different levels of permissions, and VMs to test them. @@ -28,42 +28,62 @@ The blueprint's feed tracks changes to Google Compute instances, and the Cloud F
-## Compute Engine quota monitoring - - This [blueprint](./quota-monitoring) shows a practical way of collecting and monitoring [Compute Engine resource quotas](https://cloud.google.com/compute/quotas) via Cloud Monitoring metrics as an alternative to the recently released [built-in quota metrics](https://cloud.google.com/monitoring/alerts/using-quota-metrics). A simple alert on quota thresholds is also part of the blueprint. - -
- ## Delegated Role Grants This [blueprint](./iam-delegated-role-grants) shows how to use delegated role grants to restrict service usage.
+## Network Dashboard + + This [blueprint](./network-dashboard/) provides an end-to-end solution to gather some GCP Networking quotas and limits (that cannot be seen in the GCP console today) and display them in a dashboard. The goal is to allow for better visibility of these limits, facilitating capacity planning and avoiding hitting these limits.. + +
+ +## On-prem Service Account key management + +This [blueprint](./onprem-sa-key-management) shows how to manage IAM Service Account Keys by manually generating a key pair and uploading the public part of the key to GCP. + +
+ ## Packer image builder This [blueprint](./packer-image-builder) shows how to deploy infrastructure for a Compute Engine image builder based on [Hashicorp's Packer tool](https://www.packer.io).
-## On-prem Service Account key management +## Compute Engine quota monitoring - -This [blueprint](./onprem-sa-key-management) shows how to manage IAM Service Account Keys by manually generating a key pair and uploading the public part of the key to GCP. + This [blueprint](./quota-monitoring) shows a practical way of collecting and monitoring [Compute Engine resource quotas](https://cloud.google.com/compute/quotas) via Cloud Monitoring metrics as an alternative to the recently released [built-in quota metrics](https://cloud.google.com/monitoring/alerts/using-quota-metrics). A simple alert on quota thresholds is also part of the blueprint.
-## Migrate for Compute Engine (v5) - This set of [blueprints](./vm-migration) shows how to deploy Migrate for Compute Engine (v5) on top of existing Cloud Foundations on different scenarios. An blueprint on how to deploy the M4CE connector on VMWare ESXi is also part of the blueprints. +## Scheduled Cloud Asset Inventory Export to Bigquery -
- -## TCP healthcheck for unmanaged GCE instances - This [blueprint](./unmanaged-instances-healthcheck) shows how to leverage [Serverless VPC Access](https://cloud.google.com/vpc/docs/configure-serverless-vpc-access) and Cloud Functions to organize a highly performant TCP healtheck for unmanaged GCE instances. + This [blueprint](./scheduled-asset-inventory-export-bq) shows how to leverage the [Cloud Asset Inventory Exporting to Bigquery](https://cloud.google.com/asset-inventory/docs/exporting-to-bigquery) feature, to keep track of your organization's assets over time storing information in Bigquery. Data stored in Bigquery can then be used for different purposes like dashboarding or analysis.
## Workload identity federation for Terraform Enterprise workflow + This [blueprint](./terraform-enterprise-wif) shows how to configure [Wokload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between [Terraform Cloud/Enterprise](https://developer.hashicorp.com/terraform/enterprise) instance and Google Cloud.
+ +## TCP healthcheck for unmanaged GCE instances + + This [blueprint](./unmanaged-instances-healthcheck) shows how to leverage [Serverless VPC Access](https://cloud.google.com/vpc/docs/configure-serverless-vpc-access) and Cloud Functions to organize a highly performant TCP healtheck for unmanaged GCE instances. + +
+ +## Migrate for Compute Engine (v5) + + This set of [blueprints](./vm-migration) shows how to deploy Migrate for Compute Engine (v5) on top of existing Cloud Foundations on different scenarios. An blueprint on how to deploy the M4CE connector on VMWare ESXi is also part of the blueprints. + +
+ +## Configuring Workload Identity Federation from apps running on Azure + + This [blueprint](./workload-identity-federation) shows how to set up everything, both in Azure and Google Cloud, so a workload in Azure can access Google Cloud resources without a service account key. This will be possible by configuring workload identity federation to trust access tokens generated for a specific application in an Azure Active Directory (AAD) tenant. + +
diff --git a/blueprints/cloud-operations/adfs/README.md b/blueprints/cloud-operations/adfs/README.md index a690f1ea..0b954884 100644 --- a/blueprints/cloud-operations/adfs/README.md +++ b/blueprints/cloud-operations/adfs/README.md @@ -1,19 +1,19 @@ -# AD FS +# Active Directory Federation Services -This blueprint does the following: +This blueprint does the following: Terraform: - (Optional) Creates a project. - (Optional) Creates a VPC. - Sets up managed AD -- Creates a server where AD FS will be installed. This machine will also act as admin workstation for AD. +- Creates a server where AD FS will be installed. This machine will also act as admin workstation for AD. - Exposes AD FS using GLB. Ansible: - Installs the required Windows features and joins the computer to the AD domain. -- Provisions some tests users, groups and group memberships in AD. The data to provision is in the files directory of the ad-provisioning ansible role. There is script available in the scripts/ad-provisioning folder that you can use to generate an alternative users or memberships file. +- Provisions some tests users, groups and group memberships in AD. The data to provision is in the files directory of the ad-provisioning ansible role. There is script available in the scripts/ad-provisioning folder that you can use to generate an alternative users or memberships file. - Installs AD FS In addition to this, we also include a Powershell script that facilitates the configuration required for Anthos when authenticating users with AD FS as IdP. @@ -26,8 +26,8 @@ The diagram below depicts the architecture of the blueprint: Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fcloud-foundation-fabric&cloudshell_print=cloud-shell-readme.txt&cloudshell_working_dir=blueprints%2Fcloud-operations%2Fadfs), then go through the following steps to create resources: -* `terraform init` -* `terraform apply -var project_id=my-project-id -var ad_dns_domain_name=my-domain.org -var adfs_dns_domain_name=adfs.my-domain.org` +- `terraform init` +- `terraform apply -var project_id=my-project-id -var ad_dns_domain_name=my-domain.org -var adfs_dns_domain_name=adfs.my-domain.org` Once the resources have been created, do the following: diff --git a/blueprints/cloud-operations/workload-identity-federation/README.md b/blueprints/cloud-operations/workload-identity-federation/README.md index fb990342..ad6feaed 100644 --- a/blueprints/cloud-operations/workload-identity-federation/README.md +++ b/blueprints/cloud-operations/workload-identity-federation/README.md @@ -1,9 +1,9 @@ -# Configuring workload identity federation to access Google Cloud resources from apps running on Azure +# Configuring Workload Identity Federation to access Google Cloud resources from apps running on Azure The most straightforward way for workloads running outside of Google Cloud to call Google Cloud APIs is by using a downloaded service account key. However, this approach has 2 major pain points: * A management hassle, keys need to be stored securely and rotated often. -* A security risk, keys are long term credentials that could be compromised. +* A security risk, keys are long term credentials that could be compromised. Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. @@ -19,17 +19,17 @@ The provided terraform configuration will set up the following architecture: * On Azure: - * An Azure Active Directory application and a service principal. By default, the new application grants all users in the Azure AD tenant permission to obtain access tokens. So an app role assignment will be required to restrict which identities can obtain access tokens for the application. + * An Azure Active Directory application and a service principal. By default, the new application grants all users in the Azure AD tenant permission to obtain access tokens. So an app role assignment will be required to restrict which identities can obtain access tokens for the application. - * Optionally, all the resources required to have a VM configured to run with a system-assigned managed identity and accessible via SSH on a public IP using public key authentication, so we can log in to the machine and run the `gcloud` command to verify that everything works as expected. + * Optionally, all the resources required to have a VM configured to run with a system-assigned managed identity and accessible via SSH on a public IP using public key authentication, so we can log in to the machine and run the `gcloud` command to verify that everything works as expected. * On Google Cloud: - * A Google Cloud project with: + * A Google Cloud project with: - * A workload identity pool and provider configured to trust the AAD application + * A workload identity pool and provider configured to trust the AAD application - * A service account with the Viewer role granted on the project. The external identities in the workload identity pool would be assigned the Workload Identity User role on that service account. + * A service account with the Viewer role granted on the project. The external identities in the workload identity pool would be assigned the Workload Identity User role on that service account. ## Running the blueprint @@ -42,7 +42,7 @@ Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/c Once the resources have been created, do the following to verify that everything works as expected: -1. Log in to the VM. +1. Log in to the VM. If you have created the VM using this terraform configuration proceed the following way: @@ -72,7 +72,6 @@ Once the resources have been created, do the following to verify that everything `gcloud projects describe PROJECT_ID` - Once done testing, you can clean up resources by running `terraform destroy`. diff --git a/blueprints/data-solutions/README.md b/blueprints/data-solutions/README.md index 968d7b9c..4919f29a 100644 --- a/blueprints/data-solutions/README.md +++ b/blueprints/data-solutions/README.md @@ -6,32 +6,32 @@ They are meant to be used as minimal but complete starting points to create actu ## Blueprints +### Cloud SQL instance with multi-region read replicas + + +This [blueprint](./cloudsql-multiregion/) creates a [Cloud SQL instance](https://cloud.google.com/sql) with multi-region read replicas as described in the [Cloud SQL for PostgreSQL disaster recovery](https://cloud.google.com/architecture/cloud-sql-postgres-disaster-recovery-complete-failover-fallback) article. + +
+ ### GCE and GCS CMEK via centralized Cloud KMS This [blueprint](./cmek-via-centralized-kms/) implements [CMEK](https://cloud.google.com/kms/docs/cmek) for GCS and GCE, via keys hosted in KMS running in a centralized project. The blueprint shows the basic resources and permissions for the typical use case of application projects implementing encryption at rest via a centrally managed KMS service. +
-### Cloud Storage to Bigquery with Cloud Dataflow with least privileges +### Cloud Composer version 2 private instance, supporting Shared VPC and external CMEK key + + +This [blueprint](./composer-2/) creates a [Cloud Composer](https://cloud.google.com/composer/) version 2 instance on a VPC with a dedicated service account. The solution supports as inputs: a Shared VPC and Cloud KMS CMEK keys. - This [blueprint](./gcs-to-bq-with-least-privileges/) implements resources required to run GCS to BigQuery Dataflow pipelines. The solution rely on a set of Services account created with the least privileges principle.
### Data Platform Foundations This [blueprint](./data-platform-foundations/) implements a robust and flexible Data Foundation on GCP that provides opinionated defaults, allowing customers to build and scale out additional data pipelines quickly and reliably. -
-### SQL Server Always On Availability Groups - - -This [blueprint](./data-platform-foundations/) implements SQL Server Always On Availability Groups using Fabric modules. It builds a two node cluster with a fileshare witness instance in an existing VPC and adds the necessary firewalling. The actual setup process (apart from Active Directory operations) has been scripted, so that least amount of manual works needs to performed. -
- -### Cloud SQL instance with multi-region read replicas - - -This [blueprint](./cloudsql-multiregion/) creates a [Cloud SQL instance](https://cloud.google.com/sql) with multi-region read replicas as described in the [Cloud SQL for PostgreSQL disaster recovery](https://cloud.google.com/architecture/cloud-sql-postgres-disaster-recovery-complete-failover-fallback) article.
### Data Playground starter with Cloud Vertex AI Notebook and GCS @@ -40,11 +40,18 @@ This [blueprint](./cloudsql-multiregion/) creates a [Cloud SQL instance](https:/ This [blueprint](./data-playground/) creates a [Vertex AI Notebook](https://cloud.google.com/vertex-ai/docs/workbench/introduction) running on a VPC with a private IP and a dedicated Service Account. A GCS bucket and a BigQuery dataset are created to store inputs and outputs of data experiments. +
-### Cloud Composer version 2 private instance, supporting Shared VPC and external CMEK key +### Cloud Storage to Bigquery with Cloud Dataflow with least privileges - -This [blueprint](./composer-2/) creates a [Cloud Composer](https://cloud.google.com/composer/) version 2 instance on a VPC with a dedicated service account. The solution supports as inputs: a Shared VPC and Cloud KMS CMEK keys. -
\ No newline at end of file + This [blueprint](./gcs-to-bq-with-least-privileges/) implements resources required to run GCS to BigQuery Dataflow pipelines. The solution rely on a set of Services account created with the least privileges principle. + +
+ +### SQL Server Always On Availability Groups + + +This [blueprint](./data-platform-foundations/) implements SQL Server Always On Availability Groups using Fabric modules. It builds a two node cluster with a fileshare witness instance in an existing VPC and adds the necessary firewalling. The actual setup process (apart from Active Directory operations) has been scripted, so that least amount of manual works needs to performed. + +
diff --git a/blueprints/data-solutions/cloudsql-multiregion/README.md b/blueprints/data-solutions/cloudsql-multiregion/README.md index babacd58..5bdc6329 100644 --- a/blueprints/data-solutions/cloudsql-multiregion/README.md +++ b/blueprints/data-solutions/cloudsql-multiregion/README.md @@ -39,7 +39,7 @@ If `project_create` is left to `null`, the identity performing the deployment ne Click on the image below, sign in if required and when the prompt appears, click on “confirm”. -[![Open Cloudshell](images/button.png)](https://goo.gle/GoCloudSQL) +[![Open Cloudshell](../../../assets/images/cloud-shell-button.png)](https://goo.gle/GoCloudSQL) This will clone the repository to your cloud shell and a screen like this one will appear: @@ -81,7 +81,8 @@ This implementation is intentionally minimal and easy to read. A real world use - Using VPC-SC to mitigate data exfiltration ### Shared VPC -The example supports the configuration of a Shared VPC as an input variable. + +The example supports the configuration of a Shared VPC as an input variable. To deploy the solution on a Shared VPC, you have to configure the `network_config` variable: ``` @@ -94,12 +95,14 @@ network_config = { ``` To run this example, the Shared VPC project needs to have: - - A Private Service Connect with a range of `/24` (example: `10.60.0.0/24`) to deploy the Cloud SQL instance. - - Internet access configured (for example Cloud NAT) to let the Test VM download packages. + +- A Private Service Connect with a range of `/24` (example: `10.60.0.0/24`) to deploy the Cloud SQL instance. +- Internet access configured (for example Cloud NAT) to let the Test VM download packages. In order to run the example and deploy Cloud SQL on a shared VPC the identity running Terraform must have the following IAM role on the Shared VPC Host project. - - Compute Network Admin (roles/compute.networkAdmin) - - Compute Shared VPC Admin (roles/compute.xpnAdmin) + +- Compute Network Admin (roles/compute.networkAdmin) +- Compute Shared VPC Admin (roles/compute.xpnAdmin) ## Test your environment diff --git a/blueprints/data-solutions/cloudsql-multiregion/images/button.png b/blueprints/data-solutions/cloudsql-multiregion/images/button.png deleted file mode 100644 index 21a3f3de9d130679049a1085476073e5bf54a43d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10762 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

|qk#Uhq&wZFS}5zbJ7fn!XL? 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 This [blueprint](../gke/binauthz/) shows how to create a CI and a CD pipeline in Cloud Build for the deployment of an application to a private GKE cluster with unrestricted access to a public endpoint. The blueprint enables a Binary Authorization policy in the project so only images that have been attested can be deployed to the cluster. The attestations are created using a cryptographic key pair that has been provisioned in KMS. + +
+ +### Multi-cluster mesh on GKE (fleet API) + + This [blueprint](../gke/multi-cluster-mesh-gke-fleet-api/) shows how to create a multi-cluster mesh for two private clusters on GKE. Anthos Service Mesh with automatic control plane management is set up for clusters using the Fleet API. This can only be done if the clusters are in a single project and in the same VPC. In this particular case both clusters having being deployed to different subnets in a shared VPC. + +
+ ### Multitenant GKE fleet This [blueprint](./multitenant-fleet/) allows simple centralized management of similar sets of GKE clusters and their nodepools in a single project, and optional fleet management via GKE Hub templated configurations. @@ -16,14 +28,5 @@ They are meant to be used as minimal but complete starting points to create actu This [blueprint](../networking/shared-vpc-gke/) shows how to configure a Shared VPC, including the specific IAM configurations needed for GKE, and to give different level of access to the VPC subnets to different identities. It is meant to be used as a starting point for most Shared VPC configurations, and to be integrated to the above blueprints where Shared VPC is needed in more complex network topologies. -
- -### Binary Authorization Pipeline - - This [blueprint](../gke/binauthz/) shows how to create a CI and a CD pipeline in Cloud Build for the deployment of an application to a private GKE cluster with unrestricted access to a public endpoint. The blueprint enables a Binary Authorization policy in the project so only images that have been attested can be deployed to the cluster. The attestations are created using a cryptographic key pair that has been provisioned in KMS. -
- -### Multi-cluster mesh on GKE (fleet API) - - This [blueprint](../gke/multi-cluster-mesh-gke-fleet-api/) shows how to create a multi-cluster mesh for two private clusters on GKE. Anthos Service Mesh with automatic control plane management is set up for clusters using the Fleet API. This can only be done if the clusters are in a single project and in the same VPC. In this particular case both clusters having being deployed to different subnets in a shared VPC. +
diff --git a/blueprints/networking/README.md b/blueprints/networking/README.md index e234cc25..c4a3d2f0 100644 --- a/blueprints/networking/README.md +++ b/blueprints/networking/README.md @@ -6,11 +6,30 @@ They are meant to be used as minimal but complete starting points to create actu ## Blueprints +### Decentralized firewall management + + This [blueprint](./decentralized-firewall/) shows how a decentralized firewall management can be organized using the [firewall factory](../factories/net-vpc-firewall-yaml/). + +
+ +### Network filtering with Squid + + This [blueprint](./filtering-proxy/) how to deploy a filtering HTTP proxy to restrict Internet access, in a simplified setup using a VPC with two subnets and a Cloud DNS zone, and an optional MIG for scaling. + +
+ +## HTTP Load Balancer with Cloud Armor + + This [blueprint](./glb-and-armor/) contains all necessary Terraform modules to build a multi-regional infrastructure with horizontally scalable managed instance group backends, HTTP load balancing and Google’s advanced WAF security tool (Cloud Armor) on top to securely deploy an application at global scale. + +
+ ### Hub and Spoke via Peering This [blueprint](./hub-and-spoke-peering/) implements a hub and spoke topology via VPC peering, a common design where a landing zone VPC (hub) is connected to on-premises, and then peered with satellite VPCs (spokes) to further partition the infrastructure. The sample highlights the lack of transitivity in peering: the absence of connectivity between spokes, and the need create workarounds for private service access to managed services. One such workaround is shown for private GKE, allowing access from hub and all spokes to GKE masters via a dedicated VPN. +
### Hub and Spoke via Dynamic VPN @@ -18,6 +37,19 @@ The sample highlights the lack of transitivity in peering: the absence of connec This [blueprint](./hub-and-spoke-vpn/) implements a hub and spoke topology via dynamic VPN tunnels, a common design where peering cannot be used due to limitations on the number of spokes or connectivity to managed services. The blueprint shows how to implement spoke transitivity via BGP advertisements, how to expose hub DNS zones to spokes via DNS peering, and allows easy testing of different VPN and BGP configurations. + +
+ +### ILB as next hop + + This [blueprint](./ilb-next-hop/) allows testing [ILB as next hop](https://cloud.google.com/load-balancing/docs/internal/ilb-next-hop-overview) using simple Linux gateway VMS between two VPCs, to emulate virtual appliances. An optional additional ILB can be enabled to test multiple load balancer configurations and hashing. + +
+ +### Nginx-based reverse proxy cluster + + This [blueprint](./nginx-reverse-proxy-cluster/) how to deploy an autoscaling reverse proxy cluster using Nginx, based on regional Managed Instance Groups. The autoscaling is driven by Nginx current connections metric, sent by Cloud Ops Agent. +
### DNS and Private Access for On-premises @@ -25,6 +57,19 @@ The blueprint shows how to implement spoke transitivity via BGP advertisements, This [blueprint](./onprem-google-access-dns/) uses an emulated on-premises environment running in Docker containers inside a GCE instance, to allow testing specific features like DNS policies, DNS forwarding zones across VPN, and Private Access for On-premises hosts. The emulated on-premises environment can be used to test access to different services from outside Google Cloud, by implementing a VPN connection and BGP to Google CLoud via Strongswan and Bird. + +
+ +### Calling a private Cloud Function from on-premises + + This [blueprint](./private-cloud-function-from-onprem/) shows how to invoke a [private Google Cloud Function](https://cloud.google.com/functions/docs/networking/network-settings) from the on-prem environment via a [Private Service Connect endpoint](https://cloud.google.com/vpc/docs/private-service-connect#benefits-apis). + +
+ +### Calling on-premise services through PSC and hybrid NEGs + + This [blueprint](./psc-hybrid/) shows how to privately connect to on-premise services (IP + port) from GCP, leveraging [Private Service Connect (PSC)](https://cloud.google.com/vpc/docs/private-service-connect) and [Hybrid Network Endpoint Groups](https://cloud.google.com/load-balancing/docs/negs/hybrid-neg-concepts). +
### Shared VPC with GKE and per-subnet support @@ -32,24 +77,5 @@ The emulated on-premises environment can be used to test access to different ser This [blueprint](./shared-vpc-gke/) shows how to configure a Shared VPC, including the specific IAM configurations needed for GKE, and to give different level of access to the VPC subnets to different identities. It is meant to be used as a starting point for most Shared VPC configurations, and to be integrated to the above blueprints where Shared VPC is needed in more complex network topologies. -
- -### ILB as next hop - - This [blueprint](./ilb-next-hop/) allows testing [ILB as next hop](https://cloud.google.com/load-balancing/docs/internal/ilb-next-hop-overview) using simple Linux gateway VMS between two VPCs, to emulate virtual appliances. An optional additional ILB can be enabled to test multiple load balancer configurations and hashing. -
- -### Calling a private Cloud Function from on-premises - - This [blueprint](./private-cloud-function-from-onprem/) shows how to invoke a [private Google Cloud Function](https://cloud.google.com/functions/docs/networking/network-settings) from the on-prem environment via a [Private Service Connect endpoint](https://cloud.google.com/vpc/docs/private-service-connect#benefits-apis). -
- -### Calling on-premise services through PSC and hybrid NEGs - - This [blueprint](./psc-hybrid/) shows how to privately connect to on-premise services (IP + port) from GCP, leveraging [Private Service Connect (PSC)](https://cloud.google.com/vpc/docs/private-service-connect) and [Hybrid Network Endpoint Groups](https://cloud.google.com/load-balancing/docs/negs/hybrid-neg-concepts). -
- -### Decentralized firewall management - - This [blueprint](./decentralized-firewall/) shows how a decentralized firewall management can be organized using the [firewall factory](../factories/net-vpc-firewall-yaml/). +
diff --git a/blueprints/cloud-operations/glb_and_armor/README.md b/blueprints/networking/glb-and-armor/README.md similarity index 96% rename from blueprints/cloud-operations/glb_and_armor/README.md rename to blueprints/networking/glb-and-armor/README.md index 25ffec90..0c9a802e 100644 --- a/blueprints/cloud-operations/glb_and_armor/README.md +++ b/blueprints/networking/glb-and-armor/README.md @@ -2,7 +2,7 @@ ## Introduction -This repository contains all necessary Terraform modules to build a multi-regional infrastructure with horizontally scalable managed instance group backends, HTTP load balancing and Google’s advanced WAF security tool (Cloud Armor) on top to securely deploy an application at global scale. +This blueprint contains all necessary Terraform modules to build a multi-regional infrastructure with horizontally scalable managed instance group backends, HTTP load balancing and Google’s advanced WAF security tool (Cloud Armor) on top to securely deploy an application at global scale. This tutorial is general enough to fit in a variety of use-cases, from hosting a mobile app's backend to deploy proprietary workloads at scale. @@ -62,7 +62,7 @@ Note: To grant a user a role, take a look at the [Granting and Revoking Access]( Click on the button below, sign in if required and when the prompt appears, click on “confirm”. -[![Open Cloudshell](shell_button.png)](https://goo.gle/GoCloudArmor) +[![Open Cloudshell](../../../assets/images/cloud-shell-button.png)](https://goo.gle/GoCloudArmor) This will clone the repository to your cloud shell and a screen like this one will appear: diff --git a/blueprints/cloud-operations/glb_and_armor/architecture.png b/blueprints/networking/glb-and-armor/architecture.png similarity index 100% rename from blueprints/cloud-operations/glb_and_armor/architecture.png rename to blueprints/networking/glb-and-armor/architecture.png diff --git a/blueprints/cloud-operations/glb_and_armor/cloud_shell.png b/blueprints/networking/glb-and-armor/cloud_shell.png similarity index 100% rename from blueprints/cloud-operations/glb_and_armor/cloud_shell.png rename to blueprints/networking/glb-and-armor/cloud_shell.png diff --git a/blueprints/cloud-operations/glb_and_armor/main.tf b/blueprints/networking/glb-and-armor/main.tf similarity index 100% rename from blueprints/cloud-operations/glb_and_armor/main.tf rename to blueprints/networking/glb-and-armor/main.tf diff --git a/blueprints/cloud-operations/glb_and_armor/outputs.tf b/blueprints/networking/glb-and-armor/outputs.tf similarity index 100% rename from blueprints/cloud-operations/glb_and_armor/outputs.tf rename to blueprints/networking/glb-and-armor/outputs.tf diff --git a/blueprints/cloud-operations/glb_and_armor/variables.tf b/blueprints/networking/glb-and-armor/variables.tf similarity index 100% rename from blueprints/cloud-operations/glb_and_armor/variables.tf rename to blueprints/networking/glb-and-armor/variables.tf diff --git a/blueprints/networking/nginx-reverse-proxy-cluster/README.md b/blueprints/networking/nginx-reverse-proxy-cluster/README.md index c3101a15..b8436283 100644 --- a/blueprints/networking/nginx-reverse-proxy-cluster/README.md +++ b/blueprints/networking/nginx-reverse-proxy-cluster/README.md @@ -1,20 +1,17 @@ # Nginx-based reverse proxy cluster -This blueprint shows how to deploy an autoscaling reverse proxy cluster using Nginx, based on regional -Managed Instance Groups. +This blueprint shows how to deploy an autoscaling reverse proxy cluster using Nginx, based on regional Managed Instance Groups. ![High-level diagram](reverse-proxy.png "High-level diagram") -The autoscaling is driven by Nginx current connections metric, sent by Cloud Ops Agent. +The autoscaling is driven by Nginx current connections metric, sent by Cloud Ops Agent. -The example is for Nginx, but it could be easily adapted to any other reverse proxy software (eg. -Squid, Varnish, etc). +The example is for Nginx, but it could be easily adapted to any other reverse proxy software (eg. Squid, Varnish, etc). ## Ops Agent image -There is a simple [`Dockerfile`](Dockerfile) available for building Ops Agent to be run -inside the ContainerOS instance. Build the container, push it to your Container/Artifact -Repository and set the `ops_agent_image` to point to the image you built. +There is a simple [`Dockerfile`](Dockerfile) available for building Ops Agent to be run inside the ContainerOS instance. Build the container, push it to your Container/Artifact Repository and set the `ops_agent_image` to point to the image you built. + ## Variables diff --git a/blueprints/third-party-solutions/README.md b/blueprints/third-party-solutions/README.md index 10b7ced2..c7cbec73 100644 --- a/blueprints/third-party-solutions/README.md +++ b/blueprints/third-party-solutions/README.md @@ -7,3 +7,11 @@ The blueprints in this folder show how to automate installation of specific thir ### OpenShift cluster bootstrap on Shared VPC This [example](./openshift/) shows how to quickly bootstrap an OpenShift 4.7 cluster on GCP, using typical enterprise features like Shared VPC and CMEK for instance disks. + +
+ +### Wordpress deployment on Cloud Run + + This [example](./wordpress/cloudrun/) shows how to deploy a functioning new Wordpress website exposed to the public internet via CloudRun and Cloud SQL, with minimal technical overhead. + +
diff --git a/blueprints/third-party-solutions/wordpress/cloudrun/README.md b/blueprints/third-party-solutions/wordpress/cloudrun/README.md index ee1e2d90..4ca10796 100644 --- a/blueprints/third-party-solutions/wordpress/cloudrun/README.md +++ b/blueprints/third-party-solutions/wordpress/cloudrun/README.md @@ -36,11 +36,11 @@ If `project_create` is left to null, the identity performing the deployment need If you want to deploy from your Cloud Shell, click on the image below, sign in if required and when the prompt appears, click on “confirm”. -[![Open Cloudshell](images/button.png)](https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fcloud-foundation-fabric&cloudshell_workspace=blueprints%2Fthird-party-solutions%2Fwordpress%2Fcloudrun) - +[![Open Cloudshell](../../../../assets/images/cloud-shell-button.png)](https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fcloud-foundation-fabric&cloudshell_workspace=blueprints%2Fthird-party-solutions%2Fwordpress%2Fcloudrun) Otherwise, in your console of choice: -``` {shell} + +```bash git clone https://github.com/GoogleCloudPlatform/cloud-foundation-fabric ``` @@ -70,6 +70,7 @@ Once you have the required information, head back to your cloned repository. Mak Configure the Terraform variables in your `terraform.tfvars` file. See [terraform.tfvars.sample](terraform.tfvars.sample) as starting point - just copy it to `terraform.tfvars` and edit the latter. See the variables documentation below. **Notes**: + 1. If you will want to change your admin password later on, please note that it will only work in the admin interface of Wordpress, but not with redeploying with Terraform, since Wordpress writes that password into the database upon installation and ignores the environment variables (that you can change with Terraform) after that. 2. If you have the [domain restriction org. policy](https://cloud.google.com/resource-manager/docs/organization-policy/restricting-domains) on your organization, you have to edit the `cloud_run_invoker` variable and give it a value that will be accepted in accordance to your policy. @@ -81,22 +82,27 @@ Initialize your Terraform environment and deploy the resources: terraform init terraform apply ``` + The resource creation will take a few minutes. **Note**: you might get the following error (or a similar one): + ``` {shell} │ Error: resource is in failed state "Ready:False", message: Revision '...' is not ready and cannot serve traffic.│ ``` + You might try to reapply at this point, the Cloud Run service just needs several minutes. ### Step 4: Use the created resources Upon completion, you will see the output with the values for the Cloud Run service and the user and password to access the `/admin` part of the website. You can also view it later with: + ``` {shell} terraform output # or for the concrete variable: terraform output cloud_run_service ``` + 1. Open your browser at the URL that you get with that last command, and you will see your Wordpress installation. 2. Add "/admin" in the end of the URL and log in to the admin interface, using the outputs "wp_user" and "wp_password". diff --git a/blueprints/third-party-solutions/wordpress/cloudrun/images/button.png b/blueprints/third-party-solutions/wordpress/cloudrun/images/button.png deleted file mode 100644 index 21a3f3de9d130679049a1085476073e5bf54a43d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10762 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 Date: Thu, 27 Oct 2022 09:18:34 +0200 Subject: [PATCH 21/27] Update README.md --- .../cloud-operations/network-dashboard/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/blueprints/cloud-operations/network-dashboard/README.md b/blueprints/cloud-operations/network-dashboard/README.md index 4d62ed5f..5e640fdd 100644 --- a/blueprints/cloud-operations/network-dashboard/README.md +++ b/blueprints/cloud-operations/network-dashboard/README.md @@ -15,12 +15,14 @@ Three metric descriptors are created for each monitored resource: usage, limit a Clone this repository, then go through the following steps to create resources: - Create a terraform.tfvars file with the following content: - - organization_id = "" - - billing_account = "" - - monitoring_project_id = "project-0" # Monitoring project where the dashboard will be created and the solution deployed - - monitored_projects_list = ["project-1", "project2"] # Projects to be monitored by the solution - - monitored_folders_list = ["folder_id"] # Folders to be monitored by the solution - - v2 = true|false # Set to true to use V2 Cloud Functions environment + ```tfvars + organization_id = "" + billing_account = "" + monitoring_project_id = "project-0" # Monitoring project where the dashboard will be created and the solution deployed + monitored_projects_list = ["project-1", "project2"] # Projects to be monitored by the solution + monitored_folders_list = ["folder_id"] # Folders to be monitored by the solution + v2 = false # Set to true to use V2 Cloud Functions environment + ``` - `terraform init` - `terraform apply` From b021d84633d6ae798d1daa15dc45a76d793cb403 Mon Sep 17 00:00:00 2001 From: Ayman Farhat <823713+aymanfarhat@users.noreply.github.com> Date: Thu, 27 Oct 2022 16:07:24 +0200 Subject: [PATCH 22/27] Fix formatting for gcloud dataflow job launch command (#924) This fix is to prevent errors similar to "ERROR: (gcloud.dataflow.jobs.run) unrecognized arguments: df-loading@tf-pso-workshop-test.iam.gserviceaccount.com" when copy pasting the example code. Line 8 misses a space between the statement and line break, leading to missing a space between parameters when evaluated by the gcloud command. --- .../data-solutions/gcs-to-bq-with-least-privileges/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/README.md b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/README.md index 1d3f9397..915ada21 100644 --- a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/README.md +++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/README.md @@ -145,13 +145,13 @@ Once this is done, the 3 files necessary to run the Dataflow Job will have been Run the following command to start the dataflow job: - gcloud --impersonate-service-account=orchestrator@$SERVICE_PROJECT_ID.iam.gserviceaccount.com dataflow jobs run test_batch_01 \ + gcloud --impersonate-service-account=orchestrator@$SERVICE_PROJECT_ID.iam.gserviceaccount.com dataflow jobs run test_batch_01 \ --gcs-location gs://dataflow-templates/latest/GCS_Text_to_BigQuery \ --project $SERVICE_PROJECT_ID \ --region europe-west1 \ --disable-public-ips \ --subnetwork https://www.googleapis.com/compute/v1/projects/$SERVICE_PROJECT_ID/regions/europe-west1/subnetworks/subnet \ - --staging-location gs://$PREFIX-df-tmp\ + --staging-location gs://$PREFIX-df-tmp \ --service-account-email df-loading@$SERVICE_PROJECT_ID.iam.gserviceaccount.com \ --parameters \ javascriptTextTransformFunctionName=transform,\ From e20de3b86a5eea42ecbf4452b499e1264444905c Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Thu, 27 Oct 2022 17:12:04 +0200 Subject: [PATCH 23/27] fix service account create (#923) --- blueprints/gke/binauthz/main.tf | 16 +++--- .../multi-cluster-mesh-gke-fleet-api/gke.tf | 20 ++++--- blueprints/networking/shared-vpc-gke/main.tf | 16 +++--- modules/gke-hub/README.md | 4 +- modules/gke-nodepool/README.md | 55 +++++++++++++++++-- modules/gke-nodepool/main.tf | 21 +++---- modules/gke-nodepool/variables.tf | 8 ++- tests/modules/gke_nodepool/fixture/main.tf | 39 ++++++++----- .../modules/gke_nodepool/fixture/variables.tf | 6 +- tests/modules/gke_nodepool/test_plan.py | 4 +- 10 files changed, 125 insertions(+), 64 deletions(-) diff --git a/blueprints/gke/binauthz/main.tf b/blueprints/gke/binauthz/main.tf index 79323943..0c3655e4 100644 --- a/blueprints/gke/binauthz/main.tf +++ b/blueprints/gke/binauthz/main.tf @@ -99,13 +99,15 @@ module "cluster" { } module "cluster_nodepool" { - source = "../../../modules/gke-nodepool" - project_id = module.project.project_id - cluster_name = module.cluster.name - location = var.zone - name = "nodepool" - service_account = {} - node_count = { initial = 3 } + source = "../../../modules/gke-nodepool" + project_id = module.project.project_id + cluster_name = module.cluster.name + location = var.zone + name = "nodepool" + service_account = { + create = true + } + node_count = { initial = 3 } } module "kms" { diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/gke.tf b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/gke.tf index 73ab19b1..6c769d92 100644 --- a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/gke.tf +++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/gke.tf @@ -44,15 +44,17 @@ module "clusters" { } module "cluster_nodepools" { - for_each = var.clusters_config - source = "../../../modules/gke-nodepool" - project_id = module.fleet_project.project_id - cluster_name = module.clusters[each.key].name - location = var.region - name = "nodepool-${each.key}" - node_count = { initial = 1 } - service_account = {} - tags = ["${each.key}-node"] + for_each = var.clusters_config + source = "../../../modules/gke-nodepool" + project_id = module.fleet_project.project_id + cluster_name = module.clusters[each.key].name + location = var.region + name = "nodepool-${each.key}" + node_count = { initial = 1 } + service_account = { + create = true + } + tags = ["${each.key}-node"] } module "hub" { diff --git a/blueprints/networking/shared-vpc-gke/main.tf b/blueprints/networking/shared-vpc-gke/main.tf index 9d141acc..59d07d2d 100644 --- a/blueprints/networking/shared-vpc-gke/main.tf +++ b/blueprints/networking/shared-vpc-gke/main.tf @@ -219,11 +219,13 @@ module "cluster-1" { } module "cluster-1-nodepool-1" { - source = "../../../modules/gke-nodepool" - count = var.cluster_create ? 1 : 0 - name = "nodepool-1" - project_id = module.project-svc-gke.project_id - location = module.cluster-1.0.location - cluster_name = module.cluster-1.0.name - service_account = {} + source = "../../../modules/gke-nodepool" + count = var.cluster_create ? 1 : 0 + name = "nodepool-1" + project_id = module.project-svc-gke.project_id + location = module.cluster-1.0.location + cluster_name = module.cluster-1.0.name + service_account = { + create = true + } } diff --git a/modules/gke-hub/README.md b/modules/gke-hub/README.md index 2573ac9d..1a3c547c 100644 --- a/modules/gke-hub/README.md +++ b/modules/gke-hub/README.md @@ -257,7 +257,7 @@ module "cluster_1_nodepool" { location = "europe-west1" name = "nodepool" node_count = { initial = 1 } - service_account = {} + service_account = { create = true } tags = ["cluster-1-node"] } @@ -292,7 +292,7 @@ module "cluster_2_nodepool" { location = "europe-west4" name = "nodepool" node_count = { initial = 1 } - service_account = {} + service_account = { create = true } tags = ["cluster-2-node"] } diff --git a/modules/gke-nodepool/README.md b/modules/gke-nodepool/README.md index d464656f..4c471c60 100644 --- a/modules/gke-nodepool/README.md +++ b/modules/gke-nodepool/README.md @@ -21,7 +21,13 @@ module "cluster-1-nodepool-1" { ### Internally managed service account -To have the module auto-create a service account for the nodes, define the `service_account` variable without setting its `email` attribute. You can then specify service account scopes, or use the default. The service account resource and email (in both plain and IAM formats) are then available in outputs to assign IAM roles from your own code. +There are three different approaches to defining the nodes service account, all depending on the `service_account` variable where the `create` attribute controls creation of a new service account by this module, and the `email` attribute controls the actual service account to use. + +If you create a new service account, its resource and email (in both plain and IAM formats) are then available in outputs to reference it in other modules or resources. + +#### GCE default service account + +To use the GCE default service account, you can ignore the variable which is equivalent to `{ create = null, email = null }`. ```hcl module "cluster-1-nodepool-1" { @@ -30,7 +36,44 @@ module "cluster-1-nodepool-1" { cluster_name = "cluster-1" location = "europe-west1-b" name = "nodepool-1" - service_account = {} +} +# tftest modules=1 resources=1 +``` + +#### Externally defined service account + +To use an existing service account, pass in just the `email` attribute. + +```hcl +module "cluster-1-nodepool-1" { + source = "./fabric/modules/gke-nodepool" + project_id = "myproject" + cluster_name = "cluster-1" + location = "europe-west1-b" + name = "nodepool-1" + service_account = { + email = "foo-bar@myproject.iam.gserviceaccount.com" + } +} +# tftest modules=1 resources=1 +``` + +#### Auto-created service account + +To have the module create a service account, set the `create` attribute to `true` and optionally pass the desired account id in `email`. + +```hcl +module "cluster-1-nodepool-1" { + source = "./fabric/modules/gke-nodepool" + project_id = "myproject" + cluster_name = "cluster-1" + location = "europe-west1-b" + name = "nodepool-1" + service_account = { + create = true + # optional + email = "spam-eggs" + } } # tftest modules=1 resources=2 ``` @@ -53,10 +96,10 @@ module "cluster-1-nodepool-1" { | [nodepool_config](variables.tf#L109) | Nodepool-level configuration. | object({…}) | | null | | [pod_range](variables.tf#L131) | Pod secondary range configuration. | object({…}) | | null | | [reservation_affinity](variables.tf#L148) | Configuration of the desired reservation which instances could take capacity from. | object({…}) | | null | -| [service_account](variables.tf#L158) | Nodepool service account. If this variable is set to null, the default GCE service account will be used. If set and email is null, a service account will be created. If scopes are null a default will be used. | object({…}) | | null | -| [sole_tenant_nodegroup](variables.tf#L167) | Sole tenant node group. | string | | null | -| [tags](variables.tf#L173) | Network tags applied to nodes. | list(string) | | null | -| [taints](variables.tf#L179) | Kubernetes taints applied to all nodes. | list(object({…})) | | null | +| [service_account](variables.tf#L158) | Nodepool service account. If this variable is set to null, the default GCE service account will be used. If set and email is null, a service account will be created. If scopes are null a default will be used. | object({…}) | | {} | +| [sole_tenant_nodegroup](variables.tf#L169) | Sole tenant node group. | string | | null | +| [tags](variables.tf#L175) | Network tags applied to nodes. | list(string) | | null | +| [taints](variables.tf#L181) | Kubernetes taints applied to all nodes. | list(object({…})) | | null | ## Outputs diff --git a/modules/gke-nodepool/main.tf b/modules/gke-nodepool/main.tf index 6a3714f0..0c35c8d0 100644 --- a/modules/gke-nodepool/main.tf +++ b/modules/gke-nodepool/main.tf @@ -31,17 +31,14 @@ locals { ) # if no attributes passed for service account, use the GCE default # if no email specified, create service account - service_account_create = ( - var.service_account != null && try(var.service_account.email, null) == null - ) service_account_email = ( - local.service_account_create + var.service_account.create ? google_service_account.service_account[0].email - : try(var.service_account.email, null) + : var.service_account.email ) service_account_scopes = ( - try(var.service_account.scopes, null) != null - ? var.service_account.scopes + var.service_account.oauth_scopes != null + ? var.service_account.oauth_scopes : [ "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/logging.write", @@ -60,9 +57,13 @@ locals { } resource "google_service_account" "service_account" { - count = local.service_account_create ? 1 : 0 - project = var.project_id - account_id = "tf-gke-${var.name}" + count = var.service_account.create ? 1 : 0 + project = var.project_id + account_id = ( + var.service_account.email != null + ? split("@", var.service_account.email)[0] + : "tf-gke-${var.name}" + ) display_name = "Terraform GKE ${var.cluster_name} ${var.name}." } diff --git a/modules/gke-nodepool/variables.tf b/modules/gke-nodepool/variables.tf index dec5b823..15c8a151 100644 --- a/modules/gke-nodepool/variables.tf +++ b/modules/gke-nodepool/variables.tf @@ -158,10 +158,12 @@ variable "reservation_affinity" { variable "service_account" { description = "Nodepool service account. If this variable is set to null, the default GCE service account will be used. If set and email is null, a service account will be created. If scopes are null a default will be used." type = object({ - email = optional(string) - oauth_scopes = optional(list(string)) + create = optional(bool, false) + email = optional(string, null) + oauth_scopes = optional(list(string), null) }) - default = null + default = {} + nullable = false } variable "sole_tenant_nodegroup" { diff --git a/tests/modules/gke_nodepool/fixture/main.tf b/tests/modules/gke_nodepool/fixture/main.tf index aaa030b9..4ee27482 100644 --- a/tests/modules/gke_nodepool/fixture/main.tf +++ b/tests/modules/gke_nodepool/fixture/main.tf @@ -14,22 +14,31 @@ * limitations under the License. */ +resource "google_service_account" "test" { + project = "my-project" + account_id = "gke-nodepool-test" + display_name = "Test Service Account" +} + module "test" { - source = "../../../../modules/gke-nodepool" - project_id = "my-project" - cluster_name = "cluster-1" - location = "europe-west1-b" - name = "nodepool-1" - gke_version = var.gke_version - labels = var.labels - max_pods_per_node = var.max_pods_per_node - node_config = var.node_config - node_count = var.node_count - node_locations = var.node_locations - nodepool_config = var.nodepool_config - pod_range = var.pod_range - reservation_affinity = var.reservation_affinity - service_account = var.service_account + source = "../../../../modules/gke-nodepool" + project_id = "my-project" + cluster_name = "cluster-1" + location = "europe-west1-b" + name = "nodepool-1" + gke_version = var.gke_version + labels = var.labels + max_pods_per_node = var.max_pods_per_node + node_config = var.node_config + node_count = var.node_count + node_locations = var.node_locations + nodepool_config = var.nodepool_config + pod_range = var.pod_range + reservation_affinity = var.reservation_affinity + service_account = { + create = var.service_account_create + email = google_service_account.test.email + } sole_tenant_nodegroup = var.sole_tenant_nodegroup tags = var.tags taints = var.taints diff --git a/tests/modules/gke_nodepool/fixture/variables.tf b/tests/modules/gke_nodepool/fixture/variables.tf index 420b9eb0..18376ec5 100644 --- a/tests/modules/gke_nodepool/fixture/variables.tf +++ b/tests/modules/gke_nodepool/fixture/variables.tf @@ -65,9 +65,9 @@ variable "reservation_affinity" { default = null } -variable "service_account" { - type = any - default = null +variable "service_account_create" { + type = bool + default = false } variable "sole_tenant_nodegroup" { diff --git a/tests/modules/gke_nodepool/test_plan.py b/tests/modules/gke_nodepool/test_plan.py index fd63f332..75d1cc14 100644 --- a/tests/modules/gke_nodepool/test_plan.py +++ b/tests/modules/gke_nodepool/test_plan.py @@ -21,9 +21,9 @@ def test_defaults(plan_runner): def test_service_account(plan_runner): - _, resources = plan_runner(service_account='{email="foo@example.org"}') + _, resources = plan_runner() assert len(resources) == 1 - _, resources = plan_runner(service_account='{}') + _, resources = plan_runner(service_account_create='true') assert len(resources) == 2 assert 'google_service_account' in [r['type'] for r in resources] From 3dc7b5dcdf5f080b69f610a75e42eeca51a72c17 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Thu, 27 Oct 2022 18:03:24 +0200 Subject: [PATCH 24/27] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c43d3fda..fb3484be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. ### BLUEPRINTS +- [[#924](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/924)] Fix formatting for gcloud dataflow job launch command ([aymanfarhat](https://github.com/aymanfarhat)) +- [[#921](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/921)] Align documentation, move glb blueprint ([ludoo](https://github.com/ludoo)) - [[#915](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/915)] TFE OIDC with GCP WIF blueprint added ([averbuks](https://github.com/averbuks)) - [[#899](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/899)] Static routes monitoring metrics added to network dashboard BP ([maunope](https://github.com/maunope)) - [[#909](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/909)] GCS2BQ: Move images and templates in sub-folders ([lcaggio](https://github.com/lcaggio)) @@ -41,6 +43,7 @@ All notable changes to this project will be documented in this file. ### DOCUMENTATION +- [[#921](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/921)] Align documentation, move glb blueprint ([ludoo](https://github.com/ludoo)) - [[#898](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/898)] Update FAST bootstrap README.md ([juliocc](https://github.com/juliocc)) - [[#878](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/878)] chore: update cft and fabric ([bharathkkb](https://github.com/bharathkkb)) - [[#863](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/863)] Fabric vs CFT doc ([ludoo](https://github.com/ludoo)) @@ -68,6 +71,7 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#923](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/923)] Fix service account creation error in gke nodepool module ([ludoo](https://github.com/ludoo)) - [[#908](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/908)] GKE module: autopilot fixes ([ludoo](https://github.com/ludoo)) - [[#906](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/906)] GKE module: add managed_prometheus to features ([apichick](https://github.com/apichick)) - [[#916](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/916)] Add support for DNS routing policies ([juliocc](https://github.com/juliocc)) From 74b17703cdd22fca7550675f020cf218b68ef9bd Mon Sep 17 00:00:00 2001 From: Krzysztof Mucha Date: Thu, 27 Oct 2022 19:54:00 +0200 Subject: [PATCH 25/27] Add support for deployment type and api proxy type for Apigee organization --- modules/apigee-organization/README.md | 59 ++++++++++++------- modules/apigee-organization/main.tf | 16 ++++- modules/apigee-organization/variables.tf | 15 ++++- .../apigee_organization/fixture/main.tf | 15 +++-- .../modules/apigee_organization/test_plan.py | 18 +++++- 5 files changed, 91 insertions(+), 32 deletions(-) diff --git a/modules/apigee-organization/README.md b/modules/apigee-organization/README.md index eceb4d13..150553a1 100644 --- a/modules/apigee-organization/README.md +++ b/modules/apigee-organization/README.md @@ -13,10 +13,16 @@ module "apigee-organization" { analytics_region = "us-central1" runtime_type = "CLOUD" authorized_network = "my-vpc" - apigee_environments = [ - "eval1", - "eval2" - ] + apigee_environments = { + eval1 = { + api_proxy_type = "PROGRAMMABLE" + deployment_type = "PROXY" + } + eval2 = { + api_proxy_type = "CONFIGURABLE" + deployment_type = "ARCHIVE" + } + } apigee_envgroups = { eval = { environments = [ @@ -42,12 +48,18 @@ module "apigee-organization" { runtime_type = "CLOUD" authorized_network = "my-vpc" database_encryption_key = "my-data-key" - apigee_environments = [ - "dev1", - "dev2", - "test1", - "test2" - ] + apigee_environments = { + dev1 = { + api_proxy_type = "PROGRAMMABLE" + deployment_type = "PROXY" + } + dev2 = { + api_proxy_type = "CONFIGURABLE" + deployment_type = "ARCHIVE" + } + test1 = {} + test2 = {} + } apigee_envgroups = { dev = { environments = [ @@ -80,10 +92,13 @@ module "apigee-organization" { project_id = "my-project" analytics_region = "us-central1" runtime_type = "HYBRID" - apigee_environments = [ - "eval1", - "eval2" - ] + apigee_environments = { + eval1 = { + api_proxy_type = "PROGRAMMABLE" + deployment_type = "PROXY" + } + eval2 = {} + } apigee_envgroups = { eval = { environments = [ @@ -105,15 +120,15 @@ module "apigee-organization" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [analytics_region](variables.tf#L17) | Analytics Region for the Apigee Organization (immutable). See https://cloud.google.com/apigee/docs/api-platform/get-started/install-cli. | string | ✓ | | -| [project_id](variables.tf#L61) | Project ID to host this Apigee organization (will also become the Apigee Org name). | string | ✓ | | -| [runtime_type](variables.tf#L66) | Apigee runtime type. Must be `CLOUD` or `HYBRID`. | string | ✓ | | +| [project_id](variables.tf#L72) | Project ID to host this Apigee organization (will also become the Apigee Org name). | string | ✓ | | +| [runtime_type](variables.tf#L77) | Apigee runtime type. Must be `CLOUD` or `HYBRID`. | string | ✓ | | | [apigee_envgroups](variables.tf#L22) | Apigee Environment Groups. | map(object({…})) | | {} | -| [apigee_environments](variables.tf#L31) | Apigee Environment Names. | list(string) | | [] | -| [authorized_network](variables.tf#L37) | VPC network self link (requires service network peering enabled (Used in Apigee X only). | string | | null | -| [billing_type](variables.tf#L75) | Billing type of the Apigee organization. | string | | null | -| [database_encryption_key](variables.tf#L43) | Cloud KMS key self link (e.g. `projects/foo/locations/us/keyRings/bar/cryptoKeys/baz`) used for encrypting the data that is stored and replicated across runtime instances (immutable, used in Apigee X only). | string | | null | -| [description](variables.tf#L49) | Description of the Apigee Organization. | string | | "Apigee Organization created by tf module" | -| [display_name](variables.tf#L55) | Display Name of the Apigee Organization. | string | | null | +| [apigee_environments](variables.tf#L31) | Apigee Environment Names. | map(object({…})) | | {} | +| [authorized_network](variables.tf#L48) | VPC network self link (requires service network peering enabled (Used in Apigee X only). | string | | null | +| [billing_type](variables.tf#L86) | Billing type of the Apigee organization. | string | | null | +| [database_encryption_key](variables.tf#L54) | Cloud KMS key self link (e.g. `projects/foo/locations/us/keyRings/bar/cryptoKeys/baz`) used for encrypting the data that is stored and replicated across runtime instances (immutable, used in Apigee X only). | string | | null | +| [description](variables.tf#L60) | Description of the Apigee Organization. | string | | "Apigee Organization created by tf module" | +| [display_name](variables.tf#L66) | Display Name of the Apigee Organization. | string | | null | ## Outputs diff --git a/modules/apigee-organization/main.tf b/modules/apigee-organization/main.tf index 148711a9..a498135b 100644 --- a/modules/apigee-organization/main.tf +++ b/modules/apigee-organization/main.tf @@ -15,6 +15,14 @@ */ locals { + env_pairs = flatten([ + for env_name, env in var.apigee_environments : { + api_proxy_type = env.api_proxy_type + deployment_type = env.deployment_type + env_name = env_name + } + ]) + env_envgroup_pairs = flatten([ for eg_name, eg in var.apigee_envgroups : [ for e in eg.environments : { @@ -37,9 +45,11 @@ resource "google_apigee_organization" "apigee_org" { } resource "google_apigee_environment" "apigee_env" { - for_each = toset(var.apigee_environments) - org_id = google_apigee_organization.apigee_org.id - name = each.key + for_each = { for env in local.env_pairs : env.env_name => env } + api_proxy_type = each.value.api_proxy_type + deployment_type = each.value.deployment_type + name = each.key + org_id = google_apigee_organization.apigee_org.id } resource "google_apigee_envgroup" "apigee_envgroup" { diff --git a/modules/apigee-organization/variables.tf b/modules/apigee-organization/variables.tf index b2b3eac9..b3d13e15 100644 --- a/modules/apigee-organization/variables.tf +++ b/modules/apigee-organization/variables.tf @@ -30,8 +30,19 @@ variable "apigee_envgroups" { variable "apigee_environments" { description = "Apigee Environment Names." - type = list(string) - default = [] + type = map(object({ + api_proxy_type = optional(string, "API_PROXY_TYPE_UNSPECIFIED") + deployment_type = optional(string, "DEPLOYMENT_TYPE_UNSPECIFIED") + })) + default = {} + validation { + condition = alltrue([for k, v in var.apigee_environments : contains(["API_PROXY_TYPE_UNSPECIFIED", "PROGRAMMABLE", "CONFIGURABLE"], v.api_proxy_type)]) + error_message = "Allowed values for api_proxy_type \"API_PROXY_TYPE_UNSPECIFIED\", \"PROGRAMMABLE\" or \"CONFIGURABLE\"." + } + validation { + condition = alltrue([for k, v in var.apigee_environments : contains(["DEPLOYMENT_TYPE_UNSPECIFIED", "PROXY", "ARCHIVE"], v.deployment_type)]) + error_message = "Allowed values for deployment_type \"DEPLOYMENT_TYPE_UNSPECIFIED\", \"PROXY\" or \"ARCHIVE\"." + } } variable "authorized_network" { diff --git a/tests/modules/apigee_organization/fixture/main.tf b/tests/modules/apigee_organization/fixture/main.tf index 9dfb49bc..37fa536b 100644 --- a/tests/modules/apigee_organization/fixture/main.tf +++ b/tests/modules/apigee_organization/fixture/main.tf @@ -21,10 +21,17 @@ module "test" { runtime_type = "CLOUD" billing_type = "EVALUATION" authorized_network = var.network - apigee_environments = [ - "eval1", - "eval2" - ] + apigee_environments = { + eval1 = { + api_proxy_type = "PROGRAMMABLE" + deployment_type = "PROXY" + } + eval2 = { + api_proxy_type = "CONFIGURABLE" + deployment_type = "ARCHIVE" + } + eval3 = {} + } apigee_envgroups = { eval = { environments = [ diff --git a/tests/modules/apigee_organization/test_plan.py b/tests/modules/apigee_organization/test_plan.py index ec2312c9..6e873bc0 100644 --- a/tests/modules/apigee_organization/test_plan.py +++ b/tests/modules/apigee_organization/test_plan.py @@ -23,7 +23,7 @@ def resources(plan_runner): def test_resource_count(resources): "Test number of resources created." - assert len(resources) == 6 + assert len(resources) == 7 def test_envgroup_attachment(resources): @@ -42,3 +42,19 @@ def test_envgroup(resources): assert envgroups[0]['name'] == 'eval' assert len(envgroups[0]['hostnames']) == 1 assert envgroups[0]['hostnames'][0] == 'eval.api.example.com' + + +def test_env(resources): + "Test environments." + envs = [r['values'] for r in resources if r['type'] + == 'google_apigee_environment'] + assert len(envs) == 3 + assert envs[0]['name'] == 'eval1' + assert envs[0]['api_proxy_type'] == 'PROGRAMMABLE' + assert envs[0]['deployment_type'] == 'PROXY' + assert envs[1]['name'] == 'eval2' + assert envs[1]['api_proxy_type'] == 'CONFIGURABLE' + assert envs[1]['deployment_type'] == 'ARCHIVE' + assert envs[2]['name'] == 'eval3' + assert envs[2]['api_proxy_type'] == 'API_PROXY_TYPE_UNSPECIFIED' + assert envs[2]['deployment_type'] == 'DEPLOYMENT_TYPE_UNSPECIFIED' From 29cde275f0cc0c889717a7d71c68b01d3e9ab3ee Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Fri, 28 Oct 2022 08:13:04 +0200 Subject: [PATCH 26/27] fix backwards compatibility for vpc subnet descriptions (#926) --- modules/net-vpc/README.md | 4 ++-- modules/net-vpc/subnets.tf | 32 +++++++++++++++++++------------- modules/net-vpc/variables.tf | 1 + 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index 84377bd8..0d6a231e 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -276,8 +276,8 @@ flow_logs: # enable, set to empty map to use defaults | [subnet_iam](variables.tf#L133) | Subnet IAM bindings in {REGION/NAME => {ROLE => [MEMBERS]} format. | map(map(list(string))) | | {} | | [subnets](variables.tf#L139) | Subnet configuration. | list(object({…})) | | [] | | [subnets_proxy_only](variables.tf#L164) | List of proxy-only subnets for Regional HTTPS or Internal HTTPS load balancers. Note: Only one proxy-only subnet for each VPC network in each region can be active. | list(object({…})) | | [] | -| [subnets_psc](variables.tf#L176) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] | -| [vpc_create](variables.tf#L186) | Create VPC. When set to false, uses a data source to reference existing VPC. | bool | | true | +| [subnets_psc](variables.tf#L176) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] | +| [vpc_create](variables.tf#L187) | Create VPC. When set to false, uses a data source to reference existing VPC. | bool | | true | ## Outputs diff --git a/modules/net-vpc/subnets.tf b/modules/net-vpc/subnets.tf index 0496405b..ae094ecf 100644 --- a/modules/net-vpc/subnets.tf +++ b/modules/net-vpc/subnets.tf @@ -72,13 +72,17 @@ locals { } resource "google_compute_subnetwork" "subnetwork" { - for_each = local.subnets - project = var.project_id - network = local.network.name - name = each.value.name - region = each.value.region - ip_cidr_range = each.value.ip_cidr_range - description = try(each.value.description, "Terraform-managed.") + for_each = local.subnets + project = var.project_id + network = local.network.name + name = each.value.name + region = each.value.region + ip_cidr_range = each.value.ip_cidr_range + description = ( + each.value.description == null + ? "Terraform-managed." + : each.value.description + ) private_ip_google_access = each.value.enable_private_access secondary_ip_range = each.value.secondary_ip_ranges == null ? [] : [ for name, range in each.value.secondary_ip_ranges : @@ -107,9 +111,10 @@ resource "google_compute_subnetwork" "proxy_only" { name = each.value.name region = each.value.region ip_cidr_range = each.value.ip_cidr_range - description = try( - each.value.description, - "Terraform-managed proxy-only subnet for Regional HTTPS or Internal HTTPS LB." + description = ( + each.value.description == null + ? "Terraform-managed proxy-only subnet for Regional HTTPS or Internal HTTPS LB." + : each.value.description ) purpose = "REGIONAL_MANAGED_PROXY" role = ( @@ -124,9 +129,10 @@ resource "google_compute_subnetwork" "psc" { name = each.value.name region = each.value.region ip_cidr_range = each.value.ip_cidr_range - description = try( - each.value.description, - "Terraform-managed subnet for Private Service Connect (PSC NAT)." + description = ( + each.value.description == null + ? "Terraform-managed subnet for Private Service Connect (PSC NAT)." + : each.value.description ) purpose = "PRIVATE_SERVICE_CONNECT" } diff --git a/modules/net-vpc/variables.tf b/modules/net-vpc/variables.tf index 89207479..a7aa2077 100644 --- a/modules/net-vpc/variables.tf +++ b/modules/net-vpc/variables.tf @@ -179,6 +179,7 @@ variable "subnets_psc" { name = string ip_cidr_range = string region = string + description = optional(string) })) default = [] } From 2267384a8d81a414b9ed6d62c1388eaeaf011510 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Fri, 28 Oct 2022 08:30:17 +0200 Subject: [PATCH 27/27] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb3484be..65841252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,8 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#926](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/926)] Fix backwards compatibility for vpc subnet descriptions ([ludoo](https://github.com/ludoo)) +- [[#927](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/927)] Add support for deployment type and api proxy type for Apigee org ([kmucha555](https://github.com/kmucha555)) - [[#923](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/923)] Fix service account creation error in gke nodepool module ([ludoo](https://github.com/ludoo)) - [[#908](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/908)] GKE module: autopilot fixes ([ludoo](https://github.com/ludoo)) - [[#906](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/906)] GKE module: add managed_prometheus to features ([apichick](https://github.com/apichick))