terada_life

rpn LP自動デプロイ設計書

1. 概要

現状の課題

  • Next.js(静的エクスポート)製のLP「rpn」を公開日に手動でビルド→FTPツールで手動アップロードしている
  • 1回の公開で複数パスへの配置が必要(環境変数を変えて複数回ビルド)
  • 公開日に人手が必要で、休日や早朝のリリースに対応しづらい
  • 手作業のため、アップロードミスや対応漏れのリスクがある

目的

GitHub Actions(GHA)を使い、**指定した日時に複数パスへ自動でFTPアップロード(予約デプロイ)**を実現する。

前提

  • Next.jsの静的エクスポート(next exportout/ ディレクトリ)
  • ビルドは開発者がローカルで手動実行する(GHA上ではビルドしない)
  • ビルド時に BASE_PATH / NEXT_PUBLIC_BASE_PATH 環境変数でパスを切り替え、複数回ビルドする
  • ビルド成果物はリポジトリ内の指定フォルダにコミットして管理する
  • FTPアップロード先はロリポップサーバー(FTPS接続)

2. デプロイ対象の構成

1案件で複数パスへの配置が発生する。以下は具体例。

rpnファイルの配置先

ビルド時 BASE_PATHFTPアップロード先内容
/202604/21_FB/0/rpn/202604/21_FB/0/rpn/rpn本体(パターン0)
/202604/21_FB/4/rpn/202604/21_FB/4/rpn/rpn本体(パターン4)
/rpn/rpn/rpn本体(共通)

リダイレクトファイルの配置先

FTPアップロード先リダイレクト先
/202604/21_FB/0//202604/21_FB/0/rpn/
/202604/21_FB/4//202604/21_FB/4/rpn/

3. ビルド手順(ローカル)

基本コマンド

# パスごとに環境変数を切り替えてビルド
export BASE_PATH=/202604/21_FB/0/rpn
export NEXT_PUBLIC_BASE_PATH=/202604/21_FB/0/rpn
npm run build
# → out/ にビルド成果物が生成される
# → builds/{案件名}/202604_21_FB_0_rpn/ にコピー

export BASE_PATH=/202604/21_FB/4/rpn
export NEXT_PUBLIC_BASE_PATH=/202604/21_FB/4/rpn
npm run build
# → builds/{案件名}/202604_21_FB_4_rpn/ にコピー

export BASE_PATH=/rpn
export NEXT_PUBLIC_BASE_PATH=/rpn
npm run build
# → builds/{案件名}/rpn/ にコピー

既存のビルドスクリプトは scripts/ ディレクトリを参照。

ローカル開発

export BASE_PATH=""
export NEXT_PUBLIC_BASE_PATH=""
npm run dev

4. 全体フロー

[開発者] 環境変数を切り替えながらローカルで複数回ビルド
    ↓
[開発者] builds/{案件名}/{パス別フォルダ}/ にそれぞれ配置
    ↓
[開発者] リダイレクトファイルも builds/{案件名}/redirects/ に配置
    ↓
[開発者] コミット&プッシュ
    ↓
[開発者] GitHub Actions UI → "Run workflow" でデプロイ予約
         (案件名、公開日時を入力)
    ↓
[GHA] deploy-schedule.json に予約情報を追記&コミット
    ↓
[開発者] リリース時刻に "デプロイ実行" を手動dispatch
         (または cron で自動検知 ※オプション)
    ↓
[GHA] deploy-config.json を読み取り
      各パスのビルド成果物をFTPS経由でアップロード
    ↓
[GHA] ステータスを "deployed" に更新

5. アーキテクチャ

2つのGitHub Actions Workflowで構成する。

Workflow 1: schedule-deploy.yml(デプロイ予約登録)

項目内容
トリガーworkflow_dispatch(手動実行)
役割デプロイ予約情報を deploy-schedule.json に登録

入力パラメータ

パラメータ説明
project_name案件名(builds/ 配下のフォルダ名)202604_21_FB
deploy_datetimeデプロイ予定日時(JST)2026-04-01 10:00

Workflowサンプル

name: デプロイ予約登録

on:
  workflow_dispatch:
    inputs:
      project_name:
        description: '案件名(builds/配下のフォルダ名)'
        required: true
        type: string
      deploy_datetime:
        description: 'デプロイ日時(JST) 例: 2026-04-01 10:00'
        required: true
        type: string

jobs:
  register:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: 入力値バリデーション
        run: |
          # ビルド成果物フォルダの存在確認
          if [ ! -d "builds/${{ github.event.inputs.project_name }}" ]; then
            echo "::error::builds/${{ github.event.inputs.project_name }} が見つかりません"
            exit 1
          fi

          # deploy-config.json の存在確認
          if [ ! -f "builds/${{ github.event.inputs.project_name }}/deploy-config.json" ]; then
            echo "::error::deploy-config.json が見つかりません"
            exit 1
          fi

          # 日時フォーマットチェック(YYYY-MM-DD HH:MM)
          if ! echo "${{ github.event.inputs.deploy_datetime }}" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$'; then
            echo "::error::日時フォーマットが不正です(例: 2026-04-01 10:00)"
            exit 1
          fi

      - name: スケジュール登録
        run: |
          DEPLOY_ISO=$(echo "${{ github.event.inputs.deploy_datetime }}:00+09:00" | tr ' ' 'T')

          # deploy-schedule.json が無ければ空配列で初期化
          if [ ! -f deploy-schedule.json ]; then
            echo '[]' > deploy-schedule.json
          fi

          # 新規エントリを追加
          cat deploy-schedule.json | jq \
            --arg name "${{ github.event.inputs.project_name }}" \
            --arg dt "$DEPLOY_ISO" \
            '. += [{
              "project_name": $name,
              "deploy_datetime": $dt,
              "status": "pending",
              "deployed_at": null
            }]' > tmp.json && mv tmp.json deploy-schedule.json

      - name: コミット&プッシュ
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add deploy-schedule.json
          git commit -m "chore: デプロイ予約追加 - ${{ github.event.inputs.project_name }}"
          git push

Workflow 2: deploy.yml(デプロイ実行)

項目内容
トリガーworkflow_dispatch(手動実行)※メイン
トリガー(オプション)schedule(cron)※リリース日前後のみ有効化
役割予約時刻に達した案件の全パスをFTPS経由でアップロード

運用方針: リリースは月1回のため、常時cronは不要。リリース時刻に手動dispatchで実行するのが基本。必要に応じてリリース前後だけcronを有効化する。

処理フロー

  1. deploy-schedule.json を読み込み
  2. status: "pending" かつ deploy_datetime ≤ 現在時刻 のエントリを検出
  3. 該当案件の deploy-config.json を読み取り、各デプロイ先を取得
  4. 各パスのビルド成果物をFTPS経由でアップロード
  5. リダイレクトファイルもアップロード
  6. ステータスを deployed(成功時)または failed(失敗時)に更新

Workflowサンプル

name: デプロイ実行

