petitviolet blog

    GCP IAP protected Cloud Run Application by Terraform

    2023-01-18

    GCPCloudRunTerraform

    Google Cloud(GCP) has Identity-Aware Proxy that uses ID and contexts to protect applications and VMs from unexpected access.

    https://cloud.google.com/iap

    Thanks to IAP, it's a way easy to protect an application running on Cloud Run by requiring Google login within the organization, for example.

    Preparation

    Before writing Terraform codes, IAP requires one-shot operation in Cloud console.

    IAP API can be enabled by this terraform.

    resource "google_project_service" "project" {
      project = var.gcp_project
      service = "iamcredentials.googleapis.com"
    }
    

    Then, you can find OAuth consent screen configuration in cloud console under APIs & Services group.

    oauth consent screen

    If you'd like to create an application that requires Google sign-in within your organization, make User Type internal instead of external.

    User Type Internal

    After creating OAuth consent screen, its brand name is needed in after step. It can be fetched by gcloud command.

    $ gcloud iap oauth-brands list
    ---
    applicationTitle: project-xxx
    name: projects/xxx/brands/yyy
    orgInternalOnly: true
    supportEmail: hoge@example.com
    

    Terraform

    In Terraform, variables should be declared in variables.tf but use locals here instead for simplicity.

    locals {
        gcp_project    = "awesome-project"
        region         = "asia-northeast1"
        app_name       = "myapp"
        app_image_name = "us-docker.pkg.dev/cloudrun/container/hello"
        user_emails    = ["hoge@example.com"]
        iap_brand_name = "projects/xxx/brands/yyy"
    }
    

    Next is to create google_iap_client that provision Identity-Aware Proxy.

    # Enable API
    resource "google_project_service" "iap" {
      project = local.gcp_project
      service = "iap.googleapis.com"
    }
    
    resource "google_iap_client" "project_client" {
      display_name = "IAP client"
      brand        = local.iap_brand_name
    }
    

    Then, define a Cloud Run application and related resources in Terraform.

    # service account for the cloud run application
    resource "google_service_account" "app" {
      account_id   = "app"
      display_name = "ServiceAccount for CloudRun App"
    }
    resource "google_service_account_iam_member" "app" {
      service_account_id = google_service_account.app.name
      role               = "roles/run.serviceAgent"
      member             = "serviceAccount:${google_service_account.app.email}"
    }
    resource "google_service_account_iam_member" "app" {
      service_account_id = google_service_account.app.name
      role               = "roles/iam.serviceAccountUser"
      member             = "serviceAccount:${google_service_account.app.email}"
    }
    

    In addition, google_iap_brand resource exists but it seems creating IAP brand has been completed by creating OAuth consent screen.

    The below snippet is to provision a Cloud Run application with related network stuff.

    resource "google_cloud_run_service" "app" {
      provider = google-beta
    
      name                       = local.app_name
      location                   = local.region
      autogenerate_revision_name = true
    
      metadata {
        annotations = {
          "run.googleapis.com/ingress" = "internal-and-cloud-load-balancing"
        }
      }
    
      template {
    
        spec {
          service_account_name  = google_service_account.app.email
    
          containers {
            image = local.app_image_name
          }
        }
      }
    }
    
    # provision related network for Cloud Run
    resource "google_compute_region_network_endpoint_group" "app" {
      name                  = "${local.app_name}-neg"
      network_endpoint_type = "SERVERLESS"
      region                = local.region
      cloud_run {
        service = google_cloud_run.app.name
      }
    }
    
    resource "google_compute_backend_service" "app" {
      depends_on = [
        google_iap_client.project_client
      ]
    
      name        = "${local.app_name}-service"
      protocol    = "HTTP"
      port_name   = "http"
      timeout_sec = 30
    
      backend {
        group = google_compute_region_network_endpoint_group.app.id
      }
    
      enable_cdn = false
    
      # enable IAP protection on Cloud Run
      iap {
        oauth2_client_id     = google_iap_client.project_client.client_id
        oauth2_client_secret = google_iap_client.project_client.secret
      }
    }
    
    # allow access to restricted users
    resource "google_iap_web_backend_service_iam_binding" "cloud_run_iap_binding" {
      web_backend_service = google_compute_backend_service.app.name
      role                = "roles/iap.httpsResourceAccessor"
      members             = [for email in local.user_emails : "user:${email}"]
    }
    
    
    data "google_iam_policy" "limited_access" {
      binding {
        role = "roles/run.invoker"
        # > IAM must be configured to grant allUsers
        # https://cloud.google.com/iap/docs/enabling-cloud-run
        members = [
          "allUsers",
        ]
      }
    }
    
    resource "google_cloud_run_service_iam_policy" "limited_access" {
      location = google_cloud_run_service.app.location
      project  = google_cloud_run_service.app.project
      service  = google_cloud_run_service.app.name
    
      policy_data = data.google_iam_policy.limited_access.policy_data
    }
    

    As a result, these Terraform codes create a Cloud Run application being protected by IAP which requires Google sign-in within the Google organization. In addition, for disclaimer, these Terraform snippets are pulled out from my application codes so that these should be essentially true but not guaranteed in case copy-paste-ed these code.