Chienomi

Chienomiのアプリ 2度の変遷を追う

プログラミング::beginners

Chienomiは2022-10-09のサーバー移設後、12日, 15日の2度、アプリケーションが刷新された。

といっても、Chienomi自体はご存知のとおりPureBuilder Simplyで組まれており、webアプリケーションを必要としない。 Chienomiで動作しているアプリケーションは「検索」「いいね」「コメント」の3つである。

この3段階の手の加え方は、日常的プログラミングに慣れている人には馴染み深い手法だ。 だが、仕事でしかプログラミングしない人はあまり感覚として出てこないものかもしれない。

そこで、Chienomiのアプリケーション改修について紹介しよう。 (ただし、今回のアプリケーションは非公開だ。)

第一段階

4時間ほどかかった最も困難な作業が第一段階だ。

ChienomiはもともとConoHa WING上で動作しており、それを前提にしたアプリケーションが実装されていた。 ConoHa WING上では単にCGIとして動作するだけではなく、SSHでログインできるサーバー環境があり、ユーザー個別の仮想サーバー環境が作られる。VPSのようにユーザーが操作できる仮想ルートを持っているわけではないが、他のユーザーのディレクトリは見えない。

このため、ホームディレクトリ上のアプリケーションをCGIから呼ぶようなこともできる。 CGIはSuEXECで呼ばれるが、そのためにすべて当該ユーザーレベルで動作すると考えて良い。 また、この仕組み上、ホームディレクトリ上にインタープリタを用意して、そのインタープリタでCGIを動作させることもできる。

実際、ConoHa WING上で動作させているプログラムの中には、自分でビルドしたRuby 3.01で動作するものもある。 また、この仕組みのために、RubyGemsの利用も可能で、RackのCGI Handlerを用いているものもある。

VPSに移すとなると、Nginxベースになる。 WINGはNginx-Apacheの構成で、CGIはApacheで動作する。

NginxでCGIを動作させる方法はいくつかあるが、効率を考えるとあまりよくない。 いっそNginx-Apacheの構成でも相対的にそれほど悪くないほどだ。 簡単なのはWebrickを動作させ、CGIパスを設定してWebrickに移譲することだが、結局のところWING向けに構築してあるCGIがそのまま動作するわけではない(データベースが置けるパスであったり、インタープリタの場所であったりが特殊だったりする)ので、どうせ直すならちゃんと最適化させたい。

そこで、

  • CGIアプリケーションをライブラリとして動作するようにする
  • Sinatra (Thin)でアプリケーションサーバーを書き、ライブラリ化したアプリケーションを呼び出せるようにする
  • アプリケーションサーバーを管理するSystemdユニットを書いてNginxと接続
  • テンプレートを更新して新しいアプリケーションパスにつながるようにする

という手順を踏んだ。

大きく変わった部分として、従来、CGIはそれ自体で完結するものであるのに対して、一部のプログラムはライブラリを読む。 そのライブラリパスはホームディレクトリ以下にならざるを得ないため、特殊な配置をしており、絶対パスでrequireしていた。

これに対し、アプリケーション部分がライブラリ化され、それとは別に従来からライブラリとして存在していたものがある。 これについて、ライブラリ化されたアプリケーションはrequire_relativeで読むようにした。 つまり、ひとつのアプリケーションサーバーが使うファイル群はあるディレクトリ以下に集約されており、require_relativeで読むようにする(Node.jsのアプリケーションサーバーに似た構成)。 一方、もともとライブラリだった共有ライブラリは/usr/local/lib/ruby/site_ruby以下に配置し、これを$:に追加するようにした。 $:への追加タイミングは難しいが、今回の場合アプリケーションサーバー本体の先頭で読めば良い。

第二段階

WINGのCGIの仕様は、.cgiファイルを直接実行するようになっている。

実はMimir Yokohamaのアプリケーションは.cgiファイルはZshスクリプトで、間接的にRubyスクリプトを呼ぶようになっている。これにより共有可能な仕組みになっているが、それ以外に関してはサイトごとそれぞれにCGIスクリプトが置かれている。 中身は「ほとんど同じ」であるものもあるが、それぞれサイト固有のパラメータが埋め込まれているほか、一部仕様も異なる。 例えば、コメントシステムはモデレーションについての言及があるが、モデレートされるサイトのアプリにだけモデレーションの機能があり、モデレートされないChienomiのコメントシステムはモデレーションの機能自体を持っていない。

