OpenCVをAWS Lambda + Python + Serverless Frameworkで動かす

OpenCVをAWS Lambda + Python + Serverless Frameworkで動かしてみました。

先日、[PillowをLambdaで動かす](https://suzuki-navi.hatenablog.com/entry/2021/02/02/163647)記事を書きましたが、このときと違って、OpenCVはyumでインストールの必要なshared objectに依存しているのでだいぶ面倒でした。

# 手順概要

1. serverless-python-requirements インストール
2. Pythonサンプルコードを記述
3. requirements.txt と serverless.yml と Dockerfile にOpenCV動作に必要な事項を記述
4. あとはデプロイ

OpenCV特有は3のみです。


# 手順詳細

## serverless-python-requirements インストール

AWS Lambda + Python + Serverless FrameworkにPythonのパッケージをインストールする方法は以前の記事に書きました。

- [Serverless FrameworkとAWS Lambda with Pythonの環境にpipインストール](https://qiita.com/suzuki-navi/items/b5f513f21d37365c248f)

これに従って、まずは `serverless create` して、`serverless-python-requirements`プラグインをインストールします。

```console
$ serverless create --template aws-python3

$ serverless plugin install -n serverless-python-requirements
```

ServerlessFrameworkのバージョンはv2.18.0でした。

## Pythonソースコード

`handler.py` の内容です。OpenCVを参照できることを確認できる最小限です。

```python
import cv2

def hello(event, context):
    print("Hellow, OpenCV!")
    print(cv2.__version__)
```

## requirements.txt

OpenCVのパッケージ名を記述します。この1行のみです。

```
opencv-python
```

## serverless.yml

`serverless.yml` の記載がもっとも面倒でした。成功例を書きます。

```yaml
service: sample

frameworkVersion: '2'

provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
  region: ap-northeast-1

functions:
  hello:
    handler: handler.hello
    events:
      - httpApi: "*"

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    dockerizePip: true
    dockerFile: Dockerfile
    dockerExtraFiles:
      - /usr/lib64/libGL.so.1
      - /usr/lib64/libgthread-2.0.so.0
      - /usr/lib64/libglib-2.0.so.0
      - /usr/lib64/libGLX.so.0
      - /usr/lib64/libX11.so.6
      - /usr/lib64/libXext.so.6
      - /usr/lib64/libGLdispatch.so.0
      - /usr/lib64/libxcb.so.1
      - /usr/lib64/libXau.so.6
```

## Dockerfile

`Dockerfile` を作成します。`serverless.yml`からファイル名で参照しています。

```docker
FROM lambci/lambda:build-python3.8

RUN yum install -y mesa-libGL
```


## デプロイと実行

デプロイ。

```console
$ serverless deploy -v
```

デプロイされたLambdaを実行するとCloudWatch Logsに以下が出力されました。

```
Hellow, OpenCV!
4.5.1
```


# serverless.yml を書くまでの道のり

最初は以下だけで動かそうとしました。

```yaml
service: sample

frameworkVersion: '2'

provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
  region: ap-northeast-1

functions:
  hello:
    handler: handler.hello

plugins:
  - serverless-python-requirements
```

これでデプロイしてLambdaを実行すると、CloudWatch Logsに以下のエラーが吐かれました。

```
[ERROR] Runtime.ImportModuleError: Unable to import module 'handler': libGL.so.1: cannot open shared object file: No such file or directory
```

`libGL.so.1` というファイルが不足しているので、これを追加すればよいのですが、これは環境に依存していそうなので、手元にある同じファイル名をコピーしただけではたぶん動きません。Lambdaの動くAmazon Linux環境でこのファイルを用意する必要があります。

これをするために`serverless.yml` にdockerの記述をします。以下の記述です。

```yaml
custom:
  pythonRequirements:
    dockerizePip: true
    dockerFile: Dockerfile
    dockerExtraFiles:
      - ...
```

`Dockerfile`も用意します。

```docker
FROM lambci/lambda:build-python3.8

RUN yum install -y mesa-libGL # OpenCVに必要なパッケージをインストール
```

これを書くだけでserverlessがデプロイ時にDockerを起動して、Lambdaの動くAmazon Linux環境を再現し、その中でyumインストールしてくれます。`serverless.yml`の`dockerExtraFiles`に記載したファイルを、yumインストール後に抜き出して、Lambdaデプロイイメージに同梱してくれます。

`dockerExtraFiles`に書いたリストは、デプロイして実行時のエラーメッセージから1つずつ書き足して、成功するまで繰り返しました。

エラーメッセージにはsoファイル名しか表示されませんので、以下のコマンドでLambdaの動くAmazon Linux環境の中に入ってみて、`yum install -y mesa-libGL`してから、soファイルのありかを探しました。

```console
$ docker run -it --rm lambci/lambda:build-python3.8 bash
```

Amazon Linux上では `/usr/lib64/libGL.so.1` は `/usr/lib64/libGL.so.1.7.0` へというように、すべてシンボリックリンクになっていますが、`serverless.yml`には実態ではなくシンボリックリンクだけ記述すれば動きました。

## 注意事項

ここに書いているsoファイルのリストはサンプルPythonコードを動かすために最小限のものです。エラーメッセージを見て、足りないsoファイルを追加するというのを繰り返しましたのみです。従ってOpenCVのすべての動作がこれだけで足りてるかどうかはわかりません。

## ハマりどころ1

`.serverless/requirements/`の中にLambdaのイメージが展開されるのですが、Dockerで構築されるためか、soファイルがroot権限になり、試行錯誤の過程で serverless.yml の変更が権限不足で反映できないというトラブルがありました。原因がわかればroot権限でそのディレクトリを削除することで解決しましたが、それに気が付くまで時間をだいぶ消耗しました。

## ハマりどころ2

serverlessが内部でDockerを利用するため、serverless自体をDockerの中で動かしたらデプロイ時にDockerでエラーになりました。Dockerのvolumeも使っていたので、Dockerのsocket共有でもうまくいかず、あきらめました。

## ハマりどころ3

最初はLambdaのLayerにOpenCVを入れたかったのですが、`LD_LIBRARY_PATH` がLayerには通っていないため、OpenCVを動作させることはできませんでした。