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

ローカルの開発環境に作った kind のクラスタで自作の API を動かし、ソースを直すたびに image を作り直してクラスタに入れ直す作業を Tilt に任せたい。
この記事では、Go で書いた小さな API と Dockerfile、Deployment と Service の YAML を用意し、Tiltfile に 3 行を書いて tilt up を実行する。そのあと、ソースを 1 行変えて保存するだけで、image が作り直され、Pod が入れ替わり、http://localhost:8000/ の返事が変わるところまでを確認する。
contents
前提
次の状態から始める。
- 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-... を作る | Docker | docker 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 のプロセスがローカルの開発環境で出している画面。クラスタの中のものではない。

最初に開くのは一覧で、行が 2 つある。
| 行 | 何か |
|---|---|
(Tiltfile) | Tiltfile そのものの読み込み。読み込みに失敗すると、ここがエラーになる |
hello-api | Tiltfile の 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 が見たのは名前だけで、順番は次のとおり。
- kubeconfig の
current-contextを読む。prod - その context の
clusterを読む。prod 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
- [official] Install | Tilt
- [official] Launching & Managing Resources | Tilt
- [official] Tilt UI | Tilt
- [official] Tiltfile API Reference: docker_build | Tilt
- [official] Tiltfile API Reference: k8s_yaml | Tilt
- [official] Tiltfile API Reference: k8s_resource | Tilt
- [official] Example: Go | Tilt
- [official] Tiltfile API Reference: allow_k8s_contexts | Tilt
- [official] Choosing a Local Dev Cluster | Tilt
- [official] GitHub - tilt-dev/clusterid: Kubernetes cluster detection, for tools that interact with local clusters
[Prev] Tilt の概要と役割

