FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects
main.tf 13.7 KiB
Newer Older
# 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.
      {
Wajdi Hajji's avatar
Wajdi Hajji committed
        "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
Monty Dawson's avatar
Monty Dawson committed
        # 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" {
Wajdi Hajji's avatar
Wajdi Hajji committed
  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
  }
}
Paul Rudin's avatar
Paul Rudin committed

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
  }
Paul Rudin's avatar
Paul Rudin committed
}

# 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" {
  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
  ]
}