見出し画像

AWS Lambda(Python)のモノレポ構成と開発環境

みなさんこんにちは!
ワンキャリアでデータエンジニアをしている野田です!(GitHub:@tsugumi-sys
最近はウイスキーにハマっています!ジャパニーズウイスキーの価格が高騰する中、お手頃で美味しいものはないかと血眼になって探している日々です。最近飲んだ中で一番美味しかったのはシーバスリーガル18年です。さて、趣味の話はこれくらいにして、早速本題に入っていきましょう!

本記事では、AWS Lambda(Python)を用いてデータパイプラインなどを構築する際のディレクトリ構成やテスト方法等の知見を共有します。こうすべきだというベストプラクティスの提示ではなく、一つの設計パターンとして受け取っていただけると幸いです。



背景

昨年、新卒でワンキャリアにエンジニアとして入社してから、AWS Lambda(以下、Lambda)で複数のデータパイプラインを構築する業務を行っており、インフラの構築からCI/CD整備、アプリケーションコードの実装まで一貫して取り組んできました。

事例:

  • DBスナップショット作成時に、自動でデータをBigQueryに転送する。

  • DBクラスタスナップショット作成時に、自動でBIツール接続用クラスタを復元する。

上の事例も文字に起こしてみると簡単そうですが、実際に開発するとなると複数のLambdaを連携させるパイプラインを組む必要があります。そのため、壊れにくいパイプラインをより効率的・スピーディに構築するために、様々な工夫を行ってきました。
今回は、そのLambdaのパイプライン開発環境の改善内容をお伝えしていこうと思います。


ディレクトリ構成

モノレポ構成

まず、ディレクトリ構成についてです。紆余曲折ありましたが、最終的に以下の構成に落ち着きました。

.
└── aws_lambdas/
    ├── utilities
    ├── lambdaA
    ├── lambdaB
    └── tests/
        ├── utilities
        ├── lambdaA
        └── lambdaB

インフラの構築とアプリケーションの実装を一貫して行うことから、モノレポ構成を採用しました。一般的なモノレポ構成に基づいて、インフラコードとアプリケーションコード(Lamdaの実行コード)を分ける構成としました。

infraディレクトリ

infraディレクトリにはTerraformコードが格納されます。AWSとGoogle Cloudの両方のインフラが必要だったので、それぞれディレクトリで分割しています。

aws_lambdasディレクトリ

aws_lambdasディレクトリにはLambdaの実行コードが格納されています。このディレクトリは以下のような構成になっています。

.
└── aws_lambdas/
    ├── utilities
    ├── lambdaA
    ├── lambdaB
    └── tests/
        ├── utilities
        ├── lambdaA
        └── lambdaB

utilitiesディレクトリでは異なるLambda間で共有利用するメソッド等を管理しています。例えば、EventBridgeから渡すパラメータを元に対象のイベントか否かを判断するバリデーションコードなどです。
さらに、異なるLambdaはお互い依存しないように管理しています。例えば、ambdaAは自分自身の実行コードに加えて、utilitiesコードのみに依存します。したがって、Lambdaのデプロイ時には、実行コードに加えてutilitiesディレクトリの2ディレクトリのみをデプロイします。
ユニットテストでも同様の構成をとることで、新しい機能追加や修正の際には変更箇所・影響範囲が限定されます。これにより、開発者・レビュワー双方の負担が小さくなり開発スピードが改善されました。

githubディレクトリ

githubディレクトリにはLambdaのデプロイワークフローのコードが格納されています。このディレクトリは以下のような構成になっています。

.
└── .github/
    └── workflows/
        ├── _deploy_lambda.yml
        ├── staging_deploy_lambdaA.yml
        ├── prod_deploy_lambdaA.yml
        ├── staging_deploy_lambdaB.yml
        ├── prod_deploy_lambdaB.yml
        └── python_ci.yml

Lambdaのデプロイコードは_deploy_lambda.ymlで共通部分を切り出しています。各Lambdaのデプロイは共通したワークフローにワーキングディレクトリ等のパラメータを渡すことで実行しています。これにより、新しいLambdaのデプロイワークフロー追加の際には最低限のコードを書くだけで済みます。


CI/CD

こちらも紆余曲折・試行錯誤がありましたが、下図の構成となりました。

TerraformのCI/CDには全社的に採用されているTerraform Cloudを採用しました。開発初期はterraform apply等のコマンドを手動・Github Actions等で実行していました。その後、terraformの構築・運用コスト削減のために当時試験的に導入されていたTerraform Cloudをすぐ取り入れました。ワークフロー設定や権限管理等がよりシンプルになり、リソース反映作業がとても楽になりました。AWS・Google Cloud・検証/本番環境のそれぞれにワークスペースを作成して運用しています。

LambdaのコードデプロイにはGitHub Actionsを利用しています。AWSへの認証にはOpen ID Connect(OIDC)を採用しています。OIDC用IAMロールの権限管理を同じリポジトリのインフラコードで管理することで、認証の作成・権限管理を一元的に行っています。作成したLambdaのコードデプロイ権限だけを付与することで、最小権限で運用しています。また、OIDCではリポジトリからのアクセス制限も追加することでよりセキュリティの高い認証となるように心がけています(初期には強めの権限をもつIAMユーザーのキーを認証に用いていました)。

参考:


自動テスト

Lambdaを構築するにあたって、苦労したのが外部リソースとのインタラクションを伴うLambdaの検証でした。外部リソースとの接続にはPythonで利用可能なAWS SDKであるboto3を用いています。ローカルでboto3は機能しないため、モックする必要があります。boto3をモックした上でユニットテストを書いて、検証環境にデプロイして動作確認をしていきます。この段階で、boto3の接続部分でエラーが発生することが多発しました。デバッグプロセスが長く非常に開発体験が悪かったです。

ドキュメントをしっかり読めば良いと言えばそれまでなのですが、事前に自動で検知できるならそれに越したことはありません。より精緻なモックを構築するにはコストがかかりすぎます。

よって、motoというboto3のモッキングツールを導入しました。このツールはまさにboto3の精緻なモックが可能となります。例えば、下図のようにLambdaがRDSとParamter Storeに接続しているケースの場合、motoでモックした環境下ではboto3を使ってRDSの作成やParameter Storeへの値の格納が可能になります。リクエストパラメータに不正がないかチェックし、正しいパラメータでリソースが作成されたか検証可能です。


motoの導入により、デバッグプロセスが短縮化され開発体験が大きく向上しました。これにより、機能追加・改善のサイクルをより高速化させることができました。

また、使用中に見つけたバグの修正や機能追加を行っているうちに、motoにがっつりコントリビュートしていくことにもなりました。
詳細は過去のテックブログで書いていますので、興味がある方はぜひご覧ください。


開発ツール等

ここでは、主にPythonを用いた開発に使用しているツール等の紹介をしていきます。

Pythonのリンター&フォーマッタにはRust製のRuffというツールを利用しています。PylintやFlake8等のツールと比べて、高速に実行できるのを売りにしています。開発体験は非常に良いです。motoのリンター・フォーマッタをRuffにしたのは私です(プチ自慢)。最近はこちらにもコントリビュートしています。

他には下記のようなツールを利用しています。静的解析ツールはpre-commitで実行できるように整備もしました。パイプライン等の比較的小さいプロジェクトでも、なるべく自動化して人間がチェックしないといけない部分を減らすように心がけています。

その他のツール:

  • Pythonのバージョン管理:mise

  • Pythonの型チェック:mypy

  • タイポチェック:typos


得られた教訓

今回、モノレポ構成をはじめとしたLambdaのパイプライン開発環境の改善内容をまとめました。これらの経験を通じて得られた教訓は2つあると考えています。

1つ目は、より良いディレクトリ構成を考える上で、モジュール同士の依存度が重要な観点であるということです。本などで当たり前に謳われている内容ではありますが、改めて自分の頭で理解し実践できたことには大きな意味があると考えています。この観点は、Lambdaの構築にかかわらず広く活かせるものです。例えば、多くのフレームワークで存在するディレクトリ構成のベストプラクティスには、依存度の観点が必ず出てきます。

2つ目は、自動テストすべき部分を特定し効果的に実施することです。今回のケースだと、Lambdaが外部リソースと接続している部分に対して自動テストを行い、開発の効率化を行うことができました。テストカバレッジ100%を目指すべきだということではなく、テストを行うことで得られるリターンが大きい部分に対して効果的に行うことが重要だということです。


おわりに

今回の開発で得られた知見は、自分の他の業務でも大いに役立っています。ここで共有した内容が他の人の役に立つと嬉しいです。
ここまで読んでいただきありがとうございました!


▼ワンキャリアのエンジニア組織のことを知りたい方はまずこちら

▼カジュアル面談を希望の方はこちら

▼エンジニア求人票


この記事が参加している募集

この記事が気に入ったらサポートをしてみませんか?