マイクロサービスのローカル開発をTiltでちょっと楽にする

こんにちは、バックエンドエンジニアの鈴木(善)です。

昨年に引き続き、今年もHRBrainではアドベントカレンダーをやっていきます!

qiita.com

この記事はその第一日目となります。

みなさん、マイクロサービスの開発ってどうされてますか?

HRBrainでは現在マイクロサービス構成をとっており、日々の開発では個人のマシン上で各サービスを立ち上げて動作確認やデバッグができるようになっています(以下、ローカル開発環境と呼びます)。サービスが成長するとともにこの環境にも課題が出てきました。
これを改善するため「Tilt」というソフトウェアを試したところよかったので、今はTiltベースのローカル開発環境に乗り換えています。
Tiltに関する日本語の情報もまだ少なそうなので、今回は一つの導入事例として紹介してみようと思います。

本記事ではまず弊社の抱えていた課題に触れ、次にTiltの概要を説明します。最後に改善後の構成とそこで得られた知見(効果やTips等)を紹介します。

【注】理想的には、開発したいサービスを1つだけ個人のマシンで立ち上げればよい状態を目指していますがそこはまだ道半ばです。また、Telepresence等のリモート環境の一部を置き換える方法もありますが、そことの比較検討はこれからのため、本記事には含まれておりません🙏

弊社が抱えていたローカル開発環境の課題

改善前の構成

改善前、弊社の構成は次のようになっていました。(ローカル開発環境に関するもののみ抜粋し、簡略化して記載しています)

root
├── apps
│   ├── serviceA
│   │   ├── backend
│   │   │   ├── Dockerfile
│   │   │   └── docker-compose.yaml
│   │   └── frontend
│   │       ├── Dockerfile
│   │       └── docker-compose.yaml
│   ├── serviceB
│   │   ├── ...
│   └── serviceC
│       └── ...
└── up.sh

serviceA/backendserviceB/frontendなどが各マイクロサービスの単位です。改善前はそれぞれの下にdocker-compose.yamlDockerfileがありました。
このようになっている背景として、開発当初それぞれ独立したリポジトリに分けていたものを、1つに統合してモノレポ化したという経緯もあったりします。

times.hrbrain.co.jp

そしてroot配下に、すべてのサービスを立ち上げるためのシェルスクリプト(up.sh)があるという構成でした。

抱えていた課題

【課題1】「必要なサービスだけ起動する」がだんだん大変になってきた

弊社では、事業の成長とともにサービス数も約1年半で8個から29個に増えました。サービス間の依存関係もそれなりに増えていっています。
docker-compose.yamlは各サービスに分かれている(前述)ため、サービス間の依存関係はソースコード上に表現できておらず、ドキュメントでカバーされている状態でした。

サービス数の少ない頃は何も考えずにすべて立ち上げても問題はありませんでした。ただサービスが増えるに連れ、マシンリソース的にそれも難しくなり、開発したいものが依存しないサービスを起動させない運用も必要になっていました。

【課題2】ホットリロードが不安定

バックエンドは主にGoで開発しています。airなどを使い、ホットリロードもできるようにしていました。
ただ、M1 Macが出始めたあたりから「ホットリロードがたまに失敗する」という声が出てきました*1

Tiltを試すきっかけ

これらの課題に対し、【課題1】のみであればdocker-compose.yamlを1つにまとめるだけでも解決できます。Tiltには、 live_update というホットリロードの仕組み(後述)も備わっていたため、「もしかしたらホットリロードの問題(【課題2】)も解決できるかも?」という期待もあり試してみることにしました。

Tiltとは?

tilt.dev

Tiltとはマイクロサービスの開発を支援してくれるソフトウェアです。
「支援」というのは、例えば

  • コード変更を検知してコンテナを再ビルドしてくれるので、すぐ動作確認できる。
  • TiltのUIで、ビルドエラーやランタイムエラーを素早く確認できるため、コード変更にミスがあってもすぐ気付ける。
  • コマンド一発で必要なマイクロサービスを起動できるようになるため、新入社員のオンボーディングも楽になる。

などがあります。

2分の紹介動画もあります。こちらを観ると雰囲気をつかみやすいかと思います*2

