Serverless Operations, inc

>_cd /blog/id_x6tu73nci9z0

title

サーバレスアプリケーションのデプロイにおけるSAMとAWS CDKとServerless Frameworkの比較

summary

2024年現在サーバレスアプリケーションのデプロイには何を使っていますか?AWSにおけるメジャーなデプロイツールといえば、AWS CDK(Cloud Development Kit)、SAM(Serverless Application Model)、Serverless Framework、Terraform等が挙げられるかと思いますが、サーバレスアプリケーションの開発においては恐らく前の3者を使うことが多いのではないでしょうか。本記事ではこれらのツールの違い及びインストールからデプロイまでの手順について説明しています。ツール選定の参考になれば幸いです。

まずは、AWS SAM(Serverless Application Model)、AWS CDK(Cloud Development Kit)、Serverless Frameworkの違いを表にまとめます。

特徴

AWS SAM

AWS CDK

Serverless Framework

主な目的

サーバーレスアプリケーションの構築と管理

AWSリソースのプログラム的定義と管理

サーバーレスアプリケーションの構築と管理

開発モデル/言語サポート

YAML

TypeScript, Python, Java, C#, Go

YAML, TypeScript

ローカル実行・テスト

sam localでローカル実行可能

ローカルでのサポートは弱い

serverless-offlineプラグインでローカル実行可能

デプロイ方法

sam deployでAWSへ直接デプロイ

cdk deployでAWSへ直接デプロイ

serverless deployでAWSへ直接デプロイ

学習曲線

比較的簡単

プログラミング知識が必要

比較的簡単

スタックの管理

CloudFormationに依存

CloudFormationに依存

CloudFormationに依存

プラグイン拡張

なし

なし

コミュニティプラグインにより拡張可能

公式サポート

AWS公式

AWS公式

Serverless.inc

費用

無料

無料

年間200万ドル以上の売上のある組織では有料

特徴的な違いとして、ツールとしての思想がAWS CDKと、Serverless Framework及びSAMとで大きく違うことが分かります。Serverless Framework及びSAMはYAMLベースで設定を記述していくのに対してAWS CDKはプログラムベースで設定を記述していきます。これはサーバレスの歴史的な経緯が作用しているとも言えます。サーバレスがまだ黎明期だったころの2015 - 2016年頃にServerless Framework及びSAMはリリースされました。この頃はAWS Lambdaを中心としてAmazon DynamoDBやAmazon API Gatewayとつなぐだけなど、小規模なアーキテクチャが多かったです。しかし、新たに様々なサーバレスなサービスが登場するにつれてやれることも増えた反面、そのアーキテクチャは複雑になりました。結果、膨大なYAMLの設定が必要になる中で、その可読性の悪さがIaCにおけるひとつの課題になりました。その中で現れたのがAWS CDKであり、プログラミングでインフラを定義するという新しいやり方でした。

この特徴を踏まえて言うと、Serverless Framework及びSAMはそれなりにシンプルなサーバレスアプリケーションに向いていますし、一定以上のAWSリソースを使ってIaCをやるとなるとやはりAWS CDKが向いていることになると考えています。しかし、その反面プログラムにあまり慣れていないという場合はAWS CDKを使うことへの学習コストに時間が割かれるでしょう。

SAMとServerless Frameworkを比較すると、やはりAWS公式であることとそうでないこと、無料か有料である部分をが大きなポイントになるでしょう。そう考えると今後はやはりSAMを選ぶ方が多くなってくるのではないでしょうか。また、Serverless Frameworkはコミュニティプラグンインが充実していますが、有料となった今、そのオープンソースコミュニティとしての熱量は下がってきているようにも感じます。その中での開発の継続性を考えた時に、AWS公式であるSAMが有利と言わざるえを得ないかと考えています。

TypeScriptの使用

TypeScriptは、JavaScriptに型の概念を導入したプログラミング言語で、大規模なプロジェクトでのコードの安全性や保守性を向上させることを目的としています。AWS Lambdaの開発においても生のJavaScriptで開発するよりもTypeScriptを使用した方が確実に生産性は向上するでしょう。さらに今回取り上げるデプロイツールと組み合わせて、デプロイ時にテストやトランスパイラといったCI/CDのワークフローを実施することで、快適な開発環境を手に入れることが出来ます。以降はTypeScriptベースでツールのインストールからデプロイまでを確認していきます。

AWS CDK + TypeScriptを使ったアプリケーションのデプロイ

AWS CDKは、AWSのインフラをプログラムコードとして定義するためのフレームワークです。AWSリソースをプログラムによって宣言的に作成、設定、管理できるようにするツールです。JSONやYAMLで管理するツールと比較するとプログラムで記述でき、ループ処理などが使えるためより柔軟な設定が可能となります。

