第17回「プログラミング体験ノススメ」


Linux システムを使うということは、多かれ少なかれプログラムを自分で読み書きすることだと考えます。 Linux システムを使う以上、シェルスクリプトと無縁でいられる人は滅多にいないでしょう。

OSSは「問題が見つかったところをアップデートしながら(修正を加えながら)利用していく」のに対し、業務システムは「最初から完璧に仕上げたものをアップデートせずに(修正を加えずに)利用していく」という基本姿勢で動いていると思います。業務システムにOSSを採用するということは、OSSの基本姿勢を受け入れるということですので、アップデートと無縁でいることはできません。
内容を把握せず、十分な試験を行わずにOSSを導入してしまった結果、長期安定試験や負荷試験では発見できない不具合に悩まされています。そして、自前でプログラムを書くことを避け、他人が書いたサポート対象のプログラムを組み合わせて構築した結果、内容がブラックボックス化して、自力で切り分けができなくなるという不幸が起きています。それは、脆弱性など緊急性が要求される際の対応に如実に表れます。

既存のプログラムは、いろいろな要望に対応しようとしてさまざまな機能を詰め込んだ結果、複雑になりすぎた感じがあります。そのため、変更を加える際の影響範囲を見極められずにリグレッションが発生したり、内容を把握していないために errata の適用要否の判断に迷ったりというのが、正直な状況ではないかと思います。そんな状況を変えるには、必要な機能だけを持つ専用のプログラムを自分で作ることも視野に入れてほしいです。そこで今回は、既存のプログラムに頼り切るなという話をします。

自作プログラムの例として、サーバの死活監視を行うプログラムを考えてみましょう。Unix 的な考え方に従った場合、単機能なプログラムを作って組み合わせたシェルスクリプトとして実現することになるでしょう。

---------- 死活監視スクリプト ここから ----------
#!/bin/sh
if curl -I -s http://www.example.com/ | head -n 1 | grep -qF 200
then
  echo "Server is alive."
  exit 0
else
  echo "Server is dead."
  exit 1
fi
---------- 死活監視スクリプト ここまで ----------

次に、この死活監視スクリプトにはどんな罠が存在しているかを考えてみましょう。このスクリプトはコマンドの exit コードに基づいて判断している訳ですが、「コマンドの exit コードが 0 ではないことが、サーバーがダウンしていることを意味しているとは限らない」ということに注意してください。例えば、メモリー不足やリソース使用量の上限制限に引っ掛かったことが原因で、 curl / head / grep を実行するための子プロセスの作成に失敗するかもしれませんし、ウィルス対策ソフトのスキャン処理が実行中であったためにプログラムの起動や共有ライブラリファイルなどのオープンに失敗するかもしれません。また、途中のネットワークがダウンしているなど、サーバー側ではない問題によって curl コマンドがエラーとなるかもしれません。他にもいろいろな可能性が考えられます。問題が発生すると原因究明と再発防止策が重要視される傾向がありますが、このスクリプトは何の情報も記録に残してくれないため、「どうして稼働中のサーバーがダウンしていると判断されてしまったの?」と質問されても、満足な回答をすることができないのです。

exit コードで伝達できる情報は、何らかの問題事象が発生したかどうか程度です。しかも、利用する側が問題だと考える条件と、プログラム が 0 以外の exit コードを設定する条件とが合致しているとも限りません。 exit コード頼みのシェルスクリプトではエラーハンドリングが困難です。そこで、C言語のプログラムに置き換えた場合を考えてみましょう。

---------- C言語による死活監視プログラム例 ここから ----------
#include 
#include 
#include 
#include 
#include 

static in_addr_t get_addr(const char *hostname) {
    struct hostent *hp = gethostbyname(hostname);
    return *(in_addr_t *) hp->h_addr_list[0];
}

int main(int argc, char *argv[]) {
    char buffer[8192];
    char const query[] = "HEAD / HTTP/1.0\r\nHost: www.example.com\r\n\r\n";
    struct sockaddr_in addr;
    const int fd = socket(PF_INET, SOCK_STREAM, 0);
    unsigned int status;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = get_addr("www.example.com");
    addr.sin_port = htons(80);
    connect(fd, (struct sockaddr *) &addr, sizeof(addr));
    send(fd, query, sizeof(query) - 1, 0);
    shutdown(fd, SHUT_WR);
    recv(fd, buffer, sizeof(buffer) - 1, MSG_WAITALL);
    close(fd);
    sscanf(buffer, "HTTP/%*u.%*u %u", &status); 
    if (status == 200) {
        printf("Server is alive\n");
        return 0;
    } else {
        printf("Server is dead\n");
        return 1;
    }
}
---------- C言語による死活監視プログラム例 ここまで ----------

上記のプログラム例では、勉強目的のために意図的に、エラーハンドリングを省略しています (*1) 。どのようなエラーや問題が発生しうるかを考え、適切に書き直してみましょう。

