Ubuntu 22.04でホームディレクトリをNFSで共有している環境でrubygems.rbがうまく動作しなかった

# はじめに

Xubuntuデスクトップを利用している環境ではマルチユーザーを前提としているため、ホームディレクトリをNFSで共有して、適宜autofsでマウントしています。

少し前に22.04にアップグレードして順調に稼動していましたが、RubyでSinatraアプリケーションを構築しようとした時に、NFS上に作業用ディレクトリを置いていたことが原因で問題が発生しました。

エラーからすぐにNFS環境が原因であることは分ったのですが、ローカルに作業用ディレクトリを準備することは適切な対応ではなかったため、根本的に対処する必要がありました。

Ubuntu 22.04ではパッケージ版のRubyが3.0系列で、rubygems.rbのバージョンが古いことが根本的な原因でした。またSnapで最新版が提供されていますが、NFS上へのアクセスがネットワークアクセスとなってしまうため対応が難しい点もあり、コンパイルしたバージョンを展開することにしました。

この顛末についてまとめておきます。

## 検証用環境の作り方

VMWare Workstation Pro上でUbuntu 22.04のイメージをcloneして作業した時のメモです。

前提として追加したNFS領域用のディスクを/dev/sdbとして認識しています。

```bash:検証環境でNFS領域を準備するための作業
## パッケージの導入
$ sudo apt install nfs-common nfs-kernel-server

## /dev/sdb1の準備
$ sudo fdisk /dev/sdb
$ sudo mkfs.ext4 /dev/sdb1
$ udevadm info -q all -n /dev/sdb1 | grep 'S: disk/by-uuid' | awk -F': ' '{print $2}'

## mount /dev/sdb1
$ sudo mkdir -p /export/nfs
$ sudo mount /dev/sdb1 /export/nfs
$ sudo chmod 1777 /export/nfs

## export nfs and mount to /work
$ sudo mkdir /work
$ echo "/export/nfs 127.0.0.1/32(rw,async,no_root_squash)" | sudo tee -a /etc/exports
$ sudo systemctl restart nfs-server rpcbind
$ sudo mount 127.0.0.1:/export/nfs /work
```

以降の記録は実機で出力させたものと仮想環境でも確認したものが混在しています。

# エラーの状況

パッケージ版のRuby 3.0を利用します。

```bash:
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.3 LTS
Release:        22.04
Codename:       jammy

$ ruby -v
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux-gnu]
```

NFS領域に適当な作業用ディレクトリを準備し、Gemfileを準備します。

```ruby:Gemfile
source 'https://rubygems.org'

gem "webrick"
gem "sinatra"
gem "sinatra-cross_origin"
```

次にbundleコマンドで関連するライブラリを"./lib"ディレクトリにインストールします。

```bash:
$ bundle config set path lib
$ bundle install
```

次のようなエラーメッセージが得られます。

```text:
Fetching gem metadata from https://rubygems.org/....                                                                     
--- ERROR REPORT TEMPLATE -------------------------------------------------------                            
Errno::EBADF: Bad file descriptor @ rb_file_flock - /nfs/home/yasu/work/lib/ruby/3.0.0/specifications/webrick-1.8.1.gemspec

  /usr/lib/ruby/vendor_ruby/rubygems.rb:786:in `flock'
  /usr/lib/ruby/vendor_ruby/rubygems.rb:786:in `block in open_with_flock'
  /usr/lib/ruby/vendor_ruby/rubygems.rb:783:in `open'
  /usr/lib/ruby/vendor_ruby/rubygems.rb:783:in `open_with_flock'  
  ...
```

# エラーが発生した背景

Ubuntu 20.04でのrubyパッケージは2.7でしたが、22.04では3.0系列がパッケージとなっています。

同梱されているrubygems.rbファイルを比較すると、以前はなかったflockを呼び出すロジックが含まれるようになりました。

一応コードはNFSの存在も意識していて次のようになっています。

```ruby:/usr/lib/ruby/vendor_ruby/rubygems.rbより抜粋
Open a file with given flags, and protect access with flock

  def self.open_with_flock(path, flags, &block)
    File.open(path, flags) do |io|
      if !java_platform? && !solaris_platform?
        begin
          io.flock(File::LOCK_EX)
        rescue Errno::ENOSYS, Errno::ENOTSUP
        end
      end
      yield io
    end
  rescue Errno::ENOLCK # NFS
    if Thread.main != Thread.current
      raise
    else
      File.open(path, flags) do |io|
        yield io
      end
    end
  end
```

エラーメッセージの冒頭にあったように、実際には**Errno::EBADF:**が発生しているため、コードがうまく例外を補足できていません。

Ubuntuで``man 2 flock``をみると、ENLOCKはメモリ不足に起因するエラーとなっているのでコメントのようにNFSに対処するためには不十分のようにも感じられます。

試しに次のようにコードを変更すると問題なく動作します。

```diff:
--- /usr/lib/ruby/vendor_ruby/rubygems.rb.orig  2023-12-14 01:51:13.842516042 +0000
+++ /usr/lib/ruby/vendor_ruby/rubygems.rb       2023-12-14 01:52:11.259535632 +0000
@@ -789,7 +789,7 @@
       end
       yield io
     end
-  rescue Errno::ENOLCK # NFS
+  rescue Errno::ENOLCK, Errno::EBADF # NFS
     if Thread.main != Thread.current
       raise
     else
```

