Google Cloud RunをIAPで保護する基本構成をterraformで作成する

2024/07/07 17:15

※ 商品のリンクをクリックして何かを購入すると私に少額の報酬が入ることがあります【広告表示】

IAPとは

IAP(Identity-Aware Proxy)は、Google Cloud Platformのサービスに対して、Google Workspaceのユーザー認証を行うプロキシサービスです。

組織の設定がないGoogle CloudプロジェクトではIAPを設定できません。

組織を作るには

組織の設定はGoogle WorkspaceかCloud Identityで行います。

Cloud Identity は無料版とPremium版があり、無料版でもIAPを使えます。

ドメインさえあればCloud Identityを作成して、IAPを使ってWebアプリを保護することができます。

terraformでIAPを設定する

IAPにはinternalとexternalがあります。internalは組織に紐づいたドメインのユーザーのみアクセスできるようになります。

externalはterraformのみでは作成できないのと、今回の目的に合致したinternalを使います。

IAPは一度作成すると簡単には消せませんので、試す際には注意してください。専用のプロジェクトを使って試すのが良いかもしれません。

※ module内のterraform定義はmain.tfとvariables.tf、outputs.tfで定義するのが一般的ですが、全てのモジュールで同じ名前となり、ファイルをショートカットで開く際に不便なのでファイルの接頭にモジュール名をつけています

  # modules/iap/iap_main.tf
  variable "project" {}
  variable "support_email" {}
  output "google_iap_client_client_id" {
    value = google_iap_client.this.client_id
  }
  output "google_iap_client_secret" {
    value = google_iap_client.this.secret
  }

  resource "google_iap_brand" "iap_brand" {
    support_email     = var.support_email
    application_title = "IAP App name"
    project           = var.project
  }

  # ついgoogle_iap_brandをGoogle Consoleから作ってしまった場合には、以下のコマンドでbrandを確認してgoogle_iap_clientを設定する
  # gcloud alpha iap oauth-brands list
  resource "google_iap_client" "this" {
    display_name = "IAP client"
    brand        = google_iap_brand.iap_brand.name
  }

これでresourceは作成されますが、別途gcloudコマンドの実行が必要です。最後に説明します。

SSL証明書の設定

IAPにはSSL証明書が必要です。

ドメインのZoneもterraformで作成すればterraformのみで完結しますが、zoneは何かのおりに消してしまうと危険なのでterraformで管理せずに手動で作成しています。

  # modules/certificate/certificate_main.tf
  variable "project" {}
  variable "domain" {}

  output "ssl_certificate_id" {
    value = google_compute_managed_ssl_certificate.cert.id
  }
  output "ssl_policy_id" {
    value = google_compute_ssl_policy.this.id
  }

  locals {
    managed_domains = tolist([var.domain])
  }

  # https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_managed_ssl_certificate#example-usage---managed-ssl-certificate-recreation
  resource "random_id" "certificate" {
    byte_length = 4
    prefix      = "cert-"

    keepers = {
      domains = join(",", local.managed_domains)
    }
  }

  resource "google_compute_managed_ssl_certificate" "cert" {
    name     = random_id.certificate.hex

    lifecycle {
      create_before_destroy = true
    }

    managed {
      domains = local.managed_domains
    }
  }

  resource "google_compute_ssl_policy" "this" {
    name            = "${var.project}-ssl-policy"
    profile         = "MODERN"
    min_tls_version = "TLS_1_2"
  }

このモジュールでresourceを作成した時点では、SSL証明書はPROVISIONINGから先に進みません。

あとで決まるIP AddressをSSL証明書と同じ名前でAレコードを手動でCloud DNSに登録し、しばらくするとSSL証明書が完成します(従来のSSL証明書)。

Cloud Runの設定

Cloud RunのterraformでArtifact Registryも同時に作ってしまいます。Artifact Registryを同時に作るのでこの時点ではCloud RunにデプロイするDocker Imageはありません。 そのため、Cloud Run用のダミーイメージ us-docker.pkg.dev/cloudrun/container/hello を指定します。

