コンテナランタイムのLambdaを使ってEC2で動いていたバッチ処理をサーバーレス化したことによって得られた知見
こんにちは、あれんぱちゃんです。
ナビタイムジャパンでユーザーの移動ログ分析の研究開発を担当しています。今回はEC2上で動かしていたバッチをlambdaを使ったサーバーレス構成に変更したことによる効果と、そのメリットについてご紹介します。
普段業務でバッチ処理の開発にかかわっている方や、Lambdaなどを使ったサーバーレス開発に興味のある開発者の方に読んでいただければ幸いです。
はじめに
私が所属するチームでは、ユーザーの車での走行ログから一時不停止や進入が禁止されている道路への誤侵入、スピード違反などの交通違反を分析するバッチ処理を開発しています。
このシステムはC++で実装されており、EC2インスタンス上で送られてきたログを受け取り、pythonのプログラムからこのシステムをサブプロセスとして実行し、分析結果を蓄積するという構成で運用されていました。
パフォーマンスには何も問題はありませんが、この構成には以下の課題がありました。
デプロイに必要なフローが多い
EC2だけでなく、オートスケーリングの設定やインスタンス起動時のuserdata(起動時に実行するスクリプトの設定)など、交通違反分析のコアロジック以外の部分の運用コストが発生する。
単純なバッチ処理であればEC2上で動かすのではなくlambdaを使ったサーバーレス構成を採用するという選択肢も候補として挙げられます。
lambdaを導入してサーバーレス構成とすることで以下のメリットがあります。
インフラコストが削減できる
EC2インスタンスを使った運用だと、リクエストが来ない深夜帯でもサーバを稼働させ続ける必要があり、その分余計なインフラコストが発生します。lambdaはリクエストが処理されている時間にのみ課金されるためインフラコストが削減できます。
文字通りサーバのことを気にする必要がなくなる
EC2インスタンスを使ってバッチ処理を動かす場合、オートスケーリングの設定など、サーバー自体の設定を必要に応じて都度調整する必要があります。lambdaを利用したサーバーレス構成を導入することで、リクエスト量に応じて同時実行量のスケーリングを任せることができるようになります。これにより、文字通りサーバーのことを考えずに集中したいロジックの改善に割くリソースを増やすことができます。
しかし、この運用が開始した当初は以下の理由で採用を見送った経緯がありました。
違反分析に必要な地図データが2GBほどある
従来Lambdaで大容量ファイルを扱う場合はS3から/tmpにファイルをDLするようなアーキテクチャが選択されていました
この/tmpディレクトリの利用可能な上限は512MBまでに制限されていたため、当時の要件としてはlambdaを採用することができませんでした。
Lambdaがコンテナイメージをサポートするようになった
2020年12月のアップデートで、lambdaのランタイムとしてコンテナイメージを利用できるようになりました。
このコンテナイメージを利用することで、GoやRustなどのコンパイル済み言語や、Lambda がベースイメージを提供していない言語または言語バージョン (Node.js 19 など) を使ったlambdaをデプロイすることができるようになります。
また、このコンテナイメージを使うことで、最大10GBまでのコンテナイメージをサーバーレスな基盤上で動作させることが可能となりました。
コンテナイメージをランタイムとすることで、大きなデータをインプットとした交通違反分析の処理をlambda上で動かすことができます。
これらのメリットを踏まえ、EC2上で動かしていたバッチ処理をlambdaを使ったサーバーレスな構成に置き換えることを決定しました。
実装してみる
今回サーバーレス構成を導入するにあたって、プログラムに採用する言語についても社内で近年導入実績が増えていること、pythonと比較して高速で動作することを鑑みてpythonからgoに置き換えることにしました。以下のような組み合わせでバッチを構成するようになりました。
ディレクトリ構成は以下のようになっています。
.
├── Dockerfile // ランタイムとなるDockerfile
├── bin
│ └── main // main.goをコンパイルしたbinファイル
├── data
│ └── mapdata // 地図データ
├── go.mod
├── go.sum
├── lib
│ ├── setting.yaml // 交通違反分析を実施するための設定ファイル
│ └── trafficViolationAnalysis // 交通違反分析をするライブラリ
├── main.go // 交通違反分析バッチ処理のエントリーポイントとなるgoのプログラム
└── template.yaml // AWS SAMを使ってLambda関数をデプロイするための設定ファイル
goのプログラムでは、SQSへのキューの蓄積をトリガーにLambdaが発火し、S3から走行ログを取得し交通違反分析を実施し、その結果をS3に保存するという処理を実施します。
今回デプロイするlambdaはコンテナイメージをランタイムとするので、Dockerfileを作成する必要があります。Dockerfileは下記のような内容になります。
# lambdaを動かすベースイメージを指定
FROM public.ecr.aws/lambda/provided:al2023
# LD_LIBRARY_PATHを上書き
ENV LD_LIBRARY_PATH=/lib/trafficViolation/:$LD_LIBRARY_PATH
# コンパイルしたgoのプログラムをコンテナ上にコピー
COPY ./bin/main /
# 地図データをコンテナ上にコピー
COPY .data/mapdata data/mapdata/
# コンパイルした交通違反を分析するプログラムをコンテナ上にコピー
COPY ./lib/trafficViolationAnalysis /lib/trafficViolationAnalysisLib/
template.yamlでpackageTypeをImageに指定することで、先ほど作成したDockerfileをエントリーポイントとしてLambda関数を実行できます。`LambdaFunction`という名前でバッチ処理を定義しています。
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
"コンテナイメージをランタイムとするlambdaをビルドするためのtemplate.yaml"
Globals:
Function:
Timeout: 30
Environment:
Variables:
# タイムゾーン
TZ: "Asia/Tokyo"
Resources:
LambdaFunction:
Type: AWS::Serverless::Function
Properties:
PackageType: Image
ImageConfig:
EntryPoint:
- /main # Dockerfileで指定したエントリーポイントを指定
ImageUri: !Ref ImageUri
FunctionName: LambdaFunction
MemorySize: 1024
これらのgoのファイル、Dockerfile, template.yamlを使ってSAMでLambda関数をデプロイすることで、大きなデータをインプットとするプログラムをAWS環境上で動かすことができるようになりました。
さいごに
今回EC2インスタンス上で動かしていたバッチ処理をLambdaを使ったサーバーレス構成に変えたことで、以下のような結果が得られました。
リリースまでのリードタイムを削減できた
今までのシステム構成だとリリース時にEC2インスタンスが立ち上がりプログラムが稼働するまでにオートスケールの設定、インスタンスの起動、システムのスタートを待つ必要がありました。
今回サーバーレス対応を実施したことで、スケーリング処理をAWS側に任せられるようになり、Lambdaへのデプロイも短時間で完了するようになったため、リリースまでのリードタイムを85%削減することができました
サーバーそのものの運用を気にする必要がなくなった
今までのシステム構成では、バッチを動かすための言語のバージョンアップや、OSのサポート終了に伴うアップデートなど、コアロジック以外の部分での運用作業が発生していました。
サーバーレス対応を実施したことで、バッチを動かすために必要なインフラの設定や運用を気にする必要がなくなり、コアロジックそのものの機能改善や実装に時間を割くことができるようになりました。
コンテナイメージを使ってデプロイするlambdaを利用することで、バッチ実行の選択肢が広がりました。また、サーバーレスな構成を導入することで本当に集中したいコアロジックの改善に時間を使えるようになることを改めて実感することができました。
この記事を読んでいただいた方の身の回りに、応用できるものがあればぜひ試してみてください。最後までお読みいただきありがとうございました。