Terraformを使ってGKEクラスタを作成する

# 概要
あえてGoogle Cloud SDKを使わずにTerraformを使ってGKEクラスタを作成したので、そのときのメモ書きです。

# 前提
OS: macOS 10.14.4
Terraformバージョン: v0.11.13

# 事前知識
## Terraformとは
クラウドプロバイダ上(AWSやGCPなど)のインフラ構成を管理・自動化できるツール。
例えばGCP上にインスタンスを作成したい場合、インスタンスの種類や数などの設定をファイルに定義しておいてそれを実行すれば自動でインスタンスを作成できます。
IaaSだけでなくPaaSやCaaSなども可能。

- [Terraform by HashiCorp](https://www.terraform.io/)
- [10分で理解するTerraform - Qiita](https://qiita.com/Chanmoro/items/55bf0da3aaf37dc26f73)

## Kubernetesとは
[公式サイト](https://kubernetes.io/ja/)より。
> Kubernetes (K8s)は、デプロイやスケーリングを自動化したり、コンテナ化されたアプリケーションを管理したりするための、オープンソースのシステムです。

Kubernetesは奥が深いので詳しい説明はここでは割愛。

## GKEとは
Google Kubernetes Engine。
GCP上でKubernetes環境を利用できるようにしたマネージド型のサービス。
Web UI上から簡単に操作するだけでKubernetes環境を立ち上げることができます。
今回はその辺の操作をTerraformを使って自動化します。

- [Google Kubernetes Engine &nbsp;|&nbsp; Kubernetes Engine
      &nbsp;|&nbsp; Google Cloud](https://cloud.google.com/kubernetes-engine/?hl=ja)

# 今回作成するGKEクラスタ構成
料金を安く済ませることを軸に以下のように設定しました。

|設定項目|設定値|
|:-:|:-:|
|マシンタイプ|f1-micro|
|プリエンプティブノード数|2|
|通常ノード数|1|
|ゾーン|us-west1-a|

以下の記事を参考に算出。

- [Google Compute Engine の料金 &nbsp;|&nbsp; Compute Engine ドキュメント
      &nbsp;|&nbsp; Google Cloud](https://cloud.google.com/compute/pricing?hl=ja)
- [GKEで最安値のKubernetesクラスタを作る方法 | エンジニアの眠れない夜](https://sleepless-se.net/2018/12/11/gke-kubernetes/)
- [安価なGKE（k8s）クラスタを作って趣味開発に活用する - えいのうにっき](https://blog.a-know.me/entry/2018/06/17/220222)

これだけだとなぜこの設定値にしたのか不明なので少し解説。

## マシンタイプ
マシンタイプは安く済ませるためと、後述するGCPの[Always Free](https://cloud.google.com/free/?hl=ja)枠を使って1台インスタンスを稼働させるために最低ランクのf1-microにしました。

## ノードについて
今回は通常ノードと[プリエンプティブノード](https://cloud.google.com/compute/docs/instances/preemptible?hl=ja)の2種類を使ってクラスタを構成しました。
なぜ通常ノードだけではないかというと、プリエンプティブノードの料金が圧倒的に安いからです。
そもそもプリエンプティブノードとは、GCEの余ったリソースを使って立てられるインスタンスであり、他のタスクがリソースを必要とする場合は強制的にシャットダウンさせられます。そのため、使い勝手はあまり良くないです。
しかし、Kubernetesとして稼働させればKubernetesのオートヒーリング機能によって自動で復旧することができるので、そこまで不便に感じずに利用することができます。
ただし、全てプリエンプティブなノードにしてしまうと、全ノードが落ちたときに復旧できなくなるので、1台は常に稼働している通常ノードを確保しています。
そして、f1-microであればGCPの[Always Free](https://cloud.google.com/free/?hl=ja)枠で常に1台無料で利用できるため、通常ノードの1台をこれにあてています。
したがって今回はこのようなプリエンプティブなノードと通常ノードでの構成となりました。

## ゾーン
GCPの[Always Free](https://cloud.google.com/free/?hl=ja)枠を利用するためにus-west1-aを指定します。

# 実際にやってみた
## プロジェクトの作成
GCP上でプロジェクトを作成します。
そして発行されるプロジェクトIDを控えます。

## Terraformのインストール
homebrewでインストールできるのでインストールします。

```
$ brew install terraform
```

## 設定ファイルの作成
GCP上にGKEクラスタを作成する設定内容をファイルに書いていきます。
Terraformでは`*.tf`という拡張子ファイルに設定を記述していきます。

### プロバイダの設定


```
provider "google" {
  project = "<プロジェクトID>"
  region  = "us-west1"
  zone    = "us-west1-a"
}
```

設定を適用するクラウドプロバイダの設定を記述します。
今回はGCPの設定を書きます。
ここで設定する項目はデフォルトの設定値となるため、後述するリソースに設定されていれば書かなくても問題はないです。

|設定項目|内容|
|:-:|:-:|
|project|適用するデフォルトのプロジェクトID|
|region|適用するデフォルトのリージョン|
|zone|適用するデフォルトのゾーン|

- [Getting Started with the Google provider - Terraform by HashiCorp](https://www.terraform.io/docs/providers/google/getting_started.html)
- [Provider: Google Cloud Platform - Terraform by HashiCorp](https://www.terraform.io/docs/providers/google/index.html)

### 変数の設定
後述するリソースの設定で共通の設定値が出てくるので先に変数化しておきます。
Terraformではvariableブロックを使って変数を定義します。

```
variable "location" {
  type = "string"
  default = "us-west1-a"
}
```

変数を呼び出すときは`var.<変数名>`という形で呼び出すことができます。

- [Input Variables - Configuration Language - Terraform by HashiCorp](https://www.terraform.io/docs/configuration/variables.html)

### リソースの設定
クラスタ、通常ノード、プリエンプティブノードの3つの設定を書きます。

#### クラスタ
[google_container_cluster](https://www.terraform.io/docs/providers/google/r/container_cluster.html)リソースで設定します。

```
resource "google_container_cluster" "primary" {
  name     = "my-gke-cluster"
  location = "${var.location}"

  remove_default_node_pool = true
  initial_node_count = 1

  master_auth {
    username = ""
    password = ""
  }
}
```

|設定項目|内容|型|必須 or 任意|
|:-:|:-:|:-:|:-:|
|name|クラスタ名|string|必須|
|location|マスタクラスタのゾーンもしくはリージョンを指定<br>ゾーンの場合は1ゾーンのみでクラスタが作成される<br>リージョンの場合はその中の複数ゾーン跨る形でクラスタが作成される|string|任意|
|remove_default_node_pool|trueの場合クラスタ作成時にデフォルトのノードプールを削除する|bool|任意|
|initial_node_count|デフォルトのノードプールに作成されるノード数<br>`remove_default_node_pool`をtrueにした場合は1以上を設定する必要がある|number|任意|
|master_auth.username|Kubernetes masterにアクセスするときのbasic認証に使用するユーザ名を指定する|string|任意|
|master_auth.password|Kubernetes masterにアクセスするときのbasic認証に使用するパスワードを指定する|string|任意|

#### 通常ノード
[google_container_node_pool](https://www.terraform.io/docs/providers/google/r/container_node_pool.html)リソースで設定します。

```
resource "google_container_node_pool" "primary_nodes" {
  name       = "my-gke-node-pool"
  location   = "${var.location}"
  cluster    = "${google_container_cluster.primary.name}"
  node_count = 1

  management {
    auto_repair = true
  }

  node_config {
    machine_type = "f1-micro"

    metadata {
      disable-legacy-endpoints = "true"
    }

    oauth_scopes = [
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
    ]
  }
}
```

|設定項目|内容|型|必須 or 任意|
|:-:|:-:|:-:|:-:|
|name|ノードプール名|string|任意|
|cluster|ノードプールを作成するクラスタ名|string|必須|
|location|クラスタが存在するリージョンもしくはゾーンを指定|string|任意|
|node_count|ノード数|number|任意|
|management.auto_repair|trueの場合ノードが自動修復される|bool|任意|
|node_config.machine_type|GCEのマシンタイプを指定|string|任意|
|node_config.metadata|クラスタ内のインスタンスに渡すメタデータをkey/value形式で指定する|object|任意|
|node_config.oauth_scopes|デフォルトサービスアカウントが利用できるGoogle APIのスコープを書く|list|任意|

#### プリエンプティブノード
[google_container_node_pool](https://www.terraform.io/docs/providers/google/r/container_node_pool.html)リソースで設定します。
設定項目はほとんど通常ノードと同じですが、唯一`node_config.preemtpible`項目をtrueに設定しています。

```
resource "google_container_node_pool" "primary_preemptible_nodes" {
  name       = "my-gke-preemptible-node-pool"
  location   = "${var.location}"
  cluster    = "${google_container_cluster.primary.name}"
  node_count = 2

  management {
    auto_repair = true
  }

  node_config {
    preemptible  = true
    machine_type = "f1-micro"

    metadata {
      disable-legacy-endpoints = "true"
    }

    oauth_scopes = [
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
    ]
  }
}
```

|設定項目|内容|型|必須 or 任意|
|:-:|:-:|:-:|:-:|
|node_config.preemptible|trueの場合プリエンプティブなノードとなる<br>デフォルトはfalse|bool|任意|

### 最終的なtfファイル

```tf:gke.tf
provider "google" {
  project = "<プロジェクトID>"
  region  = "us-west1"
  zone    = "us-west1-a"
}

variable "location" {
  type = "string"
  default = "us-west1-a"
}

resource "google_container_cluster" "primary" {
  name     = "my-gke-cluster"
  location = "${var.location}"

  remove_default_node_pool = true
  initial_node_count = 1

  master_auth {
    username = ""
    password = ""
  }
}

resource "google_container_node_pool" "primary_nodes" {
  name       = "my-gke-node-pool"
  location   = "${var.location}"
  cluster    = "${google_container_cluster.primary.name}"
  node_count = 1

  management {
    auto_repair = true
  }

  node_config {
    machine_type = "f1-micro"

    metadata {
      disable-legacy-endpoints = "true"
    }

    oauth_scopes = [
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
    ]
  }
}

resource "google_container_node_pool" "primary_preemptible_nodes" {
  name       = "my-gke-preemptible-node-pool"
  location   = "${var.location}"
  cluster    = "${google_container_cluster.primary.name}"
  node_count = 2

  management {
    auto_repair = true
  }

  node_config {
    preemptible  = true
    machine_type = "f1-micro"

    metadata {
      disable-legacy-endpoints = "true"
    }

    oauth_scopes = [
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
    ]
  }
}
```

## サービスアカウントの作成
TerraformがGCP APIを叩く用のサービスアカウントを[こちら](https://console.cloud.google.com/apis/credentials/serviceaccountkey?_ga=2.76612949.-1747551614.1537171419)から作成します。
作成後、JSONファイルをダウンロードし、パスを環境変数にセットします。

```zsh:.zshrc
export GOOGLE_CLOUD_KEYFILE_JSON={{path}}
```

- [Getting Started with the Google provider - Terraform by HashiCorp](https://www.terraform.io/docs/providers/google/getting_started.html#adding-credentials)

## 初期化
リソースを操作するためのプラグインを導入するために最初だけ初期化をしてあげる必要があります。
tfファイルがある配下で以下コマンドを実行します。

```bash
$ terraform init
```

すると、直下に`.terraform`ディレクトリが作成されます。これが作成されれば初期化は完了です。

## 実行計画
実際に適用する時のdry run的なものを確認できます。

```bash
$ terraform plan
```

実行すると以下のように表示されるので、これを見て実行内容が正しいか確認します。

```
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + google_container_cluster.primary
      id:                                              <computed>
      additional_zones.#:                              <computed>
      addons_config.#:                                 <computed>
      cluster_autoscaling.#:                           <computed>
      cluster_ipv4_cidr:                               <computed>
      enable_binary_authorization:                     <computed>
      enable_kubernetes_alpha:                         "false"
      enable_legacy_abac:                              "false"
      enable_tpu:                                      <computed>
      endpoint:                                        <computed>
      initial_node_count:                              "1"
      instance_group_urls.#:                           <computed>
      ip_allocation_policy.#:                          <computed>
      location:                                        "us-west1-a"
      logging_service:                                 <computed>
      master_auth.#:                                   "1"
      master_auth.0.client_certificate:                <computed>
      master_auth.0.client_key:                        <computed>
      master_auth.0.cluster_ca_certificate:            <computed>
      master_version:                                  <computed>
      monitoring_service:                              <computed>
      name:                                            "my-gke-cluster"
      network:                                         "default"
      network_policy.#:                                <computed>
      node_config.#:                                   <computed>
      node_locations.#:                                <computed>
      node_pool.#:                                     <computed>
      node_version:                                    <computed>
      project:                                         <computed>
      region:                                          <computed>
      remove_default_node_pool:                        "true"
      zone:                                            <computed>

  + google_container_node_pool.primary_nodes
      id:                                              <computed>
      cluster:                                         "my-gke-cluster"
      initial_node_count:                              <computed>
      instance_group_urls.#:                           <computed>
      location:                                        "us-west1-a"
      management.#:                                    "1"
      management.0.auto_repair:                        "true"
      management.0.auto_upgrade:                       "false"
      max_pods_per_node:                               <computed>
      name:                                            "my-gke-node-pool"
      name_prefix:                                     <computed>
      node_config.#:                                   "1"
      node_config.0.disk_size_gb:                      <computed>
      node_config.0.disk_type:                         <computed>
      node_config.0.guest_accelerator.#:               <computed>
      node_config.0.image_type:                        <computed>
      node_config.0.local_ssd_count:                   <computed>
      node_config.0.machine_type:                      "f1-micro"
      node_config.0.metadata.%:                        "1"
      node_config.0.metadata.disable-legacy-endpoints: "true"
      node_config.0.oauth_scopes.#:                    "2"
      node_config.0.oauth_scopes.1277378754:           "https://www.googleapis.com/auth/monitoring"
      node_config.0.oauth_scopes.172152165:            "https://www.googleapis.com/auth/logging.write"
      node_config.0.preemptible:                       "false"
      node_config.0.service_account:                   <computed>
      node_count:                                      "1"
      project:                                         <computed>
      region:                                          <computed>
      version:                                         <computed>
      zone:                                            <computed>

  + google_container_node_pool.primary_preemptible_nodes
      id:                                              <computed>
      cluster:                                         "my-gke-cluster"
      initial_node_count:                              <computed>
      instance_group_urls.#:                           <computed>
      location:                                        "us-west1-a"
      management.#:                                    "1"
      management.0.auto_repair:                        "true"
      management.0.auto_upgrade:                       "false"
      max_pods_per_node:                               <computed>
      name:                                            "my-gke-preemptible-node-pool"
      name_prefix:                                     <computed>
      node_config.#:                                   "1"
      node_config.0.disk_size_gb:                      <computed>
      node_config.0.disk_type:                         <computed>
      node_config.0.guest_accelerator.#:               <computed>
      node_config.0.image_type:                        <computed>
      node_config.0.local_ssd_count:                   <computed>
      node_config.0.machine_type:                      "f1-micro"
      node_config.0.metadata.%:                        "1"
      node_config.0.metadata.disable-legacy-endpoints: "true"
      node_config.0.oauth_scopes.#:                    "2"
      node_config.0.oauth_scopes.1277378754:           "https://www.googleapis.com/auth/monitoring"
      node_config.0.oauth_scopes.172152165:            "https://www.googleapis.com/auth/logging.write"
      node_config.0.preemptible:                       "true"
      node_config.0.service_account:                   <computed>
      node_count:                                      "2"
      project:                                         <computed>
      region:                                          <computed>
      version:                                         <computed>
      zone:                                            <computed>


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

------------------------------------------------------------------------
```

## 実行
実際に適用します。
途中で`Do you want to perform these actions?`と聞かれるので問題ない場合は`yes`と答えます。

```bash
$ terraform apply
```

最後に以下のように表示されれば成功です。

```
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
```

# 補足情報
## tfstateファイルについて
Terraformは`*.tfstate`という拡張子のファイルで現在の環境を認識します。
そのため、tfstateファイルは削除すると実際の環境情報と差異が出てしまうので**絶対に削除してはいけません**。
tfstateファイルはクラウドストレージなどリモートで保管することが推奨されています。
GCSで管理するやり方を[こちら](https://qiita.com/kawakawaryuryu/items/58d8afbb21155c2e9572)にまとめてあります。

- [State - Terraform by HashiCorp](https://www.terraform.io/docs/state/purpose.html)

## リソースの破棄
リソースの破棄は以下コマンドで実現できます。

```bash
$ terraform destroy
```

dry run的に確認する場合は以下で実現できます。

```
$ terraform plan -destroy
```

## tfファイルのフォーマットに関するvimプラグイン
tfファイルをvimで修正する場合、以下プラグインを入れるとフォーマットや色付けがされて便利です。
[hashivim/vim-terraform: basic vim/terraform integration](https://github.com/hashivim/vim-terraform)

以上