non-pure-Pythonの外部モジュールをLambda上で扱うのに苦労した話

# はじめに
こんにちは、GxPの@ttanaka-gxpです。
この記事はグロースエクスパートナーズ Advent Calendar 2022の7日目です。

Serverless Frameworkを使ってnon-pure-Pythonの外部モジュールを含んだPythonアプリをデプロイした際に苦労した話です。
ハマりどころがあるというのは認識していたので覚悟はしていたのですが、思った以上に時間を取られてしまいました…
その分、勉強にはなったので自分の中での整理も兼ね、今回記事にさせて頂きます。

# やりたかったこと
RDS for SQL Serverに保存している基幹データに対し、関連システム向けに参照用途でのデータ連携の仕組みを構築したいというのが要件としてあり、API Gateway＋Lambda＋RDSの構成でデータ参照のAPIをPoCとして構築することになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942766/0907b6cb-c0ea-6ccc-552e-57c11458956a.png)

# 開発環境
| ツール | バージョン | 備考 |
| ---- | ---- | ----|
| Node.js | 16.13.0 |Serverless Framework用 |
| npm | 8.1.0 | Serverless Framework用 |
| Python | 3.8.10 | Lambda実行コード用 |
| pip | 21.1.2 | Lambda実行コード用 |
| データベース | SQL Server 2017 | お客様のAWS環境上にRDSで構築されており、<br>踏み台サーバ経由でポートフォワードし接続している |

ローカルの開発環境はWindows10にPythonを直接インストールしvenvで仮想環境を作成しています。

# 苦労した流れ
先に結論として大きく下記3点の対応が必要でした。
1. Pythonで環境依存のnon-pure-Pythonの外部モジュールを扱う場合、実行環境(今回はLambda)と同等の環境で`pip install`する必要がある
2. 今回扱ったpyodbc(SQL Server接続用のパッケージ)はプロジェクトをコンパイルする環境にODBCドライバーをインストールしてから`pip install`する必要がある。
3. Lambda上でpyodbcを動作させるためにはODBCドライバー自体も**正しいパス構成**でデプロイしておく必要がある。

### 図１＋対応１を実施してデプロイ ⇒ 失敗
**図１ローカルでDB接続成功**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942766/2ab0ca16-86b5-243b-b9d7-47ddb8b558be.png "図１")
今回のアプリはECS Fargateに移植する可能性もあったので、Webアプリとして動作できるようFlaskを使って構築しており、図１のように作業PCのWindows環境上ではServerless FrameworkのコマンドでFlaskアプリとして起動しDB疎通が出来た時点でLambdaへのデプロイを進めようとしていました。
（ 一応、[API Gateway、ALBのバックエンドとして、Lambda上でFlaskアプリを動かすのに使う](https://www.serverless.com/plugins/serverless-wsgi)プラグインについて）

合わせて、対応１の必要性は元々認識していたため、Serverless Frameworkの[Serverless Python Requirements](https://www.serverless.com/plugins/serverless-python-requirements)プラグインを追加することで対応しました。
dockerizePipのオプションをtrueにすることで、実行環境であるLambda相当のコンテナでコンパイルしてくれるようになります。

ここまでは順調に進んでいたのですが、いざデプロイしようとしたところ図２のようにpyodbcがNot Found Errorでパッケージングが失敗してしまい、調べた結論としてコンパイルする環境にもODBCドライバーを入れておくといった対応２が必要があることが分かりました。
Windows環境だとODBCドライバーがインストール済みだったので中々気づけませんでした…

**図２パッケージング失敗**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942766/785aa99f-463e-50b8-658a-21566676b12c.png)

### 対応２を実施しデプロイ ⇒ 失敗
対応２としてコンパイルする環境にODBCドライバーをインストールしておく必要があったため、コンパイル用に必要なパッケージをインストールするよう記載したDockerfileを用意し、dockerizePipでそれを使うようにしたことで解決しました。
パッケージングのエラーも解消し無事デプロイも出来たのですが、デプロイされたものを動かしてみたところ、図３の状態となりました。
**図３Lambda上でNot Foundエラー**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942766/0677069e-09d7-7690-1baf-d207d452f467.png)

デプロイしているパッケージ(実体はZipファイル)の中を調べてみたところ、pyodbc、ODBCドライバーは含まれてたのですが、パス構成が誤っていたためにNot Found Errorとなっており、ライブラリや必要なパッケージを正しいパスに配置する対応３が必要であることが分かりました。

### 対応３を実施しデプロイ ⇒ 成功！
対応３の内容を実現するにはパッケージ内のフォルダ構成を変える必要があるのですが、この部分
はServerless Frameworkでカスタム出来ない部分だったので一緒にデプロイすることは諦め、単体でLambda Layerとして手動デプロイするようにしたことで解決出来ました。
ここまでやってデプロイしたところ図４になり無事リリースすることが出来ました。

**図４成功！**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942766/7eea44fb-2bd4-95b2-887d-4b2cac5c0b77.png)

上記対応した時のコードのうち、Lambda Layerを作成している部分
https://github.com/t-tanaka-2/pyodbc-sample/tree/main/layer/pyodbc

# 最後に
ずっとこの記事を書こうと思いつつなんだかんだ1年たってしまいました…
当時のチャットやコミットログなどを見返しはしましたが、ところどころ記憶で書いているので細かいところで間違っているかもしれないです…
ただ最終的に動作しているので正しいものではあるはずです。
↑でハマっていた時、公式ドキュメントは当然ながらQiitaやStack Overflowのような記事が大いに参考になったので、この記事も誰かの参考になってくれたら幸いです。

# Lambda関連で余談
Lambda＋RDSの構成はLambdaがスケールすることによりDBコネクション数が枯渇する問題があるため、RDS Proxyを使う予定だったのですが、~~SQL Serverはサポートされていませんでした…
現時点でサポートしているのはMySQLとPostgreSQLだけのようです。~~
MySQL、PostgreSQLの他にMariaDBとSQL Serverもサポートされたようです！
https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.html
今回は検証の意味合いが強くそこまでアクセス負荷は見込んでいなかったので、Lambdaの同時実行数に制限を入れる方向で対応しています。

また、Lambda＋RDS構成の場合、プライベートサブネット上のRDSと通信するため、VPC Lambdaにする必要がありますが、IP枯渇やコールドスタート時に起動時間が遅いなど課題がいくつかある認識でした。ただ、現状は色々改善されており、それらについて下記の資料にまとまっているので同じ構成をとる場合は一読しておくと参考になるかと思います。
https://pages.awscloud.com/rs/112-TZM-766/images/EV_amazon-rds-aws-lambda-update_Jul28-2020_lambda_x_rds.pdf

あと先日のre:invent 2022でLambda SnapStartなるものが発表されており、コールドスタートの時間を短縮できるもののようでした。
このシステムもFlaskを起動する都合上、コールドスタート時は若干時間がかかるのでこれは良いのでは！？と思ったのですが今のところJava11しかサポートしていないようです。残念。
ただ個人的にはLambda上でJavaを動かすって一番最初に除外する選択肢だったので、実際どんなものか今度触ってみようと思います。
https://aws.amazon.com/jp/blogs/news/new-accelerate-your-lambda-functions-with-lambda-snapstart/