LambdaからBOX APIを叩こうとしたら思いの外ハマったメモ

# 概要
最近、オンラインストレージサービスの[BOX](https://www.box.com/ja-jp/home)を利用し始め、何らかのデータをS3から抽出してBoxにアップロードする処理を自動化するためにBOXのAPIを使う機会がありました。その際、思うようにいかないハマったポイントがいくつかあったので、備忘として残します。

# 実現したいこと
S3から対象データを検索して取得し、そのデータをBOXにアップロードする仕組みをChatopsで実現する例を考えます。
構成と処理の流れは以下のようになります。
![face imagesの取得自動化.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/dfe2ab4e-420c-70ba-b8f9-0132922695cf.png)

今回はAWS Chatbotを活用してChatOpsを作っていますが、これはよくある構成なので本記事では詳しく触れません。

# 実現までの道のり
## Boxの設定

まずはBox側の設定について紹介します。タイトルにハマったとありますが、Boxは特に詰まるところもなく、すんなり進められました。

### 認証方法の選択

まずアプリケーションを利用するための認証方法を確認します。次のドキュメントに説明が書かれています。

cf. https://developer.box.com/guides/authentication/select/

今回はLambdaからBoxを操作したかったので、対話を必要としないJWTでの認証方法を選択します。JWTはCustom Appしか対応していないのでこれを選択します。

### Custom Appの作成

Custom Appを作成します。`https://${BOX_DOMAIN}/developers/console` のような形式のURLからBox Developer Consoleに入り、「Platformアプリの作成ボタン」を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/d6e31ce4-3a4d-826b-acdc-ee5e69f3139e.png)

アプリタイプはカスタムアプリを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/61e0a174-6597-59b2-de7e-d267aa26cff0.png)

いろいろ聞かれるので、適当に入力していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/2b1cde36-93db-c3df-9b37-fc33b8efcc53.png)

サーバー認証（JWT）使用を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/54246330-a998-c402-1f32-f1e3490acabc.png)


### Custom Appの設定

「構成」タブの「アプリケーションスコープ」からこのアプリで利用したい機能を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/a0e61b2e-a607-8ee8-ddf8-1f4dd445c749.png)

「公開キーの追加と管理」から「公開/秘密キーペアを生成」を押下すると、「xxxxxxx_config.json」のようなファイルがダウンロードされるので大切に保存します。（なくした場合はページ下部の「JSON形式でダウンロード」から再取得できます。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/0134e8ba-05b4-39dd-dde1-a4b7933574a1.png)

Custom Appの設定が完了したら、承認タブの「確認して送信」を押下します。このボタンを押した後、Boxの管理者に連絡して承認してもらいましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/e67286f1-6e2c-1fb2-ec8b-6db53a576d07.png)


### 対象フォルダへのサービスアカウントのアクセス許可

