AWS SAM × Ruby で効率よくLambdaを開発するための5つのTips
こんにちは。最近エレクトリックアップライトベースの練習をしている小木です。
クラッソーネでは、社内の各種ワークフローを自動化することで、より効率的に業務を行えるよう取り組んでいます。
これまでは Make というノーコードツール を利用して Slack 通知や Notion からのデータ取得、Google スプレッドシートへの書き込みなどを自動化していました。
Make 利用に関する過去のブログ記事 ↓
しかし、Make のシナリオ(Make で定義したワークフロー)が増えたり、人の退職や時間の経過によって仕様がわからなくなったりと、メンテナンスコストがかさんでしまいました。
そこで、Make で実装された各種ワークフローを AWS Lambda に置き換えていくプロジェクトが始動しました。
Lambda の実装には AWS SAM(Serverless Application Model) を使用しています。
AWS SAM は、ローカル環境で Lambda ファンクションを実行できたり、そのまま AWS 環境にデプロイできたりと、とても便利です。
ただ、「よし、これからガンガン開発していくぞ!」というテンションで使うと、少し面倒に感じる部分もありました。
具体的には、
- sam local invoke する前に sam build しないとコードの変更が反映されない(これは lambda ファンクションの実装に Ruby を使っているので面倒くささを感じるのかもしれない)
- sam local invoke が遅い(Docker のコンテナ上で実行されるので仕方のない面もある・・・)
ということで、今回はオレの考えた Lambda ファンクション開発の Tipsをいくつか紹介します。紹介するコード例は Ruby で書いていますが、言語に依存しない考え方なので応用できるはずです。
1. lambda_handler と ビジネスロジック(処理の本体) を分離する
sam init コマンドでテンプレートから Lambda ファンクションを作成すると、以下のようなコードが生成されると思います。
tree -L 2
.
├── README.md
├── business_logic
│ ├── Gemfile
│ ├── Gemfile.lock
│ └── app.rb
└── template.yaml
app.rb
def lambda_handler(event:, context:)
{
statusCode: 200,
body: {}
}.to_json
end
ここで、 lambda_handler 内にいきなりビジネスロジックを実装しないほうがいいです。デバッグやテストがしづらいからですね。
ビジネスロジックはクラス化して lambda_handler
と分離します。
app.rb
# このクラスにビジネスロジックを実装する
class BusinessLogic
def execute
end
end
def lambda_handler(event:, context:)
business_logic = BusinessLogic.new
result = business_logic.execute
{
statusCode: 200,
body: {
result:
}.to_json
}
end
2. ビジネスロジッククラスを単独で実行できるようにする
1 でビジネスロジックをクラスに分けました。これを Lambda 経由でなくローカルで実行できるようにします。
app.rb
class BusinessLogic
def execute
end
end
# このファイル(app.rb)が直接実行されたときだけ処理を実行
if __FILE__ == $0
business_logic = BusinessLogic.new
result = business_logic.execute
puts result
end
if __FILE__ == $0
はそのファイルが直接実行されたときだけブロック内のコードが実行される、という条件文です(他の言語にも同様のテクニックはあるんじゃないかなぁと)
こうすることで以下のように SAM CLI を使わずにプロセスのコードの動作検証ができる、というわけです。
cd business_logic
bundle exec ruby app.rb
このように、
- ビジネスロジックを Lambda を経由せずに実装、ユニットテスト
sam local invoke
で Lambda 環境上でのテスト- デプロイ
- AWS Management Console 上、またはステージング環境に Lambda ファンクションを結合してテスト
のように開発サイクルを回すのが効率良いのではと思っています。
3. 環境変数で実行環境を分ける
開発用だったり、問題が起きた時のデバッグ用だったりに使うため、デプロイする Lambda ファンクションは本番用と検証用の少なくとも 2 つ用意するのが良いと思います。
(本番用と検証用それぞれのデプロイには SAM CLI の --config-env
オプションを利用します)
template.yaml
Parameters:
AppEnv:
Type: String
Default: dev
AllowedValues: [dev, stg, prd]
Description: 環境名を指定してください。dev, stg, prdのいずれかを指定してください。
Resources:
BusinessLogicFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub ${AppEnv}-BusinessLogicFunction
Description: !Sub ${AppEnv}, BusinessLogicFunction
CodeUri: business_logic/
Handler: app.lambda_handler
Runtime: ruby3.4
Environment:
Variables:
APP_ENV: !Ref AppEnv
Lambda の環境変数に実行環境を表す変数を定義します。ここでは AppEnv
という名前で定義しています。
コード内では以下のように利用できます。
app.rb
class BusinessLogic
def execute
if app_env == 'stg'
puts "Running in staging environment"
elsif app_env == 'prd'
puts "Running in production environment"
else
puts "Running in development environment"
end
end
private
def app_env
ENV['APP_ENV'] || 'dev'
end
end
実行環境を示す環境変数 APP_ENV
によってコード内で処理を分岐することができるようになりました。これだけだと当たり前では?みたいなコードですが、APP_ENV
を定義したのは次でやりたいことがあるからです。
4. ビジネスロジックで利用するシークレット値は Lambda の環境変数ではなく AWS Secrets Manager から取得する
これまでは AppEnv と同様に環境変数でシークレットを渡していました。これはこれで安全なのですが、シークレットの値が増えるとデプロイコマンドが煩雑になったりタイプミスしやすかったり、シークレットの値を毎回コピペしないといけなかったりと手間がかかっていました。
例えば外部の API を複数コールするような Lambda ファンクションの場合こんな感じでした。
sam deploy --config-env staging \
--parameter-overrides AppEnv="stg" \
ApiKey="" \
NotionToken="" \
NotionProjectDatabaseId="" \
SlackBotToken="" \
SlackSigningSecret="" \
SlackListenChannelId=""
そこで、ビジネスロジック側で利用するシークレットは Secrets Manager に登録し、ビジネスロジッククラスの初期化時に環境変数としてロードするようにしました。
SecretsManager
stg/my_secret
{
"API_KEY": "staging環境用のAPIキー",
"NOTION_TOKEN": "staging環境用のトークン"
//...
}
require 'aws-sdk-secretsmanager'
def get_secret(secret_name)
client = Aws::SecretsManager::Client.new(region: 'ap-northeast-1')
response = client.get_secret_value(secret_id: secret_name)
JSON.parse(response.secret_string)
end
class BusinessLogic
def initialize
load_secrets
end
private
def load_secrets
secrets = get_secret("#{app_env}/my_secret")
# 環境変数にセットする
secrets.each do |key, value|
ENV[key] = value
end
end
end
外部 API のキーも本番用・検証用に分かれているので、先ほど設定した APP_ENV
の値でそれぞれの環境用のシークレットを Secrets Manager から取得する感じです。
こうすることで、
- Lambda ファンクションの挙動に関する設定は Lambda の環境変数
- ビジネスロジックで利用する秘匿情報は Secrets Manager
と差別化でき、管理がしやすくなりました。
5. AWS プロファイルは SAM CLI のオプションで指定する
さて、上のサンプルコードで Aws::SecretsManager::Client
を使っていますが、AWS にアクセスするために AWS プロファイルの設定が必要です。
Lambda 上でコードが実行されるときは AWS のプロファイル情報は自動的に割り当てられますが、
stg / prd 環境、開発環境でどのプロファイルが読み込まれるのかは意識しておく必要があります。
私は以前このような実装にしていました。
client = if ENV['APP_ENV'] == 'dev'
credentials = Aws::Credentials.new(
ENV['AWS_ACCESS_KEY_ID'],
ENV['AWS_SECRET_ACCESS_KEY']
)
Aws::SecretsManager::Client.new(
region: 'ap-northeast-1',
credentials:
)
else
Aws::SecretsManager::Client.new(
region: 'ap-northeast-1'
)
end
これだと、 APP_ENV
が適切に設定されていないと AWS のリソースにアクセスできなかったり、最悪の場合違う AWS 環境のリソースを利用してしまったりと危うい実装でした。
AWS SDK も SAM CLI も ~/.aws/credentials
に設定されているデフォルトプロファイルを暗黙的に読んでくれるので、コード中でキーをわざわざ設定する必要はありません。
アクセスキーではなく、プロファイルを指定しましょう!
別のプロファイルを利用したい場合は sam
コマンドの --profile
パラメータで利用するプロファイルを指定できます。
# どちらの方法でもok
AWS_PROFILE=project-a-user sam local invoke
sam local invoke --profile project-a-user
2 で紹介したビジネスロジッククラスのコードを単体で実行する場合にも環境変数として AWS_PROFILE
を指定してあげればコード内で指定したプロファイルが利用されます。
AWS_PROFILE=project-a-user bundle exec ruby app.rb
【追記】
スミマセン、あとで色々検証したところ
client = Aws::SecretsManager::Client.new;
みたいに認証情報を aws sdk でデフォルトで読み込む値を利用するような実装では、
sam local invoke
で Lambda ファンクションを実行したときに認証情報が正しく読み込まれませんでした。
例えば以下のようなデバッグコードを入れて
logger.debug "=== Environment Variables ==="
logger.debug "APP_ENV: #{ENV['APP_ENV']}"
logger.debug "AWS_PROFILE: #{ENV['AWS_PROFILE']}"
logger.debug "AWS_ACCESS_KEY_ID: #{ENV['AWS_ACCESS_KEY_ID']}"
ローカルで実行してみると、
```ruby
AWS_PROFILE=sample-user sam local invoke
こんな出力になります。
D, [2025-05-30T05:25:18.276124 #19] DEBUG -- : === Environment Variables ===
D, [2025-05-30T05:25:18.276200 #19] DEBUG -- : APP_ENV: dev
D, [2025-05-30T05:25:18.276231 #19] DEBUG -- : AWS_PROFILE:
D, [2025-05-30T05:25:18.276255 #19] DEBUG -- : AWS_ACCESS_KEY_ID: "~/.aws/credentialsに登録されているsample-userのアクセスキー"
これちょっと不思議な挙動なんですけど、
Lambda 側の環境変数には AWS_PROFILE
が渡っていないのに、ローカルの ~/.aws/credentials に定義しているプロファイルの AWS_ACCESS_KEY_ID
はちゃんと渡っているんですよねー。謎。
(※ あえて書いていませんが、 AWS_SECRET_ACCESS_KEY
も渡ってきています)
なんというか不思議な挙動です。
けっきょく、 sam local invoke
で認証情報を利用するために以下のようなコードを書いています。
client = if ENV['APP_ENV'] == 'dev'
Aws::SecretsManager::Client.new(
region: 'ap-northeast-1',
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
)
else
Aws::SecretsManager::Client.new
end
dev
というのはローカル(= sam local
) で利用するときアクセスキーを利用する分岐に入るために設定した環境変数です。
まとめ
ということで、AWS SAM を使って Lambda ファンクションを開発するときの個人的 Tips を紹介しました。
SAM CLI には sam sync
コマンド(コードの変更を検知して差分を Lambda にデプロイしてくれる。 sam build / sam deploy を手動で実行する必要がなくなり、高速に開発を進めることができる)もありますが、デプロイに多少なりとも時間がかかるので私はほとんど使っていません。デプロイ済みの Lambda ファンクションをデバッグするのには便利かな。