長らく、Githubを利用しているのですが、恥ずかしながら、Github Actionを使った事がなかったため、初めてのGithub Action構築です。
今回の環境では、デプロイ先がSFTPのみの接続方式のため、Githubのリポジトリに変更が生じたら、自動でSFTPによりデプロイ先に転送する仕組みを作りたかったのです。
私密鍵の中身はリポジトリにも記事にも置きません。GitHub Actions の Secret だけに登録します。
git push したら、GitHub Actions が SFTP で公開ディレクトリへファイルを上げる、という流れを組み立てました。
パスワード認証ではなく SSH 鍵認証です。転送は SFTP のみで、SCP や rsync によるリモート転送は使いません。
この記事で作るもの
flowchart LR pushMain["push to main"] --> gha["GitHub Actions"] gha --> checkout["checkout"] checkout --> prepare["exclude .git and .github"] prepare --> sftp["SFTP with SSH key"] sftp --> remote["sftp.example.com:/home/deploy/public_html/example-site"]
| 項目 | この記事での例 | 自分の環境で用意するもの |
|---|---|---|
| GitHub リポジトリ | your-github-id/example-site | デプロイしたいリポジトリ(private 推奨) |
| ローカル作業ディレクトリ | ~/git/example-site | そのリポジトリのクローン先 |
| SFTP ホスト | sftp.example.com | ホスト名、または接続用の IP アドレス |
| SFTP ユーザ | deploy | サーバ上のログイン名 |
| リモートパス | /home/deploy/public_html/example-site | 公開したいディレクトリ |
| SSH 鍵 | ~/.ssh/id_ed25519 | パスフレーズなしの私密鍵(公開鍵はサーバへ登録) |
| GitHub Secret 名 | SFTP_SSH_PRIVATE_KEY | ワークフローから参照する名前。一致させる |
仕様:
- トリガーは
mainへの push -
.gitと.githubは転送しない - 基本形は上書きアップロード(サーバ側だけに残ったファイルは消さない)
- 完全同期にする場合は、後述の
lftp mirror --deleteに切り替える
準備段階
ワークフローを書く前に、次を揃えておきます。ここが抜けていると、あとから Actions のログを見ても原因が切り分けにくくなります。
1. 手元のツール
- GitHub アカウント
-
gitと GitHub CLI (gh)(ログイン済み) - SSH クライアント(macOS / Linux なら標準で入っていることが多い)
gh のログイン確認:
gh auth status Git 操作プロトコルは HTTPS でも SSH でも構いません。Secret の登録には repo と workflow スコープが必要です。
2. SFTP サーバ側
次が使える状態か、自分のマシンから確認します。
- SFTP(通常はポート 22)でログインできる
- デプロイ先ディレクトリが存在する
- そのディレクトリへ書き込める
リモートディレクトリは事前に作っておきます。ワークフローの cd は、無いディレクトリでは失敗します。
ssh [email protected] "mkdir -p /home/deploy/public_html/example-site" 手元から SFTP できるかも確認します。
sftp [email protected] つながったら cd /home/deploy/public_html/example-site と ls で、パスと権限を見ておきます。
GitHub Actions の runner は GitHub 側の outbound IP から接続します。サーバのファイアウォールや tcpwrapper で接続元を絞っている場合は、Actions からの SSH を許可する必要があります。
3. SSH 鍵ペア
パスフレーズなしの ED25519 鍵を用意します。既存のデプロイ用鍵があればそれを使い、無ければ新規作成します。
ssh-keygen -t ed25519 -C "github-actions-example-site" -f ~/.ssh/id_ed25519 -N "" - 私密鍵:
~/.ssh/id_ed25519(GitHub Secret に登録する。リポジトリには置かない) - 公開鍵:
~/.ssh/id_ed25519.pub(サーバのauthorized_keysに追加する)
公開鍵をサーバへ登録する例:
ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected] またはサーバ上で:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
cat >> ~/.ssh/authorized_keys << 'EOF'
(id_ed25519.pub の1行を貼る)
EOF
chmod 600 ~/.ssh/authorized_keys 登録後、パスワードを聞かれずに入れることを確認します。
ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes [email protected] true この記事のワークフローは、パスフレーズ付き私密鍵には対応していません。使うならパスフレーズなしのデプロイ専用鍵にしてください。
4. GitHub リポジトリ
ローカルにプロジェクトがある場合:
cd ~/git/example-site
git init -b main
gh repo create example-site --private --source=. --remote=origin すでにリポジトリがある場合は、origin が GitHub を向いていることだけ確認します。
git remote -v private でも Actions は使えます。Settings → Actions → General で、ワークフローが無効になっていないことを確認します。
5. GitHub Secret に私密鍵を登録する
私密鍵はファイルとしてコミットしません。リポジトリの Secret だけに置きます。
CLI:
gh secret set SFTP_SSH_PRIVATE_KEY --repo your-github-id/example-site < ~/.ssh/id_ed25519
gh secret list --repo your-github-id/example-site Web UI:
- リポジトリ → Settings → Secrets and variables → Actions
- New repository secret
- Name は
SFTP_SSH_PRIVATE_KEY(ワークフローと同じ名前) - Secret には私密鍵の全文を貼る
-
-----BEGIN OPENSSH PRIVATE KEY-----から-----END OPENSSH PRIVATE KEY-----まで - 末尾の改行も残す
-
登録後に値は再表示できません。名前と更新日時だけ確認できます。
6. 準備のチェックリスト
sftp [email protected] できる authorized_keys にある SFTP_SSH_PRIVATE_KEY を登録した ここまで終わってから、ワークフローを置きます。
実装
ワークフローファイル
リポジトリに .github/workflows/sftp-deploy.yml を置きます。
ホスト・ユーザ・パスは環境変数に書いてあります。Secret は私密鍵だけです。
私密鍵は env: 経由でファイル化します。GitHub Secret の改行を保ったまま runner 上の sftp に渡すためです。
name: SFTP Deploy
on:
push:
branches:
- main
jobs:
sftpDeploy:
name: Deploy files via SFTP
runs-on: ubuntu-latest
env:
remoteHost: "sftp.example.com"
remoteUser: "deploy"
remotePath: "/home/deploy/public_html/example-site"
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Prepare files excluding git metadata
id: prepareFiles
run: |
deployDir="${{ runner.temp }}/sftp-deploy"
mkdir -p "${deployDir}"
rsync -a --exclude '.git/' --exclude '.github/' ./ "${deployDir}/"
fileCount=$(find "${deployDir}" -mindepth 1 | wc -l | tr -d ' ')
echo "fileCount=${fileCount}"
if [ "${fileCount}" -eq 0 ]; then
echo "hasFiles=false" >> "${GITHUB_OUTPUT}"
echo "転送対象が空のため、SFTP をスキップします。"
else
echo "hasFiles=true" >> "${GITHUB_OUTPUT}"
fi
- name: Deploy via SFTP
if: steps.prepareFiles.outputs.hasFiles == 'true'
env:
SFTP_SSH_PRIVATE_KEY: ${{ secrets.SFTP_SSH_PRIVATE_KEY }}
run: |
set -euo pipefail
set +x
if [ -z "${SFTP_SSH_PRIVATE_KEY}" ]; then
echo "SFTP_SSH_PRIVATE_KEY が空です。GitHub Secret を確認してください。" >&2
exit 1
fi
install -d -m 700 "${HOME}/.ssh"
keyFile="${HOME}/.ssh/id_ed25519"
printf '%s\n' "${SFTP_SSH_PRIVATE_KEY}" | tr -d '\r' > "${keyFile}"
chmod 600 "${keyFile}"
echo "私密鍵の指紋を確認します(中身は出力しません)。"
ssh-keygen -l -f "${keyFile}"
ssh-keyscan -H "${remoteHost}" >> "${HOME}/.ssh/known_hosts"
deployDir="${RUNNER_TEMP}/sftp-deploy"
batchFile="${RUNNER_TEMP}/sftp-batch"
{
echo "cd ${remotePath}"
(
cd "${deployDir}"
find . -mindepth 1 -maxdepth 1 | sed 's|^\./||'
) | while IFS= read -r entryName; do
echo "put -r ${deployDir}/${entryName}"
done
echo "bye"
} > "${batchFile}"
echo "SFTP バッチ内容:"
cat "${batchFile}"
sftp -b "${batchFile}" \
-i "${keyFile}" \
-o IdentitiesOnly=yes \
-o ConnectTimeout=10 \
"${remoteUser}@${remoteHost}" remoteHost / remoteUser / remotePath だけ、自分の値に変えてください。
コミットして有効化する
cd ~/git/example-site
git add .github/workflows/sftp-deploy.yml
git commit -m "Add GitHub Actions workflow to deploy via SFTP on main."
git push -u origin main 初回の push で Action が走ります。転送対象が .github だけのときは、準備ステップが空判定して SFTP をスキップします。サイトのファイルを main に載せて push すると、実際の転送が走ります。
確認:
gh run list --repo your-github-id/example-site またはリポジトリの Actions タブです。
仕組み
-
mainへの push でジョブが起動する -
actions/checkoutがリポジトリを runner に置く - ローカルの
rsyncで.gitと.githubを除いたコピーを作る(これは runner 内の準備。サーバへの転送ではない) - Secret を環境変数から鍵ファイルへ書き出す
-
ssh-keygen -lで鍵が読めることだけ確認する(指紋は出る。中身は出さない) -
ssh-keyscanでホスト鍵をknown_hostsに入れる - 各ファイル・ディレクトリに対する
put -rのバッチを作り、sftp -bで上書きする
完全同期にする場合
前述の sftp -b と put -r は、リポジトリにあるファイルを上書きするだけです。リポジトリから消したファイルはサーバに残ります。
リポジトリの内容と公開ディレクトリを一致させるには、ローカルに無いリモートファイルを消す完全同期が必要です。
サーバが SFTP のみでもできる理由
rsync --delete はリモートでコマンド実行が必要なので、SFTP のみのサーバでは使えません。
lftp の sftp:// は GitHub Actions 側(クライアント)で動き、サーバには SFTP プロトコルだけを話します。サーバに lftp もシェルも rsync も不要です。
| 方法 | クライアント | サーバに必要なもの |
|---|---|---|
sftp -b • put -r | runner 標準の sftp | SFTP のみで可(上書きのみ) |
lftp mirror --delete | runner に lftp を入れる | SFTP のみで可(完全同期) |
rsync --delete | runner の rsync | リモートでの SSH コマンド実行 + rsync が必要 → 不可 |
完全同期で追加する準備は、その SFTP ユーザにデプロイ先での 削除権限 があることだけです。
ワークフローの変更点
鍵の渡し方や .git / .github の除外はそのまま使い、転送部分を lftp に切り替えます。
ubuntu-latest には lftp が入っていないので、ジョブ内でインストールします。
- name: Install lftp
if: steps.prepareFiles.outputs.hasFiles == 'true'
run: sudo apt-get update && sudo apt-get install -y lftp Deploy ステップの末尾を、sftp -b から次に変えます。
sftp:connect-program は引用符で囲みます。囲まないとコマンドが ssh だけになり、鍵が渡りません。
あわせて open -u ユーザ, のようにパスワードを空にします。これがないと lftp がパスワード入力待ちになり、鍵認証に進みません。
deployDir="${RUNNER_TEMP}/sftp-deploy"
echo "lftp で ${deployDir}/ を ${remotePath}/ へ完全同期します。"
lftpScript="${RUNNER_TEMP}/lftp-script"
cat > "${lftpScript}" <<EOF
set sftp:auto-confirm yes
set net:timeout 10
set sftp:connect-program "ssh -a -x -i ${keyFile} -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10"
open -u ${remoteUser}, sftp://${remoteHost}
mirror --reverse --delete --verbose ${deployDir}/ ${remotePath}/
bye
EOF
lftp -f "${lftpScript}" --reverse はローカル → リモート、--delete はローカルに無いリモートファイルの削除です。末尾の / は、ディレクトリの中身を同期するために残します。
転送対象が空のときは、今までどおり同期しません。--delete 付きの空ディレクトリ同期は、リモートを全削除してしまうためです。
注意
--delete は、サーバ側にだけあるファイルも消します。
- サーバで手置きした設定
- アプリが書き出すログやアップロード
- リポジトリに入れていない
.htaccess
残したいものがあるならリポジトリ側に含めるか、mirror の --exclude で外します。
リモートパスは公開ディレクトリ本体を指してください。親ディレクトリを指すと、関係ないファイルまで消えます。
この実装の範囲
- 転送対象が空なら SFTP は実行しない(リモート全削除を防ぐ)
- 基本形は上書きのみ。完全同期にするなら
lftp mirror --deleteを使う - パスフレーズ付き私密鍵は使わない
- リモートディレクトリは事前作成が必要
まとめ
準備は「サーバに公開鍵とディレクトリ」「GitHub にリポジトリと Secret」の2系統です。
ワークフロー側は、私密鍵を環境変数経由でファイル化し、Ubuntu runner 上で SFTP する形です。
上書きだけなら sftp -b、リポジトリとサーバを一致させるなら lftp mirror --delete にします。どちらもサーバは SFTP のみで動きます。
main にコンテンツを push すれば、あとは Actions が公開ディレクトリへ反映します。