【AWS】 Serverless Framework から AWS SAM への移行を検討してみた

# はじめに
bravesoft株式会社でエンジニアをしているはっしーです。
最近携わっている受託の案件でLambdaをよく触っているのですが、Serverless Framework(v3)で構築されています。

・2024年度末でv3のサポートが終了するためクリティカルなセキュリティ問題やバグにしか対応されないためセキュリティーリスクが上がってくるのと、いずれかはAWS Lambdaのランタイムサポートが切れるため、v3からv4へのバージョンアップが必要になるかと思います。

https://www.serverless.com/framework/docs/guides/upgrading-v4

・v4から年間売上が200万ドル以上の企業は有償になるためライセンスの問題が発生する

:::note warn
前年度に年間収益が200万ドルを超えた組織がv4を利用する場合に対象となります。ただし、200万ドル以下だったかどうかの審査はないため自己申告のようです
:::

https://www.serverless.com/pricing

などの問題があるため、この機会にAWS SAMへ移行しようと考えました。
今回はサンプルを実装しながらAWS SAMへ理解を深めてければと思います。

# AWS SAMとは？
AWS SAM（Serverless Application Model）は、AWSが提供するサーバーレスアプリケーションの開発、デプロイ、テストを簡素化するためのフレームワークです。

Lambda関数、API Gateway、DynamoDB、Step Functionsなどを1つのテンプレートで簡潔に定義し、サーバーレスアプリケーションを効率的に構築できます。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html


# Serverless Framework と AWS SAM の違いについて

| **特徴**                    | **Serverless Framework**                                     | **AWS SAM (Serverless Application Model)**                       |
|-----------------------------|--------------------------------------------------------------|------------------------------------------------------------------|
| **提供元**                   | オープンソースのフレームワーク、サードパーティ製のツール     | AWS公式のフレームワーク                                          |
| **サポートされるクラウド**   | 主にAWS、他にもAzureやGoogle Cloud、Kubernetes等          | 主にAWS（AWS LambdaやAPI Gateway等のサービスを中心にサポート）|
| **設定ファイル**             | `serverless.yml`                                            | `template.yaml`（AWS CloudFormationを拡張したYAMLファイル）      |
| **デプロイツール**           | `serverless` CLI（`sls deploy`）                            | `sam` CLI（`sam deploy`）                                         |
| **対応サービス**             | Lambda、API Gateway、DynamoDB、S3、SNS、SQS、その他のAWSサービスをサポート | Lambda、API Gateway、DynamoDB、S3、SNS、SQSなどAWSの各種サービス |
| **デプロイの簡便さ**         | 比較的簡単で、AWS以外のクラウドにも対応                     | AWSに特化しているため、AWSでのデプロイが簡単                    |
| **ローカル開発サポート**     | `serverless-offline` プラグインでAPI GatewayとLambdaのローカル実行が可能  | `sam local` コマンドを使ってLambdaやAPI Gatewayのローカル実行が可能 |
| **テンプレートの可用性**     | 独自のプラグインやテンプレートが豊富                        | 主にAWSサービスに特化した標準テンプレートを提供                 |
| **拡張性**                   | プラグインを利用して柔軟に拡張可能                         | AWSの設定に特化しているが、CloudFormationとの親和性が高い      |
| **モニタリングとトレーシング** | AWS CloudWatchと統合、外部ツール（Datadog、NewRelic等）で拡張可能 | CloudWatchと連携したモニタリングやトレーシングが簡単に利用可能  |
| **学習コスト**               | 少し高い（AWS以外のクラウドサービスにも対応している分設定が多い） | AWSに特化しているため、AWSユーザーにとっては学習コストが低い    |
| **公式サポート**             | オープンソースであり、公式サポートはなし                   | AWSによる公式サポートが提供される                               |
| **インフラの管理**           | サーバーレスのリソース定義はYAMLで行い、CloudFormationを裏で利用 | 完全にCloudFormationを利用し、リソース管理やデプロイが行われる  |


# 環境情報
今回サンプルでの確認した環境は以下のとおりです。

