Ritolabo
  1. Home
  2. Kubernetes
  3. Tilt
  4. Tilt の Live Update: image を作り直さずにコンテナを更新する

Tilt の Live Update: image を作り直さずにコンテナを更新する

  • 公開日
  • カテゴリ:Tilt
  • タグ:Kubernetes,Tilt,学習メモ
Tilt の Live Update: image を作り直さずにコンテナを更新する

Tilt を使ってローカルの kind のクラスタでアプリを動かすと、ソースを保存するたびに image が作り直され、Pod が入れ替わる。1 行の変更でも、image の作成、kind の Node への読み込み、Pod の入れ替えが毎回行われる。

この記事では、Tilt の Live Update を使い、image も Pod も作り直さずに、動いているコンテナの中の実行ファイルだけを置き換えて反映する。Go で書いた小さな API で、保存してから返事が変わるまでに何が起きるかを確認する。

contents

  1. 前提
  2. Live Update
    1. Go のアプリで必要になること
  3. 動かすアプリ
    1. API のソース
    2. Linux 用の実行ファイル
    3. Dockerfile.tilt
    4. Deployment と Service
  4. Tiltfile
    1. restart_process 拡張
    2. local_resource
    3. docker_build_with_restart の引数
    4. resource_deps
  5. tilt up
  6. ソースの保存から反映まで
    1. 反映にかかった時間
    2. Pod が作り直されたとき
  7. tilt down
  8. 参考 URL

前提

次の状態から始める。

  • Docker Desktop が起動している
  • kind のクラスタがあり、kubectl config current-context が kind-k8s-study のように kind のクラスタを指している
  • tilt コマンドが入っている(この記事は v0.37.8)
  • ローカルの開発環境に Go が入っている(この記事は 1.25.0。macOS、Apple Silicon)

Live Update

Live Update は、image を作り直さずに、動いているコンテナの中のファイルを書き換えて反映する Tilt の機能。Tiltfile の docker_build に live_update として、次の手順を書く。

  • sync: ローカルの開発環境のファイルが変わったら、コンテナの中にコピーする
  • run: コピーのあとに、コンテナの中でコマンドを実行する

Kubernetes の Pod は Node の上で動き、volume としてつなげるのも Node のディレクトリなので、Docker Compose のようにローカルの開発環境のソースのディレクトリをそのままコンテナにつなぐことはできない。Live Update は、変わったファイルをその都度コンテナにコピーすることで、これに近い動きをする。

Go のアプリで必要になること

PHP や Node.js のように、ソースのファイルをそのまま読んで動く言語なら、ファイルをコピーするだけで反映される。Go の場合、コンテナで動いているのはソースではなく go build で作った実行ファイルなので、保存のたびに次の 3 つが必要になる。

  1. go build で実行ファイルを作り直す
  2. 実行ファイルをコンテナにコピーする
  3. コンテナの中のプログラムを起動し直す(動いているプログラムは、実行ファイルを置き換えても古いまま動き続けるため)

この記事では、Tilt の公式ドキュメントの Go の例(Example: Go)と同じく、1 をローカルの開発環境で行い、2 を sync、3 を Tilt の拡張 restart_process で行う。

動かすアプリ

ディレクトリの構成は次のとおり。

app/
├── main.go
├── go.mod
├── Dockerfile          # 本番用
├── Dockerfile.tilt     # Tilt で開発するときだけ使う
├── .dockerignore
├── .gitignore
├── k8s/
│   ├── deployment.yaml
│   └── service.yaml
└── Tiltfile

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 の行だけ。

Linux 用の実行ファイル

go build はローカルの開発環境で行う。kind の Node とその上のコンテナは Linux で動いているので、macOS で何も指定せずに作った実行ファイルは動かない。GOOS=linux を付けて Linux 用の実行ファイルを作る。

$ CGO_ENABLED=0 GOOS=linux go build -o build/hello-api-server .

$ file build/hello-api-server
# => build/hello-api-server: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, ...
  • ELF: Linux 用の実行ファイルの形式
  • ARM aarch64: CPU の種類。GOARCH を指定していないので、ローカルの開発環境(Apple Silicon)と同じものになる。kind の Node もローカルの開発環境の Docker の上で動いているので、CPU は同じ
  • statically linked: CGO_ENABLED=0 を付けたので、C 言語のライブラリに頼らない 1 つの実行ファイルになっている

このコマンドは、あとで Tiltfile に書いて Tilt に実行させる。できる build/ は .gitignore に書いて Git に入れない。

Dockerfile.tilt

Tilt が image を作るときに使う Dockerfile。ローカルの開発環境で作った実行ファイルを alpine にコピーするだけで、go build は行わない。

