委譲か継承か、それが問題だ

この記事は 2013/4/1 に旧ブログの “Syn の独り言” で記載したものを
加筆、修正した者です。

なんか、「動的型付け言語では継承はダメ、委譲を使え」みたいな意見を聞いた。
いや、Rails だって、model は ActiveRedord 継承して作るじゃん。
お前、David さんディスってんのかよ

まあ、冗談は置いといて、
その人の周りに継承を使いすぎる人がいたのかもしれない。
僕が彼の意見を曲解しているのかもしれない。
でも、ちょっと気になったので今の僕の考えをまとめてみた。

まず、話のスコープをはっきりさせよう。
「オブジェクト指向」というと、少し範囲が広すぎる。
今回は、Java, Python, Ruby 等を前提に考えてみる。

僕の意見はこうだ。

まず、オブジェクト指向とかプログラムとか置いといて物作り一般に
広く言える事だが、

大きい物を作りたければ、疎結合で抽象化されている小さいパーツに分けろ。

疎結合とはパーツ外部からパーツ内部に対して与える影響が少ない状態の事。
逆に、影響が多い状態は密結合。

抽象化の定義を書くと色々と長くなるが、簡単に言うと
「抽象化されている」とは「直感的に使える」くらいに考えて欲しい。

人間、一度に多くの事を考える事は苦手だ。
また、頭のなかに有る作業用メモリが溢れると途端に難しくなる。
大きい = 複雑 = 難しい
くらいに考えて良いだろう。

そこで、疎結合で抽象化された小さいパーツに分けるのだ。
疎結合で小さければ、各パーツを作っている時にそのパーツだけに専念出来る。

IT のシステムに置いてパーツを疎結合にするには、パーツが外部とやり取りをする
インターフェースで外部から内部に出来る事を制限すれば良い。
(ここで言うインターフェースとは、Java の interface ではなく
外部との接続口という意味。)
インターフェースによる制限が厳しいほどパーツは疎結合になり、
制限が緩いほどパーツは密結合になる。
また、このインターフェースが直感的であれば、そのパーツは抽象化されていると
言えるだろう。

パーツの内部のやり取りの制限は緩くてよい。
というか、内部のやり取りを厳しく制限出来るならば、
さらに小さいパーツに分ける方が良いだろう。
(もちろん、パーツが充分小さければ分けないという選択肢もあり得るが。)

具体例を上げよう。
例えば、ある処理には速度向上のためにバッファリングが必要な場合。

その処理自体を行う為のパーツを用意したとして、バッファリングはどこで行うか?
今回は、バッファリングをあるその処理を行うパーツ自体に持たせる事を考えよう。
この為にはバッファをそのパーツ内部に持ち、そのバッファを扱う
インターフェースを用意しなければ良い。

上記の制限を行うと、どうなるか?
まず、パーツ内部を実装する際に「外部からバッファを操作される」という可能性を
考えなくて良くなる。
また、外部からこのパーツを使用する際もバッファリングという難しい処理を忘れて
直感的に使えるようになる。
内部の実装時も、外部の実装時も同時に考える項目数を減らす事ができるのだ。

後でバッファリングに関する部分を改修する時にも、この事は役に立つだろう。
改修時にはパーツ内部だけ考えれば良い。
なぜなら外部ではバッファリングの事を考えなくても良いようにしたから。

もちろん、制限を加えた事によって悪い事も発生する。
今回の場合、外部からはバッファリングの細かいチューニングが出来なくなった。
この事による速度の低下が致命的な場合は、この制限を加えたのは
間違いだったのだろう。
前の方に書いたが、「可能な範囲で」疎結合にするのだ。

さて、ずいぶん前置きが長くなってしまったがここからクラスの話に戻る。

クラスとは、プログラムにおけるパーツの一種。

「一種」と書いたのは、パーツの分け方が一通りでは無いから。
(ファイルを分けたり、関数で分けたり。)
多くの場合、public method がクラスの内部と外部のやり取りをする為の
インターフェースとなる。

では、クラスの委譲、継承とは何か?
どちらも、小さいクラスから大きいクラスを作る方法である。
両者の違いは小さいクラスに対する大きいクラスのアクセスの方法にある。

委譲は小さいクラスへ、そのインターフェースを通してアクセスする。
継承は小さいクラスの内部へ直接アクセスする。

委譲のメリットは、何と言っても小さいクラスと大きいクラスの疎結合性を
保てる事だ。
小さいクラスで外部と疎結合にするインターフェースを実装できるならば
小さいクラスと大きいクラスが疎結合になる。

逆にデメリットは、実装する必要のあるインターフェースが増える事だろう。
委譲の場合、大きいクラスでは、外部に公開するインターフェースを
全て実装しなくてはいけない。
たとえ、ほとんど同じインターフェースが小さいクラスにあったとしてもだ。
また、小さいクラスでは大きいクラスが必要とするインターフェースを
全て実装しなくてはいけない。

