CLOVER🍀

That was when it all began.

LocalStackの提供するAWS SAMを使って、LocalStackにAmazon API Gateway+AWS Lambdaをデプロイしてみる

これは、なにをしたくて書いたもの?

前に、LocalStackの提供するAWS CLIを使ってLocalStackを操作してみました。

LocalStackの提供するAWS CLIを使って、LocalStackを操作する - CLOVER🍀

こちらと類似のもので、LocalStackの提供するAWS SAM(aws-sam-cli-local)もあったので、こちらを試してみることに
しました。

GitHub - localstack/aws-sam-cli-local: Simple wrapper around AWS SAM CLI for use with LocalStack

これはLocalStackの提供するAWS CLIawslocal)と似たようなもので、エンドポイントをLocalStackに向けた
AWS SAMのラッパーです。

環境

今回の環境は、こちら。

$ localstack --version
0.12.17.5


$ python3 -V
Python 3.8.10


$ aws --version
aws-cli/2.2.35 Python/3.8.8 Linux/5.4.0-81-generic exe/x86_64.ubuntu.20 prompt/off


$ awslocal --version
aws-cli/2.2.35 Python/3.8.8 Linux/5.4.0-81-generic exe/x86_64.ubuntu.20 prompt/off

LocalStack自体のインストールは割愛します。AWS Lambdaの実行には、docker-reuseを指定して起動。

$ LAMBDA_EXECUTOR=docker-reuse localstack start

aws-sam-cli-localをインストールして、Amazon API GatewayAWS Lambdaを動かしてみる

まずはaws-sam-cli-localをインストール。

$ pip3 install aws-sam-cli-local==1.1.0.1

AWS SAMのラッパーなのですが、aws-sam-cli-localの依存関係に含まれているので明示的にインストールしなくても
大丈夫です。自分でインストールしていても問題ありません。

バージョン。

$ samlocal --version
SAM CLI, version 1.30.0

では、こちらに沿ってAmazon API GatewayAWS LambdaのHello Worldアプリケーションを作成して、LocalStackに
デプロイしてみましょう。

チュートリアル: Hello World アプリケーションのデプロイ - AWS Serverless Application Model

init。

$ samlocal init

アプリケーション名はsam-app、ランタイムはPython 3.8とします。

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

生成されたディレクトリに移動。

$ cd sam-app

こんな感じにディレクトリツリーができています。

$ tree
.
├── README.md
├── __init__.py
├── events
│   └── event.json
├── hello_world
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── template.yaml
└── tests
    ├── __init__.py
    ├── integration
    │   ├── __init__.py
    │   └── test_api_gateway.py
    ├── requirements.txt
    └── unit
        ├── __init__.py
        └── test_handler.py

5 directories, 13 files

Lambda関数。

hello_world/app.py

import json

# import requests


def lambda_handler(event, context):
    """Sample pure Lambda function

    Parameters
    ----------
    event: dict, required
        API Gateway Lambda Proxy Input Format

        Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format

    context: object, required
        Lambda Context runtime methods and attributes

        Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html

    Returns
    ------
    API Gateway Lambda Proxy Output Format: dict

        Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
    """

    # try:
    #     ip = requests.get("http://checkip.amazonaws.com/")
    # except requests.RequestException as e:
    #     # Send some context about this error to Lambda Logs
    #     print(e)

    #     raise e

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
            # "location": ip.text.replace("\n", "")
        }),
    }

Amazon API Gatewayを通したレスポンス形式に沿っていますね。

テンプレート。

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.8
      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

ビルド。

$ samlocal build

次はデプロイなのですが、今回はS3バケットを先に作成しておきます。

$ awslocal s3 mb s3://my-bucket

デプロイ。AWS CloudFormationのスタック名は任意の名称で。--regionを明示しているのはAWS_REGION環境変数
設定していないからのようです。aws configureもしていないのですが、実施していたらよいのかもしれませんね。
--s3-bucketでは、先ほど作成したS3バケットを指定。