ここではAWS CDK+TypeScriptを使った開発において、インストールからAWS環境にAmazon API Gateway + AWS Lambdaをデプロイするまでの手順を説明します。まずはAWS CDK自体をインストールします。Amazon Linuxなどnpmが使える環境ではnpmのコマンドでインストールできますし、Macを使用している場合にはbrewを使ってもインストールできます。

% npm install -g aws-sdk
or
% brew install aws-sdk

初回のデプロイが実施されていない場合はcdk bootstrapコマンドを実行してAWS環境をセットアップします。

 cdk bootstrap                                  
 ⏳  Bootstrapping environment aws://99999999999/us-east-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
CDKToolkit: creating CloudFormation changeset...
 ✅  Environment aws://99999999999/us-east-1 bootstrapped.

プロジェクトのセットアップをします。cdk initコマンドを実行すると言語ごとのプロジェクトのスケルトンを生成してくれます。今回はTyprScriptを使用するためこれを引数に指定します。まずは専用のディレクトリを作成してそこに移動、その後、cdk initを実行します。

% mkdir hello-cdk
% cd hello-cdk
% cdk init app --language typescript 

AWS Lambda用の型定義ファイルをインストールします。

% npm install -D @types/aws-lambda

そして以下のようなフォルダ構成を作ります。今回は最小限のスタックをデプロイするので、それに必要なファイルのみをピックアップしています。

  • bin/hello-cdk.tscdk deploy実行時に実行されるスクリプト
  • lib/hello-cdk-stack.ts :CDKスタックの最上位のコンストラクタのコレクション
  • lib/hello-cdk.ts :Amazon API Gateway + AWS Lambdaのスタックが定義されたファイル。lib/hello-cdk-stack.tsから呼び出されます
  • lib/hello-cdk.function.ts :Lambdaファンクションのコードを格納したファイルです。
├── bin
│   └── hello-cdk.ts
├── lib
│   ├── hello-cdk-stack.ts
│   ├── hello-cdk.function.ts
│   └── hello-cdk.ts

lib/hello-cdk.ts でAmazon API Gateway + AWS Lambdaのスタックを構成します。NodejsFunction コンストラクタでLambdaファンクションを、LambdaRestApi コンストラクタで定義したLambdaファンクションをAmazon API Gatewayにアタッチした構成を記述できます。


import { Construct } from 'constructs';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { LambdaRestApi } from 'aws-cdk-lib/aws-apigateway';
  
export class HelloCdk extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);
    const helloFunction = new NodejsFunction(this, 'function');
    new LambdaRestApi(this, 'apigw', {
      handler: helloFunction,
    });
  }
}

lib/hello-cdk.function.ts では、実際に実行されるLambda ファンクション内のソースコードを定義します。以下はHello WorldのメッセージとHTTPのステータスコードの200を返すだけの関数です。

import { Context, APIGatewayProxyResult, APIGatewayEvent } from 'aws-lambda';

export const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
    console.log(`Event: ${JSON.stringify(event, null, 2)}`);
    console.log(`Context: ${JSON.stringify(context, null, 2)}`);
    return {
        statusCode: 200,
        body: JSON.stringify({
            message: 'hello world',
        }),
    };
};

lib/hello-cdk-stack.ts で、Amazon API Gateway + AWS LambdaのHelloCdkスタックを呼び出します。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { HelloCdk } from './hello-cdk';
  
export class HelloCdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    new HelloCdk(this, 'hello-world');
  }
}

bin/hello-cdk.ts にCDKがデプロイされるときに走るコードを定義します。

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { HelloCdkStack } from '../lib/hello-cdk-stack';

const app = new cdk.App();
new HelloCdkStack(app, 'HelloCdkStack', {});

cdk deployを実行します。実行するとDockerコンテナが呼び出されて、Lambdaファンクションをトランスパイルしデプロイを行います。

% cdk deploy

デプロイ後に発行されたエンドポイントにGETリクエストを送るとちゃんとメッセージが返ってきました。

%  curl https://jei2j6v5x6.execute-api.us-east-1.amazonaws.com/prod/
{"message":"hello world"}                             

SAM + TypeScriptを使ったアプリケーションのデプロイ

SAMはAWSが提供するデプロイツールです。特にサーバーレスアプリケーションを簡潔に定義することに特化しており、AWS Lambda関数やAPI Gatewayの設定をYAML/JSONのシンプルなテンプレート形式で書くことができます。また、サーバレスアプリケーションの開発時にローカル開発が難しいという課題がありますが、これを解決するためのローカル開発のツールも提供されています。複雑なプログラミングの知識を必要としないため、CDKと比べるとその学習コストは低いと言えるでしょう。

