状況
プレイブックは正常に開始されます — ホストの到達確認が通過し、タスクが実行され始めます — そして途中のどこかで、すべてが以下のエラーとともに停止します:
fatal: [webserver-01]: FAILED! => {"msg": "SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh"}
ホストは起動しています。今この瞬間、手動でSSH接続できます。しかしAnsibleは処理を完了できません。これが厄介なところです — これは到達不可能なホストのエラーではありません。接続は実行途中で切断されたのです。
なぜこれが起こるのか
AnsibleはControlMasterを通じてSSH接続を多重化します — 複数のタスク操作で共有される1つのマスターソケットです。長時間実行されるタスクがSSHパイプラインを停止させると(アイドルタイムアウト、ファイアウォールのRSTパケット、バッファオーバーフロー)、Ansibleがまだ生きていると思っている間に、基盤となるソケットが死にます。その死んだソケットへの次の書き込みがこのエラーを引き起こします。
よくある原因:
- ファイアウォールまたはNATデバイスがアイドル状態のTCP接続を切断する — AWSセキュリティグループ、GCPファイアウォールルール、または非アクティブ状態が350〜600秒続いた後にリセットする企業プロキシで一般的
- キープアライブトラフィックなしにSSHサーバーの
ClientAliveIntervalより長く実行されるタスク - SSHパイプバッファを圧迫する大量のデータ転送
- セッション中のリモートホストのDNS解決失敗
- リモートホストの
sshdがリソース制限に達して接続を切断する - 長いプレイブック内のタスク間でAnsibleのControlMasterソケットが古くなる
クイックフィックス — 今すぐプレイブックを動かす
ControlMasterの持続を無効にし、キープアライブを追加します。プレイブックまたはインベントリに以下を追加してください:
# In your playbook
- hosts: all
vars:
ansible_ssh_extra_args: '-o ServerAliveInterval=30 -o ServerAliveCountMax=10 -o ControlMaster=no'
またはインベントリでホストごとに設定します:
[webservers]
webserver-01 ansible_ssh_extra_args="-o ServerAliveInterval=30 -o ServerAliveCountMax=10 -o ControlMaster=no"
そして最初からやり直す代わりに、失敗した箇所から再実行します:
ansible-playbook site.yml --start-at-task="The task that failed" -v
ServerAliveInterval=30は30秒ごとにキープアライブを送信します。アイドル接続を切断するほとんどのファイアウォールは少なくとも60秒待つため、これによりソケットが維持されます。ControlMaster=noはタスクごとに新しいSSH接続を強制し、前回のクラッシュから残っている古いソケットをバイパスします。
根本原因の診断
恒久的な修正を確定する前に、どの原因に該当するかを特定してください。
キープアライブ / ファイアウォールの問題かどうか確認する
詳細なSSHデバッグでプレイブックを実行します:
ANSIBLE_SSH_ARGS="-vvv" ansible-playbook site.yml 2>&1 | grep -E "(debug|channel|packet|timeout|Broken)"
出力にchannel X: open failedまたはBroken pipeが表示される場合、パイプが切断されています。Connection timed outはネットワークまたは上流のファイアウォールの問題を示しています。
SSHキープアライブの動作を手動でテストする
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=10 user@host "sleep 300 && echo done"
このコマンドはSSHセッションを5分間何もしない状態で開いたままにします — 長いタスク中にAnsibleが行うこととまったく同じです。ここでは成功するがプレイブックでは失敗する場合、以下のSSH設定修正が答えです。ここでも切断される場合は、より深いネットワーク問題を追う必要があります。
リモートホストのsshdログを確認する
sudo journalctl -u sshd --since "30 minutes ago" | grep -E "(disconnect|timeout|error)"
# or on older systems:
grep -i "disconnect\|timeout\|error" /var/log/auth.log | tail -50
サーバー側からTimeout, client not respondingが表示される場合、切断はサーバー側から開始されたことを意味します — クライアントがキープアライブを送信しておらず、sshdが諦めたのです。
恒久的な修正 — ansible.cfg
これらをグローバルに設定することで、すべてのプレイブックがSSH引数を繰り返すことなく恩恵を受けられます:
[defaults]
timeout = 30
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ServerAliveInterval=30 -o ServerAliveCountMax=10
pipelining = True
control_path_dir = /tmp/.ansible/cp
主要な設定の説明:
ServerAliveInterval=30— クライアントが30秒ごとにキープアライブを送信するServerAliveCountMax=10— 接続が切断されたと判断する前に最大10回のキープアライブ欠落を許容する(合計5分)ControlPersist=60s— マスター接続は最後の使用から60秒間生き続け、無期限ではない;古いソケットのリスクを大幅に削減pipelining = True— タスクごとにSSH操作をバッチ処理し、ラウンドトリップを減らし、タスク途中で接続が切断される機会を減らす
ファイアウォールのRSTパケットに悩まされている場合(AWS/GCP/Azure)
クラウドプロバイダーのファイアウォールは通常、350〜600秒以上アイドル状態の接続をリセットします。リモートホストのsshdも自分側からキープアライブを送信するよう設定します:
# /etc/ssh/sshd_config
ClientAliveInterval 30
ClientAliveCountMax 10
TCPKeepAlive yes
sudo systemctl reload sshd
クライアント側のキープアライブだけに頼ると、すべての責任がAnsibleにかかります。両側からキープアライブを送信すれば、どちらの側も切断された接続を検出でき、完全なタイムアウトを待つ必要がありません。
パイプラインがsudoで問題を引き起こす場合
一部のsudo設定ではTTYが必要で、パイプラインと競合します。有効にした後にsudo: no tty presentが表示される場合は、リモートホストで修正します:
# Add to /etc/sudoers via visudo:
Defaults !requiretty
古いControlMasterソケットのクリア
クラッシュしたプレイブックは死んだソケットを残します。それらの古いファイルは、ネットワークが正常に戻った後でも次の実行で同じエラーを引き起こします:
ls /tmp/.ansible/cp/
rm -f /tmp/.ansible/cp/*
またはディレクトリ全体を削除する代わりに、特定のソケットを選択的に終了させます:
ansible all -m ping # Still failing? Sockets are stale.
ssh -O stop -o ControlPath=/tmp/.ansible/cp/%r@%h:%p user@host
修正の確認
ansible.cfgの変更を適用した後、本番環境で信頼する前に意図的に遅いタスクでテストします:
ansible webservers -m command -a "sleep 60 && echo 'connection survived'"
60秒後にconnection survivedが表示されれば、キープアライブが機能しています。その後、ログを記録しながらフルプレイブックを実行します:
ansible-playbook site.yml -v 2>&1 | tee playbook-run.log
出力でSSH Errorを確認します。ヒットがなければ修正が維持されています。
ヒント
このエラーが大規模なフリート全体で断続的に発生する場合 — 一部のホストは失敗し、他は失敗しない — 問題は通常サブネットレベルです。異なるホストは異なるファイアウォールルールを持つ異なるネットワークパスを経由します。ホストごとのSSH設定を追う前に、まずネットワークトポロジーをマッピングしてください。異なるホストがどのCIDR範囲に属し、異なるファイアウォールポリシーに当たるかを素早く把握するために、ToolCraftのSubnet Calculatorを活用しています。ブラウザのみで動作し、どこにもデータは送信されません。
データベースマイグレーションやパッケージインストールなどの長時間実行タスクでは、SSH接続を常に開いたままにする代わりにasyncとpollの使用を検討してください:
- name: Run long database migration
command: python manage.py migrate
async: 600 # Allow up to 10 minutes
poll: 15 # Check every 15 seconds
Ansibleはタスクを起動して切断し、スケジュールに従って完了を確認します。SSHパイプが切断されるほど長く開いたままになることはありません — これにより、定期的に1〜2分以上かかる操作でこの問題全体を回避できます。

