🤗

Tiltでカスタムコントローラーの開発を効率化しよう

2021/12/10に公開

みなさん、Kubernetesのカスタムコントローラーを開発するとき、どのように動作確認してますか?
多少の違いはあれど、おそらく以下のような手順を踏んでいるのではないでしょうか?

  • ソースコードやマニフェストを書き換える。
  • コード生成ツール(controller-genなど)を実行してマニフェストやソースコードを生成する。
  • コンテナイメージをビルドする。
  • コンテナイメージをローカルレジストリにpushする。
  • マニフェストをKubernetesクラスタに適用する。
  • Podを再起動する。

ソースコードをちょっと書き換えるたびにこれらを実行していると、時間もかかるしとても面倒ですよね。

そこで本記事ではTiltというツールを利用して、カスタムコントローラーの開発を効率化する方法について紹介したいと思います。

Tiltとは

Tiltは、Kubernetes上でのアプリケーション開発をサポートするためのツールです。
ソースコードやマニフェストの変更を監視して、コンテナイメージの再ビルド、Kubernetesクラスタへのマニフェストの適用、Podの再起動などを自動的におこなってくれます。

https://tilt.dev

本記事ではカスタムコントローラーを対象にしていますが、カスタムコントローラーだけでなく、Kubernetes上での様々なアプリケーション開発に利用することができます。

事前準備

事前準備として、以下のツールをインストールしておきます。

また、開発対象のカスタムコントローラー実装を用意してください。
本記事では、下記のページのMarkdownViewコントローラーを利用します。

Kubernetesクラスタの立ち上げ

まずは開発用のKubernetesクラスタを用意します。今回はkindを利用することとします。
また必須ではありませんが、Tiltのスピードアップのためローカルコンテナレジストリも用意します[1]

TiltではローカルにKubernetesクラスタを立ち上げるためのツールが用意されています。

https://github.com/tilt-dev/ctlptl

これを利用して、以下のようにkindとローカルコンテナレジストリを立ち上げます。

cat <<EOF | ctlptl apply -f -
apiVersion: ctlptl.dev/v1alpha1
kind: Registry
name: ctlptl-registry
port: 5005
---
apiVersion: ctlptl.dev/v1alpha1
kind: Cluster
product: kind
registry: ctlptl-registry
EOF

Tiltfile

続いてTiltfileを用意します。

Tiltfileとは、どのファイルを監視して、どのようにコンテナイメージをビルドし、どのようにKubernetesリソースを適用するのかというルールを記述するためのファイルです。
Tiltfileは、StarlarkというPythonに似た言語で記述することができます。

Tiltfileを一から記述するのは面倒ですが、kubebuilder用のextensionが用意されているので、これを利用することにしましょう。

また、カスタムコントローラーにAdmission Webhookを実装する場合はcert-managerも必要になりますが、同様にextensionが用意されています。

この2つのextensionを利用してTiltfileを書いてみましょう。

load('ext://cert_manager', 'deploy_cert_manager')
load('ext://kubebuilder', 'kubebuilder') 

deploy_cert_manager(version="v1.6.1")
kubebuilder("zoetrope.github.io", "view", "v1", "MarkdownView")

deploy_cert_manager()の引数には、インストールしたいcert-managerのバージョンを指定します。
kubebuilder()の引数には、カスタムコントローラーのdomain, group, version, kindを指定します。

これでOKと言いたいところなのですが、yamlファイルを書き換えてもリロードされないなど期待通りに動かないところがありました。

そこで、extensionのファイルをベースに以下のように書き換えることにしました。

load('ext://restart_process', 'docker_build_with_restart')
load('ext://cert_manager', 'deploy_cert_manager')