このようにサイトごとにカスタマイズされており、ChienomiのCGIをサーバー化したということは、そのままだとサイトが増えればそれだけサーバーを増やす必要があるということを意味する。 それは不毛なので、汎化した。

これはそれほど難しいことではない。例えば、このようになっている。

get '/apps/basic/:site/plusone' do
  p1 = PlusOne.new request, params["site"]
  p1.p1
end

このsiteというパラメータはサイトを識別するIDとして広く使われる。 これは、設定ファイル(連想配列)のキーであったり、ファイル名の一部分であったりする。

そのように使うこと自体は容易だが、そのような使い方をするということはセキュリティリスクになる。 だから、そもそも意図した文字列以外で呼び出せないようにしなくてはならない。

location /apps/basic/chienomi {
  #...
}

これによりサイト固有の値を作り、それをアプリケーションに渡せるようになった。 その値が安全であることも分かっているので、設定ファイルを読み込むなり、固有のデータベースファイルへ書き込むなり自由だ。

これによって抽象化が可能になり、複数のサイトを共有させられるようになった。

第三段階

第三段階はコメントのみに対するものだ。 前述のように、アプリケーションの機能自体に差異がある。 大部分の違いはログ形式が異なるというようなことだが、それらはいずれにせよ合わせるしかないものだ。

だが、Chienomiに合わせた関係で、コメントにはモデレーション機能がない。 そもそもコメント機能はメンテナンスしづらい(管理者投稿や削除がかなりめんどくさい)ものなので、そこもついでに改善する。

従来、コメントシステムはMimir Yokohamaのものを除き、DBMに保存するものであった。 これは保存における様々な面倒を避けるためだが、実はRubyのバージョンの違いでDBMの形式も変わるようになっていたりもする。

そこでファイルにYAMLで保存するように変更した。 従来、URLパスがキーであったが、それをそのままファイル名にすると脆弱性になる。 そこで、パスをSHA2で変換するようにした。アクセスできる対象は秘匿されていないし、実在しないパスにアクセスされたとしても特に困るようなものではないので、この単純な処理でも危険性はない。

ただし、キーがSHA2のヘックスダイジェストになると、ファイル名からページを推測することは困難になる。 そこで従来、

[*comments]

という構造だったが、

{
  "page" => page,
  "comments =>" [*comments]
}

という構造に変更した。 これで、必要であればgrepで探せる。

加えて、commentsdeletedpendingという属性を追加し、これらが真であるものは除外するようにした。 削除はYAMLファイルを編集してdeleted: yesとするだけでいいし、モデレートする場合はpendingtrueであるものを探し、pending: noにした上で、必要であればdeleted: yesとすれば良い。

YAMLファイルは人の手で編集することが容易なので、プログラム側でのインターフェイスが特に必要ないのはメリットだ。 あまり激しく変更される場合は手動編集で競合排他するのが難しいが、うまく分離できれば考えなくて良いレベルになる。

これで機能面で統一できない部分を解消し、なおかつ従来課題だった部分を改善できた。

日常的プログラミングにおける心得

こうしたことは日常的なプログラミングではよくあることだ。

日常的なプログラミングでは明確、かつ特定の目的がある。 だから、それを達成するものを書くことになる。 それは、コマンドラインで打ったときにtypoになりそうな長いコマンドであるからくらいの理由であるシェルスクリプトであるかもしれない。 Zshはマルチライン編集が可能だが、BashだとできないのでBashだとよりよくあることだ。

それをもう少し使えるものにすることがある。 その一回の目的を達成するものだったが、それと同じ、あるいは似たような機能を必要として使いまわすことが発生するようなケースだ。

このような時には変数や引数が登場する。 プログラムとしても、プログラマの成長としても、望ましい発展だ。

やがて、成長したプログラムを公開するときがやってくる。 そのときには、安全性や汎用性なども含めた上てで仕様が適切かを検討し、品質を上げる必要があるだろう。 もともと想定していなかった状況のことも考える必要がある。 また、現状ではあまり使わないから問題ないというようなものも、より使うのであれば問題が発生するかもしれないので、その意味でも想定の幅を広げなくてはいけない。

日常的なツールの初期段階という意味でも、リトルプログラマの初期段階という意味でも、洗練されていない簡単なプログラムからスタートするのは正しいことであり、そこから必要であれば汎用性を向上させ、そして公開するのであれば安全に作るというのは「よくあること」でも「正しい過程」でもあり、恐れる必要はない。 そのように成長を遂げるプログラムは栄誉なのだ。

もっとも、webアプリケーションに関しては、最初から様々な状況を想定してセキュアに作る必要はあるが。