GKEのデプロイパイプラインを構築する

# はじめに
GKE構築のパイプラインを作成したときのメモ書きです。
自分用の備忘録の側面が強いため、参考にさせて頂いたQiita記事と重複する部分が多いです。
今回はGKEの構築のツールとして「Terraform」、パイプラインの実行環境として「Github Actions」を利用しています。

# 環境
パイプライン上で利用する環境についてはソースコード内に記述されているため、ローカルの操作に必要なもののみ記述  
* macOS Catalina Version 10.15.4
* fish (利用シェル)
* homebrew 2.2.10
* Google Cloud SDK 272.0.0
* gsutil version: 4.46
* kubectl v1.15.5

# 前準備
## gcloudのインストール
homebrew を利用してインストールする。

```bash
$ brew cask install google-cloud-sdk

# 利用するシェルに応じたpathの設定
# fishの場合は、 ~/.config/fish/config.fshに以下の内容を記述
source /usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/path.fish.inc
```

## Kubernetes Engine APIを有効にする
```bash
gcloud services enable container.googleapis.com
```
[Kubernetes Engine APIのドキュメント](https://cloud.google.com/kubernetes-engine/docs/reference/rest)

# Google Cloud Storage
GKE構築にTerraformを使うため、stateファイルの置き場所としてGCSを利用する。  
バケット名はtf-state-locationとして作成する。  

```bash
# バケットのタイプと名前を環境変数に設定
$ set GCS_CLASS multi_regional
$ set GCS_BUCKET_NAME tf-state-location

# タイプと名前を引数にバケットを作成  
$ gsutil mb -p $GCP_PROJECT -c $GCS_CLASS -l us gs://$GCS_BUCKET_NAME/
```

# サービスアカウント
パイプラインの実行ユーザーとしてサービスアカウントを作成する。  

## サービスアカウントの作成
サービスアカウント名はgke-terraform-service-accountとして作成する。

```bash
# GCPにプロジェクト名を取得して環境変数に設定
$ set GCP_PROJECT (gcloud info --format='value(config.project)')

# サービスアカウント名を環境変数に設定
$ set TERRAFORM_SA gke-terraform-service-account

# プロジェクトと名前を引数にサービスアカウント を作成
$ gcloud iam service-accounts create $TERRAFORM_SA --project=$GCP_PROJECT --display-name $TERRAFORM_SA

# 作成したサービスアカウントのID情報を環境変数に設定(後の章利用する)
$ set TERRAFORM_SA_EMAIL (gcloud iam service-accounts list --project=$GCP_PROJECT --filter="displayName:$TERRAFORM_SA" --format='value(email)')
```

## サービスアカウントへの権限の付与
サービスアカウントが実行ユーザーとなるため、GCPに対する必要な権限を付与する。  

### GKE作成に関する権限の付与

```bash
$ 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

# 結果確認用
gcloud projects get-iam-policy $GCP_PROJECT \
    --flatten="bindings[].members" \
    --filter="bindings.members:serviceAccount:$TERRAFORM_SA_EMAIL" \
    --format='table(bindings.members,bindings.role)'
```

### GCSのバケットへの権限付与
パイプラインの中でstateファイル置き場として用意したgcsのバケットにアクセスする必要があるため、サービスアカウントその権限を付与する。  

```bash
# バケットに対するAdmin権限を付与する(serviceaccountがstorage.adminロールを持っているためやらなくても可)
$ gsutil iam ch serviceAccount:$TERRAFORM_SA_EMAIL:objectAdmin gs://$GCS_BUCKET_NAME/
```

### サービスアカウントのkeyファイル(json)の作成
Terraformを実行するときに必要な、サービスアカウントのkeyを作成する。  

```bash
# key作成場所のディレクトリ情報を環境変数に設定して、ディレクトリ作成
$ set TERRAFORM_SA_DEST ~/.gcp/terraform-service-account.json
$ mkdir -p (dirname $TERRAFORM_SA_DEST)

# サービスアカウントのkeyファイルを指定したディレクトリに作成
$ gcloud iam service-accounts keys create $TERRAFORM_SA_DEST --iam-account $TERRAFORM_SA_EMAIL
```

# GKEデプロイパイプラインの構築
この章で紹介するコードは以下のGitリポジトリ上で公開している。
参考にした公式ページ等の情報もreadmeに記述しています。
https://github.com/sabure500/gke-terraform

## Terraform
.tfファイルは以下のTerraform公式のGKE用のサンプルコードを参考に作成する。  
[google_container_cluster](https://www.terraform.io/docs/providers/google/r/container_cluster.html)  

### 環境変数の定義
Terraform内で利用する環境変数を定義する。

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

variable "cluster_name" {
  default = "my-gke-cluster"
}

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

variable "machine_type" {
  default = "f1-micro"
}

variable "preemptible_machine_type" {
  default = "e2-small"
}

variable "network" {
  default = "default"
}

variable "min_master_version" {
  default = "1.15.11-gke.3"
}

variable "node_version" {
  default = "1.15.11-gke.3"
}
```

### GCPのプロバイダ情報の定義
```hcl:provider.tf
provider "google" {
  version = "3.17.0"
  project = var.project
  region  = var.location
}
```

### Backend(tfstateファイルの保管場所)の定義
.tfstate ファイルを管理するための Backend を定義する。
バケット名は、事前に作成した GCS バケット名を指定する。

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

### GKEの定義
記述に関する詳細はGitリポジトリ内のreadmeを参照

```hcl:main.tf
terraform {
  required_version = "0.12.24"
}

resource "google_container_cluster" "primary" {
  name     = 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

  monitoring_service = "none"
  logging_service    = "none"

  master_auth {
    username = ""
    password = ""

    client_certificate_config {
      issue_client_certificate = false
    }
  }
}

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

  management {
    auto_repair = true
  }

  node_config {
    machine_type = var.machine_type
    oauth_scopes = [
      "https://www.googleapis.com/auth/devstorage.read_only",
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring"
    ]
  }
}

resource "google_container_node_pool" "primary_preemptible_nodes" {
  name       = "${var.cluster_name}-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 = var.preemptible_machine_type
    oauth_scopes = [
      "https://www.googleapis.com/auth/devstorage.read_only",
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
    ]
  }
}
```

## Github Actions
上記で作成した.tfファイルのTerraformによる実行はGithub Actionsを用いて行う。
Github ActionsによるTerraformの実行は以下の公式のサンプルを参考に作成する。
[Terraform Github Actions Get Started](https://www.terraform.io/docs/github-actions/getting-started.html)

### Test用のJobの作成
masterブランチ以外では、terraformのinit,validate,planまでを実行するJobを作成する。  

```yaml:terraform-test.yaml
name: 'terraform-test'
on:
  push:
    branches-ignore:
      - master

