TiltでKubernetesローカル開発を高速化:ビルド・プッシュ・デプロイループから永遠に解放される

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

従来のKubernetes開発の問題点

一般的なKubernetesのローカルワークフローはこうだ:コードを1行変更する → docker build → イメージにタグを付けてプッシュ → YAMLマニフェストを適用 → ロールアウトを待つ → ログを確認する。1日20回繰り返すと、プログレスバーを眺める時間が30〜90分積み上がる。それは開発時間ではない。待機時間だ。

私はマイクロサービスプロジェクトで、まさにそのループの中に3週間いた——サービスが6つ、コンパイル言語が2種、そして日増しに焦れるチーム。やがてもっとよい方法を探し始め、行き着いたのがTiltだった。それはローカルKubernetes開発の考え方をまるごと変えてしまった。

従来のサイクル vs. Tilt:実際に何を比べているのか

この2つのアプローチは同じ問題を異なる方法で解決する。差を理解することで、セットアップにどれだけ投資すべきかの判断がしやすくなる。

従来のビルド・プッシュ・デプロイサイクル

手動のフローはおおむねこのようになる:

docker build -t myapp:dev .
docker push registry.local/myapp:dev
kubectl set image deployment/myapp myapp=registry.local/myapp:dev
kubectl rollout status deployment/myapp

これは機能する。だが、小さな変更のたびに最低でも1〜3分かかる。1日30回繰り返せば、待機時間の合計は30〜90分になる。コンパイル言語の場合——中規模のGoサービスはコールドビルドで2〜5分、Javaはさらに長くなることも多い——その差はすぐに広がる。

Tiltが同じワークフローをどう扱うか

TiltはPythonに似た設定言語であるStarlarkで書かれたTiltfileを導入し、開発ループ全体を定義する。ソースファイルを監視し、自動的に再ビルドをトリガーし、localhost:10350のブラウザベースのダッシュボードからログをストリーミングする。

# Tiltfile — 最小構成の例
docker_build('myapp', '.')
k8s_yaml('k8s/deployment.yaml')
k8s_resource('myapp', port_forwards=8080)

tilt upを1回実行したら、あとはそのままにしておける。ファイルを保存するたびに自動で再ビルドと再デプロイがトリガーされる。ダッシュボードはビルド状態、Podのログ、エラーをリアルタイムで表示する。最大の目玉機能はライブアップデートだ:Tiltは変更されたファイルを実行中のコンテナに直接同期し、イメージの再ビルドを完全にスキップできる。PythonやNode.jsのアプリであれば、変更が5秒以内に反映される。

メリットとデメリット

Tiltが本当に優れている点

  • イテレーション速度:ライブファイル同期により、インタープリタ言語のサイクルは10秒以内になる。再ビルドも再プッシュも不要——変更ファイルがコンテナに直接同期されるだけだ。
  • 初日から使える開発者体験tilt up1回で、何十もの手動ステップを置き換える。新しいチームメンバーがリポジトリをクローンすれば、数時間ではなく数分で動く環境が整う。ランブック不要。
  • 統合された可観測性:Tilt UIはすべてのサービスのPod状態、ビルドログ、テスト結果を1つのビューに集約する。5つのターミナルタブを行き来する必要がなくなる。
  • クラスター内デバッグ:Tiltから直接ポートフォワードして、IDEのデバッガーを実行中のPodにアタッチできる——kubectl execの煩わしい操作は不要だ。
  • CIとの一致tilt ciはCIパイプラインとまったく同じワークフローを実行する。ローカルで通れば、CIでも通る。「自分のマシンでは動く」という謎が解消される。

Tiltが苦手な点

  • Starlarkの学習コスト:シンプルな設定なら20分程度で完了する。条件ロジックを含む複雑なマルチサービス構成は、正しく整えるまでに数日かかることもあり、継続的なメンテナンスも必要だ。
  • メモリオーバーヘッド:Tiltは永続的なウォッチャープロセスを実行する。ローカルクラスター、Docker、IDEをすでに動かしているノートPCでは、その重さを感じるだろう——特にRAMが16GB未満の場合は。
  • チーム全体の採用が必要:チームの半数が手動デプロイを続けていると、Tiltfileは乖離していく。全員が使わなければ、管理者にとってのメンテナンス負担になる。

Tiltをスキップすべき場面

  • 単発のインフラ変更:Helmのアップグレード、RBACの編集、名前空間のセットアップ
  • ビルド自動化が上流ですでに処理されているCI/CDパイプライン
  • 共有レジストリからプルし、ローカルビルドを伴わない環境

推奨セットアップ

ローカルKubernetes開発で私がよく使うスタック:

  • k3d — Docker内で動作する軽量なk3sクラスター。30秒以内に起動し、クリーンにシャットダウンでき、minikubeと比べてわずかなリソースしか使わない。
  • Tilt — 開発ループ全体をオーケストレーションする
  • バンドルされたローカルレジストリ — 開発中にリモートへのプッシュが不要。Docker Hubのレート制限、認証タイムアウト、リモートホストへのプッシュの往復レイテンシを排除する。

