公式ドキュメント
環境
- Python: 3.8
- Chalice: 1.26.6
Chaliceでapp.pyの分割
chaliceでは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を使用して、プロジェクト直下のapp.py
とは別で複数モジュールに分割できる。
ディレクトリ構成:
foo-api
├── app.py
└── chalicelib
├── articles
│ └── app.py
└── photos
└── app.py
各モジュールの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}
from chalice import Blueprint
photos_app = Blueprint(__name__)
@photos_app.route('/photos', methods=['POST'])
def registerPhoto():
# ~~~
# Something Code
# ~~~
return {'photo': id}
作成したモジュールをプロジェクト直下の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関数やSQSをトリガーとする関数の場合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)
回避方法:
- Blueprintを使用せず
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
# ~~~
例:
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に付与されるポリシーを自動生成してくれる。
(コマンドで生成されるポリシーを確認可能)
$ chalice gen-policy
この自動生成はソースコードで使用しているboto3
、およびデコレータから判断されて生成されている。
そのため、boto3
をラップしているライブラリ(pynamodbなど)を使用してるとライブラリの分は生成対象に入らない。
その場合は自動生成でなく、手動でポリシーを用意する必要がある。
1.ポリシー自動生成の設定変更
Chalice
プロジェクトルート直下にある.chalice
内に設定ファイルが格納されているため、編集する。
.
├── .chalice
│ └── config.json # chalice設定ファイル
├── app.py
└── requirements.txt
下記のautogen_policy
をfalse
に変更する。(ない場合は追加する)
{
"version": "2.0",
"app_name": "foo-app",
"stages": {
"dev": {
"autogen_policy": false
},
"prod": {
"autogen_policy": false // ステージ毎に設定可能
}
}
}
2.IAMポリシーファイルの作成
IAMポリシーのファイルは.chalice
配下にpolicy-<stage-name>.json
の形式で配置する。
.
├── .chalice
│ ├── config.json
│ ├── policy-dev.json # devステージ用
│ └── policy-prod.json # prodステージ用
├── app.py
└── requirements.txt
ファイルはIAM JSONポリシーの構文に則ったものを用意。
DynamoDB操作ポリシー例:
"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テンプレートをコマンドで出力することができる。
$ chalice generate-pipeline -b buildspec.yml --pipeline-version v2 pipeline.json
出力されるファイルは必要最低限の構成で記述されいているため、実際にテストをしたり、承認プロセスを入れたりするにはファイルを修正しなくてはならない。
大体下記の流れで、パイプラインが組まれている。
- リポジトリの
master
ブランチが変更されたのをトリガーにパイプラインを開始する - CodeBuildでChaliceをビルド(
chalice package
)する - CloudFormationでデプロイする(change set)
ファイルはかなり長大なので、CloudFormation初心者にはかなり辛い。そのうえjsonファイルなのでコメントも入れることができずメンテナンスが大変。
CloudFormationデザイナーのエディタを使用してyamlに変換した方が、初心者は編集しやすいと思う。
レイヤーの更新
chaliceは自動でレイヤー作る機能がある。
{
"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
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等)
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を利用してモック化することが多いと思われるが、AWS Rekognition
などmotoでサポートされていないサービスに関してはbotocore.stubで回避することも出来る。
運用に関して
以下のデメリットがあり大規模システムより小規模システムに向いている(と思う)
- リソース名をステージ毎(dev, prod)に任意で付けられないものがある。(API Gatewayなど)
- そのためタグを設定するが全リソースには付与されない
- 細かい環境設定は手動でやらざるおえない(カスタムドメインのマッピングやポリシー等)
- 提供されているデプロイコマンドの
chalice deploy
はチーム開発に向かない- デプロイする度に更新されるファイルを共有しなくてはならない