LoginSignup
2
4

More than 1 year has passed since last update.

【GCP】統合版MinecraftサーバーをGCEで建てる【統合版Minecraft】

Last updated at Posted at 2022-04-05

概要

GCPで統合版Minecraftサーバー建てて、Discordから起動/停止する。

目的

  • Realmsにお金を払うのがなんか嫌だったので、自分で建ててみる
  • 自宅PCで管理するのは嫌なので、GCPで建てる
  • Always free のVMではスペック厳しそうなので、自動もしくは簡単にサーバー起動/停止する仕組みが必要
  • せっかくなので、Qiitaを始めてみる(自分用メモ)

事前調査

  • 構築方法
    昔はGCP公式ドキュメントにMinecraftサーバの建て方があったようだが、今は無かった(リンク切れ、ドキュメント検索しても出てこない)……まあ情報かき集めてどうにかしよう。

  • サーバー起動/停止について
    Discordと連携して出来そう。
    後ほど(構築時)、下記を参考にさせていただきます、感謝。
     
    DiscordからGCP上のマイクラサーバーを制御してインスタンス料金をケチる話
     
    Discord連携用にゲームサーバーとは別に1台建てた方が良さそう。
    既に、GCPで1台Always freeのサーバーを1台建てているので、これを流用する。
    ちなみに、Among UsのAutoMuteUsボットを常駐させているだけなので、共存しても問題ないだろう。

  • Switch版Minecraftで、Realms以外の外部サーバーへの接続
    出来るみたい。
    本記事の本筋ではないのと、公式の設定方法ではないためここでは割愛。

  • IaC 化について
    いざという時にサクッと削除や再作成が出来るよう、また自身のスキルアップのためにも出来る限り IaC 化していく。
    下記記事がやりたいこととほぼ丸かぶりだったので、ありがたく参考にさせていただきました、感謝。
     
    Terraform で GCP の組織設定・共有 VPC・VPN 作成

1. 構築

1.1. 事前準備

前述の通り、元々GCP環境は持っていた。
しかし、複数環境や複数サービス(プロジェクト)を管理する想定ではなかったので、この機会に整えることに。

管理方法を検討したところ、組織(Organization)作成し、ホストプロジェクト・サービスプロジェクトという単位で管理するのが良さそうだったので、そちらを採用することに。

まず事前に下記を手動にて実施。
ドメインは、以前使っていたものが使えなくなっていたので新規に取得した。

  1. ドメイン取得
  2. Cloud Identity 設定
  3. 組織の作成
  4. 既存プロジェクト、請求先アカウントを組織へ移動

ここまでは手動で設定。
以降は、出来る限り terraform による IaC 化を進める。

1.2. インフラ構築

ざっくり2段階。

  1. terraform 用サービスアカウント、State ファイル用GCSの作成、その他権限周り設定など terraform 実行環境用プロジェクトを作成
  2. 組織権限、ホストプロジェクト、共有VPCなどインフラ部分の構築

1.2.1. terraform 実行環境構築

まずは、terraform の実行環境を整えていく。
ローカルの terraform 実行環境とは別の話で、下記を作成・設定する話である。

  1. terraform 用プロジェクト
  2. terraform 用サービスアカウント
  3. State ファイル用GCS
  4. その他権限周り設定

mainコードは下記。前提条件・事前準備もコメント記載しているので、若干長いので折り畳んでおく。
(以降のコードについても、長い物については適宜折り畳む、部分引用などの場合は「(中略)」などを使用する)

main.tf
main.tf
## 内容
# Terraform 用のプロジェクトを作成する
# 参照 https://cloud.google.com/community/tutorials/managing-gcp-projects-with-terraform
## 前提
# 組織を作成済み
# 課金アカウント作成済み
# 管理ユーザグループを admin.google で作成済み
## 組織管理者が課金アカウントへ権限付与できるようにする
# gcloud config set account [課金アカウントのアドミンアカウント]
# gcloud beta billing accounts list
# gcloud beta billing accounts add-iam-policy-binding [var.gcp_common.billing_account] --member=user:[org admin user account] --role roles/billing.admin
## 組織管理者のアカウントで gcloud コマンドを利用できるようにする
# gcloud auth login [org admin user account]
# gcloud config set account [org admin user account]
# gcloud auth application-default login
## 組織管理者が組織ポリシーを編集できるようにする
# gcloud organizations list
# gcloud organizations add-iam-policy-binding [var.gcp_common.org_id] --member=user:[org admin user account] --role=roles/orgpolicy.policyAdmin
## workspace を "dev", "prd" などにする
# terraform workspace new dev

provider "google" {}

# Terraform Project 作成
resource "google_project" "terraform" {
  name                = join("-", [var.gcp_common.org_name, var.terraform_pj.identity_name, terraform.workspace])
  project_id          = join("-", [var.gcp_common.org_name, var.terraform_pj.identity_name, "03", terraform.workspace])
  org_id              = var.gcp_common.org_id
  billing_account     = var.gcp_common.billing_account
  auto_create_network = false
}

# Sevice API 有効化 (google_project と同じ terraform で実施が必須)
resource "google_project_service" "terraform" {
  project                    = google_project.terraform.id
  disable_dependent_services = true

  for_each = toset([
    "cloudresourcemanager.googleapis.com",
    "serviceusage.googleapis.com",
    "cloudidentity.googleapis.com",
    "cloudbilling.googleapis.com",
    "iam.googleapis.com",
    "compute.googleapis.com",
    "container.googleapis.com",
    "accesscontextmanager.googleapis.com", # VPC Service Controls に必要
    "essentialcontacts.googleapis.com",    # essentialcontacts
  ])
  service = each.value

  depends_on = [
    google_project.terraform,
  ]
}

# Terraform サービスアカウントの作成
resource "google_service_account" "terraform" {
  account_id   = "terraform"
  display_name = "Terraform IaC Account"
  project      = google_project.terraform.project_id

  depends_on = [
    google_project.terraform,
  ]
}

# Terraform へホストプロジェクトの閲覧ロールを付与
resource "google_project_iam_binding" "storage_serviceusage" {
  project = google_project.terraform.project_id
  for_each = toset([
    "roles/storage.admin",
    "roles/serviceusage.serviceUsageAdmin",
  ])

  role = each.value

  members = [
    join(":", ["serviceAccount", google_service_account.terraform.email]),
    join(":", ["group", var.admin_user_group.email]),
  ]

  depends_on = [
    google_service_account.terraform,
  ]
}

# Terraform へホストプロジェクトの閲覧ロールを付与
resource "google_project_iam_binding" "viewer" {
  project = google_project.terraform.project_id
  for_each = toset([
    "roles/viewer",
  ])

  role = each.value

  members = [
    join(":", ["serviceAccount", google_service_account.terraform.email]),
  ]

  depends_on = [
    google_service_account.terraform,
  ]
}

# Terraform へホストプロジェクトの編集ロールを付与
resource "google_project_iam_binding" "editor" {
  project = google_project.terraform.project_id
  for_each = toset([
    "roles/editor",
  ])

  role = each.value

  members = [
    join(":", ["group", var.admin_user_group.email]),
  ]

  depends_on = [
    google_service_account.terraform,
  ]
}

# Terraform へ組織内のプロジェクト作成権限を付与
resource "google_organization_iam_binding" "terraform" {
  org_id = google_project.terraform.org_id
  for_each = toset([
    "roles/resourcemanager.projectCreator",
    #    "roles/billing.projectManager",
    "roles/billing.user",
    "roles/compute.xpnAdmin",
    "roles/resourcemanager.projectIamAdmin",
    "roles/resourcemanager.organizationAdmin",
    "roles/orgpolicy.policyAdmin",
    "roles/resourcemanager.folderAdmin",
    "roles/accesscontextmanager.policyAdmin", # VPC SC 時に必要
  ])
  role = each.value

  members = [
    join(":", ["serviceAccount", google_service_account.terraform.email]),
    join(":", ["group", var.admin_user_group.email])
  ]

  depends_on = [
    google_project.terraform,
    google_service_account.terraform,
  ]
}

# Terraform へ課金アカウントの利用権限を付与
resource "google_billing_account_iam_binding" "user" {
  billing_account_id = google_project.terraform.billing_account
  role               = "roles/billing.user"
  members = [
    join(":", ["serviceAccount", google_service_account.terraform.email]),
    join(":", ["group", var.admin_user_group.email])
  ]

  depends_on = [
    google_project.terraform,
  ]
}

resource "google_organization_iam_binding" "essentialcontacts" {
  org_id = google_project.terraform.org_id
  for_each = toset([
    "roles/essentialcontacts.admin", # essentialcontacts
  ])
  role = each.value

  members = [
    join(":", ["serviceAccount", google_service_account.terraform.email]),
    join(":", ["group", var.admin_user_group.email])
  ]
}

# Terraform のステートファイル置き場の作成
resource "google_storage_bucket" "terraform" {
  name          = join("-", [google_project.terraform.project_id, "terraform-backet"])
  project       = google_project.terraform.project_id
  location      = "US"
  force_destroy = true
  storage_class = "STANDARD"

  versioning {
    enabled = true
  }

  lifecycle_rule {
    condition {
      num_newer_versions = 5
    }
    action {
      type = "Delete"
    }
  }

  depends_on = [
    google_project.terraform,
  ]
}
variables.tf
variables.tf
variable "gcp_common" {
  default = {
    org_name        = "hoge"
    org_id          = "hoge"
    billing_account = "hoge"
  }
}

variable "admin_user_group" {
  default = {
    email = "hoge"
  }
}

variable "terraform_pj" {
  default = {
    identity_name = "hoge"
  }
}

いくつかブロック毎にポイントを記載。

1.2.1.1. 前提条件