www.youtube.com

どんな方向けのソフトウェアかは以下に書かれているので、気になる方は参照してみてください。
Who is Tilt for? | Tilt

2021年12月現在、Tiltは1stリリースから約3年ほど経過しています(1stリリースは2018年10月)。直近では1〜2週間単位でリリースが続いています(リリース一覧)。活発に開発されているのもいいですね。
TiltはGo+TypeScriptで書かれており、macOS/Linux/Windowsの3つのプラットフォームに対応しています。

Tilt's Control Loop

Tiltでは、Tiltfileという設定ファイル(後述)をロードするところから始まり、最後はコンテナがKubernetesクラスター等にデプロイされます。そしてコード変更などを起点として再ビルドも行われます。
ガイドでは、この一連の流れをTilt's Control Loopとして説明しています。

このControl Loopには、中心的な概念としてResourceというものがあります。 この用語は設定を書く際の関数の名前にも出てくる(k8s_resource, local_resource等)ので、チュートリアルをやった後ぐらいにでも目を通しておくのがおすすめです。

なお、本記事でのデプロイ先はローカルマシンですが、Tiltではリモート環境へのデプロイも対応しています。
気になる方は以下のガイドを参照してみてください。

live_update

live_updateという機能があります。これを活用すると、コード変更から動作確認までのループをさらに早められ、個人的におすすめなので取り上げてご紹介します。
(仕組みの詳細はこちらを参照ください)

まずこの機能を使っていない場合は、ファイルの変更を検知するとコンテナが再ビルドされます。コンテナなので、実行バイナリの再ビルドと比べ時間がかかります。 live_updateを設定すると、実行バイナリだけを再ビルド&再起動できるようになります。そのため動作確認までの待ち時間も短縮できます。
live_updateで設定したディレクトリ配下のコードが変更されると、具体的には以下のことが行われます。

  1. 変更されたコードのみがコンテナ側にコピーされる。
  2. 指定したコマンド(通常は実行バイナリを再ビルドするコマンド)が実行される。
  3. プロセスが再起動される。(ガイド)

もし、既にホットリロードが備わっている仕組み(例:webpack-dev-server)で動かしているコンテナの場合は、上記1(ファイルコピー)のみ実行させることも可能です。

設定ファイル

Tiltでは、次の2種類のファイルを組み合わせて動作を定義します。

  • Tiltfile
  • Kubernetesのマニフェスト または docker-compose.yaml

Tiltfileは設定のエントリポイントなるファイルです。Starlark(Pythonの方言)で記述します。自分はStarlarkもPythonも未経験でしたが、基本的な文法ぐらいであればStarlarkのREADMEを一読するぐらいでなんとかなります。Tiltfileを書く学習コストはそこまで高くないかなというのが個人的な印象です。
コンテナのイメージや環境変数など、コンテナの設定についてはTiltfileではなくKubernetesのマニフェストやdocker-compose.yamlに記述します。

ドキュメントは、docker-compose.yamlではなくKubenetesのマニフェストと組み合わせる方法でほとんど書かれています。なのでKubernetesのマニフェストと組み合わせるのがメインの使い方のようです。docker-compose.yamlのほうは、これまで資産を持ってた人が乗り換えやすくするための移行手段という位置付けと思われます。

起動と停止

設定ができると、tilt upというコマンドでローカルに環境が立ち上がるようになります。
また、Tilt自体がサーバーになり以下のような管理UIも提供してくれます。こちらからログの確認やコンテナの手動再ビルドなどが可能です。
(apiwebの2つのコンテナを動かしている例です)

f:id:hrb-suzuki-yoshiharu:20211125223917p:plain
Tilt UI

停止はtilt downというコマンドからできます。

チュートリアル・サンプルコード

チュートリアルもあります。段階的に発展する内容でわかりやすいです。
(新しいチュートリアルもできてました。こちらからスタートするほうがよいかもしれません。)
また、以下の言語のサンプルもあるので、言語マッチする方は動かしてみるのもおすすめです。

  • Plain Old Static HTML
  • Go
  • NodeJS
  • Python
  • Java
  • C#
  • Bazel

Tiltを使った改善後の構成

