Lambda Layer上でインポートするパッケージをローカルでインストール〜アップロードまで

## これは何？
今までインフラエンジニアで開発経験など無かったのですが, 趣味やサーバレスの検証も兼ねてLambda Layerを初めて自分で作成し実装した際にライブラリが異なることで`invalid ELF header`エラーが発生した際の対処法になります.


## 調査

Pythonで、momentoライブラリを導入してLambda Layerにアップロードした際に、 `invalid ELF header` エラーが発生しました. 
(momentoについて詳しくは[こちら](https://docs.momentohq.com/ja/cache))

> https://docs.momentohq.com/ja/cache


これは、ローカルで実行しているPythonライブラリとLambdaで実行しているPythonライブラリが異なるため、ELF(Executable and Linking Format)ヘッダーの規定が異なりインポートエラーが発生しているものとなります. 

```toml
{
  "errorMessage": "Unable to import module 'lambda_function': /opt/python/cryptography/hazmat/bindings/_rust.abi3.so: invalid ELF header",
  "errorType": "Runtime.ImportModuleError",
  "requestId": "xXxXxX-xXxXxX ...",
  "stackTrace": []
}
```

そのため, Lambdaで使用しているPythonのコンテナイメージを取得し、ローカル上でパッケージインストールする方法を後述で解説します.


## imageについて

Lambdaのランタイムバージョンのイメージについては[こちら](https://gallery.ecr.aws/sam/build-python3.10)を参考にdocker pullを実行します.
実行するランタイムの最新バージョンを以下の赤線のように指定し, コピーしました.

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/fd4e7f17-c39c-47eb-6e15-de9aed0dcf20.png)


## 手順

フォルダの構成は以下の通りとなります.
```sh
./path/to/dir
├── layer.zip
├── python
│   ├── momento
│   ├── momento-1.19.1.dist-info
│   │     :
│   │     :
│   │         
│   ├── requests
│   └── requests-2.31.0.dist-info
└── requirements.txt
```

ローカルフォルダ./pythonにパッケージをインストールします. 

重要なのはフォルダ名を **./python** として、下にパッケージをインストールします. 

これはLambdaレイヤーで使用するライブラリはpythonフォルダ上である必要があるためです. 
https://repost.aws/ja/knowledge-center/lambda-import-module-error-python
> 重要: Python 用にインポートするライブラリを /python フォルダ内に配置します.



```sh
cd ./path/to/dir

docker run --rm -v ./:/var/task public.ecr.aws/sam/build-python3.10:1.110.0-20240222205900 \
pip install -r requirements.txt -t ./python
```



zipコマンドでパッケージを圧縮.
```sh
zip -r layer.zip ./python
```

圧縮できたら, あとはLambda Layerに圧縮ファイルをアップロードすれば準備完了です.

## テスト
Lambda Layerのバージョンを更新した後, 以下のLambda関数を実行します. 

> **MOMENTO_API_KEY** は Momento Cache でRead/Write権限を持ったAPIキー. 
> **MOMENTO_CACHE_NAME** は Momento Cacheのキャッシュ名を環境変数として指定する. かもしくは)

```py
import json
import os
from datetime import timedelta
import logging
from momento import CacheClient, Configurations, CredentialProvider
from momento.responses import CacheGet, CacheSet, CreateCache, ListCaches, CacheIncrement
import requests

MOMENTO_API_KEY = os.getenv('MOMENTO_API_KEY', 'xXxXxXxXxXxXxXxXxX')
MOMENTO_CACHE_NAME = os.getenv('MOMENTO_CACHE_NAME', 'test-cache-name')

logger = logging.getLogger()
logger.setLevel("INFO")

def lambda_handler(event, context):
    client = create_cache_client()
    client.set(MOMENTO_CACHE_NAME, "test-key", "test-value")
    res = client.get(MOMENTO_CACHE_NAME, "test-key")
    logger.info("Result: {}".format(res))
    return {
        'statusCode': 200,
        'body': json.dumps(res)
    }

def create_cache_client():
    cache_client = CacheClient(
        configuration=Configurations.Laptop.v1(),
        credential_provider=CredentialProvider.from_environment_variable("MOMENTO_API_KEY"),
        default_ttl=timedelta(seconds=600),
    )
    return cache_client
```

これでエラーが出力されなければOKになります. 


## リファレンス

- https://gallery.ecr.aws/sam/build-python3.10
- https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-image-repositories.html#serverless-image-repository-uris
- https://repost.aws/ja/knowledge-center/lambda-import-module-error-python