「コードをpushしたら、自動でサーバーへ反映される」状態を作りたい。よくある要望ですが、デプロイ先がVPN経由でしかアクセスできないサーバーだと、定番の手順が途中でつまずきます。
この記事は、GitHub Actions で自動デプロイを組んだものの最後の接続だけが失敗し、原因を追ったらネットワーク(VPN)だった、という記録です。環境固有の情報は伏せ、汎用的な内容としてまとめています。
やりたかったこと
やりたかったのはシンプルで、「コードをpushしたら、SFTP でサーバーへ自動アップロードする」こと。毎回手作業でファイルを上げる運用をやめたい、という目的でした。
構成は、GitHub へ push → GitHub Actions が起動 → SFTP でサーバーへアップロード、という流れ。ワークフロー(YAML)を書き、接続情報は GitHub Secrets に登録しました。
つまずいたエラー
ワークフローの読み込み、Secrets の読み込み、デプロイ処理の開始までは問題なく進みます。ところが、最後の接続でこのエラーが出ました。
ssh: connect to host *** port 22: Operation timed outYAML を見直し、Secrets を確認し、鍵の形式も直しました。それでも、接続だけが通りません。
結論:原因は設定ではなく「ネットワーク到達性」
先に結論です。原因は YAML でも Secrets でも鍵形式でもなく、実行している場所からサーバーへ届かなかったことでした。
ポイントは、このタイムアウトが「認証より前の TCP 接続の段階」で出ていることです。つまり、鍵やパスワードを試す手前で止まっている。これは設定の問題ではない、という有力なサインです。
なぜ繋がらなかったのか
サーバーを「入館証がないと入れない部屋」だとイメージすると分かりやすいです。VPN がその入館証にあたります。
- 自分のPCは VPN(入館証)を持っている → だから手作業ならアップロードできる
- GitHub Actions はクラウド上で動く”代理ロボット”で、入館証を持っていない → ドアの前までは行くが開かず、返事を待ち続けて時間切れ
GitHub がホストするランナーは、自分の手元ではなくクラウド上で動きます。そのため、ローカルで張った VPN 接続は引き継がれません。手元の Mac や PC からは届くのに、Actions からは届かない、という状態が起きていたわけです。
解決の方向は大きく2つ
VPN 内のサーバーへ自動で届けるには、考え方として次のどちらかになります。
- 実行する側(実行係)を VPN の中に置く
- サーバー側に 取りに来てもらう(pull 型)
具体的な選択肢を整理すると、こうなります。
| 方式 | 反映タイミング | アップロードされる内容 | 備考 |
|---|---|---|---|
| セルフホストランナーを VPN 内に常駐 | push直後・ほぼ即時 | コミットした確定版 | 企業構成の正攻法。実行マシンを常時起動しておく前提 |
| GitHub Actions + VPN参加(Tailscale 等) | push直後・ほぼ即時 | コミットした確定版 | クラウドランナーのまま。サーバー側にも VPN 導入が必要 |
| pull型(端末やサーバーが Git から取得) | 数分間隔 | コミットした確定版 | 外部からの到達問題が起きない。リアルタイムではない |
| 半自動(VPN接続端末でスクリプトを手動実行) | 実行したとき | 手元の内容 | 簡単・安全だが自動ではない |
どれが最適かは、常時起動できるマシンがあるか、リアルタイム性が必要か、サーバー側を触れるか、といった条件で変わります。
今回採った方法:Git → 定期pull → SFTP(VPN内の端末で)
今回は、外部の CI を使わず、VPN 内の端末が Git リポジトリを定期的に pull し、変更があれば SFTP で反映する「pull型」を採用しました。
- メリット:push 済みの確定版だけが反映される(本番向き)。外部クラウドから VPN 内へ届かない問題が起きない。
- デメリット:反映は push 直後ではなく「数分間隔」になる(リアルタイムではない)。
- 前提:その端末が起動中・VPN 接続中のあいだに動く(先に確認した「端末からサーバーへ届く」状態をそのまま活かせる)。
デプロイの大まかな手順
- デプロイ専用フォルダに Git リポジトリを clone する(初回だけ対話的に認証を通し、以降の自動 pull で聞かれないようにする)。
- パスワード認証の SFTP を自動化できるツールを用意する(Windows なら WinSCP など。標準コマンドの
sftp/scpでも可)。初回接続でホスト鍵を信頼し、フィンガープリントを控える。 - パスワードなどの秘密情報はスクリプトに直書きせず、環境変数や秘密情報ストアで管理する。
- 「pull → 変更があればアップロード」を行うスクリプトを用意する。差分アップロードにすると、rsync のように変更分だけが送られる。
- OS のスケジューラ(Windows はタスクスケジューラ、Mac/Linux は cron)に登録し、数分間隔の定期実行にする。
スクリプトの考え方(PowerShell の例)
実際のホスト名・パスは伏せて、骨組みだけ示します。秘密情報は環境変数から読み込む形にしています。
powershell
# 設定(自分の値に置き換え)
$RepoDir = "C:\deploy\site"
$Branch = "main"
$RemotePath = "/var/www/html"
$HostName = "<サーバーのホスト名>"
$UserName = "<SFTPユーザー>"
$Password = $env:DEPLOY_SFTP_PASS # 秘密情報は環境変数から取得
$HostKey = "ssh-ed25519 256 <フィンガープリント>"
# 1) 最新を取得し、変更があったかを確認
Set-Location $RepoDir
$before = git rev-parse HEAD
git pull origin $Branch | Out-Null
$after = git rev-parse HEAD
if ($before -eq $after) { return } # 変更がなければ何もしない
# 2) 変更あり → WinSCP で差分アップロード(.git などは除外)
Add-Type -Path "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
$opts = New-Object WinSCP.SessionOptions -Property @{
Protocol = [WinSCP.Protocol]::Sftp
HostName = $HostName; UserName = $UserName; Password = $Password
SshHostKeyFingerprint = $HostKey
}
$session = New-Object WinSCP.Session
$session.Open($opts)
$t = New-Object WinSCP.TransferOptions
$t.FileMask = "|.git/;.vscode/" # アップロードから除外
# 第4引数 $False = リモートの余分なファイルは削除しない(安全側)
$session.SynchronizeDirectories(
[WinSCP.SynchronizationMode]::Remote,
$RepoDir, $RemotePath, $False, $False,
[WinSCP.SynchronizationCriteria]::Time, $t).Check()
$session.Dispose()Mac や Linux で同じことをするなら、cron + rsync を使うと、より少ない設定で同じ運用が作れます。
補足:VS Code の SFTP プラグインとの違い
「保存したら自動でアップロード」できる VS Code の SFTP プラグインもあります。手軽でリアルタイムなのが魅力ですが、性質が少し異なります。
プラグインは手元のファイルを保存と同時に送るものです。Git を経由しないため、コミット前の書きかけや、うっかり保存した変更もそのまま反映されます。一方、pull 型や CI はコミットした確定版だけを反映します。
そのため、使い分けるのが現実的です。
- テスト/ステージング:保存即アップは確認が速く便利
- 本番:コミット起点(pull 型や CI)で、確定版だけを反映する方が安全
なお、プラグインの設定ファイルにサーバー情報や鍵・パスワードを書くと、リポジトリに混ざって流出する恐れがあります。秘密情報は書かず、設定ファイルはバージョン管理から除外しておきましょう。
ハマりどころと運用のコツ
- まず到達性を確認する。
Test-NetConnection <ホスト> -Port 22(Windows)などで、その端末からサーバーへ届くかを先に確かめる。ここが通らなければ、自動化以前のネットワークの問題。 - 秘密情報は直書きしない。環境変数や Secrets で管理し、もし露出したら速やかにローテーション(再発行)する。
- 同期はまず「消さない」設定から。サーバー側に手動で置いたファイル(アップロード画像など)を誤って消さないよう、最初は「リモートの余分なファイルを削除しない」設定で始める。
- 本番は専用ブランチにする。確認できたものだけをデプロイ対象のブランチへ反映すると、レビュー前のコードが公開される事故を防げる。
まとめ
port 22: timed outが出たら、設定ミスより先に「実行している場所が VPN の外にいないか」を疑う。- 外部クラウドの CI から VPN 内のサーバーへは、直接は届かない。解決の方向は「実行係を VPN 内に置く」か「サーバー側に取りに来てもらう」のどちらか。
- リアルタイム性と「確定版だけ公開する安全性」はトレードオフ。用途に合わせて方式を選ぶ。
同じ「自動化」でも、ネットワークの前提が一つ変わるだけで最適な手段は変わります。まずは到達性の確認から始めると、遠回りが減らせます。