diff --git a/modules/project-factory/README.md b/modules/project-factory/README.md index e93fc6d0..e9b26188 100644 --- a/modules/project-factory/README.md +++ b/modules/project-factory/README.md @@ -390,10 +390,10 @@ update_rules: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [factories_config](variables.tf#L91) | Path to folder with YAML resource description data files. | object({…}) | ✓ | | -| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | object({…}) | | {} | -| [data_merges](variables.tf#L49) | 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#L69) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | +| [factories_config](variables.tf#L96) | Path to folder with YAML resource description data files. | object({…}) | ✓ | | +| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | object({…}) | | {} | +| [data_merges](variables.tf#L52) | 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#L71) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | ## Outputs diff --git a/modules/project-factory/factory-projects.tf b/modules/project-factory/factory-projects.tf index aef73ef0..f03aafaa 100644 --- a/modules/project-factory/factory-projects.tf +++ b/modules/project-factory/factory-projects.tf @@ -82,16 +82,6 @@ locals { try(v.service_encryption_key_ids, null), var.data_defaults.service_encryption_key_ids ) - service_perimeter_bridges = coalesce( - var.data_overrides.service_perimeter_bridges, - try(v.service_perimeter_bridges, null), - var.data_defaults.service_perimeter_bridges - ) - service_perimeter_standard = try(coalesce( - var.data_overrides.service_perimeter_standard, - try(v.service_perimeter_standard, null), - var.data_defaults.service_perimeter_standard - ), null) services = coalesce( var.data_overrides.services, try(v.services, null), @@ -116,6 +106,11 @@ locals { try(v.tag_bindings, null), var.data_defaults.tag_bindings ) + vpc_sc = ( + var.data_overrides.vpc_sc != null + ? var.data_overrides.vpc_sc + : try(v.vpc_sc, var.data_defaults.vpc_sc, null) + ) # non-project resources service_accounts = try(v.service_accounts, {}) }) diff --git a/modules/project-factory/main.tf b/modules/project-factory/main.tf index 0df66a7c..1b42afcc 100644 --- a/modules/project-factory/main.tf +++ b/modules/project-factory/main.tf @@ -77,11 +77,6 @@ module "projects" { each.value.service_encryption_key_ids, var.data_merges.service_encryption_key_ids ) - service_perimeter_bridges = distinct(concat( - each.value.service_perimeter_bridges, - var.data_merges.service_perimeter_bridges - )) - service_perimeter_standard = each.value.service_perimeter_standard services = distinct(concat( each.value.services, var.data_merges.services @@ -91,6 +86,7 @@ module "projects" { each.value.tag_bindings, var.data_merges.tag_bindings ) + vpc_sc = each.value.vpc_sc } module "service-accounts" { diff --git a/modules/project-factory/variables.tf b/modules/project-factory/variables.tf index 437ea7e1..80fb6a96 100644 --- a/modules/project-factory/variables.tf +++ b/modules/project-factory/variables.tf @@ -24,8 +24,6 @@ variable "data_defaults" { parent = optional(string) prefix = optional(string) service_encryption_key_ids = optional(map(list(string)), {}) - service_perimeter_bridges = optional(list(string), []) - service_perimeter_standard = optional(string) services = optional(list(string), []) shared_vpc_service_config = optional(object({ host_project = string @@ -41,6 +39,11 @@ variable "data_defaults" { display_name = optional(string, "Terraform-managed.") iam_self_roles = optional(list(string)) })), {}) + vpc_sc = optional(object({ + perimeter_name = string + perimeter_bridges = optional(list(string), []) + is_dry_run = optional(bool, false) + })) }) nullable = false default = {} @@ -53,7 +56,6 @@ variable "data_merges" { labels = optional(map(string), {}) metric_scopes = optional(list(string), []) service_encryption_key_ids = optional(map(list(string)), {}) - service_perimeter_bridges = optional(list(string), []) services = optional(list(string), []) tag_bindings = optional(map(string), {}) # non-project resources @@ -74,8 +76,6 @@ variable "data_overrides" { parent = optional(string) prefix = optional(string) service_encryption_key_ids = optional(map(list(string))) - service_perimeter_bridges = optional(list(string)) - service_perimeter_standard = optional(string) tag_bindings = optional(map(string)) services = optional(list(string)) # non-project resources @@ -83,6 +83,11 @@ variable "data_overrides" { display_name = optional(string, "Terraform-managed.") iam_self_roles = optional(list(string)) }))) + vpc_sc = optional(object({ + perimeter_name = string + perimeter_bridges = optional(list(string), []) + is_dry_run = optional(bool, false) + })) }) nullable = false default = {} diff --git a/modules/project/README.md b/modules/project/README.md index b590b0c6..75388aef 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -24,6 +24,7 @@ This module implements the creation and management of one GCP project including - [Custom Roles Factory](#custom-roles-factory) - [Quotas](#quotas) - [Quotas factory](#quotas-factory) +- [VPC Service Controls](#vpc-service-controls) - [Outputs](#outputs) - [Managing project related configuration without creating it](#managing-project-related-configuration-without-creating-it) - [Files](#files) @@ -802,6 +803,7 @@ includedPermissions: ## Quotas Project and regional quotas can be managed via the `quotas` variable. Keep in mind, that metrics returned by `gcloud compute regions describe` do not match `quota_id`s. To get a list of quotas in the project use the API call, for example to get quotas for `compute.googleapis.com` use: + ```bash curl -X GET \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ @@ -874,6 +876,52 @@ cpus-ew8: region: europe-west8 ``` +## VPC Service Controls + +This module also allows managing project membership in VPC Service Controls perimeters. When using this functionality care should be taken so that perimeter management (e.g. via the `vpc-sc` module) does not try reconciling resources, to avoid permadiffs and related violations. + +```hcl +module "project" { + source = "./fabric/modules/project" + billing_account = var.billing_account_id + name = "project" + parent = var.folder_id + prefix = var.prefix + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + vpc_sc = { + perimeter_name = "accessPolicies/1234567890/servicePerimeters/default" + } +} +# tftest modules=1 resources=4 inventory=vpc-sc.yaml +``` + +Perimeter bridges and dry run configuration are also supported. + +```hcl +module "project" { + source = "./fabric/modules/project" + billing_account = var.billing_account_id + name = "project" + parent = var.folder_id + prefix = var.prefix + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + vpc_sc = { + perimeter_name = "accessPolicies/1234567890/servicePerimeters/default" + perimeter_bridges = [ + "accessPolicies/1234567890/servicePerimeters/b1", + "accessPolicies/1234567890/servicePerimeters/b2", + ] + is_dry_run = true + } +} +# tftest modules=1 resources=6 +``` ## Outputs @@ -1131,7 +1179,7 @@ module "bucket" { | [variables-tags.tf](./variables-tags.tf) | None | | | [variables.tf](./variables.tf) | Module variables. | | | [versions.tf](./versions.tf) | Version pins. | | -| [vpc-sc.tf](./vpc-sc.tf) | VPC-SC project-level perimeter configuration. | google_access_context_manager_service_perimeter_resource | +| [vpc-sc.tf](./vpc-sc.tf) | VPC-SC project-level perimeter configuration. | google_access_context_manager_service_perimeter_dry_run_resource · google_access_context_manager_service_perimeter_resource | ## Variables @@ -1164,14 +1212,13 @@ module "bucket" { | [quotas](variables-quotas.tf#L17) | Service quota configuration. | map(object({…})) | | {} | | [service_config](variables.tf#L211) | Configure service API activation. | object({…}) | | {…} | | [service_encryption_key_ids](variables.tf#L223) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | -| [service_perimeter_bridges](variables.tf#L230) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | -| [service_perimeter_standard](variables.tf#L237) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | -| [services](variables.tf#L243) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L249) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L258) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L286) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [services](variables.tf#L229) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L235) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L244) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L272) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | | [tag_bindings](variables-tags.tf#L45) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | | [tags](variables-tags.tf#L51) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [vpc_sc](variables.tf#L278) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | ## Outputs diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 94e3959a..4ff0cf93 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -226,20 +226,6 @@ variable "service_encryption_key_ids" { default = {} } -# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME -variable "service_perimeter_bridges" { - description = "Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format." - type = list(string) - default = null -} - -# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME -variable "service_perimeter_standard" { - description = "Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format." - type = string - default = null -} - variable "services" { description = "Service APIs to enable." type = list(string) @@ -288,3 +274,13 @@ variable "skip_delete" { type = bool default = false } + +variable "vpc_sc" { + description = "VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module." + type = object({ + perimeter_name = string + perimeter_bridges = optional(list(string), []) + is_dry_run = optional(bool, false) + }) + default = null +} diff --git a/modules/project/vpc-sc.tf b/modules/project/vpc-sc.tf index edaa2032..cef1d73b 100644 --- a/modules/project/vpc-sc.tf +++ b/modules/project/vpc-sc.tf @@ -16,30 +16,28 @@ # tfdoc:file:description VPC-SC project-level perimeter configuration. -moved { - from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-standard - to = google_access_context_manager_service_perimeter_resource.standard +locals { + vpc_sc_perimeters = compact(concat( + [try(var.vpc_sc.perimeter_name, null)], + try(var.vpc_sc.perimeter_bridges, []) + )) + vpc_sc_dry_run = try(var.vpc_sc.is_dry_run, false) == true } -resource "google_access_context_manager_service_perimeter_resource" "standard" { - count = var.service_perimeter_standard != null ? 1 : 0 - # this needs an additional lifecycle block in the vpc module on the - # google_access_context_manager_service_perimeter resource - perimeter_name = var.service_perimeter_standard - resource = "projects/${local.project.number}" -} +# use only if the vpc-sc module has a lifecycle block to ignore resources -moved { - from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-bridges - to = google_access_context_manager_service_perimeter_resource.bridge -} - -resource "google_access_context_manager_service_perimeter_resource" "bridge" { +resource "google_access_context_manager_service_perimeter_resource" "default" { for_each = toset( - var.service_perimeter_bridges != null ? var.service_perimeter_bridges : [] + local.vpc_sc_dry_run ? [] : local.vpc_sc_perimeters ) - # this needs an additional lifecycle block in the vpc module on the - # google_access_context_manager_service_perimeter resource - perimeter_name = each.value + perimeter_name = each.key + resource = "projects/${local.project.number}" +} + +resource "google_access_context_manager_service_perimeter_dry_run_resource" "default" { + for_each = toset( + local.vpc_sc_dry_run ? local.vpc_sc_perimeters : [] + ) + perimeter_name = each.key resource = "projects/${local.project.number}" } diff --git a/tests/modules/project/examples/vpc-sc-2.yaml b/tests/modules/project/examples/vpc-sc-2.yaml new file mode 100644 index 00000000..c3c22581 --- /dev/null +++ b/tests/modules/project/examples/vpc-sc-2.yaml @@ -0,0 +1,33 @@ +# Copyright 2024 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.project.google_access_context_manager_service_perimeter_dry_run_resource.default["accessPolicies/1234567890/servicePerimeters/b1"] + : perimeter_name: accessPolicies/1234567890/servicePerimeters/b1 + timeouts: null + ? module.project.google_access_context_manager_service_perimeter_dry_run_resource.default["accessPolicies/1234567890/servicePerimeters/b2"] + : perimeter_name: accessPolicies/1234567890/servicePerimeters/b2 + timeouts: null + ? module.project.google_access_context_manager_service_perimeter_dry_run_resource.default["accessPolicies/1234567890/servicePerimeters/default"] + : perimeter_name: accessPolicies/1234567890/servicePerimeters/default + timeouts: null + +counts: + google_access_context_manager_service_perimeter_dry_run_resource: 3 + google_project: 1 + google_project_service: 2 + modules: 1 + resources: 6 + +outputs: {} diff --git a/tests/modules/project/examples/vpc-sc.yaml b/tests/modules/project/examples/vpc-sc.yaml new file mode 100644 index 00000000..2d34d0dc --- /dev/null +++ b/tests/modules/project/examples/vpc-sc.yaml @@ -0,0 +1,27 @@ +# Copyright 2024 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.project.google_access_context_manager_service_perimeter_resource.default["accessPolicies/1234567890/servicePerimeters/default"] + : perimeter_name: accessPolicies/1234567890/servicePerimeters/default + timeouts: null + +counts: + google_access_context_manager_service_perimeter_resource: 1 + google_project: 1 + google_project_service: 2 + modules: 1 + resources: 4 + +outputs: {}