Ritolabo
  1. Home
  2. Kubernetes
  3. Tilt
  4. Tiltfile と tilt up: Kubernetes 上のアプリにソースの変更を反映する

Tiltfile と tilt up: Kubernetes 上のアプリにソースの変更を反映する

  • 公開日
  • カテゴリ:Tilt
  • タグ:Kubernetes,Tilt,学習メモ
Tiltfile と tilt up: Kubernetes 上のアプリにソースの変更を反映する

ローカルの開発環境に作った kind のクラスタで自作の API を動かし、ソースを直すたびに image を作り直してクラスタに入れ直す作業を Tilt に任せたい。

この記事では、Go で書いた小さな API と Dockerfile、Deployment と Service の YAML を用意し、Tiltfile に 3 行を書いて tilt up を実行する。そのあと、ソースを 1 行変えて保存するだけで、image が作り直され、Pod が入れ替わり、http://localhost:8000/ の返事が変わるところまでを確認する。

contents

  1. 前提
  2. 動かすアプリ
    1. API のソース
    2. Dockerfile
    3. Deployment と Service
  3. Tiltfile
  4. tilt up
    1. tilt up で誰が何をしたか
  5. Tilt の管理画面
    1. ログ
  6. ソースの保存から Pod の入れ替えまで
  7. tilt down
  8. 開発用でないクラスタでの tilt up
  9. 参考 URL

前提

次の状態から始める。

  • Docker Desktop が起動している
  • kind のクラスタがあり、kubectl config current-context が kind-k8s-study のように kind のクラスタを指している
  • tilt コマンドが入っている(macOS なら brew install tilt。この記事は v0.37.8)

Tilt は Go で書かれたプログラム 1 つで、ローカルの開発環境で動く。クラスタの中には何も入れない。

動かすアプリ

Tilt に任せる前に、クラスタで動かすアプリと、Tilt に渡すファイルを用意する。ディレクトリの構成は次のとおり。

app/
├── main.go
├── go.mod
├── Dockerfile
├── .dockerignore
├── k8s/
│   ├── deployment.yaml
│   └── service.yaml
└── Tiltfile

Tiltfile 以外は、Tilt を使わないときと同じもの。Tilt のために追加したり変えたりしたファイルはない。

API のソース

GET / に 1 行の文字列を返すだけの API。8000 番で待ち受け、リクエストを 1 件受けるごとにログを 1 行出す。

// main.go
package main

import (
	"fmt"
	"log"
	"net/http"
)

const message = "Hello from Tilt"

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		log.Printf("%s %s", r.Method, r.URL.Path)
		fmt.Fprintln(w, message)
	})

	log.Println("listening on :8000")
	log.Fatal(http.ListenAndServe(":8000", nil))
}

あとで変えるのは const message の行だけ。

Dockerfile

2 段階の Dockerfile。1 段階目で Go のコンパイラが入った image の中で go build し、2 段階目でできた実行ファイル /hello-api-server だけを小さな image にコピーする。

# 1 段階目(build): Go のコンパイラが入った image の中でビルドする
FROM golang:1.25-alpine AS build
WORKDIR /src
COPY go.mod main.go ./
RUN CGO_ENABLED=0 go build -o /hello-api-server .

# 2 段階目(run): できた実行ファイルだけを、シェルも何も入っていない小さな image に入れる
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /hello-api-server /hello-api-server
EXPOSE 8000
USER nonroot
ENTRYPOINT ["/hello-api-server"]

Dockerfile には image の名前は書けない。image の名前は build するコマンドが付けるもので、この記事では次の Tiltfile が付ける。

.dockerignore には k8s/ と Tiltfile を書いておく。image に入れないファイルを build に渡さないための Docker の仕組みで、Tilt もこの一覧を見て、ここに書いたファイルが変わっても image を作り直さない。

Deployment と Service

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello-api
  template:
    metadata:
      labels:
        app: hello-api
    spec:
      containers:
        - name: api
          image: hello-api
          ports:
            - containerPort: 8000
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: hello-api
spec:
  selector:
    app: hello-api
  ports:
    - port: 8000
      targetPort: 8000

Deployment の image: は hello-api で、タグ(:1.0 のような末尾)を付けない。Tilt が image を作るたびに、この hello-api をタグ付きの名前に置き換えてから apply する。置き換えた名前は tilt up のあとに確認する。

