From f548b65b1c72c62bcd11a686b61a113cc0cbdb71 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Thu, 7 Dec 2023 10:07:48 +0100 Subject: [PATCH] Add support for subnet-level service network user grants to project module, improve docs (#1907) * improve project factory example * light refactor of project modules shared vpc internals and docs * add support for subnet-level grants on host project --- .../factories/project-factory/README.md | 19 ++++-- .../factories/project-factory/factory.tf | 6 +- .../factories/project-factory/variables.tf | 7 +- modules/project/README.md | 65 +++++++++++++++--- modules/project/shared-vpc.tf | 65 ++++++++++++++---- modules/project/variables.tf | 7 +- .../project_factory/examples/example.yaml | 28 +++++++- .../examples/shared-vpc-subnet-grants.yaml | 66 +++++++++++++++++++ 8 files changed, 226 insertions(+), 37 deletions(-) create mode 100644 tests/modules/project/examples/shared-vpc-subnet-grants.yaml diff --git a/blueprints/factories/project-factory/README.md b/blueprints/factories/project-factory/README.md index 2e67158277..231ca8dc03 100644 --- a/blueprints/factories/project-factory/README.md +++ b/blueprints/factories/project-factory/README.md @@ -57,7 +57,7 @@ module "project-factory" { # location where the yaml files are read from factory_data_path = "data" } -# tftest modules=7 resources=26 files=prj-app-1,prj-app-2,prj-app-3 inventory=example.yaml +# tftest modules=7 resources=31 files=prj-app-1,prj-app-2,prj-app-3 inventory=example.yaml ``` ```yaml @@ -92,10 +92,19 @@ service_accounts: app-2-be: {} services: - compute.googleapis.com +- container.googleapis.com - run.googleapis.com - storage.googleapis.com shared_vpc_service_config: host_project: foo-host + service_identity_iam: + "roles/compute.networkUser": + - cloudservices + - container-engine + "roles/vpcaccess.user": + - cloudrun + "roles/container.hostServiceAgentUser": + - container-engine # tftest-file id=prj-app-2 path=data/prj-app-2.yaml ``` @@ -113,10 +122,10 @@ services: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [factory_data_path](variables.tf#L88) | Path to folder with YAML project description data files. | string | ✓ | | -| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | object({…}) | | {} | -| [data_merges](variables.tf#L46) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | -| [data_overrides](variables.tf#L66) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | +| [factory_data_path](variables.tf#L89) | Path to folder with YAML project description data files. | string | ✓ | | +| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | object({…}) | | {} | +| [data_merges](variables.tf#L47) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | +| [data_overrides](variables.tf#L67) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | ## Outputs diff --git a/blueprints/factories/project-factory/factory.tf b/blueprints/factories/project-factory/factory.tf index eabb551ad1..b8da24a49e 100644 --- a/blueprints/factories/project-factory/factory.tf +++ b/blueprints/factories/project-factory/factory.tf @@ -78,7 +78,11 @@ locals { shared_vpc_service_config = ( try(v.shared_vpc_service_config, null) != null ? merge( - { service_identity_iam = {}, service_iam_grants = [] }, + { + service_identity_iam = {} + service_identity_subnet_iam = {} + service_iam_grants = [] + }, v.shared_vpc_service_config ) : var.data_defaults.shared_vpc_service_config diff --git a/blueprints/factories/project-factory/variables.tf b/blueprints/factories/project-factory/variables.tf index a0ff81ebd3..18b315d6d8 100644 --- a/blueprints/factories/project-factory/variables.tf +++ b/blueprints/factories/project-factory/variables.tf @@ -28,9 +28,10 @@ variable "data_defaults" { service_perimeter_standard = optional(string) services = optional(list(string), []) shared_vpc_service_config = optional(object({ - host_project = string - service_identity_iam = optional(map(list(string)), {}) - service_iam_grants = optional(list(string), []) + host_project = string + service_identity_iam = optional(map(list(string)), {}) + service_identity_subnet_iam = optional(map(list(string)), {}) + service_iam_grants = optional(list(string), []) }), { host_project = null }) tag_bindings = optional(map(string), {}) # non-project resources diff --git a/modules/project/README.md b/modules/project/README.md index d253e85586..96d0e20379 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -228,9 +228,21 @@ This table lists all affected services and roles that you need to grant to servi ## Shared VPC -The module allows managing Shared VPC status for both hosts and service projects, and includes a simple way of assigning Shared VPC roles to service identities. +The module allows managing Shared VPC status for both hosts and service projects, and control of IAM bindings for API service identities. -You can enable Shared VPC Host at the project level and manage project service association independently. +Project service association for VPC host projects can be + +- autoritatively managed in the host project by enabling Shared VPC and specifying the set of service projects, or +- additively managed in service projects by by enabling Shared VPC in the host project and then "attaching" each service project independently + +IAM bindings in the host project for API service identities can be managed from service projects in two different ways: + +- via the `service_identity_iam` attribute, by specifying the set of roles and service agents +- via the `service_iam_grants` attribute that leverages a [fixed list of roles for each service](./sharedvpc-agent-iam.yaml), by specifying a list of services + +While the first method is more explicit and readable, the second method is simpler and less error prone as all appropriate roles are predefined for all required service agents (eg compute and cloud services). You can mix and match as the two sets of bindings are then internally combined. + +This example shows a simple configuration with a host project, and a service project independently attached with granular IAM bindings for service identities. ```hcl module "host-project" { @@ -272,7 +284,7 @@ module "service-project" { # tftest modules=2 resources=10 inventory=shared-vpc.yaml e2e ``` -The module allows also granting necessary permissions in host project to service identities by specifying which services will be used in service project in `grant_iam_for_services`. +This example shows a similar configuration, with the simpler way of defining IAM bindings for service identities. The list of services passed to `service_iam_grants` uses the same module's outputs to establish a dependency, as service identities are only typically available after service (API) activation. ```hcl module "host-project" { @@ -296,13 +308,47 @@ module "service-project" { "container.googleapis.com", ] shared_vpc_service_config = { - host_project = module.host-project.project_id + host_project = module.host-project.project_id + # reuse the list of services from the module's outputs service_iam_grants = module.service-project.services } } # tftest modules=2 resources=9 inventory=shared-vpc-auto-grants.yaml e2e ``` +In specific cases it might make sense to selectively grant the `compute.networkUser` role for service identities at the subnet level, and while that is best done via org policies it's also supported by this module. + +```hcl +module "host-project" { + source = "./fabric/modules/project" + billing_account = var.billing_account_id + name = "host" + parent = var.folder_id + prefix = var.prefix + shared_vpc_host_config = { + enabled = true + } +} + +module "service-project" { + source = "./fabric/modules/project" + billing_account = var.billing_account_id + name = "service" + parent = var.folder_id + prefix = var.prefix + services = [ + "compute.googleapis.com", + ] + shared_vpc_service_config = { + host_project = module.host-project.project_id + service_identity_subnet_iam = { + "europe-west1/gce" = ["compute"] + } + } +} +# tftest modules=2 resources=6 inventory=shared-vpc-subnet-grants.yaml +``` + ## Organization Policies To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project. @@ -617,7 +663,7 @@ output "compute_robot" { ### Managing project related configuration without creating it -The module offers managing all related resources without ever touching the project itself by using `project_create = false` +The module offers managing all related resources without ever touching the project itself by using `project_create = false` ```hcl module "create-project" { @@ -827,7 +873,6 @@ module "bucket" { # tftest modules=7 resources=53 inventory=data.yaml e2e ``` - ## Files @@ -840,7 +885,7 @@ module "bucket" { | [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_default_service_accounts · google_project_iam_member · google_project_service_identity | -| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_project_iam_member | +| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_compute_subnetwork_iam_member · google_project_iam_member | | [tags.tf](./tags.tf) | None | google_tags_tag_binding | | [variables.tf](./variables.tf) | Module variables. | | | [versions.tf](./versions.tf) | Version pins. | | @@ -879,9 +924,9 @@ module "bucket" { | [service_perimeter_standard](variables.tf#L276) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | | [services](variables.tf#L282) | Service APIs to enable. | list(string) | | [] | | [shared_vpc_host_config](variables.tf#L288) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L297) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L319) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | -| [tag_bindings](variables.tf#L325) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | +| [shared_vpc_service_config](variables.tf#L297) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L320) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L326) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/project/shared-vpc.tf b/modules/project/shared-vpc.tf index d728f42b2e..91a56a90dc 100644 --- a/modules/project/shared-vpc.tf +++ b/modules/project/shared-vpc.tf @@ -17,22 +17,27 @@ # tfdoc:file:description Shared VPC project-level configuration. locals { - _shared_vpc_agent_config = yamldecode(file("${path.module}/sharedvpc-agent-iam.yaml")) - _shared_vpc_agent_config_filtered = [ - for config in local._shared_vpc_agent_config : config - if contains(var.shared_vpc_service_config.service_iam_grants, config.service) + _svpc = var.shared_vpc_service_config + # read the list of service/roles for API service agents + _svpc_agent_config = yamldecode(file( + "${path.module}/sharedvpc-agent-iam.yaml" + )) + # filter the list and keep services for which we need to create IAM bindings + _svpc_agent_config_filtered = [ + for v in local._svpc_agent_config : v + if contains(local._svpc.service_iam_grants, v.service) ] - _shared_vpc_agent_grants = flatten(flatten([ - for api in local._shared_vpc_agent_config_filtered : [ - for service, roles in api.agents : [ + # normalize the list of service/role tuples + _svpc_agent_grants = flatten(flatten([ + for v in local._svpc_agent_config_filtered : [ + for service, roles in v.agents : [ for role in roles : { role = role, service = service } ] ] ])) - - # compute the host project IAM bindings for this project's service identities + # normalize the service identity IAM bindings directly defined by the user _svpc_service_iam = flatten([ - for role, services in var.shared_vpc_service_config.service_identity_iam : [ + for role, services in local._svpc.service_identity_iam : [ for service in services : { role = role, service = service } ] ]) @@ -44,9 +49,24 @@ locals { try(var.shared_vpc_host_config.service_projects, null), [] ) } - + # combine the two sets of service/role bindings defined above svpc_service_iam = { - for b in setunion(local._svpc_service_iam, local._shared_vpc_agent_grants) : "${b.role}:${b.service}" => b + for b in setunion(local._svpc_service_iam, local._svpc_agent_grants) : + "${b.role}:${b.service}" => b + } + # normalize the service identity subnet IAM bindings + _svpc_service_subnet_iam = flatten([ + for subnet, services in local._svpc.service_identity_subnet_iam : [ + for service in services : [{ + region = split("/", subnet)[0] + subnet = split("/", subnet)[1] + service = service + }] + ] + ]) + svpc_service_subnet_iam = { + for v in local._svpc_service_subnet_iam : + "${v.region}:${v.subnet}:${v.service}" => v } } @@ -90,3 +110,24 @@ resource "google_project_iam_member" "shared_vpc_host_robots" { data.google_storage_project_service_account.gcs_sa, ] } + +resource "google_compute_subnetwork_iam_member" "shared_vpc_host_robots" { + for_each = local.svpc_service_subnet_iam + project = var.shared_vpc_service_config.host_project + region = each.value.region + subnetwork = each.value.subnet + role = "roles/compute.networkUser" + member = ( + each.value.service == "cloudservices" + ? "serviceAccount:${local.service_account_cloud_services}" + : "serviceAccount:${local.service_accounts_robots[each.value.service]}" + ) + depends_on = [ + google_project_service.project_services, + google_project_service_identity.servicenetworking, + google_project_service_identity.jit_si, + google_project_default_service_accounts.default_service_accounts, + data.google_bigquery_default_service_account.bq_sa, + data.google_storage_project_service_account.gcs_sa, + ] +} diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 3a1a8eff2d..1bf26a32f4 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -298,9 +298,10 @@ variable "shared_vpc_service_config" { description = "Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config)." # the list of valid service identities is in service-agents.yaml type = object({ - host_project = string - service_identity_iam = optional(map(list(string)), {}) - service_iam_grants = optional(list(string), []) + host_project = string + service_identity_iam = optional(map(list(string)), {}) + service_identity_subnet_iam = optional(map(list(string)), {}) + service_iam_grants = optional(list(string), []) }) default = { host_project = null diff --git a/tests/blueprints/factories/project_factory/examples/example.yaml b/tests/blueprints/factories/project_factory/examples/example.yaml index 71391a526a..4e636946eb 100644 --- a/tests/blueprints/factories/project_factory/examples/example.yaml +++ b/tests/blueprints/factories/project_factory/examples/example.yaml @@ -102,12 +102,34 @@ values: environment: test team: foo timeouts: null + ? module.project-factory.module.projects["prj-app-2"].google_project_iam_member.shared_vpc_host_robots["roles/compute.networkUser:cloudservices"] + : condition: [] + project: foo-host + role: roles/compute.networkUser + ? module.project-factory.module.projects["prj-app-2"].google_project_iam_member.shared_vpc_host_robots["roles/compute.networkUser:container-engine"] + : condition: [] + project: foo-host + role: roles/compute.networkUser + ? module.project-factory.module.projects["prj-app-2"].google_project_iam_member.shared_vpc_host_robots["roles/container.hostServiceAgentUser:container-engine"] + : condition: [] + project: foo-host + role: roles/container.hostServiceAgentUser + ? module.project-factory.module.projects["prj-app-2"].google_project_iam_member.shared_vpc_host_robots["roles/vpcaccess.user:cloudrun"] + : condition: [] + project: foo-host + role: roles/vpcaccess.user module.project-factory.module.projects["prj-app-2"].google_project_service.project_services["compute.googleapis.com"]: disable_dependent_services: false disable_on_destroy: false project: test-pf-prj-app-2 service: compute.googleapis.com timeouts: null + module.project-factory.module.projects["prj-app-2"].google_project_service.project_services["container.googleapis.com"]: + disable_dependent_services: false + disable_on_destroy: false + project: test-pf-prj-app-2 + service: container.googleapis.com + timeouts: null module.project-factory.module.projects["prj-app-2"].google_project_service.project_services["run.googleapis.com"]: disable_dependent_services: false disable_on_destroy: false @@ -204,11 +226,11 @@ counts: google_essential_contacts_contact: 3 google_kms_crypto_key_iam_member: 1 google_project: 3 - google_project_iam_member: 2 - google_project_service: 10 + google_project_iam_member: 6 + google_project_service: 11 google_service_account: 3 google_storage_project_service_account: 3 modules: 7 - resources: 26 + resources: 31 outputs: {} diff --git a/tests/modules/project/examples/shared-vpc-subnet-grants.yaml b/tests/modules/project/examples/shared-vpc-subnet-grants.yaml new file mode 100644 index 0000000000..f245615fe3 --- /dev/null +++ b/tests/modules/project/examples/shared-vpc-subnet-grants.yaml @@ -0,0 +1,66 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.host-project.google_compute_shared_vpc_host_project.shared_vpc_host[0]: + project: test-host + timeouts: null + module.host-project.google_project.project[0]: + auto_create_network: false + billing_account: 123456-123456-123456 + folder_id: '1122334455' + labels: null + name: test-host + org_id: null + project_id: test-host + skip_delete: false + timeouts: null + module.service-project.google_compute_shared_vpc_service_project.shared_vpc_service[0]: + deletion_policy: null + host_project: test-host + service_project: test-service + timeouts: null + module.service-project.google_compute_subnetwork_iam_member.shared_vpc_host_robots["europe-west1:gce:compute"]: + condition: [] + project: test-host + region: europe-west1 + role: roles/compute.networkUser + subnetwork: gce + module.service-project.google_project.project[0]: + auto_create_network: false + billing_account: 123456-123456-123456 + folder_id: '1122334455' + labels: null + name: test-service + org_id: null + project_id: test-service + skip_delete: false + timeouts: null + module.service-project.google_project_service.project_services["compute.googleapis.com"]: + disable_dependent_services: false + disable_on_destroy: false + project: test-service + service: compute.googleapis.com + timeouts: null + +counts: + google_compute_shared_vpc_host_project: 1 + google_compute_shared_vpc_service_project: 1 + google_compute_subnetwork_iam_member: 1 + google_project: 2 + google_project_service: 1 + modules: 2 + resources: 6 + +outputs: {}