Chaliceのすゝめ AWSにWebApiをサーバーレスで構築する

# 前書き
皆さんは`Chalice`をご存知でしょうか、自分はこのサービスを知ったとき
軽くカルチャーショックを受けました。いままでAWSのサービスを立ち上げる時はインフラを構築して
サービスの開発というステップが必要でした。CloudformationやAWS CLIを使えばある程度自動化できたのですが、設定ファイルは必要でした。それが`Chalice`を使うとコードの記載とインフラの設定が同時にできるのです。
本記事では最も基本的な使い方であろう WebApiのサーバーレスの開発・デプロイ方法について記載したいと思います。

# 環境

本記事の環境は
- MacOS Mojave 10.14.1
- Python 3.6.7
- chalice 1.6.1
で作成しています。

# Chaliceとは
AWSが[Github](https://github.com/aws/chalice)上に公開しているPythonベースの Serverless Microframework です。ほんの数コマンドでAWSのサーバーレスサービスを立ち上げることができます。

Chalice Project: https://github.com/aws/chalice

また、[Couldformation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html)のようにインフラのためにyaml、jsonで設定ファイルを記述するのではなく、 **pythonで書いたコードがそのまま** デプロイ時にコードに記載した内容を実現するようにAWSの複数のサービスが立ち上げられます。

本記事は中でも最も一般的な用途であろうと思われる
ApiGateway + lambda の構成でのWebApiServiceのlocalデバッグとAWS環境へのデプロイを説明したいと思います。

Chalice自体は、Serverless Microframework と謳われているだけあって S3, AWS SNS, AWS SQSと連携したり、CloudFormationのtemplateをコードから生成してくれたり、CodeCommit、CodeBuild、CodePipelineと連携してCI/CDパイプラインの構築してくれたりと、Pythonを使ってmicroserviceを立ち上げるならこれだけで十分じゃないかと思わせるほど機能が充実しています。

# 開発の流れ
基本的には以下のような開発の流れになると思います。
1. 仮想環境の作成
2. chaliceのインストール
3. chaliceプロジェクトの作成
4. localサーバーの立ち上げ
5. サービスの実装
6. テスト
7. デプロイ
8. 削除(必要ならば)

※5~7を繰り返す
では手順を見ていきましょう。

# venvを使った仮想環境の構築
この作業は必須ではありませんがデバッグの観点から仮想環境を使ってデプロイ後本番環境となるべく同じパッケージで作業することをおすすめします。

```sh
# venvを利用して作業スペースを作成
$ python -m venv hello-world-workspace
# アクティベート
$ source ./hello-world-workspace/bin/activate
# (hello-world-workspace)のプレフィックスがプロンプトに表示される。
# 何も表示されないことを確認
$ pip freeze
```

# Chaliceのインストール
ChaliceはPyPIに登録されているので`pip`で簡単にインストールすることができます。

```sh
$ pip install chalice
$ chalice --version
# chalice 1.6.1, python 3.6.7, darwin 18.2.0
$ chalice --help
# Usage: chalice [OPTIONS] COMMAND [ARGS]...
# ...
```

インストールに成功すると`chalice`コマンドが実行できるようになります。

# AWSのクレデンシャルの設定
下記の作業は他のAWS CLIツール(aws-cli、amplifyなど)を使っていてすでに`~/.aws/credentials`、 `~/.aws/config`の設定が終わっている場合は必要ありません。

```sh
$ mkdir ~/.aws
$ cat >> ~/.aws/credentials
[default]
  aws_access_key_id=YOUR_ACCESS_KEY
  aws_secret_access_key=YOUR_SECRET_ACCESS_KEY
$ cat >> ~/.aws/config
[default]
  region=YOUR_REGION
```

これでApiGatewayとlambdaのデプロイ権のあるIAMを持つAWS Userの`ACCESS_KEY`が設定されていれば後述する`deploy`作業ができるようになります。

以上、インストール作業は終わりになります。

# Chaliceプロジェクトの作成
`chalice`コマンドを使うことでプロジェクトのテンプレートを作成することができます

```sh
$ chalice new-project helloworld
$ cd helloworld
$ ls -la
drwxr-xr-x   .chalice
-rw-r--r--   app.py
-rw-r--r--   requirements.txt
```
`.chalice`、 `app.py`、`requirements.txt`のようなファイル、フォルダが作成されます

`app.py`を見てみましょう

```py
from chalice import Chalice

app = Chalice(app_name='helloworld')


@app.route('/')
def index():
    return {'hello': 'world'}
```

これだけで`{"hello": "world"}`というjsonを返すWebApiを作成することができます。

`@app.route('/')`のデコレータールーティングを定義して関数で振る舞いを定義します。
[Flask](http://flask.pocoo.org)を使ったことのある人なら学習コストほぼゼロで`chalice`のAPI定義が理解できると思います。

さらに詳細をしりたい方は

- http://flask.pocoo.org
- https://github.com/aws/chalice/blob/master/README.rst

を見てください

# ローカルでサーバーの立ち上げ
プロジェクトルートフォルダで

```sh
$ chalice local
# Serving on 127.0.0.1:8000
```
を実行すると`127.0.0.8000`にローカルサーバーが立ち上がります。
以下`chalice` コマンドはプロジェクトのルートフォルダで実行することを想定しています。

```sh
$ curl hocalhost:8000
# {"hello": "world"}
```
のように実際に動作を確認することができます。
また、`app.py`を変更し保存すると自動でサーバーも更新されます

```sh
# Restarting local dev server.
# Serving on 127.0.0.1:8000
```

# デプロイする
いよいよ、AWSのApiGatewayとLambbaを立ち上げてみましょう
難しいことは何もなく

```sh
$ chalice deploy
# Resources deployed:
#  - Lambda ARN: arn:aws:lambda:XXXX:XXXX:function:helloworld-dev
#  - Rest API URL: https://XXXX.execute-api.XXXX.amazonaws.com/api/
```
のように成功すると作成されたリソースの情報が表示されます
上記のURLを実行するとlocalhostで実行した結果と同じ結果を得ることができます。

```sh
# ApiGatewayのルートを取得できる
$ chalice url
# https://XXXX.execute-api.XXXX.amazonaws.com/api/
$ curl `chalice url`
# {"hello": "world"}
```

このように非常に簡単にApiGateway+Lambdaの環境を構築することができます。

# サービスを削除する
```sh
chalice delete
```
これを実行するだけです。アクセス出来なくなったことを確認してみてください。

#少しだけAPIを拡張してみる
`/users/{user}`のようにパスを指定して呼び出すと`{"hello": "{user}"}`のようなjsonを返すWebApiを作成してみます。

app.py

```py
from chalice import Chalice

app = Chalice(app_name='helloworld')

@app.route('/')
def index():
    return {'hello': 'world'}

# この部分を追加
@app.route('/users/{user}')
def hello_user(user):
    return {'hello': user}
```
のように`{xxx}`とデコレータのなかで定義すると関数の引数を`xxx`のようにアクセスできるようになります。

```sh
$ curl localhost:8000/users/tom
# {'hello': 'tom'}
# デプロイすればApiGateway+Lambdaの設定が更新される
$ chalice deploy
$ curl `chalice url`users/tom
# {'hello': 'tom'}
```

# さらにデバッグをしやすくする
`app.debug=True`としておくと実行時の例外情報がクライアントに通知されるようになります

app.py

```py
from chalice import Chalice

app = Chalice(app_name='helloworld')
# 実行時のエラーがクライアントに通知されるようになる
app.debug = True

@app.route('/error')
def error():
    temp_dic = {'key': 'test'}
    return {'nokey': temp_dic['nokey']}
```

```sh
$ curl localhost:8000/error
# app.debug = Falseの場合
# {"Code": "InternalServerError", "Message": "An internal server error occurred."}

# app.debug = Trueの場合
# Traceback (most recent call last):
#  File "/Users/xxxx/project/test_venv/lib/python3.7/site-packages/chalice/app.py", line 726, in _get_view_function_response
#    response = view_function(**function_args)
#  File "/Users/xxxx/project/test_venv/helloworld/app.py", line xx, in error
#    return {'nokey': temp_dic['nokey']}
# KeyError: 'nokey'
```

のようになります。

# さらなるChaliceの使い方を求めて
今回の記事でchaliceの最も基本的な開発の準備方法とデプロイ方法を説明したつもりでいます。
これをきっかけでChaliceに興味を持たれた方は
https://github.com/aws/chalice のチュートリアルを一通り実践してみることをおすすめします。

# あとがき
この記事を読んでいただきありがとうございます。
私自身はコードを書くのは好きなのですが、インフラ構築やCI/DIの環境構築はなるべく簡単に済ませたいと考えている人間なので`chalice`は個人的にツボにはまりました。
本番環境での運用方法を考えると落とし穴があるのかもしれませんが、サービス開発者がとりあえずサービスを作るためのツールと考えるととても素晴らしいツールであると思います。
まだまだ、日本語記事も少なく学習が困難だと思いますが、この記事をきっかけに`chalice`に興味を持っていただければ幸いです。

今回紹介しきれなかった、Apiの認証、CORS対応、Cloudformationのテンプレートの作成やSQS連携、CI/CDパイプラインの構築などの手順を記事にできたらと思います。
とくに認証とCORS対応はWebApiを作る上では必須になるとおもうので近いうちにあげる予定です。

認証について記事を記載しました。こちらになります。
https://qiita.com/tukkyr/items/d02be9c9e4fe7d974b1c

SQSのlambdaイベントに関して: https://github.com/aws/chalice/issues/884

参考:
venv: https://qiita.com/fiftystorm36/items/b2fd47cf32c7694adc2e
chalice: https://github.com/aws/chalice