terraform用サービスアカウントをterraformで作成するためにはterraform実行権限(=各種リソースへの権限)が必要である。
……卵が先か鶏が先か、みたいな話になってしまうので、とりあえず今あるアカウント(組織管理者のアカウントが適切であろう)に、それらの権限を付与する。
これで組織管理者アカウントでterraformによるリソース作成が可能になったので、terraform用サービスアカウントを作成し、以降のリソース作成についてはterraform用サービスアカウントで認証し作成していくこととなる。

main.tf
## 前提
  (中略)
## 組織管理者のアカウントで gcloud コマンドを利用できるようにする
# gcloud auth login [org admin user account]
# gcloud config set account [org admin user account]
# gcloud auth application-default login
## 組織管理者が組織ポリシーを編集できるようにする
# gcloud organizations list
# gcloud organizations add-iam-policy-binding [var.gcp_common.org_id] --member=user:[org admin user account] --role=roles/orgpolicy.policyAdmin

1.2.1.2. 環境別デプロイ(apply)

個人環境であっても本番環境・開発環境(試験環境など言い方はいろいろあるが)は分けておきたい。
terraform の workspace 機能を使うと、同一のコードで異なる環境へのデプロイ(apply)が可能になるので、この機能を使用する。
環境毎にフォルダ・ファイルを分けてコード管理する方法もあるが、修正点の同期が面倒なので、あまり好きではない。

main.tf
## workspace を "dev", "prd" などにする
# terraform workspace new dev

1.2.1.3. Project設定

この Project はあくまで terraform 管理用なので、前述のGCSバケットや権限設定以外のリソースは不要である。
そのため「auto_create_network = false」とし、余計なリソースが作成されないようにしている。

main.tf
# Terraform Project 作成
resource "google_project" "terraform" {
  (中略)
  auto_create_network = false
}

1.2.1.4. サービスアカウント設定

terraform を実行するにあたっての権限をまとめたアカウントとなる。
個人用途であればガッツリadmin権限付与でも問題ないとは思うが、一応今回は細かく権限を付与した。

main.tf
resource "google_service_account" "terraform" {
  (中略)
}
resource "google_project_iam_binding" "storage_serviceusage" {
  (中略)
}

1.2.1.5. GCS設定

Stateファイルは通常同じ名前で作成されるので、上書きされてしまい世代管理が出来ない。
(正確に言うと、apply 実行時にバックアップファイルが作成されるので2世代分は残っていることになるが、さすがに心許ない)
Stateファイル名にタイムスタンプを付与するなどして動的に作成する方法もあるが、今回はシンプルにGCSのバージョニング&ライフサイクル機能で対応することとした。
下記コードにて、バージョニング(世代管理)と、ライフサイクル(5世代保存、それ以降は古いものから自動削除)を設定。

main.tf
resource "google_storage_bucket" "terraform" {
  (中略)
  versioning {
    enabled = true
  }

  lifecycle_rule {
    condition {
      num_newer_versions = 5
    }
    action {
      type = "Delete"
    }
  }
  (中略)
}

1.2.1.6. Output 定義

後続の作業を楽にするために、下記ような Output を定義。

output.tf
output "A001_Terraform_Service_Account" {
  value       = google_service_account.terraform.email
  description = "Terraform Account"
}

output "A002_GCP_BACKEND" {
  value = join("\n", [
    "cat > backend.tf << EOF",
    "terraform {",
    "  backend \"gcs\" {",
    "    bucket = \"${google_storage_bucket.terraform.name}\"",
    "    prefix = \"terraform/state\"",
    "  }",
    "}",
    "EOF",
    "mv backend.tf ../",
  ])
}

output "A003_Next_Commands" {
  value       = join("", ["gcloud.cmd iam service-accounts keys create terraform_serviceacoount_credential.json --iam-account ", google_service_account.terraform.email, ";cp terraform_serviceacoount_credential.json ../;cd ../"])
  description = "Next"
}

apply後、下記のような出力を得られる。

Outputs:

A001_Terraform_Service_Account = "terraform@hoge.iam.gserviceaccount.com"
A002_GCP_BACKEND = <<-EOT
    cat > backend.tf << EOF
    terraform {
      backend "gcs" {
        bucket = "hoge"
        prefix = "terraform/state"
      }
    }
    EOF
    mv backend.tf ../
EOT
A003_Next_Commands = "gcloud.cmd iam service-accounts keys create terraform_serviceacoount_credential.json --iam-account terraform@hoge.iam.gserviceaccount.com;cp terraform_serviceacoount_credential.json ../;cd ../"

「A001_Terraform_Service_Account」は先程作成した terraform 用サービスアカウントの email アドレスが出力される。
「A002_GCP_BACKEND」は「backend.tf」というファイルが出力される。
「A003_Next_Commands」は表示されたコマンドを実行することで、terraform 用サービスアカウントのクレデンシャルファイル(json)が出力される。
それぞれ、後続の terraform コードを実行する際に必要となる。

1.2.2. インフラ部構築

前述した通り、ここで必要なリソースは下記。

  1. 組織権限
  2. ホストプロジェクト
  3. 共有VPC

先程の terraform 用プロジェクトとは別管理にしているので、フォルダを変更して各コマンドを実行する。
mainコードは下記。

main.tf
main.tf
## 前提
# prestage実施済
## plan/apply前に実施
# terraform init
# terraform workspace new dev

provider "google" {
  credentials = file("../terraform_serviceacoount_credential.json")
  #  user_project_override = true
}

# provider "google-beta" {
#   credentials = file("../terraform_serviceacoount_credential.json")
# }
variables.tf
variables.tf
# org_id (下記で出力される "ID" を "org_id" の値にする)
## gcloud organizations list
# billing_account (下記で出力される "ACCOUNT_ID" を "billing_account" の値にする)
## gcloud beta billing accounts list
# gcp-terraform-admin@[xxx.xxx]
## 組織で使用するドメイン(xxx.xxx)の Cloud Identity で事前に Terraform 管理ユーザグループのアカウントグループを作成しておく(同じ権限にして切り分けよう)
# org_name
## 組織の識別子、プロジェクトの命名に必要なだけで何でも良い
# [xxx.xxx:ドメイン名], domain
## 環境のドメイン名(例:example.com)を入力する

variable "gcp_common" {
  default = {
    org_name        = "hoge"
    org_id          = "hoge"
    billing_account = "hoge"
    region          = "us-west1"
    zone            = "us-west1-b"
  }

  validation {
    condition     = (length(regexall(var.gcp_common.region, var.gcp_common.zone)) > 0)
    error_message = "Zone must be in region."
  }
}

variable "terraform-service-accounts" {
  default = "terraform@hoge.iam.gserviceaccount.com"
}

variable "organization_admin_group" {
  default = {
    email = "gcp-organization-admins@hoge"
  }
}

variable "network_admin_group" {
  default = {
    email = "gcp-network-admins@hoge"
  }
}

variable "host_project_admin_group" {
  default = {
    email = "gcp-network-admins@hoge"
  }
}

variable "service1_project_admin_group" {
  default = {
    email = "gcp-service1-admin@hoge"
  }
}

variable "host_pj" {
  default = {
    # 命名規則: A-Z, a-z, 0-9 のみ
    service_name = "host"
  }
}

variable "service1_pj" {
  default = {
    # 命名規則: A-Z, a-z, 0-9 のみ
    service_name    = "service1"
    gke_master_ipv4 = "172.18.2.0/28"
  }
}

variable "domain" {
  default = "hiko.games"
}

variable "essentinal_contacts_domains" {
  default = ["@hoge"]
}

変数については作成するリソースの物だけではなく、手動作成したリソースや、ホストプロジェクト側のリソースを呼び出すための定義をしている。

ここからは各サービス毎にファイルを分割しているため、mainはプロバイダー(クレデンシャル)指定のみとなっている。
この辺は好みで1ファイルにしても良いと思う。
 

1.2.2.1. 組織周りの設定。

organization.tf
organization.tf
## 組織ポリシー
### デフォルトネットワーク作成の無効化
resource "google_organization_policy" "skipDefaultNetworkCreation" {
  org_id     = var.gcp_common.org_id
  constraint = "compute.skipDefaultNetworkCreation"

  boolean_policy {
    enforced = true
  }
}

#### VM インスタンス用に許可される外部 IP を定義する
#resource "google_organization_policy" "vmExternalIpAccess" {
#  org_id     = var.gcp_common.org_id
#  constraint = "compute.vmExternalIpAccess"
#
#  list_policy {
#    allow {
#      values = [google_compute_instance.service01_gce1.id]
#    }
#  }
#}

### 共有 VPC ホスト プロジェクトの制限
#### service01 Floder 配下は共有 VPC を制限
resource "google_folder_organization_policy" "service01_restrictSharedVpcHostProjects" {
  folder     = google_folder.organization_service_folder.id
  constraint = "compute.restrictSharedVpcHostProjects"

  list_policy {
    allow {
      values = [google_project.host_project.id]
    }
  }
}

### Dedicated Interconnect の使用の制限
resource "google_organization_policy" "restrictDedicatedInterconnectUsage" {
  org_id     = var.gcp_common.org_id
  constraint = "compute.restrictDedicatedInterconnectUsage"

  list_policy {
    deny {
      values = ["under:${google_folder.organization_service_folder.id}"]
    }
  }
}

### Partner Interconnect の使用の制限
resource "google_organization_policy" "restrictPartnerInterconnectUsage" {
  org_id     = var.gcp_common.org_id
  constraint = "compute.restrictPartnerInterconnectUsage"

  list_policy {
    deny {
      values = ["under:${google_folder.organization_service_folder.id}"]
    }
  }
}

### VPC ピアリング使用量の制限
resource "google_organization_policy" "restrictVpcPeering" {
  org_id     = var.gcp_common.org_id
  constraint = "compute.restrictVpcPeering"

  list_policy {
    deny {
      values = ["under:${google_folder.organization_service_folder.id}"]
    }
  }
}

