Python3.7ランタイムのAWS LambdaでC拡張ライブラリを使用したい！

本記事はamazonlinuxでpython3.7を使用したい人、lambda上でpythonのC拡張ライブラリを使用したい人向け。

# 動機
Python3.7ランタイムのlambda上でscrapyを動かそうとしたところ以下のエラーでうまく実行できなかった。

```
{
  "errorMessage": "Unable to import module 'spider': cannot import name 'etree' from 'lxml' (/var/task/lxml/__init__.py)",
  "errorType": "Runtime.ImportModuleError"
}
```

調べたところ、どうやらlxmlライブラリはC言語の拡張を使用しており、そのままではlambdaで使えないとのこと。

諦めればいいものの、Python3にこれから移行していくというタイミングで困ることが多そうなので頑張って解決してみる。

PCはMacを使用。

# とりあえずEC2上で動かす

lambdaはサーバレスといっても裏ではamazonlinuxのサーバ上で動くため、とりあえずamazonlinux上で動けばいいのでは？
ということで、早速amazonlinuxのEC2を作成していく。

## EC2の作成・ssh接続

ちなみにPython3.7まではamazonlinux、3.8はamazonlinux2上で動くらしい。
[AWS Lambda ランタイム](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html)

今回はPython3.7を使用するため、amazonlinuxを使用してEC2を作成する。
<img width="1202" alt="スクリーンショット 2019-12-16 23.07.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515105/909cf0c2-8ce8-6640-a887-bf1104a7799d.png">
VPCは適当に。

直接SSH接続したいので、パブリックIPを割り当てておく。
また、セキュリティーグループはtype:ssh プロトコル:TCP ポート範囲:22 ソース:自宅のグローバルIP/32あたりで設定しておく。

keypairを作成し、`Downloads`フォルダに保存。
その後、`~/.ssh`フォルダにpemファイルを保存。

```bash
Downloads $ mv libtestkey.pem ~/.ssh

.ssh $ ls
libtestkey.pem

#秘密鍵として使えるように権限を変更
.ssh $ chmod 600 libtestkey.pem

```

AwsマネジメントコンソールでEC2のパブリックIPを確認。
<img width="451" alt="スクリーンショット 2019-12-16 23.50.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515105/ae3473aa-e647-7ea2-0c35-df3110ee5c76.png">


作成したEC2にsshで接続

```bash
$ ssh -i ~/.ssh/libtestkey.pem ec2-user@3.112.178.228
Warning: Permanently added '3.112.178.228' (ECDSA) to the list of known hosts.

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2018.03-release-notes/
13 package(s) needed for security, out of 26 available
Run "sudo yum update" to apply all updates.
```
無事に接続完了。

## EC2にpython3.7を導入

```bash:ec2
[ec2-user@ip-10-4-0-246 ~]$ yum list | grep python37
$
```
あれ、、、ない、、、。

