共有ライブラリに依存するライブラリを Lambda レイヤーとして使うことはできるのか

## 背景
ライブラリをインストールしたLambdaレイヤーを作成したら .so ファイルが見つからないというエラーが出ることがある。

```bash
libGL.so.1: cannot open shared object file: No such file or directory
```

コンテナイメージを作るのが簡単だが、Lambdaレイヤーとして共有ライブラリまで含めてパッケージ化することはできるのか？

## 前提
ライブラリはなんでもいいのだが、*opencvをPython3.9 Lambdaで使う*という目標でトライしてみる。
**`opencv-python`ライブラリを普通にインストールしてLambdaレイヤーにすると、冒頭のエラーが出る。**
opencvを使う方法は以下が考えられる。今回は3ができそうというところまで試した。
    1. コンテナイメージとしてデプロイする
    2. 共有ライブラリに依存しないライブラリを使う。
`opencv-python`は.soファイルへの依存があったが、`opencv-python-headless`は意識しないでも使えた。違いについては調べていない。
    3. 依存モジュールを全部調べて明示的にレイヤーに入れる

**現実的なのは1,2と思うので、3をあえてやる必要はないと思う。**
そもそもできたとしてもLambdaレイヤーのサイズquotaギリギリになりそうなので、仮に今opencvでできたとしても、将来や他のパッケージでできる保証はない。
つまり今回の検証は割と無駄なことをしていると思う。

## 結果まとめ

上記のニュアンスからもわかる通り、できそうであるというところまで確認したが最後まではやっていない。
### わかったこと
- .so ファイルをLambdaで読み取れる範囲(`LD_LIBRARY_PATH`)におくことは可能
- `yum install`したパッケージとその依存モジュールを特定のディレクトリに配置する方法
- **共有ライブラリに依存するライブラリのLambdaレイヤーを作成する方法はあまり実用的ではなさそうなこと**

### できなかったこと
- `opencv-python`がどの.soファイルに依存するのか正確に調べる方法がわからなかった。
エラーが出るたびにそのファイルをレイヤーに含める、ということでエラー自体は解消はできる見込みだが、明示された仕様に基づいていないため今動いたとしても突然使えなくなる可能性あり。

## 手順概要