では、継承のメリットは?
それは、実装時のステップ数が減る事だ。
小さいクラスではインターフェースを実装する必要は無い。
インターフェースを含めて小さいクラスで実装済みの物は、ほとんど全て
大きいクラスで流用できる。
一つの子クラスを複数のクラスで流用すれば、全体のステップ数が減る。

継承のデメリットは、親クラスと子クラスが密結合になる事だ。
親クラスがどんなに優れたインターフェースを持っていたとしても、
子クラスはそのインターフェースを使わないので密結合になる。

では委譲と継承はどうやって使い分けたら良いだろう?

継承は「異なるクラスの共通部分を外出しして DRY にする」為に使う。

Interface

上手において、class A と class B は青色の部分が共通している。
それだったら、青色部分を外出しして共通の親クラスとすれば DRY になる。

外出ししたクラス(親クラス)は、パーツとして必要な機能を全て
実装しているわけではない。
(Java の抽象クラスなんかが、その典型。)
そういう意味で、「小さいクラス」だ。
継承は、小さいパーツに不足している機能を付け足し、肥大化させる事で
大きい(必要な機能を全て揃えた)パーツを作る。

委譲は「クラスをより小さい、疎結合なクラスに分割して実装する為」に使う。

委譲

上手のように、class A の中で青色部分と緑色部分に分かれており、
互いのやり取りの方法が限定されていたら?
そうやって疎結合な部分に分かれているのならば、
より小さいクラスに分けて実装する事で同時に考えなくては行けない事が
もっと減るかもしれない。
(青を実装中は緑を考えなくてよい。)

小さいクラスは、インターフェースを含めて小さいパーツに必要な物を
全て備えている。
委譲は、その小さいパーツを組み立てて一回り大きいパーツを作る。
委譲によって後で組み立てられる事が保証されているから、安心してクラスを細かく
分割できるのだ。

継承の本質は、重複を避けて DRY にする事だ。
複数クラスで重複した部分が無ければ、継承をする意味が無い。

委譲の本質は、大きいパーツをより小さい疎結合なパーツに分けて実装する事だ。
小さいクラスを複数のクラスで流用出来なかったとしても、委譲する意味はある。
(もちろん、複数クラスで小さいクラスを流用できるならば、
するに越した事は無い。)

GoF を初め、多くの人が「継承よりも委譲を」と言っているのは、
下記のような場合だと考えている。
委譲と継承

クラス A は、青い部分と緑の部分に疎結合に分ける事が出来る。
クラス B は、青い部分と赤い部分に疎結合に分ける事が出来る。

この場合、青のクラスを共通の親クラスとしてクラス A と B で継承で
実装する事もできる。
青のクラスを作ってクラス A と B で委譲で実装する事も出来る。
こういう場合は、委譲にするべきだろう。

継承か委譲か迷ったら、試しに青のクラスを作ってみると良い。
(これは、継承でも委譲でも、どのみち必要になる)
青のクラスが疎結合になる制限の強いインターフェースを含め、
パーツとして必要な機能を全て備えたクラスになった場合。
継承という方法で、その疎結合性を壊す必要は無い。委譲を使えば良い。
そうならなければ、継承を使う。

ついでに、緑と赤もパーツとして必要な機能を備えた、疎結合なクラスを
作れるのならば作ると良いだろう。
たとえ青の部分を継承で実装する事になったとしても。
たとえ緑と赤のクラスを他の部分で流用出来なかったとしても。

また、よく「is-A ならば継承を、has-A や use-A ならば委譲を使え」と言う。
これは、
「has-A や use-A の場合、A がロジック的に分かれているわけだから、
実装時も疎結合なクラス A として分けられる事が多い。
is-A ならば A がロジックとして分かれていないから、
実装時も疎結合なクラスに分けられない事が多い」
という事ではないか?
僕の経験としても、そうなっている事が多い。

さて、一番最初に話を戻して、僕が違和感を感じた
「動的型付け言語で継承はダメ、委譲を使え」という意見について。
彼は

Java のような静的型付けの言語では、親の型を引き継ぐ為に継承を使う事もあるだろう。
でも、Python のような動的型付け言語では型を引き継ぐ必要は無い。
ステップ数を減らしたいのはプログラマの怠慢であり、意味は無い。
疎結合にするため、委譲を使うべき。

という結論に至っていた。

それは違うだろう。
だいたい、Java だって Interface を使えば親のクラスの型を引き継ぐ必要はない。
ステップ数を減らすための怠慢は、優れたプログラマに必要な物だ。

彼は、
「委譲を使えば疎結合に、継承を使えば密結合になる」と考えているのではないか?

