GKEにkustomizeでデプロイするためのCI/CD環境

## 概要
この記事は[CI/CDアドベントカレンダー](https://qiita.com/advent-calendar/2019/ci-cd) 25日目の記事です。クリスマスに投稿する暇があるということはそういうことです。察してください。

GKEにkustomizeで簡単なwebサーバをデプロイするためのCI/CD環境を作ったので、それをまとめていきます。ついでにkustomizeあたりの部分もちょろっと。

解説は
**0. コンテナ化**
**1. CIの初期準備**
**2. イメージをGCRにプッシュ**
**3. kustomizeでmanifest作成**
**4. GKEにデプロイさせるCD作成**

この流れになってます。
実際に作成したものは[こちらのgithub](https://github.com/KatsuyaAkasaka/deploy-gke)に置いていますのでついでに確認してください。


## 0. コンテナ化

### Dockerfileの用意
ここは本質とずれるのでささっと
`http://localhost:8080/?name=hoge` というリクエストに対して`hello, hoge` と返してくれる簡単なものををgolangで用意しました。
マルチステージビルドさせるDockerfileもシュッと書きました。
docker-composeもないと生きていけない体になっているのでついでに。

```dockerfile

FROM golang:1.12 as builder

ENV GO111MODULE=on
ENV GOOS=linux
ENV GOARCH=amd64

COPY ./ /usr/local/deploy-gke
WORKDIR /usr/local/deploy-gke
RUN cd server && go build -o binfile
RUN chmod +x server/binfile

FROM alpine
ENV GOOS=linux
ENV GOARCH=amd64
EXPOSE 8080
COPY --from=builder /usr/local/deploy-gke/server/binfile ./
RUN mkdir /lib64
RUN ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
CMD ["/binfile"]
```


``` docker-compose.yaml
version: "3.7"
services:
  server:
    image: server
    build:
      context: ./
      dockerfile: ./server/Dockerfile
    container_name: server
    ports:
      - 8080:8080
```

一部Alpine環境でバイナリ実行時に怒られる部分があるのでシンボリックリンク張っています
参考:https://stackoverflow.com/questions/34729748/installed-go-binary-not-found-in-path-on-alpine-linux-docker

### ローカルの動作確認
これで`docker-compose build` からの `docker-compose up -d` でlocalhost:8080が意図した挙動をしていることが確認できました。

<img width="489" alt="スクリーンショット 2019-12-23 23.30.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178971/9fc6c40a-b3ff-4f79-ae86-11e064e97dda.png">


## 1. CIの初期準備
### CI用のサービスアカウント作成
CIがGKEにデプロイするイメージをGCRにプッシュさせるため、CI/CD用のサービスアカウントを作成します。
今後のデプロイも考えて、`ストレージ管理者` と `Kubernetes Engine管理者` の２つのロールを渡したSAを作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178971/d26f905d-c405-532a-b4e7-051de335b4a2.png)
発行したSAから吐き出された鍵(jsonファイル)の中身をコピって`Environmental Variables`に貼り付けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178971/2494d248-b820-43b1-897a-ac3126c3aae7.png)
ついでに使いそうなものも入れておきました。

これで`.circleci/config.yml`からこの環境変数を扱えるようになったので、`config.yml`を書いていきます。

ちなみに自分は`yaml`派です。皆さんもそうだと思います。
ただ、`config.yaml`で作成すると読み込んでくれません。断腸の思いで`config.yml`にしましょう。


## 2. イメージプッシュ
### config.yml作成
`config.yml`を書いてGCRにイメージをpushしていきます。
masterマージをトリガーにしてイメージプッシュとデプロイをcloud Buildに任せる方法もありますが、個人的には管理が分散しないのでcircleCIに一元管理させるほうが好きです。