def kubebuilder(DOMAIN, GROUP, VERSION, KIND, IMG='controller:latest', CONTROLLERGEN='crd rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases;'):

    DOCKERFILE = '''FROM golang:alpine
    WORKDIR /
    COPY ./bin/manager /
    CMD ["/manager"]
    '''

    def manifests():
        return 'controller-gen ' + CONTROLLERGEN

    def generate():
        return 'controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./...";'

    def vetfmt():
        return 'go vet ./...; go fmt ./...'

    def binary():
        return 'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -o bin/manager main.go'

    installed = local("which kubebuilder")
    print("kubebuilder is present:", installed)

    DIRNAME = os.path.basename(os. getcwd())

    local_resource('make manifests', manifests(), deps=["api", "controllers"], ignore=['*/*/zz_generated.deepcopy.go'])
    local_resource('make generate', generate(), deps=["api"], ignore=['*/*/zz_generated.deepcopy.go'])

    local_resource('CRD', manifests() + 'kustomize build config/crd | kubectl apply -f -', deps=["api"], ignore=['*/*/zz_generated.deepcopy.go'])

    watch_settings(ignore=['config/crd/bases/', 'config/rbac/role.yaml', 'config/webhook/manifests.yaml'])
    k8s_yaml(kustomize('./config/dev'))

    deps = ['controllers', 'main.go']
    deps.append('api')

    local_resource('Watch&Compile', generate() + binary(), deps=deps, ignore=['*/*/zz_generated.deepcopy.go'])

    local_resource('Sample YAML', 'kubectl apply -f ./config/samples', deps=["./config/samples"], resource_deps=[DIRNAME + "-controller-manager"])

    docker_build_with_restart(IMG, '.',
     dockerfile_contents=DOCKERFILE,
     entrypoint='/manager',
     only=['./bin/manager'],
     live_update=[
           sync('./bin/manager', '/manager'),
       ]
    )

deploy_cert_manager(version="v1.6.1")
kubebuilder("zoetrope.github.io", "view", "v1", "MarkdownView")

このファイルを Tiltfileという名前でカスタムコントローラーのトップディレクトリに保存します。

次にカスタムコントローラーのマニフェストファイルを少し書き換えます。

通常、カスタムコントローラーのソースコードを変更した場合、コンテナイメージをビルドし、Deploymentを再起動し、Podが作り直されるのを待つ必要があります。
TiltのLive Update機能では、それらの手順を省略し、コンテナを立ち上げたままバイナリファイルを差し替えてプロセスを再起動することができます。
このLive Update機能を利用するためには、PodをRootユーザーで立ち上げる必要があります[3]

そこでconfig/dev/というディレクトリを作成し、その中に以下のようにkustomization.yamlmanager.yamlを作成してsecurityContextを削除します[4][5]

  • config/dev/kustomization.yaml
resources:
  - ../default
patchesStrategicMerge:
  - ./manager.yaml
  • config/dev/manager.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: controller-manager
  namespace: system
spec:
  template:
    spec:
      securityContext: null

さてTiltfileの準備ができたので、次のコマンドでTiltを起動します。Tiltfileと同じディレクトリで実行してください。

tilt up

画面に表示されたURLをブラウザで開きます。
しばらくして、以下のようにすべてのリソースに緑色のチェックマークがつけば成功です。

以上で、カスタムコントローラーの開発の準備が整いました。

Live Update

それではさっそく、Live Update機能を利用してみましょう。

適当にソースコードを書き換えて保存してみてください。「Watch&Compile」と「markdown-view-controller-manager」がUpdatingになり、少し待つとUpdatedになると思います。
これだけで、変更したソースコードがビルドされ、Kubernetesクラスタ上に反映されています。

筆者の環境では3秒程度でコントローラーのビルドと反映が完了しました。

同様に、apisの一部を書き換えるとCRDファイルが更新され、マニフェストを書き換えるとKubernetes上のリソースが更新されます。

必要な部分だけを更新してくれるので非常に高速です。

片付け

使い終わったら後片付けしましょう。

tilt upを停止した後に以下のコマンドを実行すると、tilt upでデプロイされたマニフェストがKubernetesクラスタから削除されます。

tilt down

最後に以下のコマンドで、ローカルのコンテナレジストリとkindを停止します。

cd /path/to/kind-local
./teardown-kind-with-registry.sh

おわりに

Tiltを利用すると、ソースコードの変更が一瞬で反映されて動作確認できるのでとても便利で開発が捗ります。
ぜひ本記事を参考にして使ってみてください。

脚注
  1. ローカルコンテナレジストリを使っていないと、You are running Kind without a local image registry. Tilt can use the local registry to speed up builds.というメッセージが表示されます。 ↩︎

  2. このextensionを利用すると、Tiltfileの読み込みのたびにcert-managerがリロードされて時間がかかるので、extensionを使わずに自前でcert-managerをセットアップをしたほうがいいかもしれません。 ↩︎

  3. また、readOnlyRootFilesystemも無効にする必要があります。 ↩︎

  4. Tiltfile内でsecurityContextを削除するテクニックもあるようです https://github.com/tilt-dev/tilt/issues/3060#issuecomment-779726111 ↩︎

  5. Helmを使っている場合はこのIssueのコメントのようにするとよさそうです。 ↩︎

Discussion