### ロードバランサの種類に基づいてロードバランサの作成を制限する
resource "google_organization_policy" "restrictLoadBalancerCreationForTypes" {
  org_id     = var.gcp_common.org_id
  constraint = "compute.restrictLoadBalancerCreationForTypes"

  list_policy {
    allow {
      values = ["is:INTERNAL_TCP_UDP", "is:INTERNAL_HTTP_HTTPS"]
    }
  }
}

### インターネット ネットワーク エンドポイント グループの無効化
resource "google_organization_policy" "disableInternetNetworkEndpointGroup" {
  org_id     = var.gcp_common.org_id
  constraint = "compute.disableInternetNetworkEndpointGroup"

  boolean_policy {
    enforced = true
  }
}

### IP アドレスの種類に基づいてプロトコル転送を制限する
resource "google_organization_policy" "restrictProtocolForwardingCreationForTypes" {
  org_id     = var.gcp_common.org_id
  constraint = "compute.restrictProtocolForwardingCreationForTypes"

  list_policy {
    allow {
      values = ["is:INTERNAL"]
    }
  }
}

### Cloud NAT の使用制限
resource "google_organization_policy" "restrictCloudNATUsage" {
  org_id     = var.gcp_common.org_id
  constraint = "compute.restrictCloudNATUsage"

  list_policy {
    deny {
      values = ["under:${google_folder.organization_service_folder.id}"]
    }
  }
}

#### リソースロケーションの制限
#resource "google_organization_policy" "gcp_resourceLocations" {
#  org_id     = var.gcp_common.org_id
#  constraint = "gcp.resourceLocations"
#
#  list_policy {
#    allow {
#      values = [
#        "in:asia-northeast1-locations",
#        "in:asia-northeast2-locations",
#        "is:ASIA1",
#        "in:us-west1-locations",
#        "in:us-west2-locations",
#        "is:US",
#      ]
#    }
#  }
#}

#### ドメインで制限された共有
#resource "google_organization_policy" "iam_allowedPolicyMemberDomains" {
#  org_id     = var.gcp_common.org_id
#  constraint = "iam.allowedPolicyMemberDomains"
#
#  list_policy {
#    allow {
#      values = [var.google_admin_customer_id]
#    }
#  }
#}

### ドメインで制限された共有
resource "google_organization_policy" "sql_restrictPublicIp" {
  org_id     = var.gcp_common.org_id
  constraint = "sql.restrictPublicIp"

  boolean_policy {
    enforced = true
  }
}

### 公開アクセスの防止を適用する
resource "google_organization_policy" "storage_publicAccessPrevention" {
  org_id     = var.gcp_common.org_id
  constraint = "storage.publicAccessPrevention"

  boolean_policy {
    enforced = true
  }
}

### コンシューマ向け Private Service Connect の組織全体での Google APIS 以外の無効化
resource "google_organization_policy" "disablePrivateServiceConnectCreationForConsumers" {
  org_id     = var.gcp_common.org_id
  constraint = "compute.disablePrivateServiceConnectCreationForConsumers"

  list_policy {
    deny {
      values = ["is:SERVICE_PRODUCERS"]
    }
  }
}

### コンシューマ向け Private Service Connect のサービスプロジェクトでの無効化
resource "google_folder_organization_policy" "service01_disablePrivateServiceConnectCreationForConsumers" {
  folder     = google_folder.organization_service_folder.id
  constraint = "compute.disablePrivateServiceConnectCreationForConsumers"

  list_policy {
    deny {
      values = ["is:GOOGLE_APIS", "is:SERVICE_PRODUCERS"]
    }
  }
}

### [重要な連絡先] に追加できるメールアドレスのドメインのセットを定義
resource "google_organization_policy" "essentialcontacts_allowedContactDomains" {
  org_id     = var.gcp_common.org_id
  constraint = "essentialcontacts.allowedContactDomains"

  list_policy {
    allow {
      values = var.essentinal_contacts_domains
    }
  }
}

### 許可される Google Cloud API とサービスを制限する [現状 deny と特定 API しかうまくいかない]
resource "google_folder_organization_policy" "service_serviceuser_services" {
  folder     = google_folder.organization_service_folder.id
  constraint = "serviceuser.services"

  list_policy {
    suggested_value = "compute.googleapis.com"

    deny {
      values = [
        "doubleclicksearch.googleapis.com",
        "replicapool.googleapis.com",
        "replicapoolupdater.googleapis.com",
        "resourceviews.googleapis.com",
      ]
    }
  }
}

### デフォルトのサービス アカウントに対する IAM ロールの自動付与の無効化
resource "google_organization_policy" "automaticIamGrantsForDefaultServiceAccounts" {
  org_id     = var.gcp_common.org_id
  constraint = "iam.automaticIamGrantsForDefaultServiceAccounts"

  boolean_policy {
    enforced = true
  }
}

# ドメインユーザに組織・フォルダ構成の閲覧権限付与
resource "google_organization_iam_binding" "organization_domain_viewer" {
  org_id = var.gcp_common.org_id
  for_each = toset([
    "roles/resourcemanager.organizationViewer",
    "roles/resourcemanager.folderViewer",
  ])
  role = each.value

  members = [
    join(":", ["domain", var.domain]),
  ]
}


# 組織管理者への管理権限付与
resource "google_organization_iam_binding" "organization_org_admin" {
  org_id = var.gcp_common.org_id
  for_each = toset([
    "roles/resourcemanager.organizationAdmin",
    "roles/billing.admin",
    "roles/resourcemanager.folderAdmin",
    "roles/resourcemanager.projectCreator",
    "roles/iam.organizationRoleAdmin",
    "roles/orgpolicy.policyAdmin",            # 組織ポリシー管理者
    "roles/accesscontextmanager.policyAdmin", # VPC SC 時に必要
  ])
  role = each.value

  members = [
    join(":", ["group", var.organization_admin_group.email]),
    join(":", ["serviceAccount", var.terraform-service-accounts]),
  ]

  # 削除すると管理者が削除されてしまうので偶発的な破壊を防ぐ
  # 全体を削除する場合は、管理系を手動で逃してあげる必要がある
  lifecycle {
    prevent_destroy = false
    # ignore_changes = all
  }
}

# ネットワーク管理者への共有VPC等の権限付与
resource "google_organization_iam_binding" "organization_network_admin" {
  org_id = var.gcp_common.org_id
  for_each = toset([
    "roles/compute.networkAdmin",
    "roles/compute.xpnAdmin",
    "roles/compute.securityAdmin",
  ])
  role = each.value

  members = [
    join(":", ["group", var.network_admin_group.email]),
    join(":", ["serviceAccount", var.terraform-service-accounts]),
  ]
}

# インフラ向けフォルダ
resource "google_folder" "organization_infrastructure_folder" {
  display_name = "infrastructure"
  parent       = join("/", ["organizations", var.gcp_common.org_id])

  depends_on = [
    google_organization_policy.skipDefaultNetworkCreation,
  ]
}

# サービス向けフォルダ
resource "google_folder" "organization_service_folder" {
  display_name = "service"
  parent       = join("/", ["organizations", var.gcp_common.org_id])

  depends_on = [
    google_organization_policy.skipDefaultNetworkCreation,
  ]
}

ぶっちゃけ先人の丸パクリで細かいところまでは把握しきれていない…。
後続のコードで各プロジェクトを作成していくにあたって必要となる権限や、プロジェクトが格納されるフォルダなどを作成している(たぶん)
なんとなく不要そうなところはコメントアウトしているが、実は必要かもしれないので残している。

1.2.2.2. ホストプロジェクトの設定。

host.tf
host.tf
# ホストプロジェクト用フォルダ
resource "google_folder" "host_host_folder" {
  display_name = "host"
  parent       = google_folder.organization_infrastructure_folder.name

  depends_on = [
    google_folder.organization_infrastructure_folder,
  ]
}

# ホストプロジェクト作成
resource "google_project" "host_project" {
  name            = join("-", [var.gcp_common.org_name, var.host_pj.service_name, terraform.workspace])
  project_id      = join("-", [var.gcp_common.org_name, var.host_pj.service_name, terraform.workspace])
  billing_account = var.gcp_common.billing_account
  folder_id       = google_folder.host_host_folder.name

  depends_on = [
    google_folder.host_host_folder,
  ]
}


# ホストプロジェクトの API 管理
resource "google_project_service" "host_api_enable" {
  project                    = google_project.host_project.id
  disable_dependent_services = true

  for_each = toset([
    "cloudresourcemanager.googleapis.com",
    "serviceusage.googleapis.com",
    "cloudidentity.googleapis.com",
    "cloudbilling.googleapis.com",
    "iam.googleapis.com",
    "compute.googleapis.com",              # ShareVPC 有効化時に必要
    "container.googleapis.com",            # Subnet を Kubernetes で利用させるときにホスト側でも必要
    "dns.googleapis.com",                  # CloudDNS
    "accesscontextmanager.googleapis.com", # VPC Service Controls に必要
    "essentialcontacts.googleapis.com",    # essentialcontacts
  ])
  service = each.value
  depends_on = [
    google_project.host_project,
  ]
}

# フォルダ IAM 設定
resource "google_folder_iam_binding" "host_admin" {
  folder = google_folder.host_host_folder.name
  for_each = toset([
    "roles/storage.admin",
    "roles/serviceusage.serviceUsageAdmin",
    "roles/editor",
    "roles/dns.admin", #CloudDNS
  ])

  role = each.value

  members = [
    join(":", ["group", var.network_admin_group.email]),
  ]

  depends_on = [
    google_project.host_project,
  ]
}

resource "google_project_iam_binding" "host_networkUser" {
  project = google_project.host_project.id
  for_each = toset([
    "roles/compute.networkUser"
  ])

  role = each.value

  members = [
    join(":", ["group", var.network_admin_group.email]),
    join(":", ["serviceAccount", var.terraform-service-accounts]),
  ]

  depends_on = [
    google_project.host_project,
  ]
}