``` config.yml
version: 2
references:
  setup_env: &setup_env
    name: Set Environment Variable
    command: |
      echo 'export GCP_PROJECT=${GCP_PROJECT}' >> $BASH_ENV
      echo 'export GO111MODUELE="on"' >> $BASH_ENV
      echo 'export IMAGE_PATH="asia.gcr.io/${GCP_PROJECT}/deploy-gke"' >> $BASH_ENV
      source $BASH_ENV
  install_dc: &install_dc
    name: Install docker-compose
    command: |
      curl -L https://github.com/docker/compose/releases/download/1.25.0/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
      chmod +x ~/docker-compose
      mv ~/docker-compose /usr/local/bin/docker-compose
  auth_gcloud: &auth_gcloud
    name: Authenticate gcloud
    command: |
      echo $CICD_SA > gcloud-service-key.json
      gcloud auth activate-service-account --key-file gcloud-service-key.json
      gcloud --quiet auth configure-docker
      gcloud --quiet config set project ${GCP_PROJECT}
      gcloud --quiet config set compute/zone asia-northeast1-a
      gcloud --quiet container clusters get-credentials deploy-gke
  build: &build
    working_directory: ~/deploy-gke
    name: docker-compose build
    command: docker-compose build
  push_image: &push_image
    name: push_image
    command: |
      docker tag server ${IMAGE_PATH}:${CIRCLE_SHA1}
      docker push ${IMAGE_PATH}:${CIRCLE_SHA1}
jobs:
  push_image:
    working_directory: ~/deploy-gke
    docker:
      - image: google/cloud-sdk
    steps:
      - checkout
      - setup_remote_docker
      - run: *setup_env
      - run: *install_dc
      - run: *auth_gcloud
      - run: *build
      - run: *push_image
workflows:
  version: 2
  build_and_push:
    jobs:
      - push_image
```

この`config.yml`で

```
setup_env : 環境変数をセット
install_dc : docker-composeをインストール
auth_gcloud: glcoudコマンドの認証 & 予め作成していたクラスタの設定
build: イメージ生成 & コミットハッシュでタグ付け
push_image: そのイメージをGCRにpush
```
を実行しています。

ちなみにdocker-composeをインストールする手順はcircleciが公開しています
https://circleci.com/docs/ja/2.0/docker-compose/
もちろん、すでにdocker-composeがインストールされているイメージを使用するのであればこの手順入りません。

これを実行すると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178971/3e315847-4b74-1091-ffa6-0ddaa5c8e542.png)

GCRにイメージがpushされているのが確認できました。

## 3. kustomizeでmanifest作成
続いて、デプロイするためにkustomizeでkubeのmanifestを作成していきます。

### kustomizeのディレクトリ構成
```
k8s
├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
├── manifest
│   └── production
│       └── output.yaml
└── overlays
    └── production
        ├── build.sh
        ├── deployment.yaml
        ├── kustomization.yaml
        └── service.yaml
```

