Skaffold入門 - minikube/GKEへデプロイする

(以下は2018/05/11時点の情報に基づいた記事です。)

CDツールskaffold version0.5をgithubにあるREADME.mdに従って、minikubeとGCPを使って検証します。(説明は一部意訳しています)
(GitHub: https://github.com/GoogleContainerTools/skaffold)


##Skaffoldとは

SkaffoldはKubernetesでアプリケーションのCD(継続的デリバリー)を実施するためのコマンドラインツールです。
ローカル環境で修正したアプリケーションのコードを、ローカルもしくはリモートのk8sクラスターにアプリケーションとして繰り返し適用することができます。

Skaffoldはアプリケーションのビルドからk8sへのプッシュ、そしてデプロイまでのワークフローを自動化します。
また、アプリケーションを本番環境にデプロイする際のCI/CDパイプラインとして利用することもできます。


##Skaffoldの特徴

・サーバーサイドコンポーネントが不要なため、クラスターにオーバーヘッドが発生しません。

・アプリケーションのコード変更を自動で検知して、ビルド/プッシュ/デプロイを自動化します。

・コンテナーイメージのタグを管理するため、k8sにアプリケーションをデプロイする際にyamlファイルのイメージタグを変更する作業が発生しません。

・SkaffoldではビルドとデプロイのAPIを利用することができます。APIを利用することで様々な既成のツールやワークフローと連携させることができます。

・様々なアプリケーションコンポーネントを利用することができ、アプリケーションの一部だけをビルド/デプロイすることもできます。

・特定のファイルを保存した時や、あるdeploymentをk8sクラスターに適用させた時に、同じ設定でアプリケーションをデプロイすることもできます。(※要確認)
[原文]Deploy regularly when saving files or run one off deployments using the same configuration.

##Skaffoldのアーキテクチャー

Skaffoldはビルド/プッシュ/デプロイの要素がそれぞれ疎結合なため、用途に応じてカスタマイズして実行することができます。(要検証)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/223065/5ae33927-dcb8-d54f-b243-b71e61cab692.png)

##動作モード

**Skaffold dev**
デプロイしたアプリケーションを継続的に更新します。

 - Skaffoldはソースコード及びコードとdockerイメージの依存関係を監視し、変更を検知するとアプリケーションのビルド/プッシュ/デプロイを実行します。

 - デプロイされているコンテナーからのログをストリーミングします。
 - エラー発生時には警告を表示しますが、基本的にビルドからデプロイまで継続的に実行します。

**Skaffold run**
デプロイしたアプリケーションを継続的に更新しますが、エラー発生時に終了します。以下の用途で利用できます。
 - CI/CDパイプラインとして利用できます。
 - 継続的にデプロイした後に、コードに凡ミスが無いか確認するために利用できます。

##ローカル環境(minikube)でのSkaffoldの利用


###インストールと準備
Skaffoldを始めるには以下のコンポーネントを用意します。

**1.Skaffold**

 - 最新のLinux版を導入するためには以下のコマンドを実行します。
  - `curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`


 - 最新のOSX版を導入するためには以下のコマンドを実行します。
    - `curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-darwin-amd64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`

**2.k8sクラスター**

