diff --git a/modules/cloud-run/README.md b/modules/cloud-run/README.md
new file mode 100644
index 000000000..9096cd90b
--- /dev/null
+++ b/modules/cloud-run/README.md
@@ -0,0 +1,166 @@
+# Cloud Run Module
+
+Cloud Run management, with support for IAM roles and optional Eventarc trigger creation.
+
+## Examples
+
+### Traffic split
+
+This deploys a Cloud Run service with traffic split between two revisions.
+
+```hcl
+module "cloud_run" {
+ source = "../../modules/cloud-run"
+ project_id = "my-project"
+ name = "hello"
+ revision_name = "green"
+ containers = [{
+ image = "us-docker.pkg.dev/cloudrun/container/hello"
+ command = null
+ args = null
+ env = null
+ env_from = null
+ ports = null
+ resources = null
+ volume_mounts = null
+ }]
+ traffic = {
+ "blue" = 25
+ "green" = 75
+ }
+}
+# tftest:skip
+```
+
+### Eventarc trigger (Pub/Sub)
+
+This deploys a Cloud Run service that will be triggered when messages are published to Pub/Sub topics.
+
+```hcl
+module "cloud_run" {
+ source = "../../modules/cloud-run"
+ project_id = "my-project"
+ name = "hello"
+ containers = [{
+ image = "us-docker.pkg.dev/cloudrun/container/hello"
+ command = null
+ args = null
+ env = null
+ env_from = null
+ ports = null
+ resources = null
+ volume_mounts = null
+ }]
+ pub_sub_triggers = [
+ "topic1",
+ "topic2"
+ ]
+}
+# tftest:skip
+```
+
+### Eventarc trigger (Audit logs)
+
+This deploys a Cloud Run service that will be triggered when specific log events are written to Google Cloud audit logs.
+
+module "cloud_run" {
+ source = "../../modules/cloud-run"
+ project_id = "my-project"
+ name = "hello"
+ containers = [{
+ image = "us-docker.pkg.dev/cloudrun/container/hello"
+ command = null
+ args = null
+ env = null
+ env_from = null
+ ports = null
+ resources = null
+ volume_mounts = null
+ }]
+ audit_log_triggers = [
+ {
+ service_name = "cloudresourcemanager.googleapis.com"
+ method_name = "SetIamPolicy"
+ }
+ ]
+}
+
+### Service account management
+
+To use a custom service account managed by the module, set `service_account_create` to `true` and leave `service_account` set to `null` value (default).
+
+```hcl
+module "cloud_run" {
+ source = "../../modules/cloud-run"
+ project_id = "my-project"
+ name = "hello"
+ containers = [{
+ image = "us-docker.pkg.dev/cloudrun/container/hello"
+ command = null
+ args = null
+ env = null
+ env_from = null
+ ports = null
+ resources = null
+ volume_mounts = null
+ }]
+ service_account_create = true
+}
+# tftest:skip
+```
+
+To use an externally managed service account, pass its email in `service_account` and leave `service_account_create` to `false` (the default).
+
+```hcl
+module "cloud_run" {
+ source = "../../modules/cloud-run"
+ project_id = "my-project"
+ name = "hello"
+ containers = [{
+ image = "us-docker.pkg.dev/cloudrun/container/hello"
+ command = null
+ args = null
+ env = null
+ env_from = null
+ ports = null
+ resources = null
+ volume_mounts = null
+ }]
+ service_account = local.service_account_email
+}
+# tftest:skip
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---: |:---:|:---:|
+| containers | Containers | list(object({...})) | ✓ | |
+| name | Name used for cloud run service | string | ✓ | |
+| project_id | Project id used for all resources. | string | ✓ | |
+| *audit_log_triggers* | Event arc triggers (Audit log) | list(object({...})) | | null |
+| *iam* | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| *ingress_settings* | Ingress settings | string | | null |
+| *labels* | Resource labels | map(string) | | {} |
+| *prefix* | Optional prefix used for resource names. | string | | null |
+| *pubsub_triggers* | Eventarc triggers (Pub/Sub) | list(string) | | null |
+| *region* | Region used for all resources. | string | | europe-west1 |
+| *revision_name* | Revision name | string | | null |
+| *service_account* | Service account email. Unused if service account is auto-created. | string | | null |
+| *service_account_create* | Auto-create service account. | bool | | false |
+| *traffic* | Traffic | map(number) | | null |
+| *volumes* | Volumes | list(object({...})) | | null |
+| *vpc_connector_config* | VPC connector configuration. Set `create_config` attributes to trigger creation. | object({...}) | | null |
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| service | Cloud Run service | |
+| service_account | Service account resource. | |
+| service_account_email | Service account email. | |
+| service_account_iam_email | Service account email. | |
+| service_name | Cloud Run service name | |
+| vpc_connector | VPC connector resource if created. | |
+
diff --git a/modules/cloud-run/main.tf b/modules/cloud-run/main.tf
new file mode 100644
index 000000000..907385162
--- /dev/null
+++ b/modules/cloud-run/main.tf
@@ -0,0 +1,212 @@
+/**
+ * Copyright 2021 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 {
+ prefix = var.prefix == null ? "" : "${var.prefix}-"
+ service_account_email = (
+ var.service_account_create
+ ? (
+ length(google_service_account.service_account) > 0
+ ? google_service_account.service_account[0].email
+ : null
+ )
+ : var.service_account
+ )
+
+ annotations = merge(var.ingress_settings == null ? {} : { "run.googleapis.com/ingress" = var.ingress_settings },
+ var.vpc_connector_config == null
+ ? {}
+ : try(var.vpc_connector_config.ip_cidr_range, null) == null
+ ? { "run.googleapis.com/vpc-access-connector" = var.vpc_connector_config.name }
+ : { "run.googleapis.com/vpc-access-connector" = google_vpc_access_connector.connector.0.id }
+ ,
+ try(var.vpc_connector_config.egress_settings, null) == null
+ ? {}
+ : { "run.googleapis.com/vpc-access-egress" = var.vpc_connector_config.egress_settings })
+}
+
+resource "google_vpc_access_connector" "connector" {
+ count = try(var.vpc_connector_config.ip_cidr_range, null) == null ? 0 : 1
+ project = var.project_id
+ name = var.vpc_connector_config.name
+ region = var.region
+ ip_cidr_range = var.vpc_connector_config.ip_cidr_range
+ network = var.vpc_connector_config.network
+}
+
+resource "google_cloud_run_service" "service" {
+ provider = google-beta
+ project = var.project_id
+ location = var.region
+ name = "${local.prefix}${var.name}"
+
+ template {
+ spec {
+ dynamic "containers" {
+ for_each = var.containers == null ? [] : var.containers
+ content {
+ image = containers.value["image"]
+ command = containers.value["command"]
+ args = containers.value["args"]
+ dynamic "env" {
+ for_each = containers.value["env"] == null ? {} : containers.value["env"]
+ content {
+ name = env.key
+ value = env.value
+ }
+ }
+ dynamic "env" {
+ for_each = containers.value["env_from"] == null ? {} : containers.value["env_from"]
+ content {
+ name = env.key
+ value_from {
+ secret_key_ref {
+ name = env.value["name"]
+ key = env.value["key"]
+ }
+ }
+ }
+ }
+ dynamic "ports" {
+ for_each = containers.value["ports"] == null ? {} : { for port in containers.value["ports"] : "${port.name}-${port.protocol}-${port.container_port}" => port }
+ content {
+ name = ports.value["name"]
+ protocol = ports.value["protocol"]
+ container_port = ports.value["container_port"]
+ }
+ }
+ dynamic "resources" {
+ for_each = containers.value["resources"] == null ? [] : [""]
+ content {
+ limits = containers.value["resources"]["limits"]
+ requests = containers.value["resources"]["requests"]
+ }
+ }
+ dynamic "volume_mounts" {
+ for_each = containers.value["volume_mounts"] == null ? [] : containers.value["volume_mounts"]
+ content {
+ name = volume_mounts.value["name"]
+ mount_path = volume_mounts.value["mount_path"]
+ }
+ }
+ }
+ }
+ service_account_name = local.service_account_email
+ dynamic "volumes" {
+ for_each = var.volumes == null ? [] : var.volumes
+ content {
+ name = volumes.value["name"]
+ secret {
+ secret_name = volumes.value["secret_name"]
+ dynamic "items" {
+ for_each = volumes.value["items"] == null ? [] : volumes.value["items"]
+ content {
+ key = items.value["key"]
+ path = items.value["path"]
+ }
+ }
+ }
+ }
+ }
+ }
+ dynamic "metadata" {
+ for_each = var.revision_name == null ? [] : [""]
+ content {
+ name = "${var.name}-${var.revision_name}"
+ }
+ }
+ }
+
+
+ metadata {
+ annotations = local.annotations
+ }
+
+ dynamic "traffic" {
+ for_each = var.traffic == null ? {} : var.traffic
+ content {
+ percent = traffic.value
+ revision_name = "${var.name}-${traffic.key}"
+ }
+ }
+
+}
+
+resource "google_cloud_run_service_iam_binding" "binding" {
+ for_each = var.iam
+ project = google_cloud_run_service.service.project
+ location = google_cloud_run_service.service.location
+ service = google_cloud_run_service.service.name
+ role = each.key
+ members = each.value
+}
+
+resource "google_service_account" "service_account" {
+ count = var.service_account_create ? 1 : 0
+ project = var.project_id
+ account_id = "tf-cr-${var.name}"
+ display_name = "Terraform Cloud Run ${var.name}."
+}
+
+resource "google_eventarc_trigger" "audit_log_triggers" {
+ for_each = var.audit_log_triggers == null ? {} : { for trigger in var.audit_log_triggers : "${trigger.service_name}-${trigger.method_name}" => trigger }
+ name = "${local.prefix}${each.key}-audit-log-trigger"
+ location = google_cloud_run_service.service.location
+ project = google_cloud_run_service.service.project
+ matching_criteria {
+ attribute = "type"
+ value = "google.cloud.audit.log.v1.written"
+ }
+ matching_criteria {
+ attribute = "serviceName"
+ value = each.value["service_name"]
+ }
+ matching_criteria {
+ attribute = "methodName"
+ value = each.value["method_name"]
+ }
+ destination {
+ cloud_run_service {
+ service = google_cloud_run_service.service.name
+ region = google_cloud_run_service.service.location
+ }
+ }
+}
+
+resource "google_eventarc_trigger" "pubsub_triggers" {
+ for_each = var.pubsub_triggers == null ? [] : toset(var.pubsub_triggers)
+ name = each.value == "" ? "${local.prefix}default-pubsub-trigger" : "${local.prefix}${each.value}-pubsub-trigger"
+ location = google_cloud_run_service.service.location
+ project = google_cloud_run_service.service.project
+ matching_criteria {
+ attribute = "type"
+ value = "google.cloud.pubsub.topic.v1.messagePublished"
+ }
+ dynamic "transport" {
+ for_each = each.value == null ? [] : [""]
+ content {
+ pubsub {
+ topic = each.value
+ }
+ }
+ }
+ destination {
+ cloud_run_service {
+ service = google_cloud_run_service.service.name
+ region = google_cloud_run_service.service.location
+ }
+ }
+}
diff --git a/modules/cloud-run/outputs.tf b/modules/cloud-run/outputs.tf
new file mode 100644
index 000000000..4caaef0e0
--- /dev/null
+++ b/modules/cloud-run/outputs.tf
@@ -0,0 +1,50 @@
+
+/**
+ * Copyright 2021 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 "service" {
+ description = "Cloud Run service"
+ value = google_cloud_run_service.service
+}
+
+output "service_account" {
+ description = "Service account resource."
+ value = try(google_service_account.service_account[0], null)
+}
+
+output "service_account_email" {
+ description = "Service account email."
+ value = local.service_account_email
+}
+
+output "service_account_iam_email" {
+ description = "Service account email."
+ value = join("", [
+ "serviceAccount:",
+ local.service_account_email == null ? "" : local.service_account_email
+ ])
+}
+
+output "service_name" {
+ description = "Cloud Run service name"
+ value = google_cloud_run_service.service.name
+}
+
+
+output "vpc_connector" {
+ description = "VPC connector resource if created."
+ value = try(google_vpc_access_connector.connector.0.id, null)
+}
\ No newline at end of file
diff --git a/modules/cloud-run/variables.tf b/modules/cloud-run/variables.tf
new file mode 100644
index 000000000..a6fc896d1
--- /dev/null
+++ b/modules/cloud-run/variables.tf
@@ -0,0 +1,152 @@
+
+/**
+ * Copyright 2021 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 "audit_log_triggers" {
+ description = "Event arc triggers (Audit log)"
+ type = list(object({
+ service_name = string
+ method_name = string
+ }))
+ default = null
+}
+
+variable "containers" {
+ description = "Containers"
+ type = list(object({
+ image = string
+ command = list(string)
+ args = list(string)
+ env = map(string)
+ env_from = map(object({
+ key = string
+ name = string
+ }))
+ resources = object({
+ limits = object({
+ cpu = string
+ memory = string
+ })
+ requests = object({
+ cpu = string
+ memory = string
+ })
+ })
+ ports = list(object({
+ name = string
+ protocol = string
+ container_port = string
+ }))
+ volume_mounts = list(object({
+ name = string
+ mount_path = string
+ }))
+ }))
+}
+
+variable "iam" {
+ description = "IAM bindings for topic in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+}
+
+variable "ingress_settings" {
+ description = "Ingress settings"
+ type = string
+ default = null
+}
+
+variable "labels" {
+ description = "Resource labels"
+ type = map(string)
+ default = {}
+}
+
+variable "name" {
+ description = "Name used for cloud run service"
+ type = string
+}
+
+variable "prefix" {
+ description = "Optional prefix used for resource names."
+ type = string
+ default = null
+}
+
+variable "project_id" {
+ description = "Project id used for all resources."
+ type = string
+}
+
+variable "pubsub_triggers" {
+ description = "Eventarc triggers (Pub/Sub)"
+ type = list(string)
+ default = null
+}
+
+variable "region" {
+ description = "Region used for all resources."
+ type = string
+ default = "europe-west1"
+}
+
+variable "revision_name" {
+ description = "Revision name"
+ type = string
+ default = null
+}
+
+variable "service_account" {
+ description = "Service account email. Unused if service account is auto-created."
+ type = string
+ default = null
+}
+
+variable "service_account_create" {
+ description = "Auto-create service account."
+ type = bool
+ default = false
+}
+
+variable "traffic" {
+ description = "Traffic"
+ type = map(number)
+ default = null
+}
+
+variable "volumes" {
+ description = "Volumes"
+ type = list(object({
+ name = string
+ secret_name = string
+ items = list(object({
+ key = string
+ path = string
+ }))
+ }))
+ default = null
+}
+
+variable "vpc_connector_config" {
+ description = "VPC connector configuration. Set `create_config` attributes to trigger creation."
+ type = object({
+ egress_settings = string
+ name = string
+ ip_cidr_range = string
+ network = string
+ })
+ default = null
+}
diff --git a/modules/cloud-run/versions.tf b/modules/cloud-run/versions.tf
new file mode 100644
index 000000000..72cab149d
--- /dev/null
+++ b/modules/cloud-run/versions.tf
@@ -0,0 +1,20 @@
+
+/**
+ * Copyright 2021 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"
+}
\ No newline at end of file
diff --git a/tests/modules/cloud_run/__init__.py b/tests/modules/cloud_run/__init__.py
new file mode 100644
index 000000000..bb2436ab6
--- /dev/null
+++ b/tests/modules/cloud_run/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2021 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.
+
+
diff --git a/tests/modules/cloud_run/fixture/bundle/main.py b/tests/modules/cloud_run/fixture/bundle/main.py
new file mode 100644
index 000000000..0446db3c4
--- /dev/null
+++ b/tests/modules/cloud_run/fixture/bundle/main.py
@@ -0,0 +1,13 @@
+# Copyright 2021 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.
\ No newline at end of file
diff --git a/tests/modules/cloud_run/fixture/main.tf b/tests/modules/cloud_run/fixture/main.tf
new file mode 100644
index 000000000..318ad4fc6
--- /dev/null
+++ b/tests/modules/cloud_run/fixture/main.tf
@@ -0,0 +1,43 @@
+# Copyright 2021 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.
+
+module "cloud_run" {
+ source = "../../../../modules/cloud-run"
+ project_id = "my-project"
+ name = "hello"
+ revision_name = "blue"
+ containers = [{
+ image = "us-docker.pkg.dev/cloudrun/container/hello"
+ command = null
+ args = null
+ env = null
+ env_from = null
+ ports = null
+ resources = null
+ volume_mounts = null
+ }]
+ audit_log_triggers = [
+ {
+ "service_name" : "cloudresourcemanager.googleapis.com",
+ "method_name" : "SetIamPolicy"
+ }
+ ]
+ pubsub_triggers = [
+ "topic1",
+ "topic2"
+ ]
+ iam = {
+ "roles/run.invoker" = ["allUsers"]
+ }
+}
diff --git a/tests/modules/cloud_run/fixture/variables.tf b/tests/modules/cloud_run/fixture/variables.tf
new file mode 100644
index 000000000..0446db3c4
--- /dev/null
+++ b/tests/modules/cloud_run/fixture/variables.tf
@@ -0,0 +1,13 @@
+# Copyright 2021 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.
\ No newline at end of file
diff --git a/tests/modules/cloud_run/test_plan.py b/tests/modules/cloud_run/test_plan.py
new file mode 100644
index 000000000..13cd3ecb1
--- /dev/null
+++ b/tests/modules/cloud_run/test_plan.py
@@ -0,0 +1,50 @@
+# Copyright 2021 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.
+
+
+import os
+import pytest
+
+
+FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture')
+
+
+@pytest.fixture
+def resources(plan_runner):
+ _, resources = plan_runner(FIXTURES_DIR)
+ return resources
+
+
+def test_resource_count(resources):
+ "Test number of resources created."
+ assert len(resources) == 5
+
+def test_iam(resources):
+ "Test IAM binding resources."
+ bindings = [r['values'] for r in resources if r['type']
+ == 'google_cloud_run_service_iam_binding']
+ assert len(bindings) == 1
+ assert bindings[0]['role'] == 'roles/run.invoker'
+
+def test_audit_log_triggers(resources):
+ "Test audit logs Eventarc trigger resources."
+ audit_log_triggers = [r['values'] for r in resources if r['type']
+ == 'google_eventarc_trigger' and r['name'] == 'audit_log_triggers']
+ assert len(audit_log_triggers) == 1
+
+def test_pubsub_triggers(resources):
+ "Test Pub/Sub Eventarc trigger resources."
+ pubsub_triggers = [r['values'] for r in resources if r['type']
+ == 'google_eventarc_trigger' and r['name'] == 'pubsub_triggers']
+ assert len(pubsub_triggers) == 2