terraform/modules/bq-remote-function/main.tf (126 lines of code) (raw):

# /* # * Copyright 2023 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 # * # * https://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 "random_id" "run_id" { byte_length = 8 keepers = { # this will ensure the function gets redeployed only when the code changes timestamp = timestamp() } } ##### Enable datastore API because the function is using it as a cache layer resource "google_project_service" "datastore_api" { service = "datastore.googleapis.com" disable_on_destroy = false # Prevent accidental disabling during Terraform destroy } resource "google_firestore_database" "datastore_mode_database" { project = var.project name = var.datastore_database_name location_id = var.compute_region type = "DATASTORE_MODE" concurrency_mode = "OPTIMISTIC" app_engine_integration_mode = "DISABLED" point_in_time_recovery_enablement = "POINT_IN_TIME_RECOVERY_DISABLED" delete_protection_state = "DELETE_PROTECTION_DISABLED" deletion_policy = "DELETE" depends_on = [google_project_service.datastore_api] } ##### BigQuery Connection resource "google_bigquery_connection" "connection" { connection_id = var.function_name project = var.project location = var.data_region # same region as the cloud function ## Note: The cloud resource nested object has only one output only field - serviceAccountId. cloud_resource {} } ##### Cloud Function SA ################################# resource "google_service_account" "sa_function" { project = var.project account_id = var.service_account_name display_name = "Runtime SA for Cloud Function ${var.function_name}" } # Permissions needed for the cloud function SA resource "google_project_iam_member" "sa_function_roles" { project = var.project for_each = toset(concat([ "roles/logging.logWriter", "roles/artifactregistry.reader", "roles/datastore.user" ], var.cloud_functions_sa_extra_roles )) role = each.key member = "serviceAccount:${google_service_account.sa_function.email}" } ##################################################### resource "google_storage_bucket" "resources_bucket" { name = "${var.project}-cf-${var.function_name}-resources" location = var.compute_region # same region as the cloud function force_destroy = true uniform_bucket_level_access = true } # Generates an archive of the source code compressed as a .zip file. data "archive_file" "source" { type = "zip" source_dir = var.cloud_function_src_dir output_path = var.cloud_function_temp_dir } # Add source code zip to the Cloud Function's bucket resource "google_storage_bucket_object" "zip" { source = data.archive_file.source.output_path content_type = "application/zip" # Append to the MD5 checksum of the files' content # to force the zip to be updated as soon as a change occurs name = "src-${data.archive_file.source.output_md5}.zip" bucket = google_storage_bucket.resources_bucket.name } resource "google_cloudfunctions2_function" "function" { name = var.function_name project = var.project location = var.compute_region # same region as the cloud function build_config { runtime = "python310" entry_point = var.function_entry_point # Set the entry point source { storage_source { bucket = google_storage_bucket.resources_bucket.name object = google_storage_bucket_object.zip.name } } } service_config { max_instance_count = var.cf_max_instance_count min_instance_count = var.cf_min_instance_count available_memory = var.cf_available_memory timeout_seconds = var.cf_timeout_seconds max_instance_request_concurrency = var.cf_max_instance_request_concurrency available_cpu = var.cf_available_cpu environment_variables = merge(var.env_variables, {name = "TERRAFORM_RUN_ID", value = random_id.run_id.hex}, {name = "DATASTORE_CACHE_DB_NAME", value = google_firestore_database.datastore_mode_database.name} ) # to force TF to deploy the function on each run ingress_settings = "ALLOW_INTERNAL_ONLY" all_traffic_on_latest_revision = true service_account_email = google_service_account.sa_function.email } } resource "google_cloud_run_service_iam_member" "sa_invoker" { project = var.project location = var.compute_region # same region as the cloud function service = google_cloudfunctions2_function.function.service_config[0].service role = "roles/run.invoker" member = "serviceAccount:${google_bigquery_connection.connection.cloud_resource[0].service_account_id}" depends_on = [google_bigquery_connection.connection, google_cloudfunctions2_function.function] } ######### # create a stored procedure that deploys the function and call it from outside Terraform resource "google_bigquery_routine" "routine_deploy_functions" { dataset_id = var.bigquery_dataset_name routine_id = "deploy_${var.function_name}" routine_type = "PROCEDURE" language = "SQL" definition_body = templatefile(var.deployment_procedure_path, { project = var.project dataset = var.bigquery_dataset_name function_name = "remote_${var.function_name}" connection_region = google_bigquery_connection.connection.location connection_name = google_bigquery_connection.connection.connection_id cloud_function_url = google_cloudfunctions2_function.function.service_config[0].uri } ) } ## Run a BQ job to deploy the remote functions resource "google_bigquery_job" "deploy_remote_functions_job" { job_id = "d_job_${google_bigquery_routine.routine_deploy_functions.routine_id}_${random_id.run_id.hex}" location = var.data_region # same as dataset used by the solution query { priority = "INTERACTIVE" query = "CALL ${var.bigquery_dataset_name}.${google_bigquery_routine.routine_deploy_functions.routine_id}();" create_disposition = "" # must be set to "" for scripts write_disposition = "" # must be set to "" for scripts } }