- 今回はMinikubeを利用します。(Minikubeの導入については[GitHub](https://github.com/kubernetes/minikube/releases)を参照してください)
- GKE,Docker for Mac(Edge),Docker for Windows(Edge)がテスト済みですが、基本的にどのk8sクラスターでも動作するはずです。(原文より)

**3.kubectl**

- Minikubeを利用する場合は`minikube status`コマンドで`kubectl`コマンドの向き先を設定します。

**4.Docker**
- dockerはイメージビルドのために必要です。

**5.Dockerイメージ・レジストリー**

 - minikubeかDockerによるk8sクラスターを利用している場合は本項目をスキップ可能です。
 - それ以外の場合は、ローカルのDockerが任意のDockerイメージ・レジストリーに対してイメージをプッシュ出来るように設定します。(GCPの検証の方でGCRを利用するための手順を示しています)

###インタラクティブな開発
以下手順を実行してデモを行います。
(今回はOSX上のminikube環境で実行します)

1.minikubeとdockerを起動します。dockerを起動させて、以下コマンドでminikubeの起動と構成を行います。

- minikubeの起動
 - `minikube start`

- kubectlコマンドの構成
 - `minikube status`

- dockerの構成
 - `minikube docker-env`


2.以下コマンドを実行してgit cloneを実行します
`git clone https://github.com/GoogleContainerTools/skaffold`
3.以下コマンドを実行してディレクトリーを移動します
 `cd ./Skaffold/skaffold/examples/getting-started`
4.以下コマンドを実行してSkaffoldを起動します(初回は5分程度かかりました)

```
$ skaffold dev
WARN[0002] Error processing base image for onbuild triggers: Get https://auth.docker.io/token?scope=repository%3Alibrary%2Fgolang%3Apull&service=registry.docker.io: exit status 1. Dependencies may be incomplete.
WARN[0003] Error processing base image for onbuild triggers: Get https://auth.docker.io/token?scope=repository%3Alibrary%2Falpine%3Apull&service=registry.docker.io: exit status 1. Dependencies may be incomplete.
Starting build...
Found [minikube] context, using local docker daemon.
WARN[0005] Error processing base image for onbuild triggers: Get https://auth.docker.io/token?scope=repository%3Alibrary%2Fgolang%3Apull&service=registry.docker.io: exit status 1. Dependencies may be incomplete.
WARN[0006] Error processing base image for onbuild triggers: Get https://auth.docker.io/token?scope=repository%3Alibrary%2Falpine%3Apull&service=registry.docker.io: exit status 1. Dependencies may be incomplete.
Sending build context to Docker daemon  3.072kB
Step 1/6 : FROM golang:1.10.1-alpine3.7 as builder
1.10.1-alpine3.7: Pulling from library/golang
ff3a5c916c92: Pulling fs layer
f32d2ea73378: Pulling fs layer
c6678747892c: Pulling fs layer
16b5f22d8b23: Pulling fs layer
16b5f22d8b23: Waiting
f32d2ea73378: Verifying Checksum
f32d2ea73378: Download complete
16b5f22d8b23: Verifying Checksum
16b5f22d8b23: Download complete
ff3a5c916c92: Verifying Checksum
ff3a5c916c92: Download complete
ff3a5c916c92: Pull complete
f32d2ea73378: Pull complete
c6678747892c: Verifying Checksum
c6678747892c: Download complete
c6678747892c: Pull complete
16b5f22d8b23: Pull complete
Digest: sha256:356aea725be911d52e0f2f0344a17ac3d97c54c74d50b8561f58eae6cc0871bf
Status: Downloaded newer image for golang:1.10.1-alpine3.7
 ---> 52d894fca6d4
Step 2/6 : COPY main.go .
 ---> 8795ed4d779e
Step 3/6 : RUN go build -o /app main.go
 ---> Running in baf3da8f9ce9
 ---> a4c4379e2f96
Step 4/6 : FROM alpine:3.7
3.7: Pulling from library/alpine
ff3a5c916c92: Already exists
Digest: sha256:7df6db5aa61ae9480f52f0b3a06a140ab98d427f86d8d5de0bedab9b8df6b1c0
Status: Downloaded newer image for alpine:3.7
 ---> 3fd9065eaf02
Step 5/6 : CMD ./app
 ---> Running in 9e876729ae59
 ---> ce35c98413c0
Step 6/6 : COPY --from=builder /app .
 ---> e6a41f81b005
Successfully built e6a41f81b005
Successfully tagged 585d29d2cddfb8c3bb4d3046b46e73c7:latest
Successfully tagged gcr.io/k8s-skaffold/skaffold-example:e6a41f81b0053dfcd5aff883b69f12fd4bec5dfe33506b5b7675a62555224b8e
Build complete in 4m59.348905211s
Starting deploy...
pod "getting-started" created
Deploy complete in 774.80311ms
Watching for changes...

```
5.ここまで、skaffoldは以下を実行しました。

 - (カレントディレクトリー以下にある)ローカルソースコードからのアプリケーションのビルド
 - イメージの作成とプッシュ
 - 作成したイメージが定義された`skaffold.yaml`を`kubectl apply -f`コマンドの引数として実行しています。

6.以下の出力が確認できます。

```
[getting-started] Hello world!
[getting-started] Hello world!
[getting-started] Hello world!
```

アプリケーションのデプロイは以上で完了です。
次に、別のターミナルを開いて以下の通りmain.goをアップデートします。

```
$ diff main.go main.go.bk                                             +[master]
10c10
< 		fmt.Println("Hello jerry!")
---
> 		fmt.Println("Hello world!")
```

`main.go`ファイルを保存すると、Skaffoldはファイルの変更を検知してアプリケーションの再デプロイを行います。
以下はファイル保存直後の標準出力です。

```
[getting-started] Hello world!
[getting-started] Hello world!
Starting build...
Found [minikube] context, using local docker daemon.
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 .
 ---> 8c82bea33462
Step 3/6 : RUN go build -o /app main.go
 ---> Running in 4bd8b2312a86
 ---> 23ced66a6466
Step 4/6 : FROM alpine:3.7
 ---> 3fd9065eaf02
Step 5/6 : CMD ./app
 ---> Using cache
 ---> ce35c98413c0
Step 6/6 : COPY --from=builder /app .
 ---> e1a3b9cb5df2
Successfully built e1a3b9cb5df2
Successfully tagged cf88764c0782247f181b77c16e8a0a02:latest
Successfully tagged gcr.io/k8s-skaffold/skaffold-example:e1a3b9cb5df2c695ce37f6b152a89e8f56a521e1fe475686095a6528272e12b4
Build complete in 2.177209537s
Starting deploy...
pod "getting-started" configured
Deploy complete in 634.141056ms
Watching for changes...
[getting-started] Hello jerry!
[getting-started] Hello jerry!
[getting-started] Hello jerry!
```

`Ctrl`+`C`で停止させると、以下のようにpodを削除してSkaffoldが終了します。

```
[getting-started] Hello jerry!
^C
Cleaning up...

pod "getting-started" deleted
Cleanup complete in 608.217698ms
```

利用したアプリケーションはdockerイメージとして保存されていることを確認できます。

```
 $ docker images
REPOSITORY                                             TAG                                                                IMAGE ID            CREATED             SIZE
gcr.io/k8s-skaffold/skaffold-example                   a3f8e6261aaa08019c0dcfd059d57f532bed55dc82a24d1b485fc3ac3bc626b0   a3f8e6261aaa        13 hours ago        6.16MB
```

###パイプラインによるアプリケーションのデプロイを1回のみ実行するためには
ここまでのデモでは、常にアプリケーションの変更を検知してデプロイを実行します。
ビルド/デプロイを最初の一度のみ実行するようにするためには、`skaffold dev`コマンドの代わりに以下のコマンドを実行します。

```
$ skaffold run
```

##GCP(GKE)にwebサーバーアプリケーションをデプロイする

この章では、ローカル環境からGCPにwebサーバーアプリケーションをデプロイし、Ingressで公開します。
GCPにデプロイするためには、以下の作業を実施します。

**1.kubectlコマンドの構成**
**2.イメージリポジトリーの構成**
**3.skaffold構成ファイルの変更**
**4.アプリケーションの作成**
**5.各種yamlファイルの修正/作成**
**6.デプロイの実施**

###1.kubectlコマンドの構成

以下コマンドを実行して`kubectl`コマンドの向き先をセットアップします。
`$ gcloud auth login`

ブラウザーが開いてgoogleアカウントのログインプロセスが完了すると、コマンドの標準出力に以下が表示されます。

```
$ gcloud auth login
Your browser has been opened to visit:

    https://accounts.google.com/o/oauth2/auth?redirect_uri=http.....


WARNING: `gcloud auth login` no longer writes application default credentials.
If you need to use ADC, see:
  gcloud auth application-default --help

You are now logged in as [xxxxxx@gmail.com].
Your current project is [hoge-fuga-0113].  You can change this setting by running:
  $ gcloud config set project PROJECT_ID
```

上記の場合、プロジェクトが`hoge-fuga-0113`にセットされています。
プロジェクトを変更するためには、以下コマンドでプロジェクト一覧を表示 / プロジェクト変更を行います。

```
##プロジェクト一覧
$ gcloud projects list
PROJECT_ID             NAME              PROJECT_NUMBER
soty-naky-0113  My First Project  684351541285
hoge-fuga-0113  My Project        505481851822

##プロジェクト変更
$ gcloud config set project soty-naky-0113
Updated property [core/project].
```


プロジェクト内のクラスターを確認します。

```
$ gcloud container clusters list
NAME        ZONE           MASTER_VERSION  MASTER_IP        MACHINE_TYPE   NODE_VERSION  NUM_NODES  STATUS
cluster-1   us-central1-a  1.8.8-gke.0     111.222.33.44    n1-standard-2  1.8.8-gke.0   3          RUNNING
cluster-2  us-east1-d     1.8.8-gke.0     222.333.44.55    n1-standard-1  1.8.8-gke.0   3          RUNNING
```

`kubectl`コマンドの向き先となるクラスターを指定します。

```
$ gcloud container clusters get-credentials cluster-1 --zone us-central1-a
Updated property [container/cluster].
```

`kubectl`コマンドの向き先を確認します。

```
$ kubectl config current-context
gke_soty-naky-0113_us-central1-a_cluster-1
```

以上で`kubectl`コマンドの設定は完了です。


###2.イメージリポジトリーの構成
GCPにアプリケーションをデプロイするためには、ビルドしたdockerイメージのリポジトリーとして、GCR(Google Container Repositry)を利用する必要があります。
以下コマンドを実行してGCRの認証を行います。

```
$ gcloud auth configure-docker

The following settings will be added to your Docker config file
located at [/Users/sotanakanishi/.docker/config.json]:
 {
  "credHelpers": {
    "gcr.io": "gcloud",
    "us.gcr.io": "gcloud",
    "eu.gcr.io": "gcloud",
    "asia.gcr.io": "gcloud",
    "staging-k8s.gcr.io": "gcloud"
  }
}

Do you want to continue (Y/n)?  Y

Docker configuration file updated.
```


###3.skaffold構成ファイルの変更
`skaffold.yaml`ファイルのイメージ名を更新します。
イメージ名は"[HOSTNAME]/[PROJECT-ID]/[IMAGE]"となるように指定します。
(ちなみにタグを指定したところ、イメージのpushに失敗しました)
(2018/05/21追記：イメージタグの指定はversion0.6で可能になっているようです)
(参考：[GCRでのイメージのpushとpull](https://cloud.google.com/container-registry/docs/pushing-and-pulling?hl=ja))

```skaffold.yaml
apiVersion: skaffold/v1alpha2
kind: Config
build:
  artifacts:
  - imageName: us.gcr.io/soty-naky-0113/skaffold-example
deploy:
  kubectl:
    manifests:
      - k8s-*
profiles:
  - name: gcba
    build:
      googleCloudBuild:
        projectId: soty-naky-0113
```

###4.アプリケーションの作成
簡単なwebアプリを作成します。以下の通り既存のmain.goファイルを修正します。

```main.go
package main

import (
  "fmt"
  "net/http"
)

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

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

###5.各種yamlファイルの修正/作成
yamlファイルを作成/修正します。
必要なyamlファイルは以下の通りです。

・k8s-pod.yaml
・k8s-service.yaml
・k8s-ingress.yaml


podを作成する`k8s-pod.yaml`を以下の通り修正します。

```k8s-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: go-server
  labels:
    run: go-server
spec:
  containers:
  - name: getting-started
    image: us.gcr.io/soty-naky-0113/skaffold-example:1.0
    ports:
    - containerPort: 8080
```

serviceを作成する`k8s-service.yaml`ファイルを作成します。

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

ingressを作成する`k8s-ingress.yaml`ファイルを作成します。

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


###6.デプロイの実施
`skaffold run`もしくは`skaffold dev`コマンドを実行してデプロイを開始します。
今回は`skaffold run`コマンドを実行します。

```
$ skaffold run
Starting build...
Sending build context to Docker daemon  3.072kB
Step 1/7 : FROM golang:1.10.1-alpine3.7 as builder
 ---> 52d894fca6d4
Step 2/7 : COPY main.go .
 ---> 3cde480a75b1
Step 3/7 : RUN go build -o /app main.go
 ---> Running in a91b47079942
 ---> b925a5e7494a
Step 4/7 : FROM alpine:3.7
 ---> 3fd9065eaf02
Step 5/7 : CMD ["./app"]
 ---> Using cache
 ---> dded55f368e1
Step 6/7 : COPY --from=builder /app .
 ---> 477d7207517d
Step 7/7 : EXPOSE 8080
 ---> Running in 20a7cc6e0af3
 ---> 45a1017509b5
Successfully built 45a1017509b5
Successfully tagged 7f7e166fa464161d1cbfe2879a8c85f5:latest
Successfully tagged us.gcr.io/soty-naky-0113/skaffold-example:98002e0-dirty-fe2d377b9614e39d
The push refers to repository [us.gcr.io/soty-naky-0113/skaffold-example]
24a2cefaadab: Preparing
cd7100a72410: Preparing
cd7100a72410: Layer already exists
24a2cefaadab: Pushed
98002e0-dirty-fe2d377b9614e39d: digest: sha256:04f836189a689764e0abf5ed1ba5218e8e7fc8d14c3814422cca7f1fe6603b5f size: 739
Build complete in 26.020609284s
Starting deploy...
ingress "go-ingress" configured
pod "go-server" configured
service "go-nodeport" configured
Deploy complete in 5.675552896s
```

`kubectl`コマンドで各種インスタンスが生成されていることを確認します。

```
$ kubectl get po,svc,ing                                                                        +[master]
NAME                 READY     STATUS    RESTARTS   AGE
po/go-server         1/1       Running   2          1h

NAME              CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
svc/go-nodeport   10.11.123.134   <nodes>       8080:30266/TCP   1h
svc/kubernetes    11.12.134.145     <none>        443/TCP          1d

NAME             HOSTS     ADDRESS         PORTS     AGE
ing/go-ingress   *         111.222.333.444   80        1h
```

ingressのIPアドレスをブラウザーのアドレスバーに入力するとwebサーバーにアクセスできます。(コンソールからcurlコマンドを実行いただいても構いません)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/223065/5392f056-d4ec-0ae5-7a89-e4c7467370f8.png)



webサーバーに表示させる文字を変更してみます。
main.goファイル内の"Hello, World."を別の文字列に変更します。
以下例では`"Welcome to go-server."`に変更しています。

```main.go
package main

import (
  "fmt"
  "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Welcome to go-server.")
}

func main() {
  http.HandleFunc("/", handler) // ハンドラを登録してウェブページを表示させる
  http.ListenAndServe(":8080", nil)
}

```

今回go-serverをデプロイする際に`skaffold run`コマンドを実行したため、コードの変更を検知して自動ビルド/プッシュ/デプロイのワークフローは実行されません。
変更を適用するためには再度`skaffold run`コマンドを実行します。

```
 $ skaffold run                                                                                  +[master]
Starting build...
Sending build context to Docker daemon  3.072kB
Step 1/7 : FROM golang:1.10.1-alpine3.7 as builder
 ---> 52d894fca6d4
Step 2/7 : COPY main.go .
 ---> 5a4487cba4c3
Step 3/7 : RUN go build -o /app main.go
 ---> Running in 8ef776bb77c3
 ---> 0813f84cba59
Step 4/7 : FROM alpine:3.7
 ---> 3fd9065eaf02
Step 5/7 : CMD ["./app"]
 ---> Using cache
 ---> dded55f368e1
Step 6/7 : COPY --from=builder /app .
 ---> 7d91e59abf51
Step 7/7 : EXPOSE 8080
 ---> Running in 8d6e5b227db8
 ---> 7bf2dd48b9bd
Successfully built 7bf2dd48b9bd
Successfully tagged c36245f7b2415c8b9a4564059137ec1e:latest
Successfully tagged us.gcr.io/chrome-entropy-204110/skaffold-example:98002e0-dirty-a7f641542ee9cf11
The push refers to repository [us.gcr.io/chrome-entropy-204110/skaffold-example]
e9a715bb5d9c: Preparing
cd7100a72410: Preparing
cd7100a72410: Layer already exists
e9a715bb5d9c: Pushed
98002e0-dirty-a7f641542ee9cf11: digest: sha256:49121ac808b71f8bbb8fac0b5138b310422d92659a971afc1abd5bdd249b549a size: 739
Build complete in 25.648010629s
Starting deploy...
ingress "go-ingress" configured
pod "go-server" configured
service "go-nodeport" configured
Deploy complete in 6.000242514s
```

デプロイが完了しました。
先ほどの画面をもう一度更新すると、変更が反映されていることを確認できます。
(※キャッシュの設定によっては、変更が適用されていないように見える可能性があります)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/223065/0391a3d5-b39f-0747-3db9-7e152590c181.png)