Tiltfile

Tiltfile は、Tilt に「何を、どの順で行うか」を伝えるファイル。拡張子はなく、tilt up を実行するディレクトリに置く。Starlark という Python を小さくした言語で書く。今回は関数の呼び出しが 3 つ。

# Tiltfile
docker_build('hello-api', '.')
k8s_yaml(['k8s/deployment.yaml', 'k8s/service.yaml'])
k8s_resource('hello-api', port_forwards=8000)
行Tilt が行うこと自分で実行するなら
docker_build('hello-api', '.')カレントディレクトリの Dockerfile で image hello-api を作り、kind の Node に入れるdocker build -t hello-api . と kind load docker-image hello-api --name k8s-study
k8s_yaml([...])2 つの YAML をクラスタに apply する。YAML が変わったら apply し直すkubectl apply -f k8s/deployment.yaml -f k8s/service.yaml
k8s_resource('hello-api', port_forwards=8000)Deployment hello-api の Pod の 8000 番を localhost:8000 に転送する。Pod が入れ替わっても張り直すkubectl port-forward deployment/hello-api 8000:8000

Tiltfile に書いた名前のうち、YAML と一致させるものが 2 つある。

Tiltfile に書いた名前一致させる先
docker_build の 1 つ目の引数 'hello-api'k8s/deployment.yaml の spec.template.spec.containers[].image
k8s_resource の 1 つ目の引数 'hello-api'k8s/deployment.yaml の metadata.name

Tilt は、docker_build で作る image の名前と YAML の image: が同じものを探して結び付ける。公式ドキュメントの docker_build の説明にも、「この image を Kubernetes のリソースで使うなら、この名前は spec.container.image と一致していなければならない」とある(Tiltfile API Reference: docker_build)。

tilt up

Tiltfile のあるディレクトリで tilt up を実行する。実行したターミナルは Tilt が占有し続け、Ctrl+C で止めるまで動く。

$ tilt up
Tilt started on http://localhost:10350/
v0.37.8, built 2026-09-29

(space) to open the browser
(s) to stream logs (--stream=true)
(t) to open legacy terminal mode (--legacy=true)
(ctrl-c) to exit

ターミナルはこの案内を出して止まって見えるが、Tilt はこの時点で Tiltfile を読み、Docker に image を作らせ、kind の Node に入れ、YAML を apply している。kubectl で、クラスタの中に何ができたかを確認する。

$ kubectl get deployment,service,pods
NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/hello-api   1/1     1            1           57s

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/hello-api    ClusterIP   10.96.157.196   <none>        8000/TCP   57s
service/kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP    20m

NAME                             READY   STATUS    RESTARTS   AGE
pod/hello-api-5474f64fc7-pjj5z   1/1     Running   0          57s

kubectl apply は実行していない。Tilt が k8s_yaml の 2 ファイルを apply して、Deployment、Service、Pod が 1 つずつできている。

次に、Pod が使っている image の名前を確認する。YAML には image: hello-api と書いたが、クラスタに入った Deployment では違う名前になっている。

$ kubectl get deployment hello-api -o jsonpath='{.spec.template.spec.containers[0].image}'
# => hello-api:tilt-3d563a3e4ecbd50f

hello-api:tilt-<16 文字のハッシュ> の形。Tilt は image を作るたびに、image の内容から計算したハッシュをタグにして、YAML の image: hello-api をその名前に置き換えて apply する。ソースを変えて image を作り直すとハッシュが変わり、Deployment の image: が変わり、Pod が入れ替わる。

最後に、localhost:8000 に届くことを確認する。kubectl port-forward は実行していない。k8s_resource の port_forwards=8000 を見て Tilt が張っている。

$ curl http://localhost:8000/
# => Hello from Tilt

tilt up で誰が何をしたか

tilt up の裏で行われたことは、どれも Tilt が始めている。ただし Tilt は自分では image も Pod も作らず、別のプログラムを呼び出している。行われたことごとに、Tilt が何を呼び出したかを分ける。

行われたことTilt が呼び出したもの自分で実行するなら
Dockerfile から image hello-api:tilt-... を作るDockerdocker build
その image を kind の Node に入れるkind コマンド(kind load docker-image を子プロセスとして実行)kind load docker-image
YAML の image: をタグ付きの名前に置き換えて apply するクラスタの API サーバーkubectl apply
Pod の 8000 番を localhost:8000 に転送するクラスタの API サーバーkubectl port-forward
Deployment から Pod を作る(呼び出していない。apply された Deployment を見てクラスタが作る)(Tilt を使わなくても同じ)

