aws-sam-cliのインストールとHello Worldの実行(Ubuntu 18.04 LTS, CentOS 7)

# はじめに
AWSでサーバーレスアプリケーションの開発するときに非常に便利なツールであるaws-sam-cliをインストールするときのメモ。
元々はaws-sam-localという名前だったようだが、バージョンアップに伴い改名された模様。
また、以前はnodejsベースだったが、全面的にpythonで書き直された。

# 前提
vagrantでCentOSとUbuntuを用意して、それぞれでaws-sam-cliをインストール、Hello Worldするまでの流れを記載。
なのでaws-sam-cliってそもそも何なのよ？みたいな前提知識的なことは書かない。インストールから動作確認までに特化して書く。
aws-sam-cliではHello Wolrd用のテストプロジェクトを作成する。このときLamdaの言語としてpython3.6を指定する。
そして、API Gatewayと連携させる形でHello Worldを実行させる。

ちなみにaws-sam-localを使っていた場合は、それを消してから入れ直す必要があるが、今回はサーバ自体をvagrantで新規用意するので、そのあたりの手順は記載しない。
また、vagrant自体のインストールなど、他のQiita記事などで大量に情報が得られる部分についても記述しない。

# 準備

## Ubuntu 18.04 LTSの用意

```bash
$ vagrant init ubuntu/bionic64
$ vagrant up
$ vagrant ssh
```

## CentOS 7の用意
※2019/01/16時点では、7.5.1804

```bash
$ vagrant init centos/7
$ vagrant up
$ vagrant ssh
```

# インストール
## Ubuntu 18.04 LTS

