✍️

Google Cloud/Terraform シンプルなWebサイト用構成を作ってみた

2022/04/16に公開

TL;DR

シンプルなWebサイト用構成をTerraformを使用して、Google Cloud上で作ってみようと思います。
まずは構成を考える上で検討した内容と、VPC及びGCEインスタンスを作っていこうと思います。

考えた構成

構成図

構成する上で検討した内容

  1. テスト用VPCを1つ構成し、「asia-northeast1」リージョンでサブネットを2つ作成する。
    ※構成図で「VPC1」と書いた方のVPCとなります。「VPC2」については、Cloud SQLについて書く時に改めて説明します。
  2. 作成したサブネットの内、1つにGCEインスタンス1台を配置する。
  3. GCEインスタンスには外部IPアドレスを持たせず、外部(インターネット)にアクセスする際は、Cloud NAT経由でアクセスするようにする。
  4. インタネットからのアクセスは、Cloud Load Balancing(HTTP)が受けて、GCEインスタンスにリクエストを転送できるようにする。
    ※今回はGCEインスタンス1台で構成しておりますが、本番環境ではGCEインスタンス2台以上の冗長構成を採用するケースがほとんどと思うので、ロードバランサ配下にGCEインスタンスを配置します。
  5. テスト用VPCのリソースからプライベートIPアドレスでアクセス可能なCLoud SQLインスタンス(エンジン:PostgreSQL)を配置する。

Terraformでコードを書く方針

Terraformでコードを作成する方針は、以下とします。

  1. 作成するリソースの名前の接頭辞に自分が作成したリソースとして判別できるように適当なプロジェクト名を入れる。
  2. 各リソース定義の記載前に何のリソースを作成するかコメントする。

リソースの作成~その1~

VPC

確認した内容

  1. サブネットはリージョナルリソースとなる。[1]
    ゾーンの選択は、コンピュートインスタンスを作成する際に選択する。
  2. Cloud NATはリージョナルリソースなので、一つのVPCに異なるリージョンのサブネットを配置する場合は、それぞれのサブネットにCloud NATを構成する必要がある。[2]

構築ポイント

  1. 一つのVPCを作成し、「asia-northeast1」リージョンでサブネットを2つ(an1-private1:192.168.1.0/24、an1-private2:192.168.2.0/24)を作成する。
    Google CloudのVPCは、一つのVPCに異なるアベイラビリティゾーンのサブネットを所属させることができるが、まずは同じリージョンのサブネットとして構成してみる。
  2. 「asia-northeast1」リージョンにCloud NATを作成し、外部IPを持たせないGCEインスタンスが、インターネットに接続できるようにする。
    Cloud NAT単体だけではなく、ルーティングに必要なCloud Routerも同時に作成する。
  3. Cloud NATには静的なグローバルIPアドレスを手動でアサインする。

コード

# VPC
resource "google_compute_network" "vpc_network" {
  name                    = "btc4043-vpc"
  auto_create_subnetworks = false
  routing_mode            = "REGIONAL"
}

# Subnet1(asia-northeast1)
resource "google_compute_subnetwork" "an1_private1" {
  name          = "btc4043-an1-private1"
  ip_cidr_range = "192.168.1.0/24"
  region        = "asia-northeast1"
  network       = google_compute_network.vpc_network.id
}

# Subnet2(asia-northeast1)
resource "google_compute_subnetwork" "an1_private2" {
  name          = "btc4043-an1-private2"
  ip_cidr_range = "192.168.2.0/24"
  region        = "asia-northeast1"
  network       = google_compute_network.vpc_network.id
}

# Cloud Router (for asia-northeast1)
resource "google_compute_router" "an1_router" {
  name    = "btc4043-an1-router"
  region  = google_compute_subnetwork.an1_private1.region
  network = google_compute_network.vpc_network.id
}

# External ip for Cloud NAT
resource "google_compute_address" "nat_external_ipaddress" {
  name = "btc4043-nat-external-ip"
}

# Cloud NAT
resource "google_compute_router_nat" "an1_nat" {
  name                               = "btc4043-an1-nat"
  router                             = google_compute_router.an1_router.name
  region                             = google_compute_router.an1_router.region
  nat_ip_allocate_option             = "MANUAL_ONLY"
  nat_ips                            = google_compute_address.nat_external_ipaddress.*.self_link
  source_subnetwork_ip_ranges_to_nat = "LIST_OF_SUBNETWORKS"

  subnetwork {
    name                    = google_compute_subnetwork.an1_private1.id
    source_ip_ranges_to_nat = ["ALL_IP_RANGES"]
  }

  subnetwork {
    name                    = google_compute_subnetwork.an1_private2.id
    source_ip_ranges_to_nat = ["ALL_IP_RANGES"]
  }
}

GCEインスタンス