# VPC 作成
resource "google_compute_network" "host_sharedvpc" {
  name                    = "sharedvpc"
  mtu                     = 1500
  auto_create_subnetworks = false
  routing_mode            = "GLOBAL"
  project                 = google_project.host_project.name

  depends_on = [
    google_project.host_project,
  ]
}

# 共有 VPC の有効化
resource "google_compute_shared_vpc_host_project" "host" {
  project = google_project.host_project.name

  depends_on = [
    google_project.host_project,
    google_project_service.host_api_enable,
  ]
}

やることとしては

  1. ホストプロジェクト用フォルダ作成
  2. ホストプロジェクト作成
  3. 使用する API の有効化
  4. 権限設定
  5. VPC作成と、共有VPC化

あたりが必要となる。

1.2.2.3. DNSの設定。

dns.tf
dns.tf
# googleapis
## https://cloud.google.com/vpc/docs/configure-private-google-access?hl=ja
resource "google_dns_managed_zone" "googleapis" {
  name        = "googleapis"
  project     = google_project.host_project.name
  dns_name    = "googleapis.com."
  description = "Private Access googleapis"

  visibility = "private"

  private_visibility_config {
    networks {
      network_url = google_compute_network.host_sharedvpc.id
    }
  }
  depends_on = [
    google_project.host_project,
  ]
}


resource "google_dns_record_set" "googleapis_restricted_a" {
  name         = "restricted.googleapis.com."
  managed_zone = google_dns_managed_zone.googleapis.name
  type         = "A"
  ttl          = 300
  project      = google_project.host_project.name

  rrdatas = ["199.36.153.4", "199.36.153.5", "199.36.153.6", "199.36.153.7"]

  depends_on = [
    google_dns_managed_zone.googleapis,
  ]
}

resource "google_dns_record_set" "googleapis_private_a" {
  name         = "private.googleapis.com."
  managed_zone = google_dns_managed_zone.googleapis.name
  type         = "A"
  ttl          = 300
  project      = google_project.host_project.name

  rrdatas = ["199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"]

  depends_on = [
    google_dns_managed_zone.googleapis,
    google_dns_record_set.googleapis_restricted_a,
  ]
}

resource "google_dns_record_set" "googleapis_cname" {
  name         = "*.googleapis.com."
  managed_zone = google_dns_managed_zone.googleapis.name
  type         = "CNAME"
  ttl          = 300
  project      = google_project.host_project.name
  rrdatas      = ["restricted.googleapis.com."]
  depends_on = [
    google_dns_managed_zone.googleapis,
    google_dns_record_set.googleapis_private_a,
  ]
}

# cloudbilling へのサクセスがプライベートで作業時に必要なため追加
resource "google_dns_record_set" "googleapis_cname2" {
  name         = "cloudbilling.googleapis.com."
  managed_zone = google_dns_managed_zone.googleapis.name
  type         = "CNAME"
  ttl          = 300
  rrdatas      = ["private.googleapis.com."]
  project      = google_project.host_project.name
  depends_on = [
    google_dns_managed_zone.googleapis,
    google_dns_record_set.googleapis_cname,
  ]
}

resource "google_dns_policy" "googleapis_apipolicy" {
  name                      = "apipolicy"
  enable_inbound_forwarding = true
  project                   = google_project.host_project.name

  enable_logging = false

  networks {
    network_url = google_compute_network.host_sharedvpc.id
  }
  depends_on = [
    google_dns_managed_zone.googleapis,
    google_dns_record_set.googleapis_cname2,
  ]
}

# gcr.io
## https://cloud.google.com/vpc/docs/configure-private-google-access?hl=ja#config-domain
resource "google_dns_managed_zone" "gcrio" {
  name        = "gcrio"
  project     = google_project.host_project.name
  dns_name    = "gcr.io."
  description = "Private Google Cloud Registry"

  visibility = "private"

  private_visibility_config {
    networks {
      network_url = google_compute_network.host_sharedvpc.id
    }
  }
  depends_on = [
    google_project.host_project,
  ]
}

resource "google_dns_record_set" "gcrio_private_a" {
  name         = "gcr.io."
  managed_zone = google_dns_managed_zone.gcrio.name
  type         = "A"
  ttl          = 300
  project      = google_project.host_project.name

  rrdatas = ["199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"]

  depends_on = [
    google_dns_managed_zone.gcrio,
  ]
}

resource "google_dns_record_set" "gcrio_cname" {
  name         = "*.gcr.io."
  managed_zone = google_dns_managed_zone.gcrio.name
  type         = "CNAME"
  ttl          = 300
  project      = google_project.host_project.name
  rrdatas      = ["gcr.io."]
  depends_on = [
    google_dns_managed_zone.gcrio,
    google_dns_record_set.gcrio_private_a,
  ]
}

こちらも組織周りの設定同様、細かいところまでは把握しきれていない。
VM が外部通信を行うにあたって、必要となる設定が盛り込まれている(適当)

1.2.2.4. バックエンドの設定。

項番1.2.1.6.で出力されたファイルがこちらとなる。

backend.tf
backend.tf
terraform {
  backend "gcs" {
    bucket = "hoge"
    prefix = "terraform/state/infrastructure/"
  }
}

1.3. サービス構築

MinecraftサーバーとなるGCEを構築する。
また、サービス単位(今回で言うと、「Minecraftサーバーを提供するサービス」)でプロジェクトを分けていく想定なので、ここからまたフォルダを移動して terraform を実行していく。

ちなみに、フォルダ構成は下記のようになっている。
「./terraform」フォルダなどは省略、また本記事に記載しきれていないコードのファイルも含まれる)

フォルダ構成
PS D:\Applications\terraform_1.1.7_windows_amd64\terraform\hikoly\hoge> tree /f
Folder PATH listing for volume Data
Volume serial number is hoge
D:.
   minecraft_serviceacoount_credential.json
   terraform_serviceacoount_credential.json

├───infrastructure
      .gitignore
      .terraform.lock.hcl
      backend.tf
      dns.tf
      host.tf
      main.tf
      organization.tf
      README.md
      variables.tf
   
   └───prestage
          .terraform.lock.hcl
          main.tf
          output.tf
          variables.tf
       
       └───terraform.tfstate.d
           └───dev
                   terraform.tfstate
                   terraform.tfstate.backup

└───service01
       .gitignore
       .terraform.lock.hcl
       backend.tf
       datasource.tf
       firewall.tf
       keypair.tf
       main.tf
       output.tf
       service01.tf
       service01_function.tf
       service01_gce.tf
       subnet.tf
       vars_common.tf
       vars_functions.tf
       vars_gce.tf
       vars_keypair.tf
    
    ├───scripts
           CPU使用率低下.json
           discordbot.service
           init_script.sh
           mcs_backup.sh
           mineserver-op.py
           server.properties
    
    ├───src
       ├───discordsend
              main.py
              requirements.txt
       
       └───output
           └───discordsend
                   functions.zip
    
    └───templates
            cloud-init.yaml

PS D:\Applications\terraform_1.1.7_windows_amd64\terraform\hikoly\hoge>

1.3.1. GCE(VM)作成

下記を参考にさせていただきました、感謝。

【GCP】Minecraft 1.17のサーバをGCEに建てる

主要スペックは一旦下記とした。

Parameter Value
OS ubuntu-1804-lts
インスタンスタイプ e2-standard-4
ディスクタイプ pd-standard
ディスク容量 10GB
リージョン asia-northeast2
preemptible true

追加ディスクに統合版Minecraftサーバのデータを置いた方が、万が一のVM故障、VM変更などの際に扱いやすいとの意見が散見されたのだけど、ワールドデータは別途バックアップする予定だし(詳細は後述)、なんならスナップショットなりAMIなりを取るという対処もあるので、一旦追加ディスクは無しとした。
ワールドデータが肥大化してディスク容量が足りなくなったら、その時考えることとする。

1.3.1.1. GCE 作成

主なコードは下記。

subnet.tf
subnet.tf
# Subnet 設定
resource "google_compute_subnetwork" "service01-gce-subnets" {
  name                     = "service01-gce-subnetwork"
  ip_cidr_range            = "172.18.0.0/24"
  region                   = var.gcp_common.region
  network                  = var.host_sharedvpc
  private_ip_google_access = true
  project                  = join("-", [var.gcp_common.org_name, var.host_pj.service_name, terraform.workspace])
}

resource "google_compute_subnetwork_iam_binding" "service01-gce-subnets" {
  project    = google_compute_subnetwork.service01-gce-subnets.project
  region     = google_compute_subnetwork.service01-gce-subnets.region
  subnetwork = google_compute_subnetwork.service01-gce-subnets.name
  role       = "roles/compute.networkUser"
  members = [
    join(":", ["group", var.service01_project_admin_group.email]),
  ]
}
firewall.tf
firewall.tf
resource "google_compute_firewall" "private-permit" {
  name        = "private-private-001"
  description = "Private Subnet Permit"
  network     = var.host_sharedvpc
  priority    = 1000
  direction   = "INGRESS"
  project     = join("-", [var.gcp_common.org_name, var.host_pj.service_name, terraform.workspace])

  source_ranges = ["192.168.0.0/16", "172.16.0.0/12", "10.0.0.0/8"]
  target_service_accounts = [
    google_service_account.minecraft.email,
    google_service_account.service01_public_account.email,
    google_service_account.service01_private_account.email,
  ]


  allow {
    protocol = "icmp"
  }

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

  depends_on = [
    google_service_account.minecraft,
    google_service_account.service01_public_account,
    google_service_account.service01_private_account,
  ]

}


