infrastructure/terraform/modules/activation/main.tf (796 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 # # 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 { app_prefix = "activation" source_root_dir = "../.." template_dir = "${local.source_root_dir}/templates" pipeline_source_dir = "${local.source_root_dir}/python/activation" trigger_function_dir = "${local.source_root_dir}/python/function" configuration_folder = "configuration" audience_segmentation_query_template_file = "audience_segmentation_query_template.sqlx" auto_audience_segmentation_query_template_file = "auto_audience_segmentation_query_template.sqlx" cltv_query_template_file = "cltv_query_template.sqlx" purchase_propensity_query_template_file = "purchase_propensity_query_template.sqlx" purchase_propensity_vbb_query_template_file = "purchase_propensity_vbb_query_template.sqlx" lead_score_propensity_query_template_file = "lead_score_propensity_query_template.sqlx" lead_score_propensity_vbb_query_template_file = "lead_score_propensity_vbb_query_template.sqlx" churn_propensity_query_template_file = "churn_propensity_query_template.sqlx" activation_container_image_id = "activation-pipeline" docker_repo_prefix = "${var.location}-docker.pkg.dev/${var.project_id}" activation_container_name = "dataflow/${local.activation_container_image_id}" source_archive_file_prefix = "activation_trigger_source" source_archive_file = "${local.source_archive_file_prefix}.zip" pipeline_service_account_name = "dataflow-worker" pipeline_service_account_email = "${local.app_prefix}-${local.pipeline_service_account_name}@${var.project_id}.iam.gserviceaccount.com" trigger_function_account_name = "trigger-function" trigger_function_account_email = "${local.app_prefix}-${local.trigger_function_account_name}@${var.project_id}.iam.gserviceaccount.com" builder_service_account_name = "build-job" builder_service_account_email = "${local.app_prefix}-${local.builder_service_account_name}@${var.project_id}.iam.gserviceaccount.com" activation_type_configuration_file = "${local.source_root_dir}/templates/activation_type_configuration_template.tpl" # This is calculating a hash number on the file content to keep track of changes and trigger redeployment of resources # in case the file content changes. activation_type_configuration_file_content_hash = filesha512(local.activation_type_configuration_file) activation_application_dir = "${local.source_root_dir}/python/activation" activation_application_fileset = [ "${local.activation_application_dir}/main.py", "${local.activation_application_dir}/Dockerfile", "${local.activation_application_dir}/metadata.json", "${local.activation_application_dir}/requirements.txt", "${local.activation_application_dir}/pipeline_test.py", ] # This is calculating a hash number on the files contents to keep track of changes and trigger redeployment of resources # in case any of these files contents changes. activation_application_content_hash = sha512(join("", [for f in local.activation_application_fileset : fileexists(f) ? filesha512(f) : sha512("file-not-found")])) ga4_setup_source_file = "${local.source_root_dir}/python/ga4_setup/setup.py" ga4_setup_source_file_content_hash = filesha512(local.ga4_setup_source_file) # GCP Cloud Build is not available in all regions. cloud_build_available_locations = [ "us-central1", "us-west2", "europe-west1", "asia-east1", "australia-southeast1", "southamerica-east1" ] } data "google_project" "activation_project" { project_id = var.project_id } module "project_services" { source = "terraform-google-modules/project-factory/google//modules/project_services" version = "18.0.0" disable_dependent_services = false disable_services_on_destroy = false project_id = var.project_id activate_apis = [ "artifactregistry.googleapis.com", "secretmanager.googleapis.com", "pubsub.googleapis.com", "cloudfunctions.googleapis.com", "cloudbuild.googleapis.com", "dataflow.googleapis.com", "bigquery.googleapis.com", "logging.googleapis.com", "aiplatform.googleapis.com", "bigquerystorage.googleapis.com", "storage.googleapis.com", "datapipelines.googleapis.com", "analyticsadmin.googleapis.com", "eventarc.googleapis.com", "run.googleapis.com", "cloudkms.googleapis.com" ] } # This resource executes gcloud commands to check whether the BigQuery API is enabled. # Since enabling APIs can take a few seconds, we need to make the deployment wait until the API is enabled before resuming. resource "null_resource" "check_bigquery_api" { provisioner "local-exec" { command = <<-EOT COUNTER=0 MAX_TRIES=100 while ! gcloud services list --project=${module.project_services.project_id} | grep -i "bigquery.googleapis.com" && [ $COUNTER -lt $MAX_TRIES ] do sleep 6 printf "." COUNTER=$((COUNTER + 1)) done if [ $COUNTER -eq $MAX_TRIES ]; then echo "bigquery api is not enabled, terraform can not continue!" exit 1 fi sleep 20 EOT } depends_on = [ module.project_services ] } # This resource executes gcloud commands to check whether the artifact registry API is enabled. # Since enabling APIs can take a few seconds, we need to make the deployment wait until the API is enabled before resuming. resource "null_resource" "check_artifactregistry_api" { provisioner "local-exec" { command = <<-EOT COUNTER=0 MAX_TRIES=100 while ! gcloud services list --project=${module.project_services.project_id} | grep -i "artifactregistry.googleapis.com" && [ $COUNTER -lt $MAX_TRIES ] do sleep 6 printf "." COUNTER=$((COUNTER + 1)) done if [ $COUNTER -eq $MAX_TRIES ]; then echo "pubsub api is not enabled, terraform can not continue!" exit 1 fi sleep 20 EOT } depends_on = [ module.project_services ] } # This resource executes gcloud commands to check whether the PubSub API is enabled. # Since enabling APIs can take a few seconds, we need to make the deployment wait until the API is enabled before resuming. resource "null_resource" "check_pubsub_api" { provisioner "local-exec" { command = <<-EOT COUNTER=0 MAX_TRIES=100 while ! gcloud services list --project=${module.project_services.project_id} | grep -i "pubsub.googleapis.com" && [ $COUNTER -lt $MAX_TRIES ] do sleep 6 printf "." COUNTER=$((COUNTER + 1)) done if [ $COUNTER -eq $MAX_TRIES ]; then echo "artifact registry api is not enabled, terraform can not continue!" exit 1 fi sleep 20 EOT } depends_on = [ module.project_services ] } # This resource executes gcloud commands to check whether the analyticsadmin API is enabled. # Since enabling APIs can take a few seconds, we need to make the deployment wait until the API is enabled before resuming. resource "null_resource" "check_analyticsadmin_api" { provisioner "local-exec" { command = <<-EOT COUNTER=0 MAX_TRIES=100 while ! gcloud services list --project=${module.project_services.project_id} | grep -i "analyticsadmin.googleapis.com" && [ $COUNTER -lt $MAX_TRIES ] do sleep 6 printf "." COUNTER=$((COUNTER + 1)) done if [ $COUNTER -eq $MAX_TRIES ]; then echo "analyticsadmin api is not enabled, terraform can not continue!" exit 1 fi sleep 20 EOT } depends_on = [ module.project_services ] } # This resource executes gcloud commands to check whether the dataflow API is enabled. # Since enabling APIs can take a few seconds, we need to make the deployment wait until the API is enabled before resuming. resource "null_resource" "check_dataflow_api" { provisioner "local-exec" { command = <<-EOT COUNTER=0 MAX_TRIES=100 while ! gcloud services list --project=${module.project_services.project_id} | grep -i "dataflow.googleapis.com" && [ $COUNTER -lt $MAX_TRIES ] do sleep 6 printf "." COUNTER=$((COUNTER + 1)) done if [ $COUNTER -eq $MAX_TRIES ]; then echo "dataflow api is not enabled, terraform can not continue!" exit 1 fi sleep 20 EOT } depends_on = [ module.project_services ] } # This resource executes gcloud commands to check whether the secretmanager API is enabled. # Since enabling APIs can take a few seconds, we need to make the deployment wait until the API is enabled before resuming. resource "null_resource" "check_secretmanager_api" { provisioner "local-exec" { command = <<-EOT COUNTER=0 MAX_TRIES=100 while ! gcloud services list --project=${module.project_services.project_id} | grep -i "secretmanager.googleapis.com" && [ $COUNTER -lt $MAX_TRIES ] do sleep 6 printf "." COUNTER=$((COUNTER + 1)) done if [ $COUNTER -eq $MAX_TRIES ]; then echo "secretmanager api is not enabled, terraform can not continue!" exit 1 fi sleep 20 EOT } depends_on = [ module.project_services ] } # This resource executes gcloud commands to check whether the cloudfunctions API is enabled. # Since enabling APIs can take a few seconds, we need to make the deployment wait until the API is enabled before resuming. resource "null_resource" "check_cloudfunctions_api" { provisioner "local-exec" { command = <<-EOT COUNTER=0 MAX_TRIES=100 while ! gcloud services list --project=${module.project_services.project_id} | grep -i "cloudfunctions.googleapis.com" && [ $COUNTER -lt $MAX_TRIES ] do sleep 6 printf "." COUNTER=$((COUNTER + 1)) done if [ $COUNTER -eq $MAX_TRIES ]; then echo "cloudfunctions api is not enabled, terraform can not continue!" exit 1 fi sleep 20 EOT } depends_on = [ module.project_services ] } # This resource executes gcloud commands to check whether the cloudbuild API is enabled. # Since enabling APIs can take a few seconds, we need to make the deployment wait until the API is enabled before resuming. resource "null_resource" "check_cloudbuild_api" { provisioner "local-exec" { command = <<-EOT COUNTER=0 MAX_TRIES=100 while ! gcloud services list --project=${module.project_services.project_id} | grep -i "cloudbuild.googleapis.com" && [ $COUNTER -lt $MAX_TRIES ] do sleep 6 printf "." COUNTER=$((COUNTER + 1)) done if [ $COUNTER -eq $MAX_TRIES ]; then echo "cloudbuild api is not enabled, terraform can not continue!" exit 1 fi sleep 20 EOT } depends_on = [ module.project_services ] # The lifecycle block of the google_artifact_registry_repository resource defines a precondition that # checks if the specified region is included in the vertex_pipelines_available_locations list. # If the condition is not met, an error message is displayed and the Terraform configuration will fail. lifecycle { precondition { condition = contains(local.cloud_build_available_locations, var.location) error_message = "Cloud Build is not available in your default region: ${var.location}.\nSet 'google_default_region' variable to a valid Cloud Build location, see Restricted Regions in https://cloud.google.com/build/docs/locations." } } } # This resource executes gcloud commands to check whether the IAM API is enabled. # Since enabling APIs can take a few seconds, we need to make the deployment wait until the API is enabled before resuming. resource "null_resource" "check_cloudkms_api" { provisioner "local-exec" { command = <<-EOT COUNTER=0 MAX_TRIES=100 while ! gcloud services list --project=${module.project_services.project_id} | grep -i "cloudkms.googleapis.com" && [ $COUNTER -lt $MAX_TRIES ] do sleep 6 printf "." COUNTER=$((COUNTER + 1)) done if [ $COUNTER -eq $MAX_TRIES ]; then echo "cloud kms api is not enabled, terraform can not continue!" exit 1 fi sleep 20 EOT } depends_on = [ module.project_services ] } module "bigquery" { source = "terraform-google-modules/bigquery/google" version = "9.0.0" dataset_id = local.app_prefix dataset_name = local.app_prefix description = "activation application logs" project_id = null_resource.check_bigquery_api.id != "" ? module.project_services.project_id : var.project_id location = var.data_location delete_contents_on_destroy = false } # This resouce calls a python command defined inside the module ga4_setup that is responsible for creating # all required custom events in the Google Analytics 4 property. # Check the python file ga4-setup/setup.py for more information. resource "null_resource" "create_custom_events" { triggers = { services_enabled_project = null_resource.check_analyticsadmin_api.id != "" ? module.project_services.project_id : var.project_id source_contents_hash = local.activation_type_configuration_file_content_hash source_file_content_hash = local.ga4_setup_source_file_content_hash } provisioner "local-exec" { command = <<-EOT ${var.uv_run_alias} ga4-setup --ga4_resource=custom_events --ga4_property_id=${var.ga4_property_id} --ga4_stream_id=${var.ga4_stream_id} EOT working_dir = local.source_root_dir } } # This resource calls a python command defines inside the module ga4_setup that is responsible for creating # all required custom events in the Google Analytics 4 property. # Check the python file ga4_setup/setup.py for more information. resource "null_resource" "create_custom_dimensions" { triggers = { services_enabled_project = null_resource.check_analyticsadmin_api.id != "" ? module.project_services.project_id : var.project_id source_file_content_hash = local.ga4_setup_source_file_content_hash #source_activation_type_configuration_hash = local.activation_type_configuration_file_content_hash #source_activation_application_python_hash = local.activation_application_content_hash } provisioner "local-exec" { command = <<-EOT ${var.uv_run_alias} ga4-setup --ga4_resource=custom_dimensions --ga4_property_id=${var.ga4_property_id} --ga4_stream_id=${var.ga4_stream_id} EOT working_dir = local.source_root_dir } } # This resource creates an Artifact Registry repository for the docker images used by the Activation Application. resource "google_artifact_registry_repository" "activation_repository" { project = null_resource.check_artifactregistry_api.id != "" ? module.project_services.project_id : var.project_id location = var.location repository_id = var.artifact_repository_id description = "Docker image repository for the activation application dataflow job base image" format = "DOCKER" } module "pipeline_service_account" { source = "terraform-google-modules/service-accounts/google" version = "4.4.3" project_id = null_resource.check_dataflow_api.id != "" ? module.project_services.project_id : var.project_id prefix = local.app_prefix names = [local.pipeline_service_account_name] project_roles = [ "${module.project_services.project_id}=>roles/dataflow.admin", "${module.project_services.project_id}=>roles/dataflow.worker", "${module.project_services.project_id}=>roles/bigquery.dataEditor", "${module.project_services.project_id}=>roles/bigquery.jobUser", "${module.project_services.project_id}=>roles/artifactregistry.writer", ] display_name = "Dataflow worker Service Account" description = "Activation Dataflow worker Service Account" } module "trigger_function_account" { source = "terraform-google-modules/service-accounts/google" version = "4.4.3" project_id = null_resource.check_pubsub_api.id != "" ? module.project_services.project_id : var.project_id prefix = local.app_prefix names = [local.trigger_function_account_name] project_roles = [ "${module.project_services.project_id}=>roles/secretmanager.secretAccessor", "${module.project_services.project_id}=>roles/dataflow.admin", "${module.project_services.project_id}=>roles/dataflow.worker", "${module.project_services.project_id}=>roles/bigquery.dataEditor", "${module.project_services.project_id}=>roles/pubsub.editor", "${module.project_services.project_id}=>roles/storage.admin", "${module.project_services.project_id}=>roles/artifactregistry.reader", "${module.project_services.project_id}=>roles/iam.serviceAccountUser", ] display_name = "Cloud Build Job Service Account" description = "Service Account used to submit job the cloud build job" } # This an external data that retrieves information about the Google Analytics 4 property using # a python command defined in the module ga4_setup. # This informatoin can then be used in other parts of the Terraform configuration to access the retrieved information. data "external" "ga4_measurement_properties" { program = ["bash", "-c", "${var.uv_run_alias} ga4-setup --ga4_resource=measurement_properties --ga4_property_id=${var.ga4_property_id} --ga4_stream_id=${var.ga4_stream_id}"] working_dir = local.source_root_dir # The count attribute specifies how many times the external data source should be executed. # This means that the external data source will be executed only if either the # var.ga4_measurement_id or var.ga4_measurement_secret variable is not set. count = (var.ga4_measurement_id == null || var.ga4_measurement_secret == null || var.ga4_measurement_id == "" || var.ga4_measurement_secret == "") ? 1 : 0 depends_on = [ module.project_services ] } # It's used to create unique names for resources like KMS key rings or crypto keys, # ensuring they don't clash with existing resources. resource "random_id" "random_suffix" { byte_length = 2 } # This ensures that Secret Manager has a service identity within your project. # This identity is crucial for securely managing secrets and allowing Secret Manager # to interact with other Google Cloud services on your behalf. resource "google_project_service_identity" "secretmanager_sa" { provider = google-beta project = null_resource.check_cloudkms_api.id != "" ? module.project_services.project_id : var.project_id service = "secretmanager.googleapis.com" } # This Key Ring can then be used to store and manage encryption keys for various purposes, # such as encrypting data at rest or protecting secrets. resource "google_kms_key_ring" "key_ring_regional" { name = "key_ring_regional-${random_id.random_suffix.hex}" # If you want your replicas in other locations, change the location in the var.location variable passed as a parameter to this submodule. # if you your replicas stored global, set the location = "global". location = var.location project = null_resource.check_cloudkms_api.id != "" ? module.project_services.project_id : var.project_id } # This key can then be used for various encryption operations, # such as encrypting data before storing it in Google Cloud Storage # or protecting secrets within your application. resource "google_kms_crypto_key" "crypto_key_regional" { name = "crypto-key-${random_id.random_suffix.hex}" key_ring = google_kms_key_ring.key_ring_regional.id } # Defines an IAM policy that explicitly grants the Secret Manager service account # the ability to encrypt and decrypt data using a specific CryptoKey. This is a # common pattern for securely managing secrets, allowing Secret Manager to encrypt # or decrypt data without requiring direct access to the underlying encryption key material. data "google_iam_policy" "crypto_key_encrypter_decrypter" { binding { role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" members = [ "serviceAccount:${google_project_service_identity.secretmanager_sa.email}" ] } depends_on = [ google_project_service_identity.secretmanager_sa, google_kms_key_ring.key_ring_regional, google_kms_crypto_key.crypto_key_regional ] } # It sets the IAM policy for a KMS CryptoKey, specifically granting permissions defined # in another data source. resource "google_kms_crypto_key_iam_policy" "crypto_key" { crypto_key_id = google_kms_crypto_key.crypto_key_regional.id policy_data = data.google_iam_policy.crypto_key_encrypter_decrypter.policy_data } # It sets the IAM policy for a KMS Key Ring, granting specific permissions defined # in a data source. resource "google_kms_key_ring_iam_policy" "key_ring" { key_ring_id = google_kms_key_ring.key_ring_regional.id policy_data = data.google_iam_policy.crypto_key_encrypter_decrypter.policy_data } # This module stores the values ga4-measurement-id and ga4-measurement-secret in Google Cloud Secret Manager. module "secret_manager" { source = "GoogleCloudPlatform/secret-manager/google" version = "0.7.0" project_id = google_kms_crypto_key_iam_policy.crypto_key.etag != "" && google_kms_key_ring_iam_policy.key_ring.etag != "" ? module.project_services.project_id : var.project_id secrets = [ { name = "ga4-measurement-id" secret_data = (var.ga4_measurement_id == null || var.ga4_measurement_secret == null) ? data.external.ga4_measurement_properties[0].result["measurement_id"] : var.ga4_measurement_id automatic_replication = false }, { name = "ga4-measurement-secret" secret_data = (var.ga4_measurement_id == null || var.ga4_measurement_secret == null) ? data.external.ga4_measurement_properties[0].result["measurement_secret"] : var.ga4_measurement_secret automatic_replication = false }, ] # By commenting the user_managed_replication block, you will deploy replicas that may store the secret in different locations in the globe. # This is not a desired behaviour, make sure you're aware of it before doing it. # By default, to respect resources location, we prevent resources from being deployed globally by deploying secrets in the same region of the compute resources. user_managed_replication = { ga4-measurement-id = [ # If you want your replicas in other locations, uncomment the following lines and add them here. # Check this example, as reference: https://github.com/GoogleCloudPlatform/terraform-google-secret-manager/blob/main/examples/multiple/main.tf#L91 { location = var.location kms_key_name = google_kms_crypto_key.crypto_key_regional.id } ] ga4-measurement-secret = [ { location = var.location kms_key_name = google_kms_crypto_key.crypto_key_regional.id } ] } depends_on = [ data.external.ga4_measurement_properties, google_kms_crypto_key.crypto_key_regional, google_kms_key_ring.key_ring_regional, google_project_service_identity.secretmanager_sa, google_kms_crypto_key_iam_policy.crypto_key, google_kms_key_ring_iam_policy.key_ring ] } # This module creates a Cloud Storage bucket to be used by the Activation Application module "pipeline_bucket" { source = "terraform-google-modules/cloud-storage/google//modules/simple_bucket" version = "9.0.1" project_id = null_resource.check_dataflow_api.id != "" ? module.project_services.project_id : var.project_id name = "${local.app_prefix}-app-${module.project_services.project_id}" location = var.location # When deleting a bucket, this boolean option will delete all contained objects. # If false, Terraform will fail to delete buckets which contain objects. force_destroy = true lifecycle_rules = [{ action = { type = "Delete" } condition = { age = 365 with_state = "ANY" matches_prefix = var.project_id } }] iam_members = [{ role = "roles/storage.admin" member = "serviceAccount:${local.pipeline_service_account_email}" }] depends_on = [ module.pipeline_service_account.email ] } # This resource binds the service account to the required roles resource "google_project_iam_member" "cloud_build_job_service_account" { depends_on = [ module.project_services, null_resource.check_artifactregistry_api, data.google_project.project, ] project = null_resource.check_artifactregistry_api.id != "" ? module.project_services.project_id : var.project_id member = "serviceAccount:${var.project_number}-compute@developer.gserviceaccount.com" for_each = toset([ "roles/cloudbuild.serviceAgent", "roles/cloudbuild.builds.builder", "roles/cloudbuild.integrations.owner", "roles/logging.logWriter", "roles/logging.admin", "roles/storage.admin", "roles/iam.serviceAccountTokenCreator", "roles/iam.serviceAccountUser", "roles/iam.serviceAccountAdmin", "roles/cloudfunctions.developer", "roles/run.admin", "roles/appengine.appAdmin", "roles/container.developer", "roles/compute.instanceAdmin.v1", "roles/firebase.admin", "roles/cloudkms.cryptoKeyDecrypter", "roles/secretmanager.secretAccessor", "roles/cloudbuild.workerPoolUser", "roles/cloudbuild.serviceAgent", "roles/cloudbuild.builds.editor", "roles/cloudbuild.builds.viewer", "roles/cloudbuild.builds.approver", "roles/cloudbuild.integrations.viewer", "roles/cloudbuild.integrations.editor", "roles/cloudbuild.connectionViewer", "roles/cloudbuild.connectionAdmin", "roles/cloudbuild.readTokenAccessor", "roles/cloudbuild.tokenAccessor", "roles/cloudbuild.workerPoolOwner", "roles/cloudbuild.workerPoolEditor", "roles/cloudbuild.workerPoolViewer", "roles/artifactregistry.admin", "roles/viewer", "roles/owner", ]) role = each.key } data "google_project" "project" { project_id = null_resource.check_cloudbuild_api != "" ? module.project_services.project_id : var.project_id } # This module creates a Cloud Storage bucket to be used by the Cloud Build Log Bucket module "build_logs_bucket" { source = "terraform-google-modules/cloud-storage/google//modules/simple_bucket" version = "9.0.1" project_id = null_resource.check_cloudbuild_api != "" ? module.project_services.project_id : var.project_id name = "${local.app_prefix}-logs-${module.project_services.project_id}" location = var.location # When deleting a bucket, this boolean option will delete all contained objects. # If false, Terraform will fail to delete buckets which contain objects. force_destroy = true lifecycle_rules = [{ action = { type = "Delete" } condition = { age = 365 with_state = "ANY" matches_prefix = var.project_id } }] iam_members = [ { role = "roles/storage.admin" member = "serviceAccount:${var.project_number}-compute@developer.gserviceaccount.com" } ] depends_on = [ data.google_project.project, google_project_iam_member.cloud_build_job_service_account ] } # This resource creates a bucket object using as content the audience_segmentation_query_template_file file. data "template_file" "audience_segmentation_query_template_file" { template = file("${local.template_dir}/activation_query/${local.audience_segmentation_query_template_file}") vars = { mds_project_id = var.mds_project_id mds_dataset_suffix = var.mds_dataset_suffix } } resource "google_storage_bucket_object" "audience_segmentation_query_template_file" { name = "${local.configuration_folder}/${local.audience_segmentation_query_template_file}" content = data.template_file.audience_segmentation_query_template_file.rendered bucket = module.pipeline_bucket.name } data "template_file" "auto_audience_segmentation_query_template_file" { template = file("${local.template_dir}/activation_query/${local.auto_audience_segmentation_query_template_file}") vars = { mds_project_id = var.mds_project_id mds_dataset_suffix = var.mds_dataset_suffix } } # This resource creates a bucket object using as content the auto_audience_segmentation_query_template_file file. resource "google_storage_bucket_object" "auto_audience_segmentation_query_template_file" { name = "${local.configuration_folder}/${local.auto_audience_segmentation_query_template_file}" content = data.template_file.auto_audience_segmentation_query_template_file.rendered bucket = module.pipeline_bucket.name } data "template_file" "cltv_query_template_file" { template = file("${local.template_dir}/activation_query/${local.cltv_query_template_file}") vars = { mds_project_id = var.mds_project_id mds_dataset_suffix = var.mds_dataset_suffix } } # This resource creates a bucket object using as content the cltv_query_template_file file. resource "google_storage_bucket_object" "cltv_query_template_file" { name = "${local.configuration_folder}/${local.cltv_query_template_file}" content = data.template_file.cltv_query_template_file.rendered bucket = module.pipeline_bucket.name } data "template_file" "churn_propensity_query_template_file" { template = file("${local.template_dir}/activation_query/${local.churn_propensity_query_template_file}") vars = { mds_project_id = var.mds_project_id mds_dataset_suffix = var.mds_dataset_suffix } } # This resource creates a bucket object using as content the churn_propensity_query_template_file file. resource "google_storage_bucket_object" "churn_propensity_query_template_file" { name = "${local.configuration_folder}/${local.churn_propensity_query_template_file}" content = data.template_file.churn_propensity_query_template_file.rendered bucket = module.pipeline_bucket.name } data "template_file" "purchase_propensity_query_template_file" { template = file("${local.template_dir}/activation_query/${local.purchase_propensity_query_template_file}") vars = { mds_project_id = var.mds_project_id mds_dataset_suffix = var.mds_dataset_suffix } } # This resource creates a bucket object using as content the purchase_propensity_query_template_file file. resource "google_storage_bucket_object" "purchase_propensity_query_template_file" { name = "${local.configuration_folder}/${local.purchase_propensity_query_template_file}" content = data.template_file.purchase_propensity_query_template_file.rendered bucket = module.pipeline_bucket.name } # This resource creates a bucket object using as content the purchase_propensity_vbb_query_template_file file. data "template_file" "purchase_propensity_vbb_query_template_file" { template = file("${local.template_dir}/activation_query/${local.purchase_propensity_vbb_query_template_file}") vars = { mds_project_id = var.mds_project_id mds_dataset_suffix = var.mds_dataset_suffix activation_project_id = var.project_id dataset = module.bigquery.bigquery_dataset.dataset_id } } resource "google_storage_bucket_object" "purchase_propensity_vbb_query_template_file" { name = "${local.configuration_folder}/${local.purchase_propensity_vbb_query_template_file}" content = data.template_file.purchase_propensity_vbb_query_template_file.rendered bucket = module.pipeline_bucket.name } data "template_file" "lead_score_propensity_query_template_file" { template = file("${local.template_dir}/activation_query/${local.lead_score_propensity_query_template_file}") vars = { mds_project_id = var.mds_project_id mds_dataset_suffix = var.mds_dataset_suffix } } # This resource creates a bucket object using as content the lead_score_propensity_query_template_file file. resource "google_storage_bucket_object" "lead_score_propensity_query_template_file" { name = "${local.configuration_folder}/${local.lead_score_propensity_query_template_file}" content = data.template_file.lead_score_propensity_query_template_file.rendered bucket = module.pipeline_bucket.name } # This resource creates a bucket object using as content the lead_score_propensity_vbb_query_template_file file. data "template_file" "lead_score_propensity_vbb_query_template_file" { template = file("${local.template_dir}/activation_query/${local.lead_score_propensity_vbb_query_template_file}") vars = { mds_project_id = var.mds_project_id mds_dataset_suffix = var.mds_dataset_suffix activation_project_id = var.project_id dataset = module.bigquery.bigquery_dataset.dataset_id } } resource "google_storage_bucket_object" "lead_score_propensity_vbb_query_template_file" { name = "${local.configuration_folder}/${local.lead_score_propensity_vbb_query_template_file}" content = data.template_file.lead_score_propensity_vbb_query_template_file.rendered bucket = module.pipeline_bucket.name } # This data resources creates a data resource that renders a template file and stores the rendered content in a variable. data "template_file" "activation_type_configuration" { template = file("${local.template_dir}/activation_type_configuration_template.tpl") vars = { audience_segmentation_query_template_gcs_path = "gs://${module.pipeline_bucket.name}/${google_storage_bucket_object.audience_segmentation_query_template_file.output_name}" auto_audience_segmentation_query_template_gcs_path = "gs://${module.pipeline_bucket.name}/${google_storage_bucket_object.auto_audience_segmentation_query_template_file.output_name}" cltv_query_template_gcs_path = "gs://${module.pipeline_bucket.name}/${google_storage_bucket_object.cltv_query_template_file.output_name}" purchase_propensity_query_template_gcs_path = "gs://${module.pipeline_bucket.name}/${google_storage_bucket_object.purchase_propensity_query_template_file.output_name}" purchase_propensity_vbb_query_template_gcs_path = "gs://${module.pipeline_bucket.name}/${google_storage_bucket_object.purchase_propensity_vbb_query_template_file.output_name}" churn_propensity_query_template_gcs_path = "gs://${module.pipeline_bucket.name}/${google_storage_bucket_object.churn_propensity_query_template_file.output_name}" lead_score_propensity_query_template_gcs_path = "gs://${module.pipeline_bucket.name}/${google_storage_bucket_object.lead_score_propensity_query_template_file.output_name}" lead_score_propensity_vbb_query_template_gcs_path = "gs://${module.pipeline_bucket.name}/${google_storage_bucket_object.lead_score_propensity_vbb_query_template_file.output_name}" } } # This resource creates a bucket object using as content the activation_type_configuration.json file. resource "google_storage_bucket_object" "activation_type_configuration_file" { name = "${local.configuration_folder}/activation_type_configuration.json" content = data.template_file.activation_type_configuration.rendered bucket = module.pipeline_bucket.name # Detects md5hash changes to redeploy this file to the GCS bucket. detect_md5hash = base64encode("${local.activation_type_configuration_file_content_hash}${local.activation_application_content_hash}") } # This module submits a gcloud build to build a docker container image to be used by the Activation Application module "activation_pipeline_container" { source = "terraform-google-modules/gcloud/google" version = "3.5.0" platform = "linux" create_cmd_body = <<-EOT builds submit \ --project=${module.project_services.project_id} \ --region ${var.location} \ --default-buckets-behavior=regional-user-owned-bucket \ --tag ${local.docker_repo_prefix}/${google_artifact_registry_repository.activation_repository.name}/${local.activation_container_name}:latest \ --gcs-log-dir=gs://${module.build_logs_bucket.name} \ ${local.pipeline_source_dir} EOT destroy_cmd_body = "artifacts docker images delete --project=${module.project_services.project_id} ${local.docker_repo_prefix}/${google_artifact_registry_repository.activation_repository.name}/${local.activation_container_name} --delete-tags" create_cmd_triggers = { source_contents_hash = local.activation_application_content_hash } module_depends_on = [ module.build_logs_bucket ] } # This module executes a gcloud command to build a dataflow flex template and uploads it to Dataflow module "activation_pipeline_template" { source = "terraform-google-modules/gcloud/google" version = "3.5.0" platform = "linux" create_cmd_body = "dataflow flex-template build --project=${module.project_services.project_id} \"gs://${module.pipeline_bucket.name}/dataflow/templates/${local.activation_container_image_id}.json\" --image \"${local.docker_repo_prefix}/${google_artifact_registry_repository.activation_repository.name}/${local.activation_container_name}:latest\" --sdk-language \"PYTHON\" --metadata-file \"${local.pipeline_source_dir}/metadata.json\"" destroy_cmd_body = "storage rm --project=${module.project_services.project_id} \"gs://${module.pipeline_bucket.name}/dataflow/templates/${local.activation_container_image_id}.json\"" create_cmd_triggers = { source_contents_hash = local.activation_application_content_hash } module_depends_on = [ module.activation_pipeline_container.wait ] } # This resource creates a Pub Sub topic to be used by the Activation Application resource "google_pubsub_topic" "activation_trigger" { name = "activation-trigger" project = null_resource.check_pubsub_api.id != "" ? module.project_services.project_id : var.project_id } # This data resource generates a ZIP archive file containing the contents of the specified source_dir directory data "archive_file" "activation_trigger_source" { type = "zip" output_path = "${local.trigger_function_dir}/${local.source_archive_file}" source_dir = "${local.trigger_function_dir}/trigger_activation" } # This module creates a Cloud Sorage bucket and sets the trigger_function_account_email as the admin. module "function_bucket" { source = "terraform-google-modules/cloud-storage/google//modules/simple_bucket" version = "9.0.1" project_id = null_resource.check_cloudfunctions_api.id != "" ? module.project_services.project_id : var.project_id name = "${local.app_prefix}-trigger-${module.project_services.project_id}" location = var.location # When deleting a bucket, this boolean option will delete all contained objects. # If false, Terraform will fail to delete buckets which contain objects. force_destroy = true lifecycle_rules = [{ action = { type = "Delete" } condition = { age = 365 with_state = "ANY" matches_prefix = module.project_services.project_id } }] iam_members = [{ role = "roles/storage.admin" member = "serviceAccount:${local.trigger_function_account_email}" }] depends_on = [ module.trigger_function_account.email ] } # This resource creates a bucket object using as content the activation_trigger_archive zip file. resource "google_storage_bucket_object" "activation_trigger_archive" { name = "${local.source_archive_file_prefix}_${data.archive_file.activation_trigger_source.output_sha256}.zip" source = data.archive_file.activation_trigger_source.output_path bucket = module.function_bucket.name } # This resource creates a Cloud Function version 2, with a python 3.11 runtime using the activation_trigger_archive zip file in the bucket as source code. resource "google_cloudfunctions2_function" "activation_trigger_cf" { name = "activation-trigger" project = null_resource.check_cloudfunctions_api.id != "" ? module.project_services.project_id : var.project_id location = var.trigger_function_location # Build config to prepare the code to run on Cloud Functions 2 build_config { runtime = "python311" source { storage_source { bucket = module.function_bucket.name object = google_storage_bucket_object.activation_trigger_archive.name } } entry_point = "subscribe" docker_repository = "projects/${module.project_services.project_id}/locations/${var.trigger_function_location}/repositories/gcf-artifacts" } # Sets the trigger for the Cloud Function using the Pub Sub topic created above event_trigger { event_type = "google.cloud.pubsub.topic.v1.messagePublished" pubsub_topic = google_pubsub_topic.activation_trigger.id retry_policy = "RETRY_POLICY_DO_NOT_RETRY" trigger_region = var.trigger_function_location } # Service endpoint configuration for the Cloud Function service_config { available_memory = "256M" max_instance_count = 3 timeout_seconds = 60 ingress_settings = "ALLOW_INTERNAL_ONLY" service_account_email = module.trigger_function_account.email environment_variables = { ACTIVATION_PROJECT = module.project_services.project_id ACTIVATION_REGION = var.location ACTIVATION_TYPE_CONFIGURATION = "gs://${module.pipeline_bucket.name}/${google_storage_bucket_object.activation_type_configuration_file.output_name}" TEMPLATE_FILE_GCS_LOCATION = "gs://${module.pipeline_bucket.name}/dataflow/templates/${local.activation_container_image_id}.json" PIPELINE_TEMP_LOCATION = "gs://${module.pipeline_bucket.name}/tmp/" LOG_DATA_SET = module.bigquery.bigquery_dataset.dataset_id PIPELINE_WORKER_EMAIL = module.pipeline_service_account.email } # Sets the environment variables from the secrets stored on Secret Manager secret_environment_variables { project_id = null_resource.check_cloudfunctions_api.id != "" ? module.project_services.project_id : var.project_id key = "GA4_MEASUREMENT_ID" secret = split("/", module.secret_manager.secret_names[0])[3] version = split("/", module.secret_manager.secret_versions[0])[5] } secret_environment_variables { project_id = null_resource.check_cloudfunctions_api.id != "" ? module.project_services.project_id : var.project_id key = "GA4_MEASUREMENT_SECRET" secret = split("/", module.secret_manager.secret_names[1])[3] version = split("/", module.secret_manager.secret_versions[1])[5] } } # lifecycle configuration ignores the changes to the source zip file lifecycle { ignore_changes = [build_config[0].source[0].storage_source[0].generation] } } # This modules runs cloud commands that adds an invoker policy binding to a Cloud Function, allowing a specific service account to invoke the function. module "add_invoker_binding" { source = "terraform-google-modules/gcloud/google" version = "3.5.0" platform = "linux" create_cmd_body = "functions add-invoker-policy-binding ${google_cloudfunctions2_function.activation_trigger_cf.name} --project=${google_cloudfunctions2_function.activation_trigger_cf.project} --region=${google_cloudfunctions2_function.activation_trigger_cf.location} --member=\"serviceAccount:${data.google_project.activation_project.number}-compute@developer.gserviceaccount.com\"" destroy_cmd_body = "functions remove-invoker-policy-binding ${google_cloudfunctions2_function.activation_trigger_cf.name} --project=${google_cloudfunctions2_function.activation_trigger_cf.project} --region=${google_cloudfunctions2_function.activation_trigger_cf.location} --member=\"serviceAccount:${data.google_project.activation_project.number}-compute@developer.gserviceaccount.com\"" }