# Dockerfile.tilt
FROM alpine:3.24
WORKDIR /app
COPY build/hello-api-server /app/build/hello-api-server
EXPOSE 8000
ENTRYPOINT ["/app/build/hello-api-server"]

本番用の Dockerfile は別に持つ。こちらは Dockerfile の中で go build し、できた実行ファイルだけを gcr.io/distroless/static-debian12:nonroot に入れる 2 段階のもの。

Dockerfile(本番用)Dockerfile.tilt(開発用)
go buildDockerfile の中で行う行わない。ローカルの開発環境で作った実行ファイルをコピーする
土台の imagedistroless(シェルも tar も入っていない最小の image)alpine(シェルと tar が入った小さな Linux)

開発用の土台を alpine にするのは、Live Update がコンテナの中のコマンドを使うため。

  • Tilt はファイルをコピーするとき、コンテナの中で tar を実行してファイルを受け取らせる
  • restart_process 拡張は、プログラムを起動し直す合図をコンテナの中のシェル(sh)で送る

distroless にはどちらも入っていない。拡張の README にも、scratch や distroless のようなシェルのない image には対応しないとある(restart_process: Unsupported Cases)。

そのため、Tilt が作る開発用の image と、本番用の image は中身が異なる。本番用の image は CI などが Dockerfile から作り、開発の都合(シェル、ローカルの開発環境で作った実行ファイル)を持ち込まない。

Deployment と Service

YAML は Live Update のために変えるところはない。image: はタグを付けずに hello-api と書き、Tilt が作った image のタグ付きの名前に置き換えて apply する。

# 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

Tiltfile

# Tiltfile
load('ext://restart_process', 'docker_build_with_restart')

local_resource(
    'hello-api-compile',
    'CGO_ENABLED=0 GOOS=linux go build -o build/hello-api-server .',
    deps=['main.go', 'go.mod'],
)

docker_build_with_restart(
    'hello-api',
    '.',
    dockerfile='Dockerfile.tilt',
    entrypoint=['/app/build/hello-api-server'],
    only=['./build'],
    live_update=[
        sync('./build', '/app/build'),
    ],
)

k8s_yaml(['k8s/deployment.yaml', 'k8s/service.yaml'])

k8s_resource('hello-api', port_forwards=8000, resource_deps=['hello-api-compile'])

restart_process 拡張

1 行目の load は、Tilt の拡張 restart_process から関数 docker_build_with_restart を読み込む。拡張は Tiltfile に関数を追加する部品で、公式のものは GitHub の tilt-dev/tilt-extensions で配布されている。tilt up のときに Tilt が GitHub からコードを取得する。

docker_build_with_restart は、docker_build に次の 2 つを加えたもの。

  • image に小さなプログラム tilt-restart-wrapper を追加し、コンテナの起動コマンドを「tilt-restart-wrapper が entrypoint のコマンドを起動する」形に変える。tilt-restart-wrapper は、ファイル /tmp/.restart-proc が書き換わると、起動したプログラムを止めて起動し直す
  • live_update の最後に run('date > /tmp/.restart-proc') を追加する。コピーが終わるたびにこのファイルが書き換わり、プログラムが起動し直す

コンテナの中は次の形になる。コンテナが最初に起動したプログラムは tilt-restart-wrapper で、これは動き続ける。起動し直すのは、その下の hello-api-server だけ。

コンテナ
└── tilt-restart-wrapper   # コンテナが最初に起動したプログラム。動き続ける
    └── hello-api-server   # tilt-restart-wrapper が起動したアプリ。Live Update のたびに起動し直す

local_resource

local_resource('名前', 'コマンド', deps=[...]) は、ローカルの開発環境でコマンドを実行する resource(Tilt の管理画面の一覧の 1 行)を作る。deps に書いたファイルが変わるたびに、コマンドを実行し直す。

ここでは、main.go か go.mod が変わるたびに、Linux 用の実行ファイル build/hello-api-server を作り直す。

docker_build_with_restart の引数

引数意味
'hello-api'、'.'image の名前と、image を作るときに Docker に渡すディレクトリ(docker_build と同じ)
dockerfile='Dockerfile.tilt'開発用の Dockerfile を使う
entrypoint=[...]起動するコマンド。tilt-restart-wrapper が起動し直すのはこのコマンド
only=['./build']Docker に渡すのは build/ だけにする
live_update=[sync('./build', '/app/build')]build/ の中身が変わったら、コンテナの /app/build/ にコピーする