Cloud Runはterraformと別でイメージのデプロイを行います。デプロイを行った後にterraformを実行すると、terraform外の変更が感知されてしまうため、変更を無視するためにlifecycleを設定します。

  # modules/cloudrun/cloudrun_main.tf
  variable "account_no" {}
  variable "project" {}
  variable "location" {}
  variable "region" {}
  variable "suffix" {}
  variable "cloudrun_service_name" {}
  variable "google_artifact_registry_repository_name" {}

  output "service_name" {
    value = google_cloud_run_service.this.name
  }

  resource "google_cloud_run_service" "this" {
    # cloudコマンドでCloud Runへデプロイする際にはこの名前を使うので、適切な名前にしましょう
    name     = var.cloudrun_service_name
    location = var.location
    metadata {
      annotations = {
        # Cloud Runへのアクセスは内部からか、Cloud Load Balancerを経由したもののみに制限する
        "run.googleapis.com/ingress" = "internal-and-cloud-load-balancing",
      }
    }
    template {
      metadata {
        annotations = {
          # 費用的にインスタンス数を制限したい場合にはここで設定する
          "autoscaling.knative.dev/maxScale" = "1",
        }
      }
      spec {
        # Cloud Runのサービスアカウント。Cloud Runに権限をつけたい場合にはこのユーザーに権限を付与すれば良い
        service_account_name = google_service_account.cloudrun_service_account.email
        containers {
          image = "us-docker.pkg.dev/cloudrun/container/hello"
          # artifact registryのイメージを指定する場合はこういうパスになる
          # image = format(
          #   "asia-northeast1-docker.pkg.dev/%s/%s/%s:latest",
          #   var.project_id,
          #   var.google_artifact_registry_repository_name,
          #   var.image_name
          # )
        }
      }
    }
    lifecycle {
      # terraform外からCloud Runに別のイメージをデプロイしても、terraformが変更を検知しないようにする
      ignore_changes = [
        metadata[0].annotations["run.googleapis.com/client-name"],
        metadata[0].annotations["run.googleapis.com/client-version"],
        template[0].spec[0].containers[0].image,
      ]
    }
  }

  # Cloud Runのサービスアカウント
  resource "google_service_account" "cloudrun_service_account" {
    account_id = format(
      "forcloudrun-%s",
      var.suffix,
    )
    display_name = "Cloud Run Service Account"
  }

  # Serverless NEGに対するIAMポリシーの設定
  resource "google_project_iam_member" "neg_iam" {
    project = var.project
    role    = "roles/compute.networkUser"
    member  = "serviceAccount:${google_service_account.cloudrun_service_account.email}"
  }

  # Artifact Registryのリポジトリ。ここにDockerイメージを保存する
  resource "google_artifact_registry_repository" "cloudrun_repository" {
    provider      = google
    location      = var.location
    repository_id = var.google_artifact_registry_repository_name
    format        = "DOCKER"
  }

  # IAPからCloud Runを起動できるようにする。IAPの設定が終わっていることが前提なので、最初にterraform applyした時点では失敗する
  resource "google_cloud_run_service_iam_binding" "iap_invoker" {
    project    = var.project
    location   = google_cloud_run_service.this.location
    service    = google_cloud_run_service.this.name
    role       = "roles/run.invoker"

    members = [
      # account_noを別の何かから取れれば…。ダッシュボードに表示されている
      "serviceAccount:service-${var.account_no}@gcp-sa-iap.iam.gserviceaccount.com"
    ]
  }

Load Balancerの設定

IAPを使うためにはLoad Balancerが必要です。

IAP → Load Balancer → Cloud Runで、IAPを通過したリクエストのみCloud Runにアクセスできるようになります。

80番ポートへのアクセスは443番ポートへリダイレクトするように設定するのが通例ですが、今時はデフォルトSSLなので設定していません。必要に応じて追加しましょう。

resourceとしてgoogle_compute_global_addressを作成しています。Google CloudのIP Addressは(おそらく)IP anycastなので、この1つのIPアドレスを使います。

