AWS Lambda 上で Net::HTTP::Post による multipart Upload が失敗する件
問題
AWS Lambda 上のRubyの関数経由で API を叩くプログラムが以下のエラーで落ちた
system temporary path is world-writable: /tmp
/tmp is world-writable: /tmp
. is not writable: /var/task
Error: could not find a temporary directory
Error の前のメッセージは warning っぽい
コードはおよそ以下のようになっていた
require 'net/http'
def send_attachment(attachment)
post = Net::HTTP::Post.new(uri)
attachment_body = attachment.body.decoded.force_encoding('ASCII-8BIT')
form_data = [
['file', StringIO.new(attachment_body), { filename: attachment.filename, content_type: attachment.mime_type }]
]
post.set_form form_data, 'multipart/form-data'
Net::HTTP.start('example.com', 80, use_ssl: true) do |http|
http.request(post)
end
end
環境
- Lambda Runtime
- Ruby 3.3
- Architecture
- x86_64
原因
- Ruby 側で
/tmp
をテンポラリディレクトリとして利用しようとしているが、ディレクトリのパーミッションが world-writable [1] である
ワークアラウンド
方針
- 独自にディレクトリを設定して、wold-writable ではない パーミッションを設定する
例
例えば以下のように /tmp
配下に private-tmp
というディレクトリを作って chmod
でパーミッションの設定を行う
# 専用の temp ディレクトリを作っておく
PRIVATE_TMP = '/tmp/private-tmp'
ENV['TMPDIR'] = PRIVATE_TMP
if Dir.exist?(PRIVATE_TMP)
puts "#{PRIVATE_TMP} already exist"
else
FileUtils.mkdir(PRIVATE_TMP, mode: 0700)
end
def lambda_handler(event:, context:)
# `TMPDIR` 環境変数を利用するので問題なし
p Dir.tmpdir
end
-
他のユーザ・他のグループから読み(r)/書き(w)/実行(x)可能で、かつ、sticky bit が立っていない状態を表しているらしい ↩︎
以下は調査の経緯
エラーメッセージについて調べる
以下によると、セキュリティ観点から temporary ファイルのパーミッションをチェックして、適切ではないパーミッションが設定された場所に temporary ファイルが作成できないようになったらしい。
コンテナについて調べる
AWS Lambda の Ruby ランタイムのコンテナがどのような設定になっているか調べる
上記リポジトリを clone して、以下で Ruby ランタイムのブランチに切り替え
git checkout ruby3.3
以下でコンテナイメージをビルド
docker build -t ruby3.3:local -f Dockerfile.ruby3.3 .
ビルド前に Git LFS のセットアップが必要だった
インストール
brew install git-lfs
git lfs install
Git LFS でファイルをローカルにダウンロードする
git lsf pull
ダウンロードしたファイルの中身も確認しておく
tar -tf x86_64/0767a5530547b0a2f1305ecdf29d2238c9dba99bfcda97a7d589dfdef3c1bd16.tar.xz | less
Ruby 関連のコマンドやライブラリなどが含まれている
var/lang
var/lang/bin
var/lang/bin/bundle
var/lang/bin/bundle.lock
var/lang/bin/bundler
var/lang/bin/bundler.lock
var/lang/bin/erb
var/lang/bin/gem
var/lang/bin/irb
var/lang/bin/racc
var/lang/bin/racc.lock
var/lang/bin/rake
var/lang/bin/rake.lock
var/lang/bin/rbs
var/lang/bin/rbs.lock
var/lang/bin/rdbg
var/lang/bin/rdbg.lock
var/lang/bin/rdoc
var/lang/bin/ri
var/lang/bin/ruby
... (略) ...
tar -tf x86_64/4a1a18ec974289442c220349379879e0be174492cd21be651168f2efd7f6da58.tar.xz | less
こっちは Gem
var/runtime/extensions
var/runtime/gems
var/runtime/gems/aws_lambda_ric-3.0.0
var/runtime/gems/aws_lambda_ric-3.0.0/Gemfile
var/runtime/gems/aws_lambda_ric-3.0.0/LICENSE
var/runtime/gems/aws_lambda_ric-3.0.0/NOTICE
var/runtime/gems/aws_lambda_ric-3.0.0/README.md
var/runtime/gems/aws_lambda_ric-3.0.0/aws_lambda_ric.gemspec
... (略) ...
tar -tf x86_64/723857be1338657a062a5b40f529ea594c12dcfe3e50c2b10a00c611012f910b.tar.xz
entrypoint の shell script
tar: Archive entry has empty or unreadable filename ... skipping.
lambda-entrypoint.sh
tar: Error exit delayed from previous errors.
ワーニングらしきメッセージはよくわからない
tar -tf x86_64/afa9570d8e88fff239a8db4b528a895f8485cc6868351b6d92df684fb821a2db.tar.xz
ライセンス関連?
tar: Archive entry has empty or unreadable filename ... skipping.
THIRD-PARTY-LICENSES.txt
tar: Error exit delayed from previous errors.
tar -tf x86_64/e4795398be36b75eb725e1e5505fafe2fa7910af23f745f97b5b083fc9aa1838.tar.xz
usr/local/bin
のなにか
tar: Archive entry has empty or unreadable filename ... skipping.
usr
usr/local
usr/local/bin
usr/local/bin/aws-lambda-rie
tar: Error exit delayed from previous errors.
途中で出力が止まってるのかも?
tar -tf x86_64/e58e411439bf2f8971d86988b770e3611f50d9c7179720908bea0efafbce3688.tar.xz | less
/usr/bin
配下のLinux標準コマンド群 sh
や ls
普通にあるな
./usr/bin/ls
./usr/bin/sh
/tmp
ディレクトリの状態を調べる
コンテナの entrypoint を迂回して sh
でコンテナの中を確認する
$ docker run --rm -it --entrypoint '' ruby3.3:local ls -l /
... (略) ...
drwxrwxrwt 2 root root 4096 Jun 28 01:01 tmp
sticky bit は問題なさげ
stat
コマンドでも
$ docker run --rm -it --entrypoint '' ruby3.3:local stat -c %a /tmp
1777
問題なさそう
だとすると... tmpdir で warn "#{name} is world-writable: #{dir}"
このワーニングが出る理由がわからない... 🤔
完全に想像だけど、コンテナランタイムによって tempfs の挙動が違うということがあるかもしれない。
と、以下の runc の Issue を見て思った。
もしそうであれば、ローカル (Docker) では sticky bit 立っているように見えるが、AWS Lambda で実行している際には sticky bit が立っていないように振る舞う(まだ確認していないのでこのような表現になっている)という現象に説明はつく。
AWS Lambda のコンテナ実行環境について調べる
確か Firecracker というキーワードを聞いた記憶(うろおぼえ)
AWS Lambda 実行環境を確認する
AWS Lambda で以下のような関数を実行してコンテナから見た /tmp
の様子を確認する
def lambda_handler(event:, context:)
puts "#### Running Lambda ####"
puts `ls -l /`
{ statusCode: 200, body: JSON.generate('Hello from Lambda!') }
end
実行結果
total 76
-rw-r--r-- 1 root root 14375 Jan 1 2000 THIRD-PARTY-LICENSES.txt
lrwxrwxrwx 1 root root 7 Jan 30 2023 bin -> usr/bin
dr-xr-xr-x 2 root root 4096 Jan 30 2023 boot
drwxr-xr-x 2 root root 200 Aug 16 06:07 dev
drwxr-xr-x 34 root root 4096 May 10 11:16 etc
drwxr-xr-x 2 root root 4096 Jan 30 2023 home
-rwxr-xr-x 1 root root 397 Jul 31 08:50 lambda-entrypoint.sh
lrwxrwxrwx 1 root root 7 Jan 30 2023 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Jan 30 2023 lib64 -> usr/lib64
drwxr-xr-x 2 root root 4096 Apr 25 19:25 local
drwxr-xr-x 2 root root 4096 Jan 30 2023 media
drwxr-xr-x 2 root root 4096 Jan 30 2023 mnt
drwxr-xr-x 2 root root 4096 Jan 30 2023 opt
dr-xr-xr-x 124 root root 0 Aug 16 06:14 proc
dr-xr-x--- 2 root root 4096 Jan 30 2023 root
drwxr-xr-x 5 root root 4096 Apr 25 19:25 run
lrwxrwxrwx 1 root root 8 Jan 30 2023 sbin -> usr/sbin
drwxr-xr-x 2 root root 4096 Jan 30 2023 srv
drwxr-xr-x 2 root root 4096 Apr 25 19:25 sys
drwxrwxrwx 2 root root 4096 Jun 4 16:07 tmp
drwxr-xr-x 12 root root 4096 Jul 31 08:47 usr
drwxr-xr-x 24 root root 4096 May 9 21:46 var
やはり tmp
の sticky bit は立っていない。
Lambda Function の Runtime settings は以下
- Runtime:
Ruby 3.3
- ArchitectureInfo:
x86_64
Dir.tmpdir
を実行してみる
def lambda_handler(event:, context:)
# TODO implement
puts "#### Running Lambda ####"
p Dir.tmpdir
{ statusCode: 200, body: JSON.generate('Hello from Lambda!') }
end
エラーが再現した
system temporary path is world-writable: /tmp
/tmp is world-writable: /tmp
. is not writable: /var/task
Error raised from handler method
{
"errorMessage": "could not find a temporary directory",
"errorType": "Function<ArgumentError>",
"stackTrace": [
"/var/lang/lib/ruby/3.3.0/tmpdir.rb:43:in `tmpdir'",
"/var/task/lambda_function.rb:11:in `lambda_handler'",
"/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric/lambda_handler.rb:28:in `call_handler'",
"/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric.rb:88:in `run_user_code'",
"/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric.rb:66:in `start_runtime_loop'",
"/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric.rb:49:in `run'",
"/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric.rb:221:in `bootstrap_handler'",
"/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric.rb:203:in `start'",
"/var/runtime/index.rb:4:in `<main>'"
]
}
Lambda 上で Dir.tmpdir
利用すると確実にエラーが発生する
ランタイムのバージョンが関係している説
真偽不明だけど、過去のランタイムバージョンでは問題はなかった説がある。
今の Runtime version
INIT_START
Runtime Version: ruby:3.3.v12
Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:9cc8b6f58ab551d6089e31155c615e4f92eb9b4ad017cdc2e1788102a97e8625