確認した内容

  1. サービスアカウントとアクセススコープの違いについて把握し、適切な権限を付与する必要がある。[3]
  2. IAP接続に必要なアクセス開放設定をファイアウォールで設定する必要があり、ソースIPアドレスは指定がある。[4]

構築ポイント

  1. GCEインスタンスに付与するサービスアカウントを明示的に作成する。
    ※インスタンス上のアプリケーションで、Google Cloud上の他リソースに対してアクセスが必要な場合は、適切なロールを付与する。
  2. デプロイしたGCEインスタンスから後述するCloud SQLインスタンスにDB接続できるように、アクセススコープとして「cloud-platform」以外に「sql-admin」も付与しておく。
  3. 今回の構成では、踏み台用インスタンスは配置しないので、外部IPアドレスを持たないGCEインスタンスに対しては、IAP経由でSSH接続できるようにする。
  4. インスタンスタイプは「e2-micro」、OSのイメージは「centos-stream-8」を選択する。
    GECインスタンスをデプロイした後、apacheのインストール、index.htmlなど配置する。

コード

# Service account for GCE instance
resource "google_service_account" "sc_gce1" {
  account_id   = "btc4043-sc-gce1"
  display_name = "Service Account for GCE1"
}

# Firewall1 for GCE instance
resource "google_compute_firewall" "fw_gce1" {
  name    = "btc4043-fw-gce1"
  network = google_compute_network.vpc_network.name

  direction = "INGRESS"

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  source_ranges = [
    "35.235.240.0/20"
  ]

}

# GCE instance
resource "google_compute_instance" "gce1" {
  name         = "btc4043-gce1"
  machine_type = "e2-micro"
  zone         = "asia-northeast1-b"

  boot_disk {
    initialize_params {
      size  = 20
      image = "centos-stream-8"
    }
  }

  network_interface {
    subnetwork = google_compute_subnetwork.an1_private1.self_link
  }

  metadata = {
    Name = "btc4043-fw-gce1"
  }

  service_account {
    email  = google_service_account.sc_gce1.email
    scopes = ["cloud-platform", "sql-admin"]
  }

  tags = ["btc4043-fw-gce1"]

}

インスタンスグループ

確認した内容

  1. Cloud Load BalacingのバックエンドサービスとしてGCEインスタンスを構成する場合は、対象GECインスタンスのディスクイメージからインスタンステンプレートを作成し、そのテンプレートからインスタンスグループを構成する必要がある。[5]
  2. GCEインスタンスでは、Cloud Load Balacingからのヘルスチェックを受け入れる必要があるので、ヘルスチェックを受け入れるため、指定グローバルIPアドレスレンジからのファイアウォール許可設定を追加する必要がある。[6]
  3. マネージドインスタンスグループでは、名前付きポートの割り当てを行い、Cloud Load Balancingのバックエンドサービスの構成内で登録しておく必要があり、この名前付きポートにリクエストが到達する。[7]

構築ポイント

  1. 前回のGCEインスタンス用コードを使ってデプロイしたインスタンスにIAP接続し、httpd、postgresqlのパッケージインストールを実施後、httpdの自動起動設定とindex.htmlの配置などの初期設定を行う。
  2. Google Cloud Consoleから初期設定が完了したGCEインスタンスのストレージのイメージを手動で作成する。
  3. インスタンステンプレートは、項番2で作成したディスクイメージを基とし、IAP接続以外に、Cloud Load Balancingからのヘルスチェックを受け入れられるように、ファイアウォール許可設定を追加する。
  4. インスタンスグループは、マネジメントインスタンスグループとして構成し、シングルゾーンでインスタンス1台のみ起動させて、自動スケーリングは設定なしという構成にする。
    また、GCEインスタンス上で茶華道しているHTTPサービス(TCP/80番)を名前付きポートとして構成する。

コード

# Instanece disk image (made by console)
data "google_compute_image" "gce1_image" {
  name = "btc4043-gce1-image"
}

# Firewall2 for GCE instance
resource "google_compute_firewall" "fw_gce2" {
  name    = "btc4043-fw-gce2"
  network = google_compute_network.vpc_network.name

  direction = "INGRESS"

  allow {
    protocol = "tcp"
    ports    = ["80"]
  }

  source_ranges = [
    "35.191.0.0/16",
    "130.211.0.0/22"
  ]

}

# Instance template
resource "google_compute_instance_template" "gce1_template" {
  name        = "btc4043-gce1-template"
  description = "gce1 instance template"

  tags = ["btc4043-fw-gce1", "btc4043-fw-gce2"]

  machine_type = "e2-micro"

  scheduling {
    automatic_restart = true
  }

  disk {
    source_image = data.google_compute_image.gce1_image.self_link
    auto_delete  = true
    boot         = true
  }

  network_interface {
    subnetwork = google_compute_subnetwork.an1_private1.self_link
  }

  service_account {
    email  = google_service_account.sc_gce1.email
    scopes = ["cloud-platform", "sql-admin"]
  }
}

