AWS Chaliceを触ってみた

#AWS Chaliceって？
[Amazon API Gateway](https://aws.amazon.com/jp/api-gateway/) や [AWS Lambda](https://aws.amazon.com/jp/lambda/) をCLIから半自動でさくっと作れるものらしい。
AWS Lambda（とAmazon API Gateway）は好きだから触ってみました。

#準備
##Pythonの仮想環境作り
[AWSのChaliceハンズオン](https://aws.amazon.com/jp/blogs/startup/event-report-chalice-handson/)で仮想環境を作ることを推奨しているので、必要に応じて作ります。
###仮想環境を作成するVirtualenvのインストール
```bash
$ pip install virtualenv
```
###仮想環境の作成
```bash
# 『~/.virtualenvs/chalice-handson』は環境名
# AWSのハンズオンに合わせているだけなので、任意の環境名で大丈夫です
virtualenv ~/.virtualenvs/chalice-handson
```
###仮想環境の有効化
```bash
$ source ~/.virtualenvs/chalice-handson/bin/activate
# 『source』は『.』でも代用可能なため以下のコマンドでも上と同じ意味になります
$ . ~/.virtualenvs/chalice-handson/bin/activate
```
###仮想環境の終了
仮想環境を終了したい場合は以下のコマンドです。
仮想環境を終了しても作られた仮想環境のファイルは残るので、再度有効化のコマンドを打つことで仮想環境に入ることができます。

```bash
$ deactivate
```
##Chaliceのインストール
```bash
$ pip install chalice
# インストールできたか確認
$ chalice --version
chalice 1.12.0, python 3.7.3, darwin 19.6.0
```

##AWS認証情報
`~/.aws/credentials` と `~/.aws/config` がない場合は以下のコマンドで設定しておきます。
ないとAWSにデプロイするときに怒られます。

```bash
$ aws configure
```

#プロジェクトの新規作成
それでは準備ができましたので、軽くプロジェクト作成からAWSへデプロイまでの流れを見てみましょう。
最初に以下のコマンドでプロジェクトを作成します。

```bash
# 『helloworld』はAWSのハンズオンで使っていたプロジェクト名で、任意のプロジェクト名を指定可
$ chalice new-project helloworld
```

プロジェクト作成で以下のファイルが作られます。

```bash
.
├── .chalice
│   └── config.json
├── .gitignore
├── app.py
└── requirements.txt
```

自動作成された `app.py` の中身は以下のようになっています。
中身はシンプルで、API Gatewayのエンドポイントのindexにアクセスしたらレスポンスボディとして{'hello': 'world'}を返すようになっています。
レスポンスボディの{'hello': 'world'}はプロジェクト名がhelloworld以外でも{'hello': 'world'}で設定されます。

```python
from chalice import Chalice

app = Chalice(app_name = 'helloworld')

@app.route('/')
def index():
    return {'hello': 'world'}    # プロジェクト名がhelloworld以外でも、ここは {'hello': 'world'} です
```
#ローカルでのテスト
実際に上で説明した挙動になっているか（アクセスしたら{'hello': 'world'}が返ってくるか）ローカル環境で試してみましょう。
以下のコマンドでローカルサーバーを起動できます。:+1: 簡単でいいね！

```bash
$ chalice local
Serving on http://127.0.0.1:8000
```

ブラウザでアクセスしてみると `{"hello": "world"}` が表示されるはずです。

#AWSにデプロイ
大丈夫そうなので、AWSにデプロイして外からもアクセスできるようにしてみましょう。
デプロイは簡単で以下のコマンドを実行するだけです。

```bash
$ chalice deploy
```
するとAWS上で以下が自動で作成されます。

 - IAMロール
 - Lambda関数
 - API Gateway

デプロイを行うと以下の構成になります。

```bash
.
├── .chalice
│   ├── config.json
│   ├── deployed
│   │   └── dev.json
│   └── deployments
│       └── xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-python3.8.zip
├── .gitignore
├── app.py
└── requirements.txt
```

最後に `Rest API URL` としてAPI Gatewayのエンドポイントが表示されるので、ブラウザでアクセスしてみましょう。
Rest API URLが分からなくなった場合は以下のコマンドで表示できます。

```bash
$ chalice url
```

#リクエストハンドリング
ここまででAWS Chaliceの基本的な使い方が見えてきたと思いますので、もう少しコントロールするための機能を見ていきましょう。
##URLパラメータ
以下のように `app.py` の `@app.route('/')` のパスパターンにパラメータを記述することができ、パラメータとして受け取った値をメソッド内の引数として使用できます。

```python
from chalice import Chalice

app = Chalice(app_name = 'helloworld')

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

# 以下を追加
@app.route('/hello/{name}')
def greet(name):
    return {'hello': name}
``` 
`app.py` を編集したら再度 `chalice deploy` を行います。
デプロイが完了したらAPI Gatewayのエンドポイントに `/hello/『任意の値』` をつけてブラウザでアクセスしてみましょう。
`{"hello": "『任意の値』"}` の表示がされたはずです。
『任意の値』として渡された値は数値であっても文字列になるので注意してください。

##HTTPメソッド
以下のように `app.py` の `@app.route('/')`にメソッドを書くことでHTTPメソッドを指定可能。

```python
from chalice import Chalice

app = Chalice(app_name = 'helloworld')

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

@app.route('/hello/{name}')
def greet(name):
    return {'hello': name}

# 以下を追加
@app.route('/hello', methods = ['POST'])
def hello():
    return {'hello': 'POST world'}
```

`app.py` を編集したら再度 `chalice deploy` を行います。
今回追加したPOSTはブラウザからでは確認できないので、Pythonの対話モードでさくっと確認してみましょう。

```bash
$ python
>>> import requests    # requests が入っていなければ、pip install requests
>>> response = requests.post('『API Gatewayのエンドポイント』/hello', data = {})
>>> print(response.text)
{"hello":"POST world"}
```

##メタデータにアクセス
ここからのコードは変更ブロックとレスポンスだけ記載していきます。
メタデータ取得は`app.current_request.『ほにゃらら』`の形で指定します。
###HTTP Method

```python
@app.route('/hello')
def hello():
    return {'metadata': app.current_request.method}

# レスポンス
{"metadata":"GET"}
```

###Query Parameters

```python
@app.route('/hello')
def hello():
    return {'metadata': app.current_request.query_params}

# レスポンス（リクエストは /hello?test1=abc&test2=123&test2=456）
# 同名のパラメータがリクエストで指定された場合、chaliceでは後勝ちになる（test2=123&test2=456 だと test2=456 になる）
{"metadata":{"test1":"abc","test2":"456"}}
```

###Request Body - Raw
バイト型でリクエストボディを取得します。
もし `Content-Type: application/json` であれば、`app.current_request.raw_body` の代わりに `app.current_request.json_body` が使えます。こちらは文字列型で取得します。

```python
@app.route('/hello')
def hello():
    return {'metadata': app.current_request.raw_body}

# リクエスト
$ python
>>> import requests, json
>>> response = requests.post('『API Gatewayのエンドポイント』/hello', data = json.dumps({'hello': 'world'}), headers = {'Content-Type': 'application/json' })
>>> print(response.text)
# レスポンス
{"metadata":{"hello":"world"}}
```
#レスポンスハンドリング
次にレスポンスを見ていきましょう。
##カスタムHTTPレスポンス
任意のステータスコードやヘッダを返したい場合は、レスポンスクラスのオブジェクトに任意の情報を含めて返します。
`import Response` を忘れないようにしてください。
（関係ないですが、`json.dumps`を`json.dump` と`s`抜きで書いてしまい、`{“Code”:“InternalServerError”,“Message”:“An internal server error occurred.“}`のレスポンスが返ってきてミスに気づかずしばらくハマった...:persevere:）

```python
from chalice import Chalice, Response
import json

app = Chalice(app_name='helloworld')

@app.route('/')
def index():
    return Response(
        body = json.dumps({'hello':'world'}),
        headers = {'Content-Type': 'application/json'},
        status_code = 200
    )

@app.route('/text')
def text():
    return Response(
        body = 'Hello, World',
        headers = {'Content-Type': 'text/plain'},
        status_code = 200
    )
```

##エラーHTTPレスポンス
エラーレスポンスを返すためのクラスがあるので、それを利用します。
下の例だと403 ForbiddenErrorです。
こちらもエラークラスのインポートを忘れないように。

```python
from chalice import Chalice, ForbiddenError

app = Chalice(app_name='helloworld')

@app.route('/forbidden')
def forbidden():
    raise ForbiddenError(
        '403!'
    )
```

他には以下のエラークラスがあります。（[【お手軽ハンズオンで AWS を学ぶ】サーバーレスな RESTful API を構築しよう！ Chaliceで実現する Python アプリ開発 | AWS Startup ブログ](https://aws.amazon.com/jp/blogs/startup/event-report-chalice-handson/) 参照）
上で紹介したカスタムHTTPレスポンスを使えば、用意されていないエラーコードのレスポンスを返すことも可能です。（もちろんエラーコードが用意されているエラーをカスタムHTTPレスポンスで返すことも可能です。）

* BadRequestError - return a status code of 400
* UnauthorizedError - return a status code of 401
* ForbiddenError - return a status code of 403
* NotFoundError - return a status code of 404
* ConflictError - return a status code of 409
* UnprocessableEntityError - return a status code of 422
* TooManyRequestsError - return a status code of 429
* ChaliceViewError - return a status code of 500

#CORSの有効化
シンプルに以下の書き方でOKです。

```python
@app.route('/', methods = ['POST'], cors = True)
```

#デコレータ
今までずっと `@app.route('/')` という形で書いてきましたが、これはAPI Gateway + AWS Lambdaでの形になります。
他のデコレータを使うことでAPI Gateway以外のAWS Lambda連携が可能です。
現在サポートしているものは以下になります。

- 単独AWS Lambda：`@app.lambda_function`
- Amazon CloudWatch Events + AWS Lambda：`@app.schedule`
- Amazon S3 + AWS Lambda：`@app.on_s3_event`
- Amazon SQS + AWS Lambda：`@app.on_sqs_message`
- Amazon SNS + AWS Lambda：`@app.on_sns_message`

詳しい使い方やサンプルコードは [Lambda(Python) や API Gateway の管理を Chalice でやってみた](https://qiita.com/chibi929/items/5ff4e8f33f959c3f5da4) がとても参考になります。

#削除
`chalice deploy` でAWSにデプロイされたIAMロール、Lambda、API Gateway（@app.routeの場合）は以下のコマンドで一括削除ができます。

```bash
$ chalice delete
```

#まとめ
AWS Chaliceを使うとだいぶ簡単にサーバーレス環境の構築が出来るようです。
削除も簡単なので気軽に作ったり消したり出来るのがいいですね。
もう少し具体的な例も書こうと思っていたのですが、すでに長い記事になってしまったので別の記事でまとめることにします。

#We're hiring!
AIチャットボットを開発しています。
ご興味ある方は [Wantedlyページ](https://www.wantedly.com/projects/266837) からお気軽にご連絡ください！

#参考記事
- [【お手軽ハンズオンで AWS を学ぶ】サーバーレスな RESTful API を構築しよう！ Chaliceで実現する Python アプリ開発 | AWS Startup ブログ](https://aws.amazon.com/jp/blogs/startup/event-report-chalice-handson/)
- [【AWS Black Belt Online Seminar】Dive deep into AWS Chalice - YouTube](https://www.youtube.com/watch?v=u4LKbQZawaQ)
- [Lambda(Python) や API Gateway の管理を Chalice でやってみた - Qiita](https://qiita.com/chibi929/items/5ff4e8f33f959c3f5da4)