| 項目                | バージョン       　　| 備考                                        |
| ------------------- | ------------------ | --------------------------------------------------------------------------------------- |
| Mac                 | 14.5                 | macOSのバージョン                            |
| AWS SAM             | 1.126.0             | AWS SAM（Serverless Application Model）のインストール方法については[公式ガイド](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started.html)を参照 |
| Node                | 20.19.0              | Node.jsのバージョン                        |
| Python              | 3.9                  | Pythonのバージョン                                                                         |
| Docker Desktop | 任意 |　|

# AWS SAMを触ってみる
AWS Lambdaを使用してポケモン情報を取得するLambdaとAPIを構築してみました。

## AWS SAM プロジェクトを作成する
1.homebrewでSAM CLIをインストールする

```
brew install aws-sam-cli
```

2.プロジェクトを初期化する
```
sam init
```

3.テンプレートを選択してサンプルSAMプロジェクトを作成する

```
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Data processing
        3 - Hello World Example with Powertools for AWS Lambda
        4 - Multi-step workflow
        5 - Scheduled task
        6 - Standalone function
        7 - Serverless API
        8 - Infrastructure event management
        9 - Lambda Response Streaming
        10 - Serverless Connector Hello World Example
        11 - Multi-step workflow with Connectors
        12 - GraphQLApi Hello World Example
        13 - Full Stack
        14 - Lambda EFS example
        15 - DynamoDB Example
        16 - Machine Learning
Template: 1

Use the most popular runtime and package type? (Python and zip) [y/N]: y

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: y
X-Ray will incur an additional cost. View https://aws.amazon.com/xray/pricing/ for more details

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: N

Would you like to set Structured Logging in JSON format on your Lambda functions?  [y/N]: y
Structured Logging in JSON format might incur an additional cost. View https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-pricing for more details

Project name [sam-app]: aws-sam-pokemon-test
```

4.Pokemonの情報を取得するスクリプトを書いてみる

https://pokeapi.co/

PokeAPIは簡単に言うとポケモンのデータを取得するAPIで、RESTfulAPIもしくはGraphQLの形式でデータを取得することができます。今回はpokeapiを利用してポケモンの情報を取得する簡単なLambdaを書いてみました

```get_pokemon_info.py
import json
import requests

def lambda_handler(event, context):
    pokemon_name = event['queryStringParameters']['name']

    if not pokemon_name:
        return {
            'statusCode': 400,
            'body': json.dumps({
                'message': 'ポケモンの名前をクエリパラメータで指定してください。'
            })
        }
    
    url = f'https://pokeapi.co/api/v2/pokemon/{pokemon_name.lower()}'

    try:
        response = requests.get(url)
        response.raise_for_status()
        pokemon_data = response.json()

        return {
            'statusCode': 200,
            'body': json.dumps({
                'name': pokemon_data['name'],
                'id': pokemon_data['id'],
                'types': [type_info['type']['name'] for type_info in pokemon_data['types']],
                'height': pokemon_data['height'],
                'weight': pokemon_data['weight']
            })
        }

    except requests.exceptions.RequestException as e:
        return {
            'statusCode': 500,
            'body': json.dumps({
                'message': f'ポケモン情報の取得中にエラーが発生しました: {str(e)}'
            })
        }

```

5.依存ライブラリのインストール（必要な場合）
requirements.txt を作成し、そのライブラリをインストールします。pokemon/ フォルダ内に requirements.txt ファイルを作成し、以下のように記述します。

```pokemon/requirements.txt
requests
```

6.ローカルでLambdaを実行する際のデータを生成する
ローカルで Lambda 関数をテストするためには、events/xxx.json ファイルが必要です。
このファイルには、Lambda 関数が受け取るイベントデータが含まれます。以下のような内容にしてください。

```events/get_pokemon.json
{
    "queryStringParameters": {
      "name": "pikachu"
    }
}
```

7.template.yamlを修正する

```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'

Description: >
  aws-sam-pokemon-test
  Sample SAM Template for aws-sam-pokemon-test

Globals:
  Function:
    Timeout: 3
    MemorySize: 128
    Tracing: Active
    LoggingConfig:
      LogFormat: JSON
  Api:
    TracingEnabled: true

Resources:
  GetPokemonFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: pokemon/
      Handler: get_pokemon_info.lambda_handler
      Runtime: python3.9
      MemorySize: 128
      Timeout: 10
      Events:
        ApiGateway:
          Type: Api
          Properties:
            Path: /pokemon
            Method: get

```