image を kind の Node に入れるのは Docker ではなく、Tilt が実行する kind load docker-image。kind に image を渡すレジストリがないときの動きで、公式ドキュメントにも「レジストリが見つからなければ、Tilt はずっと遅い kind load で image を読み込む」とある(Choosing a Local Dev Cluster: Kind)。

Tilt の管理画面

tilt up のターミナルでスペースキーを押すと、ブラウザで http://localhost:10350/ が開く。これが Tilt の管理画面で、Tilt のプロセスがローカルの開発環境で出している画面。クラスタの中のものではない。

Tilt の管理画面の一覧。(Tiltfile) と hello-api の 2 行が表示されている

最初に開くのは一覧で、行が 2 つある。

行何か
(Tiltfile)Tiltfile そのものの読み込み。読み込みに失敗すると、ここがエラーになる
hello-apiTiltfile の 3 行で作られたまとまり。image hello-api の build、Deployment hello-api と Service hello-api の apply、port-forward

Tilt はこの 1 行を「resource」と呼ぶ。公式ドキュメントの定義は「Tilt が管理する作業のひとまとまり。たとえば、build する Docker image 1 つと、apply する Kubernetes の YAML」(Launching & Managing Resources: On Resources)。Kubernetes のリソース(Pod、Deployment)とは別の言葉で、Tilt が「一緒に更新するもの」としてまとめた単位。今回は Deployment の名前 hello-api がそのまま resource の名前になっている。Service は、Deployment の Pod と同じラベル(app: hello-api)を選んでいるので、同じ resource に自動で入る。

hello-api の行には、次が表示されている。

  • Status: 上の行が build と apply の状態(Updated in ...)、下の行がクラスタの中の Pod の状態(Runtime Ready)。Docker の build や apply が失敗すると上の行が、build は通ったがアプリが起動直後に落ちるような失敗は下の行がエラーになる
  • Pod ID: kubectl get pods で見えた Pod の名前
  • Endpoints: localhost:8000 のリンク。port_forwards=8000 で張ったもの
  • Mode: AUTO は「ファイルが変わったら自動で更新する」。切り替えて「手動で更新する」にもできる

ログ

hello-api の行をクリックすると、その resource の詳細に移る。中心はログで、Tilt と Docker が出した build のログと、Pod のコンテナが出したログ(kubectl logs で見えるものと同じ)が時間順に並ぶ。

tilt up の直後の build のログは次のとおり(Docker の build の詳細は省略)。

STEP 1/3 — Building Dockerfile: [hello-api]
Building Dockerfile for platform linux/arm64:
  FROM golang:1.25-alpine AS build
  ...

STEP 2/3 — Pushing hello-api:tilt-3d563a3e4ecbd50f
     Loading image to KIND
     Image: "docker.io/library/hello-api:tilt-3d563a3e4ecbd50f" with ID "sha256:3d56..." not yet present on node "k8s-study-control-plane", loading...
     Image: "docker.io/library/hello-api:tilt-3d563a3e4ecbd50f" with ID "sha256:3d56..." not yet present on node "k8s-study-worker", loading...
     Image: "docker.io/library/hello-api:tilt-3d563a3e4ecbd50f" with ID "sha256:3d56..." not yet present on node "k8s-study-worker2", loading...

STEP 3/3 — Deploying
     Applying YAML to cluster
     Objects applied to cluster:
       → hello-api:service
       → hello-api:deployment

     Step 1 - 6.91s (Building Dockerfile: [hello-api])
     Step 2 - 1.76s (Pushing hello-api:tilt-3d563a3e4ecbd50f)
     Step 3 - 0.06s (Deploying)
     DONE IN: 8.73s

STEP 1/3 が Docker の build、STEP 2/3 が kind の 3 つの Node への image の読み込み、STEP 3/3 が apply。「image の build が失敗したのか、Pod が起動に失敗したのか、アプリがエラーを出しているのか」を、ターミナルを行き来せずにこの 1 画面で見分けられる。

ソースの保存から Pod の入れ替えまで

main.go を 1 行変えて保存するだけで、image が作り直され、Pod が入れ替わり、localhost:8000 の返事が変わることを確認する。

