ENECHANGE Developer Blog

ENECHANGE開発者ブログ

Cursorを使ってServerless FrameworkからAWS SAMへの移行をしてみた

こんにちは。Energy Data Dev1チームのマネージャーの宮尾です。

社内のLambdaの運用基盤を「Serverless Framework」から「AWS SAM(Serverless Application Model)」へ移行する作業を担当しました。 Lambdaではメール送信や電力使用量ファイルの変換処理を行っています。 本記事では、その移行過程とAIエージェント「Cursor」を利用した際の所感を記載します。

移行理由

これまでServerless Framework v3を利用していましたが、v4から有料プランが必須となるため、無償で利用できるAWS公式のSAMへ移行することにしました。 v3を使い続ける選択肢もありましたが、アップデートがないことによるセキュリティ面での不安やコスト面の理由が主です。

移行作業の流れ

1. 既存Serverless Frameworkプロジェクトの確認

まず、既存のserverless.ymlをCursorに読み込ませ、SAMテンプレートへの変換方針を検討しました。主要なリソース(Lambda、SQS、DynamoDBなど)はCursorの支援で抽出し、SAM用のtemplate.yamlの雛形を作成しました。

2. Python依存パッケージの管理

AWS SAMではpoetryを直接利用できないため、poetry exportコマンドでrequirements.txtを生成し、そのファイルをLayerとしてデプロイに利用しました。これにより、Serverless Framework時代のpoetryベースの開発体験を維持しつつ、SAMのLayer機能で依存パッケージを管理できました。 AIエージェントとのやり取りはだいたい10往復くらいで動くものが完成しました。 プロンプトとしては、「poetryを使っているのですが、それを使うようにすることはできますか?」とか、「このエラーはなんですか?」等のだいぶざっくりした質問でした。 エラーをコピペして尋ねることが大多数だったかと思います。

deploy:
 mkdir -p layer/
 poetry export -f requirements.txt --output layer/requirements.txt --without-hashes

このようにして、poetryの利便性とSAMの運用を両立しています。

3. デプロイ・テストの自動化

デプロイやテストの自動化には、GitHub ActionsではなくMakefileを利用しました。CursorにMakefileの書き方を相談しながら、下記のような内容で運用しています。 AWS SAMではcacheファイルやディレクトリを除外してデプロイする機能はないため、Makefileではそれらを削除するようにしています。 こちらはそんなに大層なことをやっていなかったのと、ビルド方法はもう提示してくれていたので、数回のやりとりで完成させられました。

STAGE ?= test-stg


deploy:
 mkdir -p layer/
 poetry export -f requirements.txt --output layer/requirements.txt --without-hashes
 rm -rf app/__pycache__ .aws-sam
 poetry run sam build
 poetry run sam deploy --parameter-overrides Stage=$(STAGE)


test:
 PYTHONPATH=app poetry run pytest tests/test_dr_mail_by_ses.py

4. 環境変数の管理方法の違い

各クライアントごとにLambdaを新規で作成する際、環境変数の管理方法に苦労しました。Serverless Frameworkでは、XXX-stg.yamlXXX-prod.yamlで各ステージやクライアントごとにYAMLファイルを分けて環境変数を読み込むことができ、柔軟な管理が可能でした。

一方、AWS SAMではデプロイ時に外部ファイルから環境変数を直接読み込む仕組みがなく、Serverless Frameworkのようなファイル分割運用ができません。そのため、samconfig.tomlで各ステージごとにparameter_overridesを設定し、パラメータを切り替える運用にしました。 こちらもやりとりが多く、大体20往復くらいは会話しました。 ひたすらプロンプトにエラーを貼り付ける作業を繰り返しておりました。

[test-stg.deploy.parameters]
stack_name = "ses-sendmail-test-stg"
s3_bucket = "aws-sam-assets"
s3_prefix = "ses-sendmail-test-stg"
parameter_overrides = "Stage=test-stg"
confirm_changeset = false

このように、samconfig.tomlでステージごとにパラメータを切り替えることで、環境ごとの設定を管理しています。Serverless Frameworkのような柔軟さはありませんが、SAMの標準的な運用方法として落ち着きました。

