フューチャー技術ブログ

リリース直前にライブラリのインストールエラーが発生した際にどのように対応したか

(DALL-E3 で生成)

概要

先日、本番リリースを控えたシステムで OSS ライブラリのインストール起因のエラーが発生しました。
実際に起きた事象と、どのように検討して対応したのかを残すべく、ポストモーテムの形式で当記事を書きました。

ポストモーテム

発生日

2023/12/30

サマリ

AWS Glue (PythonShell)を起動する際に、awswrangler ライブラリをインストールする工程で、依存関係にある lxml のインストールに失敗しました。その結果、Glue ジョブの起動に失敗しエラーとなりました。

バージョン

システム/ライブラリ バージョン
AWS Glue PythonShell Glue version=3.0
Python version=Python3.9
DPU=0.0625
awswrangler 1.5.2
lxml 5.0.0

インパクト

AWS Glue PythonShell の Libraries 設定に、

wheel 形式で awswrangler を指定
or
wheel形式で awswrangler に依存しているライブラリを指定

している Glue ジョブが全て実行エラーとなりました。

根本原因

awswrangler が依存している、lxml ライブラリの最新バージョン 5.0.0 のインストール時に、以下 2種類のエラーが発生しました。

# wheel パッケージで lxml のインストールに失敗

Using legacy 'setup.py install' for lxml, since package 'wheel' is not installed.
# setup.py でインストール時に、
# gcc で src/lxml/etree.c をコンパイルする際に、out of memory エラーが発生

× Running setup.py install for lxml did not run successfully.
exit code: 1
╰─> [112 lines of output]
(省略)
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -DCYTHON_CLINE_IN_TRACEBACK=0 -I/usr/include/libxml2 -Isrc -Isrc/lxml/includes -I/.pyenv/versions/python39_loaded/include -I/.pyenv/versions/3.9.10/include/python3.9 -c src/lxml/etree.c -o build/temp.linux-x86_64-3.9/src/lxml/etree.o -w

cc1: out of memory allocating 65536 bytes after a total of 31883264 bytes
Compile failed: command '/usr/bin/gcc' failed with exit code 1
creating tmp
cc -I/usr/include/libxml2 -I/usr/include/libxml2 -c /tmp/xmlXPathInit3c3vwixm.c -o tmp/xmlXPathInit3c3vwixm.o
cc tmp/xmlXPathInit3c3vwixm.o -lxml2 -o a.out
error: command '/usr/bin/gcc' failed with exit code 1
[end of output]

note: This error originates from a subprocess, and is likely not a problem with pip.
error: legacy-install-failure
× Encountered error while trying to install package.
╰─> lxml

発生要因

lxml 最新バージョン 5.0.0 が、2023/12/30 05:29 (JST) にリリースされたためです。

検出

Glue ジョブの実行時に、 Internal service error 、タイムアウトエラー、もしくは以下のエラーが発生しました。

CommandFailedException: /tmp/glue-python-libs-XXXX/<ライブラリ>.whl installation failed after 2th retry due to exception: CalledProcessError

対応

対応案の検討

以下の A と B、 2 つの対応案を検討しました。

A. awswrangler を AWS Glue PythonShell で事前にパッケージ済みのライブラリ利用へ変更

今回利用していた awswrangler==1.5.2 は、AWS Glue PythonShell において事前にパッケージ済みのライブラリセットに含まれています。

参考: AWS Glueでの Python シェルジョブ - AWS Glue (amazon.com)

そのため、 awswrangler を明示的にインストールせずとも利用することが可能であるため、
AWS Glue ジョブの Libraries 設定から awswrangler の指定を削除することで、起動時のインストールを防ぎ、ライブラリの利用継続を行う案です。

メリット

  • AWS 側で用意されたライブラリが利用でき、一般に利用できることから、ライブラリの状態は枯れている
  • 起動時のインストール処理が省略され、実処理を実行するまでの時間が早くなり、全体的な処理速度向上が見込める
    • ※ 最大30秒程度の短縮であるため、全体のジョブ実行時間次第で支配的かどうか判断する

デメリット

  • 単体テスト/結合テスト時にインストールしていたバージョンと、 awswrangler==1.5.2 に依存する他パッケージのバージョンに差異が生まれる可能性がある
B. Glue ジョブ設定の、DPU=0.0625 から DPU=1 へ設定変更

根本原因にて記載した通り、lxmlを setup.py にてインストールする際に、out of memory エラーが発生していました。
そのため、マシンスペックを上げることで解消できないかと推測し、検証環境にて DPU=0.0625 → DPU=1 へ変更して実行したところ、正常に処理が完了しました。

メリット

  • エラーが発生する前のライブラリのバージョン状態に近い環境で、Glue ジョブを動作させることができる
    • awswranglerに依存するライブラリのバージョンをエラー直前の成功したバージョンに近い状態とできる
      • ※ 近いという表現は、設定次第で依存する他のライブラリバージョンも変更される可能性があることに由来する(後述するが、そもそも実行時にバージョンが揺れること自体が本当の根本原因である)

デメリット

  • ビルドして作成する必要があり、wheel でのインストールに失敗する lxmlのバージョンが利用される
  • DPU 値を上げるため、金銭的なコストが増加する

対応案の選択

結論、対応案 A を選択しました。

判断基準として、”不具合を追加で発生させないこと” を置きました。
システム性質上、本番環境での継続的な実行ではなく、数回の実行を安定して行うことが求められていました。その結果、ライブラリの最新バージョン追従よりも、成熟し安定したバージョンでの実行を優先しました。

