Serverless Framework から AWS SAM に移行した話
こんにちは!アルダグラムでエンジニアをしている秋田です
弊社のサービスではLambdaを使用していますが、その一部に Node.js 16 のランタイムで稼働しているものがありました。Node.js 16 ランタイムのサポート期限に対応するためにバージョンアップやフレームワーク変更を行ったので、今回はその内容を紹介したいと思います。(Lambdaランタイムのサポートについて)
対応方針
対応方針は次のようになりました。
Lambdaランタイム
ランタイムについては、そもそも実装言語をGo言語などに変更することも検討しました。ただ既存のソースコードを見たところ、Node.jsのバージョン変更にはそこまで影響を受けないように見えました。
ロジック部分にはなるべく変更を入れたくないということもあり、今回は Node.js 16 から Node.js 20 へランタイム変更の方針を取っています。
フレームワーク
これまでは Serverless Framework を使用していました。
便利なフレームワークではあるのですが、以下の点から廃止することにしました。
- Node.js 20 への対応が遅かった
- Serverless Framework V4 にてライセンス変更されている
- 現状の実装がそこまでフレームワークに依存していない
そのため、今回の対応では AWS SAM を採用することにして、ランタイム変更に合わせてフレームワーク移行を実施しています。
既存実装の課題解決
ランタイム以外にも、対応前には以下の課題がありました。
- ローカルで実行できない
- テストコードが書かれていない
- AWS SDK のバージョンが、現行のv3ではなくv2
ランタイムやフレームワークを変更するなら、ついでということでこれらの課題にも対応しました。
起きたことと対応策
進める中でいくつか問題があったので、内容と対策を書いていきます。
S3イベント通知がSAMで設定できない
一番問題だったことがこれでした。正確には「既存バケットに対してLambdaへのイベント通知を設定することが、SAMテンプレートではできない」ということです。
今回対象となったLambdaは、S3にアップロードされた画像ファイルの処理をするもので、そのトリガーとしてS3のイベント通知を利用していました。SAMテンプレートでもその設定はできるのですが、テンプレート内で定義した新規のS3バケットに限られます。すでにサービスとして稼働しているものを置き換えることはできません。
Serverless Framework では、この問題はLambdaを使った Cloud Formation のカスタムリソースで解決しているようです。
同様にカスタムリソースを新規で実装すれば解決できますが、今回は通知設定をSAMテンプレートに定義せず、Terraformで管理することにしました。
※実際の構成とは異なりますが、イメージとしては以下のようになります
data "aws_s3_bucket" "main" {
bucket = var.s3_bucket # S3バケット名
}
resource "aws_s3_bucket_notification" "bucket_notification" {
bucket = data.aws_s3_bucket.main.bucket
lambda_function {
id = "sam-function"
lambda_function_arn = var.sam_lambda_arn # SAMデプロイしたLambdaのARN
events = ["s3:ObjectCreated:*"]
}
}
apply時には以下のようになり、update in-place で処理されます。
※一部のリソース値はマスキングしています
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# module.cf_static_images.aws_s3_bucket_notification.bucket_notification will be updated in-place
~ resource "aws_s3_bucket_notification" "bucket_notification" {
id = "bucket-name"
# (2 unchanged attributes hidden)
~ lambda_function {
~ id = "old-function" -> "sam-function"
~ lambda_function_arn = "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:old-lambda" -> "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:sam-function-xxxxxxxxxxxx"
# (1 unchanged attribute hidden)
}
}
Plan: 0 to add, 1 to change, 0 to destroy.
この方法だと以下のメリット・デメリットがありますが、メリットのほうが大きいと判断しました。
- メリット
- 新規のインフラリソース管理にはTerraformを使用しており、参考情報がある
- SAMデプロイと通知切り替えを分離できるので、移行適用タイミングをコントロールしやすい
- カスタムリソースを実装しなくて済む
- デメリット
- 新規構築の場合、Terraformでのインフラ構築の前にSAMデプロイをしておく必要がある
- SAMの実装だけ見ても、何をトリガーとしているのがわかりにくい
対象S3バケットがTerraformで管理されていない
インフラはTerraformを使用していると書きましたが、現状では古いリソースについては Cloud Formation であったり、そもそもIaCになっていなかったりしています。今回の対象となるS3バケットも、Terraformでは管理されていないものでした。
これに関しては以下のようにimportすることで、問題なく管理できることが確認できました。
terraform import module.cf_static_images.aws_s3_bucket_notification.bucket_notification <S3バケット名>
ServerlessでremoveするとS3イベント通知設定も消える
これは「S3イベント通知がSAMで設定できない」にも関連していますが、稼働中のサービスで起きると困るものです。
Serverlessでデプロイしたリソースは、以下のコマンドで削除できます。
sls remove
しかし、そのまま実行するとカスタムリソースにより設定していたS3イベント通知設定も削除します。
Lambdaは作成し直しなので、その意味では一旦全部破棄したうえで再構築でも悪くないです。ただ今回はサービス停止したくないので、できればS3通知だけは削除対象外(新しい方へ置換)としたいところでした。
そこでServerlessのカスタムリソースのLambda実装を見たところ、対象イベント通知はLambda関数名との前方一致で判断しているようでした。一致しない通知に対しては特に何もしないため、Terraformで設定する通知名を 既存通知名と異なる名前にする ことで削除を回避できました。
Lambdaパーミッションをどこに書くか
こちらも「S3イベント通知がSAMで設定できない」にも関連していますが、 S3がLambdaを実行することのパーミッション設定はどこに書くか という問題です。
SAMテンプレートかTerraformの二択ですが、これはSAMテンプレートで管理することにしました。理由としては以下になります。
- Lambdaに関することはSAMで管理したい
- このパーミッション設定は AWS::Lambda::Permission タイプなので、Lambdaに属する
- 許可対象のS3のARNはバケット名から想像できるので、書くのが容易
以下、SAMテンプレートの抜粋イメージです。
Parameters:
BucketName:
Type: String
Resources:
SamFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: index.handler
Runtime: nodejs20.x
Architectures:
- x86_64
Metadata:
BuildMethod: esbuild
BuildProperties:
EntryPoints:
- index.ts
PermissionForS3:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref SamFunction
Action: lambda:InvokeFunction
Principal: s3.amazonaws.com
SourceAccount: !Ref 'AWS::AccountId'
SourceArn: !Sub "arn:aws:s3:::${BucketName}"
GitHub ActionsでのSAMデプロイが遅い
すごく遅いというわけではないですが、ちょっと遅い。
GitHub Actions でデプロイできるようにワークフローを作成したのですが、思ったより時間がかかっていました。ログを確認したところ、 aws-actions/setup-sam@v2 で時間がかかっているようです。
AWS公式ドキュメントの例では特にパラメータ指定もなく、そのまま setup-sam を使っていたのでこれを参考にしていました。しかし何とか解決できないかと確認したところ、GitHubリポジトリのほうに記載がありました。
user-installer
Linux x86-64 のみの対応ですが、これをtrueにするとPythonではなくネイティブのインストーラーを使用するようになるとのこと。設定したらかなり時間が短くなりました。条件が合うならtrue設定をおすすめします。
- name: Setup SAM
uses: aws-actions/setup-sam@v2
with:
use-installer: true
なお、この設定で Setup SAM の実行時間は 33秒 → 4秒 に短縮しました。
AWSリソースに依存するテストコードをどうするか
元々テストコードがなかったので追加することにしました。
移行前は AWS SDK がv2であったため難しかったですが、 今回v3へバージョンアップすることにより aws-sdk-client-mock が使えるようになりました。
jestでテストを実施する場合は、以下のようにS3関連のコマンドをモックすることでテスト可能にしています。
s3Mock
.on(GetObjectCommand, {
Bucket: 'test-bucket',
Key: 'test-dir/test.jpg',
})
.resolves({
ContentType: 'image/jpeg',
Body: sdkStreamMixin(fs.createReadStream(path.resolve(__dirname, '../test_images/test_original.jpg'))),
})
.on(GetObjectTaggingCommand)
.resolves({
TagSet: [],
})
.on(PutObjectCommand)
.resolves({});
この例では test_original.jpg という画像ファイルをあらかじめ用意しておき、それをstreamとしてBodyに渡すことでS3:GetCommandの実行結果を解決しています。また、タグ取得やS3へのアップロードについてもエラーにならないように、中身の指定はないですがモックしています。
最後に
今回の作業の着手時点では Serverless Framework の知見もなく、SAMや GitHub Actions にもそこまで詳しくなかったのですが、調査しながら進めることで様々な知見が得られました。今後 Serverless を使用することがあるのかは定かではないですが、作業を通してSAMの制約や今回書ききれていない細かな部分のような、周辺知識も得られたのが良かったと思います。
Serverlessを使用中または検討されている方、SAMとS3の連携を検討されている方などの参考になれば幸いです。
もっとアルダグラムエンジニア組織を知りたい人、ぜひ下記の情報をチェックしてみてください!
株式会社アルダグラムのTech Blogです。 世界中のノンデスクワーク業界における現場の生産性アップを実現する現場DXサービス「KANNA」を開発しています。 採用情報はこちら herp.careers/v1/aldagram0508/
Discussion
この問題は自分も悩まされました。。。
S3をSAMテンプレートで管理することは解決できなかったのですが、S3のイベントをLambdaに直接設定するのではなく、S3の通知イベントをEventBridgeを挟むことで、S3のイベント通知をSAMテンプレートで管理するようにしました。
今回の対応では都合上こちらを採用しませんでしたが、管理をSAMに寄せる場合はこちらのほうが良さそうだと思っていました。
自分ではそこまで検証しなかったのですが、SAMテンプレートで管理されているということなので、ちゃんとできるのですね。
情報ありがとうございます!