Chienomi

ConoHa WING上で最新のRuby (Ruby3.0) を使う

Live With Linux::server

ご挨拶

Hi, there!! はるかみです!!

ConoHa Advent Calendarも2017年から書き始め、早いものでもう5回目となる。

これまで

  • 2017 VPSをビジネスに使う話
  • 2018 Arch Linuxの入門記事
  • 2019 WINGの紹介と活用
  • 2020 VPSをSSHプロキシとして使う話

と執筆してきたが、今回は2019年以来となるWINGの話になる。

去年あたりからConoHaは方向性として完全に技術から離れていて、私としては縁遠い活動となっているため、 場違いなのではないか、もうConoHa関連の執筆はやめようかと思っていたのだが、夏頃に今回の記事の内容をやって、結構面白いものになったので続投となった。

まえがき

2020年までの4記事は基本的に初心者向けのものであったが、今回はこれまでよりは少し難しい内容になる。

内容としてはLinuxのシステム、SSH、Linux名前空間周りの知識(Chrootやcgroupsを含む)を伴うものになっている。 これらの知識がなくても、シンプルに(自分の環境に置き換えた上で)模倣することで目的を達成することはできるが、きちんと理解したい場合はnamespaces(7), unshare(1), cgroups(7), mount(8), PROC(5), sshfs(1), rsync(1)のmanpageを読み、SSHを理解することで読み解くことができるが、本記事を読む上で必要というものではない。

また、本作業はLinuxで行っており、非Linuxでは大幅に難しいものとなる。 実施する場合は一時的でもLinux環境を用意したほうが良いだろう。VMでも構わない。

ConoHa WINGのRubyのバージョンは2.5.3である。 これは、既にEOLとなったRuby 2.5系であり、2.5系最終バージョンは2020年にリリースされた2.5.9だが、2.5.3は2018年リリースである。 Rubyは安定して脆弱性修正が続けられているが、別の言い方をするとメンテナンスしないRubyは脆弱である。 例えば2.5.8ではJSONライブラリに潜んでいた割と大きな危険性を除去しており、これが含まれないのはかなり痛い。 また、EOLである2.5に関してはこれから大きな脆弱性を報告されたとしても修正されない。

Rubyの更新は繰り返しConoHaに対して求めているが、反応が全くないため、自前で現在生きているバージョンを用意しようと考えた。

ConoHa WINGのRubyはお点前ビルドなものになっており、Ruby2.0が/opt/alt/ruby20に、Ruby2.5が/opt/alt/ruby25にある。 だが、いずれも既にOut-of-Dateであり、脆弱性を含む。そして、全くメンテナンスされていない。

また、この関係でWINGのRubyは/usr/local/bin/rubyにあり、これがシンボリックリンクになっている。

このように処理系が脆弱な古いバージョンのまま放置される、というのは大きな問題だが、WINGに限った問題ではない。 レンタルサーバーではあるあるであり、実際、さくらのレンタルサーバーでも同様のことがあった。 だが、さくらと大きく違うのが、rbenvを使う方法がとれない、ということである。

また、WINGはユーザー単位のコンテナになっており、ファイルシステムはそのユーザー専用のものとして見えるようになっている。 ファイルシステムツリーは最小限のrootファイルとユーザーディレクトリのみを含む。

なお、この記事はx86_64 Linux上での実施を前提として考えている。 MacやFreeBSD等のユーザーはクロスビルドについて考えなくてはいけないためより複雑な問題をはらむ。 さらに、Windowsであればより多くの問題を含むだろう。 ConoHa WINGと同一プラットフォームでない場合、仮想マシン(WSLでも良い)を用いるのが簡単だろう。

うまくいかない方法

ruby-build

ruby-buildを使ってRuby処理系をインストールするのは順当な方法だ。 誤解のないように説明すると、これはrbenvを使う話をするのと同じことだ。 rbenvは通常、Ruby処理系の導入にruby-buildを使うためだ。

だが、WING上でruby-buildを使うのはうまく行かない。 WING自体には潤沢にメモリがあるのだが、かなり厳しいメモリクォータがかかっているようで

compiling ruby.c
compiling scheduler.c
compiling signal.c
compiling st.c
compiling sprintf.c
make: vfork: メモリを確保できません
make: vfork: メモリを確保できません
make: vfork: メモリを確保できません
make: vfork: メモリを確保できません
make: vfork: メモリを確保できません

のようにビルドはコケる。

SSHFS + chroot

ハードウェアリソース的にビルドできないのであれば、一番簡単な方法はそのファイルシステムをマウントした上でchrootし、そしてビルドすることである。

# sshfs wing:/ /mnt
# chroot /mnt

だが、実際にやってみると、「名前解決できない」という問題が発生する。 /etc/resolv.conf

nameserver 172.16.45.198
nameserver 172.16.45.199

となっているが、どうもこのネームサーバーが外部からのリクエストを受け付けないようだ。

