Webアプリのデプロイメントの悲しい状況

ここ4日間というもの、インターネットWebフォーラムをインストールしようとして大いに時間を無駄にしました。説明書きには30分でセットアップ可能と書いてあったんですけどね。

コンピュータは得意なほうだと思っているのですが、一体、何が悪かったのでしょうか? これからお教えしましょう。

私の奮闘記

辱めるのは本意ではないので製品名は言わないでおきます。この製品でひどい目にあったのは今回が初めてではありませんが、製品単体の問題というよりはもっと大きな問題だからです(”ピス・ホース(馬におしっこ)”に音が似ているとだけ言っておきましょう)。

30分でセットアップ可能と説明されている根拠は、Dockerによるインストールが正式にサポートされていて、それ以外の手段がないからです。Dockerは輝かしくも新しいコンテナ管理ソフトウェアで、それだけでも危険な香りがします。相互運用可能なデスクトップ環境全体をインストールするのに、Dockerを使わなくても問題なかったのですが、Webフォーラムは疑似仮想化OSを必要とするほど複雑なのでしょうか? うーん。

私はベンダ版のDockerをインストールしようとしましたが(私が使用しているのはUbuntu 14.04の最新のLTSリリースです)、これのバージョンは1.0で、Dockerは1年半のうちに1.7になっていました。このソフトウェアは最低でも1.2を必要としていました。このWebフォーラムはあまりにも最先端を行っているため、2年前は存在していなかったテクノロジを使わないとインストールさえ拒否するのかとイライラしました。

そこでオフィシャルに認可された方法で最新のDockerをインストールしようとしました。こうすると、もちろん、シェルにcurlをパイプすることになります。これは最悪の方法ですが、Dockerのセキュリティはひどいものですからね。もちろんインストールは失敗し、E: Unable to locate package docker-engine(docker-engineというパッケージが見つかりません)という意味のないメッセージだけが返されました。こんなパッケージマネジメントの悪夢から救ってくれるためにDockerが存在してくれていて涙が出るほどありがたいです。
少し調査してみると、32ビット版のDockerは存在しないようでした。彼らは問題ないはずだと言っている(標準の32ビットUbuntuパッケージが存在しているからという理由で)のにです。このことをREADMEにも、インストールマニュアルにも、rootとして実行した時のシェルスクリプトにも、記述する気はないようです。
この時点で私はもうDockerにうんざりして、マニュアルでインストールしてみようと決めました。所詮はRailsアプリですし、Railsアプリは以前にもインストールしたことがあります。そんなに大変なことにはならないでしょう。
ハハ。git cloneを実行した後(どうしてRubyGemsに入っていないんでしょうね)、私はおそらく6時間はRVM(Ruby Version Manager)と格闘しました。(読者の中にはこれを読んで、Rubyの環境が違うんじゃないかと助言したい人がいるでしょうが、黙っていてください。すでにRVMをインストールして色々動かしているんです)。

アプリの依存ファイルをインストールするためのbundle installを実行している時に、非常に分かりづらいエラーが出たのです。.aファイルがビルドディレクトリに存在しない、といくつかのライブラリがエラーを返してきましたが、理屈に合いません。そして、パスにはx86_64-linuxがあったので、ますます訳が分かりませんでした。

私は実は64ビットカーネルを使っていますが、ユーザ空間は32ビットです(これにはちゃんとした理由があります)。そしてRVMがビルドしたRubyのバイナリは、もちろん32ビットです。Libcと他全てが32ビットなので、そうでなければ動作しません。しかし、これらのバイナリは自分たちが64ビットのシステム上で稼働していると思っていたのです(実際そうですけどね)。一方RubyGemsはネイティブ拡張をビルドするのに、何だかバカみたいな理由でシステムアーキテクチャを使うので、全てが64ビットとしてビルドされてしまいました。ある意味この1つのパッケージが異常終了してくれてラッキーだったかもしれません。他はちゃんとビルドされていたので、最悪の場合、実行時に気付く羽目になっていたでしょうから。

私はあらゆる環境変数を試し、ファイルを手動で編集してみたり、他にもいろいろ試してみたりして、実際は32ビットだとRubyに分からせようとしましたが、無駄な努力でした。ついに私はRVMのソースコードを読むしかないと覚悟を決め、そして--32フラグが魔法のように全てを解決することを発見したのです。これはドキュメントに記述されていませんが、心配要りません。 半年前、GitHubで起こった問題についての3人からのコメントに、RVM2.0でドキュメントを修正するという返信を見つけましたから…。

さあこれでRubyが無事稼働したので、面倒なリビルドさえ終わればバッチリなはずです。最高ですね。

