「自分のマシンでは動くのに」という悪夢
深夜2時、暗いオフィスでJenkinsのパイプラインがどうしてもグリーンにならないのを眺めていた時のことを覚えています。ローカルではGoのバイナリが14秒でコンパイルされ、テストも通り、Dockerイメージのデプロイ準備も整っていました。しかしCIランナー上では、libsslヘッダーが見つからないという不可解なエラーでクラッシュしました。結局、私のローカルのUbuntuマシンにはlibssl-devがプリインストールされていたのに対し、別のプラットフォームチームが管理するJenkinsエージェントは、最小構成のAlpineイメージを実行していたことが原因でした。
これが典型的な「環境差異(environmental drift)」の問題です。何十年もの間、私たちはMakefileやBashスクリプトをビルドステップの接着剤として頼りにしてきました。Makeはローカルでのタスク実行には素晴らしいツールですが、ホスト環境をビュッフェのように扱い、すべての依存関係が自分の好みの通りに配置されていることを前提としています。
そのロジックをGitHub ActionsやGitLab CIに移行すると、ローカル環境を再現するためだけに300行ものYAMLファイルを書くことになることがよくあります。このロジックの重複はメンテナンスの罠であり、CI/CDパイプラインが失敗する最大の原因です。
なぜ現代のDevOpsにおいて従来のビルドスクリプトは失敗するのか
数多くのエンタープライズプロジェクトを調査した結果、ビルドシステムを脆弱にする2つの構造的な欠陥が見つかりました。1つ目は「**密閉性(Hermeticity)の欠如**」です。ビルドが密閉されているとは、ホストに関係なく、ビット単位で同じ出力を生成することを指します。Makefileはホストの環境変数やローカルのファイルパス、システムレベルのバイナリバージョンを漏出させるため、密閉されることは稀です。開発者がNode v20を使用し、ランナーがv18で止まっている場合、ビルドはローカルで成功しても、本番環境では警告なしに失敗する可能性があります。
2つ目の欠陥は「**キャッシュの複雑さ**」です。標準的なCIパイプラインが遅いのは、通常ゼロの状態から開始されるからです。これをactions/cacheのようなツールで補おうとしますが、これらはビルドロジックの外側に存在します。4GB of プロジェクトでマイナーな依存関係を更新すると、キャッシュの無効化が大雑把すぎてフルビルドを強制されることがよくあります。私の経験則はシンプルです。真に信頼性を高めるには、ビルドロジックをランナーのインフラから切り離す必要があります。
Makefile vs. Docker vs. Earthly:徹底比較
適切なツールの選択は、分離のニーズに依存します。Makefileはシンプルですが、分離はゼロです。Docker-in-Docker (DinD) は分離を提供しますが、CI環境で効率的にキャッシュするのが難しいことで知られています。すべてのレイヤーを手動でプッシュ・プルする必要があり、キャッシュによって節約された時間が相殺されてしまうことがよくあります。
Earthlyはこのギャップを埋めます。これはDockerfileとMakefileのハイブリッドのように機能します。Dockerでおなじみの構文(FROM, RUN, COPY)を採用しつつ、Makeスタイルの**ターゲット**を導入しています。Earthlyビルドのすべてのステップはコンテナ内で実行されます. MacBook Proで動作すれば、LinuxのCIエージェントでも全く同じように動作します。また、Earthlyは分散キャッシュをネイティブに管理します。複雑なキャッシュキーを管理することなく、ローカル端末とCIランナー間でビルドレイヤーを共有できます。
Earthlyのアプローチ:一度ビルドすれば、どこでも実行可能
導入は簡単です。EarthlyはホストマシンでDockerデーモンが動作していることだけを要求します。CLIをインストールすれば、プロジェクトのビルドロジックは単一のEarthfileに集約されます。
# LinuxにEarthlyをインストール
sudo /bin/sh -c 'wget https://github.com/earthly/earthly/releases/latest/download/earthly-linux-amd64 -O /usr/local/bin/earthly && chmod +x /usr/local/bin/earthly'
Pythonアプリケーションを例に考えてみましょう。標準的なセットアップでは、requirements.txtと別のMakefileを管理します。Earthlyでは、特定のライフサイクルステージを表すターゲットを定義します。
# Earthfile
VERSION 0.8
FROM python:3.11-slim
WORKDIR /app
setup:
COPY requirements.txt .
RUN pip install -r requirements.txt
test:
FROM +setup
COPY . .
RUN pytest .
build:
FROM +setup
COPY . .
RUN python setup.py build
SAVE ARTIFACT ./dist /dist AS LOCAL ./dist
testターゲットは+setupに依存しています。コードを1行変更してもrequirements.txtがそのままなら、Earthlyはpip installステップをスキップします。レイヤーのチェックサムを計算し、キャッシュから結果を取得します。これはコンテナ内で行われるため、ホストマシンのPythonバージョンは関係ありません。
実践的な実装:MakefileからEarthfileへの移行
移行中にMakefileをすぐに削除しないでください。代わりに、薄いラッパーとして使用します。これにより、開発者はmake testを使い続けながら、バックグラウンドでEarthlyが実際の重い処理を処理できるようになります。
**サテライトビルド(Satellite Builds)**は、大規模なチームにとって画期的な機能です。ローカルマシンがマイクロサービスのビルド負荷で苦労している場合、Earthlyの接続先を「サテライト」—専用の32コアビルドサーバーなど—に向けることができます。CLIはビルドコンテキストを透過的にリモートサーバーにオフロードし、ビルドを実行して、ログを端末にストリーミングします。これにより、すべてのエンジニアがローカル開発でデータセンターのパワーを利用できるようになります。
# リモートキャッシュを使用してローカルでターゲットを実行
earthly --remote-cache=my-registry.com/project-cache +build
分散キャッシュを活用したCI/CDの高速化
Earthlyの真の力は、CI統合時に発揮されます。Node、Python、Goの環境を構成するために50行のYAMLを書く必要はもうありません。GitHub Actionsの設定は、通常、読みやすい単一のステップに縮小されます。
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Earthlyのインストール
uses: earthly/actions-setup@v1
- name: ビルドの実行
run: earthly --push +all
--pushフラグを使用すると、Earthlyはアーティファクトをレジストリにエクスポートし、同時にビルドキャッシュをアップロードするように指示されます。5分後にチームメイトがearthly +allを実行すると、彼らのマシンはCIランナーによって生成された正確なキャッシュレイヤーをダウンロードします。この共有キャッシュを活用することで、モノリシックなアプリケーションのビルド時間が15分から120秒未満に短縮されるのを私たちは目にしてきました。
ビルドエンジニアリングに関する最終的な考察
Earthlyを採用するには、思考の転換が必要です。ビルドを一連のシェルコマンドではなく、隔離されたコンテナの「有向非巡回グラフ(DAG)」として捉える必要があります。初期設定には簡単なBashスクリプトよりも手間がかかりますが、長期的な信頼性は否定できません。
環境固有のバグは消え去ります。一貫性のないローカル環境も過去のものとなります。複雑なパイプラインを管理しているなら、まずは小さく始めてみてください。最も信頼性の低いビルドステップを選び、それをEarthlyターゲットでラップして、安定性が向上するのを確認してください。すべてのマシンで真に同一に動作するビルドを一度体験すれば、生のMakefileに戻ることは二度とないでしょう。