env:
  TERRAFORM_VERSION: 0.12.24
  TF_VAR_project: ${{secrets.PROJECT}}
  GOOGLE_CREDENTIALS: ${{secrets.GOOGLE_CREDENTIALS}}
  
jobs:
  terraform:
    name: 'terraform-test'
    runs-on: ubuntu-latest
    steps:
      - name: 'Checkout'
        uses: actions/checkout@v1
        
      - name: 'Terraform Format'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'fmt'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: 'Terraform Init'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'init'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: 'Terraform Validate'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'validate'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          
      - name: 'Terraform Plan'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'plan'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

### Apply用のJob
masterブランチにpushされた際に、terrafromのinit,applyを実行するJobを作成する。

```yaml
name: 'terraform-apply'
on:
  push:
    branches:
      - master
  # APIアクセスでイベント発火ができる条件
  repository_dispatch:
    types: [apply]

env:
  TERRAFORM_VERSION: 0.12.24
  TF_VAR_project: ${{secrets.PROJECT}}
  GOOGLE_CREDENTIALS: ${{secrets.GOOGLE_CREDENTIALS}}
  
jobs:
  terraform:
    name: 'terraform-apply'
    runs-on: ubuntu-latest
    steps:
      - name: 'Checkout'
        uses: actions/checkout@v1

      - name: 'Terraform Init'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'init'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: 'Terraform Plan'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'plan'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: 'Terraform Apply'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'apply'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

### クラスタ削除用のJob

このGKEクラスタは学習用に用意するため、GitにPushを行わずに気軽にクラスタを削除できるようにJobを用意しておく

```yaml:terraform-destroy.yaml
name: 'terraform-destroy'
on:
  # APIアクセスでイベント発火ができる条件
  repository_dispatch:
    types: [destroy]

env:
  TERRAFORM_VERSION: 0.12.24
  TF_VAR_project: ${{secrets.PROJECT}}
  GOOGLE_CREDENTIALS: ${{secrets.GOOGLE_CREDENTIALS}}
  
