AWS Chalice Tips

## 公式ドキュメント
[Documentation — AWS Chalice](https://aws.github.io/chalice/index.html)

### 環境

- Python: 3.8
- Chalice: 1.26.6

## Chaliceでapp.pyの分割

chaliceでは`app.py`にエンドポイントを設定していくため、一つのプロジェクトで複数の管理をやらせようとすると肥大する傾向がある。

例:
``` python:app.py
from chalice import Chalice
app = Chalice(app_name='foo-app')

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

################
# 記事管理
################
@app.route('/articles', methods=['GET'])
def getArticles():
    # ～
    # Something Code
    # ～～～
    return {'articles': articles }

@app.route('/articles', methods=['POST'])
def registerArticle():
    # ～
    # Something Code
    # ～～～
    return {'article': id}

################
# 写真管理
################
@app.route('/photos', methods=['POST'])
def registerPhoto():
    # ～～～
    # Something Code
    # ～～～
    return {'photo': id}
```

[Blueprints](https://aws.github.io/chalice/topics/blueprints)を使用して、プロジェクト直下の`app.py`とは別で複数モジュールに分割できる。

ディレクトリ構成:
```
foo-api
├── app.py
└── chalicelib
    ├── articles
    │   └── app.py
    └── photos
        └── app.py
```

各モジュールの`app.py`:
``` python:chalicelib/articles/app.py
from chalice import Blueprint
articles_app = Blueprint(__name__)

@articles_app.route('/articles', methods=['GET'])
def getArticles():
    # ～
    # Something Code
    # ～～～
    return {'articles': articles }

@articles_app.route('/articles', methods=['POST'])
def registerArticle():
    # ～
    # Something Code
    # ～～～
    return {'article': id}

```

``` python:chalicelib/photos/app.py
from chalice import Blueprint
photos_app = Blueprint(__name__)

@photos_app.route('/photos', methods=['POST'])
def registerPhoto():
    # ～～～
    # Something Code
    # ～～～
    return {'photo': id}
```

作成したモジュールをプロジェクト直下の`app.py`で登録することによって使用できるようになる。

``` python:app.py
from chalice import Chalice
from chalicelib.articles.app import articles_app
from chalicelib.photos.app import photos_app

app = Chalice(app_name='foo-app')

# 作成したモジュールを登録
app.register_blueprint(articles_app)
app.register_blueprint(photos_app)
```

### ※分割したことにより発生する事象(2022/3 時点)
[純粋なLambda関数](https://aws.github.io/chalice/topics/purelambda.html?highlight=lambda)や[SQSをトリガーとする関数](https://aws.github.io/chalice/api.html?highlight=sqs#SQSEvent)の場合`app.py`がスキップされるため、下記のように用意してる共通セットアップが適用されなくなる。

``` python:app.py
from chalice import Chalice
from chalicelib.middleware import error_handler

# 1.Chaliceアプリケーションを初期化する
# (Chalice独自のログセットアップなどが含まれる)
app = Chalice(app_name="bar-app")
# 2.ミドルウェアをChaliceアプリケーションにセットする
app.register_middleware(error_handler, "pure_lambda")

# 上記で行ったセットアップ処理が各モジュールで適用されない。
app.register_blueprint(first_routes)
app.register_blueprint(second_routes)
```

- [該当のイシュー](https://github.com/aws/chalice/issues/1566)

回避方法:
- Blueprintを使用せず`app.py`に記述する。
- 自作のデコレータで使用する機能に似た実装する。

``` python:app.py
from chalice import Blueprint
photos_app = Blueprint(__name__)

@photos_app.lambda_function(name="photo-lambda-func")
@setup_log(app=photos_app) # 各エンドポイントの定義で設定する必要がある
def registerPhoto():
    # ～～～
    # Something Code
    # ～～～

```
例:
``` python:log.py
import sys
import logging

FORMAT_STRING = '%(name)s - %(levelname)s - %(message)s'

def setup_log(app):
  def _decorator(func):
    @wraps(func)
    def _request_decorator(*args, **kwargs)     
      handler = logging.StreamHandler(sys.stdout)
      formatter = logging.Formatter(FORMAT_STRING)
      handler.setFormatter(formatter)
      
      app.log = logging.getLogger(func.__name__)
      app.log.propagate = False
      app.log.setLevel(logging.DEBUG)
      app.log.addHandler(handler)
      ret = func(*args, **kwargs)
      
      return ret
    return _request_decorator
  return _decorator
```

## IAMポリシーの手動付与
基本的にChaliceはソースコードを読み取って、Lambdaに付与されるポリシーを自動生成してくれる。
(コマンドで生成されるポリシーを確認可能)
``` console
$ chalice gen-policy
```
この自動生成はソースコードで使用している`boto3`、およびデコレータから判断されて生成されている。
そのため、`boto3`をラップしているライブラリ([pynamodb](https://pynamodb.readthedocs.io/en/latest/)など)を使用してるとライブラリの分は生成対象に入らない。
その場合は自動生成でなく、手動でポリシーを用意する必要がある。

### 1.ポリシー自動生成の設定変更
`Chalice`プロジェクトルート直下にある`.chalice`内に設定ファイルが格納されているため、編集する。
``` bash
.
├── .chalice
│   └── config.json # chalice設定ファイル
├── app.py
└── requirements.txt
```

下記の[`autogen_policy`](https://aws.github.io/chalice/topics/configfile.html?highlight=config#autogen-policy)を`false`に変更する。(ない場合は追加する)
``` json:.chalice/config.json
{
  "version": "2.0",
  "app_name": "foo-app",
  "stages": {
    "dev": {
      "autogen_policy": false
    },
    "prod": {
      "autogen_policy": false // ステージ毎に設定可能
    }
  }
}
```
### 2.IAMポリシーファイルの作成
IAMポリシーのファイルは`.chalice`配下に`policy-<stage-name>.json`の形式で配置する。

``` bash
.
├── .chalice
│   ├── config.json
│   ├── policy-dev.json    # devステージ用
│   └── policy-prod.json   # prodステージ用
├── app.py
└── requirements.txt
```

ファイルは[IAM JSONポリシー](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies.html)の構文に則ったものを用意。

DynamoDB操作ポリシー例:
``` json

  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem"
      ],
      "Resource": "*"
    },
  ]
}
```

## CI/CDパイプライン作成
CodePipeline用のCloudFormationテンプレートをコマンドで出力することができる。

``` console
$ chalice generate-pipeline -b buildspec.yml --pipeline-version v2 pipeline.json
```

出力されるファイルは必要最低限の構成で記述されいているため、実際にテストをしたり、承認プロセスを入れたりするにはファイルを修正しなくてはならない。
大体下記の流れで、パイプラインが組まれている。

1. リポジトリの`master`ブランチが変更されたのをトリガーにパイプラインを開始する
2. CodeBuildでChaliceをビルド(`chalice package`)する
3. CloudFormationでデプロイする(change set)

ファイルはかなり長大なので、CloudFormation初心者にはかなり辛い。そのうえjsonファイルなのでコメントも入れることができずメンテナンスが大変。
[CloudFormationデザイナー](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/working-with-templates-cfn-designer-json-editor.html)のエディタを使用してyamlに変換した方が、初心者は編集しやすいと思う。

## レイヤーの更新
chaliceは自動でレイヤー作る機能がある。

``` json
{
  "version": "2.0",
  "app_name": "foo-app",
  "stages": {
    "dev": {
      "autogen_policy": false,
      "automatic_layer": true // レイヤー自動生成有効化
    }
  }
}
```

有効にすることで、`requirements.txt`に記載されているものと、`vendor`ディレクトリ配下のパッケージがレイヤーとして生成される。
基本的に`requirements.txt`で問題ないが、公開されてない内部パッケージや、whl形式でインストールされないものは`vendor`配下に入れる必要がある(whl形式でないものは個別にwheelしてから)

## テスト

`chalice.test`にテストクライアントを提供しているので、それを用いてテストを実行することができる。

### HTTP

[**TestHTTPClient**](https://aws.github.io/chalice/api.html?highlight=test#TestHTTPClient)

``` python
from app import app
from chalice.test import Client

# Get
def Test1():
    with Client(app) as client:
        response = client.http.get("/users")

# Post
def Test2():
    with Client(app) as client:
        response = client.http.post(
            "/users",
            headers={'Content-Type':'application/json'},
            body=json.dumps({"name" : "test"})
        )
```

### Lambdaイベント(SQS等)

[**TestEventsClient**](https://aws.github.io/chalice/api.html?highlight=test#TestEventsClient)


``` python
from app import app
from chalice.test import Client

# SQS
def Test1():
    with Client(app) as client:
        client.lambda_.invoke(
            "app_sqs_handler",
            client.events.generate_sqs_event(
               message_bodies=["TestMessages"]
            )

# S3
def Test2():
    with Client(app) as client:
        client.lambda_.invoke(
            "app_s3_handler",
            client.events.generate_s3_event(
               bucket="TestBucket",
               key="TestKey"
            )
```


テスト時はAWSリソースは[moto](http://docs.getmoto.org/en/latest/)を利用してモック化することが多いと思われるが、`AWS Rekognition`など[motoでサポートされていないサービス](http://docs.getmoto.org/en/latest/docs/services/index.html)に関しては[botocore.stub](https://aws.github.io/chalice/topics/testing.html#testing-boto3-client-calls)で回避することも出来る。

## 運用に関して
以下のデメリットがあり大規模システムより小規模システムに向いている(と思う)
- リソース名をステージ毎(dev, prod)に任意で付けられないものがある。(API Gatewayなど)
  - そのため[タグを設定](https://aws.github.io/chalice/topics/configfile.html?highlight=config#tags)するが全リソースには付与されない
- 細かい環境設定は手動でやらざるおえない(カスタムドメインのマッピングやポリシー等)
- 提供されているデプロイコマンドの`chalice deploy`はチーム開発に向かない
  - デプロイする度に更新されるファイルを共有しなくてはならない