2024.01.19

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

こんにちは。グループ研究開発本部 次世代システム研究室のK.X.Dです。

今回は、有名なIaC実現のTerraformツールを勉強ために、調べならがGcloud上のシンプルなインフラ構成を作ってみようと思います。

目次
  1. やりたいこと
  2. Google Cloud/Terraformセットアップ
  3. Terraformソースコードを書く
  4. インフラ構成検証
  5. おまけ
  6. 最後に
  7. 伝言

1. やりたいこと

以下のような構成で、Gcloudで構築してみようと思います。
  1. 検証用のVPCを構成し、サブネットを2つ作成する
  2. サブネット内にGCEインスタンスを配置する
  3. プラベートGCEはインタネットからアクセスできない
  4. パップリックGCEはNAT経由でインタネットからアクセスできる(ssh, http可能)

2. Google Cloud/Terraformセットアップ

2.1 Terraformに利用するサービスアカウントを作成し、クレデンシャル情報取得する
  • GCPのサービスアカウントを作成してから、サービスアカウントのキータブでJSON形式で、新しい鍵を追加する
作成ができましたら、自動でクレデンシャルJSONファイルは保管します。

  • 画面上にサービスアカウントの新しい鍵が追加されことを確認する

2.2 GCEのCloudConsole APIを有効化する

2.3 GCEのsshキー追加する
  • パソコン上に下記のコマンドを打って、sshキーペアを生成する
ssh-keygen -t rsa -f gcp -C [email protected]
  • GCEのメタデータで生成したsshの公開キーを追加する

2.4 Terraform、gcloud cliをインストール:

自分は下記の公式サイトのインストール手順に参照しました。

https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli

https://cloud.google.com/sdk/docs/install?hl=ja

3. Terraformソースコードを書く

今回は、簡単なProjectStructureで作成しました。

creds Folderは、2.1で保管したサービスアカウントのsshキーです。

vpc_networkingは、terraformソースコードを記述しました。GCPのそれぞれリソースは、1ファイルごとで定義しています。

  • var.tf:Terraformは共通の設定値はConfig化するように「variable」ブロックを提供していますので、今回に活用しています。
variable "region" {
  type = string
  default = "asia-northeast1"
}

variable "zone" {
  type = string
  default = "asia-northeast1"
}

variable "project-name" {
  type = string
  default = "dongkx-project"
}

variable "name" {
  type = string
  description = "Name for this infrastructure"
  default = "tflearn"
}

variable "ip_cidr_range" {
  type = list(string)
  description = "List of the range of internal adddresses that are owned by this subnetwork."
  default = ["10.10.10.0/24", "10.10.20.0/24"]
}
 
  • provider.tf:TerraformのGCPプラグイン利用するために、providerブロックを使って、該当プラグインを定義しています。
※TerraformのStateについて、実行検証の時に説明します。
# gcloud plugin
provider "google" {
    project = var.project-name
    region  = var.region
    zone    = "${var.zone}-a"
    credentials = "../creds/key.json"
}

# Terraform State is saved in gcp bucket
terraform {
  backend "gcs" {
    credentials = "../creds/key.json"
    bucket = "tssate-terraformproject"
    prefix = "terraform/state"    
  }
}
  • vpc.tf:Google Cloud VPC Networkを定義する
routing_mode = “REGIONAL”は同じRegionのサブネットワークのみに伝言する。
#google compute zone will get the list of all availability zone in the specified region
data "google_compute_zones" "this" {
    region = var.region
    project = var.project-name
}

#A local value assigns a name to an expression
locals {
  type = ["public", "private"]
  zones = data.google_compute_zones.this.names
}

#output all available zones
output "az" {
  value = data.google_compute_zones.this.names
}

# VPC
resource "google_compute_network" "this" {
  name = "${var.name}-network"
  delete_default_routes_on_create = false
  auto_create_subnetworks = false
  routing_mode = "REGIONAL"
}

  • subnets.tf:サブネットワークを定義する、今回は1VPC内に2つを用意しておきました。