※ AWSのCloudFrontのように、IPアドレスが変わることを想定して60秒ごとにIPアドレスを確認するようなことはありません

  # modules/loadbalancer/loadbalancer_main.tf
  variable "project" {}
  variable "region" {}
  variable "suffix" {}
  variable "cloudrun_service_name" {}
  variable "ssl_certificate_id" {}
  variable "ssl_policy_id" {}
  variable "iap_member_groups" {}
  variable "iap_client_id" {}
  variable "iap_client_secret" {}
  variable "domain" {}

  # Aレコードに設定するIP Addressです。
  resource "google_compute_global_address" "lb_address" {
    name = format(
      "iap-cloudrun-ip-%s",
      var.suffix,
    )
  }

  # ロードバランサーバックエンド
  resource "google_compute_backend_service" "app" {
    name = format(
      "backend-service-%s",
      var.suffix,
    )
    load_balancing_scheme = "EXTERNAL"
    protocol              = "HTTP"
    backend {
      group = google_compute_region_network_endpoint_group.app_neg.id
    }
    iap {
      oauth2_client_id     = var.iap_client_id
      oauth2_client_secret = var.iap_client_secret
    }
  }

  # Cloud RunのサービスにServerless NEGを追加
  resource "google_compute_region_network_endpoint_group" "app_neg" {
    name                  = "serverless-neg"
    region                = var.region
    network_endpoint_type = "SERVERLESS"
    cloud_run {
      # リクエストの送信先CloudRun
      service = var.cloudrun_service_name
    }
  }

  # ロードバランサ
  resource "google_compute_url_map" "default" {
    name = format(
      "url-map-%s",
      var.suffix,
    )
    # デフォルト
    default_service = google_compute_backend_service.app.id
  }

  resource "google_compute_target_https_proxy" "this" {
    name = format(
      "https-proxy-%s",
      var.suffix
    )
    url_map = google_compute_url_map.default.id
    ssl_certificates = [var.ssl_certificate_id]
    ssl_policy       = var.ssl_policy_id
  }

  # フロントエンドの実設定
  resource "google_compute_global_forwarding_rule" "default" {
    name = format(
      "forwarding-rule-%s",
      var.suffix,
    )
    ip_address  = google_compute_global_address.lb_address.id
    ip_protocol = "TCP"
    port_range  = "443"
    target      = google_compute_target_https_proxy.this.id
  }

  # httpへのアクセスはhttpsへ
  resource "google_compute_url_map" "https_redirect" {
    name = "https-redirect"

    default_url_redirect {
      https_redirect         = true
      redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
      strip_query            = false
    }
  }

  # IAPを経由してアクセスできるユーザーを設定します
  resource "google_iap_web_backend_service_iam_binding" "binding_default" {
    project             = var.project
    web_backend_service = google_compute_backend_service.app.name
    role                = "roles/iap.httpsResourceAccessor"
    # https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iap_web_backend_service_iam#member/members
    # userの指定のほか、Google Groupやdomainなどの指定ができると書かれています
    members             = var.iap_member_groups
  }

モジュールを呼び出すmain.tf

terraformのstateをgcsで管理したい場合には terraform_remote_state を定義してください。gcsバケットは手動であらかじめ作成しておきます。

今回のterraformはgithubに公開しています。変更が必要なのは、このmain.tfのlocalsの値のみです。

  # environments/example/main.tf
  # ここを変更する
  locals {
    # Google Cloud Consoleのダッシュボードに表示されています。おそらく12桁の数字
    account_no = "PROJECT_NO"
    # Google CloudのプロジェクトID(表示名と違ってsaffixの数字がついていることがあるので注意)
    project  = "PROJECT_ID"
    # regionは好きに設定してください
    region   = "asia-northeast1"
    # CloudRunとArtifact Registryで利用するロケーションです
    location = "asia-northeast1"
    # Artifact Registryのリポジトリ名
    google_artifact_registry_repository_name = format(
      "iap-cloudrun-%s",
      random_id.suffix.hex,
    )
    # CloudRunにデプロイする際のサービス名suffix無しのわかりやすい名前でも良いかも
    cloudrun_service_name = format(
      "iap-cloudrun-%s",
      random_id.suffix.hex,
    )
    # アプリケーションへアクセスする際のドメイン名
    domain = "cloudrun-app.example.com"
    # https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iap_web_backend_service_iam#member/members
    # internalなIAPを利用するので、CloudIdentityやGoogle Workspaceの組織ユーザーでなければいけません。Google Groupやドメインなどの指定もできると書かれています
    iap_member_groups = [
      "user:you@example.com",
    ]
    # IAPのサポートメールアドレス
    support_email = "support@example.com"
  }
  provider "google" {
    project = local.project
    region  = local.region
  }

  provider "google-beta" {
    project = local.project
    region  = local.region
  }

  resource "random_id" "suffix" {
    byte_length = 4
  }

  module "ssl_cert" {
    source = "../../modules/certificate"
    project = local.project
    domain  = local.domain
  }

  # terraformを実行した後に以下のコマンドの実行が必要
  # 実施前にWebアクセスすると The IAP service account is not provisioned. というエラーになる
  # gcloud beta services identity create --service=iap.googleapis.com
  module "iap" {
    source = "../../modules/iap"
    project = local.project
    support_email = local.support_email
  }

  module "cloudrun" {
    source = "../../modules/cloudrun"
    account_no = local.account_no
    project = local.project
    location = local.location
    region = local.region
    suffix = random_id.suffix.hex
    cloudrun_service_name = local.cloudrun_service_name
    google_artifact_registry_repository_name = local.google_artifact_registry_repository_name
  }

  module "loadbalancer" {
    source = "../../modules/loadbalancer"
    project = local.project
    region = local.region
    suffix = random_id.suffix.hex
    cloudrun_service_name = module.cloudrun.service_name
    ssl_certificate_id = module.ssl_cert.ssl_certificate_id
    ssl_policy_id = module.ssl_cert.ssl_policy_id
    iap_member_groups = local.iap_member_groups
    iap_client_id = module.iap.google_iap_client_client_id
    iap_client_secret = module.iap.google_iap_client_secret
    domain = local.domain
  }