Pod の入れ替わりを見るために、tilt up とは別のターミナルで kubectl get pods -w を実行しておく。-w を付けると、変化があった行が出続ける。

そのうえで、エディタで main.go の const message の行を変えて保存する。

const message = "Hello from Tilt, updated"

保存した瞬間から Tilt が動く。管理画面の hello-api の行が更新中になり、ログに STEP 1/3 から DONE IN までがもう一度流れる。kubectl get pods -w のターミナルには、新しい Pod ができて、古い Pod が消える行が出る。

$ kubectl get pods -w
NAME                         READY   STATUS    RESTARTS   AGE
hello-api-5474f64fc7-f7289   1/1     Running   0          5s
hello-api-7b49898c49-wcl8j   0/1     Pending   0          0s
hello-api-7b49898c49-wcl8j   0/1     Pending   0          0s
hello-api-7b49898c49-wcl8j   0/1     ContainerCreating   0          0s
hello-api-7b49898c49-wcl8j   0/1     ContainerCreating   0          1s
hello-api-7b49898c49-wcl8j   1/1     Running             0          1s
hello-api-5474f64fc7-f7289   1/1     Terminating         0          14s
hello-api-5474f64fc7-f7289   1/1     Terminating         0          14s

Pod の名前が hello-api-5474f64fc7-... から hello-api-7b49898c49-... に変わっている。真ん中の部分(ReplicaSet の名前)が変わったのは、Deployment の image: が新しいタグに書き換わり、Deployment が新しい ReplicaSet を作って Pod を入れ替えたため。

DONE IN が出たら、返事と image の名前を確認する。port-forward は Tilt が新しい Pod に張り直しているので、打ち直すものはない。

$ curl http://localhost:8000/
# => Hello from Tilt, updated

$ kubectl get deployment hello-api -o jsonpath='{.spec.template.spec.containers[0].image}'
# => hello-api:tilt-c6419db146539b95

image のタグが tilt up の直後とは変わっている。自分が行ったのは main.go を保存しただけで、docker build、kind load、kubectl apply、kubectl port-forward のどれも実行していない。

保存のたびに docker build と kind load が走るので、反映には毎回その時間がかかる。

tilt down

tilt up のターミナルで Ctrl+C を押すと Tilt のプロセスは止まるが、クラスタの中の Deployment と Service はそのまま残る。消すには、同じディレクトリで tilt down を実行する。Tiltfile の k8s_yaml で入れたものを kubectl delete 相当で消す。

$ tilt down

$ kubectl get deployment,service,pods
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   30m

kind のクラスタと、Docker と kind の Node にある image hello-api:tilt-... は残る。

開発用でないクラスタでの tilt up

Tilt は kubectl と同じ接続先に image を送り、YAML を apply する。接続先が開発用のクラスタだと確認できないときは、何も送らずに止まる。この止まり方を、接続先の名前だけを変えて確かめる。

接続先を決めているのは kubeconfig(~/.kube/config)。kubectl も tilt もこのファイルを読む。中には 3 つの一覧があり、kind create cluster がそれぞれに 1 件ずつ書いている(証明書の文字列は kubectl config view が省略して表示する)。

$ kubectl config view
apiVersion: v1
clusters:                                  # 接続先の一覧
- cluster:
    certificate-authority-data: DATA+OMITTED
    server: https://127.0.0.1:53430
  name: kind-k8s-study
contexts:                                  # どの cluster に、どの user でつなぐかの組
- context:
    cluster: kind-k8s-study
    user: kind-k8s-study
  name: kind-k8s-study
current-context: kind-k8s-study            # kubectl と tilt が今使う組
kind: Config
users:                                     # 認証情報の一覧
- name: kind-k8s-study
  user:
    client-certificate-data: DATA+OMITTED
    client-key-data: DATA+OMITTED

ここに、server は kind-k8s-study と同じ URL のまま、名前だけ prod にした cluster と context を足し、current-context を prod に切り替える。実務でこういう context を作ることはない。接続先は同じまま名前だけを変えて、Tilt が何を見て判定しているかを見やすくするための操作。tilt up が動いていれば、先に Ctrl+C で止めておく。

$ kubectl config set-cluster prod --server="$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')" --insecure-skip-tls-verify=true
# => Cluster "prod" set.