$ samlocal deploy --stack-name my-stack --region us-east-1 --s3-bucket my-bucket

実行結果。

CloudFormation outputs from deployed stack
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                                        
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 HelloWorldApi                                                                                                                                              
Description         API Gateway endpoint URL for Prod stage for Hello World function                                                                                           
Value               https://uvpkw1p552.execute-api.us-east-1.amazonaws.com/Prod/hello/                                                                                         

Key                 HelloWorldFunction                                                                                                                                         
Description         Hello World Lambda Function ARN                                                                                                                            
Value               arn:aws:lambda:us-east-1:000000000000:function:my-stack-HelloWorldFunction-23bdfa27                                                                        

Key                 HelloWorldFunctionIamRole                                                                                                                                  
Description         Implicit IAM Role created for Hello World function                                                                                                         
Value               arn:aws:iam::000000000000:role/cf-my-stack-HelloWorldFunctionRole                                                                                          
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

あっさり作成されました。これで、LocalStack上にデプロイされています。

Amazon API Gatewayのエンドポイント、AWS LambdaのARN、IAMロールのARNが得られます。

awslocalでも、作成されたAmazon API GatewayREST APIのidを確認。

$ awslocal apigateway get-rest-apis

REST APIのidは環境変数に保存しておきましょう。

$ REST_API_ID=.....

ステージは、Prod以外にもStageがあるようです。

$ awslocal apigateway get-stages --rest-api-id $REST_API_ID
{
    "item": [
        {
            "deploymentId": ".....",
            "stageName": "Stage",
            "description": "",
            "cacheClusterEnabled": false,
            "methodSettings": {},
            "variables": {}
        },
        {
            "deploymentId": ".....",
            "stageName": "Prod",
            "description": "",
            "cacheClusterEnabled": false,
            "methodSettings": {},
            "variables": {},
            "tags": {}
        }
    ]
}

LocalStack上にデプロイされたLambda関数を、Amazon API Gateway越しに呼び出してみます。

$ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/hello
{"message": "hello world"}

LocalStackで動作させた時のAmazon API GatewayのURLのルールは、こちらに記載があります。

LocalStack / Invoking API Gateway

使えそうですね。あっさり確認OKです。

動作確認というと、sam local invokeをしてみては?という話もあるのですが、これをやるんだったら素のAWS SAMを
そのまま使えばよいのではと思うので書きません。
※動かしはしました…というか、動かした瞬間に「そのまま使うのと同じでは」と思いました…

aws-sam-cli-localの仕組み

最初に書いた通り、aws-sam-cli-localAWS SAMのラッパーです。

aws-sam-cli-localPythonスクリプト内で、エンドポイントをLocalStack向けに設定してsamコマンドを
呼び出しているものです。

https://github.com/localstack/aws-sam-cli-local/blob/64a41c2c8e46a584c6fb4b13abfc8bb62e136d48/bin/samlocal

ハマったところ

実は、ひとつだけハマったところがありまして。

sam buildした後に、sam deploy --guidedをサジェストされるのですが。

こちらに素直に従うと

$ samlocal deploy --guided --region us-east-1

S3バケット名が解決できなくてエラーになります。

Error: Unable to upload artifact HelloWorldFunction referenced by CodeUri parameter of HelloWorldFunction resource.
Parameter validation failed:
Invalid bucket name "!Ref SamCliSourceBucket": Bucket name must match the regex "^[a-zA-Z0-9.\-_]{1,255}$" or be an ARN matching the regex "^arn:(aws).*:(s3|s3-object-lambda):[a-z\-0-9]*:[0-9]{12}:accesspoint[/:][a-zA-Z0-9\-.]{1,63}$|^arn:(aws).*:s3-outposts:[a-z\-0-9]+:[0-9]{12}:outpost[/:][a-zA-Z0-9\-]{1,63}[/:]accesspoint[/:][a-zA-Z0-9\-]{1,63}$"

これを回避したくて、先にS3バケットを作成したのでした…。