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: {}