### 参考
公式が昔サンプルを出していた。[lambda-opencv](https://github.com/awslabs/lambda-opencv/tree/main)
必要なモジュールと依存関係をDockerでインストールしている。
しかし最終更新が1年以上前と古いためか、このままだとビルドができなかった。少なくとも .so ファイルのインストールはしていない。サンプルはあくまでサンプルのため、動作しなくてもメンテナンスされる保証はない。
しかし大元はこのDockerfileを参考に使わせてもらった。

### 流れ
1. opencvを指定したディレクトリにインストールする(サンプルのまんま)
1. 依存モジュールを含んでいるライブラリmesa-libGLも指定したディレクトリに`yum install`
1. 1.2. 両方をzip化
1. コンテナからホストにzipファイルを取り出す
1. レイヤーにして、Lambdaで`import cv2`を試す

## 手順詳細
### まずは .so ファイルがないエラーが出るところまで行い、流れを確認
EC2のAmazon Linux 2インスタンス起動。dockerをインストール。
先述の[lambda-opencv](https://github.com/awslabs/lambda-opencv/tree/main)のDockerfileを編集する。`git clone`でも、新たにファイル作ってコピペでも良い。
そのままだと時間がかかった上にビルドに失敗するので、まずは以下を変更する
- FROM amazonlinux　=> FROM amazonlinux:2　[^1]
- Python3.9 のところ以外は消す。
- Python3.9関連で一箇所ディレクトリにtypoがあって3.8になっているので、そこは直す

[^1]: 2023/06時点でamazonlinuxのlatestがAmazon Linux 2023になっているので、それで失敗するようになったようだ。

続いて、以下のコマンドを実行。
`--rm`オプションはコンテナ終了時に破棄してくれるのでちょっと試したりする時とかホストへのコピーだけしたいときにとても便利。
```bash
sudo docker build --tag=lambda-layer-factory:latest .
sudo docker run --rm -it -v $(pwd):/data lambda-layer-factory cp /packages/cv2-python39.zip /data
```

これをレイヤーにして、Python3.9 Lambdaに追加する。Lambdaには[lambda-opencv](https://github.com/awslabs/lambda-opencv/tree/main)サンプルのapp.pyのコードを貼り付ける。
ここで冒頭のエラーが出て、.soファイルが足りないことを知る。
```bash
libGL.so.1: cannot open shared object file: No such file or directory
```

これはAmazon Linux 2にはデフォルトで入っていないようだ。
```bash
sudo docker run --rm -ti amazonlinux:2 /bin/bash
find / -name libGL.so* ## ない
```

### libGL.so.1が見つからないエラー自体の対処法
mesa-libGL [^2]というライブラリをインストールすればいいらしい。

```bash
sudo docker run --rm -ti amazonlinux:2 /bin/bash
find / -name libGL.so* ## ない
sudo yum install mesa-libGL -y
find / -name libGL.so*
## /usr/lib64/libGL.so.1
## /usr/lib64/libGL.so.1.7.0
```
[^2]:`sudo yum install libglvnd-glx`でもこれらの.soファイルは追加できた

Amazon Linux 2 のコンテナ内ならここにPython3を入れればもう`import cv2`が実行できたが、Lambdaは読み取りのパスが違うためかエラーの解消にはならない。

### yumでディレクトリを指定したインストールをする
`--installroot`というオプションを使うようだ。

普通に実行すると以下のようなエラーが大量に出た。
```bash
sudo docker run --rm -ti amazonlinux:2 /bin/bash
sudo yum install --installroot=/home/ec2-user/lambda  libglvnd-glx
## 以下のエラー
Repository 'amzn2-core': Error parsing config: Error parsing "mirrorlist = '$awsproto://$amazonlinux.$awsregion.$awsdomain/$releasever/$product/$target/x86_64/mirror.list'": URL must be http, ftp, file or https not 
...
```

どうも`--installroot`で指定するディレクトリに以下の内容を持つyumの設定ファイルなど [^3]が必要なようだ[^4]。
```bash
cat /lambda/etc/yum.repos.d/amzn2-core.repo 
...
[amzn2-core]
name=Amazon Linux 2 core repository
mirrorlist=$awsproto://$amazonlinux.$awsregion.$awsdomain/$releasever/$product/$target/$basearch/mirror.list
```

[^3]:`amzn2-core.repo `ファイル単体をコピーしただけでは流石にダメだった
[^4]:[yumdaのDockerfile](https://github.com/lambci/yumda/blob/master/amazon-linux-2/Dockerfile)(`yum install`をLambdaレイヤー用に依存関係込みで行ってくれるサードパーティ製のツール)と[ディレクトリ指定でyum installする記事](https://blog.serverworks.co.jp/cloudshell-install-by-yum)を参考にした。

Amazon Linux 2の場合は以下にあるので、こちらをコピーすれば良い
/etc/yum.repos.d/amzn2-core.repo

### 出来上がったDockerfile
[Lambdaでは/opt/libというパスがLD_LIBRARY_PATHとして読み込み対象である](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html)ことに注意して[^5]、python lib が同じ階層になるようにzip化する。
結局サンプルと違うのは以下。先述の変更点3点にlibGl.soを書いただけである。
- FROM amazonlinux => FROM amazonlinux:2に変更
- Python3.9以外削除
- パスのタイポを修正
- libGl.soをインストールしzipへ含める

[^5]:Lambdaで`print(os.environ['LD_LIBRARY_PATH'])`を実行してもなんとなくわかる

```Dockerfile
FROM amazonlinux:2

RUN yum update -y
RUN yum install gcc openssl-devel bzip2-devel libffi-devel wget tar gzip zip make -y

# Install Python 3.9
WORKDIR /
RUN wget https://www.python.org/ftp/python/3.9.10/Python-3.9.10.tgz
RUN tar -xzvf Python-3.9.10.tgz
WORKDIR /Python-3.9.10
RUN ./configure --enable-optimizations
RUN make altinstall

# Install Python packages
RUN mkdir /packages
RUN echo "opencv-python" >> /packages/requirements.txt
RUN mkdir -p /packages/opencv-python-3.9/python/lib/python3.9/site-packages
RUN pip3.9 install -r /packages/requirements.txt -t /packages/opencv-python-3.9/python/lib/python3.9/site-packages

# Install libGl.so
# To install to original dir, it has to have /etc/yum.repos.d/amzn2-core.repo
RUN mkdir -p /temp && \
    cp -r /etc/ /temp/etc && \
    yum install --installroot=/temp --releasever=2 mesa-libGL -y
RUN cp -a /temp/usr/lib64/libGL.so* /packages/opencv-python-3.9/lib

# ls /packages/opencv-python-3.9/ ==> python lib

# Create zip files for Lambda Layer deployment
WORKDIR /packages/opencv-python-3.9/
RUN zip -r9 /packages/cv2-python39.zip .
WORKDIR /packages/
RUN rm -rf /packages/opencv-python-3.9/
```

## 上記のDockerfileを使った最終結果
同じようにzipを取り出す。
```bash
sudo docker build --tag=lambda-layer-factory:latest .
sudo docker run --rm -it -v $(pwd):/data lambda-layer-factory cp /packages/cv2-python39.zip /data
```
Lambdaレイヤーを作成し、Lambdaを動かす(`import cv2`する)と...
libGL.so.1ではなく他の共有ライブラリが見つからないというエラーになる。やったね。
```bash
"libgthread-2.0.so.0: cannot open shared object file: No such file or directory",
```

これを繰り返せばいつかは動くはずだが、不毛なのでここで止め。
多分下の記事にある.soファイルを全部入れれば解決するのだけど、この一覧をスマートに取得する方法がわからなかった。これはやはりエラーメッセージをみてその.soを入れて、を繰り返したようです。
https://qiita.com/suzuki-navi/items/d58d402422b784264d95

## そのほか試してダメだったこと
### Amazon Linux 2に含まれる.soファイルの大半をレイヤーに含める
サイズ的に無理だった。
Dockerfileに以下を追記して、`/usr/lib64`ディレクトリにある.soとつくファイルを全てLambdaのLD_LIBRARY_PATHに置けば一気に必要そうなファイルを置けるのでは、と思ったがレイヤーサイズ上限に引っかかりレイヤー作成できず。

```Dockerfile
# Amazon Linux 2 image has more .so files than in Lambda's PATH
RUN mkdir -p /packages/opencv-python-3.9/lib
RUN cp -r /usr/lib64/*.so* /packages/opencv-python-3.9/lib
```

結構サイズでかい。全部入れるのは無理でした
```
du /usr/lib64 -sh
74M     /usr/lib64
```

### LD_LIBRARY_PATHをLambda実行時に書き換える
`os.environ['LD_LIBRARY_PATH']`を書き換えるといけるという情報があった気がしたが、Lambdaでは通じなかった。これはPythonの実行前じゃないとダメ。。？なようで、使える状況が限られている or できないものなのかもしれない。