Serverless環境での開発 ─ MOSHのテスト環境構築の取り組み
MOSHでソフトウェアエンジニアをしている masuyama です。
MOSHではバックエンドAPIの実行環境としてAWS Lambdaを利用しています。
いわゆるServerless環境は、サービス運用の手間を大幅に削減できる一方で、Cloud-nativeな環境のため、開発時に実際の環境と差異が生じ、動作確認が難しくなる場面があります。
開発しようとしていたもの
MOSHでは、それまで決済プロバイダーとして Stripe を使用していましたが、新たに Fincode も利用できるようにしようとしていました。
さらに、DynamoDBに保存していた決済関連のデータを、より厳密な型の制約を持たせられるRDSへ移行 することも、このタイミングで進めることにしました。[1]
その当時の問題点
この項目だけで3記事ほど書けるほどの複雑な経緯がありますが、要約すると以下のような問題がありました。
- 認証や決済プロバイダーとの接続確認がローカル環境では難しい
- AWS上の動作確認環境(以下staging)は、
develop
ブランチにマージするとデプロイされる仕組みだった[2]ため、開発途中のものを確認するには、マージ → 確認 → リバート という手順が必要だった -
staging
環境は本番と同じ構成のため、新たに環境を作ろうとすると、大量のリソース(300以上のLambdaと関連リソース)が生成されてしまう - Lambdaの容量が限界に近い(250MB制限)
テスト環境の構築
上記の問題を回避しつつ、開発途中のものを動作確認できる環境の構築を目指しました。
-
staging
のリソースを利用し、任意のブランチのAPIをテストできるようにする - APIのLambdaを1つに集約する
- 新たにRDSとSQSを利用するため、それらの確認もできるようにする
- 基本的に利用するのはAPIのみだが、設定変更によって他のイベント用Lambdaもデプロイ可能にする
- 複数人で開発しているため、誰でもデプロイできる仕組も構築する
構成
完成イメージは以下のようになります。
- API GatewayとLambdaを作成
-
staging
環境のリソースを共有 - 記載していない細かいリソース(VPC、IAM、SMSなど)は
staging
環境のものを再利用
実現するための課題の解決
1. 250MB制限の回避
AWS Lambdaには デプロイパッケージのサイズが250MB以内 という制限があります。もともとギリギリの容量でしたが、新たにRDSを利用するため、追加でRDBドライバー等の追加のパッケージのインストールが必要になり、250MB制限内に収めるのが困難になりました。
そこで、テスト環境では ZIPでのデプロイをやめ、コンテナイメージを用いる ことにしました。
AWSが提供するベースイメージを活用し、以下のようなDockerfileを作成しました。[3]
FROM public.ecr.aws/lambda/python:3.10-x86_64
# コードをコピー
COPY . ${LAMBDA_TASK_ROOT}
# 必要なパッケージをインストール
RUN pip install -r requirements.txt
# ハンドラーを設定
CMD [ "foo/bar/test.handler" ]
2. Lambdaのハンドラー集約
通常、AWS LambdaではAPIエンドポイントごとにLambdaハンドラーを作成しますが、APIが増えるとLambdaの数も増え、管理が煩雑になります。
この問題を解決するために、Lambda-lith という1つのLambdaで複数のエンドポイントを処理する設計を採用しました。[4]
参考: Serverlessマイクロサービスの設計アプローチ
テスト環境では、独自のルーティング処理を導入し、簡易的に実現しました。
from foo.bar import user
function_map = {
"GET /users": user.list_handler,
"POST /users": user.create_handler,
"PATCH /users": user.update_handler,
....
....
....
}
def handler(event, context):
# デバッグ用ログ出力
print("handler called", {"event": event, "context": context})
# リクエストを適切なハンドラーにルーティング
key = f"{event['httpMethod']} {event['resource']}"
if key in function_map:
return function_map[key](event, context)
# 未定義のエンドポイントに対するデフォルトレスポンス
return {
"statusCode": 404,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"event": event}),
}
全エンドポイントを手動で記述するのは非効率だったため、設定ファイルをもとにコードを生成するスクリプトを作成して対応しました。
3. ChatOpsを活用したデプロイ
デプロイにはChatOpsを使う構成を採用しました。
当時のMOSHの環境では、開発リポジトリ外でGitHub Actionsを用いたデプロイが行われており[5]、構成が複雑でした。そのため、外部の仕組みとして構築しました。
デプロイにはBolt for Pythonを用いた簡易的なSlack Botを作成しています。
Slack上で以下のようなコマンドを実行することで、任意のブランチをデプロイできる仕組みを構築しました。
@deploybot checkout_be [env] [branch]
@deploybot deploy_be [env]
実行すると、デプロイ用のシェルスクリプトが実行され、テスト環境にデプロイされる仕組みです。
開発のフロー
このテスト環境の導入により、開発のフローが以下のように改善されました。
導入前
- ローカルで開発
- 開発ブランチをGitHubに push
-
develop
に merge し、staging で確認 -
revert branch
を作成し revert -
revert branch
をベースに追加開発 - (最初に戻る)
導入後
- ローカルで開発
- 開発ブランチをGitHubに push
- 開発ブランチをテスト環境にデプロイし、確認
- (最初に戻る)
この変更により、開発途中のコードを staging に影響を与えずにデプロイ・確認できるようになり、 develop
ブランチへの不必要な merge も不要になりました。
その結果、繰り返しの確認が必要な大規模な変更も、よりスムーズに進められるようになりました。
現在の staging へのデプロイフロー
さらに、現在は 任意のブランチを staging にデプロイできるようになった ため、以下のフローで直接確認することも可能になりました。
- ローカルで開発
- 開発ブランチを GitHub に push して pull request を作成
- 開発ブランチを staging にデプロイし、確認
- (最初に戻る)
これにより、staging 環境を用いた動作確認の自由度が大幅に向上し、
リリース前の検証がより柔軟かつ効率的に行えるようになっています。
今後は
現在は、FastAPIとOpenAPIのコードジェネレーターを活用したLambda-lith化が進んでおり、暫定的に集約していたハンドラーは、近いうちにその役目を終える見込みです。
また、LambdaはすべてDockerイメージベースに移行済みのため、今後は環境の整理・集約がさらに進んでいくと考えています。
一方で、そもそもの課題であった ローカル環境での開発のしにくさ については、依然として解決されていません。この点についても、今後改善を進めていきたいと考えています。
こうした 開発に関連するあらゆる課題を一緒に解決し、より良い仕組みを作っていく仲間を募集 しています!
興味のある方はぜひ一度お話ししましょう!
Discussion