メインコンテンツまでスキップ

Ruby on Rails を API Gateway + Lambda でサーバーレス運用する方法

· 約14分
Yukinobu Mine

Ruby on Rails で開発された Web アプリケーションを AWS で稼働させる際、EC2 の仮想サーバーや ECS のコンテナ環境で実行して ALB 等のロードバランサーで公開するのが一般的です。この方法は稼働時間に比例した料金となりますが、もう1つの選択肢として、時間ではなくリクエスト回数に応じた料金となる API GatewayLambda を使う方法があります。今回は、Ruby on Rails を AWS Lambda で動かす方法を解説します。

Ruby on Rails とは

Ruby on Rails は、Ruby 言語で Web アプリケーションを開発するためのフレームワークの1つです。「設定より規約 (Convention over Configuration, CoC)」というパラダイムの先駆者として有名で、他のフレームワークにも大きな影響を及ぼしました。

rails コマンドによるコードの自動生成機能が充実しており、プロジェクトの作成だけでなく、モデルやコントローラーの生成や認証機能の追加といった事まで行う事ができます。特に Active Record によるモデルとテーブル定義の自動生成は便利で、Web アプリ開発で必須となるデータベースアクセスを簡単に実装できます。

今回は、先日 aws-samples として公開された AWS Lambda handler sample for Ruby on Rails を元に、Ruby on Rails で実装された Web アプリを AWS Lambda で実行し、Amazon API Gateway で公開する方法とその仕組みについて解説します。

Ruby on Rails による REST API の実装

AWS Lambda handler sample for Ruby on Rails では、Ruby on Rails で実装した Web アプリのサンプルを提供しています。この Web アプリは、Ruby on Rails の API モード を利用して実装したシンプルな Web API で、「Hello, World!」という固定データのみを持つ SQLite データベースに対してクエリーを行う事ができます。

まずは、このサンプルアプリについて簡単に解説します。

サンプル Web アプリのモデル定義

モデルは以下のコマンドで生成し、テーブルを自動生成しています。

rails generate model Message text:string

これによって生成されたテーブル「messages」の定義は以下の通りです。

カラム名意味
idintegerメッセージ ID
textvarcharメッセージ
created_atdatetime作成日時
updated_atdatetime更新日時

固定の SQLite データベースファイルには、以下のようなデータを格納しています。

idtextcreated_atupdated_at
1Hello, World!2022-12-15 01:31:59.2915702022-12-15 01:31:59.291570

サンプル Web アプリのコントローラー定義

コントローラーは以下のコマンドで生成しています。

rails generate controller Messages

生成されたコントローラー「MessagesController」には、以下のアクションを実装しています。

名前ルート動作
indexGET /messagesメッセージ一覧を返す
showGET /messages/{id}指定された ID のメッセージを返す

メッセージは以下のような JSON データとして返しています。

{
"id": 1,
"text": "Hello, World!",
"created_at" :"2022-12-15T01:31:59.291Z",
"updated_at": "2022-12-15T01:31:59.291Z",
"url": "メッセージの URL"
}

サンプル Web アプリの動作確認

AWS Lambda handler sample for Ruby on Rails をローカルに clone し、rails ディレクトリで以下のコマンドを実行する事により、ローカル環境で動作確認できます。

rails server

cURL 等のツールで http://localhost:3000/messages に GET リクエストを行うと、以下のようなレスポンスが返されます。

curl http://localhost:3000/messages

[{"id":1,"text":"Hello, World!","created_at":"2022-12-15T01:31:59.291Z","updated_at":"2022-12-15T01:31:59.291Z","url":"http://localhost:3000/messages/1"}]

Ruby on Rails と Web サーバーの関係

Ruby on Rails において、Web サーバーへのリクエストが Web アプリで処理され、結果がレスポンスとして返されるまでの流れを簡単に解説します。

Ruby on Rails と Web サーバーの関係