僕は、
「疎結合な小さいクラスを作ったという前提の元で、委譲を使うと
その疎結合性を保つ事ができる。
継承を使うと疎結合性を保つ事が出来ない」
つまり、
「もともと疎結合に分ける事ができなければ、
委譲にしても継承にしても疎結合にならない。」

「どうせ密結合になるなら、継承の方がステップ数が短くなるから
良いのではないか?」

と考えている。

もっとも、「どうせ密結合になるなら継承の方が良い」というのは、実は少し眉唾だ。
ただ、今回の話のスコープである、Java, Ruby, Python の場合は十分に正しいだろう。

最後に、僕が大きいシステムを作る際の作業フローを紹介してみる。

まず、僕は大きいシステムを一度に実装できるほど優れた人間ではない。
最初に、僕の手に負える程度のサイズまでシステムを疎結合なパーツに分ける。

何もクラスという形で分けるとは限らない。
小さいパーツは、より小さいシステムかもしれないし
「人間の目視確認」というワークフローかもしれない。
(人間による作業を増やすのは良い事じゃないけれど。)

また、パーツを分けたら今度は既存の物をそのパーツとして使用できないか考える。
既存のシステムとか、有名なミドルウェアとか、誰かが書いたライブラリとか、
プログラム言語のビルトインのシステムとか。
(というか、パーツ分けの段階で既にこの辺りは意識している。)

流用出来ない部分は、自分で実装するしかない。
自分の手に負える範囲まで小さく分けてから実装の開始だ。

各パーツを実装する際は、できるだけ DRY に、できるだけシンプルに行いたい。
多くの関数内で共通化できる部分があれば、その部分を外だしして別の関数を
作るだろう。
多くのクラスで共通化できる部分があれば、親クラスを1個作って継承させるだろう。

細かいパーツができたら、今度はそれを組み立てる。
ミドルウェアと組み合わせる場合はドライバを使うかもしれない。
システム同士を組み合わせる場合は API を使うかもしれない。

クラスとクラス、クラスと関数を組み合わせる場合は?
その時は委譲を使うだろう。
個々のクラスは外部と疎結合になっているはずなので、それを保ったまま
組み立てるのだ。

無事に組み上がってテストが通ったら、システムは完成する。

もっとも、上記のウォーターフォール的な流れを 1回行うだけで全て上手く行く事は
少ない。
先に大きいクラスを作ってから小さいクラスを作る場合も有る。
作りながらリファクタリンングを繰り返して、パーツを組み立ててからパーツを
再作成するかもしれない。
でも、何となく分かってもらえ無いだろうか?

ちなみに、多重継承はどんな時に使うだろう?
使うとしたら、上記の作業フローで言うと、単体継承と同じフェーズだ。
例えば、下図のようなクラスを作る場合。
多重継承

各クラスの同じ色の部分は同じ内容。
しかし、赤、青、緑、黄のそれぞれは密結合。
この場合、赤、青、黄、緑のそれぞれの部分をクラスとして実装してそれを
多重継承する方法もある。

ただ、自分でこれを実装するのは、非常にしんどい。
例えば、多重継承を使わない場合、クラス A の青い部分を実装している時は
青と緑の部分だけ考えれば良い。
でも、多重継承を前提に青いクラスを実装する場合は青、緑、赤を全て一度に考える
必要がある。

これを読む方は実装する人以上にしんどいだろう。
丁寧なドキュメントを用意する必要がある。

その他、多重継承のメソッド探索順序は非常に複雑になる場合も多い。
それについてはPython 菱形の継承にも書いたので良ければ見て欲しい。

多重継承を使用するのは、上記の労力を払ってでも実装する価値がある場合のみだ。
その多くは、汎用性の高いライブラリやフレームワーク、ビルトインや
標準ライブラリのクラスだろう。

代表的な例は Ruby だと思う。
正確に言うと Ruby は単体継承しかサポートしていないが、module を複数
include 出来る。
これは、実質的にはメソッド探索順序を単純化した多重継承みたいなもんだ。

Ruby の場合、多重継承もどき(include)を前提とした親クラスもどき(module)が
標準ライブラリにあふれているので、ユーザーも多重継承もどき(include)を
行う事も多い。
でも、普通は多重継承を使う事は滅多に無いんじゃないかな?

できる ssh

この記事は 2013 年 3 月 27 日に「Syn の独り言」で書いた記事を移行したものです。

ssh って便利だ。でも、「リモートログインするためのコマンド」程度の認識しかない人も多いのでは?自分は、この小さい働き者が大好きだ。なので、今回はその紹介