注意点・所感

  • Serverless Framework特有のカスタムリソースやプラグインは自動変換が難しく、人手による確認・修正が必要でした。
  • Cursorの提案内容は必ず公式ドキュメントと突き合わせて検証する必要があります。間違いを平気で提案してくるので、最終的に頼りになるのはやはり公式ドキュメントです。
  • 雛形作成や調査の効率は向上しましたが、最終的な品質担保は人間が行う必要があります。
  • プロンプトの質が悪いと、AIエージェントの提案が間違っていることが多かったかなという印象でした。具体的な指示を出すことの大切さを感じました。

まとめ

AIエージェント(Cursor)を活用することで、調査や雛形作成の効率は上がりましたが、細かい調整や最終的な判断は人間が担う必要があると感じました。コスト面の理由で移行を検討している場合、SAMは有力な選択肢となります。


参考:最終的にできたtemplate.yaml(一例)

以下は、クライアント「test-stg」用に整理した最終的なtemplate.yamlの例です。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: DR Send Mail Lambda Functions (test-stg)


Globals:
  Function:
    Timeout: 15
    MemorySize: 256
    Runtime: python3.13
    Architectures:
      - arm64


Parameters:
  Stage:
    Type: String
    Default: test-stg
    Description: Deployment stage
    AllowedValues:
      - test-stg
  SESIdentityResource:
    Type: String
    Default: "arn:aws:ses:ap-northeast-1:123456789012:identity/test.example.com"
    Description: SES identity resource ARN


Resources:
  PythonRequirementsLambdaLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: !Sub "dr-ses-sendmail-libs-${Stage}"
      Description: "Python libs: boto3, jinja, sendgrid and more"
      ContentUri: layer/
      CompatibleRuntimes:
        - python3.13
      CompatibleArchitectures:
        - arm64
      RetentionPolicy: Retain
    Metadata:
      BuildMethod: python3.13
      BuildArchitecture: arm64


  SendDrMailBySES:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub "dr-ses-sendmail-${Stage}"
      CodeUri: app/
      Handler: send_dr_mail_by_ses.send_mail
      Description: !Sub "send DR mail by SES (${Stage})"
      Layers:
        - !Ref PythonRequirementsLambdaLayer
      Environment:
        Variables:
          STAGE: !Ref Stage
          COMPANY: test_company
          ENVIRONMENT: staging
          DYNAMO_DB_TABLE: test_stg_dr_ses_sendmail
          SES_CONFIGURATION_SET: test-stg-configuration-set
          SES_NO_TRACK: yes
          SES_RETRY_COUNT: 5
          FROM_ADDRESS: test@example.com
      Events:
        SQSEvent:
          Type: SQS
          Properties:
            Queue: "arn:aws:sqs:ap-northeast-1:123456789012:test_stg_dr_ses_sendmail.fifo"
            BatchSize: 1
      Policies:
        - Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Action:
                - ses:SendEmail
                - ses:SendRawEmail
              Resource: !Ref SESIdentityResource
            - Effect: Allow
              Action:
                - dynamodb:GetItem
                - dynamodb:Query
                - dynamodb:DeleteItem
                - dynamodb:PutItem
              Resource: "arn:aws:dynamodb:ap-northeast-1:123456789012:table/test_stg_dr_ses_sendmail"
      Tags:
        Service: test


Mappings:
  StageConfig:
    test-stg:
      Company: test_company
      Environment: staging
      DynamoDBTable: test_stg_dr_ses_sendmail
      DynamoDBTableArn: "arn:aws:dynamodb:ap-northeast-1:123456789012:table/test_stg_dr_ses_sendmail"
      SESConfigurationSet: test-stg-configuration-set
      SESNoTrack: yes
      SESRetryCount: 5
      FromAddress: test@example.com
      SESQueueArn: "arn:aws:sqs:ap-northeast-1:123456789012:test_stg_dr_ses_sendmail.fifo"


Outputs:
  PythonRequirementsLambdaLayer:
    Description: Python requirements Lambda layer
    Value: !Ref PythonRequirementsLambdaLayer

参考:最終的にできたsamconfig.toml(一例)

以下は、クライアント「test-stg」用に整理したsamconfig.tomlの例です。

version = 0.1


[test-stg.deploy.parameters]
stack_name = "ses-sendmail-test-stg"
s3_bucket = "aws-sam-assets"
s3_prefix = "ses-sendmail-test-stg"
parameter_overrides = "Stage=test-stg"
confirm_changeset = false