# Managed instance group
resource "google_compute_instance_group_manager" "gce1-mig" {
  name = "btc4043-gce1-mig"

  base_instance_name = "btc4043-gce1"
  zone               = "asia-northeast1-b"

  target_size = 1

  named_port {
    name = "http"
    port = 80
  }

  version {
    instance_template = google_compute_instance_template.gce1_template.id
  }

}

Cloud Load Balacing

確認した内容

  1. 従来型の外部HTTPロードバランサ用のTerraformサンプルコードがあったので、参考にした。[8]

Cloud Load Balacingを構成しようと試みた時、どういったリソースを順次組み立てていく必要があるのかが掴みにくかった。
そんな時に、以下のドキュメントの「単純なホストとパスのルール」を一読し、サンプルコードを参考することで、Cloud Load Balacingをどのように構成したら良いか(リクエストがどのようなフローでバックエンドサービスに到達するか)理解できた。
https://cloud.google.com/load-balancing/docs/https/traffic-management#simple_host_and_path_rule

構築ポイント

  1. 従来のグローバル外部HTTPロードバランサを導入し、予約したグローバルIPアドレスをアサインする。
  2. バックエンドサービスには先ほど作成したインスタンスグループを指定し、Cloud CDNを有効化して静的ファイルをキャッシュし、コンテンツがCDNから取得したかどうかを判別できるようにカスタムレスポンスヘッダを付与する。

コード

# reserved IP address
resource "google_compute_global_address" "clb_ip_address" {
  name = "btc4043-clb-ip-address"
}

# Forwarding rule for Regional External Load Balancing
resource "google_compute_global_forwarding_rule" "clb1" {
  name = "btc4043-clb1"

  load_balancing_scheme = "EXTERNAL"
  ip_protocol           = "TCP"
  port_range            = "80"
  target                = google_compute_target_http_proxy.clb1_httpproxy.id
  ip_address            = google_compute_global_address.clb_ip_address.id
}

# http proxy
resource "google_compute_target_http_proxy" "clb1_httpproxy" {
  name    = "btc4043-clb1-httpproxy"
  url_map = google_compute_url_map.clb1_urlmap.id
}

# url map
resource "google_compute_url_map" "clb1_urlmap" {
  name            = "btc4043-clb1-urlmap"
  default_service = google_compute_backend_service.clb1_backend.id
}

# Backend service
resource "google_compute_backend_service" "clb1_backend" {
  name                  = "btc4043-clb1-backend"
  load_balancing_scheme = "EXTERNAL"
  protocol              = "HTTP"
  timeout_sec           = 10

  enable_cdn              = true
  custom_response_headers = ["X-Cache-Hit: {cdn_cache_status}"]

  health_checks = [google_compute_health_check.clb1_healthcheck.id]

  backend {
    group           = google_compute_instance_group_manager.gce1-mig.instance_group
    balancing_mode  = "UTILIZATION"
    capacity_scaler = 1.0
  }
}

# Health check
resource "google_compute_health_check" "clb1_healthcheck" {
  name = "btc4043-clb1-healthcheck"

  http_health_check {
    port_specification = "USE_SERVING_PORT"
  }
}

Cloud SQL

確認した内容

  1. Cloud SQLプライベートアクセスの概要、 プライベートIPアドレスを持たせたいCloud SQLインスタンスが配置されるGoogleサービスプロデューサーのVPCネットワーク、自分で作成したVPCとの接続について理解する必要があったが、以下の例図が非常に参考になった。

https://cloud.google.com/sql/docs/mysql/private-ip#example

  1. RFC1918アドレス範囲からCloud SQLへのプライベートアクセスは自動的に承認されるので、すべてのプライベートクライアントがプロキシを経由せずにデータベースにアクセスできるので、承認済みネットワークの構成は必要ない。[9]

構築ポイント

  1. 自分で作成したテスト用VPCでCloud SQLのプライベートサービスアクセスを設定し、GoogleサービスプロデューサーのVPCネットワークには「192.168.10.0/24」のサブネットを割り振り、テスト用VPCとVPCピアリングを張るようにする。
  2. Cloud SQLインスタンスのエンジンは「PostgreSQL 13」とし、シングル構成とする。
    また、外部IPは持たせず、テスト用VPCからのプライベート接続を許可する。
  3. Cloud Console上のCloud SQL作成ウィザードで自動作成される「postgres」をユーザをterraformから明示的に作成する。

コード

# Assign network address for service producers
resource "google_compute_global_address" "db_subnetwork_ip_address" {

  name          = "btc4043-db-subnetwork-ip-address"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  address       = "192.168.10.0"
  prefix_length = 24
  network       = google_compute_network.vpc_network.id
}