Ruby 言語では、Web アプリとアプリケーションサーバーとの間のインターフェイス仕様として rack が広く採用されています。Ruby on Rails はこの rack に準拠しており、標準アプリケーションサーバーとして puma が採用されています。puma は、Web サーバーが受け取った HTTP リクエストを rack 仕様に基づいて Web アプリに伝え、Web アプリが rack 仕様に基づいて生成したレスポンスを HTTP レスポンスとして Web サーバーに返します。

puma は、単体でも Web サーバーとして動作させる事が可能ではありますが、Web サーバー機能が使われるのはテスト用途が多く、本番環境ではよりパフォーマンスやスケーラビリティに優れた nginx 等を利用するのが一般的となっています。(前節で rails server を実行した際は、puma を Web サーバーとしても利用した形となります)

AWS で Ruby on Rails を動かす方法

Ruby on Rails で実装された Web アプリを AWS で稼働させる方法はいくつかありますが、今回は代表的な2パターンを紹介します。

ALB + EC2 / ECS

最も一般的なのは、nginx や Ruby on Rails アプリを Amazon EC2 仮想マシンにインストールしたり、Amazon ECS で Docker コンテナ化として実行し、Application Load Balancer 等のロードバランサーで公開する方法です。

ALB + ECS のアーキテクチャ例

この方法は、Ruby on Rails の標準的な構成をほぼそのままクラウドに移行できるメリットがあります。料金は稼働時間に比例するため、常に一定以上の稼働率があるサービスに向いた方法になります。

API Gateway + Lambda

そしてもう1つの方法が、AWS Lambda で Ruby on Rails アプリを実行し、Amazon API Gateway の Lambda プロキシ統合で公開する方法です。料金はリクエスト数に比例するため、稼働が特定の時間帯に集中するようなサービスに向いた方法になります。

API Gateway + Lambda のアーキテクチャ例

AWS Lambda で Ruby on Rails を実行するには、Lambda 関数の実行環境の制約に合わせていくつかの設定変更が必要となります。また、API Gateway からは HTTP リクエストが Lambda 関数のパラメーターとして渡され、Lambda 関数の戻り値から HTTP レスポンスが生成されるため、それらを Ruby on Rails が理解できる rack 仕様に変換する処理も必要となります。

Ruby on Rails と API Gateway / Lambda の関係

Ruby on Rails を Lambda 関数化する

Ruby on Rails アプリを Lambda 関数化する方法には以下の2種類が存在します。

2022年12月現在、AWS Lambda の Ruby ランタイムが対応している Ruby 言語のバージョンは 2.7 のみのため、Ruby 3.0 以降を利用したい場合は独自のコンテナイメージを作成する必要があります。

本記事ではそれぞれの方法の詳細は割愛しますが、Dockerfile のサンプルを提供していますので、独自のコンテナイメージを作成する際は参考にしてみて下さい。

環境変数の設定

Lambda 関数を作成したら、環境変数を設定します。Ruby on Rails の一般的な環境変数に加えて、Lambda 関数の実行環境では /tmp 以外のディレクトリに書き込む事ができないという制限に対応するための以下の設定が必要となります。

環境変数設定例目的
BOOTSNAP_CACHE_DIR/tmp/cacheRails アプリの起動高速化のためのキャッシュディレクトリを /tmp 以下に変更する
RAILS_LOG_TO_STDOUT1ログをファイルではなく標準出力に出力し、CloudWatch Logs で閲覧可能にする

ハンドラー関数の実装

Lambda 関数のエントリーポイントとなるのがハンドラー関数です。API Gateway からのリクエストとレスポンスを rack 仕様に変換して Ruby on Rails アプリを実行する処理は、ハンドラー関数で行う必要があります。

Amazon API Gateway には REST API と HTTP API があり、それぞれリクエストとレスポンスを Lambda 関数とやり取りするための入出力フォーマットが異なっています。それぞれの仕様へのリンクを以下に示します。

rack の入出力仕様との相互変換

AWS Lambda handler sample for Ruby on Rails では、API Gateway の種類に応じて rack の仕様に基づいた変換処理を行うハンドラー関数のサンプルを提供しています。

上記のコードを Ruby on Rails アプリのルートディレクトリにコピーし、Lambda 関数のハンドラー関数として設定すれば、Ruby on Rails アプリを Lambda 関数として実行する事ができます。

ハンドラー関数が行っている変換処理の概要は以下の通りです。

リクエスト対応表

REST API と HTTP API の入力パラメーターは、rack の env 変数に変換されます。

rack の env 変数名意味REST APIHTTP API
REQUEST_METHODHTTP メソッドhttpMethodrequestContext.http.method
SCRIPT_NAMEパスのルート環境変数 RAILS_RELATIVE_URL_ROOT の値環境変数 RAILS_RELATIVE_URL_ROOT の値
PATH_INFOパスpathrequestContext.http.path
QUERY_STRINGクエリ文字列multiValueQueryStringParameters から再構築rawQueryString
SERVER_NAMEドメイン名headers.X-Forwarded-Host
or headers.Host
headers.x-forwarded-host
or headers.host
SERVER_PORTポートheaders.X-Forwarded-Portheaders.x-forwarded-port
SERVER_PROTOCOLHTTP バージョンrequestContext.protocolrequestContext.http.protocol
rack.url_schemeURL スキームheaders.CloudFront-Forwarded-Proto
or headers.X-Forwarded-Proto
headers.cloudfront-forwarded-proto
or headers.x-forwarded-proto
rack.inputHTTP ボディbodybody
CONTENT_LENGTHボディのサイズbody のサイズから計算body のサイズから計算
HTTP_COOKIECookie データmultiValueHeaders.Cookiecookies から再構築
HTTP_*その他のヘッダーmultiValueHeadersheaders

レスポンス対応表

rack のレスポンスは、REST API と HTTP API の出力パラメーターに変換されます。

レスポンスの要素REST APIHTTP API
ステータスコードstatusCodestatusCode
ボディbodybody
Cookie データmultiValueHeaders.Set-Cookiecookies
その他のヘッダーheaders
or multiValueHeaders
headers

サンプルを AWS で動かしてみる

AWS Lambda handler sample for Ruby on Rails では、冒頭で解説したサンプルアプリを API Gateway + Lambda 構成でデプロイする AWS CDK のサンプルも提供しています。

サンプル Web アプリの動作確認を行った後、AWS CDK のインストールと設定を行った上で、cdk ディレクトリで以下のコマンドを実行するとデプロイが開始されます。

npm ci
npx cdk deploy -c railsMasterKey=d1c2ae419e40d2c43006aacdf98cf7f0

デプロイが終わると、以下のような出力が表示されます。

RailsLambdaStack.HttpHttpApiUrlE6BB121E = https://xxxx.execute-api.ap-northeast-1.amazonaws.com/
RailsLambdaStack.RestRestApiUrl2AB183B3 = https://yyyy.execute-api.ap-northeast-1.amazonaws.com/default/

後は、ローカルで起動した時と同じように動作確認して下さい。

curl https://xxxx.execute-api.ap-northeast-1.amazonaws.com/messages

[{"id":1,"text":"Hello, World!","created_at":"2022-12-15T01:31:59.291Z","updated_at":"2022-12-15T01:31:59.291Z","url":"https://xxxx.execute-api.ap-northeast-1.amazonaws.com/messages/1"}]
curl https://yyyy.execute-api.ap-northeast-1.amazonaws.com/default/messages

[{"id":1,"text":"Hello, World!","created_at":"2022-12-15T01:31:59.291Z","updated_at":"2022-12-15T01:31:59.291Z","url":"https://yyyy.execute-api.ap-northeast-1.amazonaws.com/default/messages/1"}]

まとめ

今回は、AWS Lambda handler sample for Ruby on Rails のサンプルを中心に、Amazon API Gateway + AWS Lambda 環境で Ruby on Rails の Web アプリを稼働させるのに必要な技術的要素を解説しました。ユースケース次第では選択肢になり得ると思いますので、このような事が可能である事を覚えておいて頂ければと思います。