積み上げログ技術ブログ記事一覧へ

自主執筆記事

毎日の同期がSSHのタイムアウトで落ちていた|GitHub Actionsで3回まで掛け直す

毎日13時に走る同期処理が、4日のうち3日失敗していた。ログを見ると dial tcp のタイムアウト。コマンドが失敗したのではなく、TCP接続すら張れていない。接続の待ち時間を延ばし、間隔を空けて3回試すようにした実装を書く。

CI/CD公開: 2026/08/25更新: 2026/08/25
GitHub ActionsSSHデプロイリトライ自動化

01 / 本文

結論:コマンドの失敗ではなく、接続が張れていなかった

毎日決まった時刻に走らせている同期処理が、断続的に失敗していた。4日のうち3日が失敗、1日だけ成功という状態だった。

ログの最後はこれだけだった。

dial tcp 203.0.113.10:10022: i/o timeout

コマンドが失敗したのではない。TCPの接続すら張れていない。 サーバー側の一時的な混雑か、接続数の制限に当たっていたと考えられる。

こういう失敗は、待って掛け直せば通ることが多い。

02 / 本文

接続の待ち時間が30秒だった

使っていたのは appleboy/ssh-action で、timeout の既定値は30秒だった。これは接続を確立するまでの待ち時間で、コマンドの実行時間(command_timeout)とは別物だ。

混雑しているときに30秒で見切ると、通るはずの接続まで落とすことになる。120秒に延ばした。

03 / 本文

再試行をどう書くか

appleboy/ssh-action に再試行の機能はない。外部のアクションを足す方法もあるが、依存を増やしたくなかった。

continue-on-error と steps.<id>.outcome を組み合わせると、追加の依存なしで書ける。

1回目と2回目は continue-on-error: true にして失敗を許す。3回目だけは普通に書き、ここで落ちたときにジョブを失敗させる。

間隔は3分と5分にした。混雑が理由なら、少し待てば状況が変わる。

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: 同期(1回目)
        id: try1
        continue-on-error: true
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SSH_HOST }}
          port: 10022
          timeout: 120s          # 接続の待ち時間(既定30秒)
          command_timeout: 30m   # コマンドの実行時間
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            set -e
            cd "${{ secrets.DEPLOY_PATH }}"
            php artisan app:sync

      - name: 3分待つ
        if: steps.try1.outcome == 'failure'
        run: sleep 180

      - name: 同期(2回目)
        id: try2
        if: steps.try1.outcome == 'failure'
        continue-on-error: true
        uses: appleboy/ssh-action@v1.0.3
        with: {}                 # 1回目と同じ設定を書く

      - name: さらに5分待つ
        if: steps.try1.outcome == 'failure' && steps.try2.outcome == 'failure'
        run: sleep 300

      # 3回目で落ちたら、そこで初めてジョブを失敗にする
      - name: 同期(3回目)
        if: steps.try1.outcome == 'failure' && steps.try2.outcome == 'failure'
        uses: appleboy/ssh-action@v1.0.3
        with: {}

04 / 本文

outcome と conclusion の違い

continue-on-error: true を付けたステップでは、2つの値が食い違う。

outcome — 実際の結果。失敗していれば failure。

conclusion — continue-on-error を適用したあとの結果。失敗しても success になる。

再試行の判定に使うのは outcome だ。conclusion を見ると、1回目が失敗しても success に見えるので、2回目が走らない。

05 / 本文

冗長でも、依存を増やさない選択

同じブロックを3回書くので、見た目は冗長だ。ループで書けないのがGitHub Actionsの制約でもある。

それでも、この書き方を選んだ理由は2つある。

1. 依存が増えない。 再試行のために別のアクションを入れると、そのアクションの更新やセキュリティも見ることになる。

2. 何回目で通ったかがログに残る。 ステップが分かれているので、実行結果の一覧を見るだけで「1回目で通った」「3回目で通った」が分かる。接続の不安定さを継続的に観測できる。

06 / 本文

結果

修正後の初回実行は、こうなった。

同期(1回目)success / 同期(2回目)skipped / 同期(3回目)skipped

1回目で通った。再試行は出番なしだ。ただし、次に接続が失敗しても、1回のタイムアウトで止まることはなくなった。

毎日の処理が4日ぶりに通り、取り込むデータも増えた。

07 / 本文

まとめ

dial tcp のタイムアウトは、コマンドの失敗ではなく接続の失敗。待って掛け直せば通ることが多い。

appleboy/ssh-action の timeout は接続の待ち時間で、既定30秒。混雑するなら延ばす。

再試行は continue-on-error と outcome の組み合わせで、依存を増やさずに書ける。判定に使うのは conclusion ではなく outcome

ステップを分けて書くと、何回目で通ったかがログに残る。不安定さの観測にもなる。

コメント

この記事へのコメント

お名前だけで投稿できます。アカウント登録やログインは不要です。 いただいたコメントは確認のうえ公開します。