この投稿では、Lima と k3s を使って、ホットリロードに対応したローカル Kubernetes 開発環境を構築する方法を解説します。port-forward なしでホストマシンから直接アクセスでき、コード変更が即座に反映される快適な開発体験を実現します。
ローカルのTypeScriptファイルを編集したら、Kubernetes上のBunサーバーにホットリロードで反映される様子を動画にしたのでご覧ください↓。BunがPodの中で動いているとは思えないような、快適な開発体験になってます。
この投稿で学べること
- Lima と k3s を使った Kubernetes 環境の構築手順
- ホストマシンのディレクトリを VM にマウントする方法
- LoadBalancer 経由でホストから直接アクセスする設定
- ホットリロードを有効にした開発ワークフローの構築
Lima と k3s とは
Lima(Linux virtual machines)は、macOS 上で Linux VM を手軽に動かすためのツールです。特に、ファイルシステムの共有やネットワーク設定が柔軟に行える点が特徴です。
k3s は、Rancher 社が開発した軽量な Kubernetes ディストリビューションです。リソース消費が少なく、ローカル開発環境に適しています。Lima には k3s のテンプレートが用意されており、コマンド一発で Kubernetes 環境を立ち上げることができます。
環境構築の手順
Lima のインストールと k3s の起動
まず、Homebrew で Lima をインストールします。
brew install lima
次に、k3s テンプレートを使ってインスタンスを起動します。
limactl start template://k3s
プロンプトが表示されたら「Proceed with the current configuration」を選択します。インスタンス名は k3s になります。
kubeconfig の設定
ホストマシンから kubectl を使えるようにするため、kubeconfig を設定します。
export KUBECONFIG="/Users/suin/.lima/k3s/copied-from-guest/kubeconfig.yaml"
設定が正しく反映されているか確認します。
kubectl get nodes -o wide
ノード情報が表示されれば成功です。
ホストディレクトリのマウント設定
ここが今回の構成で最も重要なポイントです。k3s テンプレートはデフォルトで mounts: [] となっており、ホストディレクトリのマウントが無効になっています。ホットリロードを実現するには、書き込み可能なマウントを追加する必要があります。
まず、k3s インスタンスを停止して設定を編集します。
limactl stop k3s
limactl edit k3s
以下の設定を追加します。
# macOS 13+ なら virtiofs が使える(vmType: vz のとき)
mountType: "virtiofs"
# watch/hot を効かせたいので inotify 伝搬をON(writable必須)
mountInotify: true
mounts:
- location: "~/codes/github.com" # macOS側
writable: true
# Apple公式の Virtualization.framework を使う方式
vmType: vz
# ホスト(mac)とVM(k3s)間のネットワークを NAT で繋ぐ
networks:
- vzNAT: true
各設定項目について補足します。mountType: "virtiofs" は、macOS 13 以降で利用可能な高速なファイル共有方式です。mountInotify: true は、ホスト側のファイル変更イベントを VM に伝搬するための設定です。vmType: vz は Apple の Virtualization.framework を使用する設定で、vzNAT: true はホストと VM 間のネットワークを NAT で接続します。
mountInotify の制限について
Lima の mountInotify: true は experimental な機能であり、いくつかの制限があります。 実装上、特定のイベントタイプ(ATTRIB)のみしか発生せず、実際のファイル内容変更を表す MODIFY / CLOSE_WRITE / DELETE が来ないケースがあります。
この制限により、Bun / Vite / nodemon 等の hot-reload / watch 機能が期待通りに動作しないことがあります。そのため、watch を使う場合は polling ベースの方法を推奨します。この投稿では、nodemon の --legacy-watch オプションを使用してこの問題を回避しています。
設定を保存したら、k3s を再起動します。
limactl start k3s
マウントが正しく設定されているか確認します。
limactl shell k3s -- ls ~/codes/github.com/
アプリケーションの作成
サンプルとして、Bun を使ったシンプルな HTTP サーバーを作成します。
bun init
index.ts を以下の内容で作成します。
// ~/codes/github.com/suinplayground/lima-k3s/index.ts
import { serve } from "bun";
serve({
port: 4003,
fetch(request) {
return new Response("Sup");
},
});
console.log("Server is running on port 4003");
Kubernetes マニフェストの作成
dev.yaml を作成します。このマニフェストには、Namespace、Deployment、Service の定義が含まれています。
apiVersion: v1
kind: Namespace
metadata:
name: dev
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: dev
spec:
replicas: 1
selector:
matchLabels: { app: myapp }
template:
metadata:
labels: { app: myapp }
spec:
containers:
- name: app
image: oven/bun:latest
workingDir: /app
command: ["bash", "-lc"]
args:
- |
bun install
bunx nodemon --legacy-watch --watch /app --ext ts,tsx,js,jsx,json --exec "bun index.ts"
ports:
- containerPort: 4003
volumeMounts:
- name: src
mountPath: /app
# Linux用の node_modules を Pod 内で作る(macのnode_modulesは混ぜない)
- name: node-modules
mountPath: /app/node_modules
volumes:
- name: src
hostPath:
path: /Users/suin/codes/github.com/suinplayground/lima-k3s
type: Directory
- name: node-modules
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: myapp
namespace: dev
spec:
selector: { app: myapp }
type: LoadBalancer
ports:
- name: http
port: 4003
targetPort: 4003
いくつかのポイントを解説します。
nodemon に --legacy-watch オプションを指定しているのは、前述の inotify の制限を回避するためです。このオプションを使うと、ファイルシステムイベントではなく polling ベースでファイル変更を検知します。
node_modules を emptyDir としてマウントしているのは、macOS 側の node_modules と Linux 側の node_modules を分離するためです。ネイティブモジュールの互換性問題を避けることができます。
Service の type: LoadBalancer を指定することで、k3s に組み込まれている ServiceLB(旧 Klipper)が外部 IP を割り当ててくれます。
デプロイと動作確認
マニフェストを適用します。
kubectl apply -f dev.yaml
Pod が起動しているか確認します。
kubectl get pods -n dev
まず、port-forward で動作確認を行います。
kubectl -n dev port-forward svc/myapp 4003:4003
別のターミナルで以下を実行します。
curl http://localhost:4003
"Sup" が表示されれば成功です。
LoadBalancer 経由でのアクセス
port-forward を使わずに、LoadBalancer の外部 IP 経由でアクセスすることもできます。
kubectl get svc myapp -o wide -n dev
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
myapp LoadBalancer 10.43.197.89 192.168.64.3 4003:30948/TCP 69m app=myapp
EXTERNAL-IP の値(この例では 192.168.64.3)を使ってアクセスします。
curl http://192.168.64.3:4003
"Sup" が表示されれば成功です。これで、port-forward なしでホストマシンから直接アクセスできるようになりました。
EXTERNAL-IPでアクセスしてもいいですが、k3sのロードバランサーがNodePortを開いてくれるのと、limaがホストマシンの4003ポートをVMに自動ポートフォーワードしてくれるおかげで、localhostでもアクセスできます。
curl http://localhost:4003
ホットリロードの確認
最後に、ホットリロードが機能しているか確認します。
index.ts を以下のように修正して保存します。
// ~/codes/github.com/suinplayground/lima-k3s/index.ts
import { serve } from "bun";
serve({
port: 4003,
fetch(request) {
return new Response("Hello"); // ここを修正
},
});
console.log("Server is running on port 4003");
保存後、数秒待ってからリクエストを送ります。
curl http://localhost:4003
"Hello" が表示されれば、ホットリロードが正しく機能しています。
所感
Lima と k3s の組み合わせは、ローカルでの Kubernetes 開発環境として非常に優秀です。特に、LoadBalancer が標準で使えること、ホストディレクトリのマウントが柔軟に設定できることが大きな利点です。
inotify の制限は少し残念ですが、nodemon の --legacy-watch を使えば十分実用的なホットリロード環境が構築できます。Kubernetes ベースのアプリケーション開発を行う方は、ぜひ試してみてください。
最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローしてもらえると嬉しいです
→Twitter@suin