ローカルレジストリは思った以上に大きな効果をもたらす。認証トークンの期限切れ、レジストリの不達、スロットルされたプルといった環境固有の障害をカテゴリごとまるごと排除し、実際のコードとは無関係なデバッグ時間の浪費をなくせる。

# k3dをインストール
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash

# バンドルされたローカルレジストリ付きのクラスターを作成
k3d cluster create devcluster \
  --registry-create myregistry:5000 \
  -p "8080:80@loadbalancer"

# Tiltをインストール(macOS / Linux)
curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash

# 動作確認
tilt version
kubectl cluster-info

実装ガイド

ステップ1 — プロジェクト構造

シンプルなPythonウェブサービスから始める。ディレクトリ構成はこのようになる:

myapp/
├── Dockerfile
├── Tiltfile
├── requirements.txt
├── k8s/
│   ├── deployment.yaml
│   └── service.yaml
└── src/
    └── main.py

ステップ2 — ライブアップデート付きのTiltfileを書く

# Tiltfile
version_settings(constraint='>=0.33.0')

docker_build(
  'localhost:5000/myapp',
  '.',
  live_update=[
    sync('./src', '/app/src'),
    run('pip install -r requirements.txt', trigger=['requirements.txt'])
  ]
)

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

k8s_resource(
  'myapp',
  port_forwards='8080:8080',
  labels=['app']
)

live_updateブロックが速度の源だ。保存のたびにDockerイメージを再ビルドする代わりに、Tiltは変更されたファイルのみを実行中のコンテナに同期する。Pythonファイルを編集して保存すると、イメージの再ビルドなしに3〜5秒でPodに変更が反映される。

ステップ3 — KubernetesデプロイメントYAML

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: localhost:5000/myapp
        ports:
        - containerPort: 8080
        env:
        - name: ENV
          value: development

ステップ4 — 開発ループを起動する

cd myapp/
tilt up

http://localhost:10350を開く。Tiltダッシュボードはすべてのリソースのビルド状態、リアルタイムPodログ、ポートフォワードの状態を1つのビューで表示する。ファイルを保存する。ターミナルに触れることなく同期が完了するのを見届けよう。

ステップ5 — クラスター内デバッグ

実行中のPodに本物のデバッガーをアタッチするには、Dockerfileにdebugpyを追加する:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt debugpy
COPY src/ ./src/
CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "--wait-for-client", "src/main.py"]

TiltfileでデバッグポートをフォワードForthする:

k8s_resource('myapp', port_forwards=['8080:8080', '5678:5678'])

VS CodeまたはPyCharmをlocalhost:5678に向ける。ファイルはコンテナに同期され、デバッガーは接続を維持する。実際に動いているKubernetes Podにブレークポイントを設定できる。print文デバッグは正式に引退だ。

複数サービスへのスケール

マイクロサービスプロジェクトでは、すべてのサービスを同じTiltfileに定義する:

services = ['auth-service', 'api-gateway', 'notification-service']

for svc in services:
  docker_build(
    'localhost:5000/' + svc,
    './' + svc,
    live_update=[sync('./' + svc + '/src', '/app/src')]
  )
  k8s_yaml('./' + svc + '/k8s/')
  k8s_resource(svc, labels=[svc])

tilt up1回で、3つのサービス全体が協調した再ビルドトリガーと統合ログストリーミングで立ち上がる。アプリと一緒にデータベースが必要?Helmリソースとして追加しよう:

helm_resource(
  'postgres',
  'bitnami/postgresql',
  flags=['--set', 'auth.password=devpassword'],
  port_forwards='5432:5432'
)

知っておく価値のある実践的なヒント

  • パイプラインでtilt ciを使う:ヘッドレスモードでは、すべてのリソースを起動し、準備完了を待ち、テストを実行し、ステータスコードで終了する。CIはローカルで実行するものとまったく同じワークフローを実行する——環境間のズレがない。
  • Tiltバージョンを固定する:すべてのTiltfileの先頭にversion_settings(constraint='>=0.33.0')を追加する。これがないと、1台のマシンでTiltをアップグレードしただけで、チーム全体の動作がサイレントに変わることがある。
  • ライブアップデートとヘルスチェックを組み合わせる:TiltはKubernetesのreadinessプローブを尊重する。アプリがホットリロードではなく同期後にプロセスを再起動する必要がある場合は、live_update内でrun('kill 1', trigger=[...])をトリガーする。
  • セッション間でクリーンにシャットダウンするtilt downはTiltが作成したすべてのリソースを削除する。クラスターをクリーンに保ち、残存状態が次のセッションで謎の障害を引き起こすのを防ぐ。

Tiltへの移行は単なるツールのアップグレードではない。フィードバックループが3分から10秒に縮まると、より小さな変更をコミットし、より自由に実験し、ミスが積み重なる前に気づくようになる。クラスターはデプロイではなく、開発のとなる。その変化は、いかなる個別機能よりも重要だ。

Share: