Terraform で GKE を構築する

# はじめに

GKE を構築する際、実際に適用されている設定を管理したいので Terraform を使って GKE を構築していきます。
Terraform には Module として GKE が提供されていますが、今回は素の Terraform で構築していきます。

本記事の作成に当たって [JapanContainerDays v18.12](https://containerdays.jp/) で企画された「showKs」の Terraform のリポジトリが非常に参考になりました。
ありがとうございます。

[GitHub - containerdaysjp/showks-terraform: Terraform configurations to provision showk environments.](https://github.com/containerdaysjp/showks-terraform)

他にもクラウドネイティブな要素の詰まったリポジトリがあるので非常に参考になると思います。

[Japan Container Days · GitHub](https://github.com/containerdaysjp)

それでは、Teraform を使って GKE の構築と Guestbook アプリケーションをデプロイして動作確認までを試してみます。

# 環境

- macOS High Sierra Version 10.13.6
- Terraform v0.11.13
- Homebrew 2.0.6
- Google Cloud SDK 240.0.0

# Google Cloud SDK をインストールする

以下のドキュメントを参考に Google Cloud SDK をインストールし、プロジェクトの設定をしてください。
[Google Cloud SDK documentation](https://cloud.google.com/sdk/docs/)

# API を有効にする

以下の API を有効にします。

- Kubernetes Engine API

以下のコマンドで API を有効にします。

```bash
$ gcloud services enable \
    container.googleapis.com
```

# Terraform のインストール

Terraform をインストールします。
今回は Mac 環境なので、Homebrew でインストールしていきます。

```bash
$ brew install terraform
```

# Terraform のサービスアカウントを作成する

Terraform が使用するサービスアカウントを作成します。

```bash
$ GCP_PROJECT=$(gcloud info --format='value(config.project)')
$ TERRAFORM_SA=terraform-service-account

$ gcloud iam service-accounts create $TERRAFORM_SA \
    --project=$GCP_PROJECT \
    --display-name $TERRAFORM_SA
```

作成した Terraform のサービスアカウントに以下のロールを付与します。

- `roles/iam.serviceAccountUser`
- `roles/compute.admin`
- `roles/storage.admin`
- `roles/container.clusterAdmin`

```bash
$ TERRAFORM_SA_EMAIL=$(gcloud iam service-accounts list \
    --project=$GCP_PROJECT \
    --filter="displayName:$TERRAFORM_SA" \
    --format='value(email)')

$ gcloud projects add-iam-policy-binding $GCP_PROJECT \
    --role roles/iam.serviceAccountUser \
    --member serviceAccount:$TERRAFORM_SA_EMAIL

$ gcloud projects add-iam-policy-binding $GCP_PROJECT \
    --role roles/compute.admin \
    --member serviceAccount:$TERRAFORM_SA_EMAIL

$ gcloud projects add-iam-policy-binding $GCP_PROJECT \
    --role roles/storage.admin \
    --member serviceAccount:$TERRAFORM_SA_EMAIL

$ gcloud projects add-iam-policy-binding $GCP_PROJECT \
    --role roles/container.clusterAdmin \
    --member serviceAccount:$TERRAFORM_SA_EMAIL
```

# .tfstate ファイルを管理する Google Cloud Storage のバケットを作成する

Terraform では、管理しているリソース状態を `.tfstate` という拡張子を持つファイルに管理します。
デフォルトではローカルに保存されますが、共有できないためリモートに管理します。
今回は、Google Cloud Storage（GCS）をリモート先として `.tfstate` を管理するため GCS バケットを作成します。
バケット名は、GCS 全体で一意である必要があることに注意してください。
サンプルとして `tf-state-prod` という名前で作成しています。

```bash
$ GCS_CLASS=multi_regional
$ GCS_BUCKET=tf-state-prod

$ gsutil mb -p $GCP_PROJECT -c $GCS_CLASS -l asia gs://$GCS_BUCKET/
```

# .tf ファイルを作成する

## 変数を定義する

各リソースで必要な変数を定義する `variables.tf` ファイルを作成します。
GKE のバージョンは、執筆時点（2019/03）の最新 `1.12.6-gke.7` を使用します。

```hcl:variables.tf
variable "project" {}

variable "cluster_name" {
  default = "cluster"
}

variable "location" {
  default = "asia-northeast1-a"
}

variable "network" {
  default = "default"
}

variable "primary_node_count" {
  default = "3"
}

variable "machine_type" {
  default = "n1-standard-1"
}

variable "min_master_version" {
  default = "1.12.6-gke.7"
}

variable "node_version" {
  default = "1.12.6-gke.7"
}
```

## GCP のプロバイダ情報を定義する

GCP のプロバイダを利用するに当たって `provider.tf` ファイルを作成します。

```hcl:provider.tf
provider "google" {
  project = "${var.project}"
  region  = "${var.location}"
}
```

## Backend を定義する

`.tfstate` ファイルを管理するための Backend を定義する `backend.tf` ファイルを作成します。
今回は、GCS を Backend に利用します。
バケット名は、事前に作成した GCS バケット名を指定します。

```hcl:backend.tf
terraform {
  backend "gcs" {
    bucket = "tf-state-prod"
    prefix = "terraform/state"
  }
}
```

## GKE を定義する

GKE を定義するための `gke.tf` ファイルを作成します。

```hcl:gke.tf
resource "google_container_cluster" "primary" {
  name     = "primary-${var.cluster_name}"
  location = "${var.location}"

  remove_default_node_pool = true
  initial_node_count       = 1

  network = "${var.network}"

  min_master_version = "${var.min_master_version}"
  node_version       = "${var.node_version}"

  master_auth {
    username = ""
    password = ""

    client_certificate_config {
      issue_client_certificate = false
    }
  }
}

resource "google_container_node_pool" "primary_nodes" {
  name       = "primary-${var.cluster_name}-nodes"
  location   = "${var.location}"
  cluster    = "${google_container_cluster.primary.name}"
  node_count = "${var.primary_node_count}"

  management {
    auto_repair = true
  }

  node_config {
    oauth_scopes = [
      "https://www.googleapis.com/auth/devstorage.read_only",
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
      "https://www.googleapis.com/auth/service.management.readonly",
      "https://www.googleapis.com/auth/servicecontrol",
      "https://www.googleapis.com/auth/trace.append",
    ]

    machine_type = "${var.machine_type}"

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

## GKE 作成

Terraform のサービスアカウントの json ファイルを作成します。

```bash
$ TERRAFORM_SA_DEST=~/.gcp/terraform-service-account.json

$ mkdir -p $(dirname $TERRAFORM_SA_DEST)

$ TERRAFORM_SA_EMAIL=$(gcloud iam service-accounts list \
    --filter="displayName:$TERRAFORM_SA" \
    --format='value(email)')

$ gcloud iam service-accounts keys create $TERRAFORM_SA_DEST \
    --iam-account $TERRAFORM_SA_EMAIL
```

作成したサービスアカウントの json ファイルのパスを `GOOGLE_APPLICATION_CREDENTIALS` の環境変数にセットします。

```bash
$ export GOOGLE_APPLICATION_CREDENTIALS=$TERRAFORM_SA_DEST
```

Terraform の設定を元に初期セットアップを行います。
この時点で `.tfstate` ファイルが GCS に展開されます。

```bash
$ terraform init
```

GKE を作成する前に `variables.tf` ファイルで default 値を定義していない値を環境変数にセットします。
default 値を定義していない変数はコマンドライン引数や `terraform.tfvars` ファイルから渡すことも可能です。
環境変数の場合は、変数名の prefix に `TF_VAR_` が必要です。

```bash
$ export TF_VAR_project=$GCP_PROJECT
```

まずは、`terraform plan` コマンドで変更内容の確認をします。

```bash
$ terraform plan
```

問題なければ `terraform apply` コマンドで GKE を作成します。
`yes` で実行します。

```bash
$ terraform apply
```

# Guestbook アプリケーションをデプロイして確認する

Terraform で作成した GKE に対して以下のリポジトリに存在する Guestbook アプリケーションをデプロイします。
[GitHub - kubernetes/examples: Kubernetes application example tutorials](https://github.com/kubernetes/examples)

デプロイする前に作成した GKE クラスタの認証情報を取得します。

```bash
$ gcloud container clusters get-credentials primary-cluster
```

## Redis master をセットアップする

Guestbook アプリケーションは Redis を使用しています。
Redis master にデータを書き込み、Redis worker からデータを読み込む構成です。

### Redis master のデプロイ

Redis master をデプロイします。

```bash
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook/redis-master-deployment.yaml
```

作成した Pod が `1/1 Running` になるまで待機します。

```bash
$ kubectl get po
NAME                            READY   STATUS              RESTARTS   AGE
redis-master-57fc67768d-75dlf   1/1     Running             0          22s
```

表示した Pod 名から Redis master のログが表示されていることを確認します。

```bash
$ kubectl logs -f redis-master-57fc67768d-75dlf
```

### Redis master のサービスを作成

Redis master のサービスを作成します。

```bash
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook/redis-master-service.yaml
```

Redis master のサービスが作成されたことを確認します。

```bash
$ kubectl get svc
NAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
kubernetes     ClusterIP   10.55.240.1     <none>        443/TCP    22m
redis-master   ClusterIP   10.55.245.172   <none>        6379/TCP   52s
```

## Redis worker をセットアップする

Redis worker は、複数のレプリカを作成し可用性を高めています。

### Redis worker のデプロイ

Redis worker をデプロイします。

```bash
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook/redis-slave-deployment.yaml
```

作成した Pod が `1/1 Running` になるまで待機します。

```bash
$ kubectl get po
NAME                            READY   STATUS    RESTARTS   AGE
redis-master-57fc67768d-75dlf   1/1     Running   0          15m
redis-slave-57f9f8db74-fx2d7    1/1     Running   0          14s
redis-slave-57f9f8db74-gwmgk    1/1     Running   0          14s
```

### Redis worker のサービスを作成

Redis master のサービスを作成します。

```bash
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook/redis-slave-service.yaml
```

Redis master のサービスが作成されたことを確認します。

```bash
$ kubectl get svc
NAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
kubernetes     ClusterIP   10.55.240.1     <none>        443/TCP    28m
redis-master   ClusterIP   10.55.245.172   <none>        6379/TCP   6m41s
redis-slave    ClusterIP   10.55.255.148   <none>        6379/TCP   12s
```

## Guestbook のフロントエンドアプリケーションをセットアップする

Guestbook のウェブサーバーを起動します。
フロントエンドは、PHP でできたアプリケーションです。
読み書きに応じて Redis master や Redis worker と通信します。

### フロントエンドアプリケーションのデプロイ

フロントエンドアプリケーションをデプロイします。

```bash
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook/frontend-deployment.yaml
```

作成した Pod が `1/1 Running` になるまで待機します。

```bash
$ kubectl get po -l app=guestbook -l tier=frontend
NAME                        READY   STATUS    RESTARTS   AGE
frontend-654c699bc8-4qjpc   1/1     Running   0          37s
frontend-654c699bc8-rqznb   1/1     Running   0          37s
frontend-654c699bc8-txsvq   1/1     Running   0          37s
```

### フロントエンドアプリケーションのサービスを作成

Guestbook のフロントエンドは外部に公開する必要があります。
そこで外部からリクエストできるようにサービスの設定を `type: LoadBalancer` に指定します。

以下のように `frontend-service.yaml` をダウンロードして、`type: LoadBalancer` のコメントアウトを解除、`type: NodePort` をコメントアウトします。

```bash
$ wget https://raw.githubusercontent.com/kubernetes/examples/master/guestbook/frontend-service.yaml
$ sed -i "" -e "s/# type: LoadBalancer/type: LoadBalancer/" frontend-service.yaml
$ sed -i "" -e "s/type: NodePort/# type: NodePort/" frontend-service.yaml
```

編集した `frontend-service.yaml` でサービスを作成します。

```bash
$ kubectl apply -f frontend-service.yaml
```

フロントエンドアプリケーションのサービスが作成されたことを確認します。
作成したサービスの `EXTERNAL-IP` が表示されるまで待機します。

```bash
$ kubectl get svc frontend
NAME       TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)        AGE
frontend   LoadBalancer   10.55.244.103   35.187.206.29   80:31533/TCP   47s
```

## Guestbook アプリケーションにアクセスする

`EXTERNAL-IP` の IP アドレスに対してブラウザから接続すると以下のような Guestbook アプリケーションが表示されます。

![Guestbook アプリケーション](https://qiita-image-store.s3.amazonaws.com/0/129299/4c3fe1be-f8c2-aba7-5a32-9551205c6504.png "Guestbook アプリケーション")

以上で Terraform で GKE を構築し、Guestbook アプリケーションが動作することを確認しました。

# 作成したリソースの削除

作成したリソースを削除します。

## Guestbook アプリケーションの削除

フロントエンドアプリケーションのサービスを削除します。

```bash
$ kubectl delete svc frontend
```

削除コマンド実行後サービスが削除されるまで時間が掛かるので、以下のコマンドで削除されるまで確認します。

```bash
$ gcloud compute forwarding-rules list
```

## GKE クラスタの削除

Terraform で作成した GKE クラスタを削除します。
`yes` で削除します。

```bash
$ terraform destroy
```

## GCS バケットの削除

GCS バケットを削除します。

```bash
$ gsutil rm -r gs://$GCS_BUCKET/
```

## サービスアカウントの削除

サービスアカウントに紐付けたロールを削除します。

```bash
$ gcloud projects remove-iam-policy-binding $GCP_PROJECT \
    --role roles/iam.serviceAccountUser \
    --member serviceAccount:$TERRAFORM_SA_EMAIL

$ gcloud projects remove-iam-policy-binding $GCP_PROJECT \
    --role roles/compute.admin \
    --member serviceAccount:$TERRAFORM_SA_EMAIL

$ gcloud projects remove-iam-policy-binding $GCP_PROJECT \
    --role roles/storage.admin \
    --member serviceAccount:$TERRAFORM_SA_EMAIL

$ gcloud projects remove-iam-policy-binding $GCP_PROJECT \
    --role roles/container.clusterAdmin \
    --member serviceAccount:$TERRAFORM_SA_EMAIL
```

サービスアカウントを削除します。
`y` で削除します。

```bash
$ gcloud iam service-accounts delete $TERRAFORM_SA_EMAIL
```

# 参考

[Provider: Google Cloud Platform - Terraform by HashiCorp](https://www.terraform.io/docs/providers/google/index.html)
[Terraform で GCP 環境を構築してみる | GMO アドパートナーズグループ TECH BLOG byGMO](https://techblog.gmo-ap.jp/2017/11/16/terraform%E3%81%A7gcp%E7%92%B0%E5%A2%83%E3%82%92%E6%A7%8B%E7%AF%89%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B/)
[Cloud Storage documentation | Cloud Storage](https://cloud.google.com/storage/docs/)
[Create a Guestbook with Redis and PHP | Kubernetes Engine Tutorials](https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook)
[GitHub - containerdaysjp/showks-terraform: Terraform configurations to provision showk environments.](https://github.com/containerdaysjp/showks-terraform)