on:
  # 手動実行(メイン)
  workflow_dispatch:
  # cron(オプション:リリース日前後のみ有効化する場合はコメントアウトを外す)
  # schedule:
  #   - cron: '*/15 * * * *'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: 対象エントリの確認
        id: check
        run: |
          if [ ! -f deploy-schedule.json ]; then
            echo "has_targets=false" >> $GITHUB_OUTPUT
            exit 0
          fi

          NOW=$(TZ=Asia/Tokyo date +"%Y-%m-%dT%H:%M:%S+09:00")
          TARGETS=$(cat deploy-schedule.json | jq \
            --arg now "$NOW" \
            '[.[] | select(.status == "pending" and .deploy_datetime <= $now)]')
          COUNT=$(echo "$TARGETS" | jq 'length')

          if [ "$COUNT" -eq 0 ]; then
            echo "デプロイ対象なし"
            echo "has_targets=false" >> $GITHUB_OUTPUT
          else
            echo "デプロイ対象: ${COUNT}件"
            echo "has_targets=true" >> $GITHUB_OUTPUT
            echo "$TARGETS" > targets.json
          fi

      - name: FTPSデプロイ
        if: steps.check.outputs.has_targets == 'true'
        env:
          FTP_SERVER: ${{ secrets.FTP_SERVER }}
          FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
          FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
        run: |
          # lftp インストール
          sudo apt-get update && sudo apt-get install -y lftp

          # 各対象案件をデプロイ
          cat targets.json | jq -c '.[]' | while read -r entry; do
            PROJECT_NAME=$(echo "$entry" | jq -r '.project_name')
            NOW=$(TZ=Asia/Tokyo date +"%Y-%m-%dT%H:%M:%S+09:00")
            CONFIG_FILE="builds/${PROJECT_NAME}/deploy-config.json"
            DEPLOY_SUCCESS=true

            echo "=========================================="
            echo "案件デプロイ開始: $PROJECT_NAME"
            echo "=========================================="

            # deploy-config.json から各デプロイ先を読み取り
            cat "$CONFIG_FILE" | jq -c '.deployments[]' | while read -r deploy; do
              LOCAL_DIR=$(echo "$deploy" | jq -r '.local_dir')
              REMOTE_PATH=$(echo "$deploy" | jq -r '.remote_path')
              DESCRIPTION=$(echo "$deploy" | jq -r '.description // ""')

              echo "--- デプロイ: ${DESCRIPTION} ---"
              echo "  ローカル: builds/${PROJECT_NAME}/${LOCAL_DIR}"
              echo "  リモート: ${REMOTE_PATH}"

              # FTPS経由でアップロード
              lftp -c "
                set ftp:ssl-allow yes
                set ftp:ssl-force yes
                set ssl:verify-certificate no
                open -u $FTP_USERNAME,$FTP_PASSWORD $FTP_SERVER
                mirror --reverse --delete builds/${PROJECT_NAME}/${LOCAL_DIR} ${REMOTE_PATH}
                bye
              "

              if [ $? -ne 0 ]; then
                echo "::error::デプロイ失敗: ${LOCAL_DIR} → ${REMOTE_PATH}"
                DEPLOY_SUCCESS=false
              else
                echo "デプロイ成功: ${LOCAL_DIR} → ${REMOTE_PATH}"
              fi
            done

            # ステータス更新
            if [ "$DEPLOY_SUCCESS" = true ]; then
              cat deploy-schedule.json | jq \
                --arg name "$PROJECT_NAME" \
                --arg now "$NOW" \
                'map(if .project_name == $name and .status == "pending" then .status = "deployed" | .deployed_at = $now else . end)' \
                > tmp.json && mv tmp.json deploy-schedule.json
            else
              cat deploy-schedule.json | jq \
                --arg name "$PROJECT_NAME" \
                'map(if .project_name == $name and .status == "pending" then .status = "failed" else . end)' \
                > tmp.json && mv tmp.json deploy-schedule.json
            fi
          done

      - name: ステータス更新コミット
        if: steps.check.outputs.has_targets == 'true'
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add deploy-schedule.json
          git diff --cached --quiet || git commit -m "chore: デプロイステータス更新"
          git push

6. リポジトリ構成

プロジェクトルート/
├── builds/
│   └── {案件名}/                    # 案件ごとのフォルダ
│       ├── deploy-config.json       # この案件のデプロイ設定(パスのマッピング)
│       ├── 202604_21_FB_0_rpn/      # ビルド成果物(パターン0)
│       │   ├── index.html
│       │   └── ...
│       ├── 202604_21_FB_4_rpn/      # ビルド成果物(パターン4)
│       │   └── ...
│       ├── rpn/                     # ビルド成果物(共通)
│       │   └── ...
│       └── redirects/               # リダイレクトファイル
│           ├── 202604_21_FB_0/
│           │   └── index.html       # → /202604/21_FB/0/rpn/ へリダイレクト
│           └── 202604_21_FB_4/
│               └── index.html       # → /202604/21_FB/4/rpn/ へリダイレクト
├── scripts/                         # 既存のビルドスクリプト
├── deploy-schedule.json             # デプロイ予約管理ファイル
└── .github/
    └── workflows/
        ├── schedule-deploy.yml      # Workflow 1: 予約登録
        └── deploy-cron.yml          # Workflow 2: 定期チェック&実行

7. deploy-config.json(案件ごとのデプロイ設定)

各案件フォルダ内に配置し、ビルド成果物とFTPパスのマッピングを定義する。