ファイルが変わったとき、Tilt は次の規則で Live Update と image の作り直しを振り分ける(Live Update Reference)。

  • sync に書いたディレクトリの中のファイル: コンテナにコピーする(Live Update)
  • sync の外で、docker_build が見張っているディレクトリの中のファイル: image を作り直す

only がなければ、docker_build は '.' の中を全部見張る(.dockerignore に書いた k8s/ と Tiltfile は除く)。main.go は sync の ./build の外なので、保存した時点で image の作り直しになる。only=['./build'] で見張るのを build/ だけにすると、main.go の変更は hello-api-compile だけが受け取り、その結果の build/hello-api-server の変更が Live Update になる。

resource_deps

最初の tilt up では、build/hello-api-server ができるまで image を作れない(Dockerfile.tilt がコピーするファイルがない)。k8s_resource の resource_deps=['hello-api-compile'] で、hello-api-compile が終わってから hello-api の image を作るように順番を決める。

tilt up

Tiltfile のあるディレクトリで tilt up を実行する。Tilt の管理画面(http://localhost:10350/)の一覧には 3 行が並ぶ。

行何か
(Tiltfile)Tiltfile の読み込み
hello-api-compilelocal_resource。ローカルの開発環境で go build を実行する
hello-apiimage の作成、YAML の apply、port-forward

hello-api-compile が終わると、hello-api が image を作る。最初の 1 回は Live Update ではなく、image を 2 つ作る。

  1. Dockerfile.tilt から、元になる image hello-api-tilt_docker_build_with_restart_base を作る
  2. 1 の image に tilt-restart-wrapper を加えた image hello-api を作る。kind の Node に入るのはこちら

hello-api のログは次のとおり(Docker の build の詳細は一部省略)。

STEP 1/5 — Building Dockerfile: [hello-api-tilt_docker_build_with_restart_base]
Building Dockerfile for platform linux/arm64:
  FROM alpine:3.24
  WORKDIR /app
  COPY build/hello-api-server /app/build/hello-api-server
  EXPOSE 8000
  ENTRYPOINT ["/app/build/hello-api-server"]
  ...

STEP 2/5 — Pushing hello-api-tilt_docker_build_with_restart_base:tilt-b42ccfdcdda9e6d0
     Skipping push: base image does not need deploy

STEP 3/5 — Building Dockerfile: [hello-api]
Building Dockerfile for platform linux/arm64:

  FROM tiltdev/restart-helper:2026-10-21 as restart-helper

  FROM hello-api-tilt_docker_build_with_restart_base:tilt-b42ccfdcdda9e6d0
  RUN ["touch", "/tmp/.restart-proc"]
  RUN ["chmod", "666", "/tmp/.restart-proc"]
  COPY --from=restart-helper /tilt-restart-wrapper /
  COPY --from=restart-helper /entr /
  ENTRYPOINT ["/tilt-restart-wrapper", "--watch_file=/tmp/.restart-proc", "/app/build/hello-api-server"]
  ...

STEP 4/5 — Pushing hello-api:tilt-0f51adab9627cc1c
     Loading image to KIND
     ...

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

     Step 1 - 3.25s (Building Dockerfile: [hello-api-tilt_docker_build_with_restart_base])
     Step 2 - 0.00s (Pushing hello-api-tilt_docker_build_with_restart_base:tilt-b42ccfdcdda9e6d0)
     Step 3 - 2.85s (Building Dockerfile: [hello-api])
     Step 4 - 1.98s (Pushing hello-api:tilt-0f51adab9627cc1c)
     Step 5 - 0.06s (Deploying)
     DONE IN: 8.13s

STEP 3/5 の Dockerfile は、拡張が作ったもの。最後の ENTRYPOINT で、コンテナの起動コマンドが tilt-restart-wrapper に変わっている。

DONE IN が出たら、Pod、返事、image の名前を確認する。

$ kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
hello-api-66bc6695bd-pmtb7   1/1     Running   0          65s

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

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

ソースの保存から反映まで

main.go の const message の行を変えて保存する。

const message = "Hello from Tilt, live updated"

保存すると、Tilt の管理画面で hello-api-compile と hello-api が順に動く。ログは次のとおり(ファイルのパスは一部省略)。

hello-api-co… │ 1 File Changed: [main.go]
hello-api-co… │ Running cmd: CGO_ENABLED=0 GOOS=linux go build -o build/hello-api-server .
    hello-api │ 1 File Changed: [build/hello-api-server]
    hello-api │ Will copy 1 file(s) to container: [hello-api-66bc6695bd-pmtb7/api]
    hello-api │ - '.../app/build/hello-api-server' --> '/app/build/hello-api-server'
    hello-api │ [CMD 1/1] sh -c date > /tmp/.restart-proc
    hello-api │   → Container hello-api-66bc6695bd-pmtb7/api updated!
    hello-api │ 2026/10/25 03:16:11 listening on :8000
  • hello-api-compile が main.go の変更を受け取り、ローカルの開発環境で go build を実行した
  • hello-api が build/hello-api-server の変更を受け取り、コンテナの /app/build/hello-api-server にコピーした(sync)
  • [CMD 1/1] sh -c date > /tmp/.restart-proc は拡張が追加した run。これを受けて tilt-restart-wrapper がプログラムを起動し直し、listening on :8000 がもう一度出ている

tilt up のときのログにあった Building Dockerfile、Loading image to KIND、Applying YAML to cluster は出ていない。

返事、Pod、image の名前を、tilt up のあとと同じコマンドで確認する。

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

$ kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
hello-api-66bc6695bd-pmtb7   1/1     Running   0          5m16s

$ kubectl get deployment hello-api -o jsonpath='{.spec.template.spec.containers[0].image}'
# => hello-api:tilt-0f51adab9627cc1c
tilt up のあと保存のあと
curl の返事Hello from TiltHello from Tilt, live updated
Pod の名前hello-api-66bc6695bd-pmtb7同じ
RESTARTS00
image のタグhello-api:tilt-0f51adab9627cc1c同じ
  • Pod の名前が同じなので、Pod は入れ替わっていない
  • RESTARTS が 0 のままなので、コンテナも起動し直していない。起動し直したのは、コンテナの中の hello-api-server だけ
  • image のタグが同じなので、image は作られていない。コンテナの中の実行ファイルだけが新しく、image の中身とは異なる状態になっている

反映にかかった時間

Live Update のログには、tilt up のときの DONE IN に当たる行が出ない。resource ごとの更新の開始と終了の時刻は、tilt get で確認できる(新しい順に並ぶ)。

$ tilt get uiresource hello-api-compile -o jsonpath='{range .status.buildHistory[*]}{.startTime} {.finishTime}{"\n"}{end}'
2026-10-25T03:16:09.592461Z 2026-10-25T03:16:10.960506Z
2026-10-25T03:11:25.668892Z 2026-10-25T03:11:26.566798Z

$ tilt get uiresource hello-api -o jsonpath='{range .status.buildHistory[*]}{.startTime} {.finishTime}{"\n"}{end}'
2026-10-25T03:16:10.932556Z 2026-10-25T03:16:11.148685Z
2026-10-25T03:11:26.568220Z 2026-10-25T03:11:34.703245Z

1 行目が保存のときの更新。hello-api-compile(go build)が約 1.4 秒、hello-api(コピーと起動し直し)が約 0.2 秒で、保存から反映まで約 1.6 秒だった。2 行目の tilt up のときの hello-api は、image の作成を含めて約 8.1 秒。

Pod が作り直されたとき

Live Update で置き換わったのは、動いているコンテナの中のファイルだけ。Pod が作り直されると、新しいコンテナは image から起動するので、tilt up のときに image に入れた実行ファイルで動く。

tilt up をやり直したときは、最初の 1 回は必ず image を作るところから行う(Live Update Reference)。そのときは hello-api-compile が今の main.go から実行ファイルを作り直すので、新しい image には最新の実行ファイルが入る。

tilt down

tilt up のターミナルで Ctrl+C を押してから、同じディレクトリで tilt down を実行すると、Tiltfile の k8s_yaml で入れた Deployment と Service が消える。

$ tilt down

ローカルの開発環境の build/hello-api-server と、Docker の image(hello-api:tilt-...、hello-api-tilt_docker_build_with_restart_base:tilt-...、tiltdev/restart-helper)は残る。

まとめ

  • Live Update は、image を作り直さずに、動いているコンテナの中のファイルを書き換えて反映する。docker_build の live_update に sync(コピー)と run(コンテナの中のコマンド)を書く
  • Go のアプリでは、local_resource でローカルの開発環境で Linux 用の実行ファイルを作り、sync でコンテナにコピーし、restart_process 拡張の docker_build_with_restart でプログラムを起動し直す
  • restart_process 拡張と Live Update のコピーはコンテナの中のシェルと tar を使うので、開発用の image は distroless ではなく alpine などにする。本番用の Dockerfile とは分ける
  • sync の外で docker_build が見張っているファイルが変わると、image の作り直しになる。only で見張るのを build/ に絞り、main.go の変更を local_resource だけに受け取らせる
  • 保存のあとも、Pod の名前・RESTARTS・image のタグは変わらない。起動し直すのはコンテナの中のプログラムだけ

参考 URL


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

Author

rito

rito

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