# Network private service access
resource "google_service_networking_connection" "db_network_connection" {
  network                 = google_compute_network.vpc_network.id
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.db_subnetwork_ip_address.name]
}

# Cloud SQL DBinstance 
resource "google_sql_database_instance" "db01" {

  name             = "btc4043-db01"
  database_version = "POSTGRES_13"

  depends_on = [google_service_networking_connection.db_network_connection]

  settings {
    tier = "db-f1-micro"
    availability_type = "ZONAL"
    disk_size         = 10
    disk_autoresize   = false
    disk_type         = "PD_SSD"

    backup_configuration {
      enabled = true
      start_time = "20:00"
      point_in_time_recovery_enabled = true
      transaction_log_retention_days = 2

        backup_retention_settings {
          retained_backups = 2
          retention_unit   = "COUNT"
        }
    }

    ip_configuration {
      ipv4_enabled    = false
      private_network = google_compute_network.vpc_network.id
    }

    maintenance_window {
      day   = 6
      hour  = 20
    }
  }
}

# Cloud SQL user
resource "google_sql_user" "db01_user" {
  instance = google_sql_database_instance.db01.name
  name     = "postgres"
  password = "任意のパスワード"
}

デプロイ前の一仕事

  1. 今回作成するリソースに対して、以下必要なAPIをCloud Consoleで有効化
  • Compute Engine API
  • Cloud SQL Admin API
  • Service Networking API
  • Identity and Access Management (IAM) API
  • Cloud Resource Manager API
  • Network Management API
  1. プライベートサービスを管理できるようにTerraform用サービスアカウントに Compute ネットワーク管理者のロール(roles/compute.networkAdmin)を付与

https://cloud.google.com/vpc/docs/configure-private-services-access#permissions

デプロイ

「terraform apply」コマンドを発行し、デプロイを実施したところ、エラーなく各リソースのデプロイが完了しました。
Cloud SQLインスタンスはシングル構成だが、作成に20分かかりました。

terraform apply

デプロイ後の確認

インスタンスグループのGCEインスタンスにIAPでSSH接続

  1. Cloud IAP APIを有効化を実施
  2. Cloud Shellを起動して、接続
$ gcloud compute ssh btc4043-gce1-6ts1 --zone=asia-northeast1-b --tunnel-through-iap

インスタンスに接続できて、rootユーザにスイッチ可能なことを確認!

GCEインスタンスの接続元グローバルIPの確認

$ curl http://ifconfig.me/

Cloud Consoleの「VPCネットワーク」-「外部 IP アドレス」で確認したCloud NAT「btc4043-nat-external-ip」にアサインされた一致していることを確認!

GCEインスタンスからCloud SQLへのアクセス

$ psql -h 192.168.10.3 -U postgres
Password for user postgres:
psql (10.19, server 13.5)
WARNING: psql major version 10, server major version 13.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=>

GCEインスタンスからpsqlコマンドを発行し、作成したpostgresユーザとそのパスワードを使用して、Cloud SQLにアクセスできることを確認!

GCEインスタンスで稼働しているサイトへのアクセス

ブラウザを立ち上げて、Cloud Load Balacingにアサインされた静的グローバルIPアドレスを記載した上記のURLにアクセスしたら、index.htmlに保存した文字が表示されりことを確認!

まとめ

自身の経験からシンプルなWebサイト構成を考えてみて、Terraformコードを作成し、デプロイ後想定通りの動作が確認できました。
今後は、このコードを活用して、もっと実用的な構成(GCEインスタンスのオートスケーリング機能有効化、Cloud SQLのマルチ構成)なども検証してみようと思いました。

自己満足のような形になりましたが、分かりやすい説明を心掛けたり、時間の工夫など改めてブログを書く難しさを感じた10日間でした。

脚注
  1. https://cloud.google.com/vpc/docs/vpc#vpc_networks_and_subnets ↩︎

  2. https://cloud.google.com/nat/docs/overview#example-nat-simple ↩︎

  3. https://cloud.google.com/compute/docs/access/service-accounts?hl=ja#authorization ↩︎

  4. https://cloud.google.com/iap/docs/using-tcp-forwarding#create-firewall-rule ↩︎

  5. https://cloud.google.com/load-balancing/docs/backend-service#backends ↩︎

  6. https://cloud.google.com/load-balancing/docs/health-checks#firewall_rules ↩︎

  7. https://cloud.google.com/compute/docs/instance-groups/adding-an-instance-group-to-a-load-balancer?hl=ja#assign_named_ports ↩︎

  8. https://cloud.google.com/load-balancing/docs/https/ext-http-lb-tf-module-examples#with_mig_backend_and_custom_headers ↩︎

  9. https://cloud.google.com/sql/docs/mysql/configure-private-ip?hl=ja#connect_fr ↩︎

Discussion