実行方法

zoneは移譲されていることが多いでしょう。terraform外で作成したとします。

※ Cloud DNSで今回使うFQDNのサブドメイン部分ゾーンを作成し、そのNSレコードの値をドメインのDNSにNSレコードとして設定した前提、ということです。わからない場合はDNSについて学んでください

事前にやること

組織で使えるようにする

Cloud Identity か Google Workspaceで組織を作成する。

ドメインを持っていない場合には、事前に SQUARESPACE でドメインを取得しましょう。

SQUARESPACE はGoogle Domains事業の売却先です

terraform外の操作

gcloudコマンドでGoogle Cloudプロジェクトを操作できるようにします。

gcloudコマンドのインストールに関しては省略します。

認証・認可を行い、プロジェクトを設定します。

  $ gcloud auth login
  $ gcloud config set project PROJECT_ID

サービスを有効にします。terraformでも可能ですが、destroyの際にサービスが無効になり、resource削除のために結局手動で有効にすることになるので、terraformでは管理しません。

  $ gcloud services enable compute.googleapis.com
  $ gcloud services enable artifactregistry.googleapis.com
  $ gcloud services enable certificatemanager.googleapis.com
  $ gcloud services enable run.googleapis.com
  $ gcloud services enable serviceusage.googleapis.com
  $ gcloud services enable iap.googleapis.com

terraformに設定を書き込む

今回記載のファイルは以下のリポジトリにあります。ダウンロードして展開してください。

environments/example/main.tf の locals にある値を変更します。

必ず変更しないといけないのは、 account_no, project, domain, iap_member_groups support_email です。

terraformの実行

terraform/environments/exampleフォルダの直下でterraformを実行します

  $ terraform init
  $ terraform apply

terraformの実行の際、エラーが以下の1つになるまでterraform applyを繰り返します。

service-@gcp-sa-iap.iam.gserviceaccount.com のロール設定に失敗する

IAPのサービスアカウントが作られていないため、ロース設定が失敗します。以下のコマンドを実行します。

  $ gcloud beta services identity create --service=iap.googleapis.com

再び terraform applyします。

エラーなく全てのresourceが作り終わったら後少しです。

applyの途中で oauth2 エラーが発生したら

oauth2: "invalid_grant" "reauth related error (invalid_rapt)" が発生した場合には以下のコマンドを実行して、Application Default Credentials (ADC) に quota project として登録する必要があるかもしれません

  $ gcloud auth application-default login

IPアドレスの設定

Coogle Cloud Console の IPアドレス へアクセスし、ロードバランサーに設定されたIPアドレスを確認します。

次に、Google Cloud Consoleの Cloud DNS へアクセスし、localsのdomainに設定したドメイン名をAレコードとして設定します。

Certificate Managerで従来の証明書 にあるSSL証明書が有効(ACTIVE)になるまでしばらく待ちます。

有効になったら https://ドメイン へアクセスしてみましょう。 Have fun!

※ 試すだけで環境が不要になったら、terraform destroyでリソースを削除しましょう

おまけ1 Cloud Runへのデプロイ

GitHubのリポジトリにMakefileを置いています。

Makefileの変数を、ここまでに作成したresource名などに変更してください。

IMAGE_NAMEは任意の名前で良いです(ただし、(アルファベット)小文字と大文字、数字、アンダースコア、ピリオド、ダッシュで構成してください)。

dockerコマンドが使える状態で(Docker Desktopなどを利用すれば良い)以下のコマンドを実行します。

  $ make build
  $ make push
  $ make deploy

正常にdeployまで終わったら、ブラウザで表示していたページをリロードしてみましょう。

おまけ2 IAPの認証を使ってCloudRunのサービスに利用する

IAPからはJWTの認証情報が送られてきます。

JWTのAudienceは、Google Cloud Consoleの IAP の設定画面にいき、表示されているLoad Balancerの縦3点リーダーから JWTオーディエンスコードの取得 をクリックします。

以下に、GrafanaでIAPの認証を利用する場合のサンプルを示します。username_claimはsubでない方が良いかもしれません。

  [auth.jwt]
  enabled = true
  header_name = x-goog-iap-jwt-assertion
  username_claim = sub
  email_claim = email
  auto_sign_up = true
  jwk_set_url = https://www.gstatic.com/iap/verify/public_key-jwk
  cache_ttl = 60m
  ; audはGoogle Cloud ConsoleのIAPの設定画面にいき、表示されているLoad Balancerの縦3点リーダーからJWTオーディエンスコードの取得をクリックして取得します
  expect_claims = {"iss": "https://cloud.google.com/iap","aud": "/projects/<project number>/global/backendServices/<backend id>"}

Prev Entry

Next Entry