AWS Lambda RuntimeをRuby3.3にしたら外部エンコーディングが変化した話

こんにちは。Feature2 Unitのうなすけです。我々のチームの担当範囲のひとつには「データの入出力」というものがあり、お客様からAPI呼び出しやファイルアップロードなどで受け取ったデータを適切に処理するコンポーネントの運用・開発をしています。

AWS Lambdaの処理が失敗するようになった

皆さん、AWS Lambdaはお使いでしょうか。我々も様々な処理にAWS Lambdaを活用しています。一例として、ユーザーからアップロードされたCSVファイルのバリデーションを行うLambda functionをAWS Step Functionsの一部として実行しています。

ある日、機能追加として日本語を含む1CSVファイルのアップロードを許可したのですが、CSVファイルのバリデーション処理でエラーが発生するようになりました。日本語も受け入れるようにしたタイミングと同時にRuby runtimeのバージョンも上げていたので、切り分けのためRubyのバージョンを3.3から3.2に戻してみたところ、バリデーションは正常終了するようになりました。

内部の処理は変化していないのに、Rubyのバージョンが変わると日本語の処理ができなくなる……これは妙です。

Lambdaの実行環境はコンテナイメージを使用して手元で試すことができます。これを使って、以下のようなRuby scriptで実験してみました。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images-create.html

# test.rb
require "csv"

pp CSV.read("/app/test.csv")
col1,col2
abc,あいう
# FROM public.ecr.aws/lambda/ruby:3.3 きりかえ用
FROM public.ecr.aws/lambda/ruby:3.2

ENTRYPOINT [ "" ]
COPY test.csv test.rb /app/
CMD ["ruby", "/app/test.rb"]

public.ecr.aws/lambda/ruby:3.2 を使用した場合の実行結果は以下のようになりました。正しくCSVを読めていることがわかります。

$ docker run --rm -it lambdatest:ruby32
[["col1", "col2"], ["abc", "あいう"]]

public.ecr.aws/lambda/ruby:3.3 を使用した場合の実行結果は以下のようになりました。ひらがなを含む2行目で Invalid byte sequence が発生していることがわかります。

