k8sのreadOnlyRootFilesystemを有効にして、Rackアプリケーションでファイルアップロードをするための設定
kubernetesにはreadOnlyRootFilesystemというオプションがあり、このオプションを有効にするとコンテナ内がreadonlyになるためセキュリティの施策の1つとして有効にされることがあります。その状態でRack(Rails,Sinatra,etc)アプリケーションからファイルアップロードを行うときに思ったより手間取ったのでそのときの対応メモ。
Rubyのバージョンは2.6。kuberntesはGKEを利用していて、1.11.7-gke.6でした。
前提とエラーの内容
WebアプリケーションでPOSTのファイルアップロードを受け付けるにはディスクに一時ファイルを書き込みできる必要があります。readOnlyRootFilesystemを有効にした状態でファイル書き込みを許可する場合には、k8sのemptyDirを作成し、それをマウントするのが一般的に取られる設定です。(おそらくですが)
以下のようなyamlになります。
containers: image: rubyapp name: app securityContext: readOnlyRootFilesystem: true volumeMounts: - name: tmp mountPath: /tmp volumes: - name: tmp emptyDir: {}
nginxのpid書き込みなど、この設定で問題ないのですが、Rackの場合はパーミッションエラーがありうまくいきませんでした。
エラーメッセージをよく見てみると一時ファイルを書き込みしようとしてるディレクトリがなぜかRubyを実行しているカレントディクトリになっています。Rubyの問題のようなので調べて見ると、Rackがファイルアップロード時の一時ファイルを保存するのは Dir.tmpdir で得られるディレクトリということでした。Dir.tmpdirを実行してみたところ、たしかにカレントディレクトリが取得できました。ENV['TMPDIR']を"/tmp"にしても変わらずカレントディレクトリのままでした。
原因
そこで Rubyのソースコードを確認してみたところ、原因がわかりました。Dir.tmpdirは以下のようになっています
def self.tmpdir if $SAFE > 0 @@systmpdir.dup else tmp = nil [ENV['TMPDIR'], ENV['TMP'], ENV['TEMP'], @@systmpdir, '/tmp', '.'].each do |dir| next if !dir dir = File.expand_path(dir) if stat = File.stat(dir) and stat.directory? and stat.writable? and (!stat.world_writable? or stat.sticky?) tmp = dir break end rescue nil end raise ArgumentError, "could not find a temporary directory" unless tmp tmp end end
https://github.com/ruby/ruby/blob/1fae154c07b957278fd336b54256d5c57f21e0d5/lib/tmpdir.rb#L26
問題はこの条件文です。
(!stat.world_writable? or stat.sticky?)
tmpdir候補のディレクトリはこのチェックを突破しないとtmpdirになることができないようです。VolumeMountされたディレクトリを確認したところ、world_writable?はtrueかつ、stickyビットは立っていないことが確認できたので、これが原因で間違いなさそうです。
対応
VolumeMountするディレクトリのパーミッションを変更する方法を調べたのですが見つかりませんでした。
Rubyはどのユーザーでも書き込みできるディレクトリの場合はstickyビットを求めているのに対し、k8sはコンテナ前提なので複数のユーザーがいることはあまり想定していないのでしょう。
仕方ないので、RubyのDir.tmpdirを書き換えてしまいます
class Dir class << self alias org_tmpdir tmpdir end def self.tmpdir dir = ENV['TMPDIR'] return File.expand_path(dir) unless dir.nil? org_tmpdir end end
manifestsは以下のようにします。
containers: - image: rubyapp name: app env: - name: TMPDIR value: /tmp securityContext: readOnlyRootFilesystem: true volumeMounts: - name: tmp mountPath: /tmp volumes: - name: tmp emptyDir: {}
これで RubyのDir.tmpdirが"/tmp"になり、/tmpには書き込みができるのでファイルアップロードができるようになりました。