# allow 25565 for minecraft
# allow 19132 for minecraft
resource "google_compute_firewall" "allow-mcp" {
  for_each = var.gce[terraform.workspace]

  name    = "allow-mcp"
  network = var.host_sharedvpc
  project = join("-", [var.gcp_common.org_name, var.host_pj.service_name, terraform.workspace])

  allow {
    protocol = "icmp"
  }

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

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

  allow {
    protocol = "udp"
    ports    = ["19132"]
  }

  source_ranges = ["0.0.0.0/0"]
  target_tags   = [terraform.workspace, google_compute_instance.service01_gce[each.key].name]
}
service01_gce.tf
service01_gce.tf
# Global IP 作成
resource "google_compute_address" "mcs-ip" {
  name         = "mcs"
  description  = "external IP for mcs"
  network_tier = "STANDARD"
  region       = var.gcp_common.region
  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
}
# Global IP 作成 END

# GCE 作成
resource "google_compute_instance" "service01_gce" {
  for_each = var.gce[terraform.workspace]

  name         = each.key
  machine_type = each.value.machine_type
  zone         = var.gcp_common.zone
  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])

  deletion_protection = each.value.deletion_protection

  tags         = [terraform.workspace, each.key]

  boot_disk {
    auto_delete = true
    device_name = each.key
    initialize_params {
      image = each.value.image
      type  = each.value.type
      size  = each.value.size
    }
  }

  network_interface {
    subnetwork = google_compute_subnetwork.service01-gce-subnets.id
    access_config {
      nat_ip       = google_compute_address.mcs-ip.address
      network_tier = "STANDARD"
    }
  }

  service_account {
    # Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles.
    email  = google_service_account.minecraft.email
    scopes = ["cloud-platform"]
  }

  #preemptible設定
  scheduling {
    preemptible       = true
    automatic_restart = false
  }

  metadata = {
    startup-script = each.value.metadata_startup_script
    shutdown-script = each.value.metadata_shutdown_script

    block-project-ssh-keys = each.value.block-project-ssh-keys
    # GCE の metadata に必要な形式に整形して設定(改行コードの除去等)
    ssh-keys = "${var.gce_ssh.user}:${replace("${tls_private_key.keygen[each.value.key_name].public_key_openssh}", "\n", "")} ${var.gce_ssh.user}"

    user-data = file("./templates/cloud-init.yaml"),

  }
}
# GCE 作成 END

# minecraft-game-server のバックアップ置き場の作成
resource "google_storage_bucket" "mcs_backup_backet" {
  name          = join("-", [var.gcp_common.org_name, var.service01_pj, "mcs_backup_backet", terraform.workspace])
  project       = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
  location = "us-west1"
  storage_class = "REGIONAL"
  force_destroy = true

  versioning {
    enabled = true
  }

  lifecycle_rule {
    condition {
      num_newer_versions = 5
    }
    action {
      type = "Delete"
    }
  }
}
# minecraft-game-server のバックアップ置き場の作成 END

# Minecraft サービスアカウントの作成
resource "google_service_account" "minecraft" {
  account_id   = "minecraft"
  display_name = "Minecraft Administrator Account"
  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
}
# Minecraft サービスアカウントの作成 END

# Minecraft サービスアカウント用ロールの作成
resource "google_project_iam_custom_role" "minecraft-role" {
  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
  role_id     = "minecraftAdministratorRole"
  title       = "Minecraft Administrator Role"
  description = "Minecraft Administrator Role"
  permissions = ["compute.instances.start", "compute.instances.stop", "compute.instances.get", "compute.zoneOperations.get"]
}
# Minecraft サービスアカウント用ロールの作成 END

# Minecraft サービスアカウントへロール付与
resource "google_project_iam_member" "minecraft" {
  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
  #role = google_project_iam_custom_role.minecraft-role.name

  for_each = toset([
    google_project_iam_custom_role.minecraft-role.name,
    "roles/storage.admin",
  ])

  role = each.value

  member = join(":", ["serviceAccount", google_service_account.minecraft.email])

  depends_on = [
    google_service_account.minecraft,
    google_project_iam_custom_role.minecraft-role,
  ]
}
# Minecraft サービスアカウントへロール付与 END

# GIP をコンソールに出力
output "service01_gce_global_ip" {
#  value = google_compute_instance.service01_gce[each.key].network_interface[0].access_config[0].nat_ip
  value = values(google_compute_instance.service01_gce)[*].network_interface[0].access_config[0].nat_ip
}
# GIP をコンソールに出力 END
vars_gce.tf
vars_gce.tf
variable "gce" {
  default = {
    dev = {
      minecraft-game-server = {
        machine_type             = "e2-standard-4"
        image                    = "ubuntu-os-cloud/ubuntu-1804-lts"
        type                     = "pd-standard"
        size                     = "10"
        block-project-ssh-keys   = true
        key_name                 = "hoge"
        deletion_protection      = false
        metadata_startup_script  = "cd /home/minecraft\nLD_LIBRARY_PATH=.\nsudo screen -dmS mcs /home/minecraft/bedrock_server"
        metadata_shutdown_script = "cd /home/minecraft\nsudo screen -r mcs -X stuff 'stop'\n/home/minecraft/mcs_backup.sh"
      }
    }
    prd = {
    }
  }
}

主にやっていることは下記。

  1. GIP(グローバルIP)作成
  2. GCE作成
  3. minecraft-game-server のバックアップ用GCS作成
  4. Minecraft用サービスアカウント作成

Minecraft用サービスアカウントの役割は、バックアップ用GCSへのアップロード権限や、後述のGCE自動起動・停止などのスクリプト実行のインスタンス操作権限など。
変数定義(vars_gce.tf)は、GCEのパラメータや、起動・停止スクリプトを記載。

以下、コードのポイント記載。

1.3.1.2. metadata
service01_gce.tf
  metadata = {
    startup-script = each.value.metadata_startup_script
    shutdown-script = each.value.metadata_shutdown_script

    block-project-ssh-keys = each.value.block-project-ssh-keys
    # GCE の metadata に必要な形式に整形して設定(改行コードの除去等)
    ssh-keys = "${var.gce_ssh.user}:${replace("${tls_private_key.keygen[each.value.key_name].public_key_openssh}", "\n", "")} ${var.gce_ssh.user}"

    user-data = file("./templates/cloud-init.yaml"),

  }

「 metadata 」ブロックでは後述の cloud-config 用の yaml ファイルを指定してる。
「 startup-script 」は統合版Minecraftサーバの起動コマンド、「 shutdown-script 」は統合版Minecraftサーバの停止コマンドとワールドデータのバックアップスクリプトを仕込んでいる。
バックアップスクリプトの詳細は後述する。

1.3.1.3. GIP(グローバルIP)
service01_gce.tf
# Global IP 作成
resource "google_compute_address" "mcs-ip" {
  name         = "mcs"
  description  = "external IP for mcs"
  network_tier = "STANDARD"
  region       = var.gcp_common.region
  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
}
# Global IP 作成 END

外部からのアクセス用にGIP(グローバルIP)を付与している。
今回の構成上、ドメインを使用しているので、NAT+DNSでもいけるとは思うが、NATの料金が発生することや(GIPとどっちが安いか調べてないけど)、Switchで外部サーバーを追加する際にホスト名指定(アルファベット入力)が出来るかわからなかったので、IP直でいけるようGIPを付与することとした。

1.3.1.4. minecraft-game-server のバックアップ置き場
service01_gce.tf
# minecraft-game-server のバックアップ置き場の作成
resource "google_storage_bucket" "mcs_backup_backet" {
  name          = join("-", [var.gcp_common.org_name, var.service01_pj, "mcs_backup_backet", terraform.workspace])
  project       = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
  location = "us-west1"
  storage_class = "REGIONAL"
  force_destroy = true

  versioning {
    enabled = true
  }

  lifecycle_rule {
    condition {
      num_newer_versions = 5
    }
    action {
      type = "Delete"
    }
  }
}
# minecraft-game-server のバックアップ置き場の作成 END

terraform の State ファイル用 GCS と同様、バージョニング(世代管理)と、ライフサイクル(5世代保存、それ以降は古いものから自動削除)を設定。
ちなみに GCE 本体はネットワークの応答速度などを考慮し東京リージョンに構築しているが、それ以外のバックアップなどのリソースについてはコストを考慮しUSリージョンとしている。

1.3.1.5. Minecraft 用サービスアカウント作成とロール付与
service01_gce.tf
service01_gce.tf
# Minecraft 用サービスアカウントの作成
resource "google_service_account" "minecraft" {
  account_id   = "minecraft"
  display_name = "Minecraft Administrator Account"
  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
}
# Minecraft 用サービスアカウントの作成 END

# Minecraft 用サービスアカウント用ロールの作成
resource "google_project_iam_custom_role" "minecraft-role" {
  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
  role_id     = "minecraftAdministratorRole"
  title       = "Minecraft Administrator Role"
  description = "Minecraft Administrator Role"
  permissions = ["compute.instances.start", "compute.instances.stop", "compute.instances.get", "compute.zoneOperations.get"]
}
# Minecraft 用サービスアカウント用ロールの作成 END

# Minecraft 用サービスアカウントへロール付与
resource "google_project_iam_member" "minecraft" {
  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
  #role = google_project_iam_custom_role.minecraft-role.name

  for_each = toset([
    google_project_iam_custom_role.minecraft-role.name,
    "roles/storage.admin",
  ])

  role = each.value

  member = join(":", ["serviceAccount", google_service_account.minecraft.email])

  depends_on = [
    google_service_account.minecraft,
    google_project_iam_custom_role.minecraft-role,
  ]
}
# Minecraft 用サービスアカウントへロール付与 END

前述した、Minecraft用サービスアカウントの作成。
後述のGCE自動起動・停止などのスクリプト実行のためのインスタンス操作権限を、カスタムロールとして作成しバインドしている。
バックアップ用GCSへのアップロード権限については、細かく絞るのが面倒だったので「roles/storage.admin」をバインド。