改善後のTilt構成を紹介します。

ファイル構成は次のようになりました。(ローカル開発環境に関するもののみ抜粋し、簡略化して記載しています)

root
├── Titlfile
├── apps
│   ├── serviceA
│   │   ├── backend
│   │   │   └── Dockerfile
│   │   └── frontend
│   │       └── Dockerfile
│   └── serviceB
│   │   └── ...
│   └── serviceC
│       └── ...
└── docker-compose.yaml

docker-compose.yamlはリポジトリのルートに統合され、Tiltfileが新たにできました。各サービスのディレクトリにはDockerfileのみがあるという構成です。
もともとdocker-composeを使っていたこともあり、Kubenetesのマニフェストではなくdocker-compose.yamlと組み合わせています。

Tiltfileとdocker-compose.yamlは次のように責務を分けて記述しています。

  • Tiltfile
    • dockerビルドの設定、および再ビルドの走る条件
    • live_updateに関する設定
    • コンテナ同士の依存関係
  • docker-compose.yaml
    • dockerイメージ名
    • 公開するポート
    • 環境変数
    • ソースコード以外でマウントの必要なディレクト(DBのデータ等)

上記の構成において、例えばserviceAserviceBに依存し、さらにserviceBserviceCに依存している場合、

tilt up serviceA

と指定するだけで、最初にserviceCが起動し、続いてserviceB、最後にserviceAが順に起動します。

またTiltでは、このような依存関係とは別に、まとめて立ち上げたいサービスをグルーピングしておく仕組みもあります。一部この機能を使っていたりもします。

旧方式からの移行

Tiltを導入するにあたっては、旧方式と併用できるようにしてチームに徐々に浸透させていきました。
このあたりは開発でつかっているツールにもよると思いますが、弊社では次のように行いました。

  • Tilt方式に切り替える環境変数を1つ用意し、Tiltを試したい人はそれを有効にしてもらう。
  • Makefileで共通化できないものはifdefで切り替える。
  • Dockerfile内のCMDENTRYPOINT命令は書き換えずに、新設したTilt用docker-compose.yamlから上書きして使う。

導入してみてどうだった?

実際に導入してみて見えた効果や課題をいくつかご紹介します。

効果

■サービス間の依存を気にかける必要がなくなった

前述の『【課題1】「必要なサービスだけ起動する」がだんだん大変になってきた』に対する効果です。
依存関係情報を設定ファイルに定義できたことで、tilt up serviceAのようなコマンド一発で、起動したいサービスとそれが依存する最小限のもののみを立ち上げられるようになりました。 日々の開発で「アレを起動したいから、まずアレとアレを立ち上げて…」みたいなことを考えることから解放されました!

■ホットリロードが安定した

前述の『『課題2』ホットリロードが不安定』に対する効果です。
Tiltのlive_updateの仕組みに乗り換えて3ヶ月ほど経ちますが、ホットリロードは安定しています。 go generate等で大量のファイルを自動作成(または更新)するケースでも、ファイルの変更ある程度まとめて検知してくれます。再ビルドの頻度が抑えられているのも嬉しいところです。(もしかしたらGoのホットリロードツールでできていたかもしれませんが…)

■ログの内容を探しやすくなった

使ってみて意外と便利だったのが、Tilt UIのログをフィルタする機能です。
コンソールに吐かれたログの内容を正規表現でフィルタして表示することができます。 DEBUGレベルでログを出力させていると、一つの操作でも一気にログが吐かれ見たいものが流れてしまいがちですが、フィルタして表示できるのでエラーログなどの確認がとても楽です。

課題

導入してみて「ちょっと不便かも」や「何か考えないとだめそう」と感じた点です。

■kubernetesのコンテキストの切り替え

Tiltを動かすのにDocker Desktopを使うmacOSとWindowsでは、Kubernetesのクラスタをローカルのものにしておく必要があります。 (未検証ですがLinuxの場合は不要)
具体的には次のコマンド実行が必要です。

kubectl config use-context docker-desktop

業務上、別のクラスタも使う方にとっては、Tiltを起動する際にクラスタの切り替えの一手間が必要になります。

この辺りの話はインストールガイドに書かれています。

