LoginSignup
2

More than 1 year has passed since last update.

Organization

AWS Chalice Tips

公式ドキュメント

Documentation — AWS Chalice

環境

  • Python: 3.8
  • Chalice: 1.26.6

Chaliceでapp.pyの分割

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

例:

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:

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}

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で登録することによって使用できるようになる。

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がスキップされるため、下記のように用意してる共通セットアップが適用されなくなる。

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に記述する。
  • 自作のデコレータで使用する機能に似た実装する。
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
    # ~~~

例:

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に付与されるポリシーを自動生成してくれる。
(コマンドで生成されるポリシーを確認可能)

$ chalice gen-policy

この自動生成はソースコードで使用しているboto3、およびデコレータから判断されて生成されている。
そのため、boto3をラップしているライブラリ(pynamodbなど)を使用してるとライブラリの分は生成対象に入らない。
その場合は自動生成でなく、手動でポリシーを用意する必要がある。

1.ポリシー自動生成の設定変更

Chaliceプロジェクトルート直下にある.chalice内に設定ファイルが格納されているため、編集する。

.
├── .chalice
│   └── config.json # chalice設定ファイル
├── app.py
└── requirements.txt

下記のautogen_policyfalseに変更する。(ない場合は追加する)

.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の形式で配置する。

.
├── .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

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

  1. リポジトリのmasterブランチが変更されたのをトリガーにパイプラインを開始する
  2. CodeBuildでChaliceをビルド(chalice package)する
  3. 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

TestHTTPClient

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

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はチーム開発に向かない
    • デプロイする度に更新されるファイルを共有しなくてはならない

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2