ローカルでのソースコードの変更を自動で検知してKubenetesへデプロイ! Skaffoldを開発モードで使ってみました

Skaffoldを使用して、ローカルでソースコードを変更したら自動的にKubernetes(EKS)までデプロイして快適に開発できる環境の作り方をまとめてみました。
2019.01.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

おはようございます、加藤です。昨日参加した勉強会でSkaffoldの存在を知ったので、早速 ECR & EKS で試してみました。

Skaffoldとは

GoogleContainerTools/skaffold: Easy and Repeatable Kubernetes Development

SkaffoldはGoogleによって開発されている、Kubernetes(以降、k8s)への継続的な開発を容易にするコマンドラインツールです。 開発フェイズでは、アプリケーションのソースコードの変更を検知して、DockerコンテナのBuild→コンテナレジストリへのPush→k8sクラスタにDeployまでを自動で行ってくれます。 本番フェイズでも、パイプライン内でCI/CDを担当させる事が可能です。

このブログでは開発フェイズでSkaffoldをどのように使用するかをターゲットにしています。

前提

  • Amazon EKSが構築済みで利用可能であること
  • direnvがインストール済みで利用可能であること (AssumeRoleでAWSアカウントにアクセスしている場合)
  • assume-roleがインストール済みで利用可能であること (AssumeRoleでAWSアカウントにアクセスしている場合)

やってみた

Skaffoldをインストール

$ brew install skaffold

ECR認証ヘルパーをインストール

ECR認証ヘルパーをインストールします。ECRを利用する場合にはdocker login接続する方法もありますが、今回は認証ヘルパーを使用します。 awslabs/amazon-ecr-credential-helper: Automatically gets credentials for Amazon ECR on docker push/docker pull

$ brew install docker-credential-helper-ecr

認証ヘルパーに接続先を認識させる為に、Docker configを編集します。カレントディレクトリ.docker/config.jsonを作成してください。 account_id, regionは環境に応じて置換してください。AssumeRoleする場合は、AssumeRole先のaccount_idを設定します。

.docker/config.json

{
    "credHelpers": {
        "[account_id].dkr.ecr.[region].amazonaws.com": "ecr-login"
        }
}

環境変数、その他の設定

環境変数にAWS認証情報やKUBECONFIG, DOCKER_CONFIGを設定します。 また、私は複数のkubectlを利用している関係で、一時的にkubectlにPATHを通しています。

[超小ネタ]direnvを使ってkubectlとkubeconfigをプロジェクト(ディレクトリ)毎に切り替える | DevelopersIO

最終的にこんな風になりました。profile_nameは~/.aws/configで設定しているプロファイル名です。AssumeRoleしていない人は記載する必要がありません。

.envrc

eval $(assume-role [profile_name])
export KUBECONFIG="$(pwd)/.kube/config"
export DOCKER_CONFIG="$(pwd)/.docker/"
PATH_add ~/bin/kubectl/aws/v1.10.3

.envrcを作成したら下記のコマンドで有効化します。

$ direnv allow

k8sのconfigを取得します。KUBECONFIGを設定しているので、カレントディレクトリの配下に作成されます。

$ aws eks update-kubeconfig --name [cluster_name]

ECRの作成

$ REPOSITORY_URI=$(aws ecr create-repository --repository-name k8s-skaffold/skaffold-example | jq -r '.repository.repositoryUri')
$ echo $REPOSITORY_URI

チュートリアル

GitHubリポジトリからサンプルをクローンします。

$ git clone https://github.com/GoogleContainerTools/skaffold
$ cd skaffold/examples/getting-started

skaffold.yaml, k8s-pod.yamlを生成します。

$ cat > skaffold.yaml <<EOF
apiVersion: skaffold/v1alpha2
kind: Config
build:
  tagPolicy:
    envTemplate:
      template: "{{.IMAGE_NAME}}:{{.DIGEST_HEX}}"
  artifacts:
  - imageName: ${REPOSITORY_URI}
  local:
deploy:
  kubectl:
    manifests:
      - k8s-*
EOF
$ cat > k8s-pod.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: getting-started
spec:
  containers:
  - name: getting-started
    image: ${REPOSITORY_URI} 
EOF

下記の2つのファイルが作成されたはずです。

skaffold/examples/getting-started/skaffold.yaml

apiVersion: v1
kind: Pod
metadata:
  name: getting-started
spec:
  containers:
  - name: getting-started
    image: ${REPOSITORY_URI}

関連するDockerfileとGolangのプログラムも載せておきます。

マルチステージでGolangのプログラムを動かす内容となっていました。

skaffold/examples/getting-started/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 .

1秒毎にHello world!と表示するプログラムでした。

skaffold/examples/getting-started/main.go

package main

import (
	"fmt"
	"time"
)

func main() {
	for {
		fmt.Println("Hello world!")

		time.Sleep(time.Second * 1)
	}
}

Skaffoldを開発モードで起動してみます。

$ skaffold dev
WARN[0000] config version (skaffold/v1alpha2) out of date: upgrading to latest (skaffold/v1beta2)
Starting build...
Building [[account_id]].dkr.ecr.[region].amazonaws.com/k8s-skaffold/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 .
 ---> Using cache
 ---> 4c7c0ad132bc