■Tiltfileやdocker-compose.yamlが肥大化しがち

1つのTiltfileやdocker-compose.yamlにまとめていると、サービスの増加とともにどうしても肥大化し、見通しが悪くなりやすいです。
弊社ではまだリファクタリングしていませんが、loadload_dynamicという関数を使えばTiltfileは分離できそうなので、いずれはやろうかなと思っています。

■Ready for serviceになるまで待つ一工夫が必要な場合も

Tilt固有の話ではないですが、プロセスが起動を初めてからリクエストを受付可能(Ready for service)になるまで時間のかかるようサービスの場合、一工夫必要かもしれません。
Tiltは、DockerfileのCMDENTRYPOINT命令が終わるとそのサービスは完了とみなし、次のサービスの起動を始めます。 そのため、後段のサービスの起動が、前段サービスの起動を追い抜いてしまう場合があります。 「Ready for serviceになったらCMD命令を完了とする」や「前段がReady for serviceになるまで待つ」といったことの配慮が必要になる場合があります。

その他Tips

導入にあたり調べた点をTipsとして紹介します。

■モノレポでも適用できるの?

できます。
こちらの公式ブログ記事に方法が紹介されています。 ファイル変更を監視するディレクトリをサービスごとに限定することができます。

■変更を検知させたくないファイルがあるんだけど無視できる?

できます。
こちらのガイドに方法が載っています。方法はいくつかありますが、弊社では.dockerignoreを使う方法でとくに困ってません。

■docker-compose.yamlを使う場合、記述方法が一部異なる

Tiltfileとdocker-compose.yamlを組み合わせる場合、Kubernetesのマニフェストのときとは一部記述方法が異なります。
例えばlive_updateでプロセスを再起動するときは、docker_build_with_restartの代わりにrestart_containerを使う必要があります。
ガイドでは、Kubernetesのマニフェストの利用を前提とした内容が多いので、docker-compose.yamlに当てはめて上手くいかない場合は、使い方が異なる可能性も疑ったほうがよいです。
docker-compose.yamlを使う際の話はこちらのガイドに載っています。最初に一読しておくのがおすすめです。

■TiltfileにTiltの最小バージョンを定義しておくとメンテしやすい

新しく導入された関数をTiltfileで使うと、当たり前ですが、それに対応していない古いTiltではエラーになります。
Tiltfileの「メンテナー」と「利用者」が別々の場合、利用者にとっては「git pullしたらよくわからないエラーでTiltが起動しなくなった」という体験になるので避けたいところです。メンテナーが周知を頑張るにしてもそれはそれで辛いです。
min_tilt_versionという拡張を使うと、動作に必要な最低バージョンをTiltfileに定義しておけるようになります。バージョンを満たさない場合は、Tilt起動時に次のようなメッセージが表示されます。

最低バージョンは0.23.0だけどそれに満たないTiltを使っている例:

local: tilt version
+--+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Tilt Version is out of date!                          |
|       Please upgrade https://docs.tilt.dev/upgrade.html                    |
+--+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Traceback (most recent call last):
  ...(省略)...
  <builtin>: in fail
Error: Tilt version is out of date minimum required version is [0.23.0]

メンテナーは「このメッセージが出たときは最新にバージョンアップしてね」とだけ事前に伝えておくだけでよく、Tiltfileのメンテナンスも積極的にできるようになります。

まとめ

弊社でTiltを導入してみた事例を紹介しました。
Tiltは活発に開発されており、弊社が使っている機能は全体の半分にも満たないと思います。 興味をもった方はぜひ試してみてください!
この記事がローカル開発環境で困っている方の参考になれば幸いです!

HRBrainでは今回のような改善もどんどんトライしていける会社です。「サービスの開発もいいけど、自分たちを楽にするのも好きだよ」という方、一緒にいかがですか? というか一緒にやりましょう!
ちょっとでも興味を持っていただけたようでしたら、ぜひ下記ページからご応募ください!

www.hrbrain.co.jp

*1:現在airを使っていないため追えていませんが、最新版ではもしかしたら改善しているかもしれません

*2:動画に出てくるのは古いバージョンなので、UIが最新のものとはやや異なります