# Subnets
resource "google_compute_subnetwork" "this" {
  count = 2
  name = "${var.name}-${local.type[count.index]}-subnetwork"
  ip_cidr_range = var.ip_cidr_range[count.index]
  region = var.region
  network = google_compute_network.this.id
  private_ip_google_access = true
}
  • nat.tf:インタネット接続ためのNATを定義する
# Nat router
resource "google_compute_router" "this" {
  name = "${var.name}-${local.type[1]}-router"
  region = google_compute_subnetwork.this[1].region
  network = google_compute_network.this.id
}

resource "google_compute_router_nat" "name" {
    name = "${var.name}-${local.type[1]}-router-nat"
    router = google_compute_router.this.name
    region = google_compute_router.this.region
    nat_ip_allocate_option = "AUTO_ONLY"
    source_subnetwork_ip_ranges_to_nat = "LIST_OF_SUBNETWORKS"
    subnetwork {
      name = "${var.name}-${local.type[1]}-subnetwork"
      source_ip_ranges_to_nat = ["ALL_IP_RANGES"]
    }
}
  • firewall.tf:ネットワークのアクセスポリシーを定義する
公開サブネットはインターネットからssh、httpリクエストできる、プライベートサブネットはネットワーク内のhttpのみ受け入れられるという設定にしておく
resource "google_compute_firewall" "allow-ssh" {
  name = "allow-ssh"
  network = google_compute_network.this.id
  allow {
    protocol = "tcp"
    ports = ["22"]
  }
  source_ranges = ["0.0.0.0/0"]
  target_tags = ["allow-ssh"]
}

resource "google_compute_firewall" "allow-http" {
  name = "allow-http-rule"
  network = google_compute_network.this.id
  allow {
    ports = ["80"]
    protocol = "tcp"
  }
  source_ranges = ["0.0.0.0/0"]
  target_tags = ["allow-http"]
}

resource "google_compute_firewall" "allow_http_internal" {
  name = "allow-http-rule-internal"
  network = google_compute_network.this.id
  allow {
    ports = ["80"]
    protocol = "tcp"
  }
  source_ranges = [var.ip_cidr_range[0]]
  target_tags = ["allow-http-internal"]
}
  • computeengine.tf:サブネットにGCEインスタンスを立ち上げる
resource "google_compute_instance" "public_instance" {
  name = "public-instance"
  machine_type = "n2-standard-2"
  zone = "${format("%s", "${var.region}-b")}"
  tags = ["allow-ssh", "allow-http"]
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
      labels = {
        webserver = "true"
      }
    }
  }
  metadata_startup_script = <<SCRIPT
    #!/bin/bash
    apt update
    apt -y install apache2
    cat <<EOF > /var/www/html/index.html
    <html><body><p>web server running on public instance</p></body</html>
    SCRIPT

    network_interface {
        subnetwork = "${google_compute_subnetwork.this[0].name}"
        access_config {
        }
    }
}

resource "google_compute_instance" "private_instance" {
  name = "private-instance"
  machine_type = "n2-standard-2"
  zone = "${format("%s", "${var.region}-b")}"
  tags = ["allow-http-internal"]
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }
  metadata_startup_script = <<SCRIPT
    #!/bin/bash
    apt update
    apt -y install apache2
    cat <<EOF > /var/www/html/index.html
    <html><body><p>web server running on private instance</p></body</html>
    SCRIPT

    network_interface {
        subnetwork = "${google_compute_subnetwork.this[1].name}"
    }
  
}

output "publicsubnet" {
  value = google_compute_instance.public_instance.id
}

4. インフラ構成検証

Terraformのクラウドリソース定義を適用できるために、下記のコマンドを実行してました。

