AWS LambdaでPython向けLayerを作ろうとしたらはまった話

AWS Lambda（Python）を使ってGoogle APIにアクセスしようとしたらライブラリではまった話です。
（Python初心者なので誤った記述があるかもしれません）

# 環境
- 開発環境用にEC2 Amazon Linux2 + Python 3.7
- Google APIにはサービスアカウントでアクセスする
- Google API利用に必要なPythonパッケージ用にLambda Layerを作成する

# 参考にした記事
- メインのコード自体は「補足：サービスアカウントを使ったやり方」を
ほぼそのまま活用させていただきました。

https://www.coppla-note.net/posts/tutorial/google-calendar-api/

- 必要なパッケージについてはGoogleの「[Python quickstart](https://developers.google.com/docs/api/quickstart/python)」を参照しています。

# 事象
`pip -t`でインストールしたディレクトリを圧縮・Layer化したのに、Lambdaが以下のエラーを吐く。
> [ERROR] Runtime.ImportModuleError: Unable to import module 'lambda_function': No module named 'google.oauth2' Traceback (most recent call last):

※Layer用のzipは以下の手順で作成（Layerの作り方は、いつもお世話になっている[クラスメソッドさん記事](https://dev.classmethod.jp/articles/lambda-layer-basics-how-it-works/)参照）
```layer.zip
$ mkdir python
$ pip3 install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib -t python/
$ zip -r layer.zip python/
```

# 切り分け
1\. ローカルで同じようにパッケージをインストールしコードを動かしたときはエラーが出なかった
　→Layerの問題？

2\. `google.oauth2`は`google-auth-oauthlib`の前提パッケージ
　→一緒にインストールされているはず。

3\. -tで指定したパッケージのインストール先に`google.oauth2`が存在するかどうか
　→なぜかprotobufしか存在しない

```
$ ls python/google/
protobuf  _upb
```

4\. ローカル（-t 指定なし）のパッケージインストール先を見てみる
　→oauth2はある。逆にprotobufが無い。（つまりprotobufは別のところにインストールされている？）

```
$ ls /usr/local/lib/python3.7/site-packages/google
api  api_core  auth  cloud  gapic  logging  longrunning  oauth2  rpc  type
```

5\. protobufのインストール先を探す
　→`/usr/local/lib/` ではなく `/usr/local/lib64/` にインストールされている。
　　`/usr/local/lib64/`を確認すると3と同じようにprotobufしか存在しない状態。

```
$ pip3 show protobuf
Name: protobuf
Version: 4.21.7
Summary: 
Home-page: https://developers.google.com/protocol-buffers/
Author: protobuf@googlegroups.com
Author-email: protobuf@googlegroups.com
License: 3-Clause BSD License
Location: /usr/local/lib64/python3.7/site-packages
Requires: 
Required-by: google-api-core, googleapis-common-protos
$ ls /usr/local/lib64/python3.7/site-packages/google/
protobuf  _upb
````

　
 **推測**
-t を使うことでlib/ と lib64/ に分かれるはずのパッケージが同じディレクトリにインストールされてしまい、上書きされている？

# 原因
どうやら pip の --upgrade オプションと -t を併用した場合、同じ名前で異なるプラットフォーム（32bit と 64bit）のパッケージがあると追加ではなく、上書きされてしまうようです。
（upgradeだから他のパッケージには影響がないかと思いきや、ディレクトリが同じだと置き換えになる）

githubでも議論されていて、closedにはなっているもののズバリの解決策がありません。

https://github.com/pypa/pip/issues/8063
>You cannot pip install a single set of requirements that have mixed pure lib and platform lib installations with the same name, on a platform where the pure lib and platform lib folders are different, as pip will not merge these together. 

--upgradeの説明を追加したほうがいいんじゃない？とも言われていますね。

``` diff
- WARNING: Target directory /path/to/target/namespace already exists. Specify --upgrade to force replacement.
+ WARNING: Target directory /path/to/target/namespace already exists. Specify --upgrade to force replacement (existing 'namespace' will be replaced completely, not merged)
```

# 回避策
`pip -t`によるディレクトリ指定のインストールは利用できないので、pythonの仮想環境を利用し
Layer用zipファイルを作成することにします。

```
$ python3 -m venv env
$ source env/bin/activate
$ pip3 install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
$ cp -pr env/lib/python3.7/site-packages python
$ zip -r layer.zip python/
```

　 
仮想環境は lib64/ が lib/へのシンボリックリンクになっており、パッケージが同じディレクトリに共存できます。
（googleディレクトリにprotobufと他のパッケージが存在している）
```
$ ls -l env/
total 8
drwxrwxr-x 2 ec2-user ec2-user 4096 Oct 17 16:41 bin
drwxrwxr-x 2 ec2-user ec2-user    6 Oct 17 16:41 include
drwxrwxr-x 3 ec2-user ec2-user   23 Oct 17 16:41 lib
lrwxrwxrwx 1 ec2-user ec2-user    3 Oct 17 16:41 lib64 -> lib
-rw-rw-r-- 1 ec2-user ec2-user   70 Oct 17 16:41 pyvenv.cfg

$ ls env/lib/python3.7/site-packages/google
api  api_core  auth  cloud  gapic  logging  longrunning  oauth2  protobuf  rpc  type  _upb
```


# 結果
仮想環境で作成したlayer.zipを読み込ませたところ、エラーなくGoogle APIが実行できるようになりました。

> START RequestId: ee65c39b-6d0d-44b5-81e4-xxxxxxxxxx Version: $LATEST
Getting the upcoming 10 events
No upcoming events found.
{'kind': 'calendar#events', 'etag': '"xxxxxxxxxxxxxxx"', 'summary': 'Work', 'updated': '2022-10-18T01:11:51.814Z', 'timeZone': 'Asia/Tokyo', 'accessRole': 'reader', 'defaultReminders': [], 'nextSyncToken': '---------------------------------', 'items': []}
END RequestId: ee65c39b-6d0d-44b5-81e4-xxxxxxxxxx
REPORT RequestId: ee65c39b-6d0d-44b5-81e4-xxxxxxxxxx	Duration: 3485.31 ms	Billed Duration: 3486 ms	Memory Size: 128 MB	Max Memory Used: 87 MB	Init Duration: 565.76 ms