Dockerビルドの待ち時間をゼロに:SkaffoldでKubernetesのライブリロードを実現する

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

Kubernetes開発における「もどかしい現実」

Kubernetes向けの開発は、まるで登山靴を履いてマラソンをしているような感覚に陥ることがよくあります。従来のローカル開発に慣れている方なら、ファイルを保存すれば変更が即座に反映されることを期待するでしょう。しかし、Kubernetesはその流れを打ち砕きます。標準的なサイクルは非常に退屈です。コードを修正し、新しいDockerイメージをビルドし、タグを付け、レジストリにプッシュし、YAMLマニフェストを更新し、最後にkubectl applyを実行します。そして, Podが終了して再起動するのを待つのです。

これを一度やるだけなら問題ありません。しかし、1日に50回も繰り返すとなると、生産性を著しく損ないます。開発チームが毎日、合計90分以上もdocker buildの進捗バーを眺めるだけで集中力を削がれている光景を何度も見てきました。この摩擦は、開発者が頻繁にテストを行う意欲を削ぎます。その結果、巨大でリスクの高いコミットが増え、フィードバックループはまるで2005年で止まっているかのように感じられます。

なぜ「インナーループ」が壊れていると感じるのか

この遅さの原因は、ローカルのファイルシステムとクラスターのコンテナランタイムとの間の断絶にあります。Kubernetesは本番環境のワークロードをオーケストレートするために構築されたものであり、ホットリロード対応の開発サーバーとして機能するように設計されているわけではありません。コンテナを不変(イミュータブル)なアーティファクトとして扱うため、CSSを1行変更しただけでも、システムは新しい200MBのイメージを要求します。

標準的なCI/CDパイプラインは本番環境の安定性には優れていますが、自分のマシンでコーディングとデバッグを繰り返す「インナーループ(Inner Loop)」には重すぎます。本番に近い環境を維持しつつ、レジストリへのプッシュや手動のマニフェスト更新をスキップする方法が必要です。

Skaffoldがプロセスを効率化する仕組み

Skaffoldは、Kubernetesへのアプリケーションのビルド、プッシュ、デプロイのワークフローを自動化するコマンドラインツールです。IDEとクラスターの間に静かに常駐します。5つの異なるCLIコマンドを使い分ける代わりに、Skaffoldにソースコードを監視させます。変更を検知すると、必要な更新だけをトリガーします。

私の経験上、これは革新的な変化をもたらします。以前、単純なバグ修正の確認に12分かかっていたマイクロサービスプロジェクトがありましたが、Skaffoldを導入した後は、その時間が15秒未満に短縮されました。Kubernetesはデプロイの障害ではなくなり、開発環境の透明な一部となったのです。

前提条件

この記事の内容を試すには、以下のツールが必要です:

  • Kubectl: クラスター操作のための標準CLI。
  • Skaffold: 自動化を支えるエンジン。
  • ローカルクラスター: Minikube、Kind、またはDocker Desktop(K8s有効化済み)。
  • Docker: ローカルでのイメージビルドに使用。

実践的な例のセットアップ

仕組みを理解するために、シンプルなNode.jsアプリケーションを作成してみましょう。ここではNode.jsを使用しますが、これらの概念はGo、Python、Javaなどにも同様に適用できます。

ステップ1:アプリケーションコード

skaffold-demoというディレクトリを作成し、app.jsを追加します:

const http = require('http');

const requestListener = function (req, res) {
  res.writeHead(200);
  res.end('Skaffoldからこんにちは!バージョン1');
};

const server = http.createServer(requestListener);
server.listen(8080);
console.log('サーバーがポート8080で起動しました');

次に、基本的なpackage.jsonを作成します:

{
  "name": "skaffold-demo",
  "version": "1.0.0",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  }
}

ステップ2:コンテナとマニフェスト

アプリをコンテナ化するための標準的なDockerfileが必要です:

FROM node:18-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8080
CMD ["node", "app.js"]

次に、k8sフォルダを作成し、deployment.yamlを追加します。イメージ名にプレースホルダーを使用していることに注目してください。実際のタグ付けと注入はSkaffoldが管理します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: node-app
  template:
    metadata:
      labels:
        app: node-app
    spec:
      containers:
      - name: node-app
        image: node-app-image
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: node-app-service
spec:
  selector:
    app: node-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: LoadBalancer

ステップ3:Skaffoldの設定

ここで自動化の定義を行います。プロジェクトのルートにskaffold.yamlを作成します。skaffold initで自動生成することもできますが、ここではデモ用のクリーンなバージョンを使用します:

apiVersion: skaffold/v4beta3
kind: Config
build:
  artifacts:
    - image: node-app-image
      docker:
        dockerfile: Dockerfile
deploy:
  kubectl:
    manifests:
      - k8s/*.yaml

ワークフローの実行:skaffold dev

ターミナルで以下のコマンドを実行します:

skaffold dev

Skaffoldは継続的なループを開始します。イメージをビルドし、一意のハッシュでタグ付けし、メモリ上のYAMLマニフェストを更新してクラスターに適用します。最後に、Podのログをターミナルに直接ストリーミングします。クラスターの内部で動いているにもかかわらず、ローカルプロセスを実行しているかのように感じられるはずです。

app.jsを編集すると、Skaffoldは再ビルドをトリガーします。しかし、1行の変更のためにイメージ全体を再ビルドするのは依然として非効率的です。これを解決するために、**File Sync**(ファイル同期)を使用します。

File Syncによる即時更新

File Syncを使用すると、Skaffoldは変更されたファイルを動作中のコンテナに直接注入できます。イメージのビルドやPodの再起動を完全にスキップします。Node.jsのようなインタプリタ言語にとって、これは非常に大きなメリットです。

skaffold.yamlを更新して、syncセクションを追加します:

build:
  artifacts:
    - image: node-app-image
      docker:
        dockerfile: Dockerfile
      sync:
        manual:
          - src: 'app.js'
            dest: .
          - src: '*.json'
            dest: .

これで、app.jsを保存すると、Skaffoldはkubectl execを使用してファイルをコンテナにコピーします。nodemonのようなツールを併用していれば、アプリは1秒以内に内部で再起動します。これにより、ローカル開発のスピードに匹敵する「ライブリロード」体験が可能になります。

自動ポートフォワーディング

通常、サービスのテストにはIPアドレスを探したりIngressを設定したりする手間がかかります。Skaffoldはこれを自動的に処理します。skaffold.yamlに以下のブロックを追加してください:

portForward:
  - resourceType: service
    resourceName: node-app-service
    port: 80
    localPort: 9000

skaffold devが実行されている間は、単にlocalhost:9000にアクセスするだけです。Skaffoldはそのトラフィックをクラスター内のコンテナに直接ルーティングします。

まとめ

手動のdocker buildコマンドから自動化されたSkaffoldのワークフローに移行することは、タイプライターから最新のIDEに乗り換えるようなものです。デプロイに伴う思考のオーバーヘッドが解消され、ロジックの実装に集中できるようになります。sync機能を活用することで、本番環境と全く同じように動作する、軽快でレスポンスの良い開発サイクルが手に入ります。マイクロサービスを構築しているなら、Skaffoldのようなツールは単なる便利ツールではなく、開発速度を維持するための必須条件と言えるでしょう。

Share: