- 宮永崇史
- 約 6,800 文字
- 2,400 View
目次
はじめに
こんにちは、TIG/DXユニット所属の宮永です。
今回はAWS Step Functionsの動的並列処理をローカルで実行する方法をハンズオン形式でまとめました。ソースコードはこちらに格納していますのでご参考にして下さい。
https://github.com/orangekame3/stepfunctions-demo
本記事はPipenv+LocalStackで作るLambda開発環境で作成したLambda関数をベースに実装しています。本記事の実装に取り組まれる方はこちらの記事が参考になると思います。
Step Functionsとは
Step FunctionsとはAWSの各種リソースをオーケストレーションするサービスです。
類似のサービスにAirflow等があります。AirflowとStep Functionsの比較をした多賀さんの記事はとても読み応えたあるのでぜひご覧ください。
Step Functionsについては技術ブログでもこれまで取り扱っています。
- Serverless連載6: AWSのStep FunctionsとLambdaでServelessなBatch処理を実現する
- CloudWatch EventとLambdaでStepFunctions間のエラ-を伝搬する
今回はServerless連載6: AWSのStep FunctionsとLambdaでServelessなBatch処理を実現するを参考にPythonとLocalStack(Docker)で動的並列処理を実装します。
モチベーション
今回想定しているユースケースは「大規模データの集計作業をLambdaで実装する」というものです。
Lambdaの実行制限時間である15分を超えるであろう処理をStep Functionsを使ってうまく突破したいというのがモチベーションです。先程紹介したServerless連載6: AWSのStep FunctionsとLambdaでServelessなBatch処理を実現するには動的並列処理以外にもStep Functionsを応用したバッチ処理について幅広く言及しているため、一読されると良いかと思います。
ハンズオンで構築するシステム
全体のシステム概要を記載した後に機能詳細を紹介します。
システム構成図
今回構築するシステム構成図を以下に記載します。
画像左側はビジュアルワークフロー図と呼ばれるもので今回扱うStep Functionsの定義書から生成されます。画像右側はビジュアルワークフロー図に対応するシステムアーキテクチャ図です。
S3バケットからJSONを取得し、後続のLambdaでETL処理をします。
余談ですが、Visual Studio CodeにはAWS Tool Kitという拡張機能が存在します。
こちらの拡張機能を利用すればステートメント言語を下図のビジュアルワークフロー図のように可視化できます。
実装するアプリの機能詳細
こちらの記事で実装しているLambda関数と同等の機能をもつシステムを実装します。
Scatter→Gatherに注目するとJSON→ExcelのETL処理を行っています。
S3バケットには予め以下の構造をもつJSONファイルを配置しておきます。
[ |
ScatterLambda
ScatterLambdaでは上記のJSONファイルを取り込み、DataFrameに変換します。その後、DataFrameをSegmentLamdaが15分以内に処理できる単位に分割します。
分割したファイルはpickleファイルでS3バケットに格納します。
SegmentLambda
SegmentLambdaではScatterLambdaで分割されたpickleファイルを取り込みETL処理を行います。
今回行うETL処理を以下記載します。
- 「ボーナスポイント」カラムの追加
「ボーナスポイント」は以下の条件で決定します。
【条件】
会員ランクが「4,5」の会員には「ポイント」×1.25倍のボーナスポイントを、会員ランク「1,2,3」の会員には「ポイント」と同等のボーナスポイントを付与することします。
上記の条件に従ってSegmentLambdaの処理前後のテーブルをまとめると以下のようになります。
SegmentLambda処理前
| 会員番号 | 名前 | 会員ランク | ポイント | タイムスタンプ |
|---|---|---|---|---|
| 000 | 長野原 ひろし | 4 | 58 | 2021-05-16 |
| 001 | 般若 竜門 | 2 | 75 | 2021-07-19 |
| 002 | 十河 アンナ | 2 | 57 | 2021-09-06 |
SegmentLambda処理後
| 会員番号 | 名前 | 会員ランク | ポイント | タイムスタンプ | ボーナスポイント |
|---|---|---|---|---|---|
| 000 | 長野原 ひろし | 4 | 58 | 2021-05-16 | 72.5 |
| 001 | 般若 竜門 | 2 | 75 | 2021-07-19 | 75 |
| 002 | 十河 アンナ | 2 | 57 | 2021-09-06 | 57 |
GatherLambda
GatherLambdaではSegmentLambdaでETL処理をされた各pickleファイルを取り込み、ひとつのExcelファイルを作成します。
作成したExcelファイルはS3バケットにアップロードして処理を終了します。
開発環境
開発に取り組む前に筆者の開発環境を記載します。記事中Linuxコマンドを使用している箇所があります。Windowsで開発される方はWSLを使用することをおすすめいたします。
- OS Ubuntu 20.04
- Python(pyenv) 3.9
- Pipenv
- Docker
- docker compose v2
- AWS CLI v2
LocalStackの準備
実装対象が決まったので早速開発環境の準備に取り掛かります。
AWS環境をローカルに用意するためにLocalStackを利用します。使用するのは以下2つのimageです。
なお、LocalStackの環境準備はこちらの記事を参考にさせていただきました。
それでは上記2つのimageを使用したdocker-compose.ymlを記述します。
version: '3.8' |
stepfunctionsのenv_fileで環境変数を渡しています。env_fileの中身を以下記載します。
AWS_ACCOUNT_ID=test |
また、DEBUGオプションに1と選択することでLocalStackのログを細かく確認できます。
Step Functionsを実行するときにはログも確認しながらデバッグするとよいと思います。
これでLocalStackの準備が整いました。
以下のコマンドを実行してコンテナが2つ起動していることを確認してください。
docker compose up --build |
次にAWS CLIを設定します。
AWS CLIの設定
AWS CLIでは認証情報などをプロファイルとして保存できます。
AWS CLIをインストールされた方はご自身が使用しているOSのhomeディレクトリに.awsの隠しファルダがあります。(エクスプローラーなどで確認する場合は隠しフォルダを表示するように設定してください。).awsフォルダ配下には.configと.credentials2つのファイルがありますのでそれぞれ以下のように設定してください。
参考:名前付きプロファイル
今回は以下のようにlocalというプロファイルを作成しました。
[localstack] |
[local] |
次にLambdaを実装します。
Lambdaの実装
このあと、複数のファイルを作成するため、最終的なディレクトリ構造を先に記載します。
適宜参考にしてください。
最終的なディレクトリ構造
. |
前提
ローカルマシンにPython3の環境が構築されていることを前提としています。
今回Lambdaの実装にはPythonを使用します。Pipenvを使用して各Lambda関数毎にプロジェクトを作成します。
Pipenvは以下のコマンドでインストールができます。
pip install pipenv |
冒頭で記載しましたが、以前Pipenv+LocalStackで作るLambda開発環境という記事を書かせていただきました。
今回は上記の記事で実装した内容を応用します。
記事ではLambdaの開発時使用するパッケージとデプロイ時のパッケージを分離することでデプロイ時のzipファイルの容量節約する方法を紹介しています。興味がある方はぜひご覧になってください。
今回は3つLambdaを作成しますので、以下のような構成でフォルダを作成してください。
. |
まずはdemo-scatterから開発環境を準備します。
Python環境は3.9を使用します。
demo-scatter配下で以下のコマンドを実行してください。
pipenv --python 3.9 |
次に使用する外部モジュールをインストールします。
demo-scatterで使用するモジュールはpandasだけです。以下のコマンドを実行してください。
pipenv install pandas |
続いて開発環境で使用するパッケージをインストールします。以下のコマンドでpytestとmypyをインストールします。
pipenv install pytest mypy --dev |
これでプロジェクト環境が整いました。他2つのプロジェクトも同様に環境を構築します。
それぞれ必要なモジュールを記載します。
- demo-segment
- pandas
- pytest mypy (–dev)
- demo-gather
- pandas
- xlwt
- xlsxwriter
- pytest mypy (–dev)
それではScatterLambdaからロジックの実装をします。
ScatterLambda
demo-scatter配下に以下2つのファイルを作成します。
- scatter.py
- lambda.py
機能のほとんどはscatter.pyに記述し、lambda.pyではハンドラを呼び出すのみにします。
以下、lambda.pyです。
import os |
ScatterLambdaでは、ファイルを分割します。
import json |
test-bucketに格納されたsample.jsonを取得して、pandasでDataFrameに変換します。変換後はpickleファイルで保存することでSegmentLambdaでの読み込み処理を高速化しています。
関数の戻り値はS3のオブジェクトキーの一覧です。segment_definitionsをキーとした辞書にリストして格納しています。
SegmentLambda
SegmentLambdaでETL処理を行います。ETL処理時の条件を再度記載します。
【条件】
会員ランクが「4,5」の会員には「ポイント」×1.25倍のボーナスポイントを、会員ランク「1,2,3」の会員には「ポイント」と同等のボーナスポイントを付与することします。
上記の条件を実装したsegment.pyを以下記載します。
import tempfile |
ScatterLambdaと同様にsegment.pyで定義したハンドラを呼ぶlambda.pyを以下のように作成します。
import os |
GatherLambda
最後にSegmentLambdaでETL処理をしたDataFrameを取り込み、1つのExcelファイルにまとめるGatherLambdaを実装します。
こちらもScatterLambda、SegmentLamdaと同様にハンドラを記載したgather.pyとハンドラを呼ぶlambda.pyを作成します。
gather.pyは以下のようになります。
import tempfile |
gather.pyで定義したハンドラを呼ぶlambda.pyを以下に記載します。
import os |
LocalStackへのデプロイ
それでは作成したそれぞれのLambda関数をLocalStackにデプロイします。
デプロイの方法は先程紹介したこちらの記事にまとめた方法を採用します。各Lambda関数のディレクトリ内に以下のようなMakefileを作成します。
以下はdemo-scatter内のMakefileの例です。
|
上記と同等の内容のMakefileをdemo-segmet及びdemo-gatherにも作成してください。(function-nameとzipコマンド部のscatter.pyは適宜変更してください)
すべての関数内にMakefileを作成したらプロジェクトルートにもMakefileを作成します。
プロジェクトルートに作成するMakefileは以下のようにします。
.PHONY: zip delete create update invoke log download stepfunction test json |
プロジェクトルートに配置するMakefileでは各プロジェクトフォルダで定義されたMakefileを利用しています。
それでは、プロジェクトルート直下で以下のコマンドを実行してScatterLambda、SegmentLambda、GatherLambdaのすべてをzipファイル化します。
make zip |
zip化が完了していれば各ファルダのbinフォルダにlambda.zipが生成されているはずです。
Step Functionsの準備
Amazonステートメント言語
Step Functionsでは各種リソースのオーケストレーション(状態管理)JSON形式のファイルで行います。
今回採用したスキャッターギャザーメッセージングパターン(分散して集約するようなパターン)は冒頭に紹介した記事をほぼそのまま転用させていただきました。
ScatterLambdaのeventに引数を渡すため一部追加しています。
以下、今回使用するステートマシンの定義書であるprallel.jsonです。
{ |
定義書の詳細については元記事を参考にしてください。ここで注目していただきたいのはInputPathとResultPathです。ここに宣言したsegment_definitionsとsegment_resultsというパラメータをキーとして、次のLambdaに渡すデータをフィルタリングしています。
そのため、ScatterLambdaでの返り値はSegmentLambdaに渡したい配列のキーをsegment_definitionsとし、Gatherではsegment_resultsをキーに持つ要素を参照します。返り値はJSONにdumpする必要はなく、辞書型で値を渡します。
テストデータの準備
各Lambda関数のデプロイが完了し、ステートマシンの定義も完成しました。あとはStep Functionsの生成と実行をするだけです。
ステートマシンをLocalStackに作成する前に今回使用するテストデータを生成します。
テストデータはtest-bucket/test.jsonに格納します。
以下、テストデータを生成するPythonスクリプトです。utilsフォルダ配下に作成してください。
import datetime |
テストデータをLocalStack上のS3バケットに格納します。
プロジェクトルートに戻って以下コマンドを実行します。必要なコマンドはすでにMakefileに記載してあります。
make buket |
make json |
これでLocalStackのS3バケット上にtest.jsonが作成されました。
Step Functionsの実行
それではStep Functiionsを実行します。
プロジェクトルートで以下のコマンドを実行してください。
make stepfunctions |
実行するとLocalStackのログで各Lambdaが処理を開始しているのを確認できます。
ターミナルの右反面でステートマシンを作成、実行、消去しています。
ターミナルの左半面はLocalStackで書き出されるログです。
よく見るとScatterLambdaで後続のSegmentLambdaに渡したsegment_definitionsや、segment_resultsなども出力されていることがわかります。
ログを確認したい方はコンテナ起動時に-dオプションを付けずに起動してください。
またdockcer-compose.ymlのDEBUGオプションを1とすることで画像のようにタスク定義なども確認できます。
demo-gatherによってアップロードされたExcelファイルをローカルにダウンロードしましょう。
make download |
downloadに成功していればプロジェクトプロジェクトルート直下にresultフォルダが生成されtest.xlsxが生成されていると思います。
想定通りの出力が得られましたね🎉
今回はデモなので処理もステップ数も大したことはありません。
より大規模な処理が必要となるときStep Functionsでうまく分散することでLambdaで超えられない壁を突できるよううになります。
それでは、今回はここまでとしたいと思います。
今回作成したスクリプトはこちらに格納してます。
https://github.com/orangekame3/stepfunctions-demo
さいごに
いかがでしたでしょうか、Step Functionsでは性質上、複数のリソースを連動させて処理を行います。デバッグの都度リソースをデプロイをするのはかなりの労力を伴うのでローカル環境で動作確認を行えるのはとても良いですね。
今回はLambdaの並列実行でしたが、様々な用途に応用が期待できそうです。
長くなりましたが、最後まで読んでいただきありがとうございました。