1.3.1.6. GIP をコンソールに出力
service01_gce.tf
service01_gce.tf
# GIP をコンソールに出力
output "service01_gce_global_ip" {
#  value = google_compute_instance.service01_gce[each.key].network_interface[0].access_config[0].nat_ip
  value = values(google_compute_instance.service01_gce)[*].network_interface[0].access_config[0].nat_ip
}
# GIP をコンソールに出力 END
後ほど、実際にPCやSwitchのMinecraftクライアントから接続する際に必要となる GIP を出力している。 ただ、もし Switch でホスト名での接続が可能なようであれば、ここは可変IPでホスト名接続にしたいところ。 その方が子供なども視覚的に分かりやすいし、万が一GIP再作成でIPが変わってしまっても影響がない。

1.3.2. 統合版Minecraftサーバーインストール

terraform や metadata の処理順序の関係上、先に起動コマンドやバックアップスクリプトの説明となったが、ここでようやく統合版Minecraftサーバのアプリケーションを導入する。
ざっくり下記を実施(実際には clound-config でインスタンス作成時に実行されるようにする)

  1. minecraft ユーザ作成
  2. minecraft ユーザに sudo 権限付与(要らないかも)
  3. minecraft ユーザで統合版MinecraftサーバーファイルをDL&解凍
  4. 統合版Minecraftサーバーの設定ファイルを編集(とりあえずサーバー名称と難易度設定のみ)
  5. 統合版Minecraftサーバー起動!!

いやー、長かった……いや、動作確認もまだだけど。

clound-config の内容は下記。
これをインスタンス起動時の user-data に指定することによって、統合版Minecraftサーバーのダウンロード・インストール(解凍)・バックアップスクリプトの設定まで行うようにした。

1.3.2.1. cloud-init.yaml

cloud-init.yaml
cloud-init.yaml
#cloud-config
timezone: Asia/Tokyo
locale: ja_JP.utf8

users:
  - name: 'minecraft'
    plain_text_passwd: 'hoge'
    shell: '/bin/bash'
    sudo: 'ALL=(ALL) NOPASSWD:ALL'
    lock_passwd: false

# init command
runcmd:
  - "wget -P /home/minecraft https://minecraft.azureedge.net/bin-linux/bedrock-server-1.18.12.01.zip"
  - unzip /home/minecraft/bedrock-server-1.18.12.01.zip -d /home/minecraft
  - sed -i".org" -e "s/^server-name=Dedicated Server$/server-name=Hiko Game Server/g" /home/minecraft/server.properties
  - sed -i -e "s/^difficulty=easy$/difficulty=peaceful/g" /home/minecraft/server.properties
  - 
