2024.01.19
Google Cloud/Terraform シンプルなWebサイトInfraを作ってみた
こんにちは。グループ研究開発本部 次世代システム研究室のK.X.Dです。
今回は、有名なIaC実現のTerraformツールを勉強ために、調べならがGcloud上のシンプルなインフラ構成を作ってみようと思います。
1. やりたいこと
以下のような構成で、Gcloudで構築してみようと思います。
- 検証用のVPCを構成し、サブネットを2つ作成する
- サブネット内にGCEインスタンスを配置する
- プラベートGCEはインタネットからアクセスできない
- パップリック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で配信中です。ぜひフォローください。
Follow @GMO_RD