これを書き換えられればいいのだが、残念ながら権限がない。

なお、ここでSSHFSの原理を理解していないと混乱するだろう。

SSHFSは通常のファイルシステム操作のシステムコールを受け取り、それに対して裏でSFTPによるファイル操作を行う。 そのため、SSHFS上ではローカルなUIDが見えるが、実際に操作しているのはSSHセッションのユーザーである。 この場合、chrootした時点でWINGのファイルシステム上でrootを取っているように見えるが、これはローカルシステム上のrootであり、WINGのrootではない。 そして、ファイル操作はWINGのユーザーとして行われる。 だから、rootとしてのファイル操作(例えばchown)やrootのファイルを作る、root所有のファイルを変更する、といったことはシステムコールに対してSSHFSが失敗を返すため、できない。 (実際にファイルシステムを操作しているのはrootではないからだ)

ローカルの/etc/resolv.confで置き換えることができれば良いのだが、対ファイルでのbindはできない。

ローカルビルド

単純な方法として、ローカルで(ruby-buildを使って)ビルドしたものをコピーする、という方法がある。

この方法はもちろん、同一環境であれば上手く行く。しかし、私の手元の環境はManjaro Linuxであり、ConoHa WINGで採用されているRedHat Enterprise Linux 7とは乖離がかなり大きい。 実際、ライブラリ構成が異なるため、ビルド時にリンクされるライブラリがWING上にはないという問題が発生する。 Ruby全体ではかなり多くのライブラリを使用するため、異環境でビルドしたRubyを持ち込むのは難しいところだ。

unshare + chroot

本筋ではないのだが、できればchrootした先でWINGユーザーになれると嬉しい。 WINGユーザー自体はidして同一uidのユーザーをローカルにも作ればファイル操作に対してローカルと整合性がとれるようになる。

「システムユーザーとしてはrootだが、プロセスでは別ユーザーに見える」はunshareを使えばできる。 例えばUIDが10000だとすると

unshare -U --map-user=10000

とすればプロセス的にはUID 10000なbashが立ち上がる。

だが、これとchrootは両立しない。 unshareでのコマンド実行はunshareを行った後で実行される。--map-userしてしまうと、そのプロセス上ではrootではない状態になっているので、chrootが実行できない。 sudoなどを用いてchrootするという方法はあるが、今回の場合chroot先にsudosuなどが一切ないため、今度は--map-userした意味がなくなってしまう。

解決策

ビルド

まず、WINGのファイルツリーをもらってくる。 今回の場合、WING上のファイルを書き換えたいわけではなく、あくまでWING上で利用できるファイル群を作ることが目的なので、WINGのファイルシステムを再現できれば良い。

# rsync -xrv --links wing:/ /wing

chrootするroot所有のファイルが必要になるため、rootとしてやっている。 いくつかのファイルは失敗するが、必要なファイルはすべて転送可能だ。

続いてresolv.confを(手元ホストから利用可能なDNSサーバーに)書き換える。

# vim /wing/etc/resolv.conf

多分devは必要。recursiveはいらないはず。(実際はdevとprocをrbindした)

# mount --bind /dev /wing/dev

さぁ、chrootしよう。 rootの設定ファイルはないため、少し使いづらくなる。

# chroot /wing

ユーザーディレクトリに移動し、ruby-buildを用意する。

# cd home/u1000000
# git clone git://github.com/rbenv/ruby-build.git

ビルドの用意をしてビルド。

# mkdir ruby3
# cd ruby-build
# bin/ruby-build 3.0.2 /home/u1000000/ruby3

WING上への反映

まずはビルドしたRubyを転送する。 --linksしないとライブラリへのシンボリックリンクが欠けるので注意。

# rsync -rv --links /wing/home/u1000000/ruby3 wing:.

動作を確認しよう。

$ ssh wing
@wing$ export export LD_LIBRARY_PATH=$HOME/ruby3/lib:$LD_LIBRARY_PATH
@wing$ ruby3/bin/ruby --version
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]
@wing$ ruby --version
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux]

WING上でCGIとして動作させたときには、環境変数が色々足りない。 そこで、~/bin/rubyにラッパーを作っておく。ちなみに、これはBashスクリプト。

#!/bin/bash

export HOME=/home/u1000000
export PATH=$HOME/ruby3/bin:$PATH
export LD_LIBRARY_PATH=$HOME/ruby3/lib:$LD_LIBRARY_PATH
export RUBYPATH=$HOME/ruby3
export RUBYLIB=$HOME/ruby3/lib:ruby3/lib/ruby/3.0.0
export GEM_PATH=$HOME/ruby3/lib/ruby/gems/3.0.0
export GEM_HOME=$HOME/.gems3

exec $HOME/ruby3/bin/ruby "$@"

同様に~/bin/gemも作っておく。

#!/bin/bash