8.ビルドする
次のコマンドを実行するとビルドが実行されます。

``` 
sam build
```

![スクリーンショット 2024-12-17 1.16.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796792/cf231ccb-391a-3ab1-2464-797b45d254a0.png)

最終的なこんな感じの構成になるかと思います。

```ディレクトリ構造
AWS-SAM-Pokemon-test/
|── .aws-sam                     # ビルド済みアーティファクトや中間生成物が格納されるディレクトリ gitignoreに追加しましょう。
├── events/                      # ローカル実行時に使用するテストイベントを格納するディレクトリ
│   └── get_pokemon.json　　
├── pokemon                      # スクリプトの置き場
│    └── gget_pokemon_info.py    
│    └── requirements.txt        # 必要なPythonライブラリを記述するファイル
├── template.yaml                # SAMテンプレートファイルで、AWS のリソース（Lambda、API Gateway など）の設定を定義
├── samconfig.toml               # SAM CLIの設定ファイルで、デプロイ時の環境（開発環境や本番環境）の設定を記述
├── buildspec.yml                # AWS CodeBuildのビルド設定を記述するファイルです(今回は特に触れません)
```

## ローカルで実行する
### SAMでLambda関数のローカルテスト
作成したGetPokemonFunctionを実行する

```
sam local invoke GetPokemonFunction --event events/get_pokemon.json
```

### API Gatewayのエミュレートテスト
SAMではローカルでAPI Gatewayをエミュレートすることもできます。sam local start-apiコマンドを使ってローカルサーバーを立て、ブラウザやPostmanでAPIのエンドポイントを確認することできます。

1. Docker Desktopでdockerを起動しておく

1. localでsam local start-apiを実行すると自動でdocker containerが生成される
![スクリーンショット 2024-12-17 2.57.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796792/39630af7-8091-55eb-8223-ebe381f2df8f.png)

1. 念のため別のターミナルでdocker起動しているかを確認する
```
docker ps
```

![スクリーンショット 2024-12-17 3.00.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796792/da1d7f57-5372-44dc-065a-67e7c572861a.png)

#### curlの場合
```
curl "http://127.0.0.1:3000/pokemon?name=pikachu" 
```

#### Postmanの場合
```
GET:http://127.0.0.1:3000/pokemon?name=pikachu
```

![スクリーンショット 2024-12-17 2.46.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796792/57435c85-6f03-8de8-c977-d36d1b0281c3.png)

#### Apidogの場合
```
GET:http://127.0.0.1:3000/pokemon?name=pikachu
```

![スクリーンショット 2024-12-17 3.06.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796792/80700004-65ce-bfae-1fc0-74a454f8179b.png)

それぞれ問題なくポケモンの情報が取得できました

# serveress frameworkからAWS SAMに移行してみた
現状関わっている案件では、serveress frameworkで構築しているため、以下のサンプルを元にAWS SAMへの移行を試してみたいと思います。

例えばuser情報を取得するLambdaをserverless frameworkで構築していた場合

```function-get-user.py
import boto3
import uuid
from boto3.dynamodb.types import TypeSerializer
from boto3.dynamodb.conditions import Key
import simplejson as json
from os import environ

dynamodb = boto3.resource('dynamodb')
stage = environ['stage']

def query_table(table_name, key=None, value=None):
    table = dynamodb.Table(table_name)
    if key is not None and value is not None:
        filtering_exp = Key(key).eq(value)
        return table.query(KeyConditionExpression=filtering_exp)
    raise ValueError('Parameters missing or invalid')


def lambda_handler(event, context):
    serializer = TypeSerializer()

    response = query_table(
        table_name= f'user_{stage}',
        key="user_id",
        value=event['pathParameters']['user_id']
    )
    data = response.get('Items', None)

    if data is not None and len(data) > 0:
        user = data[0]
    else:
        user = None

    return {
        'statusCode': 200,
        'headers': {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Headers": "*",
            "Access-Control-Allow-Methods": "GET"
        },
        'body': json.dumps(user)
    }
```