$ docker run --rm -it lambdatest:ruby33
/var/lang/lib/ruby/3.3.0/csv/parser.rb:878:in `build_scanner': Invalid byte sequence in US-ASCII in line 2. (CSV::InvalidEncodingError)
        from /var/lang/lib/ruby/3.3.0/csv/parser.rb:402:in `parse'
        from /app/test.rb:in `each'

エンコーディングの判定

そもそも、Ruby 3.3で実行した場合のエラーメッセージに Invalid byte sequence in US-ASCII とあるのが気になります。そこで、LANG環境変数と、Rubyの外部エンコーディング(標準入出力、コマンドライン引数、open で開くファイルなどで指定がない場合に採用されるエンコーディング)を以下のスクリプトで確認してみました。

# test.rb
puts ENV["LANG"]
puts Encoding.default_external

以下のように、Ruby 3.2でも3.3でもLANG環境変数en_US.UTF-8 と指定されている(これはlambdaの実行環境における既定値2)のに対し、Rubyの外部エンコーディングRuby 3.2 の場合に UTF-8Ruby 3.3の場合に US-ASCII となっています。

$ docker run --rm -it lambdatest:ruby32
en_US.UTF-8
UTF-8
$ docker run --rm -it lambdatest:ruby33
en_US.UTF-8
US-ASCII

では、外部エンコーディングUTF-8にすればCSVファイルは読み込めるのでしょうか?以下のようにスクリプトを修正して実行してみます。

# test.rb
require "csv"
puts RUBY_VERSION
Encoding.default_external = Encoding::UTF_8
pp CSV.read("/app/test.csv")
% docker run --rm -it lambdatest:ruby33
3.3.4
[["col1", "col2"], ["abc", "あいう"]]

無事CSVファイルが読めるようになりました。めでたしめでたし……とはいきません。Encoding.default_externalRubyスクリプト内で変更することを想定されていません。

default_external を変更する前に作成した文字列と、default_external を変更した後に作成した文字列とではエンコーディングが異なる可能性があるため、Ruby スクリプト内で Encoding.default_external を設定してはいけません。代わりに、ruby -E を使用して、正しい default_external で Ruby を起動してください。 https://docs.ruby-lang.org/ja/3.3/method/Encoding/s/default_external=3d.html

なので、Lambdaの環境変数RUBYOPT="-E utf-8" を追加すれば、問題は解決しそうですね。めでたしめでたし。

LANGがen_US.UTF-8なのに外部エンコーディングがUS-ASCIIなのは何故?

解決はしたのですが、疑問が残ります。Ruby 3.2 runtimeと3.3 runtimeにおいて、LANG環境変数の値が en_US.UTF-8 というのは変化していないのに、外部エンコーディングUTF-8 から US-ASCII に変わってしまうのはなぜでしょうか。もういちどRubyのドキュメントを読んでみます。

Rubyロケールまたは -E オプションに従って default_external を決定します。ロケールの確認・設定方法については各システムのマニュアルを参照してください。 -E オプションを指定していない場合は、WindowsではUTF-8、その他のOSではロケールに従って default_external を決定します。 default_external は必ず設定されます。Encoding.locale_charmap が nil を返す場合には US-ASCII が、ロケールRubyが扱えないエンコーディングが指定されている場合には ASCII-8BIT が、default_external に設定されます。 https://docs.ruby-lang.org/ja/3.3/method/Encoding/s/default_external.html

ロケールRubyが扱えないエンコーディングが指定されている場合」の条件に入り、ASCII-8BIT (正確には設定されているのはUS-ASCII)が Encoding.default_external に設定されたのでしょうか?

実はAWSのドキュメントをよく読むと、Ruby 3.2と3.3のruntimeで、AWSによって提供されるベースイメージがAmazon Linux 2からAmazon Linux 2023に変更されています。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/ruby-image.html#ruby-image-base

それぞれのコンテナ内で locale --all-locales の結果を実行すると、このようになります。

bash-4.2# ruby -v
ruby 3.2.5 (2024-07-26 revision 31d0f1a2e7) [aarch64-linux]
bash-4.2# locale --all-locales
aa_DJ
aa_DJ.iso88591
aa_DJ.utf8
aa_ER
aa_ER@saaho
aa_ER.utf8
aa_ER.utf8@saaho
... (いっぱい)
bash-4.2# locale --all-locales |  wc -l
842
bash-5.2# ruby -v
ruby 3.3.4 (2024-07-09 revision be1089c8ec) [aarch64-linux]
bash-5.2# locale --all-locales
locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_MESSAGES to default locale: No such file or directory
locale: Cannot set LC_COLLATE to default locale: No such file or directory
C
C.utf8
POSIX
bash-5.2# 

つまり、Ruby 3.3 runtime (Amazon Linux 2023)では、enロケールは存在せず、Cロケールしか存在していません。そのような環境において LANG=en_US.UTF-8 と指定してあっても、enロケールを使うことができずに外部エンコーディングUS-ASCII になってしまっていた……ということのようです。

なので、以下のようにLANG 環境変数C.UTF-8 とすることによっても当初の問題は解決できそうです。

# test.rb
require "csv"
puts ENV["LANG"]
puts RUBY_VERSION
pp CSV.read("/app/test.csv")
FROM public.ecr.aws/lambda/ruby:3.3

ENTRYPOINT [ "" ]
COPY test.csv test.rb /app/
ENV LANG="C.UTF-8"
CMD ["ruby", "/app/test.rb"]
$ docker run --rm -it lambdatest:ruby33
C.UTF-8
3.3.4
[["col1", "col2"], ["abc", "あいう"]]

まとめ

AWS Lambda Ruby 3.3 runtimeには、enロケールが含まれていません。なので、マルチバイト文字が含まれる外部のリソースを扱う場合は、LANG 環境変数C.UTF-8 か、または RUBYOPT 環境変数-E utf-8 と設定するのをオススメします。


  1. より正確には「マルチバイト文字を含むCSVファイルを許可」ですが。
  2. https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-envvars.html