ブログ

【AWS】Web開発基盤構築時のサービス選びのコツ(EC2 vs Lambda)

この記事をSNSでシェア!
  今回事業部の取り組みの一環として、Web開発向けに再利用可能なAWS共通基盤を構築しました。
特にバックエンドサービスの選定に関しては、これまでに紆余曲折がありましたので、当時の調査結果も含めてご紹介していきます。

LambdaはJavaには不向き?

結論から先にお伝えすると、Javaでバックエンド開発を行う場合にはLambdaの使用はオススメいたしません。当社で実装したところ深刻なコールドスタートが生じてしまい、やむなくEC2へのプログラム移行を行うに至りました。ただし、Lambdaが全く使えないというわけではなく、使用するDB、開発言語、システム要件等によっては有用となるケースもありますので、ユースケースに応じた使い分けが必要となります。
詳しい経緯や原因については後述でご説明します。

共通基盤を作ったワケ

私たちBS事業部は年間30件以上の開発案件を受注しています。
従来は標準の基盤構成がなく、Web開発案件では顧客要望に合わせて基盤構成をいちから検討し構築作業を行っていました。そこでWeb開発向けのインフラ基盤を構築し、AWS Cloud Formationでテンプレート化して再利用可能なAWS共通基盤を作りました。これにより、

1. インフラ構築、テストにかけるコストの削減
2. テンプレートを流用することによるインフラ基盤の品質担保

を図ることを主な目的としています。

■ AWS Cloud Formationを活用した構築自動化(参考値)

①今回のインフラ構成(※「当初想定した基盤構成」の項参照)を1から構築した場合

必要な作業 工数
フロントエンドサービスの構築(手動) 3H
バックエンドサービスの構築(手動) 4H
その他サービスの構築(手動) 10H
合計 17H

②①と同じ構成をAWS Cloud Formationで構築した場合

必要な作業 工数
CloudFormationの実行 1.5H
実行後の微調整 0.5H
合計 2H

当初想定した基盤構成

 
 

主なサービス
サービス名 用途
Amazon CloudFront、Amazon S3
  • Webコンテンツの公開
  • Webコンテンツの保管、ログファイルの保管
Amazon API Gateway、AWS Lambda
  • バックエンド処理をLambda関数化して管理
Amazon RDS Proxy、Amazon RDS
  • Lambda⇔RDS間の中継サービス、DBへの最大接続数の管理
  • マスタデータ、トランザクションデータの管理
Amazon CloudWatch、Amazon SNS、Amazon SES、Kinesis Data Firehose
  • ログデータの蓄積
  • エラーログ出力時のアラートメール送信
  • S3へのログファイルの送信
なぜLambdaを選んだのか?

バックエンドサービスにLambdaを選定した理由は次の2つです。

1. 可能な限りサーバレスサービスを使用することで、従来のサーバ管理作業を削減したかった
2. 基盤のランニングコストを最小限に抑えたかった

構築後のコスト(人手&費用)の削減を重視してLambdaを選定しました。ちなみにLambdaとRDSの組み合わせは最大同時接続数の問題により長年アンチパターンとされていましたが、2020年に「Amazon RDS Proxy」が実装されたことによって改善されました。

【参考】サーバレスアンチパターンが無くなった日 – Qiita

使用言語
フロントエンド Vue.js(JavaScript)、Sass
バックエンド Spring Boot(Java)、MyBatis(O/Rマッパー)

学習コストの削減と、今後案件間で技術者の共有が出来るように開発実績のあるVue.jsとSpring Bootを標準のプログラム言語としました。なお、「AWS CodeStar」を使えば比較的簡単にLambdaの構築とSpring Bootプロジェクトの作成が出来るため、この方法を使ってバックエンド環境の構築を実施しました。

【参考】AWS CodeStarを使用した開発環境構築手順 – Qiita

開発過程での課題

Lambdaでのバックエンド開発に際し、次の課題が発生しました。

SpringBootのプログラムをLambda関数に変換するのに時間を要した

当初ローカル環境で開発したSpringBootのプログラムをLambdaに移送する際に、環境差異から大幅なプログラム修正が必要になりました。ただし、これらの課題ははじめからLambda環境で実装していれば起こらない問題だったため、基盤の構築は引き続きLambdaを使う想定で進めました。

No 修正箇所 詳細
1 共通処理のLambda Layer化 Lambdaのサイズ制限(250MB)により、共通処理をLambda Layerとしてパッケージ化する実装が必要となった。Lambda Layer未経験だったため、実装コストのみでなく学習コストも発生した。
2 ライブラリ差分による修正 デプロイ時にエラーを起こしてしまうため、lombok等の主要ライブラリを導入することが出来なかった。またCodeStarで生成したプロジェクトのSpring Bootのバージョンが古く、バージョンを上げると同じくデプロイエラーが発生してしまった。これらの理由により、当初の記述方法で処理を実装することができなかった。(ただしこちらは、CodeStarでプロジェクト生成したことが起因している可能性もある。)

【参考】Lambdaのサイズ制限について(AWS公式)

無事構築出来たが

その後無事に基盤を構築することはできましたが、構築基盤上でLambda関数(API)を実行した際に致命的な問題が発生しました。

Lambdaのコールドスタート