{
  "project_name": "202604_21_FB",
  "deployments": [
    {
      "description": "rpn本体(パターン0)",
      "local_dir": "202604_21_FB_0_rpn",
      "remote_path": "/202604/21_FB/0/rpn/"
    },
    {
      "description": "rpn本体(パターン4)",
      "local_dir": "202604_21_FB_4_rpn",
      "remote_path": "/202604/21_FB/4/rpn/"
    },
    {
      "description": "rpn本体(共通)",
      "local_dir": "rpn",
      "remote_path": "/rpn/"
    },
    {
      "description": "リダイレクト(パターン0)",
      "local_dir": "redirects/202604_21_FB_0",
      "remote_path": "/202604/21_FB/0/"
    },
    {
      "description": "リダイレクト(パターン4)",
      "local_dir": "redirects/202604_21_FB_4",
      "remote_path": "/202604/21_FB/4/"
    }
  ]
}

ポイント: deploy-config.json にすべてのデプロイ先を列挙することで、Workflow側はこのファイルを読むだけで全パスにデプロイできる。案件ごとにパス構成が異なっても柔軟に対応可能。


8. deploy-schedule.json スキーマ

[
  {
    "project_name": "202604_21_FB",
    "deploy_datetime": "2026-04-01T10:00:00+09:00",
    "status": "pending",
    "deployed_at": null
  }
]
フィールド説明
project_namestring案件名(builds/ 配下のフォルダ名と一致)
deploy_datetimestring (ISO 8601)デプロイ予定日時(JST)
statusstringpending / deployed / failed / cancelled
deployed_atstring | null実際にデプロイされた日時

9. GitHub Secrets 設定

リポジトリの Settings → Secrets and variables → Actions に以下を登録する。

Secret名内容
FTP_SERVERFTPサーバーホスト名
FTP_USERNAMEFTPアカウント
FTP_PASSWORDFTPパスワード

重要: FTPクレデンシャルは絶対にコードやドキュメントに直接記載しないこと。


10. 運用フロー

デプロイ予約の手順

  1. ビルド: 環境変数を切り替えながらローカルで複数回ビルド

    export BASE_PATH=/202604/21_FB/0/rpn
    export NEXT_PUBLIC_BASE_PATH=/202604/21_FB/0/rpn
    npm run build
    cp -r out/ builds/202604_21_FB/202604_21_FB_0_rpn/
    

    (パスごとに繰り返す)

  2. リダイレクトファイル配置: 必要に応じてリダイレクト用HTMLを作成・配置

  3. deploy-config.json 作成: デプロイ先のマッピングを定義

  4. コミット&プッシュ: 全成果物+設定ファイルをリポジトリにプッシュ

  5. 予約登録: GitHub → Actions → "デプロイ予約登録" → "Run workflow"

    • project_name: 案件フォルダ名(例: 202604_21_FB
    • deploy_datetime: 公開日時(例: 2026-04-01 10:00
  6. 確認: deploy-schedule.jsonpending ステータスで登録されたことを確認

デプロイの確認

  • GitHub Actions の "デプロイ定期チェック&実行" ワークフローの実行ログで確認
  • deploy-schedule.jsonstatusdeployed に変わっていれば成功

予約のキャンセル

deploy-schedule.json の該当エントリの status を手動で cancelled に変更してコミットする。


11. 注意事項・制約

項目詳細
デプロイ実行基本は手動dispatch。cronを使う場合は最大15〜30分の遅延あり
リポジトリサイズビルド成果物をコミットするため、リポジトリサイズが増加する。不要になった成果物は定期的に削除すること
同時デプロイ同一案件名で複数の pending エントリがある場合、すべてが実行される。重複登録に注意
GHA無料枠パブリックリポジトリは無制限。プライベートリポジトリは月2,000分(Freeプラン)
FTPS接続ロリポップサーバーはFTPS対応。lftp で暗号化接続する
リダイレクトファイルFTPの --delete オプションにより既存ファイルが削除される可能性がある。リダイレクト先のディレクトリとrpnディレクトリが別アップロードの場合、順序に注意

12. 今後の拡展案(参考)

  • デプロイ完了時にSlack通知を送る
  • デプロイ失敗時のリトライ機能
  • ビルド成果物の自動クリーンアップWorkflow
  • ビルド自体もGHA上で実行する(環境変数+マトリクスビルド)

作成日時: 2026/03/26