長いので先にアジェンダを書くと、こんな感じ

  • rsa 鍵を使って認証する
  • ssh-agent で秘密鍵を登録しておく
  • 認証情報を踏み台ごしに使用する
  • リモート上のコマンドを直接実行する
  • リモートの GUI を使う
  • config ファイルにデフォルト設定を記載しておく
  • ファイルを効率よく転送する
  • ディレクトリごと効率よく転送する
  • Port Forward で簡易 トンネリング・VPN を貼る

rsa 鍵を使って認証する

鍵認証とは、秘密鍵と公開鍵を割り符のように使う認証だ。ssh サーバーは公開鍵を持っており、それに対応する秘密鍵を持っているユーザーの認証を通す。

百聞は一見にしかずと言う事で、とりあえず rsa 鍵を作ってみて欲しい。ssh クライアント (ssh コマンドを実行する側)で
$ ssh-keygen -t rsa
と実行すると、公開鍵と秘密鍵が作成される。作成中に鍵の保存場所とパスワードが聞かれるが、ここは適当に決めて欲しい。

ただ、(ssh に限らず)鍵を使うアプリケーションのほとんどはデフォルトでこのパスを見に行くので特に理由がなければ変更しない事をおすすめする。

パスワードは認証に使うものではなく秘密鍵を暗号化するための物だ。秘密鍵を盗まれた時の時間稼ぎにしかなっていない事に注意しよう。

また、ssh 鍵を作成する際の -t rsa というオプションは rsa2 アルゴリズムを使用すると言う事だ。歴史的経緯により rsa1, dsa なども併記してあるサイトがあるが、2013 年 3月現在では rsa2 の方がデファクトスタンダードだ。悪い事は言わない、rsa2 を使っとけ。

さて、デフォルトのパスで作成すると、~/.ssh/id_rsa と ~/.ssh/id_rsa.pub というファイルが出来る。~/.ssh/id_rsa が秘密鍵で ~/.ssh/id_rsa.pub が公開鍵だ。

この ~/.ssh/id_rsa.pub を ssh サーバー (接続される方) の ~/.ssh/authorized_keys に記載すると、この鍵を用いて ssh の認証ができるようになる。

クライアントの id_rsa.pub をなんとかしてサーバーに転送したら、
$ mkdir -p ~/.ssh
$ chmod 700 ~/.ssh
$ cat id_rsa.pub >> ~/.ssh/authorized_keys
$ chmod 600 ~/.ssh/authorized_keys

とすればよい。cat の部分は id_rsa.pub の中身を authorized_keys へ追記しているだけなので、エディタで authorized_keys を開いてコピペでも OK だ。

.ssh や authorized_keys のパーミッションは、Linux では本来どうでも良いはず(と、記憶している)が、Redhat 等の一部の OS ではこうしないとセキュリティーチェックにより動作しないので念のため。

または、最初からパスワード認証を用いて ssh クライアントからログイン出来る場合は、ssh クライアントで
$ ssh-copy-id [user@]hostname
を実行しても良い。

これ以降、ログインする際は必ず鍵認証を使うのであれば、認証用のパスワードは不要になる。サーバーで
$ sudo passwd -d user
とするのも良いだろう。こうするとユーザーのパスワードは空になり、秘密鍵が無いとログイン出来なくなる。通常の ssh サーバーは空パスワードのログインを禁止しているためだ。
(/etc/ssh/sshd_config に “PermitEmptyPasswords yes” と書かれていなければ大丈夫)

「パスワードが存在しない」というのは、パスワードクラックに対する最強の防御策だ。ただし、コンソールからは空パスワードでログインできるので注意しよう。

複数のユーザーやクライアントからサーバーにログインしたい場合は、それぞれのクライアントで鍵を生成して両方の id_rsa.pub をサーバーの authorized_keys に登録すれば良い。手順を見てもらえるとわかるが、id_rsa.pub は authorized_keys へ追記する形となっているので同一の authorized_keys へ複数の id_rsa.pub を登録できる。

また、同じ鍵で複数のサーバーやサイトにログインできるようにする事も問題ない。同じ id_rsa.pub を複数サーバーの authorized_keys や GitHub などに登録できる。

普通は、作業マシンでは秘密鍵(と、ペアの公開鍵)が 1個、それ以外のサーバーでは authorized_keys に公開鍵が複数登録されている状態になると思う。
(authorized_keys に 1個しか登録されていないと、その作業マシンが壊れた際に少し危険)

ただし、決して1個の秘密鍵を2人のユーザーや2台のマシンで共有してはいけない。
(公開鍵はやってもいいよ。)
転送した際にどんなミスがあるか分からない。盗聴されるかもしれない。