アプリが承認されると、Box Developer Consoleの「一般設定タブ」にサービスアカウントが生成されます。アクセスを許可したいフォルダにこのサービスアカウントを招待します。招待時にどの操作を許可するかの権限レベルを選択します。各権限レベルで可能な操作に関する情報は[このドキュメント](https://support.box.com/hc/ja/articles/360044196413-%E3%82%B3%E3%83%A9%E3%83%9C%E3%83%AC%E3%83%BC%E3%82%BF%E3%81%AE%E6%A8%A9%E9%99%90%E3%83%AC%E3%83%99%E3%83%AB%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)が参考になります。今回はファイルのアップロードとサブフォルダを作れれば良いので、`アップローダー`を選択しました。

![image-2024-12-6_20-4-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/c387f579-31bd-346a-5620-fca260808d47.png)

## Lambdaの実装

BOX側の準備が終わったので、次にLambdaの準備をしていきます。
ここからはタイトルのいくつかうまくいかないポイントについて説明していきます。

環境情報は以下の通りです。
- Lambdaランタイム： Python3.12
- Box SDK： [box-python-sdk-gen](https://github.com/box/box-python-sdk-gen) v1.8.0

### ハマりポイント１ SDKの公式サポートの終了

まずはBOX SDKを使った認証の実装方法を確認すべく、[SDKを使用したJWTの認証について説明されているページ](https://ja.developer.box.com/guides/authentication/jwt/with-sdk/)を参考にしましたが、なぜか認証がうまくいきませんでした（※後述します）。SDKのソースコードを含めて色々と調べていると、使っていた関数が存在しなかったので違和感を覚え始めました。

結果、[上記のページで使われているSDKは公式サポートが終了](https://ja.developer.box.com/guides/tooling/sdks/python/)しており、[次世代のSDK](https://ja.developer.box.com/guides/tooling/sdks/python-gen/)を使うことが推奨されていました。ドキュメントがアップデートされていない箇所が所々見られる点で一つ目のハマりポイントとして挙げさせていただきました。

### ハマりポイント２ 認証が通らない原因がわかりづらい問題

次に簡単な動作確認用のスクリプトを作ってローカルで実行しました。これは適切に動作することを確認しました。ちなみにコード内で指定されている`config.json`は先ほどBoxの設定時にダウンロードしたファイルです。

```python
from box_sdk_gen import BoxClient, BoxJWTAuth, JWTConfig
 
def boxAuth():
    jwt_config = JWTConfig.from_config_file('config.json')
    auth = BoxJWTAuth(config=jwt_config)
    return auth
 
def main():
    auth = boxAuth()
    client = BoxClient(auth=auth)
 
    folder_id = '0' #適当なフォルダID     
    items = client.folders.get_folder_items(folder_id)
    for item in items.entries:
        print(item.name)

if __name__ == "__main__":
    main()
```

上記のコードをLambdaで実行可能な形に書き換えてテストしたところ、以下のようなエラーが発生しました。

```
'NoneType' object has no attribute 'encode'
```
この時のスタックトレースは以下が出力されていました。
```
"File \"/var/task/box_sdk_gen/box/jwt_auth.py\", line 330, in retrieve_token\n    new_token: AccessToken = self.refresh_token(network_session=network_session)\n",
"File \"/var/task/box_sdk_gen/box/jwt_auth.py\", line 305, in refresh_token\n    assertion: str = create_jwt_assertion(claims, jwt_key, jwt_options)\n",
"File \"/var/task/box_sdk_gen/internal/utils.py\", line 337, in create_jwt_assertion\n    return jwt.encode(\n"
```

このエラーが発生した箇所は[ここ](https://github.com/box/box-python-sdk-gen/blob/d9cff4c6adc9c5cc9ce1edf73dffe8ac5979ce71/box_sdk_gen/internal/utils.py#L336)です。ここで呼び出されるencode関数にデバッグログを仕込んだものの、関数自体が呼び出されていないようでした。[importされている箇所](https://github.com/box/box-python-sdk-gen/blob/d9cff4c6adc9c5cc9ce1edf73dffe8ac5979ce71/box_sdk_gen/internal/utils.py#L12-L17
)をよくみると、jwtモジュールでImportErrorが発生した場合はNoneが設定されています。

ここでインポートに失敗しているため、`encode`はjwtモジュールのものではなく、Pythonの標準メソッドを呼び出しているようでした。jwtにはNoneが入るので、エラーメッセージには合点がいきました。
一方でエラーを握りつぶしていた理由までは追えていません。jwt以外の認証方式の場合はそもそもjwtモジュールがインストールされないケースもあるからでしょうか。（とはいえ警告くらい出してくれたらもう少し早く気づけたのに...と思いました。）

### ハマりポイント３ LambdaとローカルPCのアーキテクチャの違い

ポイントの二つ目からの流れになりますが、試しにimportからtryを外してみたところ次のようなエラーメッセージが得られました。

```
[ERROR] Runtime.ImportModuleError: Unable to import module 'index': /var/task/cryptography/hazmat/bindings/_rust.abi3.so: cannot open shared object file: No such file or directory
```

これはよくあるやつ（？）なのでハマってはいないのですが、ローカルPCがArm64なのに対してLambdaがx86_64だったので、ローカルで生成したzipファイルを使っていたことが問題でした。
アーキテクチャをどちらかに合わせればよいのですが、今回はDocker上にx86_64用のビルド環境を作成してzipファイルを生成することで回避しました。

```dockerfile
FROM public.ecr.aws/lambda/python:3.13

# 必要なパッケージをインストール
RUN microdnf install -y zip
COPY requirements.txt ./
RUN pip install --target /var/task -r requirements.txt

# Lambda関数のコードをコピー
COPY index.py /var/task

# ZIPファイルを作成
RUN cd /var/task && zip -r /source.zip .
```

次のコマンドを実行して得られたzipファイルを利用して解決しました。

```bash
$ docker build -t lambda-build --platform linux/x86_64 .
$ container_id=$(docker run -d -it lambda-build bash)
$ docker cp ${container_id}:/source.zip .
```

Arm64に合わせたい場合はLambdaのアーキテクチャを変更し、上記と同様の手順で docker buildコマンドに`--platform linux/arm64`を指定すればOKです。私の環境ではDockerのビルド環境を利用しない場合、LambdaとローカルPCのOSの違いによる別のエラーが出たので、いずれにしてもビルド環境は必要になります。

# 最後に

今回は初めてBOXを利用して、私が勝手にハマったポイントを紹介しました。今後同じ場面に遭遇した方の解決スピードが1秒でも早くなることを願っています。