jobs:
  terraform:
    name: 'terraform-apply'
    runs-on: ubuntu-latest
    steps:
      - name: 'Checkout'
        uses: actions/checkout@v1
        
      - name: 'Terraform Init'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'init'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: 'Terraform Plan Destroy'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'plan'
          args: '-destroy -out=./destroy-plan'

      - name: 'Terraform Apply Destroy'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'apply'
          args: './destroy-plan'
```

このJobはパイプラインでは実行されず、以下のコマンドでGithubのAPIを直接叩くことで実行できる
(事前にGithubのrepo権限を持つPersonal access tokenを作成しておく必要がある)

```bash
curl -vv \
  -H "Authorization: token $PERSONAL_ACCESS_TOKEN" \
  -H "Accept: application/vnd.github.everest-preview+json" \
  "https://api.github.com/repos/sabure500/gke-terraform/dispatches" \
  -d '{"event_type": "destroy", "client_payload": {"target_brunch": "master"}}'
```
また、コマンドからdestroyを実行した時用に、Applyに関しても上記のコマンドのevent_typeをapplyにすることで実施できる

### Github Actions内で利用する環境変数の設定
上記で記述したJobでは、以下の2点を外部から受け取る環境変数として設定している。

* PROJECT : GCPのプロジェクト名
* GOOGLE_CREDENTIALS : サービスアカウントの章で作成したservice accountのKey  

これらはクレデンシャル情報のため、以下の手順でGithubのSecret機能を用いて登録する。  

1. Githubのリポジトリ選択画面にて、Settingsタブを選択する
1. Optionsの項目から、Secretsを選択する
1. Add a new secretから必要なsecretを作成する

### リポジトリ運用ルールの設定
上記で記述したJobでは、master以外のブランチでtestを実行してからmasterへpushすると実際にApplyするという流れになっている。
そのため、直接masterにpushを行われるとtestが実行されなくなってしまう。
それを防ぐために、masterへの直push禁止と、プルリクを行う際にtestJobが必ず通っている必要があるといったルールを作成する。  

1. Githubのリポジトリ選択画面にて、Settingsタブを選択する
1. Optionsの項目から、Branchesを選択する
1. Branch protection rule の Add ruleを選択する
1. Require status checks to pass before merging にチェックを入れ、Require branches to be up to date before mergingにもチェックを入れる
1. 表示されるJob名の中からmasterへのMergeの条件にしたいJobを選択する。(今回の場合はterraform-testを選択)  


# GKE構築結果の確認
## Clusterの確認
パイプラインから作成後に実際に構築できているかを確認する

```bash
# kubectl の操作対象を GKE に切り替える
gcloud container clusters get-credentials --zone us-west1-a my-gke-cluster

# 結果確認用
kubectl config current-context
```

## Sampleアプリの起動
構築したGKE上でサンプルアプリを稼働させる

```bash
# サンプルアプリ用のNamespace作成
kubectl create namespace sample

# サンプルアプリのデプロイ
kubectl -n sample create deployment hello-server --image=gcr.io/google-samples/hello-app:1.0

# サンプルアプリ公開用のservice作成
kubectl -n sample expose deployment hello-server --type=LoadBalancer --port 8080

# 外部IPの取得
kubectl -n sample get service
```

上記コマンドを実施後に、取得したIPで以下にアクセスする  
http://[EXTERNAL-IP]:8080

# 参考
* [Terraform で GKE を構築する](https://qiita.com/takasp/items/8104c175232e74217cc3)
* [サービス アカウントのドキュメント](https://cloud.google.com/iam/docs/service-accounts?hl=ja)
* [Overview of kubectl](https://kubernetes.io/docs/reference/kubectl/overview/)

# 追記
記事で書いたコードの内容だと、以下のエラーが出るようになりました。
```error creating NodePool: googleapi: Error 400: Node pools of f1-micro machines are not supported due to insufficient memory., badRequest
```
どうやらあるKubernetesのバージョン以降はf1-microはスペック不足で動かないみたいです。（今までも本当は無理だったけど制限されてなかったから作れていただけっぽい？）
自分が学習用で使う分にはe2-smallのNode１台で十分だったため、それだけ残してf1-microの部分はコメントアウトしました。
参考：https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture#memory_cpu