欲を言えば、移動やコピー、標準出力への出力も可能な限り控えたい所だ。cat で内容を出力するだけでもだめだ。どこかに画面出力のログが残っているかもしれないし、画面を後ろから見ている人がいるかもしれない。移動やコピーについても、アクセス権を間違えて誰かに見られる状態になるかもしれない。また、別パーティションにコピーされて別ハードディスクに保存されればハードディスク盗難のリスクが増える。

「大げさな」と思うかもしれないが、鍵を盗まれるのはそれだけ重大なことだ。バグの無いプログラムが無いように、ミスの無い人間もいない。こういう些細な気遣いが、重大な事件を起こすかどうかの境目だと思う。

ちなみに、aws (Amazon Web Service) ではサーバーログインのための秘密鍵をダウンロードするが、上記の理由から一時的な物と考えた方が良いだろう。最初のログイン時に初期パスワードを変更するサービスのように、初回ログイン時に鍵を変更したい。

ssh-agent で秘密鍵を登録しておく

ssh する度に秘密鍵のパスワードを入力するのは面倒だ。そんな時は、ssh-agent を用いれば良い。

client 上で
$ ssh-agent
$ ssh-add [path_to_secret_key]

とすると、秘密鍵のパスワードを聞かれるので入力しよう。すると、次からパスワードを入力する手間が省ける。

詳細にいうと、ssh-agent は /tmp にディレクトリを作成し、そこに UDS (Unix Domain Socket) を作成する。daemon としてバックグラウンドで動作する ssh-agent はそのソケットを通じて ssh が鍵の情報をやり取りするのだ。

UDS のパスや ssh-agent の PID は shell 変数に保存される。なので、同じ shell 変数が使用される限り異なる端末でも使用可能だ。screen や tmux などの仮想端末を使用する際は、仮想端末を立ち上げる前にこの作業を行うと全ての window で ssh-agent を使用できる。

なお、この作業は後述の agent forward にも通じている。秘密鍵にパスワードを設定していなかったとしても、agent forward を使用する場合は必要だ。

認証情報を踏み台ごしに使用する

例えば以下のように、目的のサーバーまで ssh でアクセスする際に間に踏み台(Step)を挟む場合。
(ネットワークの都合などで直接 ssh 出来ない事は良くある。)

踏み台で鍵を作っても良いが、管理が面倒になる。秘密鍵はローカルサーバーだけ、踏み台と目的のサーバーの authorized_keys にはローカルサーバーの公開鍵を登録する方法。

手順はこんな感じになる。

  1. ローカルサーバーの公開鍵を踏み台、サーバー両方の authorized_keys に登録する
  2. ローカルサーバーで ssh-agent と ssh-add を実行する
  3. ローカルサーバーから踏み台へ ssh ログインする際に -A オプションを付与する
    $ ssh -A step_server

こんな感じに

  • 踏み台サーバーから目的のサーバーへログインする
    $ ssh server_name

(踏み台で ssh-agent や ssh-add を実行してはダメ)すると、2回目のログインでもローカルの鍵情報が使える。

これは1回目の ssh が踏み台でも UDS を作成し、ローカルの認証情報を踏み台からでも使えるようにするからだ。鍵そのものを転送するわけではないのでセキュリティー的にはそこまで心配する必要は無い。

ssh の度に -A オプションをつければ Agent Forward は下記のような多段構成でも出来る。そのため、Agent Forward を使用すれば秘密鍵はローカルマシンでのみ作成すればよくその他のサーバーにはローカルで作成した共有鍵を登録すれば良い事になる。

ただし、セキュリティーが完全に担保されるわけではない事は覚えておいた方が良い。最も大きいリスクは、踏み台の UDS を悪用される事だろう。この UDS は ssh でログインしたユーザーしか使用できないようにパーミッションが設定されているが root ユーザーからはアクセス権を無視して使用出来る。

root を信頼できないサーバーを踏み台にする際は控えた方が良いだろう。

リモート上のコマンドを直接実行する

話は少し変わって、ssh だけでコマンドまで実行する方法。
ssh [user@]hostname command [arg1 [arg2 [...]]]と実行すると、hostname 上でコマンドを実行してくれる。

例えば、自分の環境でやってみるとこんな感じ
$ ssh sagittarius.chaos.com ls -a /usr
.
..
bin
games
include
lib
local
sbin
share
src

念のため言っておくと、sagittarius.chaos.com というのは自分で管理している VM だ。

リモート上でコマンドを実行する時は、通常のログイン時と同じ環境変数(ホームディレクトリなど)が設定されている。そのため、
$ ssh wbcchsyn@sagittarius.chaos.com ls
と実行すると、wbcchsyn のホームディレクトリのファイル一覧を表示する。

