Skaffoldを利用してGKEクラスタでの高速Docker開発を試す

# GKE (Google Kubernetes Engine) とは

Kubernetes (k8sと略す) という、Dockerコンテナのオーケストレーションツールが広く使われ始めています。
k8sをGCPのマネージドサービスとして提供されているものがGKEです。

当社では、GKEを2年以上本番運用しています。

当社の技術基盤に興味がある方は、詳しくはまず下記 Tech Blog の記事を参照いただければ幸いです。
https://allabout-tech.hatenablog.com/entry/2017/12/08/080800

# Skaffoldとは
CI/CDワークフローを高速に回すk8s用のツールです。
ローカルで開発したアプリケーション用コードを、k8sクラスタにデプロイするまでの、
いずれの段階においても自動化を行ってくれるものです。

今はアルファからベータ版になったところですので、本番で実用できるのはまだ先かと思います。
https://github.com/GoogleContainerTools/skaffold

Docker環境で開発を行う際にいつも問題になるのが、コードを書いてから
サーバへ成果物を反映するまでの手順が、非常に煩雑だということです。

これには、以下のような手順を踏む必要があります。

```
・Dockerイメージをビルドしてコンテナを作成
・コンテナイメージをDockerリポジトリにプッシュする
・リポジトリのイメージをサーバにデプロイする
```

Skaffoldでは、`skaffold dev`という開発用コマンドを1つ使うだけで、
上記の3ステップが自動実行されます。

# 本項での紹介内容
当社は現在、CI/CDツールの見直しを検討しております。
その一環として、Skaffoldを少し触ってみました。

基本的には公式のチュートリアルを少し変えてなぞりつつ、GKEのローンチを含めて
サービス用のクラスタを想定し、当社の運用ノウハウを一部取り入れながら、
ハンズオン形式で紹介していきます。

本項では、以下のことの確認を目指します。

`git cloneしてきた想定の開発リポジトリのコードを変更すると、
開発GKEクラスタにコード変更結果が反映される。`

# ローカル作業環境
MacまたはLinuxを想定しています。

利用できるGCPアカウントがあり、Googleログイン認証されて、
そちらが使える状態となっている必要があります。
また、GKEに関する管理者権限/GCRへの開発者権限が必要です。

ビルド時に必要ですので、Dockerをインストール/起動して下さい。
こちらは、マルチステージビルドに対応したバージョンでないといけません。

また、Google Cloud SDK(コマンドベースの管理ツール)もインストールして下さい。
https://cloud.google.com/sdk/docs/?hl=ja

こちらをインストールすることで、`gcloud`や`kubectl`といった
GKE運用に必要なコマンドが利用可能になります。

# ハンズオン！

### 安定版のSkaffoldコマンドをインストール

- MacならHomebrewが簡単！

```
$ brew install skaffold
==> Installing dependencies for git: gettext and pcre2
==> Installing git dependency: gettext
==> Downloading https://homebrew.bintray.com/bottles/gettext-0.19.8.1.el_capitan.bottle.tar.gz
Updating Homebrew...
```

- Linuxならバイナリを配置！

```
$ curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64
$ chmod +x skaffold
$ sudo mv skaffold /usr/local/bin
```

### GKEクラスタの準備

- ローカル環境で参照するGCPプロジェクトの向け先を変えます  
→　下記では、test01-01というプロジェクトIDと、台湾リージョンのbゾーンを選択する指定を行なっています。

```
$ export PROJECT_ID=test01-01
$ gcloud config set project ${PROJECT_ID}
$ gcloud config set compute/zone asia-east1-b
```

- コマンドでGKEを構築します  
→　下記では、標準的インスタンスでクラスタ数が2、k8sのバージョンが1.9.7のクラスタが作成されます。  
　　クラスタ名は`skaffold-test`。

```
$ gcloud container clusters create skaffold-test \
--machine-type=n1-standard-1 --num-nodes=2 --node-version=1.9.7
```

- 構築したGKEを、skaffold作業の向け先にします

```
$ gcloud config set container/cluster skaffold-test
$ gcloud container clusters get-credentials skaffold-test
```

- 構築したノードの状態を確認  
→　名前は、ハッシュやGCP側で付加される修飾子がつくので長くなります。 