write_files:
    - path: /home/minecraft/mcs_backup.sh
      content: |
        #!/bin/bash
        BACKUP_BUCKET='gs://hikogames-service01-mcs_backup_backet-dev/'

        screen -r mcs -X stuff '/save-all\n/save-off\n'
        /usr/bin/zip -r ${BASH_SOURCE%/*}/worlds.zip ${BASH_SOURCE%/*}/worlds
        /snap/bin/gsutil cp -R ${BASH_SOURCE%/*}/worlds.zip ${BACKUP_BUCKET}
        /bin/rm ${BASH_SOURCE%/*}/worlds.zip
        screen -r mcs -X stuff '/save-on\n'
      permissions: '0755'
      owner: root:root
    - path: /etc/crontab
      content: |
        0 */4 * * * /home/minecraft/mcs_backup.sh
      append: true


# reboot
power_state:
  delay: now
  mode: reboot

この cloud-config(とstartup-script)により、VM作成後自動的に統合版Minecraftサーバが起動するようになっている。
いざという時にサクッと消したり、別リージョン、あるいは別クラウドで建て直したいとなった場合にも、大幅な労力削減が見込める(そんな事があるかは別問題、というか考えてはいけない)

1.3.2.2. バックアップスクリプト

バックアップスクリプトの内容は下記。

mcs_backup.sh
#!/bin/bash
BACKUP_BUCKET='gs://hoge/'

screen -r mcs -X stuff '/save-all\n/save-off\n'
/usr/bin/zip -r ${BASH_SOURCE%/*}/worlds.zip ${BASH_SOURCE%/*}/worlds
/snap/bin/gsutil cp -R ${BASH_SOURCE%/*}/worlds.zip ${BACKUP_BUCKET}
/bin/rm ${BASH_SOURCE%/*}/worlds.zip
screen -r mcs -X stuff '/save-on\n'
  1. 稼働中の統合版Minecraftサーバを停止
  2. ワールドデータディレクトリを zip 圧縮
  3. GCS にアップロード
  4. 圧縮ファイルを削除
  5. 統合版Minecraftサーバを起動

という処理を行う。
先述の通り、VMシャットダウン時に実行しているが、cron でも一定時間毎に実行するようにしてる。
公式の推奨は4時間毎のバックアップらしい。
ただ、プレイ中にバックアップが走るのはちょっと嫌だし、終了時のバックアップのみでも良い気がする。
その場合、「5.」が不要になるので、このあたりは実際に運用してみて要検討。

1.3.3. とりあえず動作確認

自宅PCの統合版Minecraftから接続、無事ワールドに入れた。
Switchの統合版Minecraftからも問題なく接続出来た。

細かいゲーム設定は後にして、運用周りを詰めていく。
次はDiscordとの連携にチャレンジ。

1.4. Discord との連携(Discord Bot)

スクリプトも実装方法もいろいろあるので、いろいろ参考に、感謝。

Pythonで実用Discord Bot(discordpy解説)
DiscordからGCP上のマイクラサーバーを制御してインスタンス料金をケチる話
DiscordからGCPインスタンスを操作してみた話

1.4.1. スクリプト

Discord Bot の実装などは割愛。
(気が向いたら追記するかも、しないかも)

ちなみに、先述した通り Among Us 用の AutoMuteUs ボット や、それをログインさせる Discord チャンネル、Bot を操作するためのスクリプト(GCPで Always Free のサーバー上に導入)などは構築済みだったので、そこへ Bot を追加した。

そして、導入した Bot を操作するために下記のようなスクリプトを実装。
これにより、自身の Discord チャンネルに Bot を参加させ会話を監視→特定のメッセージを検知したら処理を実行、ということが出来るようになる。

mineserver-op.py
mineserver-op.py
# インストールした discord.py を読み込む
import discord
import os
import time
import subprocess
from subprocess import PIPE


# 変数定義
TOKEN = 'hoge' # Discord Bot のトークン
SERVICE_ACCOUNT = 'minecraft@hoge.iam.gserviceaccount.com' # gclud コマンド用サービスアカウント
INSTANCE_NAME = 'hoge' # 統合版Minecraftサーバー名(GCP上のVM名)
PROJECT_NAME = 'hoge' # 統合版Minecraftサーバーが属している GCP Project
ZONE_NAME = 'asia-northeast1-b' # 統合版Minecraftサーバーが属している GCPゾーン
MCS_GIP = 'hoge' # 統合版MinecraftサーバーグローバルIP
MCS_PORT = '19132' # 統合版Minecraftサーバーポート
CH_ID = hoge # Bot が発言する Discord のチャンネルID
MY_BOT_NAME = 'hoge Bot#hoge' # 上記チャンネルの Webhook URL から発言した際の Bot の名前
client = discord.Client()

# 起動時に動作する処理
@client.event
async def on_ready():
    ch = client.get_channel(CH_ID)
    await ch.send('Minecraft Administrator Bot 起動')

    # 起動したらターミナルにログイン通知が表示される
    print('ログインなう')
    print('/mcs help でコマンドの確認ができるよ')

    # debug
    #command = ['gcloud', f'--account={SERVICE_ACCOUNT} compute instances describe {INSTANCE_NAME} --project {PROJECT_NAME} --zone {ZONE_NAME} --format="value(Status)"']
    #instance_status = subprocess.check_output([f'gcloud --account={SERVICE_ACCOUNT} compute instances describe {INSTANCE_NAME} --project {PROJECT_NAME} --zone {ZONE_NAME} --format="value(Status)"'], shell=True)
    #print(str(instance_status))

# メッセージ受信時に動作する処理
@client.event
async def on_message(message):
    # メッセージ送信者がBotだった場合は処理しない、ただし特定のBotのみ処理を継続する
    if message.author.bot:
        author = message.author
        if f'{author}' == f'{MY_BOT_NAME}':
            pass
        else:
            return

#サーバーの起動
    if message.content == '/mcs start':
        await message.channel.send('統合版Minecraftサーバーを起動開始')
        await message.channel.send('※「起動完了」が表示されるまで他のコマンドを実行させないこと※')
        await message.channel.send('統合版Minecraftサーバーを起動中……')
        subprocess.run([f'gcloud --account={SERVICE_ACCOUNT} compute instances start {INSTANCE_NAME} --project {PROJECT_NAME} --zone {ZONE_NAME}'], shell=True)

        instance_status = ""
        while 'RUNNING' in str(instance_status):
            time.sleep(5)
            instance_status = subprocess.check_output([f'gcloud --account={SERVICE_ACCOUNT} compute instances describe {INSTANCE_NAME} --project {PROJECT_NAME} --zone {ZONE_NAME} --format="value(Status)"'], shell=True)
            print(str(instance_status))

        await message.channel.send('……統合版Minecraftサーバーを起動完了')
#サーバーの停止
    if message.content == '/mcs stop':
        await message.channel.send('統合版Minecraftサーバーを停止開始')
        await message.channel.send('※「停止完了」が表示されるまで他のコマンドを実行させないこと※')
        await message.channel.send('統合版Minecraftサーバーを停止中……')
        subprocess.run([f'gcloud --account={SERVICE_ACCOUNT} compute instances stop {INSTANCE_NAME} --project {PROJECT_NAME} --zone {ZONE_NAME}'], shell=True)

        instance_status = ""
        while 'TERMINATED' in str(instance_status):
            time.sleep(5)
            instance_status = subprocess.check_output([f'gcloud --account={SERVICE_ACCOUNT} compute instances describe {INSTANCE_NAME} --project {PROJECT_NAME} --zone {ZONE_NAME} --format="value(Status)"'], shell=True)
            print(str(instance_status))

        await message.channel.send('……統合版Minecraftサーバーを停止完了')

#ヘルプの表示
    if message.content == '/mcs help':
        await message.channel.send('/mcs start : 統合版Minecraftサーバの起動')
        await message.channel.send('/mcs stop : 統合版Minecraftサーバの停止')
        await message.channel.send('/mcs status : 統合版Minecraftサーバの状態確認')

#サーバーの状態確認
    if message.content == '/mcs status':
        await message.channel.send('統合版Minecraftサーバの状態確認中……')
        instance_status = subprocess.check_output([f'gcloud --account={SERVICE_ACCOUNT} compute instances describe {INSTANCE_NAME} --project {PROJECT_NAME} --zone {ZONE_NAME} --format="value(Status)"'], shell=True)
        print(str(instance_status))

        if 'RUNNING' in str(instance_status):
            await message.channel.send('……統合版Minecraftサーバ稼働中')
        elif 'TERMINATED' in str(instance_status):
            await message.channel.send('……統合版Minecraftサーバ停止中')
        else:
            await message.channel.send('……統合版Minecraftサーバの状態不明')


client.run(TOKEN)

具体的には、誰かが下記メッセージを発言すると作動。
/mcs help → 上記コマンドをチャットに表示(発言)
mcs_help.png

/mcs start → 統合版Minecraftサーバの起動
mcs_start.png

/mcs stop → 統合版Minecraftサーバの停止
mcs_stop.png

/mcs status → 統合版Minecraftサーバの状態確認
mcs_status.png

ここが最重要なので、起動確認・停止確認・ステータスチェックはわりとしっかり実装。
本当は統合版Minecraftサーバ起動後にポート疎通確認までしたかったのだけど、python からだとどうにもうまく出来なかった…。

あと、起動時にターミナルに通知とかも入れてるけど、起動を自動化したらターミナルなんて見ないのでいらなかった…。

また、本来 Bot からの発言は無視するようにしているのだが、「特定の Bot 」からの発言は、Bot であってもメッセージに対する処理を行う。

mineserver-op.py
    # メッセージ送信者がBotだった場合は処理しない、ただし特定のBotのみ処理を継続する
    if message.author.bot:
        author = message.author
        if f'{author}' == f'{MY_BOT_NAME}':
            pass
        else:
            return

これは後述する自動停止を別の Bot から行うためである。
ただ停止するだけならそのままVMを停止すれば良いのだが、その際に一応 Discord チャンネルに一言発言を添えたいので、別の Bot から「一言 + 停止コマンド」を発言し、その発言に対して本 Bot が処理を行う。

1.4.2. systemd で自動起動

サーバー起動時に Bot も自動起動してもらう必要があるので、systemd 化する。
VM metadata の startup script とかでも起動出来るけど、この場合エラー落ちなどの際に復旧しないので、systemdの方が良い。

discordbot.service
[Unit]
Description=MinecraftAdminBot
After=local-fs.target,network-online.target
ConditionPathExists=/opt/discordbot/scripts

[Service]
ExecStart=/usr/bin/python3 /opt/discordbot/scripts/mineserver-op.py
Restart=always
Type=simple

[Install]
WantedBy=multi-user.target

ちなみに、systemd化した場合、スクリプト実行ユーザーが root になるため、root で GCP サービスアカウントの認証を通す必要がある。
ここに気付かなくてしばらくハマった…… systemctl status にスクリプト実行中のログも出るので、しっかり見よう(自戒)

Mar 31 15:17:04 instance-1 python3[2843]: ERROR: (gcloud.compute.instances.describe) Your current active ...ials
Mar 31 15:17:04 instance-1 python3[2843]: Please run:
Mar 31 15:17:04 instance-1 python3[2843]: $ gcloud auth login
Mar 31 15:17:04 instance-1 python3[2843]: to obtain new credentials.
Mar 31 15:17:04 instance-1 python3[2843]: For service account, please activate it first:
Mar 31 15:17:04 instance-1 python3[2843]: $ gcloud auth activate-service-account ACCOUNT

ちなみにこの VM は何度か記載している通り以前から使用していたもので、IaC 化がされていない(手動作成)
そのため、サービスアカウントの認証情報(pemファイル)を突っ込んで、手動で認証をしている。
いずれはこの VM についても IaC 化し、認証情報については インスタンスに付与する形としたい。

1.4.3. 統合版Minecraftサーバーの自動停止

前項で手動で起動・停止が行えるにはしたが、人間なので万が一停止し忘れることもあるだろう。
そうなると、停止し忘れた人も管理人(私)もとても気まずい思いをしてしまうに違いない…。
なので、自動停止機能も実装する。

仕組みとしては下記。

  1. 統合版MinecraftサーバーのCPU使用率を監視
  2. CPU使用率が 2% を下回ったら、通知チャンネルにアラート発信
  3. アラートを受けた通知チャンネルが停止用スクリプトをキック
  4. 停止用スクリプト実行

結構面倒だったので、細かく記載していく。

1.4.3.1. 統合版MinecraftサーバーのCPU使用率を監視

メインとなるコードは下記。

service01_function.tf
service01_function.tf
resource "google_pubsub_topic" "call_function" {
  name    = join("-", [var.gcp_common.org_name, var.service01_pj, "call_function", terraform.workspace])
  project = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
}

# cloud function のソースファイルを zip 化
data "archive_file" "function_archive" {
  for_each = var.functions[terraform.workspace]

  type        = "zip"
  source_dir  = "src/${each.key}" # main.pyやrequirement.txtが入ってるdir
  output_path = "src/output/${each.key}/functions.zip" # zipファイルの出力パス
}
# cloud function のソースファイルを zip 化 END

# zip ファイルをアップロードするためのbucket作成
resource "google_storage_bucket" "functions_src" {
  name          = join("-", [var.gcp_common.org_name, var.service01_pj, "functions_src_backet", terraform.workspace])
  project       = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
  location = "us-west1"
  storage_class = "REGIONAL"
  force_destroy = true

  versioning {
    enabled = true
  }

  lifecycle_rule {
    condition {
      num_newer_versions = 5
    }
    action {
      type = "Delete"
    }
  }
}
# zip ファイルをアップロードするための bucket 作成 END

# bucket に zip ファイルをアップロード
resource "google_storage_bucket_object" "packages" {
  for_each = var.functions[terraform.workspace]

  #name    = "packages/functions.${data.archive_file.function_archive[each.key].output_md5}.zip"
  name    = join("-", ["packages/", var.gcp_common.org_name, var.service01_pj, each.key, terraform.workspace, data.archive_file.function_archive[each.key].output_md5, ".zip"])
  bucket  = google_storage_bucket.functions_src.name
  source  = data.archive_file.function_archive[each.key].output_path
}
# bucket に zip ファイルをアップロード END

# function 作成
resource "google_cloudfunctions_function" "function" {
  for_each = var.functions[terraform.workspace]

  name                  = join("-", [var.gcp_common.org_name, var.service01_pj, each.key, terraform.workspace])
  project               = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
  description           = join("-", [var.gcp_common.org_name, var.service01_pj, each.key, terraform.workspace])
  region                = var.gcp_common.region
  runtime               = each.value.runtime
  source_archive_bucket = google_storage_bucket.functions_src.name
  source_archive_object = google_storage_bucket_object.packages[each.key].name
  available_memory_mb   = each.value.available_memory_mb
  timeout               = each.value.timeout
  entry_point           = each.value.entry_point

  event_trigger {
    event_type = "google.pubsub.topic.publish"
    resource   = google_pubsub_topic.call_function.id
    failure_policy {
      retry = each.value.retry
    }
  }

    depends_on = [
      google_project_service.service01_api_enable,
      google_pubsub_topic.call_function,
    ]
}
# function 作成 END


resource "google_monitoring_notification_channel" "pubsub_ch" {
  display_name = "Alert notifications"
  description  = "Pub/Sub channel for alert notifications"
  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
  type         = "pubsub"

  labels = {
    topic = "projects/${join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])}/topics/${join("-", [var.gcp_common.org_name, var.service01_pj, "call_function", terraform.workspace])}"
  }
}

resource "google_pubsub_topic_iam_binding" "binding" {
  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
  topic = google_pubsub_topic.call_function.name

  for_each = toset([
    "roles/viewer",
    "roles/pubsub.publisher",
  ])

  role = each.value

  members = [
    join("", ["serviceAccount:", "service-", data.google_project.service01_pj.number, "@gcp-sa-monitoring-notification.iam.gserviceaccount.com"]),
    #join("", ["serviceAccount:monitoring-notification@", var.gcp_common.org_name, "-", var.service01_pj, "-", terraform.workspace, ".iam.gserviceaccount.com"]),
  ]

  depends_on = [
    #google_service_account.monitoring-notification,
    google_pubsub_topic.call_function,
  ]
}

resource "google_monitoring_alert_policy" "cpu_utilization" {
  for_each = var.gce[terraform.workspace]

  project      = join("-", [var.gcp_common.org_name, var.service01_pj, terraform.workspace])
  display_name          = "VM Instance - CPU utilization"
  #notification_channels = ["projects/${var.gcp_common.org_name}-${var.service01_pj}-${terraform.workspace}/notificationChannels/${google_monitoring_notification_channel.pubsub_ch.id}"]
  notification_channels = [google_monitoring_notification_channel.pubsub_ch.id]
  combiner              = "OR"
  conditions {
    display_name = "[CPU使用率 ${var.monitoring[terraform.workspace].cpu_utilization_threshold_percent}% 未満]"
    condition_threshold {
      #filter          = "metric.type=\"compute.googleapis.com/instance/cpu/utilization\" AND resource.type=\"gce_instance\" AND metadata.system_labels.name=\"${google_compute_instance.service01_gce[each.key]}\" AND metric.label.\"cpu_state\"= \"idle\""
      #filter          = "metric.type=\"compute.googleapis.com/instance/cpu/utilization\" AND resource.type=\"gce_instance\" AND metadata.system_labels.name=\"${google_compute_instance.service01_gce[each.key].id}\""
      filter          = "metric.type=\"compute.googleapis.com/instance/cpu/utilization\" resource.type=\"gce_instance\" metric.label.\"instance_name\"=\"hikogames-service01-minecraft-game-server-dev\""
      duration        = "900s"
      threshold_value = var.monitoring[terraform.workspace].cpu_utilization_threshold
      comparison      = "COMPARISON_LT" # 現在の値が閾値未満なら発報
      aggregations {
        alignment_period   = "300s"
        per_series_aligner = "ALIGN_MEAN"
      }
      trigger {
        percent = 100
      }
    }
  }

  # 30分メトリクスデータが存在しなかったら、インシデントクローズ
  alert_strategy {
    auto_close = "1800s"
  }

  depends_on = [
    google_monitoring_notification_channel.pubsub_ch,
  ]
}
vars_functions.tf
vars_functions.tf
variable "functions" {
  default = {
    dev = {
      discordsend = {
        runtime               = "python37"
        trigger_topic         = "call_function"
        available_memory_mb   = 128
        timeout               = 120
        retry                 = true
        entry_point           = "run"
      }
    }
  }
}

variable "monitoring" {
  default = {
    dev = {
      cpu_utilization_threshold = 0.02
      trigger_topic         = true
      available_memory_mb   = 128
      timeout               = 120
      retry                 = true
      entry_point           = "run"
    }
  }
}
  1. Pub/Sub Topic の作成
  2. Cloud Function の作成
    2.1. ソースファイルzip化
    2.2. zipファイルをアップロードするためのGCS作成
    2.3. zipファイルをアップロード
    2.4. 「1.」の Topic をトリガーとして Cloud Function デプロイ
  3. 「1.」の Topic へ通知する通知チャネルの作成
  4. 「3.」に連動し、自動でサービスアカウントが作成される(~@gcp-sa-monitoring-notification.iam.gserviceaccount.com)
  5. 「4.」のサービスアカウントに「1.」の Topic をパブリッシュする権限(roles/pubsub.publisher)をバインド
  6. Cloud Monitoring で統合版MinecraftサーバーのCPU使用率を監視し、異常時に「3.」の通知チャネルへ通知するアラートを作成する

以上となる。
きちんと動いている今でも、上記手順で正しいのか自信がないくらい、複雑で面倒だった…。
間違ってる部分もあるかもしれないので、時間があれば自分の中でも再整理したいポイント。

1.4.3.1. 統合版Minecraftサーバーの自動停止スクリプト

Cloud Functions で実行されるスクリプト。

mainコードは下記となる。

main.py
import requests

def run(event, context):
    webhook_url = 'https://discord.com/api/webhooks/hoge'

    main_content = {
      "content": "プレイヤー不在と思われるため、統合版Minecraftサーバーを停止します。もしプレイ中の方いたらごめんなさい!"
    }
    requests.post(webhook_url,main_content)

    main_content = {
      "content": "/mcs stop"
    }
    requests.post(webhook_url,main_content)

Discord 側の機能である Webhook を使用している。
対象のURLにPOSTリクエストを送ると、対象チャンネルにBotが発言してくれる。
mcs_autostop.png

1.5. まとめ

かなり長くなってしまったが、以上で構築メモ(?)完了。
一部課題は残ってはいるが、とりあえず「GCPでMinecraftサーバー建てて、Discordから起動/停止する、可能な限り IaC 化する」という目標は達成出来たのではないかと思う。

最後にハマったところなどをツラツラと書き上げて終わりとする。
(気が向いたら書き足すかも)

1.6. ハマったところ

1.6.1. terraform destroy でデフォルトの IAM 権限も削除されてしまう

コードで解決出来れば良いのだけど、方法が思い付いていないので、とりあえず対処方法。

prestage も destroy し、下記コマンドを実行する。

gcloud.cmd organizations add-iam-policy-binding [Organization ID] --member=user:[組織管理アカウント] --role=roles/resourcemanager.organizationAdmin
gcloud.cmd organizations add-iam-policy-binding [Organization ID] --member=domain:[ドメイン名] --role=roles/billing.creator
gcloud.cmd organizations add-iam-policy-binding [Organization ID] --member=domain:[ドメイン名] --role=roles/resourcemanager.projectCreator
gcloud.cmd organizations remove-iam-policy-binding [Organization ID] --member=domain:[ドメイン名] --role=roles/orgpolicy.policyAdmin

「\prestage\main.tf」27行目、「project_id」をカウントアップし、prestage再作成から再構築を実施する。

※GCPのプロジェクトは削除後、30日経過するまで「削除保留」というステータスになり復旧が出来るのだが、
 この保留期間中のプロジェクトIDも一意かどうかのチェック対象に含まれる。
 terraform で作成・削除を繰り返す際にネックになるので、プロジェクト名とIDは分けた方が良い気がする。
 (再作成・デバッグ作業でプロジェクト名を変えたくないので)

 プロジェクト名と ID はイコールにすべき、という意見も多いのだけど、個人的にはイコールである必要性を感じないかなぁ。
 上記ネックもあるし、terraform コードを書いている時に、Name 指定なのか ID 指定なのかで混乱することが多々…。

1.6.2. terraform 実行時にトークン系のエラーが出る

おそらく一括 Destroy → appry 時に、再作成されたサービスアカウントが同じ名前であっても認証が通らなくなる、のだと思う。

PS D:\Applications\terraform_1.1.7_windows_amd64\terraform\hikoly\[ドメイン名]\service01> terraform.exe plan

 Error: Error when reading or editing Project "[Project名]": Get "https://cloudresourcemanager.googleapis.com/v1/projects/[Project名]?alt=json&prettyPrint=false": oauth2: cannot fetch token: 400 Bad Request
 Response: {"error":"invalid_grant","error_description":"Invalid grant: account not found"}

クレデンシャル用の json ファイルを再生成して置換すればOK。

gcloud.cmd iam service-accounts keys create terraform_serviceacoount_credential.json --iam-account [terraform用サービスアカウント名]

1.6.3. for_each 内に depends on が必要なリソースは指定不可

仕様?
「--target」で先に depends on 対象のリソースを作れと言われた。

PS D:\Applications\terraform_1.1.7_windows_amd64\terraform\hikoly\hiko.games\service01> terraform.exe apply

 Error: Invalid for_each argument

   on service01_gce.tf line 130, in resource "google_project_iam_member" "minecraft":
  130:   for_each = toset([
  131:     google_project_iam_custom_role.minecraft-role.name,
  132:     "roles/storage.admin",
  133:   ])
     ├────────────────
      google_project_iam_custom_role.minecraft-role.name is a string, known only after apply

 The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot
 predict how many instances will be created. To work around this, use the -target argument to first apply only the
 resources that the for_each depends on.

言われた通り、先に単独で作成。

terraform.exe apply --target=google_project_iam_custom_role.minecraft-role                                               

確かにそれでエラーは回避出来たけど……うーん、美しくない。

1.6.4. VM作成時にネットワークリソースが存在しないと怒られる

google_compute_instance.service01_gce["minecraft-game-server"]: Creating...

 Error: Error creating instance: googleapi: Error 400: Invalid value for field 'resource.networkInterfaces[0].network': 'projects/hikogames-service01-dev/global/networks/sharedvpc'. The referenced network resource cannot be found., invalid

   with google_compute_instance.service01_gce["minecraft-game-server"],
   on service01_gce.tf line 12, in resource "google_compute_instance" "service01_gce":
   12: resource "google_compute_instance" "service01_gce" {

いやー、これはだいぶハマった…。
原因は「network_interface」ブロックで共有VPCを指定していたこと。
最初は普通に作れてたので、どこかのタイミングでなんとなく書いてしまったのかもしれない……。
同ブロックに GIP 設定( access_config ブロック)を追加した時が怪しい。
なんか共有VPCを指定しないといけない気がした気がする。

  network_interface {
    network    = var.host_sharedvpc
    subnetwork = google_compute_subnetwork.service01-gce-subnets.id
    access_config {
      nat_ip       = google_compute_address.mcs-ip.address
      network_tier = "STANDARD"
    }
  }

「network = var.host_sharedvpc」を削除して解決。

1.6.5. 一括 apply で GIP 作成した際に、「Compute Engine API」が有効になっていないと怒られる

仕様?、タイミングの問題?

 Error: Error creating Address: googleapi: Error 403: Compute Engine API has not been used in project 129446410799 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/compute.googleapis.com/overview?project=129446410799 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
 Details:
 [
   {
     "@type": "type.googleapis.com/google.rpc.Help",
     "links": [
       {
         "description": "Google developers console API activation",
         "url": "https://console.developers.google.com/apis/api/compute.googleapis.com/overview?project=129446410799"
       }
     ]
   },
   {
     "@type": "type.googleapis.com/google.rpc.ErrorInfo",
     "domain": "googleapis.com",
     "metadata": {
       "consumer": "projects/129446410799",
       "service": "compute.googleapis.com"
     },
     "reason": "SERVICE_DISABLED"
   }
 ]
 , accessNotConfigured

   with google_compute_address.mcs-ip,
   on service01_gce.tf line 2, in resource "google_compute_address" "mcs-ip":
    2: resource "google_compute_address" "mcs-ip" {

リトライしろと言われたのでリトライで解決。
depends on が足りていないのかも?
API サービスを有効化するリソースがあった気がするので、そいつを depends on に加えたら解決するかも?(後ほど試す予定)

1.6.5. インシデントクローズ時にも通知チャンネルへの通知が走る

原因不明、謎。
実害はないが、気持ち悪いな…。
incidents01.png
incidents02.png

2
4
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4