最初の Lambda 関数 — Hello World から始める
「まず動かしてみよう」
ダイチは翌日の昼休み、自分のAWS開発アカウントを開いた。理論は分かった。次は手を動かす番だ。最初のLambda関数として、シンプルな「Hello World」を作り、デプロイし、実行してみる。
開発環境のセットアップ
まずは必要なツールを揃える。
# AWS CLI のインストール確認
aws --version
# aws-cli/2.x.x Python/3.x.x ...
# AWS SAM CLI のインストール(Mac)
brew tap aws/tap
brew install aws-sam-cli
sam --version
# 認証情報の設定
aws configure
# AWS Access Key ID: ...
# AWS Secret Access Key: ...
# Default region name: ap-northeast-1
# Default output format: jsonINFO
AWS SAM(Serverless Application Model)は、Lambda関数のローカル開発・テスト・デプロイを簡単にするフレームワーク。CloudFormationの拡張として動作し、template.yaml 1ファイルでインフラを定義できる。
SAMプロジェクトの作成
# SAM アプリケーションの初期化
sam init \
--runtime ruby3.2 \
--name serverless-ec-api \
--app-template hello-world
cd serverless-ec-api
tree .
# .
# ├── README.md
# ├── events/
# │ └── event.json
# ├── hello_world/
# │ ├── Gemfile
# │ └── app.rb
# └── template.yaml生成されたファイルを見ていこう。
template.yaml — インフラ定義
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 30
MemorySize: 256
Runtime: ruby3.2
Environment:
Variables:
RACK_ENV: production
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: getapp.rb — Lambda ハンドラ
# frozen_string_literal: true
require 'json'
def lambda_handler(event:, context:)
# API Gateway からのリクエスト情報
http_method = event['httpMethod']
path = event['path']
query_params = event['queryStringParameters'] || {}
name = query_params['name'] || 'World'
puts "Received #{http_method} request to #{path}"
puts "Context: function_name=#{context.function_name}, " \
"remaining_time=#{context.get_remaining_time_in_millis}ms"
{
statusCode: 200,
headers: {
'Content-Type' => 'application/json',
'X-Custom-Header' => 'Lambda-Response'
},
body: JSON.generate({
message: "Hello, #{name}!",
timestamp: Time.now.iso8601,
function: context.function_name
})
}
rescue StandardError => e
puts "Error: #{e.message}"
puts e.backtrace.join("\n")
{
statusCode: 500,
body: JSON.generate({ error: 'Internal Server Error' })
}
endローカルでのテスト
SAMの最大の利点の一つは、Lambdaをローカルで実行できること。本番デプロイ前に動作確認ができる。
# ローカルで Lambda 関数を直接呼び出す
sam local invoke HelloWorldFunction --event events/event.json
# events/event.json の中身
cat events/event.json{
"httpMethod": "GET",
"path": "/hello",
"queryStringParameters": {
"name": "Daichi"
},
"headers": {
"Content-Type": "application/json"
},
"body": null
}# 実行結果
# START RequestId: xxx
# Received GET request to /hello
# Context: function_name=HelloWorldFunction, remaining_time=29998ms
# END RequestId: xxx
# REPORT RequestId: xxx Duration: 45.23 ms Billed Duration: 46 ms Memory Size: 256 MB Max Memory Used: 48 MB
# {"statusCode":200,"headers":{...},"body":"{\"message\":\"Hello, Daichi!\",\"timestamp\":\"2024-01-15T12:00:00+09:00\",\"function\":\"HelloWorldFunction\"}"}# ローカルでAPIサーバーを起動
sam local start-api --port 3001
# 別ターミナルから
curl "http://localhost:3001/hello?name=Daichi"
# {"message":"Hello, Daichi!","timestamp":"...","function":"HelloWorldFunction"}INFO
sam local はDockerを使ってLambdaの実行環境をエミュレートする。本番と同じRubyランタイムで動作確認できる。Dockerが必要なので事前にインストールしておくこと。
AWS へのデプロイ
ローカルで動作確認ができたら、本番にデプロイする。
# ビルド(依存Gemのインストール等)
sam build
# 初回デプロイ(ガイド付き)
sam deploy --guided
# Configuring SAM deploy
# =========================================
# Stack Name [sam-app]: serverless-ec-api
# AWS Region [us-east-1]: ap-northeast-1
# Confirm changes before deploy [y/N]: y
# Allow SAM CLI IAM role creation [Y/n]: Y
# Save arguments to configuration file [Y/n]: Y
# SAM configuration file [samconfig.toml]: [Enter]
# Deploying with following values
# ===============================
# Stack name : serverless-ec-api
# Region : ap-northeast-1
# Confirm changeset : True
# ...
# CloudFormation stack changeset
# ---------------------------------
# Operation LogicalResourceId Type
# --------- ---------------------- ----------------
# + Add HelloWorldFunction AWS::Lambda::Function
# + Add HelloWorldFunctionRole AWS::IAM::Role
# + Add ServerlessRestApi AWS::ApiGateway::RestApiデプロイが完了すると、API GatewayのエンドポイントURLが表示される。
# Outputs
# -------
# Key: HelloWorldApi
# Value: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
# 本番エンドポイントへのリクエスト
curl "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello?name=Daichi"
# {"message":"Hello, Daichi!","timestamp":"2024-01-15T12:05:30+09:00","function":"serverless-ec-api-HelloWorldFunction-XXXX"}CloudWatch Logsでログを確認する
Lambdaの実行ログはCloudWatch Logsに自動的に保存される。
# SAM CLIでログをリアルタイム確認
sam logs -n HelloWorldFunction --stack-name serverless-ec-api --tail
# 特定の時間範囲のログを取得
sam logs -n HelloWorldFunction \
--stack-name serverless-ec-api \
--start-time "2024-01-15T12:00:00" \
--end-time "2024-01-15T13:00:00"ログには以下のような情報が含まれる。
START RequestId: a1b2c3d4-e5f6-... Version: $LATEST
Received GET request to /hello
Context: function_name=serverless-ec-api-HelloWorldFunction-XXXX, remaining_time=29998ms
END RequestId: a1b2c3d4-e5f6-...
REPORT RequestId: a1b2c3d4-e5f6-... Duration: 45.23 ms Billed Duration: 46 ms Memory Size: 256 MB Max Memory Used: 48 MB
REPORT 行の Duration が実際の実行時間、Billed Duration が課金対象時間(1ms単位で切り上げ)だ。
実用的なRubyのLambda構造
Hello World を超えて、実際のビジネスロジックに使える構造を作ってみよう。
# app.rb — より実用的な構造
# frozen_string_literal: true
require 'json'
require 'logger'
# ロガーの設定(Lambda環境ではSTDOUTがCloudWatch Logsに転送される)
$logger = Logger.new($stdout)
$logger.level = ENV['LOG_LEVEL'] == 'DEBUG' ? Logger::DEBUG : Logger::INFO
# ハンドラ(Lambda のエントリーポイント)
def lambda_handler(event:, context:)
$logger.info("Request received: #{event['httpMethod']} #{event['path']}")
handler = resolve_handler(event)
handler.call(event, context)
rescue StandardError => e
$logger.error("Unhandled error: #{e.class} - #{e.message}")
$logger.error(e.backtrace.first(5).join("\n"))
error_response(500, 'Internal Server Error')
end
private
def resolve_handler(event)
method = event['httpMethod']
path = event['path']
case [method, path]
when ['GET', '/hello'] then method(:handle_hello)
when ['GET', '/health'] then method(:handle_health)
else method(:handle_not_found)
end
end
def handle_hello(event, _context)
name = (event['queryStringParameters'] || {})['name'] || 'World'
success_response({ message: "Hello, #{name}!" })
end
def handle_health(_event, _context)
success_response({ status: 'ok', timestamp: Time.now.iso8601 })
end
def handle_not_found(_event, _context)
error_response(404, 'Not Found')
end
def success_response(body)
json_response(200, body)
end
def error_response(status, message)
json_response(status, { error: message })
end
def json_response(status, body)
{
statusCode: status,
headers: { 'Content-Type' => 'application/json' },
body: JSON.generate(body)
}
endデプロイのベストプラクティス
# RSpec によるユニットテスト
# spec/unit/test_handler.rb
require 'json'
require_relative '../../hello_world/app'
RSpec.describe '#lambda_handler' do
let(:event) do
{
'httpMethod' => 'GET',
'path' => '/hello',
'queryStringParameters' => { 'name' => 'Daichi' }
}
end
let(:context) do
double('context',
function_name: 'test-function',
get_remaining_time_in_millis: 30000
)
end
it 'returns 200 with greeting message' do
result = lambda_handler(event: event, context: context)
expect(result[:statusCode]).to eq(200)
body = JSON.parse(result[:body])
expect(body['message']).to eq('Hello, Daichi!')
end
end# テスト実行
cd hello_world
bundle exec rspecWARNING
Lambda では $LOAD_PATH やグローバル変数がウォームスタート時に再利用される。データベース接続やHTTPクライアントなどの初期化はハンドラ関数の外(モジュールレベル)に置くと効率的だが、その分リソースが関数コンテナ間で共有されることを意識すること。
環境変数の管理
Railsの config/credentials.yml.enc に相当するのが、LambdaではAWS Secrets ManagerやSystems Manager Parameter Storeだ。
# template.yaml に環境変数を追加
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
Environment:
Variables:
DATABASE_URL: !Sub '{{resolve:ssm:/myapp/production/database_url}}'
LOG_LEVEL: INFO# コード内での使用
database_url = ENV['DATABASE_URL']
log_level = ENV.fetch('LOG_LEVEL', 'INFO')ダイチの感想
最初のLambda関数のデプロイを終え、ダイチはSlackに投稿した。
今日やったこと:
✅ SAMプロジェクト作成
✅ ローカルでHello World動作確認
✅ AWSへのデプロイ成功
✅ CloudWatch Logsでログ確認
感想:
デプロイが `sam deploy` 一発で済むのが驚き。
EC2の時はCapistranoで色々設定してたのに...
次: API Gateway + Lambda でREST APIを作る
サーバーの設定ファイルを書く必要もなく、sam deploy 一つで動く状態になった。ダイチは画像リサイズ処理の移行に向けて、確実に前進していた。