# main.tf configures top-level resources # This data source pulls the configuration for the default google provider. data "google_client_config" "current" {} # A service account which the webapp runs in the context of. resource "google_service_account" "webapp" { project = var.project account_id = coalesce(var.service_account_id, "${var.name}-run") display_name = coalesce(var.service_account_display_name, "Web application Cloud Run service account") } # Grant the webapp service account the ability to connect to the SQL instance # via the grant_sql_client_role_to_webapp_sa boolean variable. resource "google_project_iam_member" "webapp_sql_client" { count = var.grant_sql_client_role_to_webapp_sa ? 1 : 0 project = local.sql_instance_project role = "roles/cloudsql.client" member = "serviceAccount:${google_service_account.webapp.email}" } # A Cloud Run service which hosts the webapp resource "google_cloud_run_service" "webapp" { name = var.name location = var.cloud_run_region project = var.project autogenerate_revision_name = true metadata { annotations = merge( { "serving.knative.dev/creator" : "placeholder", "serving.knative.dev/lastModifier" : "placeholder", # As mentioned at https://www.terraform.io/docs/configuration/resources.html#ignore_changes # placeholders need to be created as the adding the key to the map is # considered a change and not ignored by ignore_changes. This needs to # *always* be present in the config in order for it to appear in # ignore_changes. "run.googleapis.com/ingress-status" : "placeholder", }, # Add the beta launch stage if required. var.enable_beta_launch_stage ? { # Required to be able to set ingress type and secrets volume mounts "run.googleapis.com/launch-stage" : "BETA", } : {}, # Specify the allowable ingress types. { "run.googleapis.com/ingress" : local.webapp_allowed_ingress, }, var.service_annotations, ) } template { metadata { annotations = merge( # Annotations which are always set: { # Maximum number of auto-scaled instances. For a container with # N-workers, maxScale should be less than 1/N of the maximum connection # count for the Cloud SQL instance. "autoscaling.knative.dev/maxScale" = var.max_scale # Minim number of instances. "autoscaling.knative.dev/minScale" = var.min_scale # As mentioned at https://www.terraform.io/docs/configuration/resources.html#ignore_changes # placeholders need to be created as the adding the key to the map is # considered a change and not ignored by ignore_changes "client.knative.dev/user-image" = "placeholder" "run.googleapis.com/client-name" = "placeholder" "run.googleapis.com/client-version" = "placeholder" "run.googleapis.com/sandbox" = "gvisor" }, # Annotations which are only set if there is a Cloud SQL instance: (var.sql_instance_connection_name != "") ? { # Cloud SQL instances to auto-magically make appear in the container as # Unix sockets. "run.googleapis.com/cloudsql-instances" = var.sql_instance_connection_name } : {}, # Annocations which are only set if we are allocating a static egress ip: var.enable_static_egress_ip ? { # Assign the vpc connector and indicate that it should be used for all traffic "run.googleapis.com/vpc-access-egress" = "all" "run.googleapis.com/vpc-access-connector" = google_vpc_access_connector.static-ip-connector[0].id } : {}, # Additional template annotations passed as a variable. var.template_annotations, ) } spec { # Maximum number of concurrent requests to an instance before it is # auto-scaled. For webapps which use connection pooling, it should be safe # to set this number without regard to the connection limit of the Cloud # SQL instance. This can be no greater than 80. # # See https://cloud.google.com/run/docs/about-concurrency. container_concurrency = var.container_concurrency service_account_name = google_service_account.webapp.email containers { image = var.image_name resources { limits = { cpu = var.cpu_limit memory = var.memory_limit } } dynamic "env" { for_each = var.environment_variables content { name = env.key value = env.value } } dynamic "env" { for_each = var.secrets_envars content { name = env.value["name"] value_from { secret_key_ref { name = env.value["id"] key = coalesce(env.value["version"], "latest") } } } } dynamic "volume_mounts" { for_each = var.secrets_volume content { name = volume_mounts.value["name"] mount_path = volume_mounts.value["path"] } } } dynamic "volumes" { for_each = var.secrets_volume content { name = volumes.value["name"] secret { secret_name = volumes.value["id"] items { key = coalesce(volumes.value["version"], "latest") path = volumes.value["name"] } } } } timeout_seconds = var.timeout_seconds } } traffic { percent = 100 latest_revision = true } lifecycle { ignore_changes = [ # Some common annotations which we don't care about. template[0].metadata[0].annotations["client.knative.dev/user-image"], template[0].metadata[0].annotations["run.googleapis.com/client-name"], template[0].metadata[0].annotations["run.googleapis.com/client-version"], template[0].metadata[0].annotations["run.googleapis.com/operation-id"], template[0].metadata[0].labels["run.googleapis.com/startupProbeType"], template[0].metadata[0].annotations["run.googleapis.com/sandbox"], metadata[0].annotations["run.googleapis.com/client-name"], metadata[0].annotations["run.googleapis.com/client-version"], metadata[0].annotations["run.googleapis.com/operation-id"], metadata[0].labels["run.googleapis.com/startupProbeType"], # These are only changed when "run.googleapis.com/launch-stage" is "BETA". # It's non-trivial to make ignore_changes dependent on input variables so # we always ignore these annotations even if, strictly speaking, we only # need to do so is var.enable_beta_launch_stage is true. metadata[0].annotations["serving.knative.dev/creator"], metadata[0].annotations["serving.knative.dev/lastModifier"], # If the allowed ingress variable is specified, ignore feedback about # its status. We cannot make the presence of this ignore be dependent on # "allowed_ingress" since ignore_changes needs to be a static list. metadata[0].annotations["run.googleapis.com/ingress-status"], ] } depends_on = [ google_secret_manager_secret_iam_member.secrets_access, null_resource.pre_deploy_job_trigger ] # Google Beta provider is required for mounting secrets AToW provider = google-beta } # Allow unauthenticated invocations for the webapp. resource "google_cloud_run_service_iam_member" "webapp_all_users_invoker" { count = var.allow_unauthenticated_invocations ? 1 : 0 location = google_cloud_run_service.webapp.location project = google_cloud_run_service.webapp.project service = google_cloud_run_service.webapp.name role = "roles/run.invoker" member = "allUsers" } # Domain mapping for default web-application. Only present if the domain is # verified. We use the custom DNS name of the webapp if provided but otherwise # the webapp is hosted at [SERVICE NAME].[PROJECT DNS ZONE]. We can't create # the domain mapping if the domain is *not* verified because Google won't let # us. resource "google_cloud_run_domain_mapping" "webapp" { for_each = toset(var.ingress_style == "domain-mapping" ? local.dns_names : []) location = var.cloud_run_region name = each.key metadata { # For managed Cloud Run, the namespace *must* be the project name. namespace = var.project } spec { route_name = google_cloud_run_service.webapp.name } } module "uptime_monitoring" { for_each = local.monitor_hosts source = "git::https://gitlab.developers.cam.ac.uk/uis/devops/infra/terraform/gcp-site-monitoring.git?ref=v3" host = each.value.host project = var.project alert_email_addresses = var.alerting_email_address != "" ? [var.alerting_email_address] : [] alert_notification_channels = var.alert_notification_channels uptime_check = { # Accept either e.g. "60s" or 60 for timeout and periods for compatibility # with previous releases. timeout = tonumber(trimsuffix(var.alerting_uptime_timeout, "s")) period = tonumber(trimsuffix(var.alerting_uptime_period, "s")) path = var.monitoring_path success_threshold_percent = var.alerting_success_threshold_percent alert_enabled = var.alerting_enabled } tls_check = { alert_enabled = var.alerting_enabled } # If required, configure the monitoring to use an authentication proxy, allowing # the monitoring checks to invoke the cloud run instance. authentication_proxy = { enabled = each.value.enable_auth_proxy cloud_run_project = google_cloud_run_service.webapp.project cloud_run_service_name = google_cloud_run_service.webapp.name cloud_run_region = var.cloud_run_region egress_connector = each.value.enable_egress_connector ? local.auth_proxy_egress_connector : "" egress_connector_settings = each.value.enable_egress_connector && local.auth_proxy_egress_connector != "" ? "ALL_TRAFFIC" : null } providers = { google = google.stackdriver } } # This extracts information about any currently running Cloud Run revision before # starting the plan walk. This is current behaviour, but may change in future see # https://github.com/hashicorp/terraform/issues/17034. data "google_cloud_run_service" "webapp" { name = var.name location = var.cloud_run_region } # Configure a Cloud Run Job which will be executed before the deployment of the google_cloud_run_service.webapp # resource. This is primarily useful to run database migrations, however other use cases may exist. resource "google_cloud_run_v2_job" "pre_deploy" { count = var.enable_pre_deploy_job ? 1 : 0 name = "${var.name}-pre-deploy" location = var.cloud_run_region project = var.project template { template { service_account = google_service_account.webapp.email dynamic "volumes" { for_each = var.sql_instance_connection_name != "" ? [1] : [] content { name = "cloudsql" cloud_sql_instance { instances = [var.sql_instance_connection_name] } } } dynamic "volumes" { for_each = var.secrets_volume content { name = volumes.value["name"] secret { secret = volumes.value["id"] items { version = coalesce(volumes.value["version"], "latest") path = volumes.value["name"] mode = 0 } } } } containers { image = local.pre_deploy_job_image_name command = var.pre_deploy_job_command args = var.pre_deploy_job_args dynamic "env" { for_each = local.pre_deploy_job_environment_variables content { name = env.key value = env.value } } dynamic "env" { for_each = var.secrets_envars content { name = env.value["name"] value_source { secret_key_ref { secret = env.value["id"] version = coalesce(env.value["version"], "latest") } } } } dynamic "volume_mounts" { for_each = var.sql_instance_connection_name != "" ? [1] : [] content { name = "cloudsql" mount_path = "/cloudsql" } } dynamic "volume_mounts" { for_each = var.secrets_volume content { name = volume_mounts.value["name"] mount_path = volume_mounts.value["path"] } } } } } } # Trigger the pre-deploy job using the gcloud CLI whenever the var.image_name value changes. resource "null_resource" "pre_deploy_job_trigger" { count = var.enable_pre_deploy_job && var.trigger_pre_deploy_job ? 1 : 0 triggers = merge({ image_name = var.image_name }, var.force_pre_deploy_job ? { timestamp = timestamp() } : {}) provisioner "local-exec" { command = <<EOI gcloud --project ${var.project} run jobs execute \ --region ${var.cloud_run_region} --wait ${google_cloud_run_v2_job.pre_deploy[0].name} EOI environment = { # This environment variable tells gcloud CLI to authenticate using an access token. We're using the access token # configured in the default google provider via the google_client_config data source. CLOUDSDK_AUTH_ACCESS_TOKEN = data.google_client_config.current.access_token } } depends_on = [ google_cloud_run_v2_job.pre_deploy ] }