GCP上でセキュアなCloud Runを構築する方法(Terraform事例有)
はじめに
開発の竹内(@kenta_714)です。
弊社では2022/06/12に漢方・サプリのサブスクリプションサービスであるYOJOのシステム基盤をHerokuからGCPへとリプレースしました。
その際にIaC化としてTerraformを導入してみましたので、GCPをTerraformで実装する際のノウハウを複数回に分けて記事にて共有していきます。
今回はCloud RunをセキュアにしていくためのTipsです。
Cloud Runについて
今回はCloud Runそのものの説明は割愛し、セキュリティの観点のみ説明します。
Cloud RunはGoogle Cloudの仕様上、https://<コンテナサービス名>.a.run.app というURLが必ず発行されます。このURLを使うことで簡単にCloud Runをインターネット公開できる手軽さが良いですよね。
しかしながら、Cloud Run同士を接続していい感じにGCPの他のリソースと繋げることやプライベートなDB(Cloud SQLやMemorystoreなど)と接続しようにも、設定なしではできません。Cloud Runは独自のネットワークセグメント配下に各コンテナが作成される都合上GCPのアーキテクチャの構造上内部ネットワーク配下に直接置くことはできず、GCPの様々なサービスがルーティング経路を作ってくれる感じで内部通信を実現するためです。
そのため、Cloud Runをただ一般公開するというソリューション以外を実現するにはひと手間必要となります。
次項からはその方法について5つほど例を上げていきます。
セキュアな対策1 Load Balancerの設置
先述通りCloud Runは個別のURLを取得できます。このURLをそのまま利用してしまうと色々と大変なことがあります。
例えば、DDoS攻撃を受けたときのリクエストはすべてCloud Runに直接流れてくるため、サービス内での流量制御を検討する必要があります。まぁ、なにより独自ドメインではないので印象は良くないですよね。
このような課題を解決してくれるのがCloud Load Balancingです。
Cloud Load Balancingは大きくフロントエンドとバックエンドに設定が別れています。フロントエンドはIPとロードバランサーの関連付けを担当しています。そのためドメインの設定やSSL証明書も全部まるっとこっちで面倒みてくれます。
バックエンドはCloud RunやGCE、Cloud Storageなどの他サービスとロードバランサーの関連付けを担当しています。
さらにCloud Armorを組み合わせることでより一層のセキュリティ強化を図ることも可能です。
TerraformでCloud Load Balancingを実装
以下のような要件を実装してみましょう。
静的IPを発行したい
特定のドメインへのアクセスを特定のCloud Runへ連携したい
SSL証明書を発行して手軽にHTTPS通信させたい
HTTPはHTTPSにリダイレクトさせたい
まず静的IPです。Terraformで実行しても良いのですが、静的IPを確保するのは1回で終わるので今回はGCP上から手作業で terraform-test という名前で作成し、作成された静的IPに対してTerraformを関連付けるようにします。
data "google_compute_global_address" "load-balancer-address" {
name = "terraform-test"
}
手抜きしているので簡単ですね。
次に特定のドメインへのアクセスを特定のCloud Runへ連携する方法です。
ロードバランサー上でapp、Cloud Runでtestという名前をもつサービスをバックエンドに追加する感じでやっていきます。
# -------------------------------------------------------------
# appのロードバランサーバックエンドの設定
# -------------------------------------------------------------
resource "google_compute_backend_service" "app" {
name = "backend-test-app"
protocol = "HTTP"
port_name = "http"
timeout_sec = 30
backend {
group = google_compute_region_network_endpoint_group.app.id
}
}
resource "google_compute_region_network_endpoint_group" "app" {
name = "test-neg-app"
network_endpoint_type = "SERVERLESS"
region = "asia-northeast1"
cloud_run {
# リクエストを送りたいCloud RunのTerraformのnameを設定
service = google_cloud_run_service.test.name
}
}
# -------------------------------------------------------------
# ロードバランサーの設定
# URLマッピングを行い、ロードバランサーバックエンドへ振り分ける
# -------------------------------------------------------------
resource "google_compute_url_map" "this" {
name = "terraform-test-urlmap"
default_service = google_compute_backend_service.default.id
host_rule {
hosts = 'hogehoge.com'
path_matcher = "app"
}
path_matcher {
name = "app"
default_service = google_compute_backend_service.app.id
}
}
バックエンドの設定と実際にそのバックエンドを反映するロードバランサーの設定の2つが記述している内容になります。
`host_rule` に指定されているのがCloud Runへのリクエストになります。 `google_cloud_run_service.test.name`というのは別途Cloud RunのTerraformで「test」という名前がつけられたサービスがあることを前提としています。他の細かい所についてはマニュアル参照してパラメータの意味を理解しておきましょう。
次にフロントエンドの設定です。
# -------------------------------------------------------------
# ロードバランサーのフロントエンドの設定
# -------------------------------------------------------------
# 証明書を変更してterraform applyをすると証明書がリソースに紐づいていて削除できないためにエラーになる
# そのため、先に作成→割り当て変更→古い証明書を削除するようにする。先に作成するため名前が衝突しないようにランダム文字列を設定する
# https://github.com/hashicorp/terraform-provider-google/issues/5356
resource "random_id" "certificate" {
byte_length = 4
prefix = "${var.project}-cert-"
keepers = {
domains = join(",", var.cert_domains)
}
}
# SSL証明書
resource "google_compute_managed_ssl_certificate" "this" {
name = "terraform-${random_id.certificate.hex}"
lifecycle {
create_before_destroy = true
}
managed {
domains = var.cert_domains
}
}
# SSLポリシー
resource "google_compute_ssl_policy" "this" {
name = "${var.project}-ssl-policy"
profile = "MODERN"
min_tls_version = "TLS_1_2"
}
# 証明書とフロントエンドとの関連付け
resource "google_compute_target_https_proxy" "this" {
name = "${var.project}-proxy"
url_map = google_compute_url_map.this.id
ssl_certificates = [google_compute_managed_ssl_certificate.this.id]
ssl_policy = google_compute_ssl_policy.this.id
}
# フロントエンドの実設定
resource "google_compute_global_forwarding_rule" "this" {
name = "test-frontend"
target = google_compute_target_https_proxy.this.id
ip_address = data.google_compute_global_address.load-balancer-address.id
ip_protocol = "TCP"
port_range = "443"
}
まず証明書の管理をLoad BalancerもといGCPに完全お任せにしてしまいます。(EV証明書などのセルフマネージドな証明書はもう少し面倒です)
SSLポリシーはSSLのバージョンや優先して利用する機能などの指定ができます。余程のことがない限り高めの設定(modern/TLS1.2)をおすすめします。
また、証明書を毎回生成すると作成・削除が上手くできなくなってしまうので、証明書の名前には一意になる文字列を生成するようにしています。例えばバックエンドに割り当てるドメイン(xxx.hoge.comなどのサブドメイン)が増える場合に有効です。
次に証明書とフロントエンドの関連付けをします。`google_compute_target_https_proxy`の場所です。IPもフロントエンドに関連付けます。SSL証明書を使うのでTCPは443にします。
個人的なCloud Load Balancing改善点なんですが、Cloud Load Balancingの不一致時のリクエストを簡単にHTTP STATUS 404に流せるような仕組みがあればいいなーとは思っています。(ソリューションの1つとしてはCloud Storageに404.htmlをアップロードしてそこにリクエストを飛ばすというものがありますが、いちいちやりたくない)
最後に、HTTPのリクエストをHTTPSにリダイレクトすることもCloud Load Balancingで可能です。
# ------------------------------------------------------------------------------
# httpはhttpsへリダイレクトさせる
# ------------------------------------------------------------------------------
resource "google_compute_url_map" "https_redirect" {
name = "test-https-redirect"
default_url_redirect {
https_redirect = true
redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
strip_query = false
}
}
resource "google_compute_target_http_proxy" "https_redirect" {
name = "test-http-proxy"
url_map = google_compute_url_map.https_redirect.id
}
resource "google_compute_global_forwarding_rule" "https_redirect" {
name = "test-lb-http"
target = google_compute_target_http_proxy.https_redirect.id
port_range = "80"
ip_address = data.google_compute_global_address.load-balancer-address.id
}
`default_url_redirect`がリダイレクトの設定です。今回は `MOVED_PERMANENTLY_DEFAULT(307)` にしていますが、状況に合わせてここは変更してください。
あとはCloud Run側のTerraformでも `internal-and-cloud-load-balancing` を使うことでCloud Load Balancing、内部疎通以外のアクセス(要は直接Cloud RunのURLに対するアクセス)を設定することでより一層セキュアにできます。
セキュアな対策2 Identity-aware Proxyの設定
Identity-aware Proxy(IAP)はすごくざっくりいうとGCP上で動いているアプリケーションに対するアクセス制御を簡単に実装してくれるものです。弊社では自社の薬剤師さんのみに利用して欲しいアプリケーションについては、自社ドメイン(pharma-x.co.jp)のGoogleアカウント認証のみを許可しています。
TerraformでIdentity-aware Proxyを実装
個人的にはGUIでやったほうが早いと思います。というのもIAPを使うためにはOAuthの同意画面作成が必要で、こちらはAPIがないのでTerraformでIaCすることができないからです。それにIAPの設定を頻繁に変えることもそうそうないと思いますので、IaC化よりもドキュメント化する文化のほうがむしろ大事かもしれません。
そのため、一例にはなりますが上記で作成したテスト用のバックエンドを再利用して、それにIAPを連携させるという流れで作成してみます。
ロールについてはGUI上でいうと最低限「IAP-secured Web App User」を持っていればいいので、それに対応するTerraform上での権限`roles/iap.httpsResourceAccessor`を指定します。(名前一緒にして欲しい)
IAPのロールについて:
https://cloud.google.com/iap/docs/managing-access
Terraform上のmembersに設定できる内容について:
https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iap_tunnel_iam#argument-reference
# -------------------------------------------------------------
# IAP設定
# -------------------------------------------------------------
resource "google_iap_web_backend_service_iam_binding" "this" {
web_backend_service = google_compute_backend_service.app.name
role = "roles/iap.httpsResourceAccessor"
members = [
"domain:test-domain.com",
]
}
# -------------------------------------------------------------
# appのロードバランサーバックエンドの設定 with IAP
# -------------------------------------------------------------
resource "google_compute_backend_service" "app" {
name = "backend-test-app"
protocol = "HTTP"
port_name = "http"
timeout_sec = 30
backend {
group = google_compute_region_network_endpoint_group.app.id
}
iap {
oauth2_client_id = "OAuthのクライアントID"
oauth2_client_secret = "OAuthのクライアントシークレット"
}
}
oauth2_client_xxxについては、GCPよりOAuthのクライアント情報から取得可能です。
色々と書きましたが、下記サイトが非常に参考になりました。
セキュアな対策3 Cloud Armor(WAF)で不要なアクセスをブロックする
WAFを導入するにはCloud Armorを利用する必要があります。
Cloud Load BalancerはIP直接指定することでアクセスすることができますので、無防備な状態だとCloud Loggingに大量にIP直接指定でのアクセスのログが残り、アラート監視しているととても面倒なことになります。何よりシステムを安定稼働させるためにもWAFでなるべく簡単かついい感じに負荷分散、セキュリティ対策ができるようにすることがベターですよね。
TerraformでCloud Armorを実装
Terraformで Cloud Armor を利用する場合 `google_compute_security_policy`を使います。
L7レベルのDDoS保護を有効しつつ、リクエストはすべてのIPから受け付けるというものを作ります。
# ------------------------------------------------------------------------------
# Cloud Armorの設定
# ------------------------------------------------------------------------------
resource "google_compute_security_policy" "this" {
name = "test-app-security-policy"
adaptive_protection_config {
layer_7_ddos_defense_config {
enable = true
rule_visibility = "STANDARD"
}
}
rule {
action = "allow"
priority = "2147483647"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
description = "default rule"
}
}
# -------------------------------------------------------------
# appのロードバランサーバックエンドの設定
# -------------------------------------------------------------
resource "google_compute_backend_service" "app" {
name = "backend-test-app"
protocol = "HTTP"
port_name = "http"
timeout_sec = 30
# 以下の行を追加
security_policy = google_compute_security_policy.this.id
backend {
group = google_compute_region_network_endpoint_group.app.id
}
}
Terraformの項目名とGCPのサービス名が一致していないので直感的に分かりにくいですが、これだけです。
セキュアな対策4 Internal HTTPS Load BalancingやPrivate Service Connect
弊社では利用していないのですが、GCP内に複数の内部ネットワークセグメントがあり、各Cloud Runがその配下に存在している場合、それらを相互に接続するためにInternal HTTPS Load Balancingを利用することができます。オンプレの環境だとnginxやapacheなどのWebプロキシを各アプリケーションサーバーの受け口に配備していたような環境をそのままGCPに再現することができます。内部通信もHTTPSにとなることで、盗聴のリスクを減らすことができますし、各サービスの負荷分散もそれぞれで対応可能です。
Internal HTTPS Load Balancing
https://cloud.google.com/load-balancing/docs/l7-internal
またとオンプレの内部ネットワークをセキュアに接続することができるPrivate Service Connectというものも存在します。通常オンプレの内部ネットワークは外側のネットワーク(=インターネット)から隔離されているため、他サーバーやクラウドサービスとの接続はできません。そこにセキュアなネットワークルーティングを作ることができます。
Private Service Connect
https://cloud.google.com/vpc/docs/private-service-connect
オンプレではなくGCPの内部サービスとCloud Runの疎通を実現するだけでよければ サーバーレスVPCアクセス を利用するほうが楽だと個人的には思います。詳細については過去のnote記事を参照してください。
セキュアな対策5 Cloud RunのIngressとEgressの活用
Cloud Runにはリクエストを受け付けるところと送るところの2箇所にそれぞれどのようなルールで受信送信するかを決めることができます。前者がIngress、後者がEgressと呼ばれます。
Ingressはパブリックインターネットの経由や他のCloud RunやGCE、Cloud Load BalancingなどのGCPサービス経由の場合もあります。
Egressも同じように、パブリックインターネットや他のGCPサービスへとリクエストを流すパターンがあります。
IngressはCloud RunのGUI上で以下のように設定することができます。
Egressも同じようにGUIから操作できますが若干場所が違います。
既存のCloud Runの場合は一度「新しいリビジョンの編集とデプロイ」をクリックし、「接続」タブを選択すると下部に存在します。
Cloud SQLは内部ルーティングで直接接続できます(他のサービスが同じようなUIとなっていないのは謎ですが)
VPC項目ではすでに作成されているサーバーレスVPCを選択でき、これを使うことでCloud RunをVPC配下に配置した感じで他のVPC内サービスと疎通できます。
その下の2つのラジオボタンはリクエストをVPCを通すか否かの設定です。
すべてのトラフィックをVPC経由にすると、VPCから外側のネットワークのゲートウェイを作成していないと外部疎通ができません。例えば他社のAPIを叩くようなバックエンドサーバーの場合、外部疎通ができないと困りますよね。この場合もう一方のラジオボタンを選択する……という感じで使い分けが可能です。
参考サイト:
TerraformでIngress/Egressを実装
Cloud Runの中で設定できます。
以下ではIngressで「すべての接続を許可」し、Egressで「プライベート接続のみ」というパターンの設定です。
resource "google_cloud_run_service" "this" {
name = "cloudrun-test"
location = "asia-northeast1"
autogenerate_revision_name = true
metadata {
# Ingress
"run.googleapis.com/ingress" = "all"
# Egress
annotations = {
"run.googleapis.com/vpc-access-connector" = google_vpc_access_connector.this.name
"run.googleapis.com/vpc-access-egress" = "private-ranges-only"
}
}
template {
spec {
containers {
# 実際はちゃんとしたコンテナイメージを指定する
image = "gcr.io/cloudrun/testcontainer"
}
}
}
}
IngressとEgressの他のパラメータは以下のとおりです。(2022/10/04現在)
まとめ
Cloud Load Balancing + Cloud Armor を利用することで、Cloud Runに対するアクセスの負荷分散やDDoS攻撃からの保護が簡単に実現できました。また、今回は試行していませんがInternal HTTPS Load BalancingやPrivate Service ConnectをVPCと組み合わせることで、内部的なWeb3層構造の再現やオンプレサーバーとの接続を容易に実現できることがわかりました。
これからもシステムのセキュリティ、オブザーバビリティ、保守性などを確保すべく、GCPを利用してさまざまなことに挑戦してつもりです。