リスク評価としては、インストールエラーが発生した、最新の lxml により発生する不具合リスクと、awswranglerに依存するライブラリのバージョンが下がることで発生する不具合のリスクを考慮し、定量的ではないですが後者のほうがより発生するリスクが低いと判断しました。

ただ、どちらにせよリスクは発生するため、単体テストと AWS 環境上での結合テストを再度実行し、動作保証の上で本番稼働を行いました。

修正事項

AWS Glue ジョブの Libraries 設定に指定している wheel ライブラリ内で、awswrangler をインストールしない設定へ修正しました。
具体的には、 poetry build -f wheel で独自ライブラリを wheel ファイルへビルドしていたため、awswranglerpoertry における dev group の依存へ変更しました。

before

# pyproject.toml

[tool.poetry.dependencies]
awswrangler = "2.15.1"

[tool.poetry.group.dev.dependencies]

after

# pyproject.toml

[tool.poetry.dependencies]

[tool.poetry.group.dev.dependencies]
awswrangler = "2.15.1" # dev group へ変更

学び

うまくいったこと

幸いなことに、単体テスト/結合テストにて追加の問題が発生せず、本番環境での実行も安定していました。

振り返り

後から振り返ると、リスク判断において正確な要素を抽出できていなかった点がありました。
その点は、lxml に対して実際に依存している処理があったのか否かです。lxmlへの依存は、awswrangler (正確には、内部で利用している pandas )内の処理のため、ライブラリの実装を読む必要がありましたが、正確なリスク評価としては必要だったのではと思いました。
時間的余裕次第ですが、調査をする選択肢をもっておくべきでした。

また、案の比較時に、インストールエラーが発生した lxml を利用すること自体が心理的障壁となり、選択時にバイアスがかかってしまったという点もありました。これは、エラーが発生したバージョンを使いたくないという感情から来るものでした。
客観的な判断で、より正確にリスク評価ができるとより良かったなと感じています。

そもそもの話

根本的には、実行時のライブラリのバージョンが固定されていないことが発生の要因です。
バージョンが固定 “されている”・”されていない” (※1) で少しメリット・デメリットを考えてみると、
メリットとしては、設定次第ではありますが最新のバージョンに近いバージョンが選択されることで、脆弱性の修正や性能向上等のバージョンアップによる改善を取り込むことができます。
デメリットとしては、当事象のようにバージョンが揺れることで、テスト時に発生していなかった不具合が突然発生するリスクが生まれます。

ケースバイケースではありますが、安定して運用することが第一である場合はデメリットは許容できないことが多い印象です。
そのため、ライブラリのバージョンを lock ファイル等を利用して固定の上で安定運用しつつ、もし継続して利用されるシステムの場合、適切にメンテナンスして如何にバージョンを上げていくかを考えることになるのかなと思います。(余談ですが、メンテナンスが検討できておらず、塩漬けにされて、いざ大きめの脆弱性が発見され、いきなり対応できないといった問題はありがちな気がします。)

ちなみに、AWS Glue の追加ライブラリにおいて、lock ファイル利用したライブラリのバージョン固定を行う方法は現時点ではわかりませんでした。

※1 バージョンが固定されていない とは、ライブラリに対する依存ライブラリ設定に沿っている上という前提になります。

例えば、pyproject.toml - awswrangler の設定を参考にすると、以下の通りです。

  • boto3 は 1.20.32 以上のバージョンに依存
  • pandas の場合
    • Python version が 3.9 より小さい場合は、 1.2.0 <= version < 2.1.0 の間のバージョンに依存
    • Python version が 3.9 以上の場合は、 1.2.0 <= version < 3.0.0 の間のバージョンに依存
# 参考: https://github.com/aws/aws-sdk-pandas/blob/9d5c1a67af3f5e7e8d7aa506da1b97277d3e3bd6/pyproject.toml#L28

[tool.poetry.dependencies]
python = ">=3.8, <4.0"

# Required
boto3 = "^1.20.32"
botocore = "^1.23.32"
pandas = [
{ version = ">=1.2.0,<2.1.0", markers = "python_version < \"3.9\"" },
{ version = ">=1.2.0,<3.0.0", markers = "python_version >= \"3.9\"" },
]

上記設定のため、boto3 であれば実行時に 1.20.32 以上のバージョン (最新バージョン) が利用され、pandas もバージョン指定内の最新バージョンが利用される可能性があります。

補足

lxml の wheel インストールエラー自体の解消

バージョン 5.0.1 と 5.1.0 にて、解消されたバージョンがリリースされています。

参考: lxml changelog
image.png

wheel とは

PEP 427 で定義された Python のパッケージファイルフォーマットを指します。拡張子は .whl であり、中身としては ZIP 形式のアーカイブです。

フォルダ構成は、ZIP 解凍をすることでわかりますが、以下の通りです。


├─ <ライブラリディレクトリ>
├─ ...
└─ <ライブラリ名>.dist-info
├─ METADATA
├─ RECORD
└─ WHEEL

当事象に関連する内容としては、 .dist-info 以下の METADATA に依存するライブラリのバージョンが記載されています。
pipwheel ファイルを指定してインストールした際に、Requires-Dist に記載されているライブラリがインストールされます。

Metadata-Version: 2.1
Name: <ライブラリ名>
Version: 1.0.0
Summary:
Author: XXX
Requires-Python: >=3.9.10,<3.11
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Requires-Dist: boto3 (==1.28.64)
...

Description-Content-Type: text/markdown

(README.md の内容が記載)

所感

ポストモーテムとして、振り返りをまとめてみました。
数年に1回あるかないかの問題ではありますが、同様の問題が発生した際に判断の参考になれば良いなと思います。

参考情報