GitHub上でissuesを検索すると、次のマージリクエストが確認できて既に問題は修正されているようにみえます。

* [Avoid flock on non Windows systems, since it causing issues on NFS file systems #5278](https://github.com/rubygems/rubygems/pull/5278)

ただ#5278の変更されたコード部分は本質的には変更されていないようにみえるので、念のため最新の安定板である3.2.2をコンパイルして確認します。

なおRubyのコンパイルをNFS領域で実施すると失敗するため、ext4なローカルディスク上で作業をしています。

```bash:ubuntu22.04でのruby-3.2.2のコンパイル\
## build-depでruby-3.0のコンパイルに必要なパッケージをインストール
$ sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list
$ sudo apt update
$ sudo apt build-dep ruby ruby-dev ruby-rubygems ruby-bundler
## ruby-3.2をコンパイルするために不足するパッケージを追加
$ sudo apt install zlib1g-dev libssl-dev libyaml-dev libffi-dev libreadline-dev bison
## ソースコードをダウンロードし、コンパイル+インストール
$ wget https://cache.ruby-lang.org/pub/ruby/3.2/ruby-3.2.2.tar.gz
$ tar xvzf ruby-3.2.2.tar.gz
$ cd ruby-3.2.2
$ ./configure --prefix=/usr/local/stow/ruby-3.2.2 && make && sudo make install

## xstowを使っていれば/usr/local/bin
$ cd /usr/local/stow
$ sudo xstow ruby-3.2.2
```

ruby-3.2のrubygems.rbファイルは#5278の修正が実施された後のバージョンでした。

この状態で確認すると再現することなく、無事に処理を進めることができます。

# 対応策の検討

以下の理由から別の環境でビルドしたファイルを各ノードに展開することにしました。

## ホームディレクトリをNFSで共有している場合の制限事項

ホームディレクトリにNFSを利用している場合には、snapを利用することはできません。

docker/podmanの利用は問題なく、NFS領域もvolumeとしてマウントして利用することが可能ですが、snapを利用する際にはNFS領域へのアクセスがネットワーク通信して禁止されています。

* [参考情報 - SnapパッケージをNFS上で起動する場合はAppArmorに注意](https://sh2.hatenablog.jp/entry/2023/01/24/231716)

そのため解決策としてruby-3.2を利用しようとする場合には、その方法が問題になります。

PPAで対応したパッケージが利用できれば良いのですが、snapで最新版が提供されていることなどもあって利用可能なパッケージは発見できませんでした。

そのため仮想環境でbuildしたrubyを/usr/local/stow/以下に配布して、xstowコマンドで/usr/local/{bin,include,lib,share}に展開することとしました。

``sudo make install``でインストールした後に/usr/local/stow/ruby-3.2.2ディレクトリをtar.gzでまとめてから、ansibleを利用してtar.gzファイルの配布と展開、xstowコマンドによる/usr/local/bin等への展開を行っています。

各クライアントはansibleで管理しているため必要な共有ライブラリは導入していますが、念のため各クライアントに展開してから共有ライブラリが十分であるかを簡単に確認しておきます。

```bash:
$ ldd /usr/local/bin/ruby
        linux-vdso.so.1 (0x00007fff199b5000)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f675ede4000)
        libgmp.so.10 => /lib/x86_64-linux-gnu/libgmp.so.10 (0x00007f675ed62000)
        libcrypt.so.1 => /lib/x86_64-linux-gnu/libcrypt.so.1 (0x00007f675ed28000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f675ec41000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f675ea00000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f675f277000)

## 各.soファイルの参照を確認
$ find /usr/local/stow/ruby-3.2.2 -name '*.so' -exec ldd {} \; | sort | uniq 
    ...
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff17c400000)
        libffi.so.8 => /lib/x86_64-linux-gnu/libffi.so.8 (0x00007f204653a000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fac41b5b000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fd436f9a000)
        libreadline.so.8 => /lib/x86_64-linux-gnu/libreadline.so.8 (0x00007fab713cc000)
        libssl.so.3 => /lib/x86_64-linux-gnu/libssl.so.3 (0x00007f1d74d57000)
        libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007fab7139a000)
        libyaml-0.so.2 => /lib/x86_64-linux-gnu/libyaml-0.so.2 (0x00007f298a3d8000)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f7af7042000)
    ...
```

実際はansibleのタスクで全ノード上で実施して解決できない依存関係がないか確認しています。

# さいごに

マルチユーザー環境であれば、ホームディレクトリをNFSで共有するようなことはめずらしくないと思うのですが、snapが標準では利用できないことは少し残念です。

一応、設定を変更してホームディレクトリをローカルディスク上の領域を向くように変更すれば問題ないのですが、/run/user領域などを使って動作するように出来れば便利だろうなと思います。

xstowはいまとなっては古いユーティリティですが、/usr/local以下にアプリケーションをインストールして管理するツールとしては便利なので独自にコンパイルしたアプリケーションを展開する際には使ってみてください。