こんにちは、SaaS SREチームの八代です。
私たちのチームで開発しているSPEEDAというSaaSプロダクトは、オンプレミスとGCPで構成されたハイブリッドクラウド環境上に構築されており、私たちはGoogle Anthosや、Direct Connectなどのサービスを利用し、ハイブリッドクラウド環境での運用を改善する取り組みを続けてきています。
そんな中で、2021年末ごろ、 Hybrid NEGという新しい技術が発表され、GCLBがオンプレミス環境あるいは他のパブリッククラウドに拡張することができるようになりました。
今回は、このHybrid NEGという技術を取り入れ、オンプレミス上に構築したアプリケーションをGCLBで公開できるようにしたので、それについて記事を書こうと思います。
はじめに
この記事は、以下のような人に向けて書きます。
- GCLBを使って、いい感じにオンプレのアプリケーションを公開したい
- 冗長化のため、マルチクラウドでアプリケーションを稼働させたい
- Hybrid NEGリソースをTerraformで管理したい
GCLBを利用してアプリケーションを公開することで、以下のような機能が利用できるようになります。
- Cloud Armorを利用してアクセス制限 / 攻撃からの保護を実施する
- Identity Aware Proxy (IAP)を利用して、OAuthで認証する
- CDNを利用して、コンテンツ配信を高速化する
- etc...
私たちのチームでは、Cloud Armorを利用したIPベースのアクセス制限やWAFの適用、IAPでの社内システムの公開などをよく行なっているのですが、非常に便利に使わせてもらっています。
今回、オンプレのアプリケーションでも同様の仕組みが利用できるようになったことで、運用コストの削減が実現できると感じているので、興味がある方はぜひ読んでみてください。
GCLBのアーキテクチャ
GCPの公式ドキュメントに記載されている、GCLBのアーキテクチャは以下のようになっています。