Webの世界では公開APIを使って自分好みのクライアントプログラムを自作する動きが出てきています。OSの世界だって、システムコールという公開APIがあるのだから、自分好みのシステムプログラムを自作したっていいのではないでしょうか?ちょっとしたシステムツールやユーティリティなら、C言語でも数十行から数百行程度の規模で目的を達成できると思います。自分で作れば、必要としていない機能による不具合やリソース消費に遭遇することも無くなりますので、errata に振り回されずに10年間使い続けられるでしょう。システムコールを使ったプログラミングができようになると、問題事象の再現ができるようになります。それは、ひいては問題事象を未然に防ぐことにも繋がると考えます。

サポートセンターでは自作プログラムについてはサポートしてくれないものですが、サポート対象か否かという基準で平行線を続けていては、いつまでもギャップを埋められません。頑張って最初の一歩を踏み出してください。

  • (*1) セキュリティキャンプ2014 の私のゼミでは、このプログラム例を、実際にエラーハンドリングを行うプログラムに改造することを通じて、問題が発生した場合にどう対処すればいいかを含めて設計を行うことの大切さを学び、「思いやりのあるプログラムの書き方」や「物事を最初から最後まで通して考える能力」について考えてもらいました。もちろん、bash 脆弱性glibc 脆弱性 が発見されるよりも前の時期に作成されたプログラムですので、そのような脆弱性があることは知らなかった訳ですが、必要な機能だけを持つ単純明快なプログラムであればホワイトボックス化できますので、影響の有無を判断することも簡単です。

(半田哲夫)

コーヒーブレーク「Dockerは上から目線?」

家の用事があってお休みをとっていたところ、会社から電話がかかってきて、決裁処理をして欲しいと頼まれました(マーフィーの法則発動です)。決裁をするためには、会社のシステムにログインしなければなりません。でも、大丈夫、24時間働く自宅警備員、じゃなくて管理職は、ちゃんとそのための環境を用意しています。VMwareを起動し、仮想マシンを立ち上げましたが、会社のシステムに接続しようとすると、適用されていないWindowsのセキュリティパッチの長いリストが表示され、出直してこいと怒られました。仕方ないので、Windows Updateを行い、言われるがまま再起動を繰り返すこと、数十分、やっと終わったと思ったら、アンチウイルス、Windowsファイアウォール、Java, Adobe flash等々「古すぎる」と怒られます。「あぁ、これはきっと無心の境地を学べと言われているのだな」と思い、更新マシンと化して作業していましたが、何が悪かったのか途中で仮想マシンが起動できなくなってしまいました。何度リトライしてみても起動しません。結局あきらめて会社に電話して、他の人に代行をお願いしました。読者の方でも、同じような経験をされた方は多いのではないでしょうか?

実際に行いたかった決裁処理は、ごく単純であり、接続さえできていれば数分もあれば終了する内容です。しかし、その前提となる「環境」のチェックが複雑(デリケート)であるためにこのようなことが起こります。環境のチェックに時間がかかるのは、要するに「環境」の内容が複雑であるからに他なりません。今回のケースで言えば、具体的には、仮想マシンハードウェア、そこにインストールされたOSおよび適用されているセキュリティパッチ、そのOS上にインストールされたブラウザ、各種プログラム、Java, ActiveX、アンチウイルス等々です。
こうした環境の維持は、オフィスで使っているデスクトップでも必要であり、実施しているわけですが、毎日使っていると目立たないものが、数ヶ月に一度のようになると顕在化します。環境維持のコストは構成要素の「更新」の時間だけではありません。仮想マシンの「環境」は、VMwareを実行している端末上では、数10GBの容量を使用しています。起動しなくなったときのために、気軽にバックアップをとるわけにもいきません。

「環境」維持に関するコストは、対象となる環境が大かがりで複雑であるほど増大します。その意味で、「環境」は小さく単純なほうが望ましいわけです。「仮想マシン」は、「仮想ハードウェア」というところから順に、OSやアプリケーションなどを積み重ねたものと見ることができます。ここで、発想を変えて、上から眺めてみていくとどうなるでしょうか?実行したいものとしてアプリケーションに注目し、そのアプリケーションが動作するために必要なもの、連携して動く他のアプリケーションや ライブラリ、設定がある、というように見ていくわけです(依存するものをあげる上ではOSも含まれますが、ここでは同じOS上ということを前提として話を進めます)。

使いたいアプリケーションなり機能が動作するための「環境」がコンパクトな固まりとなっていて、それを互いに交換したり、共有できたらどうでしょう?開発環境で動作したものが、本番環境で動作しない、アプリケーションをインストールしたけれど、依存関係の問題で動作しないというシステム開発者の悪夢は解消します。もし、問題が発生した場合も、サポートセンターでその環境(固まり)を手元に用意することができれば、事象を確認して、問題解決に着手できます。仮想ハードウェアやOSを含まないので容量もコンパクトで、組織内の展開も容易となりますから、自然と共通的な基盤が整理されることになるかもしれません。

そうして環境が流通するようになれば、環境(固まり)ごとにバージョンが付与されていると便利です。さらに、DBMSや主要なアプリケーションについてはその開発主体からお墨付きの環境が公開されていれば、わざわざ自分で悩むこともありません。システム構築は、「インストールして、設定を作り、試験する」から「オフィシャルな部品を中心に組み合わせて、調整する」になります。

Dockerについて、私はそんなふうに上から目線で(笑)見ています。

(原田季栄)



第17回「プログラミング体験ノススメ」