```
$ kubectl get nodes
NAME                                           STATUS    ROLES     AGE       VERSION
gke-skaffold-test-default-pool-7cc93770-fvdx   Ready     <none>    21h       v1.9.7-gke.11
gke-skaffold-test-default-pool-7cc93770-x5bv   Ready     <none>    21h       v1.9.7-gke.11
```

### skaffoldするためのyaml準備

- 適当な作業ディレクトリにskaffoldをクローンして、デプロイ元を作ります  
→　getting-startedというディレクトリにチュートリアルが入っているので、  
　　主旨に沿うように変更していきます。

```
$ git clone https://github.com/GoogleContainerTools/skaffold
$ cd skaffold/examples/getting-started
$ ls -l
total 40
-rw-r--r--  1 numa_numa  staff  144 12 12 19:30 Dockerfile
-rw-r--r--  1 numa_numa  staff  734 12 12 19:30 README.adoc
-rw-r--r--  1 numa_numa  staff  153 12 12 19:30 k8s-pod.yaml
-rw-r--r--  1 numa_numa  staff  128 12 12 19:30 main.go
-rw-r--r--  1 numa_numa  staff  158 12 12 19:30 skaffold.yaml
```

・Dockerfile
　→　変更しません、参考までに内容を記載しておきます。

```Dockerfile
FROM golang:1.10.1-alpine3.7 as builder
COPY main.go .
RUN go build -o /app main.go

FROM alpine:3.7
CMD ["./app"]
COPY --from=builder /app .
```

・k8s-pod.yaml
　→　消してください。単一のpod(サーバのコンテナ)をデプロイするための設定内容になっています。
　　　今回は当社の本番構成を模したような環境としたいので、**podの冗長(クラスタ)**を実装します。

・main.go
　→　Global IPを付与し、ブラウザで文字列が確認できる最低限の内容に変えておきます。
　　　ブラウザ上に`Hello, World!`を表示します。

```main.go
package main

import (
  "fmt"
  "net/http"
)

func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, World!")
}
```

・skaffold.yaml
　→　skaffoldのメインコンフィグです。GCR(Google Container Registry)をイメージの格納先とするよう、
　　　内容を変更しておきます。

```skaffold.yaml
apiVersion: skaffold/v1beta1
kind: Config
build:
  artifacts:
  - image: asia.gcr.io/test01-01/skaffold-example
deploy:
  kubectl:
    manifests:
      - k8s-*
profiles:
  - name: gcb
    build:
      googleCloudBuild:
        projectId: test01-01
```

・k8s-deployment.yaml
　→　新規作成します。k8s-pod.yamlの代わりであり、クラスタ構成のWEBサーバpodを構築します。
　　　当社の本番運用しているyamlを、最低限まで単純化したような内容としました。
　　　通常、本番用のdeployment.yamlでは、podが持つべきCPU/Memのリソースなども定義します。

```k8s-deployment.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: go-pod
spec:
  replicas: 2
  revisionHistoryLimit: 5
  template:
    metadata:
      labels:
        name: go-pod
    spec:
      containers:
      - name: go-pod-container
        image: asia.gcr.io/test01-01/skaffold-example
        ports:
        - containerPort: 8080
```

・k8s-service.yaml
　→　新規作成します。podが利用するport情報を定義します。
　　　こちらも、当社の本番運用しているyamlを、最低限まで単純化したような内容としました。

```k8s-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: go-service
spec:
  type: NodePort
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    name: go-pod
```


・k8s-ingress.yaml
　→　新規作成します。現状、当社では利用していないリソースです。
　　　Ingressは、Serviceに関連付けるロードバランサを自動作成し、サービス公開してくれるリソースです。
　　　当社ではGLB(Google Load Balancer)を使用していますが、今回はWEB画面がすぐに確認できるよう、
　　　最も単純なものを書きました。

```k8s-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: go-ingress
spec:
    backend:
      serviceName: go-service
      servicePort: 8080
```

### skaffold devをためす！

- ためす前に必要な1ステップ

```
$ echo {} > ~/.docker/config.json
```

上記のコマンドを打ちましょう。
この点、かなりハマりました…