さて、今度はこのバカげたアプリをどう設定するかを解明する番です。READMEには「Dockerを使え」としか書いていないので、なかなか厄介です。config/app.conf.sampleファイルは存在していましたが、これはUpstart、つまりUbuntuのサービスマネージャのためのサンプルだということがわかりました。Ubuntu上へのインストールマニュアルを最終的に発見できましたが、どこからもリンクされていませんでした。

次はデータベースの認識を”存在していない”から”存在している”にマイグレートします。Railsでは楽勝のはずです。と言うのは冗談で、私は地獄のような悪夢を経験せずには一度も成功したことがありませんが、今回も例外ではありませんでした。マニュアルにはこのアプリはスーパーユーザとして実行するように書いてあります。ちなみにPostgreSQLのマニュアルにスーパーユーザに関してどう書いてあるか見てみましょうか。

スーパーユーザ権限を使うのは危険なことです。本当に必要な時だけ使いましょう。

はい、まさにこれがWebフォーラムが必要としているもののようです。私はデータベース全体にroot権限を与えないことを選択していましたが、その結果、もちろんマイグレーションは失敗しました。サーバ上にバイナリの機能拡張を読み込むのにCREATE EXTENSIONを使うからです。データベースマイグレーションがこれを行うのは大変理にかなっています。私は必要な機能拡張さえもインストールしていませんでしたが、もちろんマニュアルにはそれが必要だとは書いていませんでした。それでともかくインストールしたのです。

postgresql-contribをインストールしたらおかしなことになりました。なるべく手短に説明しますね。私の環境ではPostgres のバージョンは9.1で、Ubuntuの最新バージョンは9.3です。元々postgresqlパッケージをインストールしていて、デスクトップでArch Linuxを利用していたので、常に最新のバージョンなっているものと思い込んでいました。しかしUbuntuは「サーバ全体を破壊しない」などと言う些細なことを気にして、常に9.1にしていてくれたのです。しかしpostgresql-contribはこれを認めず、現在の最新バージョンの9.3が必要だとして、サーバ全体をインストールしてしまいました。やれやれ。それで、私は回り道をして9.3にアップグレードしました。これは以前やったことがあったので比較的楽でした。

オーケー! データベースが設定できました。

ここまで来てもまだ、マニュアルにはBluepillと呼ばれるRubyのプロセスマネジメントライブラリをインストールし、山のような”コンフィギュレーション”(もちろん、実際にはこれはただのRubyのコードです)をコピーし、そしてそれを使ってアプリを実行し、そして更にBluepillをユーザのcrontabに@rebootとして追加し、などと他にも何だかんだと書いてありました。