ディレクトリの切り方は何でもいいですが、[公式の推奨](https://github.com/kubernetes-sigs/kustomize#2-create-variants-using-overlays)がこれなのでこれに則ります。
### kustomizeについて
ざっくりkustomizeについて説明すると、baseにあるyamlに対してoverlays以下のyamlを付け加えることで一つのマニフェストを吐き出せるヤツです。(ここらへんの指定はbuild.shで管理しています)
各環境ごとにディレクトリを切れば各環境のyamlを生成できるし、labelやイメージの設定などの共通項は`kustomization.yaml`に内包させることができます。

そのため、baseにあるyamlは全環境で共通の項目しか書かないので非常にコンパクトになります。

```base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-gke-deployment
```


また、この`kustomization.yaml`の内容は`kustomize edit` コマンドで変更することができるので、CI上で作成したimageのパスを `kustomize edit` コマンドに渡すことで、そのimageを取り込んだ`kustomization.yaml`を作成でき、それをもとにマニフェストを作成することができます。
今回はその仕組を使用してCD環境を構築します。
詳しくは[リポジトリ](https://github.com/KatsuyaAkasaka/deploy-gke)をみてください。


ちなみに今回は、type LoadBalancerのserviceとreplicasが1のDeploymentを作成しました。

```build.sh
#!/bin/sh
cd `dirname $0`

IMAGE_NAME="$1"
kustomize edit set image app-name="${IMAGE_NAME}"
kustomize build . > ../../manifest/production/output.yaml
```

この`build.sh`が何をやっているかというと、`./build.sh args:latest`を実行することで、さっきの説明の、
> そのimageを取り込んだ`kustomization.yaml`を作成でき、それをもとにマニフェストを作成することができます。

までを一気にやっています。

実行時の引数に`args:latest`を入れることで

```overlays/production/kustomize.yaml
...
images:
- name: app-name
  newName: args
  newTag: latest
```
のようにkustomization.yamlが書き換わり、
`kustomize build . > ../../manifest/production/output.yaml`
buildすることで
そのimageを用いたmanifestが`manifest/production/output.yaml`に生成されます。


ここまでくればあとは、CIで`./build.sh {前のジョブでプッシュしたイメージのパス}`を実行するだけです。

build.shですが、kubectlが1.14になってkustomizeコマンドが統合されたので、`kubectl -k` を使えば`kustomize edit`の操作がなくなっていいかも知れません。(GKEのデフォルトバージョンはまだ1.13なので注意)

## 4. GKEにデプロイさせるCDを作る

### デプロイ周りのconfig.yml
```config.yml
  apt-get_update: &apt-get_update
    name: apt-get update
    command: |
      apt-get update
      apt-get install -y wget
  install_kubectl: &install_kubectl
    name: install kubectl
    command: |
      curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
      mv kubectl /usr/local/bin
      chmod +x /usr/local/bin/kubectl
      kubectl version
  install_kustomize: &install_kustomize
    name: install kustomize
    command: |
      wget https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv3.5.1/kustomize_v3.5.1_linux_amd64.tar.gz
      tar xvzf kustomize_v3.5.1_linux_amd64.tar.gz
      mv kustomize /usr/local/bin
      chmod +x /usr/local/bin/kustomize
      kustomize version
  deploy: &deploy
    working_directory: ~/deploy-gke
    name: deploy gke
    command: |
      ./k8s/overlays/production/build.sh ${IMAGE_PATH}:${CIRCLE_SHA1}
      kubectl apply -f ./k8s/manifest/production/output.yaml
```

`kubectl`と`kustomize`をインストールさせるタスクを用意しました。
deployタスクでは先程説明したように、build.shに、引数にGCRのイメージパスを指定して実行します。
後は出力されたyamlをapplyして完成。

ちなみにjobとworkflowはこんな感じ

```yml

  deploy:
    working_directory: ~/deploy-gke
    docker:
      - image: google/cloud-sdk
    steps:
      - checkout
      - setup_remote_docker
      - run: *apt-get_update
      - run: *auth_gcloud
      - run: *install_kubectl
      - run: *install_kustomize
      - run: *deploy
workflows:
  version: 2
  build_and_push:
    jobs:
      - push_image
      - deploy:
          requires:
            - push_image
```

無事jobがすべて成功してserviceとdeploymentが作成されました。

<img width="944" alt="スクリーンショット 2019-12-23 23.15.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178971/8adef858-045f-a8f4-c021-5ac96673d016.png">

### 動作確認

serviceはtype LoadBalancerで作成したので、生成された外部IPアドレスにアクセスすると

<img width="618" alt="スクリーンショット 2019-12-23 23.18.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178971/1e7585d6-75b9-52a9-f3c3-761440dfec79.png">


無事動いていることが確認できました:tada:


## まとめ
kustomizeでCI/CDを実現するためのフローを舐めていきました。
結論[このgithub](https://github.com/KatsuyaAkasaka/deploy-gke)にコードがすべて詰まっているのでそっちを見てください。各章ごとにコミット分けています。
まさかり大歓迎です。


それではみなさん、良いお年を。