スクリプトから製品へ:GoとCobraによるクロスプラットフォームCLIツールの構築

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

社内ツールにおける配布の悩み

自分のマシンでは完璧に動くスクリプトが、チームメイトが実行した途端にクラッシュする。そんな壁に誰もが突き当たったことがあるはずです。私はかつて、クラウド展開を管理するために、乱立するBashスクリプトに頼っている12人のチームを率いていました。IntelベースのLinuxではなくM1 Macを使うエンジニアを採用した瞬間、すべてが崩壊しました。パスの問題、互換性のないsedのバージョン、不足しているjqの依存関係などにより、単純なデプロイが2時間のデバッグセッションに変わってしまったのです。

この問題は、ホスト環境のランタイムに依存していることに起因します。PythonやNode.jsはBashよりは安定していますが、それでも事前にインストールされたランタイムや、virtualenvnode_modulesのような厄介なパッケージマネージャーが必要です。このオーバーヘッドが、小規模な社内ユーティリティの導入を妨げます。インストールが簡単でなければ、人々は使いません。社内オペレーションをスケールさせるには、「依存関係ゼロ」のバイナリを作る技術を習得することが不可欠です。

適切なCLIスタックの選択

ツールチェーンの選択は単なる構文の問題ではなく、コードがいかにしてエンドユーザーに届くかという問題です。ほとんどのCLI開発は3つの陣営に分けられ、それぞれに課題があります。

  • シェルスクリプト (Bash/Zsh): 10行程度のクイックフィックスには適しています。しかし、構造化されたエラーハンドリングに欠け、100行を超えるとメンテナンスの悪夢になります。
  • スクリプト言語 (Python/Node.js): 素晴らしいライブラリが利用できますが、配布が苦痛です。同僚にバージョンマネージャー(pyenvnvmなど)の管理を強いるか、解凍に失敗しがちな50MBもの肥大化した実行ファイルを配布することになります。
  • コンパイル言語 (Go/Rust): これらは単一の静的バイナリを生成します. ランタイムも依存関係も不要です。ファイルを渡せば、相手が実行するだけで動きます。Goは、開発スピードとほぼ瞬時の実行を両立しているため、DockerやKubernetesのようなツールのデファクトスタンダードとなっています。

Goは、開発スピードとほぼ瞬時の実行を両立しているため、DockerやKubernetesのようなツールのデファクトスタンダードとなっています。

GoとCobraのエコシステム:現実的な評価

GoとCobraを使用すると強固な基盤が得られますが、導入前にトレードオフを検討する必要があります。

メリット

  • 静的リンク: Goはすべてを1つのファイルにまとめます。これにより「自分のマシンでは動く」という言い訳を事実上排除できます。
  • Cobraフレームワーク: ネストされたコマンド(git commit -mなど)、フラグのパース、シェルの補完など、面倒な処理を標準でサポートしています。
  • 圧倒的なパフォーマンス: Goバイナリは数ミリ秒で起動します。このスピードは、1日に何百回もCLIコマンドを実行する開発者にとって重要です。
  • 容易なクロスコンパイル: Linuxターミナルから1つのコマンドで、Windowsの.exeやmacOS의 バイナリをビルドできます。

課題

  • バイナリサイズ: Goの「Hello World」アプリは、ランタイムが組み込まれているため約5MBになります。参考までに、Terraformのような多機能ツールは80MBを超えることもあります。
  • 厳格な型付け: Pythonとは異なり、Goではデータの扱いに厳格さが求められます。事前のボイラープレート(定型コード)の記述が増えるため、プロトタイピングの最初の1時間は遅く感じるかもしれません。

プロフェッショナルなツールチェーン

本番環境の基準を満たすツールを構築するために、以下のスタックをお勧めします。これは主要なオープンソースプロジェクトで使用されているワークフローを反映したものです。

  1. Go (1.21+): コンパイラ。
  2. Cobra-CLI: コマンド構造の雛形作成用.
  3. GoReleaser: ビルドとGitHubリリースの自動化における業界標準.
  4. GitHub Actions: クリーンで一貫したCI環境でバイナリをビルドするため.
# 雛形作成ツールを取得
go install github.com/spf13/cobra-cli@latest

# GoReleaserをインストール(Homebrewなら簡単です)
brew install goreleaser/tap/goreleaser

ステップ・バイ・ステップ:自動化されたCLIの構築

社内設定を管理するユーティリティit-toolを作ってみましょう。構造の設計、ロジックの追加、そしてユーザーへの配布パイプラインの自動化を行います。

1. プロジェクトの雛形作成

Goモジュールを初期化することから始めます。Goが依存関係を正しく解決できるように、GitHubのリポジトリパスを使用してください。

mkdir it-tool && cd it-tool
go mod init github.com/youruser/it-tool
cobra-cli init

これによりmain.gocmd/root.goが生成されます。root.goはアプリケーションのロビーだと考えてください。ここで--verbose--configのようなグローバルフラグを定義します。

2. サブコマンドの追加

優れたCLIデザインは「動詞」に基づいています。20個のフラグを持つ乱雑なコマンドの代わりに、サブコマンドを使いましょう。ヘルスチェックを追加してみます。

cobra-cli add health

cmd/health.goの中に、init()関数があります。これを使用して、そのアクション専用のフラグを定義します。

// cmd/health.go のスニペット
var healthCmd = &cobra.Command{
    Use:   "health",
    Short: "インフラの状態をチェックする",
    Run: func(cmd *cobra.Command, args []string) {
        isFull, _ := cmd.Flags().GetBool("full")
        if isFull {
            fmt.Println("🔍 60秒間の詳細診断を実行中...")
        } else {
            fmt.Println("✅ システムは正常です!")
        }
    },
}

func init() {
    rootCmd.AddCommand(healthCmd)
    healthCmd.Flags().BoolP("full", "f", false, "包括的なチェックを実行する")
}

3. ロジックとエラーの処理

ビジネスロジックをcmd/フォルダの中に埋め込まないでください。Run関数は薄く保ち、実際の処理はinternal/パッケージに配置します。これによりコードのテストが容易になります。エラーが発生した場合は、汎用的なパニックを避けましょう。os.Stderrとクリーンな終了コードを使用して、他のスクリプトがエラーをキャッチできるようにします。

// プロフェッショナルな終了方法
fmt.Fprintf(os.Stderr, "エラー: データベースへの接続に失敗しました\n")
os.Exit(1)

4. GoReleaserによる自動化

配布は最も難しい部分です。更新のたびに手動でGOOS=windows go buildを実行するのは失敗の元です。GoReleaserがその重労働を肩代わりしてくれます。

設定を初期化します:

goreleaser init

.goreleaser.yamlを編集して、arm64(Appleシリコン用)やamd64(ほとんどのサーバー用)などの一般的なアーキテクチャをターゲットにします:

builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - linux
      - windows
      - darwin
    goarch:
      - amd64
      - arm64

5. CI/CDのゴール

.github/workflows/release.ymlを作成します。このワークフローは、gitタグをプッシュするたびにトリガーされます。すべてのプラットフォーム向けにコードをコンパイルし、約30秒で新しいGitHubリリースにバイナリを添付します。

name: release
on:
  push:
    tags: ['v*']
jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/setup-go@v4
        with: { go-version: '1.21' }
      - uses: goreleaser/goreleaser-action@v5
        with:
          distribution: goreleaser
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

リリースの準備はいいですか?タグを付けてプッシュするだけです:

git tag -a v1.0.0 -m "最初の安定版リリース"
git push origin v1.0.0

1分以内に、チームはすべてのOS向けバイナリが揃った洗練されたリリースページにアクセスできるようになります。ローカルスクリプトを、実際にスケールするプロフェッショナルなツールへと変貌させたのです。

Share: