🚛

Serverless Framework から AWS SAM に移行した話

2024/08/01に公開2

こんにちは!アルダグラムでエンジニアをしている秋田です

弊社のサービスではLambdaを使用していますが、その一部に Node.js 16 のランタイムで稼働しているものがありました。Node.js 16 ランタイムのサポート期限に対応するためにバージョンアップやフレームワーク変更を行ったので、今回はその内容を紹介したいと思います。(Lambdaランタイムのサポートについて)

対応方針

対応方針は次のようになりました。

Lambdaランタイム

ランタイムについては、そもそも実装言語をGo言語などに変更することも検討しました。ただ既存のソースコードを見たところ、Node.jsのバージョン変更にはそこまで影響を受けないように見えました。

ロジック部分にはなるべく変更を入れたくないということもあり、今回は Node.js 16 から Node.js 20 へランタイム変更の方針を取っています。

フレームワーク

これまでは Serverless Framework を使用していました。

https://www.serverless.com/

便利なフレームワークではあるのですが、以下の点から廃止することにしました。

  • Node.js 20 への対応が遅かった
  • Serverless Framework V4 にてライセンス変更されている
  • 現状の実装がそこまでフレームワークに依存していない

そのため、今回の対応では AWS SAM を採用することにして、ランタイム変更に合わせてフレームワーク移行を実施しています。

既存実装の課題解決

ランタイム以外にも、対応前には以下の課題がありました。

  • ローカルで実行できない
  • テストコードが書かれていない
  • AWS SDK のバージョンが、現行のv3ではなくv2

ランタイムやフレームワークを変更するなら、ついでということでこれらの課題にも対応しました。

起きたことと対応策

進める中でいくつか問題があったので、内容と対策を書いていきます。

S3イベント通知がSAMで設定できない

一番問題だったことがこれでした。正確には「既存バケットに対してLambdaへのイベント通知を設定することが、SAMテンプレートではできない」ということです。

今回対象となったLambdaは、S3にアップロードされた画像ファイルの処理をするもので、そのトリガーとしてS3のイベント通知を利用していました。SAMテンプレートでもその設定はできるのですが、テンプレート内で定義した新規のS3バケットに限られます。すでにサービスとして稼働しているものを置き換えることはできません。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-property-function-s3.html

Serverless Framework では、この問題はLambdaを使った Cloud Formation のカスタムリソースで解決しているようです。

https://www.serverless.com/framework/docs/providers/aws/events/s3#using-existing-buckets

同様にカスタムリソースを新規で実装すれば解決できますが、今回は通知設定を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 で時間がかかっているようです。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/deploying-using-github.html

AWS公式ドキュメントの例では特にパラメータ指定もなく、そのまま setup-sam を使っていたのでこれを参考にしていました。しかし何とか解決できないかと確認したところ、GitHubリポジトリのほうに記載がありました。

https://github.com/aws-actions/setup-sam#use-installer

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 が使えるようになりました。

https://www.npmjs.com/package/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

Discussion

iwonderiwonder

既存バケットに対してLambdaへのイベント通知を設定することが、SAMテンプレートではできない

この問題は自分も悩まされました。。。
S3をSAMテンプレートで管理することは解決できなかったのですが、S3のイベントをLambdaに直接設定するのではなく、S3の通知イベントをEventBridgeを挟むことで、S3のイベント通知をSAMテンプレートで管理するようにしました。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-events-rule.html

akitaakita

S3の通知イベントをEventBridgeを挟む

今回の対応では都合上こちらを採用しませんでしたが、管理をSAMに寄せる場合はこちらのほうが良さそうだと思っていました。
自分ではそこまで検証しなかったのですが、SAMテンプレートで管理されているということなので、ちゃんとできるのですね。
情報ありがとうございます!