Skip to content

Commit afa9c35

Browse files
daniel-citapeabody
andauthored
fix: use dry run resource to add resources to the perimeter (#204)
Co-authored-by: Andrew Peabody <[email protected]>
1 parent eef8fcb commit afa9c35

File tree

13 files changed

+302
-28
lines changed

13 files changed

+302
-28
lines changed

build/int.cloudbuild.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,30 @@ steps:
121121
- verify-scoped-example-with-egress-rule
122122
name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS'
123123
args: ['/bin/bash', '-c', 'cft test run TestScopedExampleWithEgressRule --stage destroy --verbose']
124+
# scoped example with access level dry-run
125+
- id: init-scoped-example-access-level-dry-run
126+
waitFor:
127+
- destroy-scoped-example-with-egress-rule
128+
name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS'
129+
args: ['/bin/bash', '-c', 'cft test run TestScopedExampleAccessLevelDryRun --stage init --verbose']
130+
131+
- id: apply-scoped-example-access-level-dry-run
132+
waitFor:
133+
- init-scoped-example-access-level-dry-run
134+
name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS'
135+
args: ['/bin/bash', '-c', 'cft test run TestScopedExampleAccessLevelDryRun --stage apply --verbose']
136+
137+
- id: verify-scoped-example-access-level-dry-run
138+
waitFor:
139+
- apply-scoped-example-access-level-dry-run
140+
name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS'
141+
args: ['/bin/bash', '-c', 'cft test run TestScopedExampleAccessLevelDryRun --stage verify --verbose']
142+
143+
- id: destroy-scoped-example-access-level-dry-run
144+
waitFor:
145+
- verify-scoped-example-access-level-dry-run
146+
name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS'
147+
args: ['/bin/bash', '-c', 'cft test run TestScopedExampleAccessLevelDryRun --stage destroy --verbose']
124148
tags:
125149
- 'ci'
126150
- 'integration'

examples/simple_example_access_level_dry_run/README.md renamed to examples/scoped_example_access_level_dry_run/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ This example illustrates how to use the `vpc-service-controls` module to configu
1515
| ip\_subnetworks | A list of CIDR block IP subnetwork specification. May be IPv4 or IPv6. Note that for a CIDR IP address block, the specified IP address portion must be properly truncated (i.e. all the host bits must be zero) or the input is considered malformed. For example, "192.0.2.0/24" is accepted but "192.0.2.1/24" is not. Similarly, for IPv6, "2001:db8::/32" is accepted whereas "2001:db8::1/32" is not. The originating IP of a request must be in one of the listed subnets in order for this Condition to be true. If empty, all IP addresses are allowed. | `list(string)` | n/a | yes |
1616
| parent\_id | The parent of this AccessPolicy in the Cloud Resource Hierarchy. As of now, only organization are accepted as parent. | `string` | n/a | yes |
1717
| policy\_name | The policy's name. | `string` | n/a | yes |
18-
| protected\_project\_id | Project number of the project INSIDE the regular service perimeter. | `number` | n/a | yes |
18+
| protected\_project\_number | Project number of the project INSIDE the regular service perimeter. | `number` | n/a | yes |
19+
| scopes | Folder or project on which this policy is applicable. Format: 'folders/FOLDER\_ID' or 'projects/PROJECT\_NUMBER' | `list(string)` | `[]` | no |
1920

2021
## Outputs
2122

2223
| Name | Description |
2324
|------|-------------|
24-
| policy\_name | n/a |
25+
| access\_levels\_dry\_run | Access Level in Dry\_run mode |
26+
| policy\_id | Resource name of the AccessPolicy. |
27+
| policy\_name | Name of the AccessPolicy. |
28+
| protected\_project\_number | Project number of the project INSIDE the regular service perimeter |
29+
| service\_perimeter\_name | Service perimeter name |
2530

2631
<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->

examples/simple_example_access_level_dry_run/main.tf renamed to examples/scoped_example_access_level_dry_run/main.tf

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@
1616

1717
module "access_context_manager_policy" {
1818
source = "terraform-google-modules/vpc-service-controls/google"
19-
version = "~> 7.0"
19+
version = "~> 7.1"
2020

2121
parent_id = var.parent_id
2222
policy_name = var.policy_name
23+
scopes = var.scopes
2324
}
2425

2526
module "access_level_1" {
2627
source = "terraform-google-modules/vpc-service-controls/google//modules/access_level"
27-
version = "~> 7.0"
28+
version = "~> 7.1"
2829

2930
policy = module.access_context_manager_policy.policy_id
3031
name = "single_ip_policy"
@@ -34,33 +35,43 @@ module "access_level_1" {
3435

3536
module "access_level_2" {
3637
source = "terraform-google-modules/vpc-service-controls/google//modules/access_level"
37-
version = "~> 7.0"
38+
version = "~> 7.1"
3839

3940
policy = module.access_context_manager_policy.policy_id
4041
name = "single_ip_policy_dry_run"
4142
ip_subnetworks = var.ip_subnetworks
4243
description = "Some description"
4344
}
4445

46+
resource "time_sleep" "wait_for_access_levels" {
47+
create_duration = "90s"
48+
destroy_duration = "90s"
49+
50+
depends_on = [
51+
module.access_level_1,
52+
module.access_level_2
53+
]
54+
}
55+
4556
module "regular_service_perimeter_1" {
4657
source = "terraform-google-modules/vpc-service-controls/google//modules/regular_service_perimeter"
47-
version = "~> 7.0"
58+
version = "~> 7.1"
4859

4960
policy = module.access_context_manager_policy.policy_id
50-
perimeter_name = "regular_perimeter_1"
61+
perimeter_name = "regular_perimeter_1_dry_run"
5162
description = "Some description"
52-
resources = [var.protected_project_id]
5363

64+
resources = [var.protected_project_number]
5465
restricted_services = ["bigquery.googleapis.com", "storage.googleapis.com"]
66+
access_levels = [module.access_level_1.name]
5567

56-
access_levels = [module.access_level_1.name]
57-
58-
59-
resources_dry_run = [var.protected_project_id]
68+
resources_dry_run = [var.protected_project_number]
6069
restricted_services_dry_run = ["storage.googleapis.com"]
6170
access_levels_dry_run = [module.access_level_2.name]
6271

6372
shared_resources = {
64-
all = [var.protected_project_id]
73+
all = [var.protected_project_number]
6574
}
75+
76+
depends_on = [time_sleep.wait_for_access_levels]
6677
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
output "policy_id" {
18+
description = "Resource name of the AccessPolicy."
19+
value = module.access_context_manager_policy.policy_id
20+
}
21+
22+
output "policy_name" {
23+
description = "Name of the AccessPolicy."
24+
value = var.policy_name
25+
}
26+
27+
output "protected_project_number" {
28+
description = "Project number of the project INSIDE the regular service perimeter"
29+
value = var.protected_project_number
30+
}
31+
32+
output "access_levels_dry_run" {
33+
description = "Access Level in Dry_run mode"
34+
value = module.access_level_2.name
35+
}
36+
37+
output "service_perimeter_name" {
38+
description = "Service perimeter name"
39+
value = module.regular_service_perimeter_1.perimeter_name
40+
}

examples/simple_example_access_level_dry_run/variables.tf renamed to examples/scoped_example_access_level_dry_run/variables.tf

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ variable "policy_name" {
2424
type = string
2525
}
2626

27-
variable "protected_project_id" {
27+
variable "protected_project_number" {
2828
description = "Project number of the project INSIDE the regular service perimeter."
2929
type = number
3030
}
@@ -33,3 +33,9 @@ variable "ip_subnetworks" {
3333
description = "A list of CIDR block IP subnetwork specification. May be IPv4 or IPv6. Note that for a CIDR IP address block, the specified IP address portion must be properly truncated (i.e. all the host bits must be zero) or the input is considered malformed. For example, \"192.0.2.0/24\" is accepted but \"192.0.2.1/24\" is not. Similarly, for IPv6, \"2001:db8::/32\" is accepted whereas \"2001:db8::1/32\" is not. The originating IP of a request must be in one of the listed subnets in order for this Condition to be true. If empty, all IP addresses are allowed."
3434
type = list(string)
3535
}
36+
37+
variable "scopes" {
38+
description = "Folder or project on which this policy is applicable. Format: 'folders/FOLDER_ID' or 'projects/PROJECT_NUMBER'"
39+
type = list(string)
40+
default = []
41+
}

modules/regular_service_perimeter/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ module "regular_service_perimeter_1" {
105105
| perimeter\_name | Name of the perimeter. Should be one unified string. Must only be letters, numbers and underscores | `string` | n/a | yes |
106106
| policy | Name of the parent policy | `string` | n/a | yes |
107107
| resource\_keys | A list of keys to use for the Terraform state. The order should correspond to var.resources and the keys must not be dynamically computed. If `null`, var.resources will be used as keys. | `list(string)` | `null` | no |
108+
| resource\_keys\_dry\_run | A list of keys to use for the Terraform state. The order should correspond to var.resources\_dry\_run and the keys must not be dynamically computed. If `null`, var.resources\_dry\_run will be used as keys. | `list(string)` | `null` | no |
108109
| resources | A list of GCP resources that are inside of the service perimeter. Currently only projects and VPC networks are allowed. | `list(string)` | `[]` | no |
109110
| resources\_dry\_run | (Dry-run) A list of GCP resources that are inside of the service perimeter. Currently only projects and VPC networks are allowed. If set, a dry-run policy will be set. | `list(string)` | `[]` | no |
110111
| restricted\_services | GCP services that are subject to the Service Perimeter restrictions. Must contain a list of services. For example, if storage.googleapis.com is specified, access to the storage buckets inside the perimeter must meet the perimeter's access restrictions. | `list(string)` | `[]` | no |

modules/regular_service_perimeter/main.tf

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ resource "google_access_context_manager_service_perimeter" "regular_service_peri
4949
for_each = local.dry_run ? ["dry-run"] : []
5050
content {
5151
restricted_services = var.restricted_services_dry_run
52-
resources = [for item in var.resources_dry_run : can(regex("global/networks", item)) ? format("//compute.googleapis.com/%s", item) : format("projects/%s", item)]
5352
access_levels = formatlist(
5453
"accessPolicies/${var.policy}/accessLevels/%s",
5554
var.access_levels_dry_run
@@ -86,10 +85,23 @@ locals {
8685
for rk in local.resource_keys :
8786
rk => var.resources[index(local.resource_keys, rk)]
8887
}
88+
89+
#dry-run
90+
resource_keys_dry_run = var.resource_keys_dry_run != null ? var.resource_keys_dry_run : var.resources_dry_run
91+
resources_dry_run = {
92+
for rk in local.resource_keys_dry_run :
93+
rk => var.resources_dry_run[index(local.resource_keys_dry_run, rk)]
94+
}
8995
}
9096

9197
resource "google_access_context_manager_service_perimeter_resource" "service_perimeter_resource" {
9298
for_each = local.resources
9399
perimeter_name = google_access_context_manager_service_perimeter.regular_service_perimeter.name
94100
resource = can(regex("global/networks", each.value)) ? "//compute.googleapis.com/${each.value}" : "projects/${each.value}"
95101
}
102+
103+
resource "google_access_context_manager_service_perimeter_dry_run_resource" "dry_run_service_perimeter_resource" {
104+
for_each = local.resources_dry_run
105+
perimeter_name = google_access_context_manager_service_perimeter.regular_service_perimeter.name
106+
resource = can(regex("global/networks", each.value)) ? "//compute.googleapis.com/${each.value}" : "projects/${each.value}"
107+
}

modules/regular_service_perimeter/variables.tf

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ variable "resources_dry_run" {
6565
default = []
6666
}
6767

68+
variable "resource_keys_dry_run" {
69+
description = "A list of keys to use for the Terraform state. The order should correspond to var.resources_dry_run and the keys must not be dynamically computed. If `null`, var.resources_dry_run will be used as keys."
70+
type = list(string)
71+
default = null
72+
}
73+
6874
variable "access_levels_dry_run" {
6975
description = "(Dry-run) A list of AccessLevel resource names that allow resources within the ServicePerimeter to be accessed from the internet. AccessLevels listed must be in the same policy as this ServicePerimeter. Referencing a nonexistent AccessLevel is a syntax error. If no AccessLevel names are listed, resources within the perimeter can only be accessed via GCP calls with request origins within the perimeter. Example: 'accessPolicies/MY_POLICY/accessLevels/MY_LEVEL'. For Service Perimeter Bridge, must be empty. If set, a dry-run policy will be set."
7076
type = list(string)
@@ -77,7 +83,6 @@ variable "shared_resources" {
7783
default = { all = [] }
7884
}
7985

80-
## Have to solve it like this don't want use optional flag because is still experimental
8186
variable "egress_policies" {
8287
description = "A list of all [egress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#egress-rules-reference), each list object has a `from` and `to` value that describes egress_from and egress_to.\n\nExample: `[{ from={ identities=[], identity_type=\"ID_TYPE\" }, to={ resources=[], operations={ \"SRV_NAME\"={ OP_TYPE=[] }}}}]`\n\nValid Values:\n`ID_TYPE` = `null` or `IDENTITY_TYPE_UNSPECIFIED` (only allow indentities from list); `ANY_IDENTITY`; `ANY_USER_ACCOUNT`; `ANY_SERVICE_ACCOUNT`\n`SRV_NAME` = \"`*`\" (allow all services) or [Specific Services](https://cloud.google.com/vpc-service-controls/docs/supported-products#supported_products)\n`OP_TYPE` = [methods](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions) or [permissions](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions)"
8388
type = list(object({
@@ -103,8 +108,7 @@ variable "egress_policies" {
103108
default = []
104109
}
105110

106-
## Have to solve it like this don't want use optional flag because is still experimental
107-
variable "ingress_policies" { # TODO
111+
variable "ingress_policies" {
108112
description = "A list of all [ingress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#ingress-rules-reference), each list object has a `from` and `to` value that describes ingress_from and ingress_to.\n\nExample: `[{ from={ sources={ resources=[], access_levels=[] }, identities=[], identity_type=\"ID_TYPE\" }, to={ resources=[], operations={ \"SRV_NAME\"={ OP_TYPE=[] }}}}]`\n\nValid Values:\n`ID_TYPE` = `null` or `IDENTITY_TYPE_UNSPECIFIED` (only allow indentities from list); `ANY_IDENTITY`; `ANY_USER_ACCOUNT`; `ANY_SERVICE_ACCOUNT`\n`SRV_NAME` = \"`*`\" (allow all services) or [Specific Services](https://cloud.google.com/vpc-service-controls/docs/supported-products#supported_products)\n`OP_TYPE` = [methods](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions) or [permissions](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions)"
109113
type = list(object({
110114
title = optional(string, null)
@@ -128,7 +132,6 @@ variable "ingress_policies" { # TODO
128132
default = []
129133
}
130134

131-
## Have to solve it like this don't want use optional flag because is still experimental
132135
variable "egress_policies_dry_run" {
133136
description = "A list of all [egress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#egress-rules-reference), each list object has a `from` and `to` value that describes egress_from and egress_to. Use same formatting as `egress_policies`."
134137
type = list(object({
@@ -154,7 +157,6 @@ variable "egress_policies_dry_run" {
154157
default = []
155158
}
156159

157-
## Have to solve it like this don't want use optional flag because is still experimental
158160
variable "ingress_policies_dry_run" {
159161
description = "A list of all [ingress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#ingress-rules-reference), each list object has a `from` and `to` value that describes ingress_from and ingress_to. Use same formatting as `ingress_policies`."
160162
type = list(object({

examples/simple_example_access_level_dry_run/outputs.tf renamed to test/fixtures/scoped_example_access_level_dry_run/main.tf

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
12
/**
2-
* Copyright 2024 Google LLC
3+
* Copyright 2025 Google LLC
34
*
45
* Licensed under the Apache License, Version 2.0 (the "License");
56
* you may not use this file except in compliance with the License.
@@ -14,6 +15,19 @@
1415
* limitations under the License.
1516
*/
1617

17-
output "policy_name" {
18-
value = var.policy_name
18+
resource "random_string" "suffix" {
19+
length = 6
20+
special = false
21+
upper = false
22+
}
23+
24+
module "example" {
25+
source = "../../../examples/scoped_example_access_level_dry_run"
26+
27+
parent_id = var.parent_id
28+
policy_name = "int_test_vpc_al_dry_run_${random_string.suffix.result}"
29+
scopes = var.scopes
30+
31+
protected_project_number = var.protected_project_ids["number"]
32+
ip_subnetworks = ["192.0.2.0/24"]
1933
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
output "policy_id" {
18+
description = "Resource name of the AccessPolicy."
19+
value = module.example.policy_id
20+
}
21+
22+
output "policy_name" {
23+
description = "Name of the parent policy"
24+
value = module.example.policy_name
25+
}
26+
27+
output "protected_project_number" {
28+
description = "Project number of the project INSIDE the regular service perimeter"
29+
value = module.example.protected_project_number
30+
}
31+
32+
output "service_perimeter_name" {
33+
description = "Service perimeter name"
34+
value = module.example.service_perimeter_name
35+
}
36+
37+
output "access_levels_dry_run" {
38+
description = "Access Level in Dry_run mode"
39+
value = module.example.access_levels_dry_run
40+
}

0 commit comments

Comments
 (0)