図を見ると、今回Backend service
に紐づけるbackendにNON_GCP_PRIVATE_IP_PORT
のエンドポイントを指定できるようになっています。
詳しいアーキテクチャや、gcloudを利用したGCLBの構築手順などは、以下のブログ記事で詳しく記載されているので、参考にしてみてください。
次の項からは、Terraformを利用してHybrid NEGおよびGCLBを構築するための内容を記載していきます。
Terraformを使ってGCLBを構築する
前提
今回は、以下の要件を満たすGCLBを構築しました。
- Cloud Armorを利用して、サービスの保護ができるようにする
- コスト最適化のため、単一のGCLBで複数種類のアプリケーションを公開できるようにする
- Hybrid NEGは3ゾーンで冗長化する
- マネージドSSL証明書を利用する
GCLBのアーキテクチャ図からも読み取れますが、GCLBを構成するために必要なGCPリソースの数が多く、ベタ書きだとかなり冗長になるため、moduleに切り出して作成していくこととします。
また、GCLBの作成にあたり必要な以下のリソースは本筋と離れるため、作成済みであるものとします。
- パブリックIP
- Cloud Armorによるセキュリティポリシー
moduleの呼び出し
まず、全体像のイメージを持っていただくため、moduleの呼び出し側を記載します。
module "test_hybrid_gclb" { name = "test-hybrid-gclb" source = "<module path>" certificates = ["hoge-service-ub-speeda-com", "fuga-service-ub-speeda-com"] global_address = google_compute_global_address.test_hybrid_gclb.address default_backend_service = "hoge_service" service = { hoge_service = { name = "hoge-service" host = "hoge-service.ub-speeda.com" backend_endpoints = ["xxx.xxx.xxx.xxx"] port = "80" security_policy = "" health_check_port = "80" health_check_path = "/" }, fuga_service = { name = "fuga-service" host = "fuga-service.ub-speeda.com" backend_endpoints = ["yyy.yyy.yyy.yyy"] port = "80" security_policy = "" health_check_port = "80" health_check_path = "/" } } }
service
の中がMapになっており、各バックエンドサービスへの接続情報を持った構造体が並んでいるところがポイントです。
この中で、ループを回すような処理を行い、実際のリソースを作成していきます。
moduleの実装
ループを回すためのlocal変数定義
今回は、GCPのリソース種によっては、最大3つの要素をループで回す必要が出てきます。
例えば、NEGに実際のエンドポイントを追加するためのリソース(google_compute_network_endpoint
)を追加するには、以下の3要素でループを回す必要があります。
- 公開したいバックエンドサービス(
hoge-service
,fuga-service
に相当) - ゾーン3つ
- バックエンドのIP / ポート
terraformでは、for_eachを利用した多重ループは単純にはできないので、今回はflatten関数を利用し、多重ループ相当の処理を行いました。
flatten関数を利用すると、複数の配列でループを回し、ループ中の要素を取り出して新しい構造体のMapを得ることができます。
詳しくは公式ドキュメントを参照してみてください。
今回は、ループを回すために3つのlocal変数を定義しました。
locals { # ゾーンのループを回すための定義 zones = { asia-northeast1-a = { name = "asia-northeast1-a" displayName = "ane1a" } asia-northeast1-b = { name = "asia-northeast1-b" displayName = "ane1b" } asia-northeast1-c = { name = "asia-northeast1-c" displayName = "ane1c" } } # ゾーン、バックエンドサービスのループを回すための定義 negs = flatten([ for zone_key, zone in local.zones : [ for service_key, service in var.service : { service = service zone = zone } ] ]) # ゾーン、バックエンドサービス、エンドポイントのループを回すための定義 backends = flatten([ for zone_key, zone in local.zones : [ for service_key, service in var.service : [ for backend_endpoint in service.backend_endpoints : { service = service backend_endpoint = backend_endpoint zone = zone } ] ]]) }
上記の定義をすることで、リソース作成の記述で多重ループを実現することができます。
Hybrid NEGの作成
Hybrid NEGの作成は以下のように記述します。
resource "google_compute_network_endpoint_group" "neg" { for_each = { for neg in local.negs : "${neg.service.name}.${neg.zone.displayName}" => neg } provider = google-beta network = "projects/<project_id>/global/networks/<your_vpc>" name = "${each.value.service.name}-neg-${each.value.zone.displayName}" default_port = each.value.service.health_check_port zone = each.value.zone.name network_endpoint_type = "NON_GCP_PRIVATE_IP_PORT" }
こちらでは、先ほど定義した local変数の negs
を使って、 for_each
でループを回すことで、ゾーン / バックエンドサービスごとにNEGを作成することができます。
Hybrid NEGを作成するため、network_endpoint_type
に NON_GCP_PRIVATE_IP_PORT
を指定しています。(2022年5月時点では、ベータ機能のため、providerには google-beta
を指定しています)
neg
の要素には、each.value
以下でアクセスすることができます。
for_each
中の"${neg.service.name}.${neg.zone.displayName}" => neg
の部分については、次の項で説明します。
NEGにエンドポイントを追加
次に、先ほど作成したNEGにエンドポイントを追加する記述を記載します。
resource "google_compute_network_endpoint" "default-endpoint" { for_each = { for backend in local.backends : "${backend.service.name}.${backend.zone.name}.${backend.backend_endpoint}" => backend } provider = google-beta network_endpoint_group = google_compute_network_endpoint_group.neg["${each.value.service.name}.${each.value.zone.displayName}"].name port = google_compute_network_endpoint_group.neg["${each.value.service.name}.${each.value.zone.displayName}"].default_port ip_address = each.value.backend_endpoint }
先ほど作成したNEGのリソースの指定の仕方がポイントで、neg["${each.value.service.name}.${each.value.zone.displayName}"]
のようにアクセスしています。
これは、前項で記述したfor_each
部分で、リソースにアクセスするためのキーをこのように指定しているためです。
# ${neg.service.name}.${neg.zone.displayName} の部分でリソースにアクセスするためのキーを指定 for_each = { for neg in local.negs : "${neg.service.name}.${neg.zone.displayName}" => neg }
以上で、NEGの作成が完了したので、実際にGCLBで利用できるよう記述していきます。
Backend Serviceの指定
Backend Serviceは以下のように記述しました。
resource "google_compute_backend_service" "backend_service" { for_each = var.service name = "${var.name}-${each.value.name}-backend" port_name = "http" protocol = "HTTP" timeout_sec = 10 enable_cdn = false security_policy = each.value.security_policy != "" ? each.value.security_policy : "" backend { balancing_mode = "RATE" max_rate_per_endpoint = 6000 group = google_compute_network_endpoint_group.neg["${each.value.name}.ane1a"].self_link } backend { balancing_mode = "RATE" max_rate_per_endpoint = 6000 group = google_compute_network_endpoint_group.neg["${each.value.name}.ane1b"].self_link } backend { balancing_mode = "RATE" max_rate_per_endpoint = 6000 group = google_compute_network_endpoint_group.neg["${each.value.name}.ane1c"].self_link } log_config { enable = true } health_checks = [google_compute_health_check.health_check[each.key].self_link] }
以上で、Terraformを利用してHybrid NEGを作成し、GCLBで利用する記述を記載しました。
その他のリソース
直接Hybrid NEGに関わる部分だけを記載しましたが、他にも多くリソースを作成しているので、モジュールの中身を一式記載しておきますので、作成を考えている方は参考にしてみてください。
モジュール一式の内容を見る
##### variable zone ##### variable "name" {} variable "certificates" { type = list(string) } variable "global_address" {} variable "default_backend_service" {} variable "service" { type = map(object({ name = string host = string port = string health_check_port = string health_check_path = string security_policy = string backend_endpoints = list(string) })) } locals { zones = { asia-northeast1-a = { name = "asia-northeast1-a" displayName = "ane1a" } asia-northeast1-b = { name = "asia-northeast1-b" displayName = "ane1b" } asia-northeast1-c = { name = "asia-northeast1-c" displayName = "ane1c" } } negs = flatten([ for zone_key, zone in local.zones : [ for service_key, service in var.service : { service = service zone = zone } ] ]) backends = flatten([ for zone_key, zone in local.zones : [ for service_key, service in var.service : [ for backend_endpoint in service.backend_endpoints : { service = service backend_endpoint = backend_endpoint zone = zone } ] ]]) } ### NEG resource "google_compute_network_endpoint_group" "neg" { for_each = { for neg in local.negs : "${neg.service.name}.${neg.zone.displayName}" => neg } provider = google-beta network = "projects/<project_id>/global/networks/<your_vpc>" name = "${each.value.service.name}-neg-${each.value.zone.displayName}" default_port = each.value.service.health_check_port zone = each.value.zone.name network_endpoint_type = "NON_GCP_PRIVATE_IP_PORT" } resource "google_compute_network_endpoint" "default-endpoint" { for_each = { for backend in local.backends : "${backend.service.name}.${backend.zone.name}.${backend.backend_endpoint}" => backend } provider = google-beta network_endpoint_group = google_compute_network_endpoint_group.neg["${each.value.service.name}.${each.value.zone.displayName}"].name port = google_compute_network_endpoint_group.neg["${each.value.service.name}.${each.value.zone.displayName}"].default_port ip_address = each.value.backend_endpoint } ##### resource zone ##### resource "google_compute_global_forwarding_rule" "https_forward_rule" { name = "${var.name}-https" target = google_compute_target_https_proxy.https_proxy.self_link ip_address = var.global_address port_range = "443" } resource "google_compute_global_forwarding_rule" "http_forward_rule" { name = "${var.name}-http" target = google_compute_target_http_proxy.http_proxy.self_link ip_address = var.global_address port_range = "80" } resource "google_compute_target_https_proxy" "https_proxy" { name = "${var.name}-https-proxy" url_map = google_compute_url_map.url_map.self_link ssl_certificates = var.certificates quic_override = false ssl_policy = google_compute_ssl_policy.ssl_policy.self_link } resource "google_compute_target_http_proxy" "http_proxy" { name = "${var.name}-http-proxy" url_map = google_compute_url_map.https_redirect_url_map.self_link } resource "google_compute_ssl_policy" "ssl_policy" { name = "${var.name}-ssl-policy" profile = "MODERN" min_tls_version = "TLS_1_2" } resource "google_compute_url_map" "https_redirect_url_map" { name = "${var.name}-https-redirect-url-map" default_url_redirect { redirect_response_code = "MOVED_PERMANENTLY_DEFAULT" https_redirect = true strip_query = false } } resource "google_compute_url_map" "url_map" { name = "${var.name}-url-map" default_service = google_compute_backend_service.backend_service[var.default_backend_service].self_link lifecycle { ignore_changes = [host_rule, path_matcher] } dynamic "host_rule" { for_each = var.service content { hosts = [ host_rule.value.host, ] path_matcher = "path-matcher-${host_rule.value.name}" } } dynamic "path_matcher" { for_each = var.service content { default_service = google_compute_backend_service.backend_service[path_matcher.key].self_link name = "path-matcher-${path_matcher.value.name}" } } } ######################## Backend Service ########################## resource "google_compute_backend_service" "backend_service" { for_each = var.service name = "${var.name}-${each.value.name}-backend" port_name = "http" protocol = "HTTP" timeout_sec = 10 enable_cdn = false security_policy = each.value.security_policy != "" ? each.value.security_policy : "" backend { balancing_mode = "RATE" max_rate_per_endpoint = 6000 group = google_compute_network_endpoint_group.neg["${each.value.name}.ane1a"].self_link } backend { balancing_mode = "RATE" max_rate_per_endpoint = 6000 group = google_compute_network_endpoint_group.neg["${each.value.name}.ane1b"].self_link } backend { balancing_mode = "RATE" max_rate_per_endpoint = 6000 group = google_compute_network_endpoint_group.neg["${each.value.name}.ane1c"].self_link } log_config { enable = true } health_checks = [google_compute_health_check.health_check[each.key].self_link] } ######################## Health Check ########################## resource "google_compute_health_check" "health_check" { for_each = var.service provider = google-beta name = "${var.name}-${each.value.name}-health-check" http_health_check { host = each.value.host port = each.value.health_check_port request_path = each.value.health_check_path } log_config { enable = true } }
私自身も、伸び代を感じている部分があるので、もしより良い実装方法があればコメントいただきたいです!
注意点
現時点では、Hybrid NEGをTerraformで作成する場合は、リソースを作成するProjectと同じVPCネットワークを指定する必要があります。(ネットワークをフルパスで記載すること自体は可能なのですが、処理中でProject IDが置換されてしまう実装になっています)
なので、もし共有VPCを利用しているなどで、他のプロジェクトのVPCに紐づけたい場合は gcloud
コマンドを利用して作成すると良いと思います。
弊社でも共有VPCを別プロジェクトに作成している場合があるので、時間があるときにPRを投げたいなとは思っています。
おわりに
今回は、Hybrid NEGを利用してアプリケーションをGCLBで公開するためのterraform moduleを実装する手順を説明しました。
Hybrid NEGの登場によって、オンプレのアプリケーションでもGCPと同様のセキュリティ対策や運用改善ができるようになり、非常に助かるアップデートだと感じました。
個人的には、Terraformで多重ループが発生するなどやや複雑な処理をやってみて、学びがありました。
ユーザベース SaaS事業部のSREチームでは、GCP・オンプレミス・AWSなど複数のプラットフォームを利用して、さまざまなプロダクトを運用しているので、このようなハイブリッドクラウド環境の運用改善に携わる機会が多くあります。
エンジニアも積極的に募集しているので、ご興味をお持ちいただいた方はお話ししましょう! 以下からエントリーをお待ちしております。