平凡な社会人の日記

平凡な社会人の日記です。怠惰な毎日を送っております。

AWS SAM を用いたサーバーレスアプリケーションのデプロイ(ApiGatewayV2とLambda)

本稿の目的

  • SAM について知る
  • sam-cliを用いてAWS Resourceを作成できるようになる

本稿の対象者

  • AWSのリソースをコードで管理したい。
  • Lambdaをテストしたい
  • CloudFormationに興味がある
  • パイプラインで自動テスト、ビルド、デプロイまで行いたい

はじめに

こんにちは。新卒1年目のさっちゃんです。早いもので、社会人になって半年も経ちました。この半年は分からないことだらけで、チームのリソースを割いてもらいながら、少しずつ出来ることを増やせたかなと思ってます。新人なんてそんなもんだ。

さて、AWSを触っていると次のような疑問が湧いてきます。

  • 使うサービスが多すぎて何作ったか、どんな設定をしたか分からん...。
  • Lambdaのテストどうすりゃいいんじゃ...。
  • どれを消したらどこまで影響するんだ...。
  • 人間の操作ではなく自動でデプロイしてほしい...。

これらを解決してくれるのが AWS SAM (Serverless Application Model) です。

AWS SAMとは

SAMとはなんなのか、公式の説明を引用してみましょう。

AWS サーバーレスアプリケーションモデル (SAM、Serverless Application Model) は、サーバーレスアプリケーション構築用のオープンソースフレームワークです。迅速に記述可能な構文で関数、API、データベース、イベントソースマッピングを表現できます。リソースごとにわずか数行で、任意のアプリケーションを定義して YAML を使用してモデリングできます。デプロイ中、SAM が SAM 構文を AWS CloudFormation 構文に変換および拡張することで、サーバーレスアプリケーションの構築を高速化することができます。 (AWS SAM より引用)

とのことです。なるほど完全に理解した。実はもっとわかりやすい記事があって、大変参考にしたのでここに置いておきます。(この記事見ずにこちらに飛んだ方がいいかもしれない)

AWS SAM CLI 再入門 2021.08

記事を移動するのが面倒な方のために、SAMとは何かの部分を引用します。

Serverless Application Model(SAM)は Lambda や API Gateway, DynamoDB のようなサービスを活用したサーバーレスアプリケーションのデプロイに特化した、AWS CloudFormation の拡張機能です。通常の CloudFormation テンプレートと同様に YAML/JSON でテンプレートを定義しますが、素のテンプレートより簡潔に記載できるのが特徴です。 (AWS SAM CLI 再入門 2021.08 より引用)

両者を見ると、SAM が CloudFormation の拡張機能であるということがよくわかります。知らない方もいると思いますので説明しておくと、CloudFormationとはリソースをyaml形式のファイルで一元管理、作成できるAWSのサービスです。テンプレートファイルで AWS リソース(API Gateway, Lambda, Dynamo, Cognitoなどなど)を定義し、S3に保存してコンソール上で指定するか手でアップロードすればそれらを自動で作成してくれます。すごいですね。

SAMの基本はCloudFormationなのですが、CLIベースでより簡潔に、便利にリソースを作成する事ができます。早速やってみましょう。

早速やってみよう

sam-cliのインストールについてはAWS SAM CLI 再入門 2021.08通りにやればいいのでコマンドだけ置いておきます。

brew tap aws/tap
brew install aws-sam-cli

また、AWS アカウントを持っていてIAMユーザーを作成したら、aws-cliをインストールして aws configure コマンドで設定を済ませておいてください。 さて、実際にコマンドラインでチャチャっとサーバーレスなアプリケーションを作ってみます。

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

What package type would you like to use?
    1 - Zip (artifact is a zip uploaded to S3)  
    2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1

Which runtime would you like to use?
    1 - nodejs14.x
    2 - python3.9
    3 - ruby2.7
    4 - go1.x
    5 - java11
    6 - dotnetcore3.1
    7 - nodejs12.x
    8 - nodejs10.x
    9 - python3.8
    10 - python3.7
    11 - python3.6
    12 - python2.7
    13 - ruby2.5
    14 - java8.al2
    15 - java8
    16 - dotnetcore2.1
Runtime: 2

Project name [sam-app]:   

Cloning from https://github.com/aws/aws-sam-cli-app-templates

AWS quick start application templates:
    1 - Hello World Example
    2 - EventBridge Hello World
    3 - EventBridge App from scratch (100+ Event Schemas)
    4 - Step Functions Sample App (Stock Trader)
    5 - Elastic File System Sample App
Template selection: 1

    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Runtime: python3.9
    Architectures: x86_64
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .
    
    Next steps can be found in the README file at ./sam-app/README.md

2 . sam deploy -g

cd sam-app
sam deploy -g

Configuring SAM deploy
======================

    Looking for config file [samconfig.toml] :  Not found

    Setting default arguments for 'sam deploy'
    =========================================
    Stack Name [sam-app]: 
    AWS Region [ap-northeast-1]: 
    #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
    Confirm changes before deploy [y/N]: y
    #SAM needs permission to be able to create roles to connect to the resources in your template
    Allow SAM CLI IAM role creation [Y/n]: y
    HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
    Save arguments to configuration file [Y/n]: y
    SAM configuration file [samconfig.toml]: 
    SAM configuration environment [default]: 

    Looking for resources needed for deployment:
    Creating the required resources...
    Successfully created!
     Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-10c3jq4829zh7
     A different default S3 bucket can be set in samconfig.toml

    Saved arguments to config file
    Running 'sam deploy' for future deployments will use the parameters saved above.
    The above parameters can be changed by modifying samconfig.toml
    Learn more about samconfig.toml syntax at 
    https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

Uploading to sam-app/c6ce8fa8b5a97dd022ecd006536eb5a4  847 / 847  (100.00%)

    Deploying with following values
    ===============================
    Stack name                   : sam-app
    Region                       : ap-northeast-1
    Confirm changeset            : True
    Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-10c3jq4829zh7
    Capabilities                 : ["CAPABILITY_IAM"]
    Parameter overrides          : {}
    Signing Profiles             : {}

Initiating deployment
=====================
Uploading to sam-app/3dae71abecd1456ad194f6b7925a4129.template  1126 / 1126  (100.00%)

Waiting for changeset to be created..

CloudFormation stack changeset
-------------------------------------------------------------------------------------------------
Operation                LogicalResourceId        ResourceType             Replacement            
-------------------------------------------------------------------------------------------------
+ Add                    HelloWorldFunctionHell   AWS::Lambda::Permissio   N/A                    
                         oWorldPermissionProd     n                                               
+ Add                    HelloWorldFunctionRole   AWS::IAM::Role           N/A                    
+ Add                    HelloWorldFunction       AWS::Lambda::Function    N/A                    
+ Add                    ServerlessRestApiDeplo   AWS::ApiGateway::Deplo   N/A                    
                         yment47fc2d5f9d          yment                                           
+ Add                    ServerlessRestApiProdS   AWS::ApiGateway::Stage   N/A                    
                         tage                                                                     
+ Add                    ServerlessRestApi        AWS::ApiGateway::RestA   N/A                    
                                                  pi                                              
-------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:470529257240:changeSet/samcli-deploy1633806768/fb9f510d-4b14-46da-8558-d7a6b81caf48


Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y


2021-10-10 04:13:10 - Waiting for stack create/update to complete

CloudFormation events from changeset
-------------------------------------------------------------------------------------------------
ResourceStatus           ResourceType             LogicalResourceId        ResourceStatusReason   
-------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS       AWS::IAM::Role           HelloWorldFunctionRole   -                      
CREATE_IN_PROGRESS       AWS::IAM::Role           HelloWorldFunctionRole   Resource creation      
                                                                           Initiated              
CREATE_COMPLETE          AWS::IAM::Role           HelloWorldFunctionRole   -                      
CREATE_IN_PROGRESS       AWS::Lambda::Function    HelloWorldFunction       -                      
CREATE_COMPLETE          AWS::Lambda::Function    HelloWorldFunction       -                      
CREATE_IN_PROGRESS       AWS::Lambda::Function    HelloWorldFunction       Resource creation      
                                                                           Initiated              
CREATE_IN_PROGRESS       AWS::ApiGateway::RestA   ServerlessRestApi        -                      
                         pi                                                                       
CREATE_IN_PROGRESS       AWS::ApiGateway::RestA   ServerlessRestApi        Resource creation      
                         pi                                                Initiated              
CREATE_COMPLETE          AWS::ApiGateway::RestA   ServerlessRestApi        -                      
                         pi                                                                       
CREATE_IN_PROGRESS       AWS::ApiGateway::Deplo   ServerlessRestApiDeplo   -                      
                         yment                    yment47fc2d5f9d                                 
CREATE_IN_PROGRESS       AWS::ApiGateway::Deplo   ServerlessRestApiDeplo   Resource creation      
                         yment                    yment47fc2d5f9d          Initiated              
CREATE_IN_PROGRESS       AWS::Lambda::Permissio   HelloWorldFunctionHell   Resource creation      
                         n                        oWorldPermissionProd     Initiated              
CREATE_IN_PROGRESS       AWS::Lambda::Permissio   HelloWorldFunctionHell   -                      
                         n                        oWorldPermissionProd                            
CREATE_COMPLETE          AWS::ApiGateway::Deplo   ServerlessRestApiDeplo   -                      
                         yment                    yment47fc2d5f9d                                 
CREATE_IN_PROGRESS       AWS::ApiGateway::Stage   ServerlessRestApiProdS   -                      
                                                  tage                                            
CREATE_IN_PROGRESS       AWS::ApiGateway::Stage   ServerlessRestApiProdS   Resource creation      
                                                  tage                     Initiated              
CREATE_COMPLETE          AWS::ApiGateway::Stage   ServerlessRestApiProdS   -                      
                                                  tage                                            
CREATE_COMPLETE          AWS::Lambda::Permissio   HelloWorldFunctionHell   -                      
                         n                        oWorldPermissionProd                            
CREATE_COMPLETE          AWS::CloudFormation::S   sam-app                  -                      
                         tack                                                                     
-------------------------------------------------------------------------------------------------

CloudFormation outputs from deployed stack
-------------------------------------------------------------------------------------------------
Outputs                                                                                         
-------------------------------------------------------------------------------------------------
Key                 HelloWorldFunctionIamRole                                                   
Description         Implicit IAM Role created for Hello World function                          
Value               arn:aws:iam::470529257240:role/sam-app-HelloWorldFunctionRole-1RPJFCRUSMHQJ 

Key                 HelloWorldApi                                                               
Description         API Gateway endpoint URL for Prod stage for Hello World function            
Value               https://7k76tde9fj.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/     

Key                 HelloWorldFunction                                                          
Description         Hello World Lambda Function ARN                                             
Value               arn:aws:lambda:ap-northeast-1:470529257240:function:sam-app-                
HelloWorldFunction-a1yhY93DihkE                                                                 
-------------------------------------------------------------------------------------------------

Successfully created/updated stack - sam-app in ap-northeast-1

この状態でAWS コンソール上でCloudFormation、Lambda、Api Gatewayをそれぞれみるとリソースが作成されたことが確認できます。 f:id:physics-heibon:20211010042450p:plain f:id:physics-heibon:20211010042459p:plain f:id:physics-heibon:20211010042506p:plain

さらに、作成されたAPI GatewayにあるAPIを叩くと動作する事が確認できます。これでサーバーレスなアプリケーションの初めの一歩をデプロイできました。(/Prod とステージ名を入れるのを忘れないように注意です。) f:id:physics-heibon:20211010042917p:plain

さて、ここまでで行ったことは sam initsam deploy -gのみで、LambdaやAPI Gatewayの設定は書いた覚えがありません。これらの設定は、sam-app(プロジェクト名)ディレクトリの中のtemplate.yamlにあります。

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

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.lambda_handler
      Runtime: python3.9
      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

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

注目してほしいのは、 Resourcesの中のType: AWS::Serverless:Functionです。これにより、この部分はLambdaの設定であることが宣言されており、それに伴ってPropertiesの項目が決定されます。どのリソースにどのようなPropertiesがあるかは公式のリファレンスと睨めっこして書いていきます。この設定の中に

      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

と言う部分がありますが、ここではこのLambdaがどういう時に起動するかをTypeで決めています。ApiということはApiGatewayから呼ばれることを指しており、他にもHttpApiはApiGatewayV2から、CognitoはCognitoのトリガーに紐づけられること指します。Propertiesに書かれていることは簡単ですね。

しかしここで思うのは、API Gatewayの設定はどこにも書いていません。また、このLambdaがどのAPI Gatewayに紐づくかも書いていません。実はSAMではCloudFormationと違い、どこにも紐づかないLambdaを作成した場合自動でAPI Gatewayを作成してくれます。これをSAMのドキュメントでは implicit なAPI Gatewayと言っています。(最初見た時はなんのことかわかりませんでした。)また、API GatewayにLambdaを呼び出す権限(ポリシー)をアタッチするなど、権限周りも”いい感じ”にしてくれます。

SAMが勝手にリソースを作成してくれるのは便利ですが、Cognitoによる認証を設定するなどの細かい設定をしたい場合は自分でAPI Gatewayを作成するtemplateを書く必要があります。API Gatewayを書くtemplateの例はいくつもあったので、ここではAPI GatewayV2で作成してみたいと思います。API GatewayV2(HTTP API) については 高速、低コストで、より良いAPIの構築 – HTTP APIが利用可能(GA)になりました | Amazon Web Services ブログをご覧ください。

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

Resources:
  HttpApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: Prod
      # Cognitoによる認証を行う場合
      # Auth:
      #   Authorizers:
      #     CognitoAuth:
      #       IdentitySource: "$request.header.Authorization"
      #       JwtConfiguration:
      #         issuer: !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}
      #         audience:
      #           -

  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.lambda_handler
      Runtime: python3.9
      Architectures:
        - x86_64
      # VpcConfig: VPCの設定
      # Policies: Lambdaに対する権限の追加
      Events:
        HelloWorld:
          Type: HttpApi # 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
            # API Gateway の場合は RestApiId。今回はAPI GatewayV2
            ApiId: !Ref HttpApi
            # Cognitoによる認証を挟む場合
            # Auth:
            #   Authorizer: CognitoAuth

# Cognitoによる認証を行いたい場合
  # UserPool:
  #   Type: AWS::Cognito::UserPool
  #   Properties:
  #     UserPoolName: test
  #     Policies:
  #       PasswordPolicy:
  #         MinimumLength: 8
  #         RequireLowercase: true
  #         RequireNumbers: true
  #         RequireSymbols: true
  #         RequireUppercase: true
  #     UsernameAttributes:
  #       - email
  #     Schema:
  #       - AttributeDataType: String
  #         Name: email
  #         Required: false

Outputs:
  HelloWorldApi:
    Description: "API GatewayV2 endpoint URL for Prod stage for Hello World function"
# ここも変えたよ
    Value: !Sub "https://${HttpApi}.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

deployしてみます。sam deploy(-g は不要) f:id:physics-heibon:20211010050116p:plain できました。嬉しい。ぴーすぴーす✌️

sam deleteでリソースを一括で消します。

sam delete 
        Are you sure you want to delete the stack sam-app in the region ap-northeast-1 ? [y/N]: y
        Are you sure you want to delete the folder sam-app in S3 which contains the artifacts? [y/N]: y
        - Deleting S3 object with key sam-app/c6ce8fa8b5a97dd022ecd006536eb5a4
        - Deleting S3 object with key sam-app/124bee4a9ce8b2a3aac7814cb28ec561.template
        - Deleting S3 object with key sam-app/19ddbe63ffda195b5668e7fc8c0391c6.template
        - Deleting S3 object with key sam-app/3dae71abecd1456ad194f6b7925a4129.template
        - Deleting S3 object with key sam-app/a64b7c7012f3a53e9bbb4c723db373d3.template
        - Deleting Cloudformation stack sam-app

Deleted successfully

綺麗さっぱりなくなりました。

gitlabのパイプラインで自動テスト、デプロイ

書くのに疲れてきました...。

AWS SAM CLI 再入門 2021.08をみると、sam pipeline bootstrap, sam pipeline init --bootstrapで.gitlab-ciなど必要なファイルは生成されます。ここにはpytestやdeployをパイプラインで行うための雛形が書かれているのでここから自分用に変えていけばLambdaのテストも可能です。例えばAPI GatewayからのLambdaの呼び出しに対してのモックの仕方はsamによって自動生成されているのでありがたいです。

import json

import pytest

from hello_world import app


@pytest.fixture()
def apigw_event():
    """ Generates API GW Event"""

    return {
        "body": '{ "test": "body"}',
        "resource": "/{proxy+}",
        "requestContext": {
            "resourceId": "123456",
            "apiId": "1234567890",
            "resourcePath": "/{proxy+}",
            "httpMethod": "POST",
            "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
            "accountId": "123456789012",
            "identity": {
                "apiKey": "",
                "userArn": "",
                "cognitoAuthenticationType": "",
                "caller": "",
                "userAgent": "Custom User Agent String",
                "user": "",
                "cognitoIdentityPoolId": "",
                "cognitoIdentityId": "",
                "cognitoAuthenticationProvider": "",
                "sourceIp": "127.0.0.1",
                "accountId": "",
            },
            "stage": "prod",
        },
        "queryStringParameters": {"foo": "bar"},
        "headers": {
            "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
            "Accept-Language": "en-US,en;q=0.8",
            "CloudFront-Is-Desktop-Viewer": "true",
            "CloudFront-Is-SmartTV-Viewer": "false",
            "CloudFront-Is-Mobile-Viewer": "false",
            "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
            "CloudFront-Viewer-Country": "US",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
            "Upgrade-Insecure-Requests": "1",
            "X-Forwarded-Port": "443",
            "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
            "X-Forwarded-Proto": "https",
            "X-Amz-Cf-Id": "aaaaaaaaaae3VYQb9jd-nvCd-de396Uhbp027Y2JvkCPNLmGJHqlaA==",
            "CloudFront-Is-Tablet-Viewer": "false",
            "Cache-Control": "max-age=0",
            "User-Agent": "Custom User Agent String",
            "CloudFront-Forwarded-Proto": "https",
            "Accept-Encoding": "gzip, deflate, sdch",
        },
        "pathParameters": {"proxy": "/examplepath"},
        "httpMethod": "POST",
        "stageVariables": {"baz": "qux"},
        "path": "/examplepath",
    }


def test_lambda_handler(apigw_event, mocker):

    ret = app.lambda_handler(apigw_event, "")
    data = json.loads(ret["body"])

    assert ret["statusCode"] == 200
    assert "message" in ret["body"]
    assert data["message"] == "hello world"
    # assert "location" in data.dict_keys()

おわりに

今回は

  • SAM について知る
  • sam-cliを用いてAWS Resourceを作成できるようになる

を目標として、SAMについて説明し実際にAPIGatewayV2、Lambdaを作成しました。特にAPI GatewayV2のテンプレートに関する記事は少なくて苦労しました...。 また、SAMを使ってデプロイしようとするとどうしてもCloudFormationの文法が必要になるので、一石二鳥(?)で勉強できます。楽しいね。 個人的にはCloudFormationの公式ドキュメントは見づらいし機械翻訳も変なので、慣れていないうちはterraformのリファレンスと見比べながら見てました。

Neptune, SAMときたら次は何について書こうか思案中です。pytestでlambdaの単体テストを書いてみようかな。

参考

AWS サーバーレスアプリケーションモデル

AWS SAM CLI 再入門 2021.08

【AWS Black Belt Online Seminar】AWS Serverless Application Model

Sessions With SAM (S1E2): Cognito and HTTP API

Testable Lambda: Working Effectively with Legacy Lambda