```serverless.yml
service: Sample
frameworkVersion: "3"

provider:
  name: aws
  runtime: python3.9
  region: ap-northeast-1
  stage: ${opt:stage, 'dev'}
  environment:
    stage: ${sls:stage}

functions:
  function_get_user:
    handler: function-get-user.lambda_handler
    environment:
      user_table_name: user-${self:provider.stage}
    events:
      - http:
          path: user
          method: get
          request:
            parameters:
              querystrings:
                uid: true

```

## 移行プロセス
以下のような手順で今回は移行してみます

### STEP1:設定ファイルの移行:serverless.ymlの設定をtemplate.yamlに変換する

```template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'

Resources:
  GetUserFunction:
    Type: AWS::Serverless::Function
    Properties:
    　CodeUri: user/
　　　 Handler: function-get-user.lambda_handler  # 関数のエントリーポイント
      Runtime: python3.9                         # ランタイム
      MemorySize: 128                            # メモリサイズ
      Timeout: 60                                # タイムアウト時間
      Role: arn:aws:iam::<account-id>:role/service-role/xxxxxxx # ロール
      Environment:                               # 環境変数の定義
        Variables:
          STAGE: !Ref Stage                      # stageを環境変数に設定
          TABLE_NAME: !Sub "user-${Stage}"       # 環境に応じたテーブル名を設定
      Events:                                    # HTTPイベントの設定
        UserApi:
          Type: Api
          Properties:
            Path: /user                          # API Gatewayのエンドポイント
            Method: get                          # HTTPメソッド
            RequestParameters:                   # クエリパラメータの指定
              querystrings:
                user_id: true

Parameters:
  Stage:
    Type: String
    Default: dev                                 # デフォルトのステージは "dev"
    AllowedValues:
      - dev
      - prod
    Description: The deployment stage

```

:::note info
Serverless FrameworkはYAML設定ファイルで小文字を使うことが多いですが、AWS SAMやLambdaでは大文字の環境変数名が推奨されるため今回大文字に変更しました。
:::

### STEP2:samconfig.tomlにて環境情報を修正する
Serverless FrameworkはYAMLにてregionなどの情報を記述してましたがAWS SAMではregionとかの情報などはsamconfig.tomlのというファイルに記述します。必要に応じて修正しましょう

#### Serverless FrameworkとAWS SAMの比較表
| 設定項目              | Serverless Framework (`serverless.yaml`)       | AWS SAM (`samconfig.toml`)             |
|-----------------------|-----------------------------------------------|---------------------------------------|
| **リージョン**        | `provider.region`                             | `[環境名.global.parameters].region`   |
| **ステージ**          | `provider.stage`                              | `parameter_overrides` で指定          |
| **環境変数**          | `provider.environment`                        | `template.yaml` に記述                |
| **デプロイ用のスタック名** | CLI またはカスタム設定                       | `[環境名.global.parameters].stack_name` |
| **ランタイム指定**    | `provider.runtime`                            | `template.yaml` に記述                |
| **キャッシュや並列設定** | 設定不可                                      | `[環境名.build.parameters]` で指定    |


```serverless.yamlの例
provider:
  name: aws
  runtime: python3.9
  region: ap-northeast-1
  stage: ${opt:stage, 'dev'}
  environment:
    stage: ${sls:stage}
```

serverless.yamlのproviderにて定義していたregionとか、stagなどのパラメータをsamconfig.tomlではregionや、environmentで指定したstageなどパラメータの設定を行うことができます。

#### regionを指定する
環境ごとにリージョンを指定できます。
例として、dev 環境で ap-northeast-1 を指定する場合は以下のように指定します。

```samconfig.toml
[dev.global.parameters]
region = "ap-northeast-1"
```

#### パラメータを指定する
parameter_overrides を使用して、キーと値のペアでパラメータを渡します。
例として、STAGE パラメータに dev を設定する場合は以下のように指定します。

```samconfig.toml
[dev.deploy.parameters]
parameter_overrides = "STAGE=dev"
```

### 最終的なsamconfig.tomlの例

