このサイトは、GitHub Actionsでビルドした静的ファイルをXserverにアップロードしています。デプロイがしばらく失敗したままになっていたのをきっかけに、SCPからrsyncに変更し、ついでに実行回数も絞りました。途中でハマったところも含めて残します。
1. まずSCPのデプロイが失敗していた
appleboy/scp-action のログには次のように出ていました。
ssh.ParsePrivateKey: ssh: no key found
ssh: handshake failed: ssh: unable to authenticate, attempted methods [none], no supported methods remain
Secretsに登録していた秘密鍵が、鍵として読み込めない状態でした。手元で使っている鍵をそのまま登録し直すこともできますが、Actions専用の鍵ペアを作ってサーバーの authorized_keys に追加しました。万一漏れても、その鍵だけを無効にすればよく、手元のSSHには影響しません。
$ ssh-keygen -t ed25519 -N "" -C "github-actions" -f ./id_actions
$ gh secret set SFTP_KEY < ./id_actions
2. strip_componentsを付けないと、out/ディレクトリごと置かれる
鍵を直したらデプロイは成功しましたが、ファイルがドキュメントルート直下ではなく public_html/out/ に置かれていました。source: "./out/*" と書いても、out/ というパスが付いたままアップロードされるためです。
source: "./out/*"
strip_components: 1 # out/ ディレクトリごとではなく中身を target 直下に置く
本番の表示に影響はありませんでしたが、https://example.com/out/ にサイトのコピーが公開された状態になっていたので、すぐに削除しました。
3. rsync --deleteに変更
SCPは上書きするだけで、削除したページや古いビルドファイルがサーバーに残り続けます。そこで、rsyncの --delete で out/ とサーバーを同期する形にしました。
注意点は、サーバー上でだけ管理しているものまで消してしまうことです。このサイトでは、同じドキュメントルートの中にWordPress(サブドメイン用のフォルダ)を置いているので、除外リストをファイルにまとめました。
/cms.example.com/
/.well-known/
.DS_Store
--exclude で指定したパスは、--delete でも削除されません。手元からのデプロイ(npm run deploy)とActionsで同じファイルを使っています。
- name: Deploy to Xserver via rsync
env:
SSH_KEY: ${{ secrets.SFTP_KEY }}
SSH_HOST: ${{ secrets.SFTP_HOST }}
SSH_PORT: ${{ secrets.SFTP_PORT }}
SSH_USER: ${{ secrets.SFTP_USER }}
TARGET_DIR: ${{ secrets.SFTP_TARGET_DIR }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -p "$SSH_PORT" "$SSH_HOST" >> ~/.ssh/known_hosts 2>/dev/null
rsync -az --delete --exclude-from=.rsync-exclude \
-e "ssh -p $SSH_PORT -i ~/.ssh/deploy_key" \
out/ "$SSH_USER@$SSH_HOST:$TARGET_DIR/"
--delete は消すファイルを間違えると取り返しがつかないので、初回は必ず -n(dry-run)で削除されるファイルを確認しました。
$ rsync -avzn --delete --exclude-from=.rsync-exclude out/ server:~/public_html/ | grep '^deleting'
実際に、dry-runで想定外の削除(作業途中の画像の差し替え)が見つかり、デプロイを止めて確認できました。
4. 実行回数を絞る
privateリポジトリの場合、GitHub Freeでは毎月の無料枠(分数)が決まっています。READMEを直しただけで本番デプロイが走るのは無駄なので、サイトの出力に関係するファイルが変わったときだけ動くようにしました。
on:
push:
branches:
- main
paths:
- 'src/**'
- 'public/**'
- 'content/**'
- 'package.json'
- 'package-lock.json'
- 'next.config.mjs'
# ビルド時に生成されるサイトマップの更新だけではデプロイしない
- '!public/sitemap*.xml'
workflow_dispatch:
# 連続で push したときは、古い実行を取り消して最新だけを実行する
concurrency:
group: deploy
cancel-in-progress: true
あわせて actions/setup-node の cache: npm と npm ci で、インストールの時間も短くしています。
まとめ
- Actions用のSSH鍵は専用のものを作る
scp-actionでout/*を送るならstrip_components: 1rsync --deleteは除外リストとdry-runをセットでpathsとconcurrencyで無駄な実行を減らす