1. Terraformを初期化
usr0104603@YINN1366 vpc_networking % terraform init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/google...
- Installing hashicorp/google v5.12.0...
- Installed hashicorp/google v5.12.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
2. Terraformでリソース変更確認
usr0104603@YINN1366 vpc_networking % terraform plan
Acquiring state lock. This may take a few moments...
data.google_compute_zones.this: Reading...
data.google_compute_zones.this: Read complete after 0s [id=projects/dongkx-project/regions/asia_notheast1]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # google_compute_firewall.allow-http will be created
  + resource "google_compute_firewall" "allow-http" {
      + creation_timestamp = (known after apply)
      + destination_ranges = (known after apply)
      + direction          = (known after apply)
      + enable_logging     = (known after apply)
      + id                 = (known after apply)
      + name               = "allow-http-rule"
      + network            = (known after apply)
      + priority           = 1000
      + project            = "dongkx-project"
      + self_link          = (known after apply)
      + source_ranges      = [
          + "0.0.0.0/0",
        ]
      + target_tags        = [
          + "allow-http",
        ]

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

  # google_compute_firewall.allow-ssh will be created
  + resource "google_compute_firewall" "allow-ssh" {
      + creation_timestamp = (known after apply)
      + destination_ranges = (known after apply)
      + direction          = (known after apply)
      + enable_logging     = (known after apply)
      + id                 = (known after apply)
      + name               = "allow-ssh"
      + network            = (known after apply)
      + priority           = 1000
      + project            = "dongkx-project"
      + self_link          = (known after apply)
      + source_ranges      = [
          + "0.0.0.0/0",
        ]
      + target_tags        = [
          + "allow-ssh",
        ]

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

  # google_compute_firewall.allow_http_internal will be created
  + resource "google_compute_firewall" "allow_http_internal" {
      + creation_timestamp = (known after apply)
      + destination_ranges = (known after apply)
      + direction          = (known after apply)
      + enable_logging     = (known after apply)
      + id                 = (known after apply)
      + name               = "allow-http-rule-internal"
      + network            = (known after apply)
      + priority           = 1000
      + project            = "dongkx-project"
      + self_link          = (known after apply)
      + source_ranges      = [
          + "10.10.10.0/24",
        ]
      + target_tags        = [
          + "allow-http-internal",
        ]

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

  # google_compute_instance.private_instance will be created
  + resource "google_compute_instance" "private_instance" {
      + can_ip_forward          = false
      + cpu_platform            = (known after apply)
      + current_status          = (known after apply)
      + deletion_protection     = false
      + effective_labels        = (known after apply)
      + guest_accelerator       = (known after apply)
      + id                      = (known after apply)
      + instance_id             = (known after apply)
      + label_fingerprint       = (known after apply)
      + machine_type            = "n1-standard-1"
      + metadata_fingerprint    = (known after apply)
      + metadata_startup_script = <<-EOT
            #!/bin/bash
                apt update
                apt -y install apache2
                cat <<EOF > /var/www/html/index.html
                <html><body><p>web server running on private instance</p></body</html>
        EOT
      + min_cpu_platform        = (known after apply)
      + name                    = "private-instance"
      + project                 = "dongkx-project"
      + self_link               = (known after apply)
      + tags                    = [
          + "allow-http-internal",
        ]
      + tags_fingerprint        = (known after apply)
      + terraform_labels        = (known after apply)
      + zone                    = "asia_notheast1-b"

      + boot_disk {
          + auto_delete                = true
          + device_name                = (known after apply)
          + disk_encryption_key_sha256 = (known after apply)
          + kms_key_self_link          = (known after apply)
          + mode                       = "READ_WRITE"
          + source                     = (known after apply)

          + initialize_params {
              + image                  = "debian-cloud/debian-11"
              + labels                 = (known after apply)
              + provisioned_iops       = (known after apply)
              + provisioned_throughput = (known after apply)
              + size                   = (known after apply)
              + type                   = (known after apply)
            }
        }

      + network_interface {
          + internal_ipv6_prefix_length = (known after apply)
          + ipv6_access_type            = (known after apply)
          + ipv6_address                = (known after apply)
          + name                        = (known after apply)
          + network                     = (known after apply)
          + network_ip                  = (known after apply)
          + stack_type                  = (known after apply)
          + subnetwork                  = "tflearn-private-subnetwork"
          + subnetwork_project          = (known after apply)
        }
    }

  # google_compute_instance.public_instance will be created
  + resource "google_compute_instance" "public_instance" {
      + can_ip_forward          = false
      + cpu_platform            = (known after apply)
      + current_status          = (known after apply)
      + deletion_protection     = false
      + effective_labels        = (known after apply)
      + guest_accelerator       = (known after apply)
      + id                      = (known after apply)
      + instance_id             = (known after apply)
      + label_fingerprint       = (known after apply)
      + machine_type            = "n1-standard-1"
      + metadata_fingerprint    = (known after apply)
      + metadata_startup_script = <<-EOT
            #!/bin/bash
                apt update
                apt -y install apache2
                cat <<EOF > /var/www/html/index.html
                <html><body><p>web server running on public instance</p></body</html>
        EOT
      + min_cpu_platform        = (known after apply)
      + name                    = "public-instance"
      + project                 = "dongkx-project"
      + self_link               = (known after apply)
      + tags                    = [
          + "allow-http",
          + "allow-ssh",
        ]
      + tags_fingerprint        = (known after apply)
      + terraform_labels        = (known after apply)
      + zone                    = "asia_notheast1-b"

      + boot_disk {
          + auto_delete                = true
          + device_name                = (known after apply)
          + disk_encryption_key_sha256 = (known after apply)
          + kms_key_self_link          = (known after apply)
          + mode                       = "READ_WRITE"
          + source                     = (known after apply)

          + initialize_params {
              + image                  = "debian-cloud/debian-11"
              + labels                 = {
                  + "webserver" = "true"
                }
              + provisioned_iops       = (known after apply)
              + provisioned_throughput = (known after apply)
              + size                   = (known after apply)
              + type                   = (known after apply)
            }
        }

      + network_interface {
          + internal_ipv6_prefix_length = (known after apply)
          + ipv6_access_type            = (known after apply)
          + ipv6_address                = (known after apply)
          + name                        = (known after apply)
          + network                     = (known after apply)
          + network_ip                  = (known after apply)
          + stack_type                  = (known after apply)
          + subnetwork                  = "tflearn-public-subnetwork"
          + subnetwork_project          = (known after apply)

          + access_config {
              + nat_ip       = (known after apply)
              + network_tier = (known after apply)
            }
        }
    }

  # google_compute_network.this will be created
  + resource "google_compute_network" "this" {
      + auto_create_subnetworks                   = false
      + delete_default_routes_on_create           = false
      + gateway_ipv4                              = (known after apply)
      + id                                        = (known after apply)
      + internal_ipv6_range                       = (known after apply)
      + mtu                                       = (known after apply)
      + name                                      = "tflearn-network"
      + network_firewall_policy_enforcement_order = "AFTER_CLASSIC_FIREWALL"
      + numeric_id                                = (known after apply)
      + project                                   = "dongkx-project"
      + routing_mode                              = "REGIONAL"
      + self_link                                 = (known after apply)
    }

  # google_compute_router.this will be created
  + resource "google_compute_router" "this" {
      + creation_timestamp = (known after apply)
      + id                 = (known after apply)
      + name               = "tflearn-private-router"
      + network            = (known after apply)
      + project            = "dongkx-project"
      + region             = "asia_notheast1"
      + self_link          = (known after apply)
    }

  # google_compute_router_nat.name will be created
  + resource "google_compute_router_nat" "name" {
      + enable_dynamic_port_allocation      = (known after apply)
      + enable_endpoint_independent_mapping = (known after apply)
      + icmp_idle_timeout_sec               = 30
      + id                                  = (known after apply)
      + name                                = "tflearn-private-router-nat"
      + nat_ip_allocate_option              = "AUTO_ONLY"
      + project                             = "dongkx-project"
      + region                              = "asia_notheast1"
      + router                              = "tflearn-private-router"
      + source_subnetwork_ip_ranges_to_nat  = "LIST_OF_SUBNETWORKS"
      + tcp_established_idle_timeout_sec    = 1200
      + tcp_time_wait_timeout_sec           = 120
      + tcp_transitory_idle_timeout_sec     = 30
      + udp_idle_timeout_sec                = 30

      + subnetwork {
          + name                     = "tflearn-private-subnetwork"
          + secondary_ip_range_names = []
          + source_ip_ranges_to_nat  = [
              + "ALL_IP_RANGES",
            ]
        }
    }

  # google_compute_subnetwork.this[0] will be created
  + resource "google_compute_subnetwork" "this" {
      + creation_timestamp         = (known after apply)
      + external_ipv6_prefix       = (known after apply)
      + fingerprint                = (known after apply)
      + gateway_address            = (known after apply)
      + id                         = (known after apply)
      + internal_ipv6_prefix       = (known after apply)
      + ip_cidr_range              = "10.10.10.0/24"
      + ipv6_cidr_range            = (known after apply)
      + name                       = "tflearn-public-subnetwork"
      + network                    = (known after apply)
      + private_ip_google_access   = true
      + private_ipv6_google_access = (known after apply)
      + project                    = "dongkx-project"
      + purpose                    = (known after apply)
      + region                     = "asia_notheast1"
      + secondary_ip_range         = (known after apply)
      + self_link                  = (known after apply)
      + stack_type                 = (known after apply)
    }

  # google_compute_subnetwork.this[1] will be created
  + resource "google_compute_subnetwork" "this" {
      + creation_timestamp         = (known after apply)
      + external_ipv6_prefix       = (known after apply)
      + fingerprint                = (known after apply)
      + gateway_address            = (known after apply)
      + id                         = (known after apply)
      + internal_ipv6_prefix       = (known after apply)
      + ip_cidr_range              = "10.10.20.0/24"
      + ipv6_cidr_range            = (known after apply)
      + name                       = "tflearn-private-subnetwork"
      + network                    = (known after apply)
      + private_ip_google_access   = true
      + private_ipv6_google_access = (known after apply)
      + project                    = "dongkx-project"
      + purpose                    = (known after apply)
      + region                     = "asia_notheast1"
      + secondary_ip_range         = (known after apply)
      + self_link                  = (known after apply)
      + stack_type                 = (known after apply)
    }

Plan: 10 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + az           = []
  + publicsubnet = (known after apply)

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run
"terraform apply" now.
3. Terraformでリソース変更を適用
usr0104603@YINN1366 vpc_networking % terraform apply -auto-approve
...
...
Outputs:

az = tolist([
  "asia-northeast1-a",
  "asia-northeast1-b",
  "asia-northeast1-c",
])
publicsubnet = "projects/dongkx-project/zones/asia-northeast1-b/instances/public-instance"
「apply」コマンド実行完了しましたら、クラウドに定義したリソースを反映します。

VPCネットワーク、SubNet、Firewallポリシーのそれぞれが作成されることを確認しました。
VPCネットワーク:
VPCSubnet:
Firewallポリシー:

GCE2台起動は確認しました。

private-instance:外部IPがないで内部IPのみ付いてます。インタネットから接続できない

public-instance:外部IP、内部IPの両方で付いてます。


下記のコマンドでサーバ2台の接続も確認しました。
usr0104603@YINN1366 vpc_networking % curl 35.187.208.73
    <html><body><p>web server running on public instance</p></body</html>
usr0104603@YINN1366 vpc_networking % ssh -i gcp [email protected]
Linux public-instance 5.10.0-27-cloud-amd64 #1 SMP Debian 5.10.205-2 (2023-12-31) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Fri Jan 19 00:37:15 2024 from 126.126.160.206
dong-kieuxuan@public-instance:~$ curl 10.10.20.2
    <html><body><p>web server running on private instance</p></body</html>

5. おまけ

Terraformがリソースの現在の状態をtfstateファイルで管理しています。

デフォルトではローカルにtfstateファイルが生成されますが、チームで作業すると、人々のローカル修正でマージすると、衝突になりやすいですので、

多くの場合で、リモートに保存しています。

今回は、Gcloudのバケットに保存すると定義しておきました。

6. 最後に

今回のブログで、シンプルなWebサイト構成を考えてみて、Terraformコードを作成し、デプロイ後想定通りの動作が確認できました。
今後は、このコードを活用して、もっと実用的な構成なども検証してみようと思いました。

7. 伝言

次世代システム研究室では、最新のテクノロジーを調査・検証しながら、様々なインターネットアプリケーションの開発を行うアーキテクトを募集しています。募集職種一覧からご応募をお待ちしています。

 

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事