Step 3/6 : RUN go build -o /app main.go
 ---> Using cache
 ---> ac77260c9285
Step 4/6 : FROM alpine:3.7
 ---> 9bea9e12e381
Step 5/6 : CMD ["./app"]
 ---> Using cache
 ---> 3ee2781f0277
Step 6/6 : COPY --from=builder /app .
 ---> Using cache
 ---> 73d583ae6d63
Successfully built 73d583ae6d63
Successfully tagged 2fcda617dc7ad6a6d01f58c0f91ed585:latest
The push refers to repository [[account_id]].dkr.ecr.[region].amazonaws.com/k8s-skaffold/skaffold-example]
7f88f55075f4: Preparing
d6da3c54c8f3: Preparing
d6da3c54c8f3: Layer already exists
7f88f55075f4: Layer already exists
73d583ae6d63ad751f1e4a2cbc2ddacb01c706a7141236cca0ff8b7e7a947447: digest: sha256:da3029816f7614c1b106b605790e6867fb6cb010e6cf9eb9d229dbf5b7cef0ef size: 738
Build complete in 1.216663214s
Starting test...
Test complete in 14.427µs
Starting deploy...
kubectl client version: 1.10
kubectl version 1.12.0 or greater is recommended for use with skaffold
pod "getting-started" created
Deploy complete in 6.574979148s
Watching for changes every 1s...
[getting-started] Hello world!
[getting-started] Hello world!
[getting-started] Hello world!
[getting-started] Hello world!

コンソールにHello world!と表示されることを確認できました! Skaffoldは開発モードで動いていると1秒毎にソースに変更が無いか検知してくれます。 main.goの10行目を変更してコンソールに表示するメッセージ変更してみましょう、Hello world!2に変更しました。

[getting-started] Hello world!
[getting-started] Hello world!
Starting build...
Building [[account_id]].dkr.ecr.[region].amazonaws.com/k8s-skaffold/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 .
 ---> f4bc0451d3e5
Step 3/6 : RUN go build -o /app main.go
 ---> Running in e000c7b4f4cb
 ---> 7094a3071df2
Step 4/6 : FROM alpine:3.7
 ---> 9bea9e12e381
Step 5/6 : CMD ["./app"]
 ---> Using cache
 ---> 3ee2781f0277
Step 6/6 : COPY --from=builder /app .
 ---> eeeb9037a58f
Successfully built eeeb9037a58f
Successfully tagged 33aee612daa48982cc4b37f313762ef9:latest
The push refers to repository [[account_id]].dkr.ecr.[region].amazonaws.com/k8s-skaffold/skaffold-example]
a93eb3ea645d: Preparing
d6da3c54c8f3: Preparing
d6da3c54c8f3: Layer already exists
a93eb3ea645d: Pushed
eeeb9037a58fca0c94555b46b7a03ccf93d1da503f8e8d0d30c26e844af7df7c: digest: sha256:4d61b9ab9a0fd106f55dc2f041c170926a5d8e69a3d7c787c28583e13b1f642d size: 738
Build complete in 5.363027368s
Starting test...
Test complete in 7.512µs
Starting deploy...
kubectl client version: 1.10
kubectl version 1.12.0 or greater is recommended for use with skaffold
pod "getting-started" configured
Deploy complete in 778.911883ms
Watching for changes every 1s...
[getting-started] Hello world!2
[getting-started] Hello world!2
[getting-started] Hello world!2

無事に変更を検知して表示されるメッセージが切り替わりました! アプリケーションだけではなく、マニフェストファイルの変更も検知してくれます、skaffold.yamlの13行目でk8s-*を条件に検索しています。 下記のファイルを作成してください。

skaffold/examples/getting-started/k8s-pod2.yaml

apiVersion: v1
kind: Pod
metadata:
  name: getting-started2
spec:
  containers:
    - name: getting-started2
      image: [account_id].dkr.ecr.[region].amazonaws.com/k8s-skaffold/skaffold-example

マニフェストファイルの追加を検知してデプロイが走りました。Dockerイメージは同じ物なのでビルドは発生しません。

Starting deploy...
kubectl client version: 1.10
kubectl version 1.12.0 or greater is recommended for use with skaffold
pod "getting-started2" created
Deploy complete in 4.403612632s
Watching for changes every 1s...
[getting-started] Hello world!2
[getting-started] Hello world!2
[getting-started2] Hello world!2
[getting-started2] Hello world!2
[getting-started2] Hello world!2
[getting-started] Hello world!2
[getting-started2] Hello world!2
[getting-started] Hello world!2

kubectlで起動中のPodsを確認してみました。2つ起動していることを確認できます。

$ kubectl get pods
NAME               READY     STATUS    RESTARTS   AGE
getting-started    1/1       Running   0          6s
getting-started2   1/1       Running   0          6s

あとがき

Skaffoldを使うと、変更後に即座にデプロイまで行われて開発を加速できそうだなと感じました。 今回のブログでは触れていませんが、Skaffoldはkomposeと連携してdocker-composeをマニフェストファイルに変換してデプロイまで行ってくれる機能もあります。docker-composeは触っているけどマニフェストファイルはまだ書いたことがない...という人には嬉しい機能ですね。

参考