🎤

SAM を使って AWS Lambda の Ruby ランタイムで Sinatra を動かす

に公開

概要

Ruby は随分触っていなかったが、いつの間にか AWS Lambda で Ruby ランタイムがサポートされるようになっていた。

https://docs.aws.amazon.com/lambda/latest/dg/lambda-ruby.html

3.4まで使えるらしい。SAM と Sinatra を使ってお手軽に API を構築できたりしたら何かの役に立つかも知れないのでやってみた。

手順

STEP1. SAMのインストール

SAM を使うこと自体もかなり久しぶりだった。4年ぶりくらいなので今のPCに SAM が入っていなかった。
改めてインストールする。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/install-sam-cli.html

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 公式のサンプルコードにもあるもので、ほぼそのままである。

https://github.com/aws-samples/serverless-sinatra-sample/blob/master/lambda.rb

しかし、この公式サンプルコードはメンテされていない。実際に使う場合は自分の目で見たり 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 を使う方法には二種類あるらしい。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-secrets-manager.html

  • 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

このスニペットは、

https://docs.aws.amazon.com/ja_jp/secretsmanager/latest/userguide/retrieving-secrets-ruby.html

にある。

使うときは以下のように簡単に書ける。

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 を使っていきたい。

Discussion