(ほぼ)AWS初心者が Amplify & Chalice ハンズオンに参加した話

この記事は [Opt Technologies Advent Calendar 2019](https://qiita.com/advent-calendar/2019/opt-technologies) 9日目の記事です。


# 概要

AWS loft Tokyoで定期開催されるハンズオンイベント「 **AWS Amplify & Chalice ハンズオン #03 〜怠惰なプログラマ向け お手軽アプリ開発手法〜** [^1] 」に参加してきた話。ちなみにAWS自体ほぼ初心者のワタシです。
イベントの雰囲気をざっくりあげると以下です。

- ハンズオンの２コース( Amplify と chalice の自由選択)どちらを取り組むか挙手すると Amplify:chalice = 7:3 くらいだった
- 教材を提供しながらマイペースに取り組む(コピペで十分動くがコードの中身で気になることも聴いて欲しい)
- サポータはつくが今回は dev support スポット(AWS Loft Tokyo内)に一人以上常駐する形式

AWSアカウントをもっていれば開発環境(電源, wifiなど)の整った AWS Loft Tokyo を無料利用できるので、そのきっかけにするイベントなのではとも感じました。


## Amplify とは

[AWS Amplify ハンズオンより](https://feature-webpush.dma9ecr5ksxts.amplifyapp.com/)

> サーバーレスなバックエンドをセットアップするための CLI、フロントエンドで利用できる UI コンポーネント、CI/CD やホスティングのためのコンソールを含む Web およびモバイルアプリ開発のためのフレームワークです。

用意されるハンズオンコース

- チャット機能の実装
- AI 機能の実装
- イベント・属性分析機能の実装
- [Web Pushの実装](https://feature-webpush.dma9ecr5ksxts.amplifyapp.com/amplify/webpush) [new]

モバイルアプリ構築(作成、設定、構築)からバックエンドのプロビジョニングにより管理を統合したフレームワークです。つまり、Amplify さえ使いこなせればモバイルアプリをリリースできてしまいます。
参加した回では、ServiceWorker コンポーネントを利用してブラウザ通知の実装コースが新しく追加されていました(WebPush の実装)。 
Amplify framework に興味のある方は[コチラ](https://feature-webpush.dma9ecr5ksxts.amplifyapp.com/amplify/basicsteps/)を試してみてください。
自分はイベントに参加するまで知らなかったので次回はこちらを挑戦してみようかな(やるとは言っていない)。
[AWS AmplifyFramework](https://aws-amplify.github.io/)


## chalice とは

[AWS chalice ハンズオンより](https://feature-webpush.dma9ecr5ksxts.amplifyapp.com/)

> Amazon API Gateway と AWS Lambda を使ったサーバーレスアプリケーションを素早く開発しデプロイできるサーバーレスフレームワークです。


本題です。
API gateway と lambda を使ったサーバレスアプリケーションをシンプルに構築して RESTful APIを管理したい、が使い所です。そんな chalice は、意外と会場で挑戦者が少なかったようだったので少しでも広まって欲しい気持ちから取り上げてみます。本記事でちょっとでも良さが伝わればウレシイです。
ちなみに、AWSオンラインセミナーでも紹介されていてslideshareに公開されています。

<iframe src="//www.slideshare.net/slideshow/embed_code/key/pGaZHnM6sdX72n" width="595" height="485" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe> <div style="margin-bottom:5px"> <strong> <a href="//www.slideshare.net/AmazonWebServicesJapan/20190619-aws-black-belt-online-seminar-dive-deep-into-aws-chalice" title="20190619 AWS Black Belt Online Seminar Dive Deep into AWS Chalice" target="_blank">20190619 AWS Black Belt Online Seminar Dive Deep into AWS Chalice</a> </strong> from <strong><a href="https://www.slideshare.net/AmazonWebServicesJapan" target="_blank">Amazon Web Services Japan</a></strong> </div>


### かわいさ

- 自動で`app.py`等が生成される(Flask ライクな最小構成)
- `$chalice deploy` だけで高速デプロイ
- API gateway で組み込みオーソライザーを手軽に使える
-> 直感的なインターフェース


## 準備

```
$ pip3 install virtualenv
$ virtualenv ~/.virtualenvs/chalice-handson
$ source ~/.virtualenvs/chalice-handson/bin/activate
```

## Install

```
$ pip3 install chalice
```

※念の為AWSの認証情報が正しいか設定ファイルを確認しておきましょう( `~/.aws/credentials`と`~/.aws/config` )。

## プロジェクト作成

chalice は `new-project` で新規プロジェクト作成できます。

```
$ chalice new-project helloworld
```

コマンドを実行するとディレクトリ配下に Python のマイクロフレームワークである flask の構成 + `.chalice` という、まさに最小構成ができているはずです。


## デプロイ

これをしたいがために chalice を触っていると言っても過言はありません(?)。

```
$ chalice deploy
```

バーン!...

本当にこれだけでデプロイできているの... curl してみましょう。

```
$ curl https://qxea58oupc.execute-api.us-west-2.amazonaws.com/api/
{"hello": "world"}
```

これで API Gateway と Lambdaファンクションがコマンドひとつでデプロイされてしまう。簡単。しかも速い。


## URLパラメータの取得

ここからURLパスに指定した値で欲しい情報を返す処理を追加しています。
`app.py` のデコレータ `@app.route()` に パラメータを返す View, getparam 関数を追加します。

```python
from chalice import Chalice

app = Chalice(app_name='helloworld')

CITIES_TO_STATE = {
    'seattle': 'WA',
    'portland': 'OR',
}


@app.route('/')
def index():
    return {'chalice': 'handson'}


@app.route('/cities/{city}')
def state_of_city(city):
    return {'state': CITIES_TO_STATE[city]}
```

追加できたらデプロイしてhttpコマンドを叩いてみます。

```
$ chalice deploy
$ http https://hoge.execute-api.us-east-1.amazonaws.com/api/cities/seattle
{
    "state": "WA"
}
$ http https://hoge.execute-api.ap-northeast-1.amazonaws.com/api/cities/portland
{
    "state": "OR"
}
```


## エラーメッセージとレスポンスのカスタマイズ

デフォルトでは無効になっているのデバックモードを有効にします。

```python
from chalice import Chalice

app = Chalice(app_name='helloworld')
app.debug = True
```

再びこのAPIにリクエストを投げてみると以下のようになります。

```
(chalice-handson) ➜  helloworld http https://hoge.execute-api.ap-northeast-1.amazonaws.com/api/cities/sanfrancisco
HTTP/1.1 500 Internal Server Error
Connection: keep-alive
Content-Length: 280
Content-Type: text/plain
Date: Thu, 28 Nov 2019 11:31:56 GMT
Via: 1.1 8afc3f299389b6df1d38dfe8a5520639.cloudfront.net (CloudFront)
X-Amz-Cf-Id: HWm2BSvWfbycfBqxY-rCbxzFgHzgYRAwlCY3tmEH2UzdZ_lTDGMUbQ==
X-Amz-Cf-Pop: NRT53
X-Amzn-Trace-Id: Root=1-5ddfb02b-99e9ea18bd0b2148b0be4448;Sampled=0
X-Cache: Error from cloudfront
x-amz-apigw-id: D3h24GbYNjMFhmg=
x-amzn-RequestId: 9cf0fcfb-0461-4987-b1b2-1e31544a9c36

Traceback (most recent call last):
  File "/var/task/chalice/app.py", line 1104, in _get_view_function_response
    response = view_function(**function_args)
  File "/var/task/app.py", line 22, in state_of_city
    return {'state': CITIES_TO_STATE[city]}
KeyError: 'sanfrancisco'
```
ちゃんと失敗してトレース情報を出力してくれました。実行URLのパラメータはドキュメント通りsanfranciscoで試していますが、それ以外でもやってみましょう。
今回は `500 Internal Server Error` を返してみましたが、 `400 Bad Request` なら `BadRequestError` をインポートするなどその他の例外キャッチもクラスとResponseオブジェクトでカスタマイズが可能です。

```python

from chalice import BadRequestError

@app.route('/cities/{city}')
def state_of_city(city):
    try:
        return {'state': CITIES_TO_STATE[city]}
    except KeyError:
        raise BadRequestError("Unknown city '%s', valid choices are: %s" % (
            city, ', '.join(CITIES_TO_STATE.keys())))
```

### レスポンスコードとエラー

* 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

このステップでは

> 例外を丸めすぎたり握りつぶしたりせず、ちゃんと適切な情報を返しましょうね。 

という印象的な一文がありました。明日の自分のためにもしっかり例外をキャッチするようにしたいですね。



## リクエストのメタデータ（header や body）を取得

`app.current_request` は Viewでリクエストのメタデータを参照するオブジェクトです。

```python

@app.route('/introspect')
def introspect():
    return app.current_request.to_dict()
```

こちらも http コマンドを実行して帰ってくる情報を確認してみてください。
ここでは出力が多いため割愛します。


## CORS のサポート

CORS(Cross-Origin Resource Sharing) 対応はどのようなwebエンジニアにも必要です。特にSPAのような構成のアプリケーションなどが利用シーンです。chalice ではパラメータの追加だけで対応が可能です。

```python

from chalice import CORSConfig

cors_config = CORSConfig(
    allow_origin='https://foo.example.com',
    allow_headers=['X-Special-Header'],
    max_age=600,
    expose_headers=['X-Special-Header'],
    allow_credentials=True
)


@app.route('/custom_cors', methods=['GET'], cors=cors_config)
def supports_custom_cors():
    return {'cors': True}
```

`cors` に `True` を渡すだけで実現できます。


## 定期処理を行う Lambda ファンクション

最後に、様々な Lambdaファンクションの定義の中から定期処理 `schedule` を `app.py` に実装します。
マネジメントコンソールから CloudWatch Logs で起動されたことを確認してみます。

```python

from chalice import Chalice, Rate
from datetime import datetime


@app.schedule(Rate(1, unit=Rate.MINUTES))
def periodic_task(event):
    launch_time = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
    print('function_launched:' + launch_time) # ここでlogに吐く
    return {"function_launched": launch_time}

```

いつものようにデプロイします。

```
(chalice-handson) ➜  chalice deploy
Creating deployment package.
Updating policy for IAM role: helloworld-dev
Creating lambda function: helloworld-dev-periodic_task
Updating lambda function: helloworld-dev
Updating rest API
Resources deployed:
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:hoge:function:helloworld-dev-periodic_task
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:hoge:function:helloworld-dev
  - Rest API URL: https://hoge.execute-api.ap-northeast-1.amazonaws.com/api/
```

このあとにLambdaマネジメントコンソールには webAPI の関数 `helloworld-dev` とは別の `helloworld-dev-periodic_task` が追加されています。
今回は定期実行を確認したのでモニタリング画面も確認しましょう。

<img width="640" alt="lambdaメトリクス.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66523/aeb8ed20-a273-09f7-66e3-e86d826ce26b.png">


`print` した `'function_launched:' + launch_time` はロググループ一覧から確認できます。（このメトリックスグラフ上の「ログの表示」から遷移しても確認することができました。)
実行ログに `function_launched:yyyy/mm/dd HH:mm:ss` の文字列が出力されているはずです。


# 感想とか

だいたいの動作がめちゃ速い。。お茶を飲んでいる場合ではない。
今回参加して得たことは、 chaliice の使い方もそうですがAWSが初めての人でもわかるほど丁寧なドキュメントとサポートがあったので体系理解の点でも助かりました。
自分は最近仕事で Restful な API や Lmbdaファンクションを利用した Slack bot 開発をしている際にAWSまわりは初めて触っていたので、かなり参考になりましたし、なによりハンズオンのレベルがちょうどよくAWSはじめるゾイ、な人がまず取り組める内容だと思います。


# 宣伝
Opt technologiesではハンズオンはもちろん各種イベントを開催しています、ご興味ある方は是非参加お待ちしています！
https://opt.connpass.com/



[^1]: https://amplifychalicehandson03.splashthat.com/