service_account_config for Cloud Run v2

Additional changes:
* align vpc-connector interface to Cloud Functions
* split managed and unmanaged resources into separate files, this makes
  easier to introduce further changes
* add support for contexts
* move `vpc_connector` variable to variables.tf for Cloud Functions
* remove `create` from `vpc_connector` in Cloud Functions as it was
  sharing the meaning with `vpc_connector_create`
This commit is contained in:
Wiktor Niesiobędzki
2025-10-26 14:33:26 +00:00
parent 5d46e8b86b
commit 2e42c1b548
66 changed files with 2484 additions and 1543 deletions

View File

@@ -34,7 +34,7 @@ IAM bindings support the usual syntax. Container environment values can be decla
module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
name = "hello"
name = "example-hello"
region = var.region
containers = {
hello = {
@@ -65,7 +65,7 @@ module "cloud_run" {
module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
name = "hello"
name = "example-hello"
region = var.region
containers = {
hello = {
@@ -95,7 +95,7 @@ module "cloud_run" {
module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
name = "hello"
name = "example-hello"
region = var.region
containers = {
hello = {
@@ -132,7 +132,7 @@ module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
region = var.region
name = "hello"
name = "example-hello"
containers = {
hello = {
image = "us-docker.pkg.dev/cloudrun/container/hello"
@@ -157,7 +157,7 @@ module "cloud_run" {
module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
name = "hello"
name = "example-hello"
region = var.region
containers = {
hello = {
@@ -190,7 +190,7 @@ module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
region = var.regions.secondary
name = "hello"
name = "example-hello"
containers = {
hello = {
image = "us-docker.pkg.dev/cloudrun/container/hello"
@@ -204,7 +204,7 @@ module "cloud_run" {
}
deletion_protection = false
}
# tftest modules=1 resources=2 fixtures=fixtures/vpc-connector.tf inventory=service-vpc-access-connector.yaml e2e
# tftest fixtures=fixtures/vpc-connector.tf inventory=service-vpc-access-connector.yaml e2e
```
If creation of the VPC Access Connector is required, use the `vpc_connector_create` variable which also supports optional attributes like number of instances, machine type, or throughput. The connector will be used automatically by Cloud Run Service and Job. Worker Pool does not support connector.
@@ -214,7 +214,7 @@ module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
region = var.region
name = "hello"
name = "example-hello"
containers = {
hello = {
image = "us-docker.pkg.dev/cloudrun/container/hello"
@@ -240,7 +240,7 @@ module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = module.project-service.project_id
region = var.region
name = "hello"
name = "example-hello"
containers = {
hello = {
image = "us-docker.pkg.dev/cloudrun/container/hello"
@@ -301,7 +301,7 @@ module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = module.project.project_id
region = var.region
name = "hello"
name = "example-hello"
encryption_key = module.kms.keys.key-regional.id
containers = {
hello = {
@@ -310,7 +310,7 @@ module "cloud_run" {
}
deletion_protection = false
}
# tftest modules=3 resources=11 e2e
# tftest inventory=cmek.yaml e2e
```
## Deploying OpenTelemetry Collector sidecar
@@ -509,7 +509,7 @@ module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
region = var.region
name = "hello"
name = "example-hello"
containers = {
hello = {
image = "us-docker.pkg.dev/cloudrun/container/hello"
@@ -568,7 +568,7 @@ module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
region = var.region
name = "hello"
name = "example-hello"
containers = {
hello = {
image = "us-docker.pkg.dev/cloudrun/container/hello"
@@ -595,7 +595,7 @@ module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
region = var.region
name = "hello"
name = "example-hello"
containers = {
hello = {
image = "us-docker.pkg.dev/cloudrun/container/hello"
@@ -636,7 +636,7 @@ module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
region = var.region
name = "hello"
name = "example-hello"
containers = {
hello = {
image = "us-docker.pkg.dev/cloudrun/container/hello"
@@ -684,7 +684,7 @@ module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
region = var.region
name = "hello"
name = "example-hello"
containers = {
hello = {
image = "us-docker.pkg.dev/cloudrun/container/hello"
@@ -700,42 +700,54 @@ module "cloud_run" {
## Cloud Run Service Account
To use a custom service account managed by the module, set `service_account_create` to `true` and leave `service_account` set to `null` (default).
The module by default creates a service account that is associated with the Cloud Run instance. It grants the service account `roles/logging.logWriter` and `roles/monitoring.metricWriter` roles.
To assign non-default roles, pass them as `service_account_config.roles`.
```hcl
module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
region = var.region
name = "hello"
name = "example-hello"
containers = {
hello = {
image = "us-docker.pkg.dev/cloudrun/container/hello"
}
}
service_account_create = true
deletion_protection = false
service_account_config = {
roles = [
"roles/logging.logWriter",
"roles/monitoring.metricWriter",
"roles/cloudsql.client",
"roles/cloudsql.instanceUser",
]
}
deletion_protection = false
}
# tftest inventory=service-sa-create.yaml e2e
```
To use an externally managed service account, use its email in `service_account` and leave `service_account_create` to `false` (default).
To use externally managed service account, pass its email in `service_account_config.email` and set `service_account_config.email` to `false`.
```hcl
module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
region = var.region
name = "hello"
name = "example-hello"
containers = {
hello = {
image = "us-docker.pkg.dev/cloudrun/container/hello"
}
}
service_account = module.iam-service-account.email
service_account_config = {
create = false
email = module.iam-service-account.email
}
deletion_protection = false
}
# tftest modules=2 resources=2 fixtures=fixtures/iam-service-account.tf inventory=service-external-sa.yaml e2e
# tftest fixtures=fixtures/iam-service-account.tf inventory=service-external-sa.yaml e2e
```
## Creating Cloud Run Jobs
@@ -762,7 +774,7 @@ Additional configuration can be passwed as `job_config`:
module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
name = "hello"
name = "example-hello"
region = var.region
type = "JOB"
containers = {
@@ -854,7 +866,7 @@ IAP is only supported for service. Refer to the [Configure IAP directly on cloud
module "cloud_run" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
name = "hello"
name = "example-hello"
region = var.region
launch_stage = "BETA"
containers = {
@@ -869,7 +881,7 @@ module "cloud_run" {
}
deletion_protection = false
}
# tftest modules=1 resources=2 e2e
# tftest inventory=iap.yaml e2e
```
## Adding GPUs
@@ -880,7 +892,7 @@ GPU support is available for all types of Cloud Run resources: jobs, services an
module "job" {
source = "./fabric/modules/cloud-run-v2"
project_id = var.project_id
name = "job"
name = "example-job"
region = var.region
launch_stage = "BETA"
revision = {
@@ -974,26 +986,26 @@ module "worker" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [name](variables.tf#L160) | Name used for Cloud Run service. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L165) | Project id used for all resources. | <code>string</code> | ✓ | |
| [region](variables.tf#L170) | Region used for all resources. | <code>string</code> | ✓ | |
| [name](variables.tf#L178) | Name used for Cloud Run service. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L183) | Project id used for all resources. | <code>string</code> | ✓ | |
| [region](variables.tf#L188) | Region used for all resources. | <code>string</code> | ✓ | |
| [containers](variables.tf#L17) | Containers in name => attributes format. | <code title="map&#40;object&#40;&#123;&#10; image &#61; string&#10; depends_on &#61; optional&#40;list&#40;string&#41;&#41;&#10; command &#61; optional&#40;list&#40;string&#41;&#41;&#10; args &#61; optional&#40;list&#40;string&#41;&#41;&#10; env &#61; optional&#40;map&#40;string&#41;&#41;&#10; env_from_key &#61; optional&#40;map&#40;object&#40;&#123;&#10; secret &#61; string&#10; version &#61; string&#10; &#125;&#41;&#41;&#41;&#10; liveness_probe &#61; optional&#40;object&#40;&#123;&#10; grpc &#61; optional&#40;object&#40;&#123;&#10; port &#61; optional&#40;number&#41;&#10; service &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; http_get &#61; optional&#40;object&#40;&#123;&#10; http_headers &#61; optional&#40;map&#40;string&#41;&#41;&#10; path &#61; optional&#40;string&#41;&#10; port &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; failure_threshold &#61; optional&#40;number&#41;&#10; initial_delay_seconds &#61; optional&#40;number&#41;&#10; period_seconds &#61; optional&#40;number&#41;&#10; timeout_seconds &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; ports &#61; optional&#40;map&#40;object&#40;&#123;&#10; container_port &#61; optional&#40;number&#41;&#10; name &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#41;&#10; resources &#61; optional&#40;object&#40;&#123;&#10; limits &#61; optional&#40;map&#40;string&#41;&#41;&#10; cpu_idle &#61; optional&#40;bool&#41;&#10; startup_cpu_boost &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#10; startup_probe &#61; optional&#40;object&#40;&#123;&#10; grpc &#61; optional&#40;object&#40;&#123;&#10; port &#61; optional&#40;number&#41;&#10; service &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; http_get &#61; optional&#40;object&#40;&#123;&#10; http_headers &#61; optional&#40;map&#40;string&#41;&#41;&#10; path &#61; optional&#40;string&#41;&#10; port &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; tcp_socket &#61; optional&#40;object&#40;&#123;&#10; port &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; failure_threshold &#61; optional&#40;number&#41;&#10; initial_delay_seconds &#61; optional&#40;number&#41;&#10; period_seconds &#61; optional&#40;number&#41;&#10; timeout_seconds &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; volume_mounts &#61; optional&#40;map&#40;string&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [deletion_protection](variables.tf#L97) | Deletion protection setting for this Cloud Run service. | <code>string</code> | | <code>null</code> |
| [encryption_key](variables.tf#L103) | The full resource name of the Cloud KMS CryptoKey. | <code>string</code> | | <code>null</code> |
| [iam](variables.tf#L109) | IAM bindings for Cloud Run service in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [job_config](variables.tf#L115) | Cloud Run Job specific configuration. | <code title="object&#40;&#123;&#10; max_retries &#61; optional&#40;number&#41;&#10; task_count &#61; optional&#40;number&#41;&#10; timeout &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [labels](variables.tf#L130) | Resource labels. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [launch_stage](variables.tf#L136) | The launch stage as defined by Google Cloud Platform Launch Stages. | <code>string</code> | | <code>null</code> |
| [managed_revision](variables.tf#L153) | Whether the Terraform module should control the deployment of revisions. | <code>bool</code> | | <code>true</code> |
| [revision](variables.tf#L175) | Revision template configurations. | <code title="object&#40;&#123;&#10; gpu_zonal_redundancy_disabled &#61; optional&#40;bool&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; name &#61; optional&#40;string&#41;&#10; node_selector &#61; optional&#40;object&#40;&#123;&#10; accelerator &#61; string&#10; &#125;&#41;&#41;&#10; vpc_access &#61; optional&#40;object&#40;&#123;&#10; connector &#61; optional&#40;string&#41;&#10; egress &#61; optional&#40;string&#41;&#10; network &#61; optional&#40;string&#41;&#10; subnet &#61; optional&#40;string&#41;&#10; tags &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; timeout &#61; optional&#40;string&#41;&#10; gen2_execution_environment &#61; optional&#40;any&#41; &#35; DEPRECATED&#10; job &#61; optional&#40;any&#41; &#35; DEPRECATED&#10; max_concurrency &#61; optional&#40;any&#41; &#35; DEPRECATED&#10; max_instance_count &#61; optional&#40;any&#41; &#35; DEPRECATED&#10; min_instance_count &#61; optional&#40;any&#41; &#35; DEPRECATED&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_account](variables.tf#L236) | Service account email. Unused if service account is auto-created. | <code>string</code> | | <code>null</code> |
| [service_account_create](variables.tf#L242) | Auto-create service account. | <code>bool</code> | | <code>false</code> |
| [service_config](variables.tf#L248) | Cloud Run service specific configuration options. | <code title="object&#40;&#123;&#10; custom_audiences &#61; optional&#40;list&#40;string&#41;, null&#41;&#10; eventarc_triggers &#61; optional&#40;&#10; object&#40;&#123;&#10; audit_log &#61; optional&#40;map&#40;object&#40;&#123;&#10; method &#61; string&#10; service &#61; string&#10; &#125;&#41;&#41;&#41;&#10; pubsub &#61; optional&#40;map&#40;string&#41;&#41;&#10; storage &#61; optional&#40;map&#40;object&#40;&#123;&#10; bucket &#61; string&#10; path &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#41;&#10; service_account_email &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; gen2_execution_environment &#61; optional&#40;bool, false&#41;&#10; iap_config &#61; optional&#40;object&#40;&#123;&#10; iam &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; iam_additive &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;, null&#41;&#10; ingress &#61; optional&#40;string, null&#41;&#10; invoker_iam_disabled &#61; optional&#40;bool, false&#41;&#10; max_concurrency &#61; optional&#40;number&#41;&#10; scaling &#61; optional&#40;object&#40;&#123;&#10; max_instance_count &#61; optional&#40;number&#41;&#10; min_instance_count &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; timeout &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [tag_bindings](variables.tf#L311) | Tag bindings for this service, in key => tag value id format. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [type](variables.tf#L318) | Type of Cloud Run resource to deploy: JOB, SERVICE or WORKERPOOL. | <code>string</code> | | <code>&#34;SERVICE&#34;</code> |
| [volumes](variables.tf#L328) | Named volumes in containers in name => attributes format. | <code title="map&#40;object&#40;&#123;&#10; secret &#61; optional&#40;object&#40;&#123;&#10; name &#61; string&#10; default_mode &#61; optional&#40;string&#41;&#10; path &#61; optional&#40;string&#41;&#10; version &#61; optional&#40;string&#41;&#10; mode &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; cloud_sql_instances &#61; optional&#40;list&#40;string&#41;&#41;&#10; empty_dir_size &#61; optional&#40;string&#41;&#10; gcs &#61; optional&#40;object&#40;&#123;&#10; bucket &#61; string&#10; is_read_only &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#10; nfs &#61; optional&#40;object&#40;&#123;&#10; server &#61; string&#10; path &#61; optional&#40;string&#41;&#10; is_read_only &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [vpc_connector_create](variables-vpcconnector.tf#L17) | Populate this to create a Serverless VPC Access connector. | <code title="object&#40;&#123;&#10; ip_cidr_range &#61; optional&#40;string&#41;&#10; machine_type &#61; optional&#40;string&#41;&#10; name &#61; optional&#40;string&#41;&#10; network &#61; optional&#40;string&#41;&#10; instances &#61; optional&#40;object&#40;&#123;&#10; max &#61; optional&#40;number&#41;&#10; min &#61; optional&#40;number&#41;&#10; &#125;&#41;, &#123;&#125;&#10; &#41;&#10; throughput &#61; optional&#40;object&#40;&#123;&#10; max &#61; optional&#40;number&#41;&#10; min &#61; optional&#40;number&#41;&#10; &#125;&#41;, &#123;&#125;&#10; &#41;&#10; subnet &#61; optional&#40;object&#40;&#123;&#10; name &#61; optional&#40;string&#41;&#10; project_id &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [workerpool_config](variables.tf#L362) | Cloud Run Worker Pool specific configuration. | <code title="object&#40;&#123;&#10; scaling &#61; optional&#40;object&#40;&#123;&#10; manual_instance_count &#61; optional&#40;number&#41;&#10; max_instance_count &#61; optional&#40;number&#41;&#10; min_instance_count &#61; optional&#40;number&#41;&#10; mode &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [context](variables.tf#L97) | Context-specific interpolations. | <code title="object&#40;&#123;&#10; condition_vars &#61; optional&#40;map&#40;map&#40;string&#41;&#41;, &#123;&#125;&#41; &#35; not needed here&#63;&#10; cidr_ranges &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; custom_roles &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; kms_keys &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; locations &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; networks &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; project_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; subnets &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_values &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [deletion_protection](variables.tf#L115) | Deletion protection setting for this Cloud Run service. | <code>string</code> | | <code>null</code> |
| [encryption_key](variables.tf#L121) | The full resource name of the Cloud KMS CryptoKey. | <code>string</code> | | <code>null</code> |
| [iam](variables.tf#L127) | IAM bindings for Cloud Run service in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [job_config](variables.tf#L133) | Cloud Run Job specific configuration. | <code title="object&#40;&#123;&#10; max_retries &#61; optional&#40;number&#41;&#10; task_count &#61; optional&#40;number&#41;&#10; timeout &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [labels](variables.tf#L148) | Resource labels. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [launch_stage](variables.tf#L154) | The launch stage as defined by Google Cloud Platform Launch Stages. | <code>string</code> | | <code>null</code> |
| [managed_revision](variables.tf#L171) | Whether the Terraform module should control the deployment of revisions. | <code>bool</code> | | <code>true</code> |
| [revision](variables.tf#L193) | Revision template configurations. | <code title="object&#40;&#123;&#10; gpu_zonal_redundancy_disabled &#61; optional&#40;bool&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; name &#61; optional&#40;string&#41;&#10; node_selector &#61; optional&#40;object&#40;&#123;&#10; accelerator &#61; string&#10; &#125;&#41;&#41;&#10; vpc_access &#61; optional&#40;object&#40;&#123;&#10; connector &#61; optional&#40;string&#41;&#10; egress &#61; optional&#40;string&#41;&#10; network &#61; optional&#40;string&#41;&#10; subnet &#61; optional&#40;string&#41;&#10; tags &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; timeout &#61; optional&#40;string&#41;&#10; gen2_execution_environment &#61; optional&#40;any&#41; &#35; DEPRECATED&#10; job &#61; optional&#40;any&#41; &#35; DEPRECATED&#10; max_concurrency &#61; optional&#40;any&#41; &#35; DEPRECATED&#10; max_instance_count &#61; optional&#40;any&#41; &#35; DEPRECATED&#10; min_instance_count &#61; optional&#40;any&#41; &#35; DEPRECATED&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_account_config](variables-serviceaccount.tf#L17) | Service account configurations. | <code title="object&#40;&#123;&#10; create &#61; optional&#40;bool, true&#41;&#10; display_name &#61; optional&#40;string&#41;&#10; email &#61; optional&#40;string&#41;&#10; name &#61; optional&#40;string&#41;&#10; roles &#61; optional&#40;list&#40;string&#41;, &#91;&#10; &#34;roles&#47;logging.logWriter&#34;,&#10; &#34;roles&#47;monitoring.metricWriter&#34;&#10; &#93;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_config](variables.tf#L260) | Cloud Run service specific configuration options. | <code title="object&#40;&#123;&#10; custom_audiences &#61; optional&#40;list&#40;string&#41;, null&#41;&#10; eventarc_triggers &#61; optional&#40;&#10; object&#40;&#123;&#10; audit_log &#61; optional&#40;map&#40;object&#40;&#123;&#10; method &#61; string&#10; service &#61; string&#10; &#125;&#41;&#41;&#41;&#10; pubsub &#61; optional&#40;map&#40;string&#41;&#41;&#10; storage &#61; optional&#40;map&#40;object&#40;&#123;&#10; bucket &#61; string&#10; path &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#41;&#10; service_account_email &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; gen2_execution_environment &#61; optional&#40;bool, false&#41;&#10; iap_config &#61; optional&#40;object&#40;&#123;&#10; iam &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; iam_additive &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;, null&#41;&#10; ingress &#61; optional&#40;string, null&#41;&#10; invoker_iam_disabled &#61; optional&#40;bool, false&#41;&#10; max_concurrency &#61; optional&#40;number&#41;&#10; scaling &#61; optional&#40;object&#40;&#123;&#10; max_instance_count &#61; optional&#40;number&#41;&#10; min_instance_count &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; timeout &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [tag_bindings](variables.tf#L323) | Tag bindings for this service, in key => tag value id format. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [type](variables.tf#L330) | Type of Cloud Run resource to deploy: JOB, SERVICE or WORKERPOOL. | <code>string</code> | | <code>&#34;SERVICE&#34;</code> |
| [volumes](variables.tf#L340) | Named volumes in containers in name => attributes format. | <code title="map&#40;object&#40;&#123;&#10; secret &#61; optional&#40;object&#40;&#123;&#10; name &#61; string&#10; default_mode &#61; optional&#40;string&#41;&#10; path &#61; optional&#40;string&#41;&#10; version &#61; optional&#40;string&#41;&#10; mode &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; cloud_sql_instances &#61; optional&#40;list&#40;string&#41;&#41;&#10; empty_dir_size &#61; optional&#40;string&#41;&#10; gcs &#61; optional&#40;object&#40;&#123;&#10; bucket &#61; string&#10; is_read_only &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#10; nfs &#61; optional&#40;object&#40;&#123;&#10; server &#61; string&#10; path &#61; optional&#40;string&#41;&#10; is_read_only &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [vpc_connector_create](variables-vpcconnector.tf#L17) | VPC connector network configuration. Must be provided if new VPC connector is being created. | <code title="object&#40;&#123;&#10; ip_cidr_range &#61; optional&#40;string&#41;&#10; machine_type &#61; optional&#40;string&#41;&#10; name &#61; optional&#40;string&#41;&#10; network &#61; optional&#40;string&#41;&#10; instances &#61; optional&#40;object&#40;&#123;&#10; max &#61; optional&#40;number&#41;&#10; min &#61; optional&#40;number&#41;&#10; &#125;&#41;, &#123;&#125;&#10; &#41;&#10; throughput &#61; optional&#40;object&#40;&#123;&#10; max &#61; optional&#40;number&#41;&#10; min &#61; optional&#40;number&#41;&#10; &#125;&#41;, &#123;&#125;&#10; &#41;&#10; subnet &#61; optional&#40;object&#40;&#123;&#10; name &#61; optional&#40;string&#41;&#10; project_id &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [workerpool_config](variables.tf#L374) | Cloud Run Worker Pool specific configuration. | <code title="object&#40;&#123;&#10; scaling &#61; optional&#40;object&#40;&#123;&#10; manual_instance_count &#61; optional&#40;number&#41;&#10; max_instance_count &#61; optional&#40;number&#41;&#10; min_instance_count &#61; optional&#40;number&#41;&#10; mode &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs

View File

@@ -0,0 +1,223 @@
/**
* 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.
*/
resource "google_cloud_run_v2_job" "job" {
count = var.type == "JOB" && var.managed_revision ? 1 : 0
provider = google-beta
project = local.project_id
location = local.location
name = var.name
labels = var.labels
launch_stage = var.launch_stage
deletion_protection = var.deletion_protection
template {
labels = var.revision.labels
task_count = var.job_config.task_count
template {
encryption_key = var.encryption_key
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
dynamic "vpc_access" {
for_each = local.connector == null ? [] : [""]
content {
connector = local.connector
egress = try(var.revision.vpc_access.egress, null)
}
}
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet == null ? null : lookup(
local.ctx.subnets, var.revision.vpc_access.subnet,
var.revision.vpc_access.subnet
)
network = var.revision.vpc_access.network == null ? null : lookup(
local.ctx.networks, var.revision.vpc_access.network,
var.revision.vpc_access.network
)
tags = var.revision.vpc_access.tags
}
}
}
max_retries = var.job_config.max_retries
timeout = var.job_config.timeout
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
depends_on = containers.value.depends_on
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
}
}
dynamic "ports" {
for_each = coalesce(containers.value.ports, tomap({}))
content {
container_port = ports.value.container_port
name = ports.value.name
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
dynamic "startup_probe" {
for_each = containers.value.startup_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds
timeout_seconds = containers.value.startup_probe.timeout_seconds
period_seconds = containers.value.startup_probe.period_seconds
failure_threshold = containers.value.startup_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.startup_probe.http_get == null ? [] : [""]
content {
path = containers.value.startup_probe.http_get.path
port = containers.value.startup_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "tcp_socket" {
for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""]
content {
port = containers.value.startup_probe.tcp_socket.port
}
}
dynamic "grpc" {
for_each = containers.value.startup_probe.grpc == null ? [] : [""]
content {
port = containers.value.startup_probe.grpc.port
service = containers.value.startup_probe.grpc.service
}
}
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
}
lifecycle {
ignore_changes = [
template[0].annotations["run.googleapis.com/operation-id"],
]
}
}

View File

@@ -0,0 +1,227 @@
/**
* 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.
*/
resource "google_cloud_run_v2_job" "job_unmanaged" {
count = var.type == "JOB" && !var.managed_revision ? 1 : 0
provider = google-beta
project = local.project_id
location = local.location
name = var.name
labels = var.labels
launch_stage = var.launch_stage
deletion_protection = var.deletion_protection
template {
labels = var.revision.labels
task_count = var.job_config.task_count
template {
encryption_key = var.encryption_key
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
dynamic "vpc_access" {
for_each = local.connector == null ? [] : [""]
content {
connector = local.connector
egress = try(var.revision.vpc_access.egress, null)
}
}
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet == null ? null : lookup(
local.ctx.subnets, var.revision.vpc_access.subnet,
var.revision.vpc_access.subnet
)
network = var.revision.vpc_access.network == null ? null : lookup(
local.ctx.networks, var.revision.vpc_access.network,
var.revision.vpc_access.network
)
tags = var.revision.vpc_access.tags
}
}
}
max_retries = var.job_config.max_retries
timeout = var.job_config.timeout
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
depends_on = containers.value.depends_on
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
}
}
dynamic "ports" {
for_each = coalesce(containers.value.ports, tomap({}))
content {
container_port = ports.value.container_port
name = ports.value.name
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
dynamic "startup_probe" {
for_each = containers.value.startup_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds
timeout_seconds = containers.value.startup_probe.timeout_seconds
period_seconds = containers.value.startup_probe.period_seconds
failure_threshold = containers.value.startup_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.startup_probe.http_get == null ? [] : [""]
content {
path = containers.value.startup_probe.http_get.path
port = containers.value.startup_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "tcp_socket" {
for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""]
content {
port = containers.value.startup_probe.tcp_socket.port
}
}
dynamic "grpc" {
for_each = containers.value.startup_probe.grpc == null ? [] : [""]
content {
port = containers.value.startup_probe.grpc.port
service = containers.value.startup_probe.grpc.service
}
}
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
}
lifecycle {
ignore_changes = [
client,
client_version,
template[0].annotations["run.googleapis.com/operation-id"],
template[0].template,
template[0].labels
]
}
}

View File

@@ -14,419 +14,11 @@
* limitations under the License.
*/
resource "google_cloud_run_v2_job" "job" {
count = var.type == "JOB" && var.managed_revision ? 1 : 0
provider = google-beta
project = var.project_id
location = var.region
name = var.name
labels = var.labels
launch_stage = var.launch_stage
deletion_protection = var.deletion_protection
template {
labels = var.revision.labels
task_count = var.job_config.task_count
template {
encryption_key = var.encryption_key
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
dynamic "vpc_access" {
for_each = local.connector == null ? [] : [""]
content {
connector = local.connector
egress = try(var.revision.vpc_access.egress, null)
}
}
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet
network = var.revision.vpc_access.network
tags = var.revision.vpc_access.tags
}
}
}
max_retries = var.job_config.max_retries
timeout = var.job_config.timeout
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
depends_on = containers.value.depends_on
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
}
}
dynamic "ports" {
for_each = coalesce(containers.value.ports, tomap({}))
content {
container_port = ports.value.container_port
name = ports.value.name
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
dynamic "startup_probe" {
for_each = containers.value.startup_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds
timeout_seconds = containers.value.startup_probe.timeout_seconds
period_seconds = containers.value.startup_probe.period_seconds
failure_threshold = containers.value.startup_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.startup_probe.http_get == null ? [] : [""]
content {
path = containers.value.startup_probe.http_get.path
port = containers.value.startup_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "tcp_socket" {
for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""]
content {
port = containers.value.startup_probe.tcp_socket.port
}
}
dynamic "grpc" {
for_each = containers.value.startup_probe.grpc == null ? [] : [""]
content {
port = containers.value.startup_probe.grpc.port
service = containers.value.startup_probe.grpc.service
}
}
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
}
lifecycle {
ignore_changes = [
template[0].annotations["run.googleapis.com/operation-id"],
]
}
}
resource "google_cloud_run_v2_job" "job_unmanaged" {
count = var.type == "JOB" && !var.managed_revision ? 1 : 0
provider = google-beta
project = var.project_id
location = var.region
name = var.name
labels = var.labels
launch_stage = var.launch_stage
deletion_protection = var.deletion_protection
template {
labels = var.revision.labels
task_count = var.job_config.task_count
template {
encryption_key = var.encryption_key
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
dynamic "vpc_access" {
for_each = local.connector == null ? [] : [""]
content {
connector = local.connector
egress = try(var.revision.vpc_access.egress, null)
}
}
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet
network = var.revision.vpc_access.network
tags = var.revision.vpc_access.tags
}
}
}
max_retries = var.job_config.max_retries
timeout = var.job_config.timeout
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
depends_on = containers.value.depends_on
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
}
}
dynamic "ports" {
for_each = coalesce(containers.value.ports, tomap({}))
content {
container_port = ports.value.container_port
name = ports.value.name
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
dynamic "startup_probe" {
for_each = containers.value.startup_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds
timeout_seconds = containers.value.startup_probe.timeout_seconds
period_seconds = containers.value.startup_probe.period_seconds
failure_threshold = containers.value.startup_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.startup_probe.http_get == null ? [] : [""]
content {
path = containers.value.startup_probe.http_get.path
port = containers.value.startup_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "tcp_socket" {
for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""]
content {
port = containers.value.startup_probe.tcp_socket.port
}
}
dynamic "grpc" {
for_each = containers.value.startup_probe.grpc == null ? [] : [""]
content {
port = containers.value.startup_probe.grpc.port
service = containers.value.startup_probe.grpc.service
}
}
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
}
lifecycle {
ignore_changes = [
client,
client_version,
template[0].annotations["run.googleapis.com/operation-id"],
template[0].template,
template[0].labels
]
}
}
resource "google_cloud_run_v2_job_iam_binding" "binding" {
for_each = var.type == "JOB" ? var.iam : {}
project = local.resource.project
location = local.resource.location
name = local.resource.name
role = each.key
members = each.value
role = lookup(local.ctx.custom_roles, each.key, each.key)
members = [for member in each.value : lookup(local.ctx.iam_principals, member, member)]
}

View File

@@ -15,6 +15,12 @@
*/
locals {
_ctx_p = "$"
ctx = {
for k, v in var.context : k => {
for kk, vv in v : "${local._ctx_p}${k}:${kk}" => vv
} if k != "condition_vars"
}
connector = (
var.vpc_connector_create != null
? google_vpc_access_connector.connector[0].id
@@ -37,14 +43,12 @@ locals {
}
invoke_command = local._invoke_command[var.type]
location = lookup(local.ctx.locations, var.region, var.region)
project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id)
revision_name = (
var.revision.name == null ? null : "${var.name}-${var.revision.name}"
)
service_account_email = (
var.service_account_create
? google_service_account.service_account[0].email
: var.service_account
)
_resource = {
"JOB" : (
var.managed_revision ?
@@ -67,82 +71,3 @@ locals {
uri = var.type == "SERVICE" ? local._resource[var.type].uri : ""
}
}
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 = coalesce(var.service_config.eventarc_triggers.audit_log, tomap({}))
name = "audit-log-${each.key}"
location = google_cloud_run_v2_service.service[0].location
project = google_cloud_run_v2_service.service[0].project
matching_criteria {
attribute = "type"
value = "google.cloud.audit.log.v1.written"
}
matching_criteria {
attribute = "serviceName"
value = each.value.service
}
matching_criteria {
attribute = "methodName"
value = each.value.method
}
destination {
cloud_run_service {
service = google_cloud_run_v2_service.service[0].name
region = google_cloud_run_v2_service.service[0].location
}
}
service_account = var.service_config.eventarc_triggers.service_account_email
}
resource "google_eventarc_trigger" "pubsub_triggers" {
for_each = coalesce(var.service_config.eventarc_triggers.pubsub, tomap({}))
name = "pubsub-${each.key}"
location = google_cloud_run_v2_service.service[0].location
project = google_cloud_run_v2_service.service[0].project
matching_criteria {
attribute = "type"
value = "google.cloud.pubsub.topic.v1.messagePublished"
}
transport {
pubsub {
topic = each.value
}
}
destination {
cloud_run_service {
service = google_cloud_run_v2_service.service[0].name
region = google_cloud_run_v2_service.service[0].location
}
}
service_account = var.service_config.eventarc_triggers.service_account_email
}
resource "google_eventarc_trigger" "storage_triggers" {
for_each = coalesce(var.service_config.eventarc_triggers.storage, tomap({}))
name = "storage-${each.key}"
location = google_cloud_run_v2_service.service[0].location
project = google_cloud_run_v2_service.service[0].project
matching_criteria {
attribute = "type"
value = "google.cloud.storage.object.v1.finalized"
}
matching_criteria {
attribute = "bucket"
value = each.value.bucket
}
destination {
cloud_run_service {
service = google_cloud_run_v2_service.service[0].name
region = google_cloud_run_v2_service.service[0].location
path = try(each.value.path, null)
}
}
service_account = var.service_config.eventarc_triggers.service_account_email
}

View File

@@ -89,13 +89,16 @@ module "database_run" {
}
}
}
service_account = module.run-sa.email
deletion_protection = false
service_account_config = {
create = false
email = module.run-sa.email
}
volumes = {
custom_cloudsql = {
empty_dir_size = "128k"
}
}
deletion_protection = false
}
# tftest inventory=recipe-cloudsql-iam-auth-proxy.yaml e2e
```

View File

@@ -0,0 +1,271 @@
/**
* 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.
*/
resource "google_cloud_run_v2_service" "service" {
count = var.type == "SERVICE" && var.managed_revision ? 1 : 0
provider = google-beta
project = local.project_id
location = local.location
name = var.name
ingress = var.service_config.ingress
invoker_iam_disabled = var.service_config.invoker_iam_disabled
labels = var.labels
launch_stage = var.launch_stage
custom_audiences = var.service_config.custom_audiences
deletion_protection = var.deletion_protection
iap_enabled = var.service_config.iap_config != null
template {
labels = var.revision.labels
encryption_key = var.encryption_key
revision = local.revision_name
execution_environment = (
var.service_config.gen2_execution_environment
? "EXECUTION_ENVIRONMENT_GEN2" : "EXECUTION_ENVIRONMENT_GEN1"
)
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
max_instance_request_concurrency = var.service_config.max_concurrency
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
dynamic "scaling" {
for_each = var.service_config.scaling == null ? [] : [""]
content {
max_instance_count = var.service_config.scaling.max_instance_count
min_instance_count = var.service_config.scaling.min_instance_count
}
}
dynamic "vpc_access" {
for_each = local.connector == null ? [] : [""]
content {
connector = local.connector
egress = try(var.revision.vpc_access.egress, null)
}
}
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet == null ? null : lookup(
local.ctx.subnets, var.revision.vpc_access.subnet,
var.revision.vpc_access.subnet
)
network = var.revision.vpc_access.network == null ? null : lookup(
local.ctx.networks, var.revision.vpc_access.network,
var.revision.vpc_access.network
)
tags = var.revision.vpc_access.tags
}
}
}
timeout = var.service_config.timeout
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
depends_on = containers.value.depends_on
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
cpu_idle = containers.value.resources.cpu_idle
startup_cpu_boost = containers.value.resources.startup_cpu_boost
}
}
dynamic "ports" {
for_each = coalesce(containers.value.ports, tomap({}))
content {
container_port = ports.value.container_port
name = ports.value.name
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
dynamic "liveness_probe" {
for_each = containers.value.liveness_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.liveness_probe.initial_delay_seconds
timeout_seconds = containers.value.liveness_probe.timeout_seconds
period_seconds = containers.value.liveness_probe.period_seconds
failure_threshold = containers.value.liveness_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.liveness_probe.http_get == null ? [] : [""]
content {
path = containers.value.liveness_probe.http_get.path
port = containers.value.liveness_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.liveness_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "grpc" {
for_each = containers.value.liveness_probe.grpc == null ? [] : [""]
content {
port = containers.value.liveness_probe.grpc.port
service = containers.value.liveness_probe.grpc.service
}
}
}
}
dynamic "startup_probe" {
for_each = containers.value.startup_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds
timeout_seconds = containers.value.startup_probe.timeout_seconds
period_seconds = containers.value.startup_probe.period_seconds
failure_threshold = containers.value.startup_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.startup_probe.http_get == null ? [] : [""]
content {
path = containers.value.startup_probe.http_get.path
port = containers.value.startup_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "tcp_socket" {
for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""]
content {
port = containers.value.startup_probe.tcp_socket.port
}
}
dynamic "grpc" {
for_each = containers.value.startup_probe.grpc == null ? [] : [""]
content {
port = containers.value.startup_probe.grpc.port
service = containers.value.startup_probe.grpc.service
}
}
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
lifecycle {
ignore_changes = [
client,
client_version,
template[0].annotations["run.googleapis.com/operation-id"],
]
}
}

View File

@@ -0,0 +1,274 @@
/**
* 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.
*/
resource "google_cloud_run_v2_service" "service_unmanaged" {
count = var.type == "SERVICE" && !var.managed_revision ? 1 : 0
provider = google-beta
project = local.project_id
location = local.location
name = var.name
ingress = var.service_config.ingress
invoker_iam_disabled = var.service_config.invoker_iam_disabled
labels = var.labels
launch_stage = var.launch_stage
custom_audiences = var.service_config.custom_audiences
deletion_protection = var.deletion_protection
iap_enabled = var.service_config.iap_config != null
template {
labels = var.revision.labels
encryption_key = var.encryption_key
revision = local.revision_name
execution_environment = (
var.service_config.gen2_execution_environment
? "EXECUTION_ENVIRONMENT_GEN2" : "EXECUTION_ENVIRONMENT_GEN1"
)
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
max_instance_request_concurrency = var.service_config.max_concurrency
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
dynamic "scaling" {
for_each = var.service_config.scaling == null ? [] : [""]
content {
max_instance_count = var.service_config.scaling.max_instance_count
min_instance_count = var.service_config.scaling.min_instance_count
}
}
dynamic "vpc_access" {
for_each = local.connector == null ? [] : [""]
content {
connector = local.connector
egress = try(var.revision.vpc_access.egress, null)
}
}
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet == null ? null : lookup(
local.ctx.subnets, var.revision.vpc_access.subnet,
var.revision.vpc_access.subnet
)
network = var.revision.vpc_access.network == null ? null : lookup(
local.ctx.networks, var.revision.vpc_access.network,
var.revision.vpc_access.network
)
tags = var.revision.vpc_access.tags
}
}
}
timeout = var.service_config.timeout
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
depends_on = containers.value.depends_on
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
cpu_idle = containers.value.resources.cpu_idle
startup_cpu_boost = containers.value.resources.startup_cpu_boost
}
}
dynamic "ports" {
for_each = coalesce(containers.value.ports, tomap({}))
content {
container_port = ports.value.container_port
name = ports.value.name
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
dynamic "liveness_probe" {
for_each = containers.value.liveness_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.liveness_probe.initial_delay_seconds
timeout_seconds = containers.value.liveness_probe.timeout_seconds
period_seconds = containers.value.liveness_probe.period_seconds
failure_threshold = containers.value.liveness_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.liveness_probe.http_get == null ? [] : [""]
content {
path = containers.value.liveness_probe.http_get.path
port = containers.value.liveness_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.liveness_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "grpc" {
for_each = containers.value.liveness_probe.grpc == null ? [] : [""]
content {
port = containers.value.liveness_probe.grpc.port
service = containers.value.liveness_probe.grpc.service
}
}
}
}
dynamic "startup_probe" {
for_each = containers.value.startup_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds
timeout_seconds = containers.value.startup_probe.timeout_seconds
period_seconds = containers.value.startup_probe.period_seconds
failure_threshold = containers.value.startup_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.startup_probe.http_get == null ? [] : [""]
content {
path = containers.value.startup_probe.http_get.path
port = containers.value.startup_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "tcp_socket" {
for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""]
content {
port = containers.value.startup_probe.tcp_socket.port
}
}
dynamic "grpc" {
for_each = containers.value.startup_probe.grpc == null ? [] : [""]
content {
port = containers.value.startup_probe.grpc.port
service = containers.value.startup_probe.grpc.service
}
}
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
lifecycle {
ignore_changes = [
build_config,
client,
client_version,
template[0].annotations["run.googleapis.com/operation-id"],
template[0].containers,
template[0].labels
]
}
}

View File

@@ -14,516 +14,13 @@
* limitations under the License.
*/
resource "google_cloud_run_v2_service" "service" {
count = var.type == "SERVICE" && var.managed_revision ? 1 : 0
provider = google-beta
project = var.project_id
location = var.region
name = var.name
ingress = var.service_config.ingress
invoker_iam_disabled = var.service_config.invoker_iam_disabled
labels = var.labels
launch_stage = var.launch_stage
custom_audiences = var.service_config.custom_audiences
deletion_protection = var.deletion_protection
iap_enabled = var.service_config.iap_config != null
template {
labels = var.revision.labels
encryption_key = var.encryption_key
revision = local.revision_name
execution_environment = (
var.service_config.gen2_execution_environment
? "EXECUTION_ENVIRONMENT_GEN2" : "EXECUTION_ENVIRONMENT_GEN1"
)
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
max_instance_request_concurrency = var.service_config.max_concurrency
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
dynamic "scaling" {
for_each = var.service_config.scaling == null ? [] : [""]
content {
max_instance_count = var.service_config.scaling.max_instance_count
min_instance_count = var.service_config.scaling.min_instance_count
}
}
dynamic "vpc_access" {
for_each = local.connector == null ? [] : [""]
content {
connector = local.connector
egress = try(var.revision.vpc_access.egress, null)
}
}
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet
network = var.revision.vpc_access.network
tags = var.revision.vpc_access.tags
}
}
}
timeout = var.service_config.timeout
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
depends_on = containers.value.depends_on
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
cpu_idle = containers.value.resources.cpu_idle
startup_cpu_boost = containers.value.resources.startup_cpu_boost
}
}
dynamic "ports" {
for_each = coalesce(containers.value.ports, tomap({}))
content {
container_port = ports.value.container_port
name = ports.value.name
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
dynamic "liveness_probe" {
for_each = containers.value.liveness_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.liveness_probe.initial_delay_seconds
timeout_seconds = containers.value.liveness_probe.timeout_seconds
period_seconds = containers.value.liveness_probe.period_seconds
failure_threshold = containers.value.liveness_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.liveness_probe.http_get == null ? [] : [""]
content {
path = containers.value.liveness_probe.http_get.path
port = containers.value.liveness_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.liveness_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "grpc" {
for_each = containers.value.liveness_probe.grpc == null ? [] : [""]
content {
port = containers.value.liveness_probe.grpc.port
service = containers.value.liveness_probe.grpc.service
}
}
}
}
dynamic "startup_probe" {
for_each = containers.value.startup_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds
timeout_seconds = containers.value.startup_probe.timeout_seconds
period_seconds = containers.value.startup_probe.period_seconds
failure_threshold = containers.value.startup_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.startup_probe.http_get == null ? [] : [""]
content {
path = containers.value.startup_probe.http_get.path
port = containers.value.startup_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "tcp_socket" {
for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""]
content {
port = containers.value.startup_probe.tcp_socket.port
}
}
dynamic "grpc" {
for_each = containers.value.startup_probe.grpc == null ? [] : [""]
content {
port = containers.value.startup_probe.grpc.port
service = containers.value.startup_probe.grpc.service
}
}
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
lifecycle {
ignore_changes = [
client,
client_version,
template[0].annotations["run.googleapis.com/operation-id"],
]
}
}
resource "google_cloud_run_v2_service" "service_unmanaged" {
count = var.type == "SERVICE" && !var.managed_revision ? 1 : 0
provider = google-beta
project = var.project_id
location = var.region
name = var.name
ingress = var.service_config.ingress
invoker_iam_disabled = var.service_config.invoker_iam_disabled
labels = var.labels
launch_stage = var.launch_stage
custom_audiences = var.service_config.custom_audiences
deletion_protection = var.deletion_protection
iap_enabled = var.service_config.iap_config != null
template {
labels = var.revision.labels
encryption_key = var.encryption_key
revision = local.revision_name
execution_environment = (
var.service_config.gen2_execution_environment
? "EXECUTION_ENVIRONMENT_GEN2" : "EXECUTION_ENVIRONMENT_GEN1"
)
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
max_instance_request_concurrency = var.service_config.max_concurrency
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
dynamic "scaling" {
for_each = var.service_config.scaling == null ? [] : [""]
content {
max_instance_count = var.service_config.scaling.max_instance_count
min_instance_count = var.service_config.scaling.min_instance_count
}
}
dynamic "vpc_access" {
for_each = local.connector == null ? [] : [""]
content {
connector = local.connector
egress = try(var.revision.vpc_access.egress, null)
}
}
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet
network = var.revision.vpc_access.network
tags = var.revision.vpc_access.tags
}
}
}
timeout = var.service_config.timeout
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
depends_on = containers.value.depends_on
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
cpu_idle = containers.value.resources.cpu_idle
startup_cpu_boost = containers.value.resources.startup_cpu_boost
}
}
dynamic "ports" {
for_each = coalesce(containers.value.ports, tomap({}))
content {
container_port = ports.value.container_port
name = ports.value.name
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
dynamic "liveness_probe" {
for_each = containers.value.liveness_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.liveness_probe.initial_delay_seconds
timeout_seconds = containers.value.liveness_probe.timeout_seconds
period_seconds = containers.value.liveness_probe.period_seconds
failure_threshold = containers.value.liveness_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.liveness_probe.http_get == null ? [] : [""]
content {
path = containers.value.liveness_probe.http_get.path
port = containers.value.liveness_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.liveness_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "grpc" {
for_each = containers.value.liveness_probe.grpc == null ? [] : [""]
content {
port = containers.value.liveness_probe.grpc.port
service = containers.value.liveness_probe.grpc.service
}
}
}
}
dynamic "startup_probe" {
for_each = containers.value.startup_probe == null ? [] : [""]
content {
initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds
timeout_seconds = containers.value.startup_probe.timeout_seconds
period_seconds = containers.value.startup_probe.period_seconds
failure_threshold = containers.value.startup_probe.failure_threshold
dynamic "http_get" {
for_each = containers.value.startup_probe.http_get == null ? [] : [""]
content {
path = containers.value.startup_probe.http_get.path
port = containers.value.startup_probe.http_get.port
dynamic "http_headers" {
for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({}))
content {
name = http_headers.key
value = http_headers.value
}
}
}
}
dynamic "tcp_socket" {
for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""]
content {
port = containers.value.startup_probe.tcp_socket.port
}
}
dynamic "grpc" {
for_each = containers.value.startup_probe.grpc == null ? [] : [""]
content {
port = containers.value.startup_probe.grpc.port
service = containers.value.startup_probe.grpc.service
}
}
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
lifecycle {
ignore_changes = [
build_config,
client,
client_version,
template[0].annotations["run.googleapis.com/operation-id"],
template[0].containers,
template[0].labels
]
}
}
resource "google_cloud_run_v2_service_iam_binding" "binding" {
for_each = var.type == "SERVICE" ? var.iam : {}
project = local.resource.project
location = local.resource.location
name = local.resource.name
role = each.key
members = each.value
role = lookup(local.ctx.custom_roles, each.key, each.key)
members = [for member in each.value : lookup(local.ctx.iam_principals, member, member)]
}
resource "google_iap_web_cloud_run_service_iam_member" "member" {
@@ -532,7 +29,7 @@ resource "google_iap_web_cloud_run_service_iam_member" "member" {
location = local.resource.location
cloud_run_service_name = local.resource.name
role = "roles/iap.httpsResourceAccessor"
member = each.key
member = lookup(local.ctx.iam_principals, each.key, each.key)
}
resource "google_iap_web_cloud_run_service_iam_binding" "binding" {
@@ -544,5 +41,78 @@ resource "google_iap_web_cloud_run_service_iam_binding" "binding" {
location = local.resource.location
cloud_run_service_name = local.resource.name
role = "roles/iap.httpsResourceAccessor"
members = var.service_config.iap_config.iam
members = [for member in var.service_config.iap_config.iam : lookup(local.ctx.iam_principals, member, member)]
}
# Event ARC for Cloud Run services
resource "google_eventarc_trigger" "audit_log_triggers" {
for_each = coalesce(var.service_config.eventarc_triggers.audit_log, tomap({}))
name = "audit-log-${each.key}"
location = local.resource.location
project = local.resource.project
matching_criteria {
attribute = "type"
value = "google.cloud.audit.log.v1.written"
}
matching_criteria {
attribute = "serviceName"
value = each.value.service
}
matching_criteria {
attribute = "methodName"
value = each.value.method
}
destination {
cloud_run_service {
service = local.resource.name
region = local.resource.location
}
}
service_account = var.service_config.eventarc_triggers.service_account_email
}
resource "google_eventarc_trigger" "pubsub_triggers" {
for_each = coalesce(var.service_config.eventarc_triggers.pubsub, tomap({}))
name = "pubsub-${each.key}"
location = local.resource.location
project = local.resource.project
matching_criteria {
attribute = "type"
value = "google.cloud.pubsub.topic.v1.messagePublished"
}
transport {
pubsub {
topic = each.value
}
}
destination {
cloud_run_service {
service = local.resource.name
region = local.resource.location
}
}
service_account = var.service_config.eventarc_triggers.service_account_email
}
resource "google_eventarc_trigger" "storage_triggers" {
for_each = coalesce(var.service_config.eventarc_triggers.storage, tomap({}))
name = "storage-${each.key}"
location = local.resource.location
project = local.resource.project
matching_criteria {
attribute = "type"
value = "google.cloud.storage.object.v1.finalized"
}
matching_criteria {
attribute = "bucket"
value = each.value.bucket
}
destination {
cloud_run_service {
service = local.resource.name
region = local.resource.location
path = try(each.value.path, null)
}
}
service_account = var.service_config.eventarc_triggers.service_account_email
}

View File

@@ -0,0 +1,53 @@
/**
* 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.
*/
locals {
service_account_email = (
var.service_account_config.create
? google_service_account.service_account[0].email
: lookup(
local.ctx.iam_principals,
var.service_account_config.email,
var.service_account_config.email
)
)
service_account_roles = [
for role in var.service_account_config.roles
: lookup(local.ctx.custom_roles, role, role)
]
}
resource "google_service_account" "service_account" {
count = var.service_account_config.create ? 1 : 0
project = local.project_id
account_id = coalesce(var.service_account_config.name, var.name)
display_name = coalesce(
var.service_account_config.display_name,
var.service_account_config.name,
var.name
)
}
resource "google_project_iam_member" "default" {
for_each = (
var.service_account_config.create
? toset(local.service_account_roles)
: toset([])
)
role = each.key
project = local.project_id
member = google_service_account.service_account[0].member
}

View File

@@ -25,8 +25,8 @@ locals {
resource "google_tags_location_tag_binding" "binding" {
for_each = var.tag_bindings
parent = (
"//run.googleapis.com/projects/${var.project_id}/locations/${var.region}/${local.resource_types[var.type]}/${local.resource.name}"
"//run.googleapis.com/projects/${local.project_id}/locations/${local.location}/${local.resource_types[var.type]}/${local.resource.name}"
)
tag_value = each.value
location = var.region
tag_value = lookup(local.ctx.tag_values, each.value, each.value)
location = local.location
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright 2024 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 "service_account_config" {
description = "Service account configurations."
type = object({
create = optional(bool, true)
display_name = optional(string)
email = optional(string)
name = optional(string)
roles = optional(list(string), [
"roles/logging.logWriter",
"roles/monitoring.metricWriter"
])
})
nullable = false
default = {}
}

View File

@@ -15,7 +15,7 @@
*/
variable "vpc_connector_create" {
description = "Populate this to create a Serverless VPC Access connector."
description = "VPC connector network configuration. Must be provided if new VPC connector is being created."
type = object({
ip_cidr_range = optional(string)
machine_type = optional(string)
@@ -37,4 +37,12 @@ variable "vpc_connector_create" {
}), {})
})
default = null
validation {
condition = (
var.vpc_connector_create == null ||
try(var.vpc_connector_create.instances, null) != null ||
try(var.vpc_connector_create.throughput, null) != null
)
error_message = "VPC connector must specify either instances or throughput."
}
}

View File

@@ -94,6 +94,24 @@ variable "containers" {
}
}
variable "context" {
description = "Context-specific interpolations."
type = object({
condition_vars = optional(map(map(string)), {}) # not needed here?
cidr_ranges = optional(map(string), {})
custom_roles = optional(map(string), {})
iam_principals = optional(map(string), {})
kms_keys = optional(map(string), {})
locations = optional(map(string), {})
networks = optional(map(string), {})
project_ids = optional(map(string), {})
subnets = optional(map(string), {})
tag_values = optional(map(string), {})
})
nullable = false
default = {}
}
variable "deletion_protection" {
description = "Deletion protection setting for this Cloud Run service."
type = string
@@ -231,18 +249,12 @@ variable "revision" {
)
error_message = "When providing vpc_access.network provide also vpc_access.subnet."
}
}
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
validation {
condition = try(var.revision.vpc_access.connector, null) == null || (
try(var.revision.vpc_access.connector, null) != null && var.vpc_connector_create == null
)
error_message = "Either provide connector to create in var.vpc_connector_create or provide externally managed connector in var.revision.vpc_access.connector"
}
}
variable "service_config" {

View File

@@ -14,17 +14,51 @@
* limitations under the License.
*/
locals {
_connector_subnet_name_ctx = (
try(var.vpc_connector_create.subnet.name, null) == null ? false :
contains(keys(local.ctx.subnets), var.vpc_connector_create.subnet.name)
)
# if you pass the subnet, you must pass only the name, not the whole id
_connector_subnet_name = (
local._connector_subnet_name_ctx
? reverse(split("/", local.ctx.subnets[var.vpc_connector_create.subnet.name]))[0]
: try(var.vpc_connector_create.subnet.name, null)
)
# if project is not provided, but subnet is coming from context, use project from subnet id in context
# and avoid lookups using null project
_connector_subnet_project_input = try(var.vpc_connector_create.subnet.project_id, null)
_connector_subnet_project = (
local._connector_subnet_project_input == null
? (
local._connector_subnet_name_ctx
? split("/", local.ctx.subnets[var.vpc_connector_create.subnet.name])[1]
: null
)
: lookup(
local.ctx.project_ids, local._connector_subnet_project_input,
local._connector_subnet_project_input
)
)
}
resource "google_vpc_access_connector" "connector" {
count = var.vpc_connector_create != null ? 1 : 0
project = var.project_id
project = local.project_id
name = (
var.vpc_connector_create.name != null
? var.vpc_connector_create.name
: var.name
)
region = var.region
ip_cidr_range = var.vpc_connector_create.ip_cidr_range
network = var.vpc_connector_create.network
region = local.location
ip_cidr_range = var.vpc_connector_create.ip_cidr_range == null ? null : lookup(
local.ctx.cidr_ranges, var.vpc_connector_create.ip_cidr_range,
var.vpc_connector_create.ip_cidr_range
)
network = var.vpc_connector_create.network == null ? null : lookup(
local.ctx.networks, var.vpc_connector_create.network,
var.vpc_connector_create.network
)
machine_type = var.vpc_connector_create.machine_type
max_instances = var.vpc_connector_create.instances.max
max_throughput = var.vpc_connector_create.throughput.max
@@ -33,9 +67,8 @@ resource "google_vpc_access_connector" "connector" {
dynamic "subnet" {
for_each = var.vpc_connector_create.subnet.name == null ? [] : [""]
content {
name = var.vpc_connector_create.subnet.name
project_id = var.vpc_connector_create.subnet.project_id
name = local._connector_subnet_name
project_id = local._connector_subnet_project
}
}
}

View File

@@ -0,0 +1,190 @@
/**
* 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.
*/
resource "google_cloud_run_v2_worker_pool" "default_managed" {
count = var.type == "WORKERPOOL" && var.managed_revision ? 1 : 0
provider = google-beta
project = local.project_id
location = local.location
name = var.name
labels = var.labels
launch_stage = var.launch_stage
deletion_protection = var.deletion_protection
dynamic "scaling" {
for_each = var.workerpool_config.scaling == null ? [] : [""]
content {
scaling_mode = var.workerpool_config.scaling.mode
max_instance_count = var.workerpool_config.scaling.max_instance_count
min_instance_count = var.workerpool_config.scaling.min_instance_count
manual_instance_count = var.workerpool_config.scaling.manual_instance_count
}
}
template {
labels = var.revision.labels
encryption_key = var.encryption_key
revision = local.revision_name
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
# Serverless VPC connector is not supported
# dynamic "vpc_access" {
# for_each = local.connector == null ? [] : [""]
# content {
# connector = local.connector
# egress = try(var.revision.vpc_access.egress, null)
# }
# }
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet == null ? null : lookup(
local.ctx.subnets, var.revision.vpc_access.subnet,
var.revision.vpc_access.subnet
)
network = var.revision.vpc_access.network == null ? null : lookup(
local.ctx.networks, var.revision.vpc_access.network,
var.revision.vpc_access.network
)
tags = var.revision.vpc_access.tags
}
}
}
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
lifecycle {
ignore_changes = [
client,
client_version,
template[0].annotations["run.googleapis.com/operation-id"],
]
}
}

View File

@@ -0,0 +1,193 @@
/**
* 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.
*/
resource "google_cloud_run_v2_worker_pool" "default_unmanaged" {
count = var.type == "WORKERPOOL" && !var.managed_revision ? 1 : 0
provider = google-beta
project = local.project_id
location = local.location
name = var.name
labels = var.labels
launch_stage = var.launch_stage
deletion_protection = var.deletion_protection
dynamic "scaling" {
for_each = var.workerpool_config.scaling == null ? [] : [""]
content {
scaling_mode = var.workerpool_config.scaling.mode
max_instance_count = var.workerpool_config.scaling.max_instance_count
min_instance_count = var.workerpool_config.scaling.min_instance_count
manual_instance_count = var.workerpool_config.scaling.manual_instance_count
}
}
template {
labels = var.revision.labels
encryption_key = var.encryption_key
revision = local.revision_name
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
# Serverless VPC connector is not supported
# dynamic "vpc_access" {
# for_each = local.connector == null ? [] : [""]
# content {
# connector = local.connector
# egress = try(var.revision.vpc_access.egress, null)
# }
# }
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet == null ? null : lookup(
local.ctx.subnets, var.revision.vpc_access.subnet,
var.revision.vpc_access.subnet
)
network = var.revision.vpc_access.network == null ? null : lookup(
local.ctx.networks, var.revision.vpc_access.network,
var.revision.vpc_access.network
)
tags = var.revision.vpc_access.tags
}
}
}
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
lifecycle {
ignore_changes = [
client,
client_version,
template[0].annotations["run.googleapis.com/operation-id"],
template[0].containers,
template[0].labels
]
}
}

View File

@@ -14,351 +14,11 @@
* limitations under the License.
*/
resource "google_cloud_run_v2_worker_pool" "default_managed" {
count = var.type == "WORKERPOOL" && var.managed_revision ? 1 : 0
provider = google-beta
project = var.project_id
location = var.region
name = var.name
labels = var.labels
launch_stage = var.launch_stage
deletion_protection = var.deletion_protection
dynamic "scaling" {
for_each = var.workerpool_config.scaling == null ? [] : [""]
content {
scaling_mode = var.workerpool_config.scaling.mode
max_instance_count = var.workerpool_config.scaling.max_instance_count
min_instance_count = var.workerpool_config.scaling.min_instance_count
manual_instance_count = var.workerpool_config.scaling.manual_instance_count
}
}
template {
labels = var.revision.labels
encryption_key = var.encryption_key
revision = local.revision_name
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
# Serverless VPC connector is not supported
# dynamic "vpc_access" {
# for_each = local.connector == null ? [] : [""]
# content {
# connector = local.connector
# egress = try(var.revision.vpc_access.egress, null)
# }
# }
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet
network = var.revision.vpc_access.network
tags = var.revision.vpc_access.tags
}
}
}
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
lifecycle {
ignore_changes = [
client,
client_version,
template[0].annotations["run.googleapis.com/operation-id"],
]
}
}
resource "google_cloud_run_v2_worker_pool" "default_unmanaged" {
count = var.type == "WORKERPOOL" && !var.managed_revision ? 1 : 0
provider = google-beta
project = var.project_id
location = var.region
name = var.name
labels = var.labels
launch_stage = var.launch_stage
deletion_protection = var.deletion_protection
dynamic "scaling" {
for_each = var.workerpool_config.scaling == null ? [] : [""]
content {
scaling_mode = var.workerpool_config.scaling.mode
max_instance_count = var.workerpool_config.scaling.max_instance_count
min_instance_count = var.workerpool_config.scaling.min_instance_count
manual_instance_count = var.workerpool_config.scaling.manual_instance_count
}
}
template {
labels = var.revision.labels
encryption_key = var.encryption_key
revision = local.revision_name
gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled
dynamic "node_selector" {
for_each = var.revision.node_selector == null ? [] : [""]
content {
accelerator = var.revision.node_selector.accelerator
}
}
# Serverless VPC connector is not supported
# dynamic "vpc_access" {
# for_each = local.connector == null ? [] : [""]
# content {
# connector = local.connector
# egress = try(var.revision.vpc_access.egress, null)
# }
# }
dynamic "vpc_access" {
for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""]
content {
egress = var.revision.vpc_access.egress
network_interfaces {
subnetwork = var.revision.vpc_access.subnet
network = var.revision.vpc_access.network
tags = var.revision.vpc_access.tags
}
}
}
service_account = local.service_account_email
dynamic "containers" {
for_each = var.containers
content {
name = containers.key
image = containers.value.image
command = containers.value.command
args = containers.value.args
dynamic "env" {
for_each = coalesce(containers.value.env, tomap({}))
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = coalesce(containers.value.env_from_key, tomap({}))
content {
name = env.key
value_source {
secret_key_ref {
secret = env.value.secret
version = env.value.version
}
}
}
}
dynamic "resources" {
for_each = containers.value.resources == null ? [] : [""]
content {
limits = containers.value.resources.limits
}
}
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
# CloudSQL is the last mount in the list returned by API
dynamic "volume_mounts" {
for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" }
content {
name = volume_mounts.key
mount_path = volume_mounts.value
}
}
}
}
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null }
content {
name = volumes.key
dynamic "secret" {
for_each = volumes.value.secret == null ? [] : [""]
content {
secret = volumes.value.secret.name
default_mode = volumes.value.secret.default_mode
dynamic "items" {
for_each = volumes.value.secret.path == null ? [] : [""]
content {
path = volumes.value.secret.path
version = volumes.value.secret.version
mode = volumes.value.secret.mode
}
}
}
}
dynamic "empty_dir" {
for_each = volumes.value.empty_dir_size == null ? [] : [""]
content {
medium = "MEMORY"
size_limit = volumes.value.empty_dir_size
}
}
dynamic "gcs" {
for_each = volumes.value.gcs == null ? [] : [""]
content {
bucket = volumes.value.gcs.bucket
read_only = volumes.value.gcs.is_read_only
}
}
dynamic "nfs" {
for_each = volumes.value.nfs == null ? [] : [""]
content {
server = volumes.value.nfs.server
path = volumes.value.nfs.path
read_only = volumes.value.nfs.is_read_only
}
}
}
}
# CloudSQL is the last volume in the list returned by API
dynamic "volumes" {
for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null }
content {
name = volumes.key
dynamic "cloud_sql_instance" {
for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""]
content {
instances = volumes.value.cloud_sql_instances
}
}
}
}
}
lifecycle {
ignore_changes = [
client,
client_version,
template[0].annotations["run.googleapis.com/operation-id"],
template[0].containers,
template[0].labels
]
}
}
resource "google_cloud_run_v2_worker_pool_iam_binding" "binding" {
for_each = var.type == "WORKERPOOL" ? var.iam : {}
project = local.resource.project
location = local.resource.location
name = local.resource.name
role = each.key
members = each.value
role = lookup(local.ctx.custom_roles, each.key, each.key)
members = [for member in each.value : lookup(local.ctx.iam_principals, member, member)]
}