Skaffoldは標準でDockerhubの認証を行うようで、その認証設定を保持しておくのがこのファイルです。
今回はGoogleの認証を使用して、イメージのプッシュ先としてもGCR(Google Container Registry)を
使用するので、このファイルは実質不要です。

しかし、json形式と認識され、かつファイルが存在する状態でないと、skaffoldは以下のようなエラー状態に！

```
$ skaffold dev
2018/12/12 11:46:15 Unable to read "/Users/numa_numa/.docker/config.json": open /Users/numa_numa/.docker/config.json: no such file or directory
2018/12/12 11:46:17 Unable to read "/Users/numa_numa/.docker/config.json": open /Users/numa_numa/.docker/config.json: no such file or directory
```

- skaffold dev

```
$ skaffold dev
Starting build...
Building [asia.gcr.io/test01-01/skaffold-example]...
Sending build context to Docker daemon  3.072kB
Step 1/6 : FROM golang:1.10.1-alpine3.7 as builder
 ---> 52d894fca6d4
Step 2/6 : COPY main.go .
 ---> 6eb6ec18c4f5
Step 3/6 : RUN go build -o /app main.go
 ---> Running in f48712f45d21
 ---> d5b140e4c533
Step 4/6 : FROM alpine:3.7
3.7: Pulling from library/alpine
c67f3896b22c: Pulling fs layer
c67f3896b22c: Verifying Checksum
c67f3896b22c: Download complete
c67f3896b22c: Pull complete
Digest: sha256:a52b4edb6240d1534d54ee488d7cf15b3778a5cfd0e4161d426c550487cddc5d
Status: Downloaded newer image for alpine:3.7
 ---> 34ea7509dcad
Step 5/6 : CMD ["./app"]
 ---> Running in 344b58b3b879
 ---> 2224d665c6f9
Step 6/6 : COPY --from=builder /app .
 ---> e48445fc4fc5
Successfully built e48445fc4fc5
Successfully tagged 559b2b15a7fb390531d89955043fdef7:latest
The push refers to repository [asia.gcr.io/test01-01/skaffold-example]
e55d98a8a253: Preparing
ebf12965380b: Preparing
ebf12965380b: Layer already exists
e55d98a8a253: Pushed
19097cfc-dirty-e48445f: digest: sha256:0381c9fb158f36984988e3bcd90d00538b323d6d7fead0d27cbe976cd1c97a9d size: 739
Build complete in 19.327532727s
Starting test...
Test complete in 6.753µs
Starting deploy...
kubectl client version: 1.10
kubectl version 1.12.0 or greater is recommended for use with skaffold
deployment.apps "go-pod" created
ingress.extensions "go-ingress" created
service "go-service" created
Deploy complete in 1.962841729s
Watching for changes every 1s...
Port Forwarding go-pod-f8f84fd5f-lqrjx 8080 -> 8080
Port Forwarding go-pod-f8f84fd5f-f7gsv 8080 -> 8080
```

ローカルのkubectlのバージョンが低く怒られていますが、実行は問題ないようです。

- サービス状態の確認  
→　podが生存し、サービスポートが設定され、IngressにGlobal IPが割り当てられていることを確認。

```
$ kubectl get pod,svc,ingress
NAME                         READY     STATUS    RESTARTS   AGE
pod/go-pod-5875ffdc6-7v54b   1/1       Running   0          4m
pod/go-pod-5875ffdc6-frpxl   1/1       Running   0          4m

NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
service/go-service   NodePort    10.59.243.30   <none>        8080:31194/TCP   55m
service/kubernetes   ClusterIP   10.59.240.1    <none>        443/TCP          21h

NAME                            HOSTS     ADDRESS        PORTS     AGE
ingress.extensions/go-ingress   *         35.244.132.7   80        55m
```