(私は「マトリックス」からの引用かと思っていたのですが、後から誰かがこれはbluepillと呼ばれるもので、ツールをモニタするものだと教えてくれました。ステキな名前ですが、Rubyでは平均的かもしれません。

いずれにせよ、これを全て行わないと選択し、rails serverで直接実行しました。

これでほとんど出来たはずです。今度はこれにnginxをプロキシする必要があります。アプリは親切にもコンフィギュレーションを提供してくれるのですが、これが200行の長さでした。中身のほとんどは入り組んだルールで、どのURLが静的アセットで、どれがプロキシされるべきかなどといったようなことが書いてありました。私はクソくらえと思い、全部をプロキシしてしまい、後で気が向いたら修正しようと思いました。

これで準備完了です。でもなぜかサインアップのeメールも来ません。今回は、ジョブプロセッサの”sidekig”も実行する必要があったということが分かりました。今度こそ、準備完了です。

我々は何と恐ろしいものを作り出したのか

なぜ私がこの話を書いているかといえば、完全にバカバカしい体験だということを伝えるためです。

風変りなツールが使い物にならないことはさておき、指示(訳注:リンク切れです)にそのまま従うとすれば、このWebフォーラムでは更に以下のことが必要になります。

  • ソフトウェアのソースコードをクローン(インストールではありません!)し、それをインプレースに変更する。
  • rootとして、nginxにコンフィギュレーションの何百もの行をコピペする。そしてアップグレードした時にそこが変更されないことを祈る。
  • Bluepillのために、Rubyの何百もの行をコピペする。そしてアップグレードした時にそこが変更されないことを祈る。
  • rootとして、デフォルトでは入っていないPostgres機能拡張をインストールする。
  • superuserとして、誰かの任意のデータベースコマンドを実行する。
  • rootとして、logrotate コンフィギュレーションをインストールする。

このアプリには驚くような機能はないはずです。HTTP接続をして、データベースを使い、eメールを送信する程度ですよね。どうしてこんなにも複雑なんでしょうか。

その訳をお教えしましょう。

Railsは最低

私の経験は確かに限定されていますが、私に分かる範囲で言えば、Railsアプリをインストールするのは不可能です。ソースディレクトリからコンフィギュレーションを読み込み、ソースディレクトリにログを出力するのです。手動でこれら全てのアセットをプリコンパイルしなくてはなりません。もちろんこれもソースディレクトリに書き込まれます。

Railsは世界でも最もポピュラーなWebフレームワークの1つであり、あちこちの開発者から支持されています。それなのに、Railsで書かれたものはまったくインストールできないのです。ふざけてますよね。

Unixのウソ

ずっと前、Windowsがマルチユーザに対応しておらず、管理者権限を持つアカウントで全ての操作を実行していた時代、Unixオタク(私自身も含みます)は、ユーザ毎にアカウントをいくつも作ることができるUnixの素晴らしさについて、得意気に語っていたものでした。

そう思っていた自分自身を、とても恥ずかしく思います。要点をまとめてみましょう。

  • C言語で書かれているライブラリやプログラムが見つからない場合、あなたはパッケージマネージャからインストールするためのroot権限が必要になるか、逃げ道のない場所からローカルビルドを繰り返さなければならない恐怖の世界へと突き進むことになるでしょう。共有ホスティング上にlxmlが必要だとしても、そこにはlibxml2がインストールされていないかもしれません。いいかげんにしてほしいですね。
  • ポート80にバインドできるプログラムはたった1つだけであり、実行するにはroot権限が必要です。そのため、取り得るオプションとしては、Nginxを使い、新たなアプリをインストールするためにrootを必要とするか、Apacheを使い.htaccessを実行するか、またはその他何か大変なことをするくらいです。
  • もちろん、アプリは自動的に起動してほしいですよね。そのようなアプリは@rebootを使ってcrontabに加えることができます。これはちょっとした改造で、プロセスが死んでいたらリブートできません。そのため、このアプリがやっているように、自分自身のローカルプロセスマネージャをインストールすることできます。または、多くの人たちがやっていることをしてもいいでしょう。root権限を使って、システムのデーモンマネージャに加えることです。聞いた話によると、多くの最近のデーモンマネージャといったものは、root権限を持たないユーザでも設定を進められるとのことですが、私個人としては実際にそれで動いているのを見たことがありませんし、明確にそれを説明しているものを見たこともありません。
  • ログをローテートさせたい場合、root権限が必要です。
  • Dockerが上記の問題を解決できると思っていますか? その場合、私にsudoを動かしているシェルスクリプトにどのようにcurlをパイプするのか、ぜひ教えてほしいものです。ええ、あなたがDockerグループにいる場合は、root権限で実行しているんでしたね

現在のLinuxデスクトップはマルチユーザで使う分には非常によくできています。まあ、基本的には誰も使っていませんが。サーバサイドではどうでしょう。そうですね、サーバであれば、誰もがroot権限を持っていると想定できるでしょう。そのため全てがごちゃごちゃな状態です。ユーザが異なるRubyのバージョンを複数インストールできるように設計されているRVMでさえも、sudo apt-get installか何かを実行するために、パスワードを求めてきました。

自分のマシン上で動いた

私たちは依存関係を列挙したり対処したりすることが本当に苦手です。

つまり、私たちは自分たちのソフトウェアでさえ、依存関係を表現することができません。システムパッケージマネージャがその処理を行っており、その仕事は申し分ないものです。しかし、私は開発者であり、パッケージをひたすら作る人ではありません。CライブラリをラップするPythonライブラリを書く場合、その依存関係を表現する方法はありません。そんなものあるわけがないのです。C/C++の公式に認められたリポジトリなんか、どこにも存在しないのですから。たとえ依存関係を表現できたとしても、どんないいことがあるのでしょう? LD_LIBRARY_PATHLDFLAGS=-rpathみたいなものを使って、C言語の共有ライブラリをローカルにインストールするのは、非常にうんざりすることです。私だって、よく分かりません。実際、本当に面倒な作業なので、誰もそんなことはしないでしょう。

そのため、サービス上に依存関係を列挙するものが何なくても、何の驚きもありません。Webアプリなら“Postgresが必要で、Redisはオプションで必要”と書かれたメタデータくらい持っているだろうと、あなたは思うかもしれませんが、そんなものは存在しないのです。一方、ユーザが使えるサービスを列挙できるシステムも、同様に存在しません。ないものは見つけることもできませんね。もしアプリで必要とするサービスがないものの、ドキュメント化に失敗したか、誤って設定してしまったとしたら、最初の段階でそれを調査するしかないのです。

では、次の話題に移りましょう。

Webアプリと問題レポートの困難

うまくいく部分/うまくいかない部分全体を通じて、失敗時のレポートが決定的に欠如しているのです。私の場合、問題がある場合は、基本的にみんなが私にツイートしてきてくれる情報か、IRC(インターネット・リレー・チャット)で投げかける質問に頼って解決します。この特定のアプリはジョブキューに決定的に依存しているものの、実行中でないことに気づかなかったなどといったことです。

クラッシュログをメールで送ってくるウィジェットも数個ありますが、いったいどこのマヌケがそんなものを考えたのでしょう? 何の役にも立ちません。私自身、私の完璧で実用的、かつ落ち着いたトラフィック量があるWebサイトのクラッシュをレポートした2000通以上のeメールが未読のまま放置されています。そのほとんどが、どこかの誰かが誤ったURLを打った結果をレポートしたものであり、私自身、修正する気もないものです。

しかし、アプリがダウンして完全に起動できない状態に陥った場合に、私が受信するeメールの数はゼロです。アプリが動いているものの各リクエストを処理するのに20秒費やしている場合も、eメールの数はゼロです。そして全てのページで404エラーが出ているとしても、eメールの数はゼロなのです。そうしているうちに、本当に実際のページが壊れ始めたとしたら、大量のeメールを受けることになりますが、もはやメール自体を見なくなっているため、そのメールに私が気づくことはありません。

このような問題はよくあります。それにもかかわらず、私がこれまでに見てきた唯一の解決策は、手作業でいくつものグラフを見比べることくらいなのです。

今頃はもう、こんなものが使えると期待していました

私たちに必要なのは、コマンド1つでインストールができて、設定は5分で終わり、かつ複数サーバへの拡張や共有ホスティングへの縮小が可能なアプリです。DreamHost上にインストールできないWebフォーラムは、完全なる失敗作です。

しかし私たちは、この課題に挑戦すらしてきませんでした。これを解決できる優秀な人はいても、彼らは皆TwitterやAmazonのサーバを1000万台に拡張するといった類いの作業で手一杯なのです。基本的なWebソフトウェアは日に日にインストールしづらくなり、共有ホスティングの有用性は下がる一方です。そして2つのアプリで、同じデータベースを利用する方法を思いつかないWeb開発者たちは、基本的に仮想マシン上で動作するDockerようなガラクタに群がっています。

私が欲しいのは、Webアプリのインストールと管理に特化した仲介用のWebアプリです。実装方法は見当がつきませんが、cPanelのようなものから、人気のアプリに対する間に合わせのサポートを除いたものが理想です。あとはアプリの最小必要条件を教えてくれるプロトコルもあるとうれしいですね。

そして、「”ピス・ホース”というRubyアプリをインストールしろ」と言えるようになりたいのです。そうすると、あとは上記の夢のアプリが、gemの検索やアプリの動作に必要なRubyバージョンの確認、該当バージョンのRVM環境のインストールを行ってくれます。新規にgemsetを作成して、gemのインストールもするでしょう。そして”代表的な場所”にあるメタデータファイルをチェックし、そのファイルがPostgreのデータベースとRedisのインスタンスを必要としていることを確認します。PostgreやRedisに接続するために、どんな一般的な方法が使えるかも調べてくれます。そして私にPostgreやRedisがある場所を確認し、見つけたものは全てデフォルトとして提供するのです。postgresql:///pisshorseなどの簡潔なものは受け付けてくれますが、TCP接続していない限り無意味な、10個に分かれたフィールドなどはダメでしょう。あとは、これらに問題がないかダブルチェックした上で、その内容を~/.config/webapps/pisshorseなど、どこかに存在する非常に小さな設定ファイルに書き込みます。この間に、データベース接続のTTL(有効期間)などの、どうでもいい値を設定するよう私に求めてくることはありません。そんな値は誰も気にしませんし、コンピュータならそのくらい自動で判断できますからね。

共有ホストで、Postgreデータベースが1つだけの場合は、これで万事うまくいくでしょう。なぜならそれは、Postgreのスキーマについて私たちが理解した上で使用し、アプリがそれを実際にサポートしている夢のような世界だからです。

メタデータファイルにも、必須の(もしくは、あると望ましい)システムレベルのライブラリとバイナリが全てリストアップされ、その中に未インストールのものがあれば、インストールをするよう私たちに要求します。apt-getyumpackerなどのコマンド1つで、私たちは自ら検査や実行といった処理ができます。繰り返しますが、共有ホストの場合は、自身でソフトウェアのインストールはできません。その場合は、インストローラがローカルでインストールを試みるか、単に断念するかのどちらかです。いずれにせよ、実際のところWebフォーラムはoptipngを必要としておらず、なくても続行可能だと分かるので、全く問題はありません。

その後は、ユーザ領域のデーモンマネージャにアプリを追加します。デーモンマネージャがない場合は、@rebootというワザを使えば、それが存在するかのように振る舞ってくれます。アプリにジョブキューの稼働が必要だと判断すると、それも追加します。使用するサーバはGunicornやUnicorn、uWSGIなどですが、どれを使うか私たちが気にすることは、あまりないでしょう。もしこだわりがある場合は、違うものを指定することもできます。デフォルト設定では使用者は2人だけですが、負荷を監視して、必要に応じで子プロセスを生成し、現状の平均的なトラフィックも把握してくれます。その際、マシンのメモリを食い過ぎていると判断すると、私たちにメールを送信したり、IRC上でpingを打ったりするのです。

アプリがバインドされるのは~/.config/webapps/pisshorse/pisshorse.sockなので、あまり便利とは言えません。この部分は私もまだ、どうしたらいいか悩んでいます。というのも、実際のところHTTPバーチャルホストのセットアップをどのようにするか決める手立てがなく、nginxを使っている場合は、いまだルート化する必要があるからです。ですが(複雑な)次善策なら、いくつかあります。ここで、ルート化することなく、リバースプロキシを設定してもらえる素晴らしい世界を想像してみてください。そこでは、静的アセットをキャッシングするためのルールが追加され(メタデータファイル内にも定義され)、私たちがCDN(コンテンツ・デリバリー・ネットワーク)を持っている場合には、それを要求してくるでしょう。

アプリが動いていても、ユーザがいなければ、まだ確認メールを受信していないので、ログインはできません。でもコマンドラインから実行できる管理者用コマンドが、メタデータファイルにも何個か指定されているので大丈夫です。もちろん魔法のようなWeb GUIでも同様の対処をしてくれます。

ここから先は基本的に、管理GUIのことは忘れて構いません。でも裏ではログや統計データを収集しているので、要求すればグラフを見せてくれます。そして、アプリが起動に失敗する、動作していたページで不具合が急増している、要求に対応しきれていない、ジョブキューに異常が発生した、といった事態が発生すると必ずpingが送信されます。

結局はアップグレードが必要になりますが、うれしいことに、ボタンをワンクリックするだけで済みます。その際、既存のインスタンスは読み取り専用モードになります。でも全てのアプリはこのモードに対応しているので、面倒なことにはなりません。ジョブキューは終了し、データベースはコピーされてから更新され、新たなアプリのインスタンスが別で立ち上がります。新規リクエストの対象は新しいコードになり、古いインスタンスが終了すると、古いデータベースはアーカイブされます。新しいインスタンスが、いきなりエラーを吐き出した場合は、古いコードが保持され、お叱りメールが自動でアプリの管理者に送信されます。いずれにせよ、大きな混乱を招くことはありません。

これはアプリ側にもメリットがあります。というのも、GunicornやuWSGIなど、どのサーバでアプリが動作中かを確認できる小規模なライブラリを使用しており、自身の負荷測定や再起動、要求外の単純なコードの実行など、簡単なタスクがこなせるからです。

夢は膨らみますね。

私たちは20年間も、このようなことをしています。正しく動作し、プラガブルで依存性がなく、全てを完璧にこなすアプリが、そろそろ完成していてもいい頃です。WebのGUIを捨て去れば、自動生成された汚いものではなく、手動で設定された整然としたシステム管理者のようなアプリができるはずです。

ところが私たちは、既存のものに、複雑なゴミの層をひたすら積み上げています。修復方法が、もう分からないのです。そして昨日登場したパッとしないアプリをデプロイするために、ThinからMongrel、Passenger、Heroku、Bitnami、Docker、そして更なる新しいものへと、せわしなく乗り換えています。もしくはフレームワークに、より良いSasSのインテグレーションを導入することに執着していたりします。

RubyやRail、または特定のアプリを非難するつもりは全くありませんよ。私は、ただ自分のWebソフトウェアをどうしてもデプロイしたくないのです。システム内には、互いの機能をほとんど把握していない部分が多くあるので、一部が故障すると、システム全体が停止してしまうのです。私は今、少なくともログの確認や再起動が簡単にできるように、最低でも5つの処理をtmuxで走らせています。

これは悲惨で、恥ずべき状況です。PHPに人気が集中するのも、うなずけます。私はどうやって新人のWeb開発者たちに、「これが、君らが期待していたものだよ」と伝えればいいのでしょう。