GitHub Actionsでデプロイを自動化する — 静的サイト/サーバーへの基本パターン
TL;DR
- 手動デプロイは「手順の抜け漏れ」と「環境差」で事故が起きる。pushを起点に自動化すると再現性が上がる。
- GitHub Actionsは
workflow(YAML)→job→stepの3層構造で、on:でいつ実行するかを指定する。 - 静的サイトはビルド成果物をホスティング先へ転送するだけで、数十行のYAMLで完結する。
- APIキーやSSH秘密鍵はYAMLに直書きせず、必ずリポジトリの Secrets に格納して
${{ secrets.NAME }}で参照する。 - 失敗時はジョブの
if: failure()でSlack等へ通知できる。気づかないまま壊れた状態を放置しない。
前提として、インフラ設定をGitで管理する流れは /articles/why-infra-needs-git/ で扱っている。本記事はその続きで、「Git管理されたものを、pushを起点に自動で反映する」段階の話だ。
手動デプロイの何が危険か
手動デプロイとは、おおよそ次のような作業を人間が毎回行うことを指す。
- ローカルでビルドする
- 成果物をFTPやSCPでサーバーへ転送する
- サーバーにSSHで入ってサービスを再起動する
この流れには再現性がない。問題になるのは主に次の3点だ。
| 危険 | 具体例 |
|---|---|
| 手順の抜け漏れ | ビルドを忘れて古い成果物を上げる/再起動を忘れる |
| 環境差 | 担当者のローカルのNodeバージョンが本番と違い、動くものと動かないものが出る |
| 属人化 | 「デプロイできるのは特定の1人だけ」になり、その人が不在だと止まる |
CI/CDは、この一連の手順を「コードとして」リポジトリに書いておき、機械に毎回まったく同じ順番で実行させる仕組みだ。CI(継続的インテグレーション)はビルドやテストの自動実行、CD(継続的デプロイ)はその成果物を環境へ反映する部分を指す。GitHub Actionsはこの両方をGitHubリポジトリ内で完結させられる。
GitHub Actionsの基本構造
GitHub Actionsの設定は、リポジトリの .github/workflows/ 配下に置いたYAMLファイルで定義する。構成要素は次の4つを押さえれば足りる。
| 要素 | 役割 |
|---|---|
trigger(on:) | いつ実行するか(pushされた時、PRが出た時、手動実行など) |
| workflow | 自動化の単位。1つのYAMLファイルが1つのworkflow |
| job | workflow内の実行単位。仮想マシン上で動く。複数jobは既定で並列実行 |
| step | job内の個々の手順。コマンド実行や既製アクションの呼び出し |
最小の例で構造を確認する。
# .github/workflows/hello.yml
name: Hello
on:
push:
branches: [main]
jobs:
greet:
runs-on: ubuntu-latest
steps:
- name: メッセージを表示
run: echo "main にpushされました"
on: push で「mainへのpushを起点に動く」、runs-on: ubuntu-latest で「GitHubが用意するUbuntu仮想マシン上で動く」ことを指定している。steps の各要素が、上から順に実行される。
uses: は公式・サードパーティが公開している既製の「アクション」を呼び出す指定で、run: は仮想マシン上で任意のシェルコマンドを実行する指定だ。
静的サイトをビルドして公開する
ここでは典型例として、Node製の静的サイトジェネレータでビルドし、その成果物をGitHub Pagesへ公開する流れを示す。npm run build で dist ディレクトリに成果物が出る構成を想定する。
# .github/workflows/deploy-pages.yml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
# GitHub Pagesへの公開に必要な権限
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: リポジトリを取得
uses: actions/checkout@v4
- name: Node.jsをセットアップ
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: 依存をインストール
run: npm ci
- name: ビルド
run: npm run build
- name: 成果物をアップロード
uses: actions/upload-pages-artifact@v3
with:
path: ./dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Pagesへデプロイ
id: deployment
uses: actions/deploy-pages@v4
ポイントは次の通り。
actions/checkout@v4でリポジトリのコードを仮想マシンへ取り込む。これを忘れるとファイルが無い状態でビルドが走り失敗する。npm ciはpackage-lock.jsonに固定されたバージョンで入れるため、CI環境ではnpm installより再現性が高い。needs: buildでdeployジョブがbuildジョブの完了を待つ。並列が既定なので、順序が必要な場合は明示する。
GitHub Pagesを使う場合は、リポジトリの Settings → Pages で Source を「GitHub Actions」に設定しておく必要がある。
Secrets管理 — 鍵をYAMLに直書きしない
ここが事故の起きやすい最重要ポイントだ。APIキー・トークン・SSH秘密鍵などをYAMLファイルに直接書いてはいけない。YAMLはリポジトリにコミットされるため、書いた瞬間に履歴へ永久に残る。リポジトリが公開・共有された時点で漏洩する。
GitHubには Secrets という暗号化された保管領域がある。
- リポジトリの Settings → Secrets and variables → Actions を開く
- 「New repository secret」で名前と値を登録する(例:
SSH_PRIVATE_KEY) - YAMLからは
${{ secrets.SSH_PRIVATE_KEY }}で参照する
Secretsの値はログ上で自動的に *** にマスクされる。ただし完全ではない点に注意がいる。値をBase64でデコードして出力したり、別の変数へ加工して表示すると、マスクが効かず平文で漏れることがある。デバッグ目的でも秘密情報を echo しない習慣をつける。
なお、GitHub上での秘密情報の漏洩経路は広い。コミット履歴・Issue・ログなど複数の経路がある点は /articles/github-data-leak-attack-vectors/ で別途整理している。
サーバーへSSHでデプロイする
GitHub Pagesのようなホスティングではなく、自前のVPSや専用サーバーへ反映するケースでは、SSH経由でファイル転送とサービス再起動を行う。
事前準備として、デプロイ専用のSSH鍵ペアを作り、公開鍵をサーバー側の ~/.ssh/authorized_keys に登録し、秘密鍵をSecretsの SSH_PRIVATE_KEY に格納しておく。普段使いの鍵を流用せず、用途を限定した鍵を使うのが望ましい。
# .github/workflows/deploy-ssh.yml
name: Deploy via SSH
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: リポジトリを取得
uses: actions/checkout@v4
- name: Node.jsをセットアップ
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: ビルド
run: |
npm ci
npm run build
- name: SSH鍵を準備
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts
- name: rsyncで転送
run: |
rsync -avz --delete ./dist/ \
${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/html/
- name: サービスを再起動
run: |
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \
"sudo systemctl reload nginx"
補足。
ssh-keyscanで接続先のホスト鍵をknown_hostsに登録している。これを省くと初回接続の確認プロンプトで処理が止まる。中間者攻撃対策としては、本来は既知のフィンガープリントと照合するのが厳密だ。rsync --deleteは転送元に無いファイルを転送先から削除する。古い成果物が残らない利点がある一方、対象ディレクトリを間違えると消すべきでないものを消す。パスは慎重に指定する。appleboy/ssh-actionなどの既製アクションをuses:で使う方法もあり、複数コマンドをまとめて流す場合は記述が簡潔になる。
Cloudflare Pages等のホスティングへ自動デプロイ
Cloudflare PagesやVercel、Netlifyのようなホスティングサービスは、GitHubリポジトリと直接連携する機能を持っている。その場合、Actionsを自分で書かなくても、サービス側がpushを検知してビルド・デプロイを行う。これが最も手間が少ない。
一方で、ビルド処理をActions側で行い、成果物だけをCloudflareへ渡したい場合は、公式アクション cloudflare/wrangler-action を使う。Cloudflareダッシュボードで発行したAPIトークンとアカウントIDをSecretsに入れておく。
# .github/workflows/deploy-cf-pages.yml
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: リポジトリを取得
uses: actions/checkout@v4
- name: Node.jsをセットアップ
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: ビルド
run: |
npm ci
npm run build
- name: Cloudflare Pagesへデプロイ
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=my-site
APIトークンはCloudflareダッシュボードの My Profile → API Tokens から、Pages編集に必要な権限だけを付けて発行するのが原則だ。広い権限のトークンを使い回さない。
失敗時に気づく仕組みを入れる
自動化の落とし穴は「壊れていることに気づかない」ことだ。デプロイが失敗してもジョブが赤くなるだけで、誰も画面を見ていなければ古いままの本番が放置される。失敗時に通知を飛ばすステップを足しておく。
各ステップやジョブには if: 条件を付けられる。failure() は「同じジョブ内の前のステップが失敗した場合に真」になる関数で、これを使うと「失敗時だけ通知する」が書ける。
- name: ビルドとデプロイ
run: |
npm ci
npm run build
# ここで失敗すると以降のステップは通常スキップされる
- name: 失敗をSlackへ通知
if: failure()
run: |
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"デプロイ失敗: ${{ github.repository }} / ${{ github.sha }}\"}" \
"${{ secrets.SLACK_WEBHOOK_URL }}"
${{ github.repository }} や ${{ github.sha }} はGitHub Actionsが提供するコンテキスト変数で、どのリポジトリのどのコミットで失敗したかを通知に含められる。Webhook URL自体も秘密情報なのでSecretsに入れる。
GitHubには標準で、ワークフロー失敗時に該当コミットの作成者へメール通知する機能もある。チャットツールへ流したい場合に上記のようなステップを足す、という整理になる。
まとめ
| 段階 | やること | 鍵になる指定 |
|---|---|---|
| 基本構造 | .github/workflows/*.yml に定義 | on: / jobs: / steps: |
| 静的サイト | ビルド→成果物をホスティングへ | actions/checkout / npm ci |
| Secrets | 鍵はSecretsへ格納し参照 | ${{ secrets.NAME }} |
| SSHデプロイ | 専用鍵で転送・再起動 | rsync / ssh-keyscan |
| ホスティング連携 | サービス連携 or 公式アクション | cloudflare/wrangler-action |
| 失敗通知 | 失敗時だけ知らせる | if: failure() |
手動デプロイは、回数を重ねるほど「いつもの手順」が省略され、事故の確率が上がる。pushを起点に同じ手順を機械へ任せれば、再現性が確保され、属人化も解消される。
始め方はシンプルだ。まず echo を実行するだけのworkflowを1本置いてGitHub Actionsが動くことを確認し、そこへビルドとデプロイのステップを1つずつ足していく。秘密情報をSecretsへ移すこと、失敗時の通知を入れることを忘れなければ、最初の1本は十分実用になる。