From 5056586e572c4c7a70ab56f45102e98fd29edd54 Mon Sep 17 00:00:00 2001 From: apichick Date: Sun, 31 Oct 2021 22:40:28 +0100 Subject: [PATCH] Added cloud-run module --- modules/cloud-run/README.md | 166 ++++++++++++++ modules/cloud-run/main.tf | 212 ++++++++++++++++++ modules/cloud-run/outputs.tf | 50 +++++ modules/cloud-run/variables.tf | 152 +++++++++++++ modules/cloud-run/versions.tf | 20 ++ tests/modules/cloud_run/__init__.py | 15 ++ .../modules/cloud_run/fixture/bundle/main.py | 13 ++ tests/modules/cloud_run/fixture/main.tf | 43 ++++ tests/modules/cloud_run/fixture/variables.tf | 13 ++ tests/modules/cloud_run/test_plan.py | 50 +++++ 10 files changed, 734 insertions(+) create mode 100644 modules/cloud-run/README.md create mode 100644 modules/cloud-run/main.tf create mode 100644 modules/cloud-run/outputs.tf create mode 100644 modules/cloud-run/variables.tf create mode 100644 modules/cloud-run/versions.tf create mode 100644 tests/modules/cloud_run/__init__.py create mode 100644 tests/modules/cloud_run/fixture/bundle/main.py create mode 100644 tests/modules/cloud_run/fixture/main.tf create mode 100644 tests/modules/cloud_run/fixture/variables.tf create mode 100644 tests/modules/cloud_run/test_plan.py diff --git a/modules/cloud-run/README.md b/modules/cloud-run/README.md new file mode 100644 index 00000000..9096cd90 --- /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 00000000..90738516 --- /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 00000000..4caaef0e --- /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 00000000..a6fc896d --- /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 00000000..72cab149 --- /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 00000000..bb2436ab --- /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 00000000..0446db3c --- /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 00000000..318ad4fc --- /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 00000000..0446db3c --- /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 00000000..13cd3ecb --- /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