18.04の場合、デフォルトでPython3.6がインストール済みなので、これをそのまま使う。
pipを使ってaws-sam-cliをインストールすることになるが、まずはpipを用意する必要がある。
ただし、apt経由ではなくget-pipスクリプトを使ってインストールする。
※なぜかと言うと、aptで入れたpipをupgradeすると挙動がおかしくなるから
※「[pip install --upgrade pip (10.0.0) 後の奇妙な挙動について](http://icchy.hatenablog.jp/entry/2018/04/17/064443)」を参考にさせて頂きました。ありがとうございます。
※もしaptで入れたい場合は、こちらを参考に。[How to install Pip on Ubuntu 18.04](https://linuxize.com/post/how-to-install-pip-on-ubuntu-18.04/)

また、AWS Lambdaの実行環境として、dockerを利用する。そのためにdockerも準備する必要がある。
こちらも、aptで入れられるが、docker公式としてはapt経由で入れたdockerは非公式/非サポートということなので、公式の手順でインストールする。

### pipのインストール
※[ PyPA » pip 19.0.dev0 documentation](https://pip.pypa.io/en/latest/installing/#installing-with-get-pip-py)

```
$ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
$ python3 get-pip.py
$ pip install --upgrade pip
```

get-pip.pyを実行時、もし下記のようなエラーが発生した場合は、 `python3-distutils`をインストールする。

```
$ python3 get-pip.py 
Traceback (most recent call last):
  File "get-pip.py", line 21373, in <module>
    main()
  File "get-pip.py", line 197, in main
    bootstrap(tmpdir=tmpdir)
  File "get-pip.py", line 82, in bootstrap
    import pip._internal
  File "/tmp/tmp8sqx7zqp/pip.zip/pip/_internal/__init__.py", line 40, in <module>
  File "/tmp/tmp8sqx7zqp/pip.zip/pip/_internal/cli/autocompletion.py", line 8, in <module>
  File "/tmp/tmp8sqx7zqp/pip.zip/pip/_internal/cli/main_parser.py", line 8, in <module>
  File "/tmp/tmp8sqx7zqp/pip.zip/pip/_internal/cli/cmdoptions.py", line 14, in <module>
ModuleNotFoundError: No module named 'distutils.util'
```

```
$ apt install python3-distutils
```

### dockerのインストール
※「[Ubuntu 18.04にDockerをインストールする](https://qiita.com/myyasuda/items/cb8e076f4dba5c41afbc)」 を参考にさせて頂きました。ありがとうございます。

```
$ sudo apt-get update
$ sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable test edge"
$ sudo apt-get update
$ sudo apt-get install docker-ce
$ docker --verison
```
バージョンが表示されたら正しくインストールされている。

### aws-sam-cliのインストール
awscliも必要になるので一緒に入れておく。

```
$ pip install awscli aws-sam-cli
$ sam --version
SAM CLI, version 0.10.0
```
正しくsamが実行できて、バージョンが表示されたら完了。
場合によっては、[--user]を使って、ユーザーディレクトリ内にインストールしたほうが良いかも。
自身の環境に応じて、どうぞ。

Ubuntu環境にてaws-sam-cliをインストールする際、下記のようなエラーが発生する場合は、`python3-dev`をインストールする。

```
compilation terminated.
    error: command 'x86_64-linux-gnu-gcc' failed with exit status 1
```

```
$ apt install python3-dev
```

## CentOS 7.5.1804
CentOS 7の場合、デフォルトでPython2.7がインストール済みなので、これをそのまま使う。
Ubuntu編と同じくまずはpipをインストールする。
こちらもyumでインストールできるが、aptで入れたときと同じ問題を抱えているので、大人しくget-pipスクリプトを使ってインストールする。

dockerについてもUbuntu編と同じ。yum経由ではインストールしない。
※yumで入れた場合、本記事最後のsamの動作確認の際にエラーが出て失敗してしまった(Unable to import module 'app': No module named 'app')
※原因究明はしていないのではっきりとした理由は言えないが、とりあえずyumではなく、公式手順に沿ってインストールしたほうがスムーズだと思う

### pipのインストール

```
$ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
$ python get-pip.py
$ pip install --upgrade pip
```

### dockerのインストール
※「[CentOS7にDockerをインストールする](https://qiita.com/inakadegaebal/items/be9fecce813cebec5986)」 を参考にさせて頂きました。ありがとうございます。

```
$ yum install -y yum-utils device-mapper-persistent-data lvm2
$ yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
$ yum makecache fast
$ yum install docker-ce
$ docker --verison
```

### aws-sam-cliのインストール
awscliも必要になるので一緒に入れておく。

```
$ pip install awscli aws-sam-cli
$ sam --version
SAM CLI, version 0.10.0
```
正しくsamが実行できて、バージョンが表示されたら完了。

## インストール後の共通手順
共通手順なので、Ubuntu 18.04 LTSでもCentOS 7でもどちらでも実施する。

### dockerの起動と自動起動の有効化

```
$ systemctl start docker
$ systemctl enable docker
```

一般権限のユーザを使っていて、sudoを打ってdocker操作するのが面倒くさい場合はこちらも実施。

```
$ sudo usermod -aG docker [username]
```

※dockerグループはここに載せている手順を実施していれば勝手に作られている。

### dockerの動作確認

```
$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete
Digest: sha256:2557e3c07ed1e38f26e389462d03ed943586f744621577a99efb77324b0fe535
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
```

### aws cliの初期設定

`aws configure`コマンドを使って、初期設定を行う。先にこれを行っておこないと、この後aws関連の操作を行うとき、大体エラーになる。
最終的にaws-sam-cliを使って、AWSにデプロイするなら、デプロイ先のAWSアカウントのAccess KeyやSecret Access Keyを正しい値で設定しなくてはいけない。
ただ、このあたりの話はQiitaでもその他メディアでもたくさん書かれているので、ここでは詳細には書かない。
ひとまず、今回はローカルでHello Worldするだけなので、**リージョンを指定するだけで良い。**

```
$ aws configure
AWS Access Key ID [None]:
AWS Secret Access Key [None]:
Default region name [None]: ap-northeast-1
Default output format [None]:
```

参考: regionを指定しなかったときのエラー

```
$ aws dynamodb --endpoint-url http://localhost:8000  scan --table-name test
You must specify a region. You can also configure your region by running "aws configure".
```

# aws-sam-cliの動作確認

## プロジェクトの作成

`sam init`コマンドでプロジェクトを作成する。
まえがきで書いた通り、言語としてPython3.6を指定する。(--runtime)

```
$ sam init --runtime python3.6 --name testpj
```

## Hello Wolrdの実行

API GatewayとLambdaを連携させる前提で、API Gatewayを通って流れてきたHTTPリクエストを模倣するイベントを作成する。
`sam local generate-event apigateway aws-proxy`コマンドを実行する。
当然、API Gateway以外のサービスのイベントを作成することも可能なので、興味があればヘルプを読んでみて欲しい。
出力結果を見れば、API Gateway-Lambdaの開発をやったことのある人なら、非常に見覚えのあるjsonが出力されていることがわかるはず。(長いのでここには載せない)

```
$ cd testpj
$ sam local generate-event apigateway aws-proxy > ./event.json
```

少しコードを書き換える。

```diff:hello_world/app.py
- import requests
---
+ from botocore.vendored import requests
```

**実行**
このとき、事前に作成したAPI Gatewayのイベントファイルを指定して、それを使ってLambdaを実行させる。
`sam local invoke`コマンドで実行可能。-eでイベントファイルを指定する。
初回実行時は、dockerコンテナイメージをインターネットからダウンロードしてくるので、少し時間がかかる。

```
$ sam local invoke "HelloWorldFunction" -e event.json
2019-01-16 03:10:58 Invoking app.lambda_handler (python3.6)

Fetching lambci/lambda:python3.6 Docker container image......
2019-01-16 03:11:01 Mounting /home/testuser/testpj/hello_world as /var/task:ro inside runtime container
START RequestId: 56226e0e-1dbf-47aa-860c-4ede111eec6e Version: $LATEST
END RequestId: 56226e0e-1dbf-47aa-860c-4ede111eec6e
REPORT RequestId: 56226e0e-1dbf-47aa-860c-4ede111eec6e Duration: 507 ms Billed Duration: 600 ms Memory Size: 128 MB Max Memory Used: 23 MB

{"statusCode": 200, "body": "{\"message\": \"hello world\", \"location\": \"118.15.107.117\"}"}
```

## なんでせっかくのデフォルトコードを書き換えたん？

結論から言うと、requestsモジュールがLambdaのコンテナ環境内に存在しないから。
書き換えずに実行すると、以下のようにエラーになる。

```
$ sam local invoke "HelloWorldFunction" -e event.json
2019-01-16 03:24:52 Invoking app.lambda_handler (python3.6)

Fetching lambci/lambda:python3.6 Docker container image......
2019-01-16 03:24:52 Mounting /home/testuser/testpj/hello_world as /var/task:ro inside runtime container
START RequestId: 8fb25aea-9b2e-4151-9ef1-2c72a4ab19ff Version: $LATEST
Unable to import module 'app': No module named 'requests'
END RequestId: 8fb25aea-9b2e-4151-9ef1-2c72a4ab19ff
REPORT RequestId: 8fb25aea-9b2e-4151-9ef1-2c72a4ab19ff Duration: 2 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 19 MB

{"errorMessage": "Unable to import module 'app'"}
```


LambdaでPythonを使うと、基本的にライブラリ/モジュールは一切入っていない真っ更な状態で、アプリが起動する。
一切と言っても、ほんの一部、AWSがこれは皆使うでしょー必要でしょーと認めた(？)ごく一部に限りインストールされている。
とはいえ、ほとんどない。何も使えないと思っていい。
この場合、どうするかというと、事前に自分たちでローカルディレクトリに必要な依存ライブラリ/モジュールをインストールしてzipで固めて、Lambdaにインポート、といった形をとる。とらなければならない。非常にめんどうくさい。

実際のapp.pyのコードを見てみると、jsonモジュールとrequestモジュールをインポートしている。
この内、jsonはLambdaコンテナ環境内に存在するが、requestsは存在しない。

```
$ head hello_world/app.py
import json

import requests

def lambda_handler(event, context):
    """Sample pure Lambda function

    Parameters
```

ただ、`botocore.vendored` の中にrequestsは入っているので、そこから読み出せば良い。というわけで書き換えてエラーをひとまず回避した。
他のやり方として、以下のようにモジュールをインストールしてしまう、という手もある。

```
$ cat hello_world/requirements.txt
requests==2.20.0
$ pip install -r requirements.txt -t ./
```

というより、本格的にLambdaの開発をやるなら、このやり方をとることになる。
requestsはたまたまbotocore.vendoredに含まれていたので、わざわざインストールしなくても良かったが、多くの場合そんな都合良くはいかないので。

このあたりの話は、下記Stack OverFLowの質問を参考にさせて頂きました。ありがとうございます。
[aws lambda Unable to import module 'lambda_function': No module named 'requests'](https://stackoverflow.com/questions/48912253/aws-lambda-unable-to-import-module-lambda-function-no-module-named-requests)


# あとがき
以前、AWSでCognito + API Gateway + Lambdaを使ったサーバーレスアプリケーションを開発した経験があったのだが、そのときはこういったツールは使わず、手動でしこしこ頑張っていた。
今回、新サービスの開発の一環で全面サーバーレスアーキテクチャで作ろうと思い、最初が肝心なので、遅ればせながらsamを使おうと思い至った。
当分の間、この辺のことをやり続けるので、色々Qiitaで書いていきたいなぁと思っている。(がんばろう)