export HOME=/home/u1000000
export PATH=$HOME/ruby3/bin:$PATH
export LD_LIBRARY_PATH=$HOME/ruby3/lib:$LD_LIBRARY_PATH
export RUBYPATH=$HOME/ruby3
export RUBYLIB=$HOME/ruby3/lib:ruby3/lib/ruby/3.0.0
export GEM_PATH=$HOME/ruby3/lib/ruby/gems/3.0.0
export GEM_HOME=$HOME/.gems3

exec $HOME/ruby3/bin/gem "$@"

両方executableに

$ chmod 755 ~/bin/ruby ~/bin/gem

WINGにおけるRubyGemsの問題も一緒に解決されるので、よりパワフルなCGIを書くことができる。

試してみる

GemライブラリとしてOjをインストールする。

$ ~/bin/gem install oj

そしたらCGIを書く。

#!/home/u1000000/bin/ruby

require 'oj'

obj = Oj.dump({"greeting" => "Hi This is #{RUBY_VERSION}"})

print <<EOF
Content-Type: application/json

#{obj}
EOF

アクセスするとうまくいくだろう。

{"greeting":"Hi This is 3.0.2"}

結び

レンタルサーバー、CGIと聞くとそれだけでバカにしたような発言をする人もいる。 また、レンタルサーバー側でもWordPressなどごく一部のアプリケーションを動作させることが主眼にあり、「それさえ動けば良い」という考え方がなされることが多い。 実際、WINGにしてもブロガー・インフルエンサーによるプロモーションという考え方をしているようで、ウェイトとしてもWordPressに非常に大きく偏っている。

このあたりは火種なのだが、以前ConoHaにLiteSpeedについて問い合わせをしたことがあり、ここでそもそもLiteSpeedがなんなのかConoHa側で理解していない、という問題が明らかになった。 もちろん、導入する以上は 構築した人間はそれがなにかを分かっているはずだが、プロモーションとしては不適切なものになっている(多分、裁判になったらまずい類の問題だ。実際に得られる価値範囲を逸脱して記述されているため)。

これは、WordPressの高速性をアドバンテージとして謳うためにLiteSpeedを入れているが、それ以外の点においては特に配慮をしていない、あるいはもはや「PHPアプリケーション=WordPress」ぐらいの言い方なのだろう。だから、世間的に言ってもレンタルサーバーでアプリケーションを使うのは「前時代的な今では考えられない行為」であり、レンタルサーバーというのはブロガーがWordPressのために使うものだ、という見方がされていると言っていい。

だが、実際のところ、現代においてこそその有用性は拡大している。

CGIを用いる最大の問題はプロセス起動のためのコストであり、非常に多くのリクエストをした場合にリクエストがさばききれなくなる、ということだ。 加えて言うなら、状態を共有したい場合にわざわざプロセス間通信、あるいはファイルへの書き出しとロックを必要とし、そうしたアプリケーションではさらにコストが高い。 このためにアプリケーションサーバー形式が好ましいとされてきた。

しかし、「そのサイトにはそこまで大量のリクエストがあるのか」というのが第一の疑問である。常に同時接続があるような環境でない限り、そもそもCGIのコスト問題というのは大した話ではない。 また、そんなにしょっちゅうリクエストを生成するようなアプリケーションもそれはそれで時代遅れだ。モダンなアプリケーションは計算処理の主たる部分をクライアントブラウザ内で行い、APIサーバーに対して最小限のリクエストを投げるというものになるはずだ。それによってサーバーサイドコストは下がり、CGIでもまかないやすくなっているはずだ。

Rubyに関しては、「Rackが今の標準、CGIなんか古い」という人もいるが、Rackはアプリケーションとサーバーとの接続の間にあるものなので、RackのCGIハンドラ、通称Rack/CGIが存在するし、実際に私がWINGで動かしているCGIスクリプトはRack/CGIになっている。フレームワークのSinatraもRackを使う関係で、Rack/CGIを用いてCGIによる利用が可能。 Rack/LiteSpeedが使えればレンタルサーバーの概念を覆す強力なプラットフォームとして利用できたので非常に残念だが、さくっとアプリケーションを生やすにはちょうどいい。

だが、そこで障害になるのがRuby処理系そのものである。 Ruby 2.5.3のままメンテナンスもされていない状態で強力なアプリケーションも何もない。逆にいえば、処理系やライブラリがちゃんとしていれば、たとえプロトコルがCGIであっても強力なアプリケーションを動かすことが可能だ。

今回、WINGに最新のRuby処理系を導入できたことで、WINGの利用可能性は大きく広がったとみていいだろう。 ついでにGemの利用もしやすくなったので、その意味でも強力なアプリケーションを作りやすくなった。

なお、今あなたが見ているこの記事は、静的ファイルとしてサーブされている。 PureBuilder Simplyを用いて事前にビルドされているからだ。 こうした工夫や発想の転換も、レンタルサーバーの可能性を広げる部分となるだろう。