SkaffoldとDraftを比べてみた

## 概要
今まではスクリプトにk8sのデプロイ処理を書いていましたが、Skaffoldが発表されたのでDraftと一緒に試してみます！
![logo.png](https://qiita-image-store.s3.amazonaws.com/0/62029/05d7c47a-1012-e0e7-d429-3f7ecf763b59.png)

## 環境
- [Skaffold](https://github.com/GoogleCloudPlatform/skaffold): v0.3.x
- [Draft](https://draft.sh/): v0.12.x
- [Helm](https://docs.helm.sh/): v2.8.x
- Kubernetes（以後k8s）: v1.9.x
- DockerRegistry: Google Container Registry（以後GCR）

## ソースコード
- [example-k8s-deploy](https://github.com/watawuwu/example-k8s-deploy)

## 前提
- helmを使う
    - 必須ではないため、適宜`kubectl apply`などに読み替えてください
- k8sのセットアップが完了している
- asia.gcr.ioへのログイン（認証）が完了している

## 検証してみる内容
実際の運用環境に近い内容で検証していきます。

### 想定環境
- デプロイ先の環境は`local`を含め、`stage`の2種類
    - 実際は`prod`などの環境もあると思うので、増えても対応が可能な設定にする
- 全ての環境は別々のClusterとなる
    - Namespaceで環境を分ける場合はちょっとコマンドが変わりますが実現は可能
- `local`環境は`minikube`か`Docker For (Windows|MAC)`で構築
    - 記事中は`Docker For MAC`を使用
- `stage`はGKE（k8s）を使用
- サンプルアプリケーションはGolang

### 実現したいこと
- helmを利用したい（必須ではない）
- `local`から未コミットのソースをコンパイルしてデプロイしたい
- 複数のClusterにデプロイしたい
- 生成するDockerイメージのタグを指定をしたい
- プライベートなDocker Registryを使用したい

## デプロイするソースコードの一部
全部入りソースは[こちら](https://github.com/watawuwu/example-k8s-deploy)

- サンプルアプリは`Hello world`を繰り返し表示します

``` go:main.go
package main

import (
	"fmt"
	"time"
)

func main() {
	for {
		fmt.Println("Hello world")
		time.Sleep(time.Second * 1)
	}
}
```

- Dockerファイルの内容はビルド環境と実行環境を兼ねています。

```dockerfile:Dockerfile
FROM golang:1.9.4-alpine3.7

WORKDIR /go/src
COPY main.go .
RUN go build -o app

CMD ["./app"]
```

- helmはよくある`Deployment`と`Service`の設定です
    - `Draft`用に同じような設定が`charts/app`に配置されています

```yaml:helm/Chart.yaml
apiVersion: v1
name: foo-app
version: 0.1.0
```

```yaml:helm/templates/deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: foo-app
  labels:
    app: foo-app
spec:
  replicas: {{ .Values.replicas }}
  template:
    metadata:
      labels:
        app: foo-app
    spec:
      containers:
        - name: foo-app
          image: {{ .Values.image }}
```

```yaml:helm/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: foo-app
  labels:
    app: foo-app
spec:
  type: NodePort
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
      name: foo-app
  selector:
    app: foo-app
```

```yaml:helm/values.yaml
replicas: 1
image: asia.gcr.io/{gcp_project}/test/foo-app
```

## `local`環境へのデプロイ
`local`環境へのデプロイを通じて基本的な操作を見ていきます。
どのツールも開発途中の状態（未コミット）でデプロイできますが、Gitコミットのハッシュ値を使用するのでGit管理されている必要があります。

### スクリプト（Makefile）でデプロイ
まずはスクリプトだけでデプロイする方法を試します。

処理内容は以下です。

- Dockerファイルを元にコンテナイメージを作成
- helmを使ってコンテナイメージの差し替え（デプロイ）

```makefile:Makefile
# Const
#===============================================================
gcp_project    := {gcp_project}
image_repo     := asia.gcr.io/$(gcp_project)/test/foo-app
tag            := $(shell date +'%Y-%m-%dT%H%M%S')

# Task
#===============================================================
plain-deploy: docker-build helm-apply

helm-apply:
	helm upgrade --install \
	  --set image.tag=$(tag) \
	  foo-app charts/app

docker-build:
	docker build -t $(image_repo):$(tag) .
	docker tag $(image_repo):$(tag) $(image_repo):latest

```

- `local`環境へデプロイ

```shell
$ make plain-deploy
```

### Skaffoldでデプロイ
`Skaffold`を使えば設定ファイルの設置だけでデプロイ可能です。

処理内容は以下です。

- Dockerファイルを元にコンテナイメージを作成
- helmを使ってコンテナイメージの差し替え（デプロイ）

```yaml:skaffold.yaml
apiVersion: skaffold/v1alpha2
kind: Config
build:
  tagPolicy:
    sha256: {}
  artifacts:
  - imageName: asia.gcr.io/{gcp_project}/test/foo-app
    workspace: .
  local: {}
deploy:
  helm:
    releases:
    - name: skaffold-app
      chartPath: helm
      namespace: default
      values:
        image: asia.gcr.io/{gcp_project}/test/foo-app
```

- `local`環境へデプロイ

```
$ skaffold run
```

### Draftでデプロイ
`Draft`も設定ファイルの設置だけでデプロイ可能です。

処理内容は以下です。

- Dockerファイルを元にコンテナイメージを作成
- DockerRegistryへコンテナイメージをpush
- helmを使ってコンテナイメージの差し替え（デプロイ）

他のツールと違う点はDockerRegistryへのプッシュが強制されます。
`local`環境の場合、DockerRegistryへのプッシュは不要な作業となってしまいます。
コンテナイメージのサイズが大きいと、このデメリットは大きなストレスになりそうです。

`draft create`でも作成可能ですが、内容をシンプルにしたいため今回は手動で作成します。

```toml:draft.toml
[environments]
  [environments.local]
    name = "foo-app"
    namespace = "default"
    wait = false
    watch = false
    auto-connect = false
    registry = "asia.gcr.io/{gcp_project}/test"
```

- `local`環境へデプロイ
    - `environments.local`を`environments.development`に変更すると`-e`オプションが不要になります。通常は`environments.development`を利用するほうが楽できます。

```shell
$ draft up -e local
```

## 複数環境の対応
次はGKEに構築されている`stage`環境にデプロイする方法を見ていきます。
GKEの場合、以下のコマンドでコンテキストファイルを作成できます。

```shell
$ gcloud container clusters get-credentials {cluster_name} --project {gcp_project} --zone {gcp_zone}
```

以後のコンテキストの切り替えは、以下のコマンドで実施します。

``` shell
# コンテキストの一覧取得
$ kubectl config get-contexts
CURRENT   NAME                                            CLUSTER                                         AUTHINFO                                        NAMESPACE
*         docker-for-desktop                              docker-for-desktop-cluster                      docker-for-desktop
          gke_{project-name}_{zone}_{cluster-name}        gke_{project-name}_{zone}_{cluster-name}        gke_{project-name}_{zone}_{cluster-name}

# `local`環境に切り替え
$ kubectl config use-context docker-for-desktop

# `stage`環境に切り替え
$ kubectl config use-context gke_{project-name}_{zone}_{cluster-name}
```

### スクリプト（Makefile）
複数環境への対応は、`helm`オプションの`--kube-context`を追加するだけで対応できます。

```patch:MafileV2.patch
--- MakefileV1.mk	2018-04-09 18:41:24.000000000 +0900
+++ MakefileV2.mk	2018-04-09 18:40:50.000000000 +0900
@@ -1,9 +1,18 @@
+# Option
+#===============================================================
+ENV := local
+
 # Const
 #===============================================================
 gcp_project    := {gcp_project}
 image_repo     := asia.gcr.io/$(gcp_project)/test/foo-app
+context        := docker-for-desktop
 tag            := $(shell date +'%Y-%m-%dT%H%M%S')
 
+ifeq ($(ENV),stage)
+  context := gke_{gcp_project}-{gcp_zone}_{custer_name}
+endif
+
 # Task
 #===============================================================
 plain-deploy: docker-build docker-push helm-apply
@@ -11,6 +20,7 @@ plain-deploy: docker-build docker-push h
 helm-apply:
 	helm upgrade --install \
 	  --set image.tag=$(tag) \
+	  --kube-context $(context) \
 	  foo-app charts/app
 
 docker-build:
```

以下コマンドで`stage`環境にデプロイできます。

```
$ make ENV=stage plain-deploy
```

環境ごとにパラメータを変えたいは場合、さらに以下のパッチを当てます。

```patch:MafileV2-2.patch
--- MakefileV2.mk	2018-04-09 18:40:50.000000000 +0900
+++ MakefileV2-2.mk	2018-04-09 18:29:17.000000000 +0900
@@ -21,6 +21,7 @@ helm-apply:
 	helm upgrade --install \
 	  --set image.tag=$(tag) \
 	  --kube-context $(context) \
+	  --values charts/app/env/$(ENV).yaml \
 	  foo-app charts/app
 
 docker-build:
```

以下コマンドで環境ごとの設定ファイルを作成します。

```
# 環境用に上書きファイルを追加
$ mkdir -p charts/app/env

# localは空ファイルで作成
$ touch charts/app/env/local.yaml

# stageではreplicasを2に変更
$ echo 'replicas: 2' >> charts/app/env/stage.yaml
```

以下コマンドで`stage`用パラメーターを使い、`stage`環境にデプロイできます。

```shell
$ make ENV=stage plain-deploy
```

### Skaffold
残念ながら`Skaffold`は、コンテキストの指定ができないのでスクリプトで対応する必要があります。
また`kubectl config use-context`でコンテキストを切り替えると、その後の操作（session）も切り替わった状態になるので注意してください。

- [Allow deploy steps to use a configurable context for K8s clusters](https://github.com/GoogleCloudPlatform/skaffold/issues/104)

```patch:MafileV3.patch
--- MakefileV2-2.mk	2018-04-09 18:29:17.000000000 +0900
+++ MakefileV3.mk	2018-04-09 18:30:22.000000000 +0900
@@ -15,6 +15,10 @@ endif
 
 # Task
 #===============================================================
+skaffold-deploy:
+	kubectl config use-context $(context)
+	skaffold run
+
 plain-deploy: docker-build docker-push helm-apply
 
 helm-apply:
```

以下コマンドで`stage`環境にデプロイできます。

```
$ make ENV=stage skaffold-deploy
```

環境ごとにパラメータを変えたい場合は、設定ファイルを複数用意する必要があります。

``` shell
# local用にサフィクスを追加
$ mv skaffold.yaml skaffold-local.yaml

# workaround for skaffold
# 指定ありでもskaffold.yamlの空ファイルが必要
$ touch skaffold.yaml

# stage用の設定ファイルを作成
$ cat > skaffold-stage.yaml
apiVersion: skaffold/v1alpha2
kind: Config
build:
  tagPolicy:
    sha256: {}
  artifacts:
  - imageName: asia.gcr.io/{gcp_project}/test/foo-app
    workspace: .
  local: {}
deploy:
  helm:
    releases:
    - name: foo-app
      chartPath: helm
      namespace: default
      valuesFilePath: helm/env/stage.yaml
      values:
        image: asia.gcr.io/{gcp_project}/test/foo-app

# 環境用に上書きファイルを追加
$ mkdir -p helm/env

# localは空ファイルで作成
$ touch helm/env/local.yaml

# stageではreplicasを2に変更
$ echo 'replicas: 2' >> helm/env/stage.yaml

```

作成した設定ファイルを使用できるようにタスクも書き換えます。

```patch:MafileV3-2.patch
--- MakefileV3.mk	2018-04-09 18:30:22.000000000 +0900
+++ MakefileV3-2.mk	2018-04-09 18:32:41.000000000 +0900
@@ -8,16 +8,18 @@
 gcp_project    := {gcp_project}
 image_repo     := asia.gcr.io/$(gcp_project)/test/foo-app
 context        := docker-for-desktop
 tag            := $(shell date +'%Y-%m-%dT%H%M%S')
+skaffold_yaml  := skaffold-local.yaml
 
 ifeq ($(ENV),stage)
   context := gke_{gcp_project}_asia-northeast1-a_stage-cluster
+  skaffold_yaml := skaffold-stage.yaml
 endif
 
 # Task
 #===============================================================
 skaffold-deploy:
 	kubectl config use-context $(context)
-	skaffold run
+	skaffold run -f $(skaffold_yaml)
 
 plain-deploy: docker-build docker-push helm-apply
```


以下コマンドで`stage`用パラメーターを使い、`stage`環境にデプロイできます。

```
$ make ENV=stage skaffold-deploy
```

### Draft
`Draft`は`Helm`と同じ開発元だけあって`--kube-context`オプションを指定すればコンテキストの切り替えができます。
とはいえ、コンテキストの名前が長いので他のツールと同じくMakefileにタスクを作成します。

```patch:MafileV4.patch
--- MakefileV3-2.mk	2018-04-09 18:32:41.000000000 +0900
+++ MakefileV4.mk	2018-04-09 18:35:56.000000000 +0900
@@ -17,6 +17,9 @@ endif
 
 # Task
 #===============================================================
+draft-deploy:
+	draft --kube-context $(context) up --environment $(ENV)
+
 skaffold-deploy:
 	kubectl config use-context $(context)
 	skaffold run -f $(skaffold_yaml)
```

また環境ごとにパラメータを変えたい場合は、環境ごとの設定を`draft.toml`に追加します。

```toml:draft.toml
... // 追加
  [environments.stage]
    name = "foo-app"
    namespace = "default"
    wait = false
    watch = false
    auto-connect = false
    registry = "asia.gcr.io/{gcp_project}/test"
    set = ["replicas=2"]
```

以下コマンドで`stage`にデプロイできます。

```
$ make ENV=stage draft-deploy
```

## タグの指定
CI/CDのパイプラインでは、Gitで発行したタグをDockerイメージのタグとして使用したいケースが多いと思います。
通常はhelmの設定ファイルや、各ツールの設定ファイルを書き換えれば実現できす。
ただしそれでは、パイプラインやワークフローが複雑になるので動的にコマンドラインから変更する方法を確認していきます。

### スクリプト（Makefile）
タグの対応くらいはスクリプトでも複雑な処理は不要ですね。

```patch:MafileV5.patch
--- MakefileV4.mk	2018-04-09 18:35:56.000000000 +0900
+++ MakefileV5.mk	2018-04-09 18:36:26.000000000 +0900
@@ -1,13 +1,13 @@
 # Option
 #===============================================================
 ENV := local
+TAG :=
 
 # Const
 #===============================================================
 gcp_project    := {gcp_project}
 image_repo     := asia.gcr.io/$(gcp_project)/test/foo-app
 context        := docker-for-desktop
-tag            := $(shell date +'%Y-%m-%dT%H%M%S')
 skaffold_yaml  := skaffold-local.yaml
 
 ifeq ($(ENV),stage)
@@ -15,6 +15,12 @@ ifeq ($(ENV),stage)
   skaffold_yaml := skaffold-stage.yaml
 endif
 
+ifeq ($(TAG),)
+  tag := $(shell date +'%Y-%m-%dT%H%M%S')
+else
+  tag := $(TAG)
+endif
+
 # Task
 #===============================================================
 draft-deploy:
```

以下コマンドでタグを指定してデプロイできます。

```shell
$ make ENV=stage TAG=0.3.0 plain-deploy
```

### Skaffold
`--tag`オプションを使うことで実現できます。

```patch:MafileV6.patch
--- MakefileV5.mk	2018-04-09 18:36:26.000000000 +0900
+++ MakefileV6.mk	2018-04-09 18:37:40.000000000 +0900
@@ -17,10 +17,14 @@ endif
 
 ifeq ($(TAG),)
   tag := $(shell date +'%Y-%m-%dT%H%M%S')
+  skaffold_option :=
 else
   tag := $(TAG)
+  skaffold_option := --tag $(tag)
 endif
 
+
+
 # Task
 #===============================================================
 draft-deploy:
@@ -28,7 +32,7 @@ draft-deploy:
 
 skaffold-deploy:
 	kubectl config use-context $(context)
-	skaffold run -f $(skaffold_yaml)
+	skaffold run -f $(skaffold_yaml) $(skaffold_option)
 
 plain-deploy: docker-build docker-push helm-apply
```

以下コマンドでタグを指定してデプロイできます。

```
$ make ENV=stage TAG=0.3.0 skaffold-deploy
```

### Draft
残念ながら対応していないようで、関連するIssueがありますが実装はされていないようです。

- [Improve cycle time on Minikube by eliminating push step](https://github.com/Azure/draft/issues/432)
- [`draft up` should tag and release a ":latest" tag](https://github.com/Azure/draft/issues/541)

Draftは開発時のツールとして使い、本番環境へのデプロイなどは別の方法を検討したほうが良いかもしれません。

## Conclusion
`Skaffold`と`Draft`を比べてみましたが、現状でも十分便利だと感じました。
ただ`Skaffold`はhelmのtemplate構成に制限があったりするので使用するにはWorkaroundな対応が必要です。
同様に`Draft`もまだ`stable`ではなく、`experimental`な状態のため、（draftdの削除などの）大きな変更も入るでしょう。

それでも`秘伝のスクリプト`がないチームには検討の価値があると思います。

また、すでに`秘伝のスクリプト`があるチームでは物足りなさがあると思うので、スクリプトと合わせて使う方法が良さそうです。

||スクリプト                   | Skaffold | Draft |
|------------------------------|----------|-------|---|
| 導入の容易さ                     | ×        | ○     | ○ |
| デプロイ                     | ○        | ◎     | ◎ |
| 複数環境へのデプロイ         | ◎        | △     | ◎ |
| 環境ごとのカスタムパラメータ | ○        | △     | ◎ |
| タグの指定                   | ○        | ◎     | × |




また今回の記事にはないですが、[keel](https://github.com/keel-hq/keel)も良い選択肢ですので、合わせて検討してみてください！
気が向いてたらこの記事にも追記します。