こんにちは。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-8
、Ruby 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_external
はRubyスクリプト内で変更することを想定されていません。
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
と設定するのをオススメします。
- より正確には「マルチバイト文字を含むCSVファイルを許可」ですが。↩
- https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-envvars.html↩