0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

bravesoftAdvent Calendar 2024

Day 20

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

Last updated at Posted at 2024-12-19

はじめに

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

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

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

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

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

AWS SAMとは?

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

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

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)のインストール方法については公式ガイドを参照
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の情報を取得するスクリプトを書いてみる

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

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

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を起動しておく

  2. localでsam local start-apiを実行すると自動でdocker containerが生成される
    スクリーンショット 2024-12-17 2.57.27.png

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

docker ps

スクリーンショット 2024-12-17 3.00.57.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

Apidogの場合

GET:http://127.0.0.1:3000/pokemon?name=pikachu

スクリーンショット 2024-12-17 3.06.27.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

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"

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

・初回は--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サービスとの親和性も高いため、今後のプロジェクトや既存のインフラの見直しの際に移行を検討してみる価値があります。

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


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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?