* Allow creation of dynamic tags * Extend project factory and related modules to support dynamic values * Extend folder and organization modules * project and organization readme * Simplify dynamic tag support and remove unnecessary restrictions • Schemas & Validations: Removed the restriction that forbade combining IAM fields with allowed_values_regex on tags. Updated validations in project and organization modules, and simplified all relevant JSON schemas. • Module Tag Bindings: Simplified the tag_value assignment in folder , project , gcs , bigquery-dataset , and kms modules by removing the defensive can(regex(...)) check and calling templatestring directly. • Outputs: Removed the tags_dynamic output from project and organization modules, as the same information is now available in tag_keys . • Project Factory: Updated tag_vars_projects in projects.tf to use the native namespaced_name attribute and filtered manually for dynamic tags. * fix(organization, project): fix linting and tests for dynamic tag support - Align allowed_values_regex and description extraction in _tags_merged locals to use lookup() for consistency with other fields. - Fix spacing in project context variable (alphabetical ordering). - Update organization tags test to include the new cost_center tag key with allowed_values_regex. - Update project tags test to include the new cost_center tag key and reflect the resolved allowed_values_regex on environment. * refactor(gcs): refine tag bindings and fix context test - Add _tag_bindings local to pre-resolve context references, enabling templatestring to receive a direct map reference (required by Terraform). - Use var.context.tag_vars instead of the non-existent local.ctx.tag_vars. - Fix HCL syntax in context.tfvars (escaped inner quotes). - Update context test inventory to reflect 3 tag bindings including a dynamic value resolved via templatestring. * refactor: align modules with tag binding context pattern - Add _tag_bindings local + templatestring dance to cloud-run-v2, compute-vm, folder, kms modules (bigquery-dataset already had it) - Exclude tag_vars from local.ctx in cloud-run-v2, compute-vm, folder, kms, project modules (bigquery-dataset already had it) - Add tag_vars to context variable in cloud-run-v2, compute-vm modules (others already had it) - Update all context tests with dynamic tag binding values using var.context.tag_vars * docs: add module-level tftest.yaml test instructions to GEMINI.md * docs: regenerate READMEs after tag-regex alignment - Regenerate variable tables in 7 module READMEs to reflect line number shifts from prior tag-regex changes - Add tag_vars exclusion to gcs ctx local - Fix whitespace alignment in iam-service-account and project-factory tag_vars blocks - Update tftest resource counts for organization and project - Remove tags_dynamic from organization/project output tables * fix(project-factory): update test inventory for tag_bindings module split - Move tag binding address from folder-2 to folder-2-iam in test inventory (tag_bindings moved from creation to IAM modules) - Update module instance count from 34 to 35 - Regenerate README tables after terraform fmt line shifts - Apply terraform fmt to variables.tf * refactor(project-factory): remove unnecessary depends_on from folder-iam modules Folder IAM modules depend on their own folder creation modules, not on module.projects. The explicit depends_on was leftover from an earlier design. * FAST stages * Address review comments. - FAST Stages: - Added tag_keys to output-files.tf in 0-org-setup to pass org tags via tfvars. - Sorted tag_keys and tag_values in output-files.tf. - Updated project-factory, networking, and security stages to use tag_keys. - Filtered tag_keys for dynamic tags only. - Modules: - Excluded tag_vars from local.ctx in iam-service-account and organization. - Simplified tag_value in iam-service-account. - Tests: - Updated test inventories for 0-org-setup and project-factory. * Fix tf format * Fix tfdoc * docs: add ADR for templatestring vars convention and update status of base path ADR * More tfdoc * Update schemas * Use endswith in context loop * Address review * Update FAST readmes * Update last modules * Terraform fmt * Revert alloydb * Fix whitespace --------- Co-authored-by: Ludovico Magnocavallo <ludo@qix.it>
Cloud Function Module (V1)
Cloud Function management, with support for IAM roles, optional bucket creation and bundle via GCS URI, local zip, or local source folder.
TODO
- add support for
source_repository
Examples
HTTP trigger
This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bucket for deployment, creating service account dedicated for this function, granting it roles/logging.logWriter and roles/monitoring.metricWriter roles, and delegating access control to the containing project.
module "cf-http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.regions.secondary
name = "test-cf-http"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
depends_on = [
google_project_iam_member.bucket_default_compute_account_grant,
]
}
# tftest fixtures=fixtures/functions-default-sa-iam-grants.tf inventory=http-trigger.yaml e2e
PubSub and non-HTTP triggers
Other trigger types other than HTTP are configured via the trigger_config variable. This example shows a PubSub trigger.
module "cf-http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.regions.secondary
name = "test-cf-http"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
trigger_config = {
event = "google.pubsub.topic.publish"
resource = module.pubsub.topic.name
}
depends_on = [
google_project_iam_member.bucket_default_compute_account_grant,
]
}
# tftest inventory=pubsub-non-http-trigger.yaml fixtures=fixtures/pubsub.tf,fixtures/functions-default-sa-iam-grants.tf e2e
Controlling HTTP access
To allow anonymous access to the function, grant the roles/cloudfunctions.invoker role to the special allUsers identifier. Use specific identities (service accounts, groups, etc.) instead of allUsers to only allow selective access.
module "cf-http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.regions.secondary
name = "test-cf-http"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
iam = {
"roles/cloudfunctions.invoker" = ["allUsers"]
}
depends_on = [
google_project_iam_member.bucket_default_compute_account_grant,
]
}
# tftest fixtures=fixtures/functions-default-sa-iam-grants.tf inventory=iam.yaml e2e
GCS bucket creation
You can have the module auto-create the GCS bucket used for deployment via the bucket_config variable. Setting bucket_config.location to null will also use the function region for GCS.
module "cf-http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.regions.secondary
prefix = var.prefix
name = "test-cf-http"
bucket_name = var.bucket
bucket_config = {
force_destroy = true
lifecycle_delete_age_days = 1
}
bundle_config = {
path = "assets/sample-function/"
}
depends_on = [
google_project_iam_member.bucket_default_compute_account_grant,
]
}
# tftest fixtures=fixtures/functions-default-sa-iam-grants.tf inventory=bucket-creation.yaml e2e
Service account management
To use a custom service account managed by the module, set service_account_config.create to true.
module "cf-http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.regions.secondary
name = "test-cf-http"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
service_account_config = {
create = true
}
depends_on = [
google_project_iam_member.bucket_default_compute_account_grant,
]
}
# tftest inventory=service-account.yaml fixtures=fixtures/functions-default-sa-iam-grants.tf e2e
To use an externally managed service account, pass its email in service_account_config.email and set service_account_config.create to false.
module "cf-http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.regions.secondary
name = "test-cf-http"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
service_account_config = {
create = false
email = var.service_account.email
}
depends_on = [
google_project_iam_member.bucket_default_compute_account_grant,
]
}
# tftest modules=1 resources=5 fixtures=fixtures/functions-default-sa-iam-grants.tf e2e
Custom bundle config
The Cloud Function bundle can be configured via the bundle_config variable. The only mandatory argument is bundle_config.path which can point to:
- a GCS URI of a ZIP archive
- a local path to a ZIP archive
- a local path to a source folder
When a GCS URI or a local zip file are used, a change in their names will trigger redeployment. When a local source folder is used a ZIP archive will be automatically generated and its internally derived checksum will drive redeployment. You can optionally control its name and exclusions via the attributes in bundle_config.folder_options.
module "cf-http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.regions.secondary
name = "test-cf-http"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
folder_options = {
archive_path = "bundle.zip"
excludes = ["__pycache__"]
}
}
depends_on = [
google_project_iam_member.bucket_default_compute_account_grant,
]
}
# tftest inventory=custom-bundle.yaml fixtures=fixtures/functions-default-sa-iam-grants.tf e2e
Private Cloud Build Pool
This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bucket for deployment using a pre existing private Cloud Build worker pool.
module "cf-http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.regions.secondary
name = "test-cf-http"
bucket_name = var.bucket
build_worker_pool = google_cloudbuild_worker_pool.pool.id
bundle_config = {
path = "assets/sample-function/"
}
depends_on = [
google_project_iam_member.bucket_default_compute_account_grant,
]
}
# tftest inventory=private-build-pool.yaml fixtures=fixtures/cloudbuild-custom-pool.tf,fixtures/functions-default-sa-iam-grants.tf e2e
Multiple Cloud Functions within project
When deploying multiple functions do not reuse bundle_config.archive_path between instances as the result is undefined. Default archive_path creates file in /tmp folder using project Id and function name to avoid name conflicts.
module "cf-http-one" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.regions.secondary
name = "test-cf-http-one"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
}
module "cf-http-two" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.regions.secondary
name = "test-cf-http-two"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
depends_on = [
google_project_iam_member.bucket_default_compute_account_grant,
]
}
# tftest fixtures=fixtures/functions-default-sa-iam-grants.tf inventory=multiple_functions.yaml e2e
Mounting secrets from Secret Manager
This provides the latest value of the secret var_secret as VARIABLE_SECRET environment variable and three values of path_secret mounted in filesystem:
/app/secret/ver1contains version referenced bymodule.secret-manager.version_versions["credentials:v1"]
Remember to grant access to secrets to the service account running Cloud Function.
module "cf-http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.regions.secondary
name = "test-cf-http"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
secrets = {
VARIABLE_SECRET = {
is_volume = false
project_id = var.project_number # use project_number to avoid perm-diff
secret = reverse(split("/", module.secret-manager.secrets["credentials"].name))[0]
versions = [
"latest"
]
}
"/app/secret" = {
is_volume = true
project_id = var.project_number # use project_number to avoid perm-diff
secret = reverse(split("/", module.secret-manager.secrets["credentials"].name))[0]
versions = [
"${module.secret-manager.version_versions["credentials/v1"]}:/ver1"
]
}
}
depends_on = [
google_project_iam_member.bucket_default_compute_account_grant,
]
}
module "secret-manager" {
source = "./fabric/modules/secret-manager"
project_id = var.project_id
secrets = {
credentials = {
iam = {
"roles/secretmanager.secretAccessor" = [module.cf-http.service_account_iam_email]
}
versions = {
v1 = { data = "manual foo bar spam" }
}
}
}
}
# tftest fixtures=fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e skip-tofu
Using CMEK to encrypt function resources
This encrypt bucket gcf-sources-* with the provided kms key. The repository has to be encrypted with the same kms key.
module "project" {
source = "./fabric/modules/project"
name = "cf-v1"
billing_account = var.billing_account_id
prefix = var.prefix
parent = var.folder_id
services = [
"artifactregistry.googleapis.com",
"cloudbuild.googleapis.com",
"cloudfunctions.googleapis.com",
"cloudkms.googleapis.com",
"compute.googleapis.com",
"storage.googleapis.com",
]
iam = {
# grant compute default service account that is used by Cloud Founction
# permission to read from the buckets so it can read function sources
"roles/storage.objectViewer" = [
"serviceAccount:${module.project.default_service_accounts.compute}"
]
}
}
module "kms" {
source = "./fabric/modules/kms"
project_id = module.project.project_id
keyring = {
location = var.regions.secondary
name = "${var.prefix}-keyring"
}
keys = {
"key-regional" = {
}
}
iam = {
"roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
module.project.service_agents["artifactregistry"].iam_email,
module.project.service_agents["cloudfunctions"].iam_email,
module.project.service_agents["storage"].iam_email,
]
}
}
module "artifact-registry" {
source = "./fabric/modules/artifact-registry"
project_id = module.project.project_id
location = var.regions.secondary
name = "registry"
format = { docker = { standard = {} } }
encryption_key = module.kms.key_ids["key-regional"]
iam = {
"roles/artifactregistry.createOnPushWriter" = [
# grant compute default service account that is used by Cloud Build
# permission to push compiled container into Artifact Registry
"serviceAccount:${module.project.default_service_accounts.compute}",
]
}
}
module "cf-http" {
source = "./fabric/modules/cloud-function-v1"
project_id = module.project.project_id
region = var.regions.secondary
name = "test-cf-http"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
kms_key = module.kms.key_ids["key-regional"]
repository_settings = {
repository = module.artifact-registry.id
}
}
# tftest inventory=cmek.yaml
VPC Access Connector
You can use an existing VPC Access Connector to connect to a VPC from Cloud Run.
module "cf_http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.region
name = "test-cf-http"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
vpc_connector = {
name = google_vpc_access_connector.connector.id
egress_setting = "ALL_TRAFFIC"
}
}
# tftest fixtures=fixtures/vpc-connector.tf inventory=service-vpc-access-connector.yaml
If creation of the VPC Access Connector is required, use the vpc_connector.create and vpc_connector_create variable which also supports optional attributes like number of instances, machine type, or throughput.
module "cf_http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.region
name = "test-cf-http"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
vpc_connector = {
create = true
}
vpc_connector_create = {
ip_cidr_range = "10.10.10.0/28"
network = var.vpc.self_link
instances = {
max = 10
min = 3
}
}
}
# tftest inventory=service-vpc-access-connector-create.yaml
Note that if you are using a Shared VPC for the connector, you need to specify a subnet and the host project if this is not where the Cloud Run service is deployed.
module "cf_http" {
source = "./fabric/modules/cloud-function-v1"
project_id = var.project_id
region = var.region
name = "test-cf-http"
bucket_name = var.bucket
bundle_config = {
path = "assets/sample-function/"
}
vpc_connector = {
create = true
}
vpc_connector_create = {
machine_type = "e2-standard-4"
subnet = {
name = module.net-vpc-host.subnets["${var.region}/fixture-subnet-28"].name
project_id = module.project-host.project_id
}
throughput = {
max = 300
min = 200
}
}
}
# tftest fixtures=fixtures/shared-vpc.tf inventory=service-vpc-access-connector-create-sharedvpc.yaml
Variables
| name | description | type | required | default |
|---|---|---|---|---|
| bucket_name | Name of the bucket that will be used for the function code. It will be created with prefix prepended if bucket_config is not null. | string |
✓ | |
| bundle_config | Cloud function source. Path can point to a GCS object URI, or a local path. A local path to a zip archive will generate a GCS object using its basename, a folder will be zipped and the GCS object name inferred when not specified. | object({…}) |
✓ | |
| name | Name used for cloud function and associated resources. | string |
✓ | |
| project_id | Project id used for all resources. | string |
✓ | |
| region | Region used for all resources. | string |
✓ | |
| bucket_config | Enable and configure auto-created bucket. Set fields to null to use defaults. | object({…}) |
null |
|
| build_environment_variables | A set of key/value environment variable pairs available during build time. | map(string) |
{} |
|
| build_worker_pool | Build worker pool, in projects//locations//workerPools/<POOL_NAME> format. | string |
null |
|
| context | Context-specific interpolations. | object({…}) |
{} |
|
| description | Optional description. | string |
"Terraform managed." |
|
| environment_variables | Cloud function environment variables. | map(string) |
{} |
|
| function_config | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | object({…}) |
{…} |
|
| https_security_level | The security level for the function: Allowed values are SECURE_ALWAYS, SECURE_OPTIONAL. | string |
null |
|
| iam | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) |
{} |
|
| ingress_settings | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY . | string |
null |
|
| kms_key | Resource name of a KMS crypto key (managed by the user) used to encrypt/decrypt function resources in key id format. If specified, you must also provide an artifact registry repository using the docker_repository field that was created with the same KMS crypto key. | string |
null |
|
| labels | Resource labels. | map(string) |
{} |
|
| prefix | Optional prefix used for resource names. | string |
null |
|
| repository_settings | Docker Registry to use for storing the function's Docker images and specific repository. If kms_key is provided, the repository must have already been encrypted with the key. | object({…}) |
{…} |
|
| secrets | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) |
{} |
|
| service_account_config | Service account configurations. | object({…}) |
{} |
|
| trigger_config | Function trigger configuration. Leave null for HTTP trigger. | object({…}) |
null |
|
| vpc_connector | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) |
{} |
|
| vpc_connector_create | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) |
null |
Outputs
| name | description | sensitive |
|---|---|---|
| bucket | Bucket resource (only if auto-created). | |
| bucket_name | Bucket name. | |
| function | Cloud function resources. | |
| function_name | Cloud function name. | |
| id | Fully qualified function id. | |
| invoke_command | Command to invoke Cloud Function. | |
| service_account | Service account resource. | |
| service_account_email | Service account email. | |
| service_account_iam_email | Service account email. | |
| vpc_connector | VPC connector resource if created. |