まずは、SAM CLIをインストールします。AWS SAM CLI のインストールの手順に従って環境に合わせて行ってください。そして、sam initコマンドでプロジェクトを生成します。対話形式で答えていく形式になるので、適切なものを選んでいきましょう。

 % sam init      

You can preselect a particular runtime or package type when using the `sam init` experience.
Call `sam init --help` to learn more.

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]: N

Which runtime would you like to use?
	1 - dotnet8
	2 - dotnet6
	3 - go (provided.al2)
	4 - go (provided.al2023)
	5 - graalvm.java11 (provided.al2)
	6 - graalvm.java17 (provided.al2)
	7 - java21
	8 - java17
	9 - java11
	10 - java8.al2
	11 - nodejs20.x
	12 - nodejs18.x
	13 - nodejs16.x
	14 - python3.9
	15 - python3.8
	16 - python3.12
	17 - python3.11
	18 - python3.10
	19 - ruby3.3
	20 - ruby3.2
	21 - rust (provided.al2)
	22 - rust (provided.al2023)
Runtime: 11

What package type would you like to use?
	1 - Zip
	2 - Image
Package type: 1

Based on your selections, the only dependency manager available is npm.
We will proceed copying the template using npm.

Select your starter template
	1 - Hello World Example
	2 - Hello World Example TypeScript
Template: 2

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: N

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]: 

    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Runtime: nodejs20.x
    Architectures: x86_64
    Dependency Manager: npm
    Application Template: hello-world-typescript
    Output Directory: .
    Configuration file: sam-app/samconfig.toml
    
    Next steps can be found in the README file at sam-app/README.md

すると、プロジェクト名のフォルダが生成されてその配下の以下のような構成が生成されます。

├── README.md
├── events
│   └── event.json
├── hello-world
│   ├── app.ts
│   ├── jest.config.ts
│   ├── package.json
│   ├── tests
│   │   └── unit
│   │       └── test-handler.test.ts
│   └── tsconfig.json
├── samconfig.toml
└── template.yaml

template.ymlがCloudFormationの構成を決めるファイルですが以下のようになっています。hello-worldというLambdaファンクションが生成されて、API Gatewayによるイベントが定義されているのがわかります。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

    # You can add LoggingConfig parameters such as the Logformat, Log Group, and SystemLogLevel or ApplicationLogLevel. Learn more here https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html#sam-function-loggingconfig.
    LoggingConfig:
      LogFormat: JSON
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs20.x
      Architectures:
      - x86_64
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get
    Metadata: # Manage esbuild properties
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: es2020
        Sourcemap: true
        EntryPoints:
        - app.ts

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: API Gateway endpoint URL for Prod stage for Hello World function
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: Hello World Lambda Function ARN
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: Implicit IAM Role created for Hello World function
    Value: !GetAtt HelloWorldFunctionRole.Arn

Lambdaファンクションの本体であるhello-world/app.ts を見てみるとAWS CDKの時のサンプルと同様にHello WorldとHTTPのステータスコード200を返すだけの関数が定義されています。

import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

/**
 *
 * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
 * @param {Object} event - API Gateway Lambda Proxy Input Format
 *
 * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
 * @returns {Object} object - API Gateway Lambda Proxy Output Format
 *
 */

export const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    try {
        return {
            statusCode: 200,
            body: JSON.stringify({
                message: 'hello world',
            }),
        };
    } catch (err) {
        console.log(err);
        return {
            statusCode: 500,
            body: JSON.stringify({
                message: 'some error happened',
            }),
        };
    }
};

hello-worldディレクトリに移動してnpm installを実行し、定義済みのライブラリをインストールします。

% cd hello-world
% npm install

元のディレクトリに戻ってビルドを行います。成功するとデプロイパッケージが生成されます。

% sam build
Starting Build use cache                                                                                                                                                                                                                                      
Manifest file is changed (new hash: 3a55b648027d1145164f225fad481127) or dependency folder (.aws-sam/deps/41716e1e-726a-47c1-b428-8140a0db3ff5) is missing for (HelloWorldFunction), downloading dependencies and copying/building source                     
Building codeuri: /Users/xxxxxxxxxx/src/sam-app/hello-world runtime: nodejs20.x architecture: x86_64 functions: HelloWorldFunction                                                                                                                        
 Running NodejsNpmEsbuildBuilder:CopySource                                                                                                                                                                                                                   
 Running NodejsNpmEsbuildBuilder:NpmInstall                                                                                                                                                                                                                   
 Running NodejsNpmEsbuildBuilder:EsbuildBundle                                                                                                                                                                                                                
 Running NodejsNpmEsbuildBuilder:CleanUp                                                                                                                                                                                                                      
 Running NodejsNpmEsbuildBuilder:MoveDependencies                                                                                                                                                                                                             
                                                                                                                                                                                                                                                              