- WEB画面を見てみる  
→　ブラウザで見てみます…
![WEB_01.png](https://qiita-image-store.s3.amazonaws.com/0/147015/6de9c446-4b83-74bc-ca45-1f7e420c78fe.png)

見事、サービスデプロイが行えています！！

## コードを変更してみる

getting-startedディレクトリは、gitからチェックアウトしてきたアプリケーションリポジトリを模しています。
Docker開発では、リポジトリ内にデプロイ用のマニュフェストファイル(yaml)を持ちます。

ただ、ここでのいわゆるプログラムコードはmain.goなので、こちらを変更することで本項の目的である
Docker開発が自動化されていることを確認します。

- main.goの変更  
→　下記のように表示文字列を変えてみましょう。  
　　skaffold devを実行した画面とは別にターミナルを立ち上げて作業します。

```main.goの変更
func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, World!")
}

　↓　↓　↓　↓　↓

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, All About!")
}
```

これを保存すると、Skaffoldはコードの変更を自動検知し下記のような画面出力が追加されます。

```
Starting build...
Building [asia.gcr.io/test01-01/skaffold-example]...
Sending build context to Docker daemon  3.072kB
Step 1/6 : FROM golang:1.10.1-alpine3.7 as builder
 ---> 52d894fca6d4
Step 2/6 : COPY main.go .
 ---> e3025641eb41
Step 3/6 : RUN go build -o /app main.go
 ---> Running in 4d01949d36cd
 ---> a33cf4369023
Step 4/6 : FROM alpine:3.7
 ---> 34ea7509dcad
Step 5/6 : CMD ["./app"]
 ---> Using cache
 ---> 2224d665c6f9
Step 6/6 : COPY --from=builder /app .
 ---> 89f696d81aeb
Successfully built 89f696d81aeb
Successfully tagged 24552841ff2fd2e063dbc6f46de39c89:latest
The push refers to repository [asia.gcr.io/test01-01/skaffold-example]
1264c561e5c7: Preparing
ebf12965380b: Preparing
ebf12965380b: Layer already exists
1264c561e5c7: Pushed
19097cfc-dirty-89f696d: digest: sha256:c7270a126fd8d5db4e2a383a6c2c758a7dfdb0c502dfbdf70e8f4e36d4a9e3d4 size: 739
Build complete in 15.270532362s
Starting test...
Test complete in 7.824µs
Starting deploy...
kubectl client version: 1.10
kubectl version 1.12.0 or greater is recommended for use with skaffold
deployment.apps "go-pod" configured
Deploy complete in 4.216971279s
Watching for changes every 1s...
Port Forwarding go-pod-5cf4d974bc-dj9gg 8080 -> 8080
Port Forwarding go-pod-5cf4d974bc-k4scw 8080 -> 8080
```

- 再度、WEB画面を見てみる  
→　コマンドで再度サービス状態を確認してから見てみます…

![WEB_02.png](https://qiita-image-store.s3.amazonaws.com/0/147015/04df0d37-28e3-d68d-d6b4-314a33c8c9b3.png)

podへの更新デプロイが行えているようです。

このツールを使えば、開発コードへ変更を加える場合に、**ビルド・プッシュ・デプロイという手順なし**に、
開発内容をほぼリアルタイムに確認できそうです。

### ステージングや本番へのデプロイの想定

`skaffold dev`はあくまで開発環境の確認用ですので、[Ctrl]+[C]でプロセスを停止すると、
同時にpodは消滅してしまいます。

消滅しないようにpodデプロイを行う場合は、

```
$ skaffold run
```

を使用します。

なお、GCRにイメージプッシュを行う場合に -t オプションを付けることで任意のタグ付けも可能です。
(skaffold devで変更を繰り返した場合は、イメージにはランダムなハッシュをもつタグが付きます。)

```
$ skaffold run -t atonce
```

※GCRのWEB画面から参照したプッシュイメージ
![GCR.png](https://qiita-image-store.s3.amazonaws.com/0/147015/39c89d9a-fba1-ed8d-06a3-1723ab5dcd06.png)

# おわりに

いかがでしたでしょうか。
もう一度書きますと、SkaffoldではDocker開発を回すうえでの非常に煩雑な手順、

```
・Dockerイメージビルド
・コンテナイメージのリポジトリプッシュ
・イメージをサーバにデプロイ
```
を全て自動化してくれます。

また、今回は少しハードルを上げてGKEクラスタにデプロイする例を紹介しましたが、
Minikube(ローカルで運用するk8s)やローカルのDockerでも使用することもでき、
そちらは、より単純な手順で実践可能です。

開発環境からDockerを採用する事例は、より増えてきていると思います。
みなさまもお手元の環境で、「Kubernetes + Skaffold」を試してみてはいかがでしょうか。