ということで、下記サイトを参考にソースファイルからビルドしていく。
[CentOS7にPython3.7をインストール（ソースファイルからビルド）](https://hombre-nuevo.com/python/python0068/)

```bash:ec2
# yumのアップデート
[ec2-user@ip-10-4-0-246 ~]$ sudo yum clean all
..
[ec2-user@ip-10-4-0-246 ~]$ sudo yum -y update
..
完了しました!

# 必要なパッケージをまとめてインストール
[ec2-user@ip-10-4-0-246 ~]$ sudo yum install zlib-devel libffi-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel libuuid-devel xz-devel
..
Is this ok [y/d/N]: y
..
完了しました!

# python3.7.4をダウンロード
[ec2-user@ip-10-4-0-246 ~]$ curl -O　https://www.python.org/ftp/python/3.7.4/Python-3.7.4.tgz
..

# 解凍してディレクトリへ移動
[ec2-user@ip-10-4-0-246 ~]$ tar xf Python-3.7.4.tgz
[ec2-user@ip-10-4-0-246 ~]$ cd Python-3.7.4/

# コンパイルしてインストール
[ec2-user@ip-10-4-0-246 Python-3.7.4]$ ./configure --enable-optimizations
..
configure: error: in `/home/ec2-user/Python-3.7.4':
configure: error: no acceptable C compiler found in $PATH
See `config.log' for more details

```
なんか怒られた。
C コンパイラが無い？
とりあえずgccを入れてみる。

```bash
[ec2-user@ip-10-4-0-246 ~]$sudo yum install gcc
..
y
..
完了しました!
```

もう一度実行してみる。

```bash
[ec2-user@ip-10-4-0-246 Python-3.7.4]$ ./configure --enable-optimizations
..
creating Modules/Setup
creating Modules/Setup.local
creating Makefile
```
お、できたっぽい。
ビルドしてみる。

```bash
# ビルド
[ec2-user@ip-10-4-0-246 Python-3.7.4]$ make

.. # 約20分（めっちゃテストしてた）
make[1]: ディレクトリ '/home/ec2-user/Python-3.7.4' から出ます

# インストール
[ec2-user@ip-10-4-0-246 Python-3.7.4]$ sudo make altinstall
..
Successfully installed pip-19.0.3 setuptools-40.8.0
```

pathを通してバージョンを確認。

```bash
[ec2-user@ip-10-4-0-246 bin]$ export PATH=$PATH:/usr/local/bin

[ec2-user@ip-10-4-0-246 bin]$ python3.7 -V
Python 3.7.4

[ec2-user@ip-10-4-0-246 bin]$ pip3.7 -V
pip 19.0.3 from /usr/local/lib/python3.7/site-packages/pip (python 3.7)
```
ようやく準備完了。

## 実際にamazon linux上で動かしてみる
今回はscrapyが依存しているlxmlライブラリを使用してテストしてみる。

lambdaで動かすことを想定し、buildフォルダを作成しその中にtestファイルを作成。

```bash
[ec2-user@ip-10-4-0-246 ~]$ mkdir build
[ec2-user@ip-10-4-0-246 ~]$ cd build/
[ec2-user@ip-10-4-0-246 build]$ ls
[ec2-user@ip-10-4-0-246 build]$ vim test.py
```


```build/test.py
from lxml import etree

etree.LXML_VERSION

```

buildフォルダ内にpip install

```bash
# -t .　を指定することで現在のフォルダにインストール
[ec2-user@ip-10-4-0-246 build]$ pip3.7 install lxml -t .
..
Successfully installed lxml-4.4.2

# 実行！
[ec2-user@ip-10-4-0-246 build]$ python3.7 test.py
(4, 4, 2, 0)
```

動いた！
# lambdaに乗っけてみる

## lambdaデプロイ用に色々作る

ローカルの作業環境にlibtestというフォルダを作成、その後sam initしてテンプレートを作成。

```bash

$mkdir libtest
$cd libtest

#ランタイムはpython3.7を指定
libtest $sam init --runtime python3.7
..
[*] Project initialization is now complete
```

app.pyを編集

```python:sam-app/hello_world/app.py
from lxml import etree

# lambda handler
def lambda_handler(event,context):
    print(etree.LXML_VERSION)
```

## ここで一旦ローカルでpip installするとどうなるか試す

```bash
hello_world $pip3 install lxml -t .
```

テンプレートの修正

```sam-app/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
```

デプロイ用shellの作成

```sam-app/deploy.sh
# バケット名は世界中で一意な任意の名前にする
aws s3 mb s3://rkhcx-libtest-bucket

# デプロイ用パッケージの作成
echo "デプロイ用のパッケージを作成します。"
aws cloudformation package --template-file template.yaml \
--output-template-file output-template.yaml \
--s3-bucket rkhcx-libtest-bucket

# デプロイ
aws cloudformation deploy --template-file output-template.yaml \
--stack-name libtest-stack \
--capabilities CAPABILITY_IAM
```

## デプロイ

```bash
sam-app $sh deploy.sh
..
Successfully created/updated stack - libtest-stack
```

## lambdaが作成されたので、テスト実行してみる

<img width="1292" alt="スクリーンショット 2019-12-21 23.49.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515105/1e8bf81c-a115-fc56-5834-e6beb199cda2.png">

やはりlxmlが読み込めない模様。

## 先ほどEC2上でインポートしたlxmlをローカルにコピー
まずはローカルでインポートしたlxmlライブラリを削除。
その後、EC2からlxmlフォルダをscpでコピーしてくる。

```bash
hello_world $rm -rf lxml*

hello_world $scp -i ~/.ssh/libtestkey.pem -r ec2-user@3.112.178.228:/home/ec2-user/build/lxml .
```

## 再デプロイして実行！

```bash
sam-app $sh deploy.sh
..
Successfully created/updated stack - libtest-stack
```

<img width="1308" alt="スクリーンショット 2019-12-22 0.45.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515105/f3ce2a87-ff7d-be4e-4147-2a6ec70d8319.png">

実行できた！

# 参考文献
[CentOS7にPython3.7をインストール（ソースファイルからビルド）](https://hombre-nuevo.com/python/python0068/)