Sourcemap set without --enable-source-maps, adding --enable-source-maps to function HelloWorldFunction NODE_OPTIONS                                                                                                                                           
                                                                                                                                                                                                                                                              
You are using source maps, note that this comes with a performance hit! Set Sourcemap to false and remove NODE_OPTIONS: --enable-source-maps to disable source maps.                                                                                          
                                                                                                                                                                                                                                                              

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

sam deployコマンドでデプロイパッケージをAWS環境にデプロイします。

% sam deploy

デプロイ後に表示されたエンドポイントにGETリクエストを送ると正しくHello Worldが返ってきました。

% curl https://2ejhbbxb1l.execute-api.us-east-1.amazonaws.com/Prod/hello/
{"message":"hello world"}

Serverless Framework + TypeScriptを使ったアプリケーションのデプロイ

Serverless FrameworkはSAMと同様のYAMLベースかつサーバレスアプリケーションに特化したデプロイツールです。SAMとの違いとしては、コミュニティベースのプラグインが多数開発されているため拡張性が大きいというところ、Serverless.incという会社がメンテナンスを行っているところ、Serverless Framework V4の変更点まとめの記事にあるように年間200万ドル以上の売上のある組織では有料でサブスクリプションを購入する必要があるところです。条件次第では有料にはなっていますが、サーバレスアプリケーションのデプロイツールの先駆け的な存在でもあるツールで未だに人気は衰えません。

まずはnpmでServerless Frameworkをインストールします。

% npm i serverless -g

そしてserverlessというコマンドを打つと対話形式でのプロジェクトのセットアップが開始されますが、この対話式のコマンドではTypeScriptのテンプレートが提供されていません。なので、独自で作っていきます。まずは以下のようなフォルダ構成を作ります。

 % tree
sls-test
├── handler.ts
├── serverless.yml
├── package.json
└── tsconfig.json

そして、npmで最低限必要なファイルをインストールしていきましょう。

% npm install -D ts-node
% npm install -D typescript
% npm install esbuild
% npm install -D @types/aws-lambda

次にtsconfig.jsonを以下のように設定します。

{
    "compilerOptions": {
    "target": "es2020",
    "strict": true,
    "preserveConstEnums": true,
    "noEmit": true,
    "sourceMap": false,
    "module":"es2015",
    "moduleResolution":"node",
    "esModuleInterop": true, 
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,  
  },
  "exclude": ["node_modules", "**/*.test.ts"]
}

serverless.ymlを設定します。AWS LambdaにAPI Gatewayをアタッチした設定になっています。Serverless Framework V4からは特別な設定がなくともTypescriptでコードが記述されている場合にはデプロイ時に自動でビルドを行ってくれます。また、Serverless Framework V4からはbuild というセクションが新たに導入されました。esbuildのカスタマイズが必要な場合にはその配下にesbuild の項目を設定が可能になります。詳細は、AWS Lambda Build Configurationをご参照ください。

service: sls-test

provider:
  name: aws
  runtime: nodejs20.x

functions:
  hello:
    handler: handler.hello
    events:
      - httpApi:
          path: /
          method: get

Lambdaファンクション本体は以下のようにHello WorldとHTTPステータスコードの200を返すように記述します

import { Context, APIGatewayProxyResult, APIGatewayEvent } from 'aws-lambda';

export const hello = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
    console.log(`Event: ${JSON.stringify(event, null, 2)}`);
    console.log(`Context: ${JSON.stringify(context, null, 2)}`);
    return {
        statusCode: 200,
        body: JSON.stringify({
            message: 'hello world',
        }),
    };
};

serverless deployコマンドでトランスパイルからAWSへのデプロイまでを実施します。

% serverless deploy

Deploying "sls-test" to stage "dev" (us-east-1)

✔ Service deployed to stack sls-test-dev (56s)

endpoint: GET - https://lqdc2dwf74.execute-api.us-east-1.amazonaws.com/
functions:
  hello: sls-test-dev-hello (1.3 kB)

デプロイ後に表示されたエンドポイントにアクセスすると正しくHello Worldが返ってきます。

% curl https://lqdc2dwf74.execute-api.us-east-1.amazonaws.com/
{"message":"hello world"}

まとめ

以上で、AWS CDKとSAMとServerless Frameworkを使ってツールのインストールからAPIアプリケーションのデプロイまでをやってみました。それぞれ、非常にセットアップはシンプルで簡単にデプロイまでが可能なことがわかります。最初に書いた通り、この3つのツールはやれることや学習コストなどを軸に違いがあります。皆さんのやりたいことやチームとしてのスキルセットに合わせて、この記事がツールの選定のための参考になれば幸いです。

Written by
CEO

堀家 隆宏

Takahiro Horike

  • Facebook->
  • X->
  • GitHub->

Share

Facebook->X->
Back
to list
<-