```samconfig.toml

[default.global.parameters]
version = 0.1

# 開発環境の設定
[dev.global.parameters]
stack_name = "aws-sam-test-dev-stack"   # CloudFormation スタック名

[dev.build.parameters]
region = "ap-northeast-1"               # AWS リージョン
cached = true
parallel = true

[dev.validate.parameters]
lint = true

[dev.deploy.parameters]
capabilities = "CAPABILITY_IAM"         # IAM ロールの権限設定 (CAPABILITY_IAM)
confirm_changeset = true
resolve_s3 = true                       # 自動的に S3 バケットを解決
parameter_overrides = "STAGE=dev"       # テンプレートのパラメータに渡す値をセットする

# 本番環境の情報
[prod.global.parameters]
stack_name = "aws-sam-test-prod-stack"

[prod.build.parameters]
region = "ap-northeast-1"
cached = true
parallel = true

[prod.deploy.parameters]
capabilities = "CAPABILITY_IAM"
confirm_changeset = true
resolve_s3 = true
parameter_overrides = "STAGE=prod"
```

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

### STEP3:コードの修正:環境変数やAPI Gatewayの設定に対応させる
SAM の場合、template.yaml で定義した環境変数やパラメータは、Lambda 関数で利用でるので少しだけ以下のように修正してみました。

```function-get-user.py
import boto3
import uuid
from boto3.dynamodb.types import TypeSerializer
from boto3.dynamodb.conditions import Key
import simplejson as json
from os import environ

dynamodb = boto3.resource('dynamodb')

def query_table(table_name, key=None, value=None):
    table = dynamodb.Table(table_name)
    if key is not None and value is not None:
        filtering_exp = Key(key).eq(value)
        return table.query(KeyConditionExpression=filtering_exp)
    raise ValueError('Parameters missing or invalid')

def lambda_handler(event, context):
    serializer = TypeSerializer()

    response = query_table(
        table_name= environ['TABLE_NAME'],
        key="user_id",
        value=event['pathParameters']['user_id']
    )
    data = response.get('Items', None)

    if data is not None and len(data) > 0:
        user = data[0]
    else:
        user = None

    return {
        'statusCode': 200,
        'headers': {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Headers": "*",
            "Access-Control-Allow-Methods": "GET"
        },
        'body': json.dumps(user)
    }
```



### STEP4:ビルドする
Lambda関数をデプロイする前にSAMでビルドする。

```
sam build
```

### STEP4:ローカルで動作を確認する

```
sam local invoke GetUserFunction --event events/get_user.json

{"statusCode": 200, "headers": {"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "*", "Access-Control-Allow-Methods": "GET"}, "body": {"userId": "123", "name": "Yamada Taro", "email": "yamada-taro@example.com"}}

```

### STEP5:インタラクティブモードでAPI Gatewayにdeployしてみる

```
sam deploy --config-env dev --guided
```

対話式で必要な情報を入力してください。
samconfig.toml に記載された設定がそのまま適用されますが指定された情報でSAMがCloudFormationを使用してスタックを作成し、API GatewayやLambda関数をデプロイします。

![スクリーンショット 2024-12-19 14.21.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796792/259a18b3-73df-dcf2-8ffd-711e0a3ba2a0.png)

:::note info
・初回は--guidedを使い、設定を確認しながらデプロイを行うと良いでしょう
・samconfig.tomlに設定を保存すると、次回以降はsam deploy --config-env devで迅速にデプロイ可能です。
・デプロイ環境（devやprod）ごとにパラメータを切り替えられるため、運用が簡単になります
:::

### STEP6:Deployが成功したらAPIにアクセスしてみる
curlで呼び出したらこんな感じで結果が返ってきました

```
curl -X GET "https://{api_id}.execute-api.ap-northeast-1.amazonaws.com/dev/user?uid=123"

{
  "userId": "123",
  "name": "Yamada Taro",
  "email": "yamada-taro@example.com"
}

```

# まとめ
Serverless FrameworkからAWS SAMへの移行は比較的スムーズに行うことができました。
AWS SAMは公式サポートが充実しており、シンプルな構成でAWSサービスとの親和性も高いため、今後のプロジェクトや既存のインフラの見直しの際に移行を検討してみる価値があります。  

実際の案件で完全移行が完了した際には、さらに詳しい事例やノウハウを共有する記事を書きたいと思います。  
最後までお読みいただきありがとうございました！今後の皆さんのプロジェクトの参考になれば幸いです。

----

https://www.wantedly.com/companies/www-bravesoft-co-jp

bravesoftではエンジニアを募集しております。採用ページをご確認ください

https://bravesoft.co.jp/recruit/

https://www.wantedly.com/projects/1642182