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について学んでください
事前にやること
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>"}