G-gen の藤岡です。当記事では、Google Cloud (旧称 GCP) の Cloud DNS を使うことで複数のリージョンにあるインスタンスの内部 IP アドレスで HTTP(S) 通信を振り分ける方法を紹介します。環境の作成には Terraform を使います。
Cloud DNS とは
Cloud DNS は Google Cloud が提供するマネージドな DNS サービスです。Cloud DNS に関する用語については以下の記事をご参照ください。 また当記事で設定するゾーン名やレコード名は、わかりやすいように以下の記事と同一にしています。 blog.g-gen.co.jp
Cloud DNS では一般的な DNS と同様に、1つのドメイン名(例: www.g-gen.local
)に対して複数の A レコードを持つことができます。当記事では、複数のリージョンにある Compute Engine インスタンスの内部 IP アドレスへの振り分けを行います。
- 参考:DNS レコードの概要
検証の背景
複数リージョンにまたがるバックエンドへの負荷分散
HTTP(S) 通信の負荷分散や災害対策として、一般的にはロードバランサーが利用されます。Google Cloud で提供されているロードバランサーは Cloud Load Balancing で、以下の 9 種類があります。
バックエンドの VM が複数のリージョンにまたがる場合は External HTTP(S) Load Balancing が選択肢になります。
しかし External なロードバランサーのエンドポイントはインターネットに公開されます。そのため例えば以下のようにオンプレミスと Google Cloud を Cloud VPN や Cloud Interconnect で接続している場合で、オンプレミスのクライアントから複数リージョンの Compute Engine VM に対して HTTP(S) 通信を振り分けたい場合は、選択できる Cloud Load Balancing が存在しません。その理由は、Internal なロードバランサーは単一リージョンのバックエンドにしかトラフィックを振り分けられないことに起因します。また、Internal なロードバランサー自体がリージョンに所属するリソースであるため、ロードバランサーが存在するリージョンが障害になるとバックアップリージョンへの振り分けが不可になることも問題の一つです。
→2023年8月に Cross-region internal Application Load Balancer がリリースされ、現在では内部ロードバランサー(Internal なロードバランサー)でも、複数リージョンのバックエンドにトラフィックを振り分けられるようになりました。
Cloud DNS による負荷分散
よって、インターナルな通信でバックエンドが複数のリージョンにまたがる場合、当記事で紹介する Cloud DNS による負荷分散が選択肢の 1 つとなります。
ただし、以下の条件・制約が存在します。
- RTO は数時間
- Cloud DNS には IP アドレス/VM に対するヘルスチェック・自動フェイルオーバが無いため
- ただし internal passthrough Network Load Balancer と internal Application Load Balancer へのヘルスチェック機能は存在するため、各リージョンに LB を設置すれば自動フェイルオーバが可能
- Cloud Load Balancing の詳細なトラフィック制御やインスタンスグループ機能は使用できない
なお当記事では Google Cloud 内で完結する構成となっていますが、オンプレミスと Cloud DNS を組み合わせたベストプラクティスは ドキュメント をご参照ください。
実施内容
構成図
今回作成する構成は以下の通りです。 Compute Engine インスタンスに Apache をインストールし、東京と大阪リージョンのインスタンスで /var/www/html/index.html
の内容を変えることでどちらのインスタンスに振り分けられているか確認します。
前提
- 実行環境
- Cloud Shell で後述の
Terraform のコード
を実行
- Cloud Shell で後述の
- 外部 IP アドレス
- 振り分けの確認用でインスタンスに Apache や dig をインストールするため、外部 IP アドレスを付与
- インスタンスへの接続
- インスタンスへの接続は Cloud IAP を使用
- 当記事で扱わないこと
- 各リソースの作成に必要な権限
ディレクトリ構成
以下 2 つの .tf
ファイルを用意しました。コードの内容は後述します。
fujioka@cloudshell:~/terraform (xxxx)$ tree . ├── main.tf └── variables.tf 0 directories, 2 files fujioka@cloudshell:~/terraform (xxxx)$
構築
プロジェクトの作成と請求先アカウントの紐づけ
プロジェクトを作成し、作成したプロジェクトに請求先アカウントを紐づけます。
$ gcloud projects create ${PROJECT_ID} --name=${PROJECT_NAME} --organization=${ORGANIZATION_ID} && \ gcloud beta billing projects link ${PROJECT_ID} --billing-account=${BILLING_ACCOUNT_ID}
デフォルトプロジェクトのセット
作成したプロジェクトをデフォルトプロジェクトとしてセットし、結果を確認します。
$ gcloud config set project ${PROJECT_ID} && \ gcloud config list project
Terraform の実行
Terraform を実行します。
# .tf ファイルのあるディレクトリに移動 $ cd terraform/ # 初期化 $ terraform init # 確認 (今回は 22 個のリソースが作成されます) $ terraform plan ... Plan: 22 to add, 0 to change, 0 to destroy. ... # 適用 $ terraform apply
確認
正常時の振り分けの確認
vpc-a の vm-soul から watch -d -n 3 "curl www.g-gen.local | tee -a output.log"
コマンドを実行すると、東京リージョンと大阪リージョンにあるインスタンスそれぞれにアクセスが振り分けられています。
東京リージョンのインスタンスが停止した時の挙動
vm-tokyo を手動で停止し、挙動を確認します。Cloud DNS には IP アドレスや Compute Engine VM に対してヘルスチェックをする機能がないため、www.g-gen.local
が vm-tokyo の IP アドレスに名前解決されてしまった場合、レスポンスが返ってきません。
フェイルオーバの方法
あるリージョンのサービスが停止してしまった場合、サービス停止時にアラートを飛ばして人が対処する、もしくは自動的に A レコードを削除する仕組みを作る、等の対策が必要です。
また、 Cloud DNS にはグローバルアクセスが有効化された internal passthrough Network Load Balancer と internal Application Load Balancer に限ったヘルスチェック機能が用意されています。
自動的な DNS フェイルオーバを実装したい場合、各リージョンにロードバランサを配置することも検討します。
Terraform のコード
今回使用したコードは以下の 2 つのファイルです。variables.tf
の ${PROJECT_ID}
は置き換えてください。
- main.tf
############################################## # 共通設定 # ############################################## # terraform 設定 terraform { required_version = "~> 1.3" required_providers { google = ">= 4.63.1" } } # provider 設定 provider "google" { project = var.project } # api 有効化 resource "google_project_service" "enabled_apis" { for_each = toset(var.enabled_apis_list) service = each.key disable_on_destroy = true } # サービスアカウント作成 resource "google_service_account" "service_account_for_vm" { account_id = "service-account-for-vm" display_name = "VM 用サービスアカウント" depends_on = [ google_project_service.enabled_apis ] } # サービスアカウント権限付与 resource "google_project_iam_member" "service_account_for_vm_role" { project = var.project role = "roles/compute.admin" member = "serviceAccount:${google_service_account.service_account_for_vm.email}" } ############################################## # vpc-a の設定 # ############################################## /****************************** ネットワーク設定 ******************************/ # vpc 作成 resource "google_compute_network" "vpc_a" { name = "vpc-a" auto_create_subnetworks = "false" routing_mode = "GLOBAL" depends_on = [ google_project_service.enabled_apis ] } # subnet 作成 resource "google_compute_subnetwork" "subnet_soul" { name = "subnet-soul" ip_cidr_range = var.subnet_cidr_soul region = var.region_soul network = google_compute_network.vpc_a.name } # firewall 作成 resource "google_compute_firewall" "allow_ssh_from_iap_a" { name = "allow-ssh-from-iap-a" network = google_compute_network.vpc_a.name direction = "INGRESS" allow { protocol = "tcp" ports = ["22"] } source_ranges = [var.iap_ip_range] } /****************************** gce 作成 ******************************/ resource "google_compute_instance" "vm_soul" { name = "vm-soul" machine_type = var.instance_type zone = var.zone_soul service_account { email = google_service_account.service_account_for_vm.email scopes = ["cloud-platform"] } boot_disk { initialize_params { image = var.instance_os } } metadata_startup_script = <<EOF #! /bin/bash apt update apt -y install dnsutils EOF network_interface { network = google_compute_network.vpc_a.name subnetwork = google_compute_subnetwork.subnet_soul.name network_ip = var.internal_ip_soul access_config {} } allow_stopping_for_update = true } /****************************** Cloud DNS ******************************/ # ピアリングゾーン作成 resource "google_dns_managed_zone" "g_gen_local_peering_zone" { name = "g-gen-local-peering-zone" dns_name = "g-gen.local." depends_on = [ google_project_service.enabled_apis ] visibility = "private" private_visibility_config { networks { network_url = google_compute_network.vpc_a.id } } peering_config { target_network { network_url = google_compute_network.vpc_b.id } } } ############################################## # vpc-b の設定 # ############################################## /****************************** ネットワーク設定 ******************************/ # vpc 作成 resource "google_compute_network" "vpc_b" { name = "vpc-b" auto_create_subnetworks = "false" routing_mode = "GLOBAL" depends_on = [ google_project_service.enabled_apis ] } # subnet 作成 resource "google_compute_subnetwork" "subnet_tokyo" { name = "subnet-tokyo" ip_cidr_range = var.subnet_cidr_tokyo region = var.region_tokyo network = google_compute_network.vpc_b.name } resource "google_compute_subnetwork" "subnet_osaka" { name = "subnet-osaka" ip_cidr_range = var.subnet_cidr_osaka region = var.region_osaka network = google_compute_network.vpc_b.name } # firewall 作成 resource "google_compute_firewall" "allow_http_from_vpc_a" { name = "allow-http-from-vpc-a" network = google_compute_network.vpc_b.name direction = "INGRESS" allow { protocol = "tcp" ports = ["80", "443"] } source_ranges = [var.internal_ip_range_vpc_a] } resource "google_compute_firewall" "allow_ssh_from_iap_b" { name = "allow-ssh-from-iap-b" network = google_compute_network.vpc_b.name direction = "INGRESS" allow { protocol = "tcp" ports = ["22"] } source_ranges = [var.iap_ip_range] } /****************************** gce 作成 ******************************/ resource "google_compute_instance" "vm_tokyo" { name = "vm-tokyo" machine_type = var.instance_type zone = var.zone_tokyo service_account { email = google_service_account.service_account_for_vm.email scopes = ["cloud-platform"] } boot_disk { initialize_params { image = var.instance_os } } metadata_startup_script = <<EOF #! /bin/bash apt update apt -y install apache2 cat <<EOF > /var/www/html/index.html <html><body><p>Tokyo Instance</p></body></html> EOF network_interface { network = google_compute_network.vpc_b.name subnetwork = google_compute_subnetwork.subnet_tokyo.name network_ip = var.internal_ip_tokyo access_config {} } allow_stopping_for_update = true } resource "google_compute_instance" "vm_osaka" { name = "vm-osaka" machine_type = var.instance_type zone = var.zone_osaka service_account { email = google_service_account.service_account_for_vm.email scopes = ["cloud-platform"] } boot_disk { initialize_params { image = var.instance_os } } metadata_startup_script = <<EOF #! /bin/bash apt update apt -y install apache2 cat <<EOF > /var/www/html/index.html <html><body><p>Osaka Instance</p></body></html> EOF network_interface { network = google_compute_network.vpc_b.name subnetwork = google_compute_subnetwork.subnet_osaka.name network_ip = var.internal_ip_osaka access_config {} } allow_stopping_for_update = true } /****************************** Cloud DNS ******************************/ # ゾーン作成 resource "google_dns_managed_zone" "g_gen_local_zone" { # project = var.project name = "g-gen-local-zone" dns_name = "g-gen.local." depends_on = [ google_project_service.enabled_apis ] visibility = "private" private_visibility_config { networks { network_url = google_compute_network.vpc_b.id } } } # レコード作成 resource "google_dns_record_set" "www" { project = var.project name = "www.${google_dns_managed_zone.g_gen_local_zone.dns_name}" managed_zone = google_dns_managed_zone.g_gen_local_zone.name type = "A" ttl = 3600 rrdatas = [ var.internal_ip_osaka, var.internal_ip_tokyo ] } ############################################## # vpc peering vpc-a <--> vpc-b # ############################################## resource "google_compute_network_peering" "peering1" { name = "peering1" network = google_compute_network.vpc_a.self_link peer_network = google_compute_network.vpc_b.self_link } resource "google_compute_network_peering" "peering2" { name = "peering2" network = google_compute_network.vpc_b.self_link peer_network = google_compute_network.vpc_a.self_link }
- variables.tf
############################################## # プロジェクトレベル # ############################################## variable "project" { type = string default = "${PROJECT_ID}" // プロジェクト ID } variable "enabled_apis_list" { description = "有効化するAPI" type = list(string) default = [ "cloudresourcemanager.googleapis.com", "iam.googleapis.com", "compute.googleapis.com", "dns.googleapis.com", ] } ############################################## # リージョン / ゾーン # ############################################## variable "region_tokyo" { description = "東京リージョンを指定" type = string default = "asia-northeast1" } variable "region_osaka" { description = "大阪リージョンを指定" type = string default = "asia-northeast2" } variable "region_soul" { description = "ソウルリージョンを指定" type = string default = "asia-northeast3" } variable "zone_tokyo" { description = "東京リージョンのゾーンを指定" type = string default = "asia-northeast1-a" } variable "zone_osaka" { description = "大阪リージョンのゾーンを指定" type = string default = "asia-northeast2-a" } variable "zone_soul" { description = "ソウルリージョンのゾーンを指定" type = string default = "asia-northeast3-a" } ############################################## # サブネット # ############################################## variable "subnet_cidr_tokyo" { description = "東京リージョンのサブネット範囲" type = string default = "10.0.0.0/24" } variable "subnet_cidr_osaka" { description = "大阪リージョンのサブネット範囲" type = string default = "10.0.10.0/24" } variable "subnet_cidr_soul" { description = "ソウルリージョンのサブネット範囲" type = string default = "172.16.0.0/24" } ############################################## # ipアドレス # ############################################## variable "internal_ip_tokyo" { description = "東京リージョンのインスタンス内部IPアドレス" type = string default = "10.0.0.10" } variable "internal_ip_osaka" { description = "大阪リージョンのインスタンス内部IPアドレス" type = string default = "10.0.10.10" } variable "internal_ip_soul" { description = "ソウルリージョンのインスタンス内部IPアドレス" type = string default = "172.16.0.10" } variable "internal_ip_range_vpc_a" { description = "project-aの内部IPアドレス範囲" type = string default = "172.16.0.0/24" } variable "iap_ip_range" { description = "IAP用のIPアドレス範囲" type = string default = "35.235.240.0/20" } ############################################## # インスタンス # ############################################## variable "instance_os" { description = "OSイメージ" type = string default = "debian-cloud/debian-11" } variable "instance_type" { description = "インスタンスタイプ" type = string default = "e2-micro" }
藤岡 里美 (記事一覧)
クラウドソリューション部
数年前までチキン売ったりドレスショップで働いてました!2022年9月 G-gen にジョイン。ハイキューの映画を4回は見に行きたい。
Google Cloud All Certifications Engineer / Google Cloud Partner Top Engineer 2024
Follow @fujioka57621469