$ kubectl config set-context prod --cluster=prod --user=kind-k8s-study
# => Context "prod" created.

$ kubectl config use-context prod
# => Switched to context "prod".

kubeconfig は次のように変わる。clusters と contexts に 1 件ずつ増え、current-context が書き換わった。

clusters:
- cluster:
    certificate-authority-data: DATA+OMITTED
    server: https://127.0.0.1:53430
  name: kind-k8s-study
- cluster:                                 # 追加。server は同じ URL
    insecure-skip-tls-verify: true
    server: https://127.0.0.1:53430
  name: prod
contexts:
- context:
    cluster: kind-k8s-study
    user: kind-k8s-study
  name: kind-k8s-study
- context:                                 # 追加。cluster に prod を指定
    cluster: prod
    user: kind-k8s-study
  name: prod
current-context: prod                      # 書き換わった

kubectl get nodes は同じクラスタにつながるので、これまでどおり Node の一覧が返る。この状態で tilt up を実行すると、Tiltfile を読んだ直後に止まる。

$ tilt up
Loading Tiltfile at: /Users/rito/dev/k8s/03-tilt/02-tilt-up/app/Tiltfile
Stop! prod might be production.
If you're sure you want to deploy there, add:
	allow_k8s_contexts('prod')
to your Tiltfile. Otherwise, switch k8s contexts and restart Tilt.

image のビルドにも apply にも進んでいないので、クラスタには何も送られていない。

Tilt が見たのは名前だけで、順番は次のとおり。

  1. kubeconfig の current-context を読む。prod
  2. その context の cluster を読む。prod
  3. clusters の name が prod のものを見つけ、その名前が開発用のクラスタを作るツールの付ける名前かどうかを調べる

3 の判定は、Tilt が持っている「開発用のクラスタの名前のパターン」との照合で行われる。kind- で始まる、minikube で始まる、docker-desktop で始まる、k3d- で始まる、orbstack である、など(判定の実装は Tilt が使うライブラリ tilt-dev/clusterid にある)。kind-k8s-study は kind- に当たるので開発用と判定され、prod はどれにも当たらない。どれにも当たらない名前は、Tiltfile の allow_k8s_contexts に書かれていなければ止める。

つまり prod という文字を見て止めたのではない。名前が foo でも my-cluster でも同じエラーで止まり、逆に kind-prod なら kind- に当たるので通る。エラー文の prod might be production は、止めた context の名前をそのまま文に入れているだけで、Tilt は「本番だ」と分かったわけではなく「開発用だと確認できなかった」。server の URL は見ていないので、今回のように同じクラスタを指していても止まる。

開発用と分かっているクラスタの名前がこのパターンに合わないときは、Tiltfile に allow_k8s_contexts('<context 名>') を書いて許可する(Tiltfile API Reference: allow_k8s_contexts)。判定は名前だけなので、ここに本番の context 名を書けば本番に送れてしまう。書く名前は開発用と分かっているものに限る。

確認が済んだら、context を戻して prod を消す。

$ kubectl config use-context kind-k8s-study
# => Switched to context "kind-k8s-study".

$ kubectl config delete-context prod
# => deleted context prod from /Users/rito/.kube/config

$ kubectl config delete-cluster prod
# => deleted cluster prod from /Users/rito/.kube/config

まとめ

  • Tiltfile に docker_build・k8s_yaml・k8s_resource の 3 行を書き、tilt up を実行すると、image の作成、kind の Node への読み込み、YAML の apply、port-forward を Tilt がまとめて行う
  • docker_build の image の名前は YAML の image: と、k8s_resource の名前は Deployment の metadata.name と一致させる
  • Tilt は image を作るたびに tilt-<ハッシュ> のタグを付け、YAML の image: をその名前に置き換えて apply する。ソースを保存するとタグが変わり、Deployment が Pod を入れ替える
  • 管理画面(http://localhost:10350/)では、build と apply の状態、Pod の状態、ログを 1 画面で確認できる
  • tilt down で、Tiltfile で入れた Deployment と Service を消す
  • Tilt は kubeconfig の current-context が指す cluster の名前が、kind や Minikube など開発用のツールの付ける名前のパターンに当たるかで送るかどうかを決める。当たらない名前は allow_k8s_contexts に書かない限り止まる

参考 URL


[Prev] Tilt の概要と役割

Author

rito

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級