ちなみに、ssh に -t オプションをつけると、コマンド実行前にリモートサーバーで仮想端末 (tty) を作成してくれる。何が嬉しいかと言うと、パスワード入力や yes,no の確認等を促す、tty にのみ表示される文字列も ssh ごしに表示される。対話的なコマンドを実行する際に便利だ。
$ ssh sagittarius.chaos.com touch /tmp/hoge
$ ssh sagittarius.chaos.com rm -i /tmp/hoge
rm: remove regular empty file `/tmp/hoge'?

もちろん、ここで y と入力すると削除されるし、n と入力すると削除はキャンセルされる。

こういう tty にのみ表示される文字列については、以前ここにかいたので、良かったら見て下さい。

リモートの GUI を使う

Linux の GUI は、Client-Server システムになっている。例えば firefox の場合、firefox が X-Client となり「このような内容の表示をして欲しい」と X-Server に伝える。firefox のリクエストを受け付ければ X-Server はディスプレイに firefox の GUI を表示する。

X-Client と X-Server は同じサーバーで動く事が多いが、異なるサーバーで動かす事も可能だ。X-Server は Windows でも cygwin を使えばインストール出来る。

GUI を表示させたいマシンで X-Server を立ち上げたら、X-Client を動かしたいサーバーへ-X オプションをつけて ssh 接続し、コマンドを実行する。この時、ssh でログインしてからコマンドを実行しても良いが、先ほどの方法で直接実行しても良い。
$ ssh -X sagittarius.chaos.com firefox
みたいに。

ちなみに、これをやると firefox が終わるまで terminal のプロンプトが戻ってこない。それが嫌な場合は最後に & をつけてバックグラウンドで実行するとかして頑張って下さい。

自分はよく Windows から ubuntu の emacs を使うが、その際は cygwin の xterm から
$ run ssh -X sagittarius.chaos.com emacs&
とやるとうまくいく。

まあ、ここいらのテクニックは OS や cygwin のバージョンによっても異なるので適当に試して下さい。

あと、当然 OS-X 以降の mac からでも出来ます。(それ以前は知らない)

config ファイルにデフォルト設定を記載しておく

ssh の度に -A とか -X を入力したり、長い FQDN を記載するのは面倒だ。そんな時は、~/.ssh/config にその設定を記載すれば良い。

書式としては下記のような感じだ。
Host *
StrictHostKeyChecking no
Host sagittarius
HostName sagittarius.chaos.com
User wbcchsyn
ForwardAgent yes
ForwardX11 yes
Host aries
HostName aries.chaos.com
...

Host の次が、ssh コマンドで実際に入力するホスト名。ここで * と入力すると下記のオプションは全てのホストに対して適用される。

StrictHostKeyChecking を yes にすると、初めて ssh ログインする時はに聞かれる ssh-server の fingerprint 確認にデフォルトで yes と答えてくれる。HostName は本当のホスト名(又は IP)。ssh が名前解決に使う。User はそのホストにログインする時のユーザー名、ForwardAgent を yes にすると、-A オプションをデフォルトでつける。ForwardX11 を yes にすると、-X オプションをデフォルトでつける。
(-X オプションをつけていても、違いは X-Client のコマンドを実行した時だけでそれ以外は普通の ssh コマンドとして使える。)

上記の場合、$ ssh sagittariusと入力するとssh -AX wbcchsyn@sagittarius.chaos.comを実行した事になる。初めてログインするサーバーでも FingerPrint の確認は出てこない。

その他にも色々とオプションがあるので、興味のある人は man ssh_config を見て下さい。

ファイルを効率よく転送する

リモートで実行した結果がローカルの端末に表示されたりローカルで入力した y や n の文字列がリモートで実行されるのは、ローカルの標準入出力とリモートの入出力がパイプでつながっているからだ。

これを利用すると、ファイルの転送を行ったり出来る。たとえば、mysqldump で MySQL をバックアップした物を gzip 圧縮してバックアップサーバーに保存したい場合。バックアップサーバーから
$ ssh sagittarius.chaos.com mysqldump -uroot -pMyPassword --all-databases | gzip -c > path_to_backup
のように実行すると、1行で見事リモートバックアップが実行される。

解説すると、mysqldump は mysql のデータをダンプして標準出力に渡す。

これをローカル(バックアップサーバー)がパイプで受け取り、gzip 圧縮をする。gzip の -c オプションは結果を標準出力に出すというコマンドだ。gzip は通常、ファイルを受け取ってそのファイル名に “.gz” 拡張子をつけた名前で保存するが、今回はファイルでは無く標準入力から受け取っているので、一回標準出力に出す。

最後に、gzip の結果をパイプでファイルに落としている。

このスクリプトの良い所は mysqldump と gzip が並列に動いている事だ。mysqldump した物をファイルに落としてから gzip 圧縮するより圧倒的に速い。次に、転送、gzip、ファイルへの書き込みまで全てのコマンドの確認が戻り値だけで分かることだ。この 1行スクリプトの直後に$ echo $?とかやって、0 が表示されたらバックアップはほぼ成功したと見なしてよいだろう。これは、ssl のプロトコル自体に転送時のエラーチェックのような物が含まれているためだ。

ただし、このスクリプトには 1点だけ改良の余地がある。mysqldump の結果を転送してから、ローカルで圧縮している点だ。圧縮してから転送した方が、帯域を節約できる。

帯域を節約したい場合は、以下のようにすればよい。
$ ssh sagittarius.choas.com mysqldump -uroot -pMyPassword --all-databases '|' gzip -c > path_to_backup
gzip の前のパイプ ( | ) を quotation ( ‘ ) でくくっているので、ローカルではこの | はパイプではなく文字列と見なされる。つまり、ローカルの shell が ‘|’ を文字列 | とみなし、リモートに転送する。リモートでは | は quotation でくくられていないので、通常のパイプとみなされる。よって、mysqldump の結果が gzip で圧縮されてからローカルに転送される。

以上をふまえて、cron で mysqldump を用いた日時バックアップをするスクリプトは以下で充分だ。
#/bin/sh
tmp=`mktemp` && \
ssh sagittarius.choas.com mysqldump -uroot -pMyPassword --all-databases '|' gzip -c > "$tmp" &&\
mv $tmp /var/backup/dump_`date +'%Y%m%d'.gz`

一応言っておくと、mktemp は一時ファイルを安全に作成してくれる。その一時ファイルに mysqldump の結果を保存し、成功したら /var/backup/dump_YYYYMMDD.gz にリネームする。

途中で失敗した場合、このコマンドは最後まで行かないので /var/backup 以下には正しいバックアップしか残らない。

また、これらの shell コマンドは失敗したらその場で stderr にエラーメッセージが出る。
(特別な場合は例外があるかもしれないが。)
メール送信の設定さえしておけば、cron は stdout や stderr に表示された文字列があるとその内容をメール送信する。なので、失敗したらメール通知してくれる仕組みまでバッチリだ。

なお、おまけだが quotation ( ‘ ) でくくるとリモートで解釈されるのはパイプ ( | ) だけではない。&&、;、||、* なども同じだ。また、変数 ($HOME など)もそのままではローカルの値が、quotation ( ‘ ) でくくるとリモートの値が使用される。

ディレクトリごと効率よく転送する

先ほどの例では gzip を使ったファイル転送の例を上げたが、tar を使えばディレクトリごと転送出来る。たとえば、Apache で公開するための静的コンテンツを ~/git/html というディレクトリで git 管理しているとする。これを、sagittairus.chaos.com の /var/www/html に転送するには、以下のようにすればよい。
$ tar czf - -C ~/git html | ssh wbcchsyn@sagittarius.chaos.com sudo tar xzf - -C /var/www
ただし、このコマンドを実行するには sudo 周りの権限や設定をする必要がある。例えば、こんな感じ。
$ sudo sh - c "echo 'wbcchsyn ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/wbcchsyn"
$ sudo chown root:root /etc/sudoers.d/wbcchsyn
$ sudo chmod 0440 /etc/sudoers.d/wbcchsyn
$ sudo sed -i 's/^Defaults[ \t]*requiretty/#Defaults requiretty/' /etc/sudoers

ユーザー wbcchsyn にパスワード無しで全コマンドを sudo 実行出来る権限を付与し、sudo 一般の設定として、tty が無くとも実行出来るようにしている。(一部のディストリビューションでは tty が無いと sudo を実行出来ないように設定してある。)

tar は通常、tar czf file_name directory1 [directory2 [...]]という書き方で directory1, directory2, … を圧縮、アーカイブし、file_name というファイルに出力する。しかし、ファイル名を – とすると出力を標準出力へ出す。gzip の時と同様に、出力をパイプでリモートへ渡すためだ。

-C オプションは、「このディレクトリにチェンジディレクトリしたつもりになって」という意味。

例えば、
$ cd ~
$ tar czf html.tar.gz git/html

とした場合、html.tar.gz を解凍すると最初に git というディレクトリができて、その下に html ディレクトリが展開される。
しかし、
$ cd ~/git
$ tar czf html.tar.gz html

とすると、html.tar.gz を解凍した際に html ディレクトリが作業ディレクトリ直下に直接展開される。

-C をつけた$ tar czf - -C ~/git html は ~/git/html を、解凍した時に html ディレクトリが直接出来る形でアーカイブし、標準出力に渡す。

tar が作成したアーカイブの標準出力はパイプに渡され、sagittarius.chaos.com でやはり tar を用いて解凍される。圧縮時と同様、ファイル名の – は標準入力を、-C オプションは「このディレクトリに cd したつもりになって」という意味だ。

この1行で、 ローカルの ~/git/html が sagittarius.chaos.com の /var/www/html へ転送される。

ただ、この作業も改善の余地が多いにある。

まず、最初にこの作業では .git ディレクトリも転送されてしまう。.git ディレクトリは git でレポジトリを管理するための物であり、開発中には必要だが本番では必要ない。なので、tar でアーカイブを作成する際にこのディレクトリは除くように –exclude ‘.git’ オプションを付与する。

また、tar はデフォルトではファイルのオーナーやパーミッションも保存してしまう。しかし、おそらくローカルではオーナーは作業用ユーザーであり、リモートでは apache をオーナーにしたいだろう。そのような場合、圧縮時に –owner=’apache’ オプションを付与する。

まとめると、~/git/html を sagittarius.chaos.com の /var/www/html に転送する手順は以下のようになる。
$ tar czf - -C ~/git --excoude '.git' --owner='apache' | ssh wbcchsyn@sagittarius.chaos.com sudo tar xzf - -C /var/www

Port Forward で簡易 トンネリング・VPN を貼る

例えば、学校や会社の FW の中で動いている http サーバーに外からアクセスしたいような場合。Port Forward (ポートフォワード) を用いれば FW をすり抜ける事が出来る。

上記のように、FW 内の aries で動いてる http サーバーに FW 外部の taurus からアクセスしたい場合。同じく FW 内にある sagittarius から例えば以下のように taurus に予めポートフォワードの ssh を接続しておけばよい。
$ ssh -R 8000:aries:80 taurus
この状態で taurus から localhost の 8000番ポートにアクセスすると、aries の 80 番ポート(http の内容)が表示される。
(ssh の接続を切るとポートフォワードも切れてしまうので気をつけよう。)

ちなみに、taurus と sagittarius 間の通信は ssh により暗号化されているので FW の外部における盗聴の可能性は限りなく低い。

しかし、この状態では taurus の 8000番ポートには taurus 自身からしかアクセス出来ない。taurus 以外のマシンからでも taurus の 8000番ポート経由で aries の 80番ポートにアクセスするためにはtaurus の ssh デーモンを GatewayPorts オプションを有効にした上で
$ ssh -R 0.0.0.0:8000:aries:80 taurus
と実行する。
(もちろん、ネットワーク的に taurus の 8000 番へアクセス出来る事は必要。)

ssh デーモンの GatewayPorts を有効にするには、/etc/ssh/sshd_config に GatewayPorts yesと書き込んで ssh デーモンを再起動すればよい。

このように、目的のサーバーと同じ FW 内から外部に ssh アクセスする事でポートフォワードする場合の一般的なオプションは以下になる。
$ ssh -R [bind_address]:port:host:hostport [user@]hostname

なお、ssh 接続の向きは上記と逆にする事も可能だ。


上記のように、taurus から sagitarius に ssh コネクションを張れる場合は、taurus 上で
$ ssh -L 8000:aries:80 sagittarius
とすると tausus から localhost の 8000番経由で aries の 80番ポートにアクセス出来る。

-R の時と同様、このままでは taurus 以外のマシンからは taurus の 8000番ポートへアクセス出来ない。他のマシンからでも taurus の 8000番ポート経由で aries の 80番ポートへアクセスしたい場合は
$ ssh -L 0.0.0.0:8000:aries:80 sagittarius
と実行する。-L の場合は -R と違って ssh デーモンの GatewayPorts の設定が必要ない事は少し嬉しい。

-L と -R は ssh の向きが違うだけで動作はほとんど同じだ。-L を使うと ssh client 上でポートを開き、目的のサーバーへポートフォワードする。-R を使うと ssh server でポートを開き、目的のサーバーへポートフォワードする。

-L と -R は client を左側、server を右側に書いた時、ポートを開く場所が left, right という事らしい。ただ自分は「client は L の文字が含まれ、server は R の文字が含まれる」と覚えている。

今回の例では、-R を使うと、taurus から aries を閲覧中に万一 ssh コネクションが切れると、sagittarius から再度 ssh を張り直さなくては行けない。実用にのせるためには sagittairus で ssh コネクションを監視するか、taurus からも sagittarius にログイン出来る仕組みを作る必要がある。

そのため、今回の例に限って言えば個人的には -L を使う方が良いと思う。-L を使う場合は、taurus から sagittairus へ ssh アクセスできれば全て解決だ。

どちらの場合も言える事だが、ポートフォワードは実質的に FW に穴をあける事に等しい。そもそも、FW 的にアクセス出来ないのはそれだけの理由があるかもしれない。多くの場合、ネットワーク管理者がユーザーのポートフォワードを禁止する事は難しいのが現状だが、だからと言って乱用は避けた方が良いだろう。

以上、駆け足でしたが ssh の紹介でした。