GitHub Actionsでデプロイを自動化する — 静的サイト/サーバーへの基本パターン

開発基盤・IaC・DevOps中級
GitHub ActionsCI/CDデプロイ自動化Secrets管理DevOps

TL;DR

  • 手動デプロイは「手順の抜け漏れ」と「環境差」で事故が起きる。pushを起点に自動化すると再現性が上がる。
  • GitHub Actionsは workflow(YAML)→ jobstep の3層構造で、on: でいつ実行するかを指定する。
  • 静的サイトはビルド成果物をホスティング先へ転送するだけで、数十行のYAMLで完結する。
  • APIキーやSSH秘密鍵はYAMLに直書きせず、必ずリポジトリの Secrets に格納して ${{ secrets.NAME }} で参照する。
  • 失敗時はジョブの if: failure() でSlack等へ通知できる。気づかないまま壊れた状態を放置しない。

前提として、インフラ設定をGitで管理する流れは /articles/why-infra-needs-git/ で扱っている。本記事はその続きで、「Git管理されたものを、pushを起点に自動で反映する」段階の話だ。


手動デプロイの何が危険か

手動デプロイとは、おおよそ次のような作業を人間が毎回行うことを指す。

  1. ローカルでビルドする
  2. 成果物をFTPやSCPでサーバーへ転送する
  3. サーバーに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
jobworkflow内の実行単位。仮想マシン上で動く。複数jobは既定で並列実行
stepjob内の個々の手順。コマンド実行や既製アクションの呼び出し

最小の例で構造を確認する。

# .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 builddist ディレクトリに成果物が出る構成を想定する。

# .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 cipackage-lock.json に固定されたバージョンで入れるため、CI環境では npm install より再現性が高い。
  • needs: builddeploy ジョブが build ジョブの完了を待つ。並列が既定なので、順序が必要な場合は明示する。

GitHub Pagesを使う場合は、リポジトリの Settings → Pages で Source を「GitHub Actions」に設定しておく必要がある。


Secrets管理 — 鍵をYAMLに直書きしない

ここが事故の起きやすい最重要ポイントだ。APIキー・トークン・SSH秘密鍵などをYAMLファイルに直接書いてはいけない。YAMLはリポジトリにコミットされるため、書いた瞬間に履歴へ永久に残る。リポジトリが公開・共有された時点で漏洩する。

GitHubには Secrets という暗号化された保管領域がある。

  1. リポジトリの Settings → Secrets and variables → Actions を開く
  2. 「New repository secret」で名前と値を登録する(例: SSH_PRIVATE_KEY
  3. 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本は十分実用になる。

関連記事

開発基盤・IaC・DevOpsCoolify vs Dokku vs CapRover:セルフホストPaaSの選び方開発基盤・IaC・DevOpsAnsibleとTerraformの使い分け:プロビジョニングと設定管理の境界線開発基盤・IaC・DevOps情シス担当のためのBash入門:サーバー作業で必ず使うコマンドと基本構文
記事一覧に戻る