Ubuntu日和
【第36回】SSH使うなら、これだけは覚えておきたい話
2023年10月7日 06:17
連載の「第12回」では、CLIを効率よく操作するツールとしてByobuを紹介した。この回の最後で「なおByobuを導入する最大のメリットに「セッション保護」機能がある。これについては回を改めて紹介する予定だ」と述べた。あれからちょうど1年。そろそろ続きを語ってもよい頃合いだろうか。
というわけで今回は、リモート作業最大の敵である「回線切断」と、これに対する防御策について解説しよう。
不意の回線切断、その時
一般的なPCとは異なり、サーバーには多くの場合、ディスプレイやキーボードが繋がっていない。またサーバーは、サーバールームやデータセンターなど、物理的に離れた(そして人間が普段は立ち入らない)場所に置かれているのが基本だ。特にVPSやクラウドなどで利用されている仮想サーバーは、そもそも物理的にアクセスすることができない。
そのためサーバー上で作業を行なう場合は、必然的に、ネットワークを経由したリモートログインを行なうことになる。この「ネットワークを経由したリモートログイン」に利用されるのが、本連載でも度々名前が登場している「SSH」だ。
SSHとはセキュアな通信を行なうためのプロトコルの名称だ。そしてサーバーやクライアントの実装としては、「OpenSSH」が広く利用されている。OpenSSHはUbuntuはもちろん、macOSやWindows[^1]にも標準でインストールされているため、最低限の使い方は覚えておくといいだろう。基本的な使い方は簡単で、「ssh」コマンドの後に、接続先のサーバーを指定すればいい[^2]。
ネットワーク越しであっても、一度ログインさえしてしまえば、基本的にはローカルから直接ログインしたのとまったく同じように、コマンドを実行することができる。そのためサーバーの操作において、ログイン経路の違いを意識する必要は本来ない……はずなのだが、SSHによるリモートログイン特有の問題というのが1つある。それは、ネットワーク回線は、経路上のトラブルをはじめ、さまざまな理由で切断されてしまう可能性が常にあるという点だ。
さて、SSH接続中に回線が切断されると、サーバー上では何が起こるのだろうか?
SSH接続が切断されると、カーネルはシェル対して「SIGHUP」という「シグナル」を送信する。シグナルとは、プロセスにイベントの発生を伝える仕組みだ。その中でもSIGHUPは、ハングアップを意味する。SIGHUPは主に制御端末が閉じられた時に送信され、このシグナルを受信したプロセスは、終了するのがデフォルトの動作となっている。そしてSIGHUPシグナルを受信したシェルも、そのシェル上で動作している各プロセスに対して、さらにSIGHUPシグナルを送信する。
まあ小難しい理屈はさておき、「SSH接続が切れると、そこで実行中のプロセスは、基本的に終了されてしまう」とだけ覚えておこう。
考えてみてほしい。もしも「apt upgrade」のように、OSに変更が発生するコマンドの実行中に回線が切断されてしまったら、どうなるだろう。OS全体に対して、回復不能なダメージを与えてしまう可能性も否定できない。SSHでの再接続ができなくなってしまい、仕方なく電車で数時間かけてデータセンターまで行ってサーバーを再起動したものの、そもそもOSが起動しなくなっていて泣いた、なんていうこともありうる[^3]。万が一、本番データベースのマイグレーション作業中に回線が切れてしまったら、間違いなくハラキリ・インシデントである。
最近では出先から、スマホのテザリングを使ったモバイル回線経由で作業を行なうことも多いだろう。だがモバイル回線は非常に不安定なため、いつ切れてしまってもおかしくない。また端末やSSHクライアントアプリのウィンドウをうっかり閉じてしまったり、クライアントを動かしているPCがフリーズしてしまったような場合も、回線切断と同じ扱いになる。
このように、回線切断の危険はあちこちに潜んでいる。リモートからサーバーを操作する際は、常に回線切断について意識し、切断されるとマズイ作業を行なうのであれば、それに対する策を講じなければならないということなのだ。
Byobuのセッション保護とは
SSHの接続が切れると、プロセスが終了されてしまうと述べた。だが、これにはいくつかの回避策がある。プロセスが終了してしまうのは、シェルからSIGHUPシグナルを受信するからだ。なので端的に、このシグナルを無視してしまえばいい。具体的には「nohup」を指定してプロセスを起動すれば、SIGHUPを無視できる。
下はnohupをつけてコマンドを実行する例だ。これはUbuntu 22.04 LTS日本語RemixのISOイメージをダウンロードするコマンドだが、SIGHUPシグナルを無視するようになるため、ダウンロード中にSSH接続が切れたとしても動き続ける。
$ nohup wget http://cdimage-u-toyama.ubuntulinux.jp/releases/jammy/ubuntu-ja-22.04-desktop-amd64.iso
シェルは、現在自身の上で実行しているプロセス(正確には複数プロセスをまとめたジョブという単位)を管理する「ジョブテーブル」を持っている。SIGHUPシグナルは、ジョブテーブルに登録されているプロセスに対して送信されるため、終了されたくないプロセスは、ジョブテーブルから除外し、SIGHUPシグナルの送信対象にしないという方法もある。これには「disown」コマンドを使う。ただしdisownコマンドは、フォアグラウンドジョブに対しては指定できないため、プロセスをバックグラウンドで起動する必要がある。
$ wget http://cdimage-u-toyama.ubuntulinux.jp/releases/jammy/ubuntu-ja-22.04-desktop-amd64.iso &
$ disown
上の例では、コマンドの後に「&」をつけて、wgetコマンドをバックグラウンドで起動する。その後にdisownを実行し、ジョブテーブルから除外する。こうすることでSSH接続が切れた後も、バックグラウンドでダウンロードが継続される。
だがこれらの方法には、少し問題もある。確かにプロセスは終了されず、動作し続けるものの、プロセスが端末から切り離された状態となってしまう。プロセスはユーザーの目に見えない所で動き続けるため、キーボードから何か操作をすることはできないし、結果を端末上で見ることもできなくなってしまうのだ。
上記の例のように、ダウンロード終了まで動き続ければそれでいいというコマンドであれば問題はないだろう。だが対話的に操作したかったり、結果を逐次確認したいコマンドは、バックグラウンドに押し込まれてしまうのは困る。「gdb」というデバッガを使えば、起動済みプロセスの標準入出力を別の端末に繋ぎ直すこともできるのだが、はっきり言って面倒くさすぎるし、専門的な知識も必要だ。
そこでもっとシンプルに、回線切断前に操作していたシェルそのものを保護できないだろうか。万が一回線が切断されてしまっても、保護されているシェルにもう一度接続し直せれば理想的だ。
少し前置きが長くなったが、Byobuにはこの機能が標準で用意されている。実は、Byobuはそれ自体がデーモンとして動作し、その上で別のシェルを起動している[^4]。SSH接続が切れると、当然ログインシェルは終了されてしまう。だがログインシェルから起動されたByobuはデーモンとして動き続けるため、Byobu上で起動された(ログインシェルとは別の)シェルを、回線切断から保護できるのだ。そしてサーバーに再接続したら、byobuコマンドをもう一度実行しよう。byobuコマンドは、Byobuが既に起動中だった場合、自動的に起動中のセッションにアタッチしてくれる。
このByobuの仕組みには、不意の回線切断に備える以上の意味がある。任意にデタッチとアタッチを行なうことで、クライアントのローミングが可能になるのだ。
たとえば自宅からサーバーに接続して作業している最中に、出かける時間になってしまったとしよう。しかも外出先には、今作業を行なっているノートPCを持って行かなければならない。通常であれば、諦めてコマンドを強制終了するか、遅刻覚悟でコマンドが終了するまで待つしかないだろう。だがByobuを使っていれば、サクっとデタッチして回線を切断してしまって構わない。そして外出先から改めてモバイル回線でサーバーにログインし、Byobuに再アタッチして作業を継続する、といった芸当が可能になるわけだ。
デタッチとアタッチを試してみよう
それでは実際にByobuを使って、デタッチとアタッチを体験してみよう。なおByobuのインストールや設定は、「第12回」を参照して欲しい。
Byobuを起動したら、その上で任意のコマンドを実行しよう。デタッチとアタッチを試す都合上、例としては意図的に終了するまで動き続けるコマンドが望ましい。そこで今回は、テキストエディタの「nano」を使うことにした。
nanoを起動してテキストを入力したら、回線切断を再現するため、端末のウィンドウをいきなり閉じてみよう。プロセスが実行中であるという警告が出るが、無視して閉じてしまっていい。これでサーバーとの接続は切れ、そして通常であれば、サーバー上で起動していたnanoエディタも終了されてしまうはずだ。
あらためて新しく端末を起動して、サーバーにもう一度SSHで繋ぎ直そう。そしてbyobuコマンドを実行する。すると……。
見ての通り、切断前の作業状態がそのまま保存されている。このように、Byobuを使えば回線(と端末)とシェルとを切り離し、作業内容を保護できるというわけだ。サーバーにログインしたら、とりあえずByobuを起動しておけば、もう回線切断に怯える日々とはサヨナラだ。時間のかかるコマンドや、失敗の許されない作業でも、安心して行なえるだろう。……いや、絶対に安心とは言い切れないのだが、ノーガード戦法よりは遥かに安全なのは間違いない。実際、Ubuntuサーバーのバージョンアップコマンドである「do-release-upgrade」は、内部的にByobuと同様の仕組みを利用しており、不意の回線切断からアップグレードプロセスを保護している。
ちなみに今回は不意の回線切断を想定して、端末をいきなり閉じてしまったが、これは少々お行儀が悪い。自分の意思でサーバーから切断したい場合は、明示的にデタッチ操作を行おう。Byobuの場合、デフォルトでは「F6」、もしくは「Prefix+D」でデタッチができる。
ログイン時にByobuを自動起動するには
結論としては、サーバーにログインしたらまずByobuを起動しよう、ということになるのだが、そうした決まりきった作業は自動化するのが正義だ。そしてByobuには、ログイン時に自動的に起動するという設定も用意されている。
まず「byobu-config」コマンドを実行して、Byobuの対話的設定メニューを呼び出そう。4行目に「Byobuは現在ログイン時に起動します (無効にする)」と書かれたメニューがあるので、カーソルキーでこの行を選択してEnterキーを押す。するとこの行が「Byobuは現在ログイン時に起動しません (有効にする)」に変化する。そこでもう一度、この行を選択してEnterキーを押そう。これで次回ログイン時からは、自動でByobuが起動するようになる。
モバイルシェル「Mosh」を使う
サーバーへのリモート接続にはSSHを使うのが一般的だが、SSHの代替となる別のソフトウェアも存在する。その1つがモバイルシェルこと「Mosh」だ。Moshはその名の通り、低速で不安定なモバイル環境でも、快適に利用できることを考慮して設計されている。
Moshの大きな特徴としては、高速な動作と、プロトコルレベルでのローミング機能がある。またMoshサーバーは、Byobu(の内部のtmux)と同じように、その上で起動したシェルを保護している。そのためByobuなどのソフトウェアを別途起動しなくても、回線切断に対するケアは万全だ。
Moshのローミングは、SSPというプロトコルによって実現されている。SSPはUDP上に実装されたプロトコルで、クライアントは3秒ごとに、シーケンス番号を含んだハートビートパケットをサーバーに送信している。サーバーは、より新しいシーケンス番号を含むパケットを受け取ると、そのパケットの送信元IPアドレスを、新しい通信相手として設定する。これにより、通信中にクライアントのIPアドレスが変化したとしても、シームレスに通信を継続できるようになっているのだ。これはSSHのように、TCPでセッションを張るタイプのプロトコルでは実現できない特徴だと言えるだろう。
SSPの採用によりMoshでは、通信中にいきなりノートPCの蓋を閉じてサスペンドしたとしても、別の場所でPCの蓋を開けば、何事もなかったかのように通信が再開する。それがたとえ、異なるWi-Fiに繋がり、IPアドレスが変化していてもだ。アタッチやデタッチといった操作はもちろん、SSHのように「再接続する」という操作すら必要ない。
SSHでは、連続したバイトストリームを順序よくネットワークに流す必要があった。そのため大きなデータを流してしまうと、反応が悪くなることがよくある。低速なモバイル回線上で巨大なテキストファイルをcatしてしまい、Ctrl+Cを連打しても効かず「あわわ」となるのは、SSHの利用者であれば誰もが一度くらいは経験しているだろう。
だがSSPでは、シーケンシャルにバイトストリームを流すのではなく、サーバーとクライアントで現在の状態を「同期」するという動作をする。すべてのデータを愚直に流す必要がないため、回線の速度に合わせて、適宜中間フレームをスキップすることができる。これにより、レイテンシの大きいモバイル回線であっても、良好な応答性を確保できるのだ。
こうしたメリットがあるため、もしもサーバーへのインストールが許されるのであれば、SSHのかわりにMoshを試してみるのもお勧めだ。
ただしMoshは、認証と起動にSSHを利用しているため、SSHサーバーを完全に置き換えられるわけではない点に注意してほしい。またSSHとは別のポート[^5]を解放する必要があるため、ファイアウォールの設定に制限があるような環境では、もしかしたら利用できないかもしれない。
Ubuntuでは、Moshは「mosh」パッケージでインストールできる。サーバー側とクライアント側の両方に、以下のコマンドでインストールしよう。
$ sudo apt install -y mosh
接続の仕方はSSHと同じで、「mosh」コマンドの引数に接続先のサーバーを指定する。
$ mosh 192.168.1.100
前回紹介したRaspberry Pi Zero 2 Wなどは、ネットワークがWi-Fi接続な都合もあり、サーバーとして運用していると、SSH接続が一瞬切れてしまうことがそこそこの頻度である。転ばぬ先の杖としてはもちろん、リモートワーク時代ならではの柔軟な作業環境を実現するためにも、ByobuやMoshといったソフトウェアを、是非活用してみてほしい。
[^1]: Windows 11であれば、PowerShellから「ssh.exe」を実行すればいい。もはやサードパーティのSSHクライアントをインストールする必要もないので、Windowsも本当に楽になったと思う。
[^2]: 当然だが、サーバー上でSSHサーバーが動作している必要はある。
[^3]: ほんとうにあった怖い話。
[^4]: 本記事ではByobuが~と言っているが、実際にデーモンとして動作したり、デタッチ/アタッチの機能を提供しているのはByobuそのものではなく、Byobuのバックエンドになっているtmuxだ。
[^5]:デフォルトでUDPの60001番~60999番ポートを使う。変更可能。