そもそもLambdaには、「起動時に関数インスタンスを起動し、一定時間経過後に自動でインスタンスを破棄する」という特性があります。そのため、一定時間呼び出しが行われなかったLambda関数を初回実行した際に、通信速度が遅くなってしまうという課題があります。

この特性についてはバックエンドサービスの選定時に認識していたものの、当時参照していた文献では2~3秒程度の遅延とのことだったので、許容範囲としていました。しかし、今回開発した実行環境ではこの通り・・・

  1回目 2回目 3回目 4回目 5回目 6回目 7回目
通信速度(秒)
※赤字は初回実行時
22.20 4.56 23.51 1.54 3.17 3.41 22.88

とてもじゃないですが、許容できる通信速度ではありませんでした。

■ Lambda 関数インスタンスの起動

【出典】全部教えます!サーバレスアプリのアンチパターンとチューニング

コールドスタート時の遅延を助長している原因
コンパイル型言語との相性が良くない

こちらが今回の事象の主な原因と考えられます。スクリプト型言語(Python, Node, Ruby等)の場合、コールド時・ウォーム時の通信速度に大きな差異はみられませんが、javaのようなコンパイル型言語の場合にコールドスタート時に大きく遅延が発生すると言われています。(ただし、ウォーム時はコンパイル型の方が速いという文献もあります。)

またこれ以外にも、今回使用したJavaのフレームワークであるSpringBootはサイズが大きいため、Lambda環境を圧迫します。Lambdaのサイズ上限に触れないように共通処理をLambda Layer化して容量を節約することも出来ますが、APIを何本も作るような中~大規模のシステムには不向きと言えます。

【参考】Lambdaのコールドスタートを改めて整理する – Qiita

その他に考えられる原因
VPC内にLambdaを配置している

DBにRDSを使用していることにより、本来VPC外で使用すべきサービスであるLambdaを、RDSと同じVPC内に設置する必要がありました。これにより、関数インスタンス起動時にENIの作成が必要となり、その分コールドスタート時の遅延を悪化させている可能性があります。
ただし、こちらに関しては近年VPC Lambdaの速度遅延が改善されたという記事もあります。

【参考】【速報】もうアンチパターンとは呼ばせない!!VPC Lambdaのコールドスタート改善が正式アナウンスされました!!

改善策

今回の場合はLambdaのサイズ上限などもネックとなりEC2への移行を行いましたが、Lambdaのままコールドスタートの遅延を改善する方法がいくつかありますのでご紹介します。

1. Lambdaの定期実行

Lambdaのトリガー機能を使って定期的に関数を自動実行することでウォーム状態を保つ方法。リクエスト数が増えるため、Lambdaの月額料金が増額することと、定期的に実行させていてもコールド状態になるケースが見られたため断念しました。

【参考】AWS Lambda暖気運転

2. Provisioned Concurrency

Lambdaの同時実行数を事前にプロビジョニングすることで、常にウォーム状態で関数を実行できるようにするというもの。一定の効果は見られましたが、今回開発した環境の場合Lambda関数の件数が多かったため、追加費用が高額となり見送りました。

【参考】LambdaのProvisioned Concurrencyを使って、コールドスタート対策をしてみた

3. Serverless WarmUp Plugin

定期実行用のウォーマーを作成し、関数を強制的にウォーム状態に保つプラグイン。実装コストがかかることを考え見送りました。

【参考】Serverless WarmUp Plugin(npm)

まとめ

今回の基盤構築から分かったことをもとに、LambdaとEC2の選定基準を要件ごとにまとめました。
バックエンドサービスを検討する際の目安として是非お役立てください。

システム規模 小規模 Lambda 関数の数が少ない小規模システムには適している。(ただしコンパイル型言語を使用する場合、後述のサイズ制限に抵触するおそれがあるためEC2がおすすめ)
中~大規模 EC2 Lambdaにはサイズ制限(250MB)があるため、特にJavaで開発する場合はAPI本数やライブラリ数の多いシステムには不向き。
管理コスト削減重視 Lambda サーバレスサービスのため、パッチ適用やバージョンアップ等の対応が不要。
ランニングコスト削減重視 Lambda リクエスト数が400万件/月以下のシステムであれば、Lambdaの方が安い。
処理速度重視 EC2 インスタンスが常時起動状態のためコールドスタートは発生せず、通信速度が速い。
実装の手軽さ重視 EC2 コーディング時の制限や、言語による相性は考慮しなくて良いため、その分実装は手軽。
Javaとの親和性重視 EC2 Java×Lambda時に起こるコールドスタート時の遅延や、容量オーバーによるデプロイエラーが、EC2の場合は考慮しなくて済む。

今回の場合、コールドスタート時の遅延が深刻だったことから、最終的にはEC2へのプログラム移植を行いましたが、移植することで最長20秒ほどかかっていた通信速度を平均0.14秒にまで改善することができました。
とはいえ、移植に際してEC2のセットアップなど色々やらなければならないことがあったので、そちらの手順についてもおいおい記事にしていきたいと思います。


投稿者プロフィール

大岩 香緒里
大岩 香緒里
2015年入社。会計管理システムやBIシステムのパッケージ開発を経て、現在はWebのスクラッチ案件で開発リーダーを担当しています。
この記事をSNSでシェア!