SAM を使って AWS Lambda の Ruby ランタイムで Sinatra を動かす
概要
Ruby は随分触っていなかったが、いつの間にか AWS Lambda で Ruby ランタイムがサポートされるようになっていた。
3.4まで使えるらしい。SAM と Sinatra を使ってお手軽に API を構築できたりしたら何かの役に立つかも知れないのでやってみた。
手順
STEP1. SAMのインストール
SAM を使うこと自体もかなり久しぶりだった。4年ぶりくらいなので今のPCに SAM が入っていなかった。
改めてインストールする。
Mac では brew とかでインストールするのではなく、pkg ファイルをダウンロードしてインストールするようになってるらしい。
SAM には sam init というコマンドがあるらしいのだが、今回はそれを使わず Gemini にやるべき手順を出させてそれを参照しながら環境構築した。
STEP2. Lambda と Sinatra のブリッジ
まずは普通に Sinatra アプリのコードを書いておく。その後、Sinatra を Lambda のハンドラにつなぐブリッジを書く。
lambda.rb
require 'json' require 'rack' require 'base64' require_relative 'app' # Sinatraアプリを読み込み # Sinatraアプリのインスタンス化 $app ||= App.new def handler(event:, context:) # 1. API Gateway のイベントを Rack 環境変数に変換 env = { 'REQUEST_METHOD' => event['httpMethod'], 'PATH_INFO' => event['path'], 'QUERY_STRING' => URI.encode_www_form(event['queryStringParameters'] || {}), 'SERVER_NAME' => 'localhost', 'SERVER_PORT' => '443', 'rack.version' => Rack::VERSION, 'rack.input' => StringIO.new(event['body'] || ''), 'rack.errors' => $stderr, 'rack.multithread' => false, 'rack.multiprocess' => false, 'rack.run_once' => false, 'rack.url_scheme' => 'https' } # ヘッダーを Rack 形式に追加 (HTTP_...) event['headers']&.each do |key, value| env["HTTP_#{key.upcase.tr('-', '_')}"] = value end # 2. Sinatra アプリを実行 status, headers, body = $app.call(env) # ボディの読み出し(Rack body は each 可能なオブジェクト) response_body = "" body.each { |part| response_body += part } # 3. API Gateway 形式のレスポンスを返却 { statusCode: status, headers: headers, body: response_body, isBase64Encoded: false } rescue => e # エラーハンドリング { statusCode: 500, body: { error: e.message, backtrace: e.backtrace }.to_json } end
このようなブリッジは AWS 公式のサンプルコードにもあるもので、ほぼそのままである。
しかし、この公式サンプルコードはメンテされていない。実際に使う場合は自分の目で見たり AI にチェックさせたりしたほうが良さそうだ。
STEP3. SAM のテンプレート作成
次に SAM のテンプレートを作成。 Cloudformation の YAML の拡張版みたいなもの。
template.yaml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Sinatra running on AWS Lambda Globals: Function: Timeout: 30 MemorySize: 128 Resources: SinatraFunction: Type: AWS::Serverless::Function Properties: CodeUri: . # カレントディレクトリを指定 Handler: lambda_handler.handler Runtime: ruby3.4 # 使用するバージョンに合わせて変更 Architectures: - x86_64 Policies: - SecretsManagerReadWrite Events: SinatraApi: Type: Api Properties: Path: /{proxy+} # すべてのパスをキャッチ Method: ANY # すべてのHTTPメソッドをキャッチ RootApi: # ルートパス (/) 用の設定 Type: Api Properties: Path: / Method: ANY Metadata: BuildMethod: makefile
外部サービスの API トークン等を使いたいので、 Policies に SecretsManagerReadWrite を追加している。
Secrets Manager の組み込みポリシーには AWSSecretsManagerClientReadOnlyAccess という2025年11月5日に追加された新しいポリシーもある。こちらのほうがどう考えても安全なので使いたかったのだが、こちらを使うと sam deploy でコケるので諦めた。おま国的な事情かもしれない。
また、 BuildMethod を makefile にしている。Makefile にビルドスクリプトを書いているため。
Makefile
build-SinatraFunction: cp -a * "$(ARTIFACTS_DIR)" cd "$(ARTIFACTS_DIR)" && \ bundle config set --local path 'vendor/bundle' && \ bundle config set --local without 'development test' && \ bundle install
ARTIFACTS_DIR には sam build コマンドを実行した後にビルド後のファイルが格納されるディレクトリのパス。ローカルで動作確認するときにも使う。
STEP4. ローカル動作確認とデプロイ
ローカルで動作確認するときは、
sam build && sam local start-api
を実行。デプロイのときは、
sam build && AWS_PROFILE="{AWS アカウントのプロファイル名}" sam deploy
をしている。
初回デプロイ時には
sam deploy --guided
をすると、次回以降用に設定を保存するための samconfig.toml が生成される。
補遺: Secrets Manager を使う方法
Lambda で Secrets Manager を使う方法には二種類あるらしい。
- AWS Parameters and Secrets Lambda Extension - シークレットを取得するためのシンプルな HTTP インターフェイスを提供するランタイムに依存しないソリューション
- Powertools for AWS Lambda パラメータユーティリティ - 組み込み変換で複数のプロバイダー (Secrets Manager、Parameter Store、AppConfig) をサポートするコード統合ソリューション
なんとなく後者のほうが高レベルで便利そうだが、Python、TypeScript、Java、.NET しかサポートしていない。今回は必然的に前者を使うことになる。
Ruby のコードでは以下のように書くことになる。
Gemfile
+ gem 'aws-sdk-secretsmanager', '~> 1.124'
secrets.rb
# Use this code snippet in your app. # If you need more information about configurations or implementing the sample code, visit the AWS docs: # https://aws.amazon.com/developer/language/ruby/ require 'aws-sdk-secretsmanager' def get_secret client = Aws::SecretsManager::Client.new(region: 'ap-northeast-1') begin get_secret_value_response = client.get_secret_value(secret_id: 'trello-line-connect') rescue StandardError => e # For a list of exceptions thrown, see # https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html raise e end secret = get_secret_value_response.secret_string return JSON.parse(secret) end
このスニペットは、
にある。
使うときは以下のように簡単に書ける。
require_relative 'lib/secrets' secrets = get_secret api_token = secrets['API_TOKEN']
感想
Ruby は暗黙の約束みたいなのが多くて苦手意識があり、Rails はそれに輪をかけて暗黙のルールが多く苦手だった。 しかし Sinatra は Rails に比べてそこまで暗黙のルールが多くないような気がするのでまだ書けるかもと思った。
SAM で頑張って色々構築してみたが、正直言って Cloudflare Workers での開発体験と比較すると雲泥の差だ。TypeScript の楽さも相まって、Workers 向けにコードを書いてデプロイしているときのほうが圧倒的に早いし快適。生産性には非常に大きな差があると感じた。
一旦環境ができあがってしまえば大差ないのだろうし、細かい要求が多いプロジェクトではコントローラブルな変数が多い AWS のほうが良いのだろうが、多数のプロダクトをスピード感を持って出したい場合には AWS はめちゃくちゃ足枷が多いと感じる。
もちろん AWS のほうができることは遥かに多いし、サーバーレスアプリ開発の生産性だけで優劣を比較できるような話ではないものの、エッジでアプリを動かすということに特化している Cloudflare には他者が真似できない強みがあると実感した。
今後も理由が無ければ Cloudflare Workers を使っていきたい。