Cloud DNSで複数リージョンインスタンスの内部IPアドレスへ振り分ける

記事タイトルとURLをコピーする

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 アドレスへの振り分けを行います。

検証の背景

複数リージョンにまたがるバックエンドへの負荷分散

HTTP(S) 通信の負荷分散や災害対策として、一般的にはロードバランサーが利用されます。Google Cloud で提供されているロードバランサーは Cloud Load Balancing で、以下の 9 種類があります。

画像は公式より引用

バックエンドの VM が複数のリージョンにまたがる場合は External HTTP(S) Load Balancing が選択肢になります。

しかし External なロードバランサーのエンドポイントはインターネットに公開されます。そのため例えば以下のようにオンプレミスと Google Cloud を Cloud VPNCloud Interconnect で接続している場合で、オンプレミスのクライアントから複数リージョンの Compute Engine VM に対して HTTP(S) 通信を振り分けたい場合は、選択できる Cloud Load Balancing が存在しません。その理由は、Internal なロードバランサーは単一リージョンのバックエンドにしかトラフィックを振り分けられないことに起因します。また、Internal なロードバランサー自体がリージョンに所属するリソースであるため、ロードバランサーが存在するリージョンが障害になるとバックアップリージョンへの振り分けが不可になることも問題の一つです。

→2023年8月に Cross-region internal Application Load Balancer がリリースされ、現在では内部ロードバランサー(Internal なロードバランサー)でも、複数リージョンのバックエンドにトラフィックを振り分けられるようになりました。

オンプレミスと Google Cloud の構成例

Cloud DNS による負荷分散

よって、インターナルな通信でバックエンドが複数のリージョンにまたがる場合、当記事で紹介する Cloud DNS による負荷分散が選択肢の 1 つとなります。

ただし、以下の条件・制約が存在します。

  1. RTO は数時間
    • Cloud DNS には IP アドレス/VM に対するヘルスチェック・自動フェイルオーバが無いため
    • ただし internal passthrough Network Load Balancer と internal Application Load Balancer へのヘルスチェック機能は存在するため、各リージョンに LB を設置すれば自動フェイルオーバが可能
  2. Cloud Load Balancing の詳細なトラフィック制御やインスタンスグループ機能は使用できない

なお当記事では Google Cloud 内で完結する構成となっていますが、オンプレミスと Cloud DNS を組み合わせたベストプラクティスは ドキュメント をご参照ください。

実施内容

構成図

今回作成する構成は以下の通りです。 Compute Engine インスタンスに Apache をインストールし、東京と大阪リージョンのインスタンスで /var/www/html/index.html の内容を変えることでどちらのインスタンスに振り分けられているか確認します。

構成図

前提

  • 実行環境
    • Cloud Shell で後述の Terraform のコード を実行
  • 外部 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" コマンドを実行すると、東京リージョンと大阪リージョンにあるインスタンスそれぞれにアクセスが振り分けられています。

東京リージョンへ振り分け
大阪リージョンへ振り分け

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