How to use GitHub Actions for devops
先月、私のチームは金曜日の午後に本番環境へバグをデプロイしてしまいました。設定ファイルでの単なるセミコロンの欠落が、ローカルのチェックを通過し、マージされ、結果として決済サービスを2時間ダウンさせてしまったのです。この大騒動の後、私たちは本物のCI/CDパイプラインが必要だと痛感しました。「プッシュする前にテストしたはずだから大丈夫」という、これまで頼りにしていたようなパイプラインではなく。
GitHub Actionsについては以前から聞いていたものの、避けていました。YAMLファイル、ランナー、マトリックスビルド——小規模なチームにはインフラのオーバーヘッドが大きすぎるように思えたからです。しかし、ついに腰を据えてセットアップしてみると、コアコンセプトを理解してしまえば驚くほどシンプルだと分かりました。つまずいた部分も含め、私が実際に何をしたのかを順を追って説明しましょう。
## コアコンセプト(実際に知っておくべきこと)
GitHub Actionsには独自の用語があり、他のことを理解する前に、まず以下の4つのキーワードを押さえる必要があります。
- **Workflow(ワークフロー)**: 自動化されたプロセス全体のこと。リポジトリ内に置かれるYAMLファイルです。
- **Event(イベント)**: ワークフローをトリガーするもの(push、pull request、新しいissueなど)。
- **Job(ジョブ)**: 同じランナー(仮想マシン)上で実行されるステップのグループ。ジョブは並列にも逐次にも実行できます。
- **Step(ステップ)**: ジョブ内の個々のタスク。スクリプトを実行するか、再利用可能な「アクション」を呼び出します。
- **Runner(ランナー)**: ジョブを実行するサーバー。GitHubは無料のLinux、Windows、macOSのVMを提供していますが、独自にホストすることも可能です。
レシピに例えると、ワークフローがレシピ全体、イベントは夕食を作るという決断、ジョブは調理場、ステップは個々の手順といったところです。
## ステップ1:最初のワークフローを作成する
私はNode.jsプロジェクト向けのシンプルなCIパイプラインを作成することから始めました。最初につまずいたのはファイルの配置場所です。ワークフローは、リポジトリのルートにある`.github/workflows/`に置かなければなりません。最初、私は`.github/`直下に置いてしまい、なぜ何も起こらないのかと悩みました。
私が作成した基本的なワークフローは以下の通りで、`.github/workflows/ci.yml`として保存しました。
```yaml
name: CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
```
これを分解して説明しましょう。
- `name`: GitHub Actionsタブに表示される、人間が読めるだけのラベルです。
- `on`: イベントトリガーです。ここでは、`main`ブランチをターゲットとしたpushまたはPRのたびにワークフローが実行されます。
- `jobs`: `test`という1つのジョブがあります。
- `runs-on`: GitHubがホストする`ubuntu-latest`ランナーを使用しています。
- `steps`: 各ステップは、アクション(`uses:`)を使用するか、シェルコマンド(`run:`)を実行します。
`actions/checkout@v4`のステップは非常に重要です。最初の試行でこれを忘れ、`npm ci`コマンドが`package.json`を見つけられない理由を突き止めるのに20分も費やしてしまいました。ランナーは空のワークスペースから起動するため、最初にコードをチェックアウトしなければなりません。
## ステップ2:ビルドとデプロイのジョブを追加する
基本的なテストパイプラインが動くようになった後、デプロイのステップを追加したくなりました。ここでジョブと依存関係の出番です。デプロイはテストが成功した後にのみ、かつ`main`へのpush時のみ(PRではなく)実行されるようにしたかったのです。
```yaml
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
deploy:
needs: test
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- name: Deploy to staging
run: echo "Deploying to staging server..."
# 実際には、ここでデプロイアクションやスクリプトを使用します
```
ここでの重要な追加点は以下の通りです。
- `needs: test` — これにより、`deploy`ジョブは`test`ジョブが正常に完了するまで待機するようになります。これがなければ、両方のジョブが並列で実行されてしまいます。
- `if: github.event_name == 'push'` — この条件により、デプロイはpull requestではなく、直接のpush時のみ実行されることが保証されます。PRはテストされますが、デプロイはされません。
## ステップ3:シークレットの扱い(私の失敗を真似しないでください)
実際のデプロイには、APIキーや認証情報、その他のシークレットが必要です。私の最初の直感は、YAMLファイルにデプロイトークンをハードコードすることでした。絶対にやってはいけません。リポジトリへの読み取りアクセス権がある人なら誰でも見られてしまいます。
GitHubには組み込みのシークレットストアがあります。リポジトリの設定(Settings)→ Secrets and variables → Actions に移動し、そこにシークレットを追加します。そして、ワークフロー内で次のように参照します。
```yaml
- name: Deploy to production
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
run: |
./deploy-script.sh
```
一つの驚きは、シークレットがログ内で自動的にマスクされることです。スクリプトが誤ってシークレットの値を出力しても、GitHubはそれを`***`に置き換えます。ただし、落とし穴があります。シークレットが4文字未満の場合、マスクされません。また、終了コードやタイミングを通じてシークレットが漏洩する可能性のあるコマンドを構成した場合も、漏れてしまうことがあります。シークレットは慎重に扱いましょう。
## ステップ4:マルチ環境テストのためのマトリックスビルド
次の課題は、異なるNode.jsバージョン間でコードが正しく動作することを確認することでした。ここでマトリックスビルドが威力を発揮します。各バージョンごとに別々のジョブを書く代わりに、マトリックス戦略を定義します。
```yaml
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
```
この1つのジョブ定義により、各Node.jsバージョンに1つずつ、合計3つの並列テスト実行が作成されます。アプリが複数のオペレーティングシステムもサポートする必要がある場合は、次のように追加できます。
```yaml
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
```
これにより、6つの並列ジョブ(3つのOS × 2つのNodeバージョン)が作成されます。非常に強力ですが、GitHubの無料枠では、プライベートリポジトリに対して月に2,000分しか提供されないことに注意してください。マトリックスビルドは、特に10倍の消費レートで分を消費するmacOSランナーで、あっという間にこの時間を使い切ってしまいます。
## ステップ5:高速化のためのキャッシュ
数回実行した後、ワークフローが遅いことに気づきました。`npm ci`が毎回すべてをゼロからダウンロードしていたのです。キャッシュを追加することで、ビルド時間を3分から45秒未満に短縮できました。
```yaml
- name: Cache node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
```
`key`はロックファイルのハッシュを使用しているため、依存関係が変更されるとキャッシュは自動的に無効化されます。`restore-keys`のフォールバックにより、完全に一致するものがない場合、そのOSの最新のキャッシュが使用され、実行後に更新されます。
## 実践的なヒントと正直な限界
ここ数ヶ月GitHub Actionsを使ってみて、学んだことは以下の通りです。
**ヒント:**
- アクションのバージョンは、必ず`@v4`のように指定するか、さらに特定のコミットSHAで固定してください。あるアクションが`main`ブランチを破壊的変更付きで更新したせいで、ワークフローが壊れたことがありました。
- CIでは`npm install`ではなく`npm ci`を使用してください。その方が速く、ロックファイルに厳密に従うため、「私の環境では動くのに」という不一致を防げます。
- Actionsタブのライブログは驚くほど役立ちます。失敗した特定の行をクリックして共有可能なリンクを取得できるため、チームメイトに障害を見てもらうのに最適です。
- シンプルに始めましょう。初日から10個のジョブを持つパイプラインを構築しようとしないでください。まずは基本的なテストワークフローを動かし、そこから反復していきましょう。
**限界:**
- プライベートリポジトリの月2,000分の無料枠は、マトリックスビルドやmacOSランナーを使うとあっという間に尽きます。使用量を監視してください。
- ワークフローのYAMLファイルは長くなり、管理が難しくなります。カスタムアクションの作成や再利用可能なワークフローの使用を除けば、モジュール化する優れた方法がなく、それらは複雑さを増すだけです。
- 失敗したワークフローのデバッグはイライラする可能性があります。失敗後にランナーにSSH接続することはできません。状態を検査するために余分な`run: echo`ステップを追加しなければならず、これは面倒です。
- セルフホストランナーはより多くの制御を提供しますが、メンテナンスとセキュリティの強化が必要です。GitHubのホストランナーのような「セットして忘れる」ソリューションではありません。
GitHub Actionsは、私たちのチームのコードのリリース方法を根本的に変えました。あの金曜日の決済障害?あれ以来発生していません。なぜなら、設定エラーや失敗したテスト、ビルドの問題が本番環境に到達する前に、CIパイプラインが確実に捕捉してくれるからです。セットアップにかかったのはわずか1午後でしたが、それ以来、数え切れないほどの時間を節約できています。