Makefileはもう古い:DevOps自動化の未来がTaskfileである理由

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

深夜2時の本番パイプライン崩壊

午前2時、新しいマイクロサービスのデプロイが失敗していました。ターミナルには赤文字の羅列が並び、不気味なエラーが表示されていました:make: *** [build] Error 1。ローカル環境では数秒で終わるビルドが、Jenkinsのランナー上では即座にクラッシュしたのです。

30分かけてログを調査した結果、原因を突き止めました。新人開発者のIDEが自動整形によってハードタブを4つのスペースに書き換えていたのです。さらに悪いことに、クリーンアップタスクの sed -i コマンドがmacOSでは動作するものの、AlpineベースのCIコンテナでは失敗していました。GNU版とBSD版の sed では、インプレース編集の扱いが異なるからです。これこそが古典的な Makefile の罠です。強力ではありますが、脆弱でプラットフォームに依存し、1976年当時の構文に縛られているのです。

その夜、私はチームに自動化を管理するためのより良い方法が必要だと確信しました。現代のDevOpsの標準言語であるYAMLで記述できるツールが必要だったのです。

課題:なぜMakefileは現代のワークフローで苦戦するのか

Makefileはもともと70年代にCプログラムをコンパイルするために設計されました。現在でも事実上のタスクランナーとして君臨していますが、クラウドネイティブなチームにとっては重い技術的負債を抱えています。

  • タブの制約: タブの代わりに誤ってスペースが1つ入るだけで、自動化全体が壊れます。高度なIDEの自動整形が当たり前の時代において、この制約は生産性を著しく阻害します。
  • プラットフォーム間の不整合: Makefileはホストのシェルに依存します。grepfindsed などを使用すると、開発者のMacBookから LinuxベースのCIランナーやWindows環境に移行した際に失敗する可能性が高くなります。
  • 不透明な構文: 変数、.env ファイル、条件分岐の扱いは、Makefileでは難解になりがちです。結局、書いた本人にしか分からない「一度書いたら二度と触りたくない」スクリプトになってしまいます。
  • 手動の依存関係マッピング: Makefileはファイルのタイムスタンプを追跡します。「内容」が変わっていない場合にタスクをスキップしたい場合、手動でファイル依存関係をマッピングする必要があり、複雑なプロジェクトではエラーの元になります。

解決策:Taskfile (go-task) の登場

そこで私が見つけたのが Taskfile(正式名称は go-task)です。Goで書かれたYAMLベースのタスクランナーで、軽量(5MBの単一バイナリ)であり、レガシーな Makefile にまつわるほぼ全ての悩みを解決してくれます。

主要なインフラリポジトリを Taskfile に移行して以来、「自分のマシンでは動くのに」というCIの失敗は90%減少しました。今では小さなGoバイナリから大規模なKubernetesデプロイまで、私たちの標準ツールとなっています。

Taskのインストール

導入は非常に簡単です。Taskはデフォルトでクロスプラットフォームに対応しています. macOSやLinuxでHomebrewを使用している場合は、コマンド一つでインストールできます:

brew install go-task/tap/go-task

CI/CDパイプラインの場合は、スタンドアロンのバイナリをダウンロードするか、公式のDockerイメージを使用できます。ビルドエージェントに make がインストールされているかどうかを心配する必要はもうありません。

初めてのTaskfileを作成する

具体的な例を見てみましょう。Makefile の代わりに Taskfile.yml を作成します。以下は、アプリケーションのクリーン、ビルド、テストを行う設定例です。

version: '3'

vars:
  BINARY_NAME: my-app

tasks:
  build:
    desc: アプリケーションをビルドする
    cmds:
      - go build -o {{.BINARY_NAME}} main.go
    sources:
      - ./*.go
    generates:
      - "{{.BINARY_NAME}}"

  test:
    desc: ユニットテストを実行する
    cmds:
      - go test -v ./...

  clean:
    desc: ビルド成果物を削除する
    cmds:
      - rm -f {{.BINARY_NAME}}

Makefileに対する主な利点:

  1. 読みやすい変数: {{.VAR_NAME}} 構文を使用します。これは Makefile の $(VAR)$${VAR} といった複雑な書き方よりも遥かに明快です。
  2. コンテンツベースのキャッシュ: sourcesgenerates キーに注目してください。Taskfile はファイルのチェックサムを計算します。コードに変更がない場合、そのタスクはスキップされます。この機能だけで、私たちのCIビルド時間は25%短縮されました。
  3. 即座に生成されるドキュメント: desc フィールドを追加することで、チームメンバーは task --list を実行するだけで、利用可能なコマンドの一覧を自動的に確認できます。

環境変数と依存関係のネイティブサポート

深夜のトラブル対応で大きな問題となったのが .env ファイルの管理でした。Makefile では複雑な回避策なしには扱いにくいものですが、Taskfile では標準機能としてサポートされています。

version: '3'

dotenv: ['.env']

tasks:
  deploy:
    desc: 本番環境へデプロイする
    deps: [build, test]
    cmds:
      - echo "バージョン $VERSION を $ENV にデプロイしています"
      - ./scripts/deploy.sh
    preconditions:
      - sh: "[ "$ENV" == 'production' ]"
        msg: "エラー:このタスクは本番環境のみに制限されています!"

ポイントの解説:

  • ネイティブなDotenv対応: Taskは .env ファイル内の変数を自動的にシェル環境に注入します。
  • 明示的な依存関係: deploy タスクは、buildtest が正常に完了するまで実行されません。
  • 安全のための事前条件: ロジックゲートを追加しました。開発者が $ENV 変数を正しく設定せずに本番デプロイを実行しようとすると、Taskfile は分かりやすいカスタムエラーメッセージを表示して終了します。

真のクロスプラットフォーム互換性

先ほどの sed の問題を覚えていますか? Taskfile は内部シェルインタプリタ (mvdan.cc/sh) を使用します。これにより、Windows、Linux、macOS でコマンドが全く同じように動作することが保証されます。ホストが Bash、Zsh、PowerShell のどれを使っているかを気にする必要はありません。

どうしてもプラットフォーム固有のロジックが必要な場合も、Taskならスマートに処理できます:

tasks:
  list-files:
    cmds:
      - cmd: dir
        platforms: [windows]
      - cmd: ls
        platforms: [linux, darwin]

直接比較

私たちのチームが最終的に Makefile を引退させた理由は以下の通りです:

機能 Makefile Taskfile (go-task)
構文 厳格なタブ、壊れやすい クリーンで標準的なYAML
Windows対応 WSLやCygwinが必要 ネイティブGoバイナリ
タスクロジック ファイルベースのみ 明示的な depspreconditions
キャッシュ タイムスタンプベース コンテンツのチェックサムベース
検索性 手動の help ターゲット 標準機能の task --list

パイプラインの近代化

新しいプロジェクトを始める場合や、隠れた空白文字のデバッグに疲れたなら、Taskfile は最良の選択肢です。「Everything as Code(すべてをコードとして管理)」の潮流に適合しつつ、誰でも使えるシンプルさを維持しています。

まずはスモールスタートで構いません。今日から全てを書き換える必要はありません。ローカル開発用Taskfile.yml を作成し、Dockerコンテナの起動やリンターの実行に使ってみてください。チェックサムベースのキャッシュの速さを一度体験すれば、もう元には戻れなくなるでしょう。

次に深夜の緊急対応が発生したときは、タブとスペースの混同ではなく、本質的なロジックのバグが原因であるべきです。

Share: