diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13601331..d3e1d4e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+- new `vpc-sc` module
- add support for Shared VPC to the `project` module
## [2.4.2] - 2020-07-09
diff --git a/README.md b/README.md
index 8ca9bca1..883394b5 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ Currently available modules:
- **compute** - [VM/VM group](./modules/compute-vm), [MIG](./modules/compute-mig), [GKE cluster](./modules/gke-cluster), [GKE nodepool](./modules/gke-nodepool), [COS container](./modules/cos-container) (coredns, mysql, onprem, squid)
- **data** - [GCS](./modules/gcs), [BigQuery dataset](./modules/bigquery-dataset), [Pub/Sub](./modules/pubsub), [Datafusion](./modules/datafusion), [Bigtable instance](./modules/bigtable-instance)
- **development** - [Cloud Source Repository](./modules/source-repository), [Container Registry](./modules/container-registry), [Artifact Registry](./modules/artifact-registry)
-- **security** - [KMS](./modules/kms), [SecretManager](./modules/secret-manager)
+- **security** - [KMS](./modules/kms), [SecretManager](./modules/secret-manager), [VPC Service Control](./modules/vpc-sc)
- **serverless** - [Cloud Functions](./cloud-functions)
For more information and usage examples see each module's README file.
diff --git a/modules/README.md b/modules/README.md
index 4c73872a..722b5ee4 100644
--- a/modules/README.md
+++ b/modules/README.md
@@ -58,6 +58,7 @@ Specific modules also offer support for non-authoritative bindings (e.g. `google
- [Cloud KMS](./kms)
- [Secret Manager](./secret-manager)
+- [VPC Service Control](./vpc-sc)
## Serverless
diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md
new file mode 100644
index 00000000..5840e58f
--- /dev/null
+++ b/modules/vpc-sc/README.md
@@ -0,0 +1,113 @@
+# VPC Service Control Module
+
+This module allows managing VPC Service Control (VPC-SC) properties:
+
+- [Access Policy](https://cloud.google.com/access-context-manager/docs/create-access-policy)
+- [Access Levels](https://cloud.google.com/access-context-manager/docs/manage-access-levels)
+- [VPC-SC Perimeters](https://cloud.google.com/vpc-service-controls/docs/service-perimeters)
+
+The Use of this module requires credentials with the [correct permissions](https://cloud.google.com/access-context-manager/docs/access-control) to use Access Context Manager.
+
+## Example VCP-SC standard perimeter
+
+```hcl
+module "vpc-sc" {
+ source = "../../modules/vpc-sc"
+ org_id = 1234567890
+ access_policy_title = "My Access Policy"
+ access_levels = {
+ my_trusted_proxy = {
+ combining_function = "AND"
+ conditions = [{
+ ip_subnetworks = ["85.85.85.52/32"]
+ members = []
+ negate = false
+ }]
+ }
+ }
+ access_level_perimeters = {
+ my_trusted_proxy = ["perimeter"]
+ }
+ perimeters = {
+ perimeter = {
+ type = "PERIMETER_TYPE_REGULAR"
+ dry_run_config = null
+ enforced_config = {
+ restricted_services = ["storage.googleapis.com"]
+ vpc_accessible_services = ["storage.googleapis.com"]
+ }
+ }
+ }
+ perimeter_projects = {
+ perimeter = {
+ enforced = [111111111,222222222]
+ }
+ }
+}
+```
+
+## Example VCP-SC standard perimeter with one service and one project in dry run mode
+```hcl
+module "vpc-sc" {
+ source = "../../modules/vpc-sc"
+ org_id = 1234567890
+ access_policy_title = "My Access Policy"
+ access_levels = {
+ my_trusted_proxy = {
+ combining_function = "AND"
+ conditions = [{
+ ip_subnetworks = ["85.85.85.52/32"]
+ members = []
+ negate = false
+ }]
+ }
+ }
+ access_level_perimeters = {
+ enforced = {
+ my_trusted_proxy = ["perimeter"]
+ }
+ }
+ perimeters = {
+ perimeter = {
+ type = "PERIMETER_TYPE_REGULAR"
+ dry_run_config = {
+ restricted_services = ["storage.googleapis.com", "bigquery.googleapis.com"]
+ vpc_accessible_services = ["storage.googleapis.com", "bigquery.googleapis.com"]
+ }
+ enforced_config = {
+ restricted_services = ["storage.googleapis.com"]
+ vpc_accessible_services = ["storage.googleapis.com"]
+ }
+ }
+ }
+ perimeter_projects = {
+ perimeter = {
+ enforced = [111111111,222222222]
+ dry_run = [333333333]
+ }
+ }
+}
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---: |:---:|:---:|
+| access_policy_title | Access Policy title to be created. | string
| ✓ | |
+| org_id | Organization id in nnnnnn format. | number
| ✓ | |
+| *access_level_perimeters* | Enforced mode -> Access Level -> Perimeters mapping. Enforced mode can be 'enforced' or 'dry_run' | map(map(list(string)))
| | {}
|
+| *access_levels* | Access Levels. | map(object({...}))
| | {}
|
+| *perimeter_projects* | Perimeter -> Enforced Mode -> Projects Number mapping. Enforced mode can be 'enforced' or 'dry_run'. | map(map(list(number)))
| | {}
|
+| *perimeters* | Set of Perimeters. | map(object({...}))
| | {}
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| access_levels | Access Levels. | |
+| access_policy_name | Access Policy resource | |
+| org_id | Organization id dependent on module resources. | |
+| perimeters_bridge | VPC-SC bridge perimeter resources. | |
+| perimeters_standard | VPC-SC standard perimeter resources. | |
+
diff --git a/modules/vpc-sc/main.tf b/modules/vpc-sc/main.tf
new file mode 100644
index 00000000..9f7d13f3
--- /dev/null
+++ b/modules/vpc-sc/main.tf
@@ -0,0 +1,165 @@
+/**
+ * Copyright 2020 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.
+ */
+
+locals {
+ access_policy_name = google_access_context_manager_access_policy.default.name
+
+ standard_perimeters = {
+ for key, value in var.perimeters :
+ key => value if value.type == "PERIMETER_TYPE_REGULAR"
+ }
+
+ bridge_perimeters = {
+ for key, value in var.perimeters :
+ key => value if value.type == "PERIMETER_TYPE_BRIDGE"
+ }
+
+ perimeter_access_levels_enforced = try(transpose(var.access_level_perimeters.enforced), null)
+ perimeter_access_levels_dry_run = try(transpose(var.access_level_perimeters.dry_run), null)
+}
+
+resource "google_access_context_manager_access_policy" "default" {
+ parent = "organizations/${var.org_id}"
+ title = var.access_policy_title
+}
+
+resource "google_access_context_manager_access_level" "default" {
+ for_each = var.access_levels
+ parent = "accessPolicies/${local.access_policy_name}"
+ name = "accessPolicies/${local.access_policy_name}/accessLevels/${each.key}"
+ title = each.key
+
+ dynamic "basic" {
+ for_each = try(toset(each.value.conditions), [])
+
+ content {
+ combining_function = try(each.value.combining_function, null)
+ conditions {
+ ip_subnetworks = try(basic.value.ip_subnetworks, null)
+ members = try(basic.value.members, null)
+ negate = try(basic.value.negate, null)
+ }
+ }
+ }
+}
+
+resource "google_access_context_manager_service_perimeter" "standard" {
+ for_each = local.standard_perimeters
+ parent = "accessPolicies/${local.access_policy_name}"
+ description = "Terraform managed."
+ name = "accessPolicies/${local.access_policy_name}/servicePerimeters/${each.key}"
+ title = each.key
+ perimeter_type = each.value.type
+
+ # Enforced mode configuration
+ dynamic "status" {
+ for_each = each.value.enforced_config != null ? [""] : []
+
+ content {
+ resources = formatlist(
+ "projects/%s", try(lookup(var.perimeter_projects, each.key, {}).enforced, [])
+ )
+ restricted_services = each.value.enforced_config.restricted_services
+ access_levels = formatlist(
+ "accessPolicies/${local.access_policy_name}/accessLevels/%s",
+ try(lookup(local.perimeter_access_levels_enforced, each.key, []), [])
+ )
+
+ dynamic "vpc_accessible_services" {
+ for_each = each.value.enforced_config.vpc_accessible_services != [] ? [""] : []
+
+ content {
+ enable_restriction = true
+ allowed_services = each.value.enforced_config.vpc_accessible_services
+ }
+ }
+ }
+ }
+
+ # Dry run mode configuration
+ use_explicit_dry_run_spec = each.value.dry_run_config != null ? true : false
+ dynamic "spec" {
+ for_each = each.value.dry_run_config != null ? [""] : []
+
+ content {
+ resources = formatlist(
+ "projects/%s", try(lookup(var.perimeter_projects, each.key, {}).dry_run, [])
+ )
+ restricted_services = try(each.value.dry_run_config.restricted_services, null)
+ access_levels = formatlist(
+ "accessPolicies/${local.access_policy_name}/accessLevels/%s",
+ try(lookup(local.perimeter_access_levels_dry_run, each.key, []), [])
+ )
+
+ dynamic "vpc_accessible_services" {
+ for_each = try(each.value.dry_run_config.vpc_accessible_services != [] ? [""] : [], [])
+
+ content {
+ enable_restriction = true
+ allowed_services = try(each.value.dry_run_config.vpc_accessible_services, null)
+ }
+ }
+ }
+ }
+
+ # Uncomment if used alongside `google_access_context_manager_service_perimeter_resource`,
+ # so they don't fight over which resources should be in the policy.
+ # lifecycle {
+ # ignore_changes = [status[0].resources]
+ # }
+
+ depends_on = [
+ google_access_context_manager_access_level.default,
+ ]
+}
+
+resource "google_access_context_manager_service_perimeter" "bridge" {
+ for_each = local.bridge_perimeters
+ parent = "accessPolicies/${local.access_policy_name}"
+ description = "Terraform managed."
+ name = "accessPolicies/${local.access_policy_name}/servicePerimeters/${each.key}"
+ title = each.key
+ perimeter_type = each.value.type
+
+ # Enforced mode configuration
+ dynamic "status" {
+ for_each = try(lookup(var.perimeter_projects, each.key, {}).enforced, []) != null ? [""] : []
+
+ content {
+ resources = formatlist("projects/%s", try(lookup(var.perimeter_projects, each.key, {}).enforced, []))
+ }
+ }
+
+ # Dry run mode configuration
+ dynamic "spec" {
+ for_each = try(lookup(var.perimeter_projects, each.key, {}).dry_run, []) != null ? [""] : []
+
+ content {
+ resources = formatlist("projects/%s", try(lookup(var.perimeter_projects, each.key, {}).dry_run, []))
+ }
+ }
+
+ # Uncomment if used alongside `google_access_context_manager_service_perimeter_resource`,
+ # so they don't fight over which resources should be in the policy.
+ # lifecycle {
+ # ignore_changes = [status[0].resources]
+ # }
+
+ depends_on = [
+ google_access_context_manager_service_perimeter.standard,
+ google_access_context_manager_access_level.default,
+ ]
+}
diff --git a/modules/vpc-sc/outputs.tf b/modules/vpc-sc/outputs.tf
new file mode 100644
index 00000000..e3e56c1c
--- /dev/null
+++ b/modules/vpc-sc/outputs.tf
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2020 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.
+ */
+
+output "org_id" {
+ description = "Organization id dependent on module resources."
+ value = var.org_id
+ depends_on = [
+ google_organization_iam_audit_config,
+ google_organization_iam_binding.authoritative,
+ google_organization_iam_custom_role.roles,
+ google_organization_iam_member.additive,
+ google_organization_policy.boolean,
+ google_organization_policy.list
+ ]
+}
+
+output "access_policy_name" {
+ description = "Access Policy resource"
+ value = local.access_policy_name
+}
+
+output "access_levels" {
+ description = "Access Levels."
+ value = {
+ for key, value in google_access_context_manager_access_level.default :
+ key => value
+ }
+}
+
+output "perimeters_standard" {
+ description = "VPC-SC standard perimeter resources."
+ value = {
+ for key, value in google_access_context_manager_service_perimeter.standard :
+ key => value
+ }
+}
+
+output "perimeters_bridge" {
+ description = "VPC-SC bridge perimeter resources."
+ value = {
+ for key, value in google_access_context_manager_service_perimeter.bridge :
+ key => value
+ }
+}
diff --git a/modules/vpc-sc/variables.tf b/modules/vpc-sc/variables.tf
new file mode 100644
index 00000000..adcb6b79
--- /dev/null
+++ b/modules/vpc-sc/variables.tf
@@ -0,0 +1,66 @@
+/**
+ * Copyright 2020 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.
+ */
+
+variable "access_levels" {
+ description = "Access Levels."
+ type = map(object({
+ combining_function = string
+ conditions = list(object({
+ ip_subnetworks = list(string)
+ members = list(string)
+ negate = string
+ }))
+ }))
+ default = {}
+}
+
+variable "access_level_perimeters" {
+ description = "Enforced mode -> Access Level -> Perimeters mapping. Enforced mode can be 'enforced' or 'dry_run'"
+ type = map(map(list(string)))
+ default = {}
+}
+
+variable "access_policy_title" {
+ description = "Access Policy title to be created."
+ type = string
+}
+
+variable "org_id" {
+ description = "Organization id in nnnnnn format."
+ type = number
+}
+
+variable "perimeters" {
+ description = "Set of Perimeters."
+ type = map(object({
+ type = string
+ dry_run_config = object({
+ restricted_services = list(string)
+ vpc_accessible_services = list(string)
+ })
+ enforced_config = object({
+ restricted_services = list(string)
+ vpc_accessible_services = list(string)
+ })
+ }))
+ default = {}
+}
+
+variable "perimeter_projects" {
+ description = "Perimeter -> Enforced Mode -> Projects Number mapping. Enforced mode can be 'enforced' or 'dry_run'."
+ type = map(map(list(number)))
+ default = {}
+}
diff --git a/modules/vpc-sc/versions.tf b/modules/vpc-sc/versions.tf
new file mode 100644
index 00000000..bc4c2a9d
--- /dev/null
+++ b/modules/vpc-sc/versions.tf
@@ -0,0 +1,19 @@
+/**
+ * Copyright 2020 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.
+ */
+
+terraform {
+ required_version = ">= 0.12.6"
+}