VPC SC module refactor (#3062)

* Remove bridge perimeters

* Update FAST stages

* Allow project ids in perimeter definitions

* Preserve order order for ingress/egress policies

* Use CAI

* Use CAI

* Fix tests
This commit is contained in:
Julio Castillo
2025-05-09 14:37:03 +02:00
committed by GitHub
parent ac2193082d
commit 7ceb814986
14 changed files with 276 additions and 296 deletions

View File

@@ -1,4 +1,4 @@
# Copyright 2024 Google LLC
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -125,7 +125,7 @@ module "vpc-sc" {
access_levels = var.vpc_sc_access_levels
egress_policies = var.vpc_sc_egress_policies
ingress_policies = merge(var.vpc_sc_ingress_policies, local._sink_ingress_policies)
service_perimeters_regular = {
perimeters = {
shielded = {
# Move `spec` definition to `status` and comment `use_explicit_dry_run_spec` variable to enforce VPC-SC configuration
# Before enforcing configuration check logs and create Access Level, Ingress/Egress policy as needed

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2024 Google LLC
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -569,7 +569,7 @@ module "vpc_sc" {
}
}
}
service_perimeters_regular = {
perimeters = {
cloudrun = {
status = {
resources = [

View File

@@ -318,6 +318,6 @@ Some references that might be useful in setting up this stage:
| name | description | sensitive | consumers |
|---|---|:---:|---|
| [tfvars](outputs.tf#L43) | Terraform variable files for the following stages. | ✓ | |
| [vpc_sc_perimeter_default](outputs.tf#L49) | Raw default perimeter resource. | ✓ | |
| [tfvars](outputs.tf#L39) | Terraform variable files for the following stages. | ✓ | |
| [vpc_sc_perimeter_default](outputs.tf#L45) | Raw default perimeter resource. | ✓ | |
<!-- END TFDOC -->

View File

@@ -59,6 +59,7 @@ module "vpc-sc" {
context = local.context
}
)
ingress_policies = var.ingress_policies
service_perimeters_regular = var.perimeters
ingress_policies = var.ingress_policies
perimeters = var.perimeters
project_id_search_scope = "organizations/${var.organization.id}"
}

View File

@@ -17,11 +17,7 @@
locals {
tfvars = {
perimeters = {
for k, v in try(module.vpc-sc.service_perimeters_regular, {}) :
k => v.id
}
perimeters_bridge = {
for k, v in try(module.vpc-sc.service_perimeters_bridge, {}) :
for k, v in try(module.vpc-sc.perimeters, {}) :
k => v.id
}
}
@@ -49,5 +45,5 @@ output "tfvars" {
output "vpc_sc_perimeter_default" {
description = "Raw default perimeter resource."
sensitive = true
value = try(module.vpc-sc.service_perimeters_regular["default"], null)
value = try(module.vpc-sc.perimeters["default"], null)
}

View File

@@ -12,12 +12,10 @@ If you are using [Application Default Credentials](https://cloud.google.com/sdk/
- [Scoped policy](#scoped-policy)
- [Access policy IAM](#access-policy-iam)
- [Access levels](#access-levels)
- [Service perimeters](#service-perimeters)
- [Bridge type](#bridge-type)
- [Regular type](#regular-type)
- [Perimeters](#perimeters)
- [Automatic Project ID to Project Number Conversion](#automatic-project-id-to-project-number-conversion)
- [Factories](#factories)
- [Notes](#notes)
- [TODO](#todo)
- [Files](#files)
- [Variables](#variables)
- [Outputs](#outputs)
@@ -112,36 +110,7 @@ module "test" {
# tftest modules=1 resources=2 inventory=access-levels.yaml
```
### Service perimeters
Bridge and regular service perimeters use two separate variables, as bridge perimeters only accept a limited number of arguments, and can leverage a much simpler interface.
The regular perimeters variable exposes all the complexity of the underlying resource, use [its documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_service_perimeter) as a reference about the possible values and configurations.
If you need to refer to access levels created by the same module in regular service perimeters, you can either use the module's outputs in the provided variables, or the key used to identify the relevant access level. The example below shows how to do this in practice.
If you are managing perimeter membership outside of this module via `google_access_context_manager_service_perimeter_resource`, for example at project creation in a project factory, you might want to uncomment the lifecycle blocks that are defined but currently unused in `service-perimeters-regular.tf` and `service-perimeters-bridge.tf`.
#### Bridge type
```hcl
module "test" {
source = "./fabric/modules/vpc-sc"
access_policy = "12345678"
service_perimeters_bridge = {
b1 = {
status_resources = ["projects/111110", "projects/111111"]
}
b2 = {
spec_resources = ["projects/222220", "projects/222221"]
use_explicit_dry_run_spec = true
}
}
}
# tftest modules=1 resources=2 inventory=bridge.yaml
```
#### Regular type
### Perimeters
```hcl
module "test" {
@@ -205,7 +174,7 @@ module "test" {
}
}
}
service_perimeters_regular = {
perimeters = {
r1 = {
status = {
access_levels = ["a1", "a2"]
@@ -224,13 +193,56 @@ module "test" {
# tftest modules=1 resources=3 inventory=regular.yaml
```
## Automatic Project ID to Project Number Conversion
As a convenience, this module can optionally convert project IDs to project numbers. Set `var.project_id_search_scope` to a folder or organization ID to define the search scope.
The caller must have `cloudasset.assets.searchAllResources` permission to perform the search. Roles like `roles/accesscontextmanager.policyAdmin`, `roles/cloudasset.viewer`, or `roles/viewer` grant this.
```hcl
module "vpc-sc" {
source = "./fabric/modules/vpc-sc"
project_id_search_scope = var.org_id
access_policy = "12345678"
ingress_policies = {
i1 = {
from = {
identities = [
"serviceAccount:foo@myproject.iam.gserviceaccount.com"
]
resources = ["projects/my-source-project"]
}
to = {
operations = [{
method_selectors = ["*"]
service_name = "storage.googleapis.com"
}]
resources = ["projects/my-destionation-project"]
}
}
}
perimeters = {
p = {
spec = {
ingress_policies = ["i1"]
resources = ["projects/my-destionation-project"]
}
use_explicit_dry_run_spec = true
}
}
}
# tftest skip because uses data sources
```
## Factories
This module implements support for five distinct factories, used to create and manage perimeters, bridges, access levels, egress policies, and ingress policies via YAML files.
This module implements support for four distinct factories, used to create and manage perimeters, access levels, egress policies, and ingress policies via YAML files.
JSON Schema files for each factory object are available in the [`schemas`](./schemas/) folder, and can be used to validate input YAML data with [`validate-yaml`](https://github.com/gerald1248/validate-yaml) or any of the available tools and libraries.
This is an example that uses only three factories and leave perimeter management in tfvars. Note that the factory configuration points to folders, where each file represents one resource.
3Note that the factory configuration points to folders, where each file represents one resource.
```hcl
module "test" {
@@ -240,30 +252,62 @@ module "test" {
access_levels = "data/access-levels"
egress_policies = "data/egress-policies"
ingress_policies = "data/ingress-policies"
perimeters = "data/perimeters"
context = {
resource_sets = {
foo_projects = ["projects/321", "projects/654"]
}
}
}
service_perimeters_regular = {
perimeter-north = {
description = "Main perimeter"
status = {
access_levels = ["geo-it", "identity-user1"]
resources = ["projects/1111", "projects/2222"]
restricted_services = ["storage.googleapis.com"]
egress_policies = ["gcs-sa-foo"]
ingress_policies = ["sa-tf-test-geo", "sa-tf-test"]
vpc_accessible_services = {
allowed_services = ["storage.googleapis.com"]
enable_restriction = true
}
}
}
}
}
# tftest modules=1 resources=3 files=a1,a2,e1,i1,i2 inventory=factory.yaml
# tftest modules=1 resources=3 files=p1,a1,a2,e1,i1,i2 inventory=factory.yaml
```
```yaml
description: Main perimeter
status:
access_levels:
- "geo-it"
- "identity-user1"
resources:
- "projects/1111"
- "projects/2222"
restricted_services:
- "storage.googleapis.com"
egress_policies:
- "gcs-sa-foo"
ingress_policies:
- "sa-tf-test-geo"
- "sa-tf-test"
vpc_accessible_services:
allowed_services:
- "storage.googleapis.com"
enable_restriction: yes
# tftest-file id=p1 path=data/perimeters/perimeter-north.yaml schema=perimeters.schema.json
```
```yaml
description: "Main perimeter"
status:
access_levels:
- geo-it
- identity-user1
resources:
- projects/1111
- projects/2222
restricted_services:
- storage.googleapis.com
egress_policies:
- gcs-sa-foo
ingress_policies:
- sa-tf-test-geo
- sa-tf-test
vpc_accessible_services:
allowed_services:
- storage.googleapis.com
enable_restriction: true
# tftest-file id=p1 path=data/perimeters/perimeter-north.yaml schema=perimeters.schema.json
```
```yaml
@@ -329,59 +373,10 @@ to:
# tftest-file id=i2 path=data/ingress-policies/sa-tf-test-geo.yaml schema=ingress-policy.schema.json
```
But perimeters could also defined in a yaml file.
```hcl
module "test" {
source = "./fabric/modules/vpc-sc"
access_policy = "12345678"
factories_config = {
access_levels = "data/access-levels"
egress_policies = "data/egress-policies"
ingress_policies = "data/ingress-policies"
perimeters = "data/perimeters"
context = {
resource_sets = {
foo_projects = ["projects/321", "projects/654"]
}
}
}
}
# tftest modules=1 resources=3 files=a1,a2,e1,i1,i2,r1 inventory=factory.yaml
```
```yaml
description: Main perimeter
status:
access_levels:
- "geo-it"
- "identity-user1"
resources:
- "projects/1111"
- "projects/2222"
restricted_services:
- "storage.googleapis.com"
egress_policies:
- "gcs-sa-foo"
ingress_policies:
- "sa-tf-test-geo"
- "sa-tf-test"
vpc_accessible_services:
allowed_services:
- "storage.googleapis.com"
enable_restriction: yes
# tftest-file id=r1 path=data/perimeters/perimeter-north.yaml schema=perimeters.schema.json
````
## Notes
- To remove an access level, first remove the binding between perimeter and the access level in `status` and/or `spec` without removing the access level itself. Once you have run `terraform apply`, you'll then be able to remove the access level and run `terraform apply` again.
## TODO
- [ ] implement support for the `google_access_context_manager_gcp_user_access_binding` resource
<!-- TFDOC OPTS files:1 -->
<!-- BEGIN TFDOC -->
## Files
@@ -393,8 +388,7 @@ status:
| [iam.tf](./iam.tf) | IAM bindings | <code>google_access_context_manager_access_policy_iam_binding</code> · <code>google_access_context_manager_access_policy_iam_member</code> |
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_access_context_manager_access_policy</code> |
| [outputs.tf](./outputs.tf) | Module outputs. | |
| [service-perimeters-bridge.tf](./service-perimeters-bridge.tf) | Bridge service perimeter resources. | <code>google_access_context_manager_service_perimeter</code> |
| [service-perimeters-regular.tf](./service-perimeters-regular.tf) | Regular service perimeter resources. | <code>google_access_context_manager_service_perimeter</code> |
| [perimeters.tf](./perimeters.tf) | Regular service perimeter resources. | <code>google_access_context_manager_service_perimeter</code> |
| [variables.tf](./variables.tf) | Module variables. | |
| [versions.tf](./versions.tf) | Version pins. | |
@@ -406,13 +400,13 @@ status:
| [access_levels](variables.tf#L17) | Access level definitions. | <code title="map&#40;object&#40;&#123;&#10; combining_function &#61; optional&#40;string&#41;&#10; conditions &#61; optional&#40;list&#40;object&#40;&#123;&#10; device_policy &#61; optional&#40;object&#40;&#123;&#10; allowed_device_management_levels &#61; optional&#40;list&#40;string&#41;&#41;&#10; allowed_encryption_statuses &#61; optional&#40;list&#40;string&#41;&#41;&#10; require_admin_approval &#61; bool&#10; require_corp_owned &#61; bool&#10; require_screen_lock &#61; optional&#40;bool&#41;&#10; os_constraints &#61; optional&#40;list&#40;object&#40;&#123;&#10; os_type &#61; string&#10; minimum_version &#61; optional&#40;string&#41;&#10; require_verified_chrome_os &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#41;&#10; &#125;&#41;&#41;&#10; ip_subnetworks &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; members &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; negate &#61; optional&#40;bool&#41;&#10; regions &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; required_access_levels &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; vpc_subnets &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; description &#61; optional&#40;string&#41;&#10; title &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [access_policy_create](variables.tf#L73) | Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format, scopes are in 'folders/456789' or 'projects/project_id' format. | <code title="object&#40;&#123;&#10; parent &#61; string&#10; title &#61; string&#10; scopes &#61; optional&#40;list&#40;string&#41;, null&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [egress_policies](variables.tf#L83) | Egress policy definitions that can be referenced in perimeters. | <code title="map&#40;object&#40;&#123;&#10; title &#61; optional&#40;string&#41;&#10; from &#61; object&#40;&#123;&#10; access_levels &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; identity_type &#61; optional&#40;string&#41;&#10; identities &#61; optional&#40;list&#40;string&#41;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#10; to &#61; object&#40;&#123;&#10; external_resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; operations &#61; optional&#40;list&#40;object&#40;&#123;&#10; method_selectors &#61; optional&#40;list&#40;string&#41;&#41;&#10; permission_selectors &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_name &#61; string&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [factories_config](variables.tf#L126) | Paths to folders that enable factory functionality. | <code title="object&#40;&#123;&#10; access_levels &#61; optional&#40;string&#41;&#10; bridges &#61; optional&#40;string&#41;&#10; egress_policies &#61; optional&#40;string&#41;&#10; ingress_policies &#61; optional&#40;string&#41;&#10; perimeters &#61; optional&#40;string&#41;&#10; context &#61; optional&#40;object&#40;&#123;&#10; resource_sets &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_sets &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; identity_sets &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam](variables.tf#L144) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings](variables.tf#L150) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; members &#61; list&#40;string&#41;&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings_additive](variables.tf#L165) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [ingress_policies](variables.tf#L180) | Ingress policy definitions that can be referenced in perimeters. | <code title="map&#40;object&#40;&#123;&#10; title &#61; optional&#40;string&#41;&#10; from &#61; object&#40;&#123;&#10; access_levels &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; identity_type &#61; optional&#40;string&#41;&#10; identities &#61; optional&#40;list&#40;string&#41;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#10; to &#61; object&#40;&#123;&#10; operations &#61; optional&#40;list&#40;object&#40;&#123;&#10; method_selectors &#61; optional&#40;list&#40;string&#41;&#41;&#10; permission_selectors &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_name &#61; string&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_perimeters_bridge](variables.tf#L222) | Bridge service perimeters. | <code title="map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; title &#61; optional&#40;string&#41;&#10; spec_resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; status_resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; use_explicit_dry_run_spec &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_perimeters_regular](variables.tf#L234) | Regular service perimeters. | <code title="map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; title &#61; optional&#40;string&#41;&#10; spec &#61; optional&#40;object&#40;&#123;&#10; access_levels &#61; optional&#40;list&#40;string&#41;&#41;&#10; egress_policies &#61; optional&#40;list&#40;string&#41;&#41;&#10; ingress_policies &#61; optional&#40;list&#40;string&#41;&#41;&#10; restricted_services &#61; optional&#40;list&#40;string&#41;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; vpc_accessible_services &#61; optional&#40;object&#40;&#123;&#10; allowed_services &#61; list&#40;string&#41;&#10; enable_restriction &#61; optional&#40;bool, true&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#10; status &#61; optional&#40;object&#40;&#123;&#10; access_levels &#61; optional&#40;list&#40;string&#41;&#41;&#10; egress_policies &#61; optional&#40;list&#40;string&#41;&#41;&#10; ingress_policies &#61; optional&#40;list&#40;string&#41;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; restricted_services &#61; optional&#40;list&#40;string&#41;&#41;&#10; vpc_accessible_services &#61; optional&#40;object&#40;&#123;&#10; allowed_services &#61; list&#40;string&#41;&#10; enable_restriction &#61; optional&#40;bool, true&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#10; use_explicit_dry_run_spec &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [factories_config](variables.tf#L126) | Paths to folders that enable factory functionality. | <code title="object&#40;&#123;&#10; access_levels &#61; optional&#40;string&#41;&#10; egress_policies &#61; optional&#40;string&#41;&#10; ingress_policies &#61; optional&#40;string&#41;&#10; perimeters &#61; optional&#40;string&#41;&#10; context &#61; optional&#40;object&#40;&#123;&#10; resource_sets &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_sets &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; identity_sets &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam](variables.tf#L143) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings](variables.tf#L149) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; members &#61; list&#40;string&#41;&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings_additive](variables.tf#L164) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [ingress_policies](variables.tf#L179) | Ingress policy definitions that can be referenced in perimeters. | <code title="map&#40;object&#40;&#123;&#10; title &#61; optional&#40;string&#41;&#10; from &#61; object&#40;&#123;&#10; access_levels &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; identity_type &#61; optional&#40;string&#41;&#10; identities &#61; optional&#40;list&#40;string&#41;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#10; to &#61; object&#40;&#123;&#10; operations &#61; optional&#40;list&#40;object&#40;&#123;&#10; method_selectors &#61; optional&#40;list&#40;string&#41;&#41;&#10; permission_selectors &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_name &#61; string&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [perimeters](variables.tf#L221) | Regular service perimeters. | <code title="map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; title &#61; optional&#40;string&#41;&#10; spec &#61; optional&#40;object&#40;&#123;&#10; access_levels &#61; optional&#40;list&#40;string&#41;&#41;&#10; egress_policies &#61; optional&#40;list&#40;string&#41;&#41;&#10; ingress_policies &#61; optional&#40;list&#40;string&#41;&#41;&#10; restricted_services &#61; optional&#40;list&#40;string&#41;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; vpc_accessible_services &#61; optional&#40;object&#40;&#123;&#10; allowed_services &#61; list&#40;string&#41;&#10; enable_restriction &#61; optional&#40;bool, true&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#10; status &#61; optional&#40;object&#40;&#123;&#10; access_levels &#61; optional&#40;list&#40;string&#41;&#41;&#10; egress_policies &#61; optional&#40;list&#40;string&#41;&#41;&#10; ingress_policies &#61; optional&#40;list&#40;string&#41;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; restricted_services &#61; optional&#40;list&#40;string&#41;&#41;&#10; vpc_accessible_services &#61; optional&#40;object&#40;&#123;&#10; allowed_services &#61; list&#40;string&#41;&#10; enable_restriction &#61; optional&#40;bool, true&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#10; use_explicit_dry_run_spec &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [project_id_search_scope](variables.tf#L254) | Set this to an organization or folder ID to use Cloud Asset Inventory to automatically translate project ids to numbers. | <code>string</code> | | <code>null</code> |
## Outputs
@@ -423,8 +417,7 @@ status:
| [access_policy](outputs.tf#L30) | Access policy resource, if autocreated. | |
| [access_policy_name](outputs.tf#L37) | Access policy name. | |
| [id](outputs.tf#L42) | Fully qualified access policy id. | |
| [service_perimeters_bridge](outputs.tf#L47) | Bridge service perimeter resources. | |
| [service_perimeters_regular](outputs.tf#L52) | Regular service perimeter resources. | |
| [perimeters](outputs.tf#L47) | Regular service perimeter resources. | |
<!-- END TFDOC -->
## Tests
@@ -451,7 +444,7 @@ module "test" {
}
}
}
service_perimeters_regular = {
perimeters = {
default = {
status = {
access_levels = ["geo-it"]

View File

@@ -22,7 +22,7 @@ locals {
}
}
_data_paths = {
for k in ["access_levels", "bridges", "egress_policies", "ingress_policies", "perimeters"] : k => (
for k in ["access_levels", "egress_policies", "ingress_policies", "perimeters"] : k => (
var.factories_config[k] == null
? null
: pathexpand(var.factories_config[k])
@@ -92,16 +92,6 @@ locals {
}
}
}
bridges = {
for k, v in local._data.bridges :
k => {
description = try(v.description, null)
title = try(v.title, null)
spec_resources = try(v.spec_resources, null)
status_resources = try(v.resources, null)
use_explicit_dry_run_spec = try(v.use_explicit_dry_run_spec, false)
}
}
perimeters = {
for k, v in local._data.perimeters :
k => {

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2024 Google LLC
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,49 @@ locals {
google_access_context_manager_access_policy.default[0].name,
var.access_policy
)
cai_query = join(" OR ",
formatlist(
"\"//cloudresourcemanager.googleapis.com/projects/%s\"",
local._project_ids
)
)
do_cai_query = (
var.project_id_search_scope == null
? false
: length(local._project_ids) > 0
)
# collect project ids and convert them to numbers
_all_project_identifiers = distinct(flatten([
for k, v in local.perimeters : [
try(v.status.resources, []),
try(v.spec.resources, []),
[
for _, vv in local.ingress_policies : [
try(vv.from.resources, []),
try(vv.to.resources, [])
]
],
[
for _, vv in local.egress_policies : [
try(vv.from.resources, []),
try(vv.to.resources, [])
]
],
]
]))
_project_ids = [
for x in local._all_project_identifiers :
trimprefix(x, "projects/")
if can(regex("^projects/[a-z]", x))
]
project_number = (local.do_cai_query
? {
for x in data.google_cloud_asset_search_all_resources.projects[0].results :
(trimprefix(x.name, "//cloudresourcemanager.googleapis.com/")) => x.project
}
: {}
)
}
resource "google_access_context_manager_access_policy" "default" {
@@ -27,3 +70,12 @@ resource "google_access_context_manager_access_policy" "default" {
title = var.access_policy_create.title
scopes = var.access_policy_create.scopes
}
data "google_cloud_asset_search_all_resources" "projects" {
count = local.do_cai_query ? 1 : 0
scope = var.project_id_search_scope
asset_types = [
"cloudresourcemanager.googleapis.com/Project"
]
query = "name=${local.cai_query}"
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2022 Google LLC
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -44,12 +44,7 @@ output "id" {
value = local.access_policy
}
output "service_perimeters_bridge" {
description = "Bridge service perimeter resources."
value = google_access_context_manager_service_perimeter.bridge
}
output "service_perimeters_regular" {
output "perimeters" {
description = "Regular service perimeter resources."
value = google_access_context_manager_service_perimeter.regular
}

View File

@@ -21,13 +21,13 @@
# google_access_context_manager_service_perimeters resource
locals {
egress_policies = merge(local.data.egress_policies, var.egress_policies)
ingress_policies = merge(local.data.ingress_policies, var.ingress_policies)
regular_perimeters = merge(local.data.perimeters, var.service_perimeters_regular)
egress_policies = merge(local.data.egress_policies, var.egress_policies)
ingress_policies = merge(local.data.ingress_policies, var.ingress_policies)
perimeters = merge(local.data.perimeters, var.perimeters)
}
resource "google_access_context_manager_service_perimeter" "regular" {
for_each = local.regular_perimeters
for_each = local.perimeters
parent = "accessPolicies/${local.access_policy}"
name = "accessPolicies/${local.access_policy}/servicePerimeters/${each.key}"
description = each.value.description
@@ -45,8 +45,10 @@ resource "google_access_context_manager_service_perimeter" "regular" {
]
)
resources = flatten([
for r in spec.value.resources :
lookup(var.factories_config.context.resource_sets, r, [r])
for r in spec.value.resources : try(
var.factories_config.context.resource_sets[r],
[local.project_number[r]], [r]
)
])
restricted_services = flatten([
for r in coalesce(spec.value.restricted_services, []) :
@@ -54,13 +56,13 @@ resource "google_access_context_manager_service_perimeter" "regular" {
])
dynamic "egress_policies" {
for_each = spec.value.egress_policies == null ? {} : {
for_each = spec.value.egress_policies == null ? [] : [
for k in spec.value.egress_policies :
k => local.egress_policies[k]
}
merge(local.egress_policies[k], { key = k })
]
iterator = policy
content {
title = coalesce(policy.value.title, policy.key)
title = coalesce(policy.value.title, policy.value.key)
dynamic "egress_from" {
for_each = policy.value.from == null ? [] : [""]
content {
@@ -86,8 +88,10 @@ resource "google_access_context_manager_service_perimeter" "regular" {
}
dynamic "sources" {
for_each = flatten([
for r in policy.value.from.resources :
lookup(var.factories_config.context.resource_sets, r, [r])
for r in policy.value.from.resources : try(
var.factories_config.context.resource_sets[r],
[local.project_number[r]], [r]
)
])
iterator = resource
content {
@@ -101,8 +105,10 @@ resource "google_access_context_manager_service_perimeter" "regular" {
content {
external_resources = policy.value.to.external_resources
resources = flatten([
for r in policy.value.to.resources :
lookup(var.factories_config.context.resource_sets, r, [r])
for r in policy.value.to.resources : try(
var.factories_config.context.resource_sets[r],
[local.project_number[r]], [r]
)
])
roles = policy.value.to.roles
@@ -131,13 +137,13 @@ resource "google_access_context_manager_service_perimeter" "regular" {
}
dynamic "ingress_policies" {
for_each = spec.value.ingress_policies == null ? {} : {
for_each = spec.value.ingress_policies == null ? [] : [
for k in spec.value.ingress_policies :
k => local.ingress_policies[k]
}
merge(local.ingress_policies[k], { key = k })
]
iterator = policy
content {
title = coalesce(policy.value.title, policy.key)
title = coalesce(policy.value.title, policy.value.key)
dynamic "ingress_from" {
for_each = policy.value.from == null ? [] : [""]
content {
@@ -157,8 +163,10 @@ resource "google_access_context_manager_service_perimeter" "regular" {
}
dynamic "sources" {
for_each = flatten([
for r in policy.value.from.resources :
lookup(var.factories_config.context.resource_sets, r, [r])
for r in policy.value.from.resources : try(
var.factories_config.context.resource_sets[r],
[local.project_number[r]], [r]
)
])
content {
resource = sources.value
@@ -170,8 +178,10 @@ resource "google_access_context_manager_service_perimeter" "regular" {
for_each = policy.value.to == null ? [] : [""]
content {
resources = flatten([
for r in policy.value.to.resources :
lookup(var.factories_config.context.resource_sets, r, [r])
for r in policy.value.to.resources : try(
var.factories_config.context.resource_sets[r],
[local.project_number[r]], [r]
)
])
roles = policy.value.to.roles
dynamic "operations" {
@@ -199,7 +209,7 @@ resource "google_access_context_manager_service_perimeter" "regular" {
}
dynamic "vpc_accessible_services" {
for_each = spec.value.vpc_accessible_services == null ? {} : { 1 = 1 }
for_each = spec.value.vpc_accessible_services == null ? [] : [""]
content {
allowed_services = flatten([
for r in spec.value.vpc_accessible_services.allowed_services :
@@ -222,8 +232,10 @@ resource "google_access_context_manager_service_perimeter" "regular" {
]
)
resources = flatten([
for r in status.value.resources :
lookup(var.factories_config.context.resource_sets, r, [r])
for r in status.value.resources : try(
var.factories_config.context.resource_sets[r],
[local.project_number[r]], [r]
)
])
restricted_services = flatten([
for r in coalesce(status.value.restricted_services, []) :
@@ -231,13 +243,13 @@ resource "google_access_context_manager_service_perimeter" "regular" {
])
dynamic "egress_policies" {
for_each = status.value.egress_policies == null ? {} : {
for_each = status.value.egress_policies == null ? [] : [
for k in status.value.egress_policies :
k => local.egress_policies[k]
}
merge(local.egress_policies[k], { key = k })
]
iterator = policy
content {
title = coalesce(policy.value.title, policy.key)
title = coalesce(policy.value.title, policy.value.key)
dynamic "egress_from" {
for_each = policy.value.from == null ? [] : [""]
content {
@@ -263,8 +275,10 @@ resource "google_access_context_manager_service_perimeter" "regular" {
}
dynamic "sources" {
for_each = flatten([
for r in policy.value.from.resources :
lookup(var.factories_config.context.resource_sets, r, [r])
for r in policy.value.from.resources : try(
var.factories_config.context.resource_sets[r],
[local.project_number[r]], [r]
)
])
iterator = resource
content {
@@ -304,13 +318,13 @@ resource "google_access_context_manager_service_perimeter" "regular" {
}
dynamic "ingress_policies" {
for_each = status.value.ingress_policies == null ? {} : {
for_each = status.value.ingress_policies == null ? [] : [
for k in status.value.ingress_policies :
k => local.ingress_policies[k]
}
merge(local.ingress_policies[k], { key = k })
]
iterator = policy
content {
title = coalesce(policy.value.title, policy.key)
title = coalesce(policy.value.title, policy.value.key)
dynamic "ingress_from" {
for_each = policy.value.from == null ? [] : [""]
content {
@@ -331,8 +345,10 @@ resource "google_access_context_manager_service_perimeter" "regular" {
}
dynamic "sources" {
for_each = flatten([
for r in policy.value.from.resources :
lookup(var.factories_config.context.resource_sets, r, [r])
for r in policy.value.from.resources : try(
var.factories_config.context.resource_sets[r],
[local.project_number[r]], [r]
)
])
content {
resource = sources.value
@@ -344,8 +360,10 @@ resource "google_access_context_manager_service_perimeter" "regular" {
for_each = policy.value.to == null ? [] : [""]
content {
resources = flatten([
for r in policy.value.to.resources :
lookup(var.factories_config.context.resource_sets, r, [r])
for r in policy.value.to.resources : try(
var.factories_config.context.resource_sets[r],
[local.project_number[r]], [r]
)
])
roles = policy.value.to.roles
dynamic "operations" {
@@ -373,7 +391,7 @@ resource "google_access_context_manager_service_perimeter" "regular" {
}
dynamic "vpc_accessible_services" {
for_each = status.value.vpc_accessible_services == null ? {} : { 1 = 1 }
for_each = status.value.vpc_accessible_services == null ? [] : [""]
content {
allowed_services = flatten([
for r in status.value.vpc_accessible_services.allowed_services :

View File

@@ -1,64 +0,0 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Bridge service perimeter resources.
# this code implements "additive" service perimeters, if "authoritative"
# service perimeters are needed, switch to the
# google_access_context_manager_service_perimeters resource
locals {
bridge_perimeters = merge(local.data.bridges, var.service_perimeters_bridge)
}
resource "google_access_context_manager_service_perimeter" "bridge" {
for_each = local.bridge_perimeters
parent = "accessPolicies/${local.access_policy}"
name = "accessPolicies/${local.access_policy}/servicePerimeters/${each.key}"
title = each.key
perimeter_type = "PERIMETER_TYPE_BRIDGE"
use_explicit_dry_run_spec = each.value.use_explicit_dry_run_spec
dynamic "spec" {
for_each = each.value.spec_resources == null ? [] : [""]
content {
resources = flatten([
for r in each.value.spec_resources :
lookup(var.factories_config.context.resource_sets, r, [r])
])
}
}
dynamic "status" {
for_each = each.value.status_resources == null ? [] : [""]
content {
resources = flatten([
for r in each.value.status_resources :
lookup(var.factories_config.context.resource_sets, r, [r])
])
}
}
# lifecycle {
# ignore_changes = [spec[0].resources, status[0].resources]
# }
depends_on = [
google_access_context_manager_access_policy.default,
google_access_context_manager_access_level.basic,
google_access_context_manager_service_perimeter.regular
]
}

View File

@@ -127,7 +127,6 @@ variable "factories_config" {
description = "Paths to folders that enable factory functionality."
type = object({
access_levels = optional(string)
bridges = optional(string)
egress_policies = optional(string)
ingress_policies = optional(string)
perimeters = optional(string)
@@ -219,19 +218,7 @@ variable "ingress_policies" {
}
}
variable "service_perimeters_bridge" {
description = "Bridge service perimeters."
type = map(object({
description = optional(string)
title = optional(string)
spec_resources = optional(list(string))
status_resources = optional(list(string))
use_explicit_dry_run_spec = optional(bool, false)
}))
default = {}
}
variable "service_perimeters_regular" {
variable "perimeters" {
description = "Regular service perimeters."
type = map(object({
description = optional(string)
@@ -263,3 +250,9 @@ variable "service_perimeters_regular" {
default = {}
nullable = false
}
variable "project_id_search_scope" {
description = "Set this to an organization or folder ID to use Cloud Asset Inventory to automatically translate project ids to numbers."
type = string
default = null
}

View File

@@ -73,7 +73,25 @@ values:
service_name: storage.googleapis.com
resources:
- projects/123456789
roles: []
title: gcs-sa-foo
ingress_policies:
- ingress_from:
- identities:
- serviceAccount:test-tf@myproject.iam.gserviceaccount.com
identity_type: null
sources:
- resource: null
ingress_to:
- operations:
- method_selectors: []
service_name: '*'
resources:
- projects/1234567890
- projects/321
- projects/654
roles: []
title: sa-tf-test-geo
- ingress_from:
- identities:
- serviceAccount:test-tf-0@myproject.iam.gserviceaccount.com
@@ -92,20 +110,8 @@ values:
service_name: compute.googleapis.com
resources:
- '*'
- ingress_from:
- identities:
- serviceAccount:test-tf@myproject.iam.gserviceaccount.com
identity_type: null
sources:
- resource: null
ingress_to:
- operations:
- method_selectors: []
service_name: '*'
resources:
- projects/1234567890
- projects/321
- projects/654
roles: []
title: sa-tf-test
resources:
- projects/1111
- projects/2222

View File

@@ -75,22 +75,6 @@ values:
roles: null
title: gcs-sa-foo
ingress_policies:
- ingress_from:
- identities:
- serviceAccount:test-tf-2@myproject.iam.gserviceaccount.com
identity_type: null
sources:
- access_level: '*'
resource: null
ingress_to:
- operations:
- method_selectors: []
service_name: '*'
resources:
- '*'
roles:
- roles/storage.objectViewer
title: sa-roles
- ingress_from:
- identities:
- serviceAccount:test-tf-0@myproject.iam.gserviceaccount.com
@@ -107,6 +91,22 @@ values:
- '*'
roles: null
title: sa-tf-test
- ingress_from:
- identities:
- serviceAccount:test-tf-2@myproject.iam.gserviceaccount.com
identity_type: null
sources:
- access_level: '*'
resource: null
ingress_to:
- operations:
- method_selectors: []
service_name: '*'
resources:
- '*'
roles:
- roles/storage.objectViewer
title: sa-roles
resources:
- projects/1111
- projects/2222