cloudformation(SAM)でapi-gateway,lambda を生成する

cloudformation(SAM)でapi-gateway,lambda を生成する

2023/11/05 21:00:00
Program
Ubuntu, Aws

前提 #

awsリソース構成 #

以下の通りのサービス構成とする

作成手順 #

  1. プロジェクトを作成する
    sam cli でプロジェクトを作成する。これによりプロジェクトディレクトリが作成されアプリのベースとなる
  2. get/postのapiを追加する
    samconfig.toml を作成する
  3. CORS 対応
  4. api 共通ロジックをlayerを追加し対応する
  5. デプロイ
    template.yml, samconfig.toml を使用してaws sam cli でデプロイを実行する

プロジェクトを作成する #

作業エリアを作成しsamプロジェクトを作成する

  • python 3.11.5 を使ってアプリを作成する
  • 念のため python 3.11.5 の依存する sam をインストールしておく
# 作業エリア作成
$ mkdir test
$ cd test

# python 3.11.5 インストール&設定
$ pyenv install 3.11.5
$ pyenv local 3.11.5

# sam インストール(python3.11.5 に sam がパッケージとしてインストールされるので注意。python3.11.5以外は使えない)
$ pip install aws-sam-cli
# sam バージョン確認
$ sam --version
SAM CLI, version 1.97.0

# sam アプリベース作成
$ sam init --runtime=python3.11
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 - Hello World Example With Powertools for AWS Lambda
        3 - Infrastructure event management
        4 - Multi-step workflow
        5 - Lambda EFS example
        6 - Serverless Connector Hello World Example
        7 - Multi-step workflow with Connectors
Template: 1

Based on your selections, the only Package type available is Zip.
We will proceed to selecting the Package type as Zip.

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

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/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]: # 追加料金が必要らしいのでやめ

Project name [sam-app]: apitest-app

Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment)

    -----------------------
    Generating application:
    -----------------------
    Name: apitest-app
    Runtime: python3.11
    Architectures: x86_64
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .
    Configuration file: apitest-app/samconfig.toml

    Next steps can be found in the README file at apitest-app/README.md


Commands you can use next
=========================
[*] Create pipeline: cd apitest-app && sam pipeline init --bootstrap
[*] Validate SAM template: cd apitest-app && sam validate
[*] Test Function in the Cloud: cd apitest-app && sam sync --stack-name {stack-name} --watch

作成された apitest-app プロジェクト配下の階層を確認しておく

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

6 directories, 14 files

デフォルトでは、hello_world が作成されるので、その api が動作するか確認しておく

$ cd apitest-app
# sam 実行
#  初回はdocker imageをダウンロードしてくるので遅い。。。
$ sam local start-api
Initializing the lambda functions containers.
Local image is out of date and will be updated to the latest runtime. To skip this, pass in the parameter --skip-pull-image
Building image..............................................................................................................................................................................................................................
Using local image: public.ecr.aws/lambda/python:3.11-rapid-x86_64.

Mounting /home/developer/test/apitest-app/hello_world as /var/task:ro,delegated, inside runtime container
Containers Initialization is done.
Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your
functions, changes will be reflected instantly/automatically. If you used sam build before running local commands, you will need to re-run
sam build for the changes to be picked up. You only need to restart SAM CLI if you update your AWS SAM template
2023-11-06 14:00:33 WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:3000
2023-11-06 14:00:33 Press CTRL+C to quit

# 別ターミナルから以下を実施
$ curl http://127.0.0.1:3000/hello 
{"message": "hello world"}

get/postのapiを追加する #

以下の手順で追加する

  • git 管理開始
  • GET api 追加
  • POST api 追加
  • helloworld は邪魔なので破棄

git 管理開始 #

メモ:

$ cd apitest-app
# このディレクトリ配下は python 3.11.5 を使うことを明示(venv使うために必要)
$ pyenv local 3.11.5
$ git init
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint:   git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint:   git branch -m <name>
# 上記のようなヒント表示されるが無視しておくことにする。

# git 登録(.gitignore は sam が初期設定したものが生成済みである)
$ git add .
$ git status
On branch master
No commits yet
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   .gitignore
        new file:   README.md
        new file:   __init__.py
        new file:   events/event.json
        new file:   hello_world/__init__.py
        new file:   hello_world/app.py
        new file:   hello_world/requirements.txt
        new file:   samconfig.toml
        new file:   template.yaml
        new file:   tests/__init__.py
        new file:   tests/integration/__init__.py
        new file:   tests/integration/test_api_gateway.py
        new file:   tests/requirements.txt
        new file:   tests/unit/__init__.py
        new file:   tests/unit/test_handler.py

$ git commit -m "first commit"

GET api 追加 #

hello_world ディレクトリをコピーして GET 用 api を追加

$ cd apitest-app
$ cp -r hello_world my_get_function

my_get_function/app.py を message 節を get と分かるように変更しておく

$ my_get_function/app.py

...
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "get api",     # <-- ここ変更しただけ
            # "location": ip.text.replace("\n", "")
        }),
    }

template.yaml に my_get_function を追加しておく

$ emacs template.yaml

# Resources 節に追加
Resources:
  # ↓ここから追加
  MyGetFunction:
    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: my_get_function/
      Handler: app.lambda_handler
      Runtime: python3.11
      Architectures:
        - x86_64
      Events:
        MyGet:
          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: /myget
            Method: get
  # ↑まで追加
  ...
  # 最下部にある Outputs 節にも追加したならしていい。ログが出力されるだけなので追加しなくてもいい
  Outputs:
    # ↓ここから追加
    MyGetApi:
      Description: "API Gateway endpoint URL for Prod stage for my get function"
      Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/myget/"
    MyGetFunction:
      Description: "My Get Lambda Function ARN"
      Value: !GetAtt MyGetFunction.Arn
    MyGetFunctionIamRole:
      Description: "Implicit IAM Role created for My Get function"
      Value: !GetAtt MyGetFunctionRole.Arn
    # ↑まで追加

これで一度、myget が動作するか確認しておく

# api スタート
$ sam local start-api

# 別ターミナルから以下を実施
$ curl http://127.0.0.1:3000/myget
{"message": "get api"}

メモ:template.yaml の差分

$ git diff –cached template.yaml

diff --git a/template.yaml b/template.yaml
index a216784..692648d 100644
--- a/template.yaml
+++ b/template.yaml
@@ -26,6 +26,21 @@ Resources:
             Path: /hello
             Method: get

+  MyGetFunction:
+    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: my_get_function/
+      Handler: app.lambda_handler
+      Runtime: python3.11
+      Architectures:
+        - x86_64
+      Events:
+        MyGet:
+          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: /myget
+            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
@@ -39,3 +54,14 @@ Outputs:
   HelloWorldFunctionIamRole:
     Description: "Implicit IAM Role created for Hello World function"
     Value: !GetAtt HelloWorldFunctionRole.Arn
+
+  MyGetApi:
+    Description: "API Gateway endpoint URL for Prod stage for my get function"
+    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/myget/"
+  MyGetFunction:
+    Description: "My Get Lambda Function ARN"
+    Value: !GetAtt MyGetFunction.Arn
+  MyGetFunctionIamRole:
+    Description: "Implicit IAM Role created for My Get function"
+    Value: !GetAtt MyGetFunctionRole.Arn
+

問題なく動作したらgitに登録

$ git add .
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   my_get_function/__init__.py
        new file:   my_get_function/app.py
        new file:   my_get_function/requirements.txt
        modified:   template.yaml
$ git commit -m "add GET method api(myget)"

POST api 追加 #

GET と同じ方法で追加する

$ cd apitest-app
$ cp -r hello_world my_post_function

my_post_function/app.py を message 節を get と分かるように変更しておく

$ my_post_function/app.py

...
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "post api",     # <-- ここ変更しただけ
            # "location": ip.text.replace("\n", "")
        }),
    }

template.yaml に my_post_function を追加しておく

$ emacs template.yaml

# Resources 節に追加
Resources:
  # ↓ここから追加
  MyPostFunction:
    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: my_post_function/
      Handler: app.lambda_handler
      Runtime: python3.11
      Architectures:
        - x86_64
      Events:
        MyGet:
          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: /mypost
            Method: post
  # ↑まで追加
  ...
  # 最下部にある Outputs 節にも追加したならしていい。ログが出力されるだけなので追加しなくてもいい
  Outputs:
    # ↓ここから追加
    MyPostApi:
      Description: "API Gateway endpoint URL for Prod stage for my post function"
      Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/mypost/"
    MyGetFunction:
      Description: "My Post Lambda Function ARN"
      Value: !GetAtt MyPostFunction.Arn
    MyGetFunctionIamRole:
      Description: "Implicit IAM Role created for My Post function"
      Value: !GetAtt MyPostFunctionRole.Arn
    # ↑まで追加

これで一度、mypost が動作するか確認しておく

# api スタート
$ sam local start-api

# 別ターミナルから以下を実施
$ curl -X POST http://127.0.0.1:3000/mypost
{"message": "post api"}

問題なく動作したらgitに登録

$ git add .
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   my_get_function/__init__.py
        new file:   my_get_function/app.py
        new file:   my_get_function/requirements.txt
        modified:   template.yaml
$ git commit -m "add POST method api(mypost)"

メモ:template.yaml の差分

$ git diff –cached template.yaml

diff --git a/template.yaml b/template.yaml
index 1b1c5ec..617555e 100644
--- a/template.yaml
+++ b/template.yaml
@@ -41,6 +41,21 @@ Resources:
             Path: /myget
             Method: get

+  MyPostFunction:
+    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: my_post_function/
+      Handler: app.lambda_handler
+      Runtime: python3.11
+      Architectures:
+        - x86_64
+      Events:
+        MyGet:
+          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: /mypost
+            Method: post
+
 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
@@ -65,3 +80,12 @@ Outputs:
     Description: "Implicit IAM Role created for My Get function"
     Value: !GetAtt MyGetFunctionRole.Arn

+  MyPostApi:
+    Description: "API Gateway endpoint URL for Prod stage for my post function"
+    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/mypost/"
+  MyPostFunction:
+    Description: "My Post Lambda Function ARN"
+    Value: !GetAtt MyPostFunction.Arn
+  MyPostFunctionIamRole:
+    Description: "Implicit IAM Role created for My Post function"
+    Value: !GetAtt MyPostFunctionRole.Arn

問題なく動作したらgitに登録

$ git add .
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   my_post_function/__init__.py
        new file:   my_post_function/app.py
        new file:   my_post_function/requirements.txt
        modified:   template.yaml
$ git commit -m "add POST method api(mypost)"

helloworld は邪魔なので破棄 #

hello_world メソッドロジックを削除

$ cd apitest-app
$ rm -rf hello_world

template.yaml からも hello_world 破棄

$ git diff –cached template.yaml

diff --git a/template.yaml b/template.yaml
index 617555e..b76360f 100644
--- a/template.yaml
+++ b/template.yaml
@@ -11,21 +11,6 @@ Globals:
     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.11
-      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
-
   MyGetFunction:
     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:
     
 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
     

hello_world 破棄をgitの登録

$ git add .
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        deleted:    hello_world/__init__.py
        deleted:    hello_world/app.py
        deleted:    hello_world/requirements.txt
        modified:   template.yaml
$ git commit -m "remove hello_world api"

CORS 対応 #

ブラウザでアクセスするとCORS問題が発生するはずなので、CORS対応を追加する
CORS関連を完全に理解しているわけではないので、今後色々なパターンで実験する必要がある
apple系のOSだとブラウザのセキュリティ要件が厳しいはずなので追加対応が必要になる可能性がある

local.html を作成し CORS が発生することを確認 #

まずは、local.html を作成し、myget, mypost api にアクセスしてみる
api は単純リクエストと非単純リクエストの両方をそれぞれ試す。

クライアント(html)の保存先作成

$ cd apitest-app
$ mkdir client

$ emacs client/local.html

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>Local</title>
  </head>
  <body>
    <h1>Local</h1>
    <button type="button" class="btn btn-primary" onclick="GetSimpleRequest()">GET単純リクエスト</button>
    <button type="button" class="btn btn-primary" onclick="GetNonSimpleRequest()">GET非単純リクエスト</button>
    <br>
    <button type="button" class="btn btn-warning" onclick="PostSimpleRequest()">POST単純リクエスト</button>
    <button type="button" class="btn btn-warning" onclick="PostNonSimpleRequest()">POST非単純リクエスト</button>

    <script>
    // GET 単純リクエスト
    function GetSimpleRequest() {
        axios({
            method: 'get',
            url: 'http://127.0.0.1:3000/myget',
            headers: {
                'Content-Type': 'text/plain'
            },
        })
        .then(function (response) {
           console.log(response);
        })
        .catch(function (error) {
           console.log(error);
        });
     }
    // GET 非単純リクエスト
    function GetNonSimpleRequest() {
        axios({
            method: 'get',
            url: 'http://127.0.0.1:3000/myget',
            headers: {
                'Content-Type': 'application/json'
            },
            data: {
                message: 'Hello, world!'
            }
        })
        .then(function (response) {
            console.log(response);
        })
        .catch(function (error) {
            console.log(error);
        });
     }

    // POST 単純リクエスト
    function PostSimpleRequest() {
        axios({
            method: 'post',
            url: 'http://127.0.0.1:3000/mypost',
            headers: {
                'Content-Type': 'text/plain'
            },
            data: 'Hello, world!'
        })
        .then(function (response) {
            console.log(response);
        })
        .catch(function (error) {
            console.log(error);
        });
         // alert("Simple");
     }
    // POST 非単純リクエスト
    function PostNonSimpleRequest() {
        axios({
            method: 'post',
            url: 'http://127.0.0.1:3000/mypost',
            headers: {
                'Content-Type': 'application/json'
            },
            data: {
                message: 'Hello, world!'
            }
        })
        .then(function (response) {
            console.log(response);
        })
        .catch(function (error) {
            console.log(error);
        });
         // alert("NonSimple");
     }
    </script>

    <!-- Optional JavaScript; choose one of the two! -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

    <!-- Option 1: Bootstrap Bundle with Popper -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>

    <!-- Option 2: Separate Popper and Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
  </body>
</html>

上記の local.html を ubuntu google-chrome で起動して確認する
※windows host の chrome を使用するとネットワーク空間がwsl2と違うためlocalhostが正しく処理されない可能性がある
 また、wsl2.0.0 の新機能を使えばネットワーク空間を共有してlocalhostにアクセスできる気もする…ようわからん

$ google-chrome client/local.html

プリフライトがあろうがなかろうが、以下の通り、CORS エラーが発生する

CORS エラー確認とれたら git に登録しておく

$ git add .
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   client/local.html
$ git commit -m "add client/local.html"

CORS 対応をしエラーが発生しないことを確認する #

各apiのProperties節にapi-gatewayでのCORS対応情報(RestApiID)を追加し対応する
また、各apiのレスポンス情報にCORS対応情報を追記する

メモ:

  • “Access-Control-Allow-Methods” や “Access-Control-Allow-Headers” の影響範囲がいまいち想定と違う感じがする
  • https 対応時にはさらに追加が必要やったりするかも

$ emacs template.yaml

Resources:
  MyGetFunction:
    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: my_get_function/
      Handler: app.lambda_handler
      Runtime: python3.11
      Architectures:
        - x86_64
      Events:
        MyGet:
          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: /myget
            Method: get
            RestApiId: # <--- ここに MyApi 参照追加
              Ref: MyApi

  MyPostFunction:
    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: my_post_function/
      Handler: app.lambda_handler
      Runtime: python3.11
      Architectures:
        - x86_64
      Events:
        MyGet:
          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: /mypost
            Method: post
            RestApiId: # <--- ここに MyApi 参照追加
              Ref: MyApi

  MyApi: # <-- ここ追加
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      Cors:
        AllowOrigin: "'*'"
        AllowMethods: "'OPTIONS,GET,POST'"
        AllowHeaders: "'Content-Type'"

各api のレスポンスにCORS対応情報を追加する

$emacs my_get_function/app.py

    res = { # <-- レスポンスにCORS対応情報追記
        "statusCode": 200,
        "headers": {
            "Access-Control-Allow-Origin": "*",  # ここを適切なオリジンに設定
            "Access-Control-Allow-Methods": "GET,OPTIONS",  # 許可するメソッドを設定
            "Access-Control-Allow-Headers": "Content-Type"  # 許可するヘッダーを設定
        },
        "body": json.dumps({
            "message": "get api",
            # "location": ip.text.replace("\n", "")
        }),
    }
    return res

$emacs my_post_function/app.py

...
    res = { # <-- レスポンスにCORS対応情報追記
        "statusCode": 200,
        "headers": {
            "Access-Control-Allow-Origin": "*",  # ここを適切なオリジンに設定
            "Access-Control-Allow-Methods": "POST,OPTIONS",  # 許可するメソッドを設定
            "Access-Control-Allow-Headers": "Content-Type"  # 許可するヘッダーを設定
        },
        "body": json.dumps({
            "message": "post api",
            # "location": ip.text.replace("\n", "")
        }),
    }
    return res

上記対応後、同じ方法で確認する

$ sam local start-api

# 別ターミナルから以下を実施
$ google-chrome client/local.html

以下の通り、単純リクエスト/非単純リクエストともに問題なくアクセスできるようになった

CORS 問題が解決したら git に登録しておく

$ git add .
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   my_get_function/app.py
        modified:   my_post_function/app.py
        modified:   template.yaml
$ git commit -m "add CORS"

layer(共通ロジック)を追加 #

共通ロジックはDBアクセスを想定してDB情報を取得できるようにする
ただし、実際にはDBには接続しない。理由は、RDSProxy/RDS 等のサービス起動が必要なため今回はやめておく。サービス起動したい場合は、別途 cfn で対応すればいい。

共通ロジックを作成 #

my_layer ディレクトリを作成して、DB情報管理する db_info.py を作成する
なお、python の場合は、レイヤーディレクトリにpythonディレクトリが必要である

$ mkdir -p my_layer/python

$ emacs my_layer/python/db_info.py

class DBInfo():
    def __init__(self):
        super().__init__()
        self.connections = {}
        self.connections['host'] = 'locahost'
        self.connections['port'] = 8081
        self.connections['dbname'] = 'dbname'
        self.connections['user'] = 'user'
        self.connections['password'] = 'password'

my_layer を template.yaml に追加 #

この追加をすることで各apiからmy_layer内のロジックにアクセスできるようになる
ただし、ここからは、sam build を実施しないと、layer が各apiから呼び出すことができないので注意
$ sam local start-api だけの場合、build は実施されずに、実施したパスで動作が開始する。これは sam 仕様である。 通常は $ sam build, $ sam local xxx としたほうがいいが、ホットリロードを編集中ソースにも影響させたい場合は build しないようにしてもいい。 この場合は、各apiディレクトリ内に必要はパッケージを手動で配置する必要がある。
理解できない場合は、素直に sam build を実施するべき

  • ※build後、各apiやmy_layerがどのような構成になっているかを確認したいなら、プロジェクト直下の .aws-sam/build 内を参照すれば理解できる。ただし、layer だけは特別な仕組みで各apiからの参照設定がされている様子

$ emacs template.yaml

...
Resources:
  MyGetFunction:
    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: my_get_function/
      Handler: app.lambda_handler
      Runtime: python3.11
      Architectures:
        - x86_64
      Events:
        MyGet:
          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: /myget
            Method: get
            RestApiId:
              Ref: MyApi
      Layers: # <-- ここに layer 参照追加
        - !Ref MyLayer

  MyPostFunction:
    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: my_post_function/
      Handler: app.lambda_handler
      Runtime: python3.11
      Architectures:
        - x86_64
      Events:
        MyGet:
          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: /mypost
            Method: post
            RestApiId:
              Ref: MyApi
      Layers: # <-- ここに layer 参照追加
        - !Ref MyLayer

  MyLayer: # <-- ここに layer 設定追加
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: my_layer
      Description: My layer description
      ContentUri: my_layer/
      CompatibleRuntimes:
        - python3.11
    Metadata:
      BuildMethod: python3.11
...

my_layer を 各apiから呼び出す #

$ emacs my_get_function/app.py

from db_info import DBInfo # <--- layer にあるクラス参照追記
...
    dbinfo = DBInfo() # <--- layer にあるクラス生成

    res = {
        "statusCode": 200,
        "headers": {
            "Access-Control-Allow-Origin": "*",  # ここを適切なオリジンに設定
            "Access-Control-Allow-Methods": "GET,OPTIONS",  # 許可するメソッドを設定
            "Access-Control-Allow-Headers": "Content-Type"  # 許可するヘッダーを設定
        },
        "body": json.dumps({
            "message": "get api(host:{}/port:{}/)".format(dbinfo.connections['host'], dbinfo.connections['port']), # <--- db情報参照
            # "location": ip.text.replace("\n", "")
        }),
    }
    return res

$ emacs my_post_function/app.py

from db_info import DBInfo # <--- layer にあるクラス参照追記
...
    dbinfo = DBInfo() # <--- layer にあるクラス生成

    res = {
        "statusCode": 200,
        "headers": {
            "Access-Control-Allow-Origin": "*",  # ここを適切なオリジンに設定
            "Access-Control-Allow-Methods": "POST,OPTIONS",  # 許可するメソッドを設定
            "Access-Control-Allow-Headers": "Content-Type"  # 許可するヘッダーを設定
        },
        "body": json.dumps({
            "message": "post api(host:{}/port:{}/)".format(dbinfo.connections['host'], dbinfo.connections['port']), # <--- db情報参照
            # "location": ip.text.replace("\n", "")
        }),
    }
    return res

実行しdb参照できるか確認する

# layer を参照するために build が必要(か、手動で各apiに配置する必要がある)
$ sam build
# 実行
$ sam local start-api
Initializing the lambda functions containers.
MyLayer is a local Layer in the template            <--- layer追加するとこのログが出力される

# 別ターミナルから以下を実施
$ google-chrome client/local.html

以下の通り、myget,mypostともにDB情報が参照できてる


layerが期待通り動作したら git に登録しておく

# sam build を実施すると直下に.aws-sam ディレクトリが生成されるので.gitignoreに追記しておく
$ emacs .gitingore
...
.aws-sam/  <-- 追記

# 登録
$ git add .
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   .gitignore
        modified:   my_get_function/app.py
        new file:   my_layer/python/db_info.py
        modified:   my_post_function/app.py
        modified:   template.yaml
$ git commit -m "add layer"

デプロイ #

デプロイ前には、samconfig.toml に必要事項の記載必要
具体的には以下の項目の追記が必要らしい

  • s3_prefix
    sam deploy 時の変更差分をs3で管理しているらしく、それの識別として必要な項目らしい 通常はアプリ名の “apitest-app” としておく
  • region
    リージョン
    デフォルトは “ap-northeast-1” となっている様子

以下はオプション

  • profile aws config に記載したデプロイ先のプロファイル名
  • image_repositories ようわからん。。。デフォルトは [] になってる

ここでは、手動追記はやめて sam deploy に –guided オプションを追加して対応する
こうしておけば、必須入力項目の変更があった場合にも気づける気がするから。。。

初回デプロイ(失敗する) #

–guided オプションを付加してデプロイを実施する
–profile オプションを指定しているのは複数のawsアカウント思っていて XXX 環境にデプロイしたいために指定している

$ sam deploy --guided --profile XXX

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

        Looking for config file [samconfig.toml] :  Found
        Reading default arguments  :  Success

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [apitest-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]:
        ## SAM CLIがAWS Identity and Access Management (IAM) ロールを作成することを許可するかどうか
        #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’を入力すると、操作が失敗した場合でも以前にプロビジョニングされたリソースの状態が保持される
        ## 'N’を入力すると(これがデフォルトです)、操作が失敗した場合にはロールバックが行われ、変更が元に戻される
        #Preserves the state of previously provisioned resources when an operation fails
        Disable rollback [y/N]:
        ## 認証を持っていないことを指摘し、それが問題ないかどうかを尋ねている
        ## 'y’を入力すると、認証なしでHelloWorldFunctionをデプロイすることを許可
        MyGetFunction has no authentication. Is this okay? [y/N]: y
        MyPostFunction has no authentication. Is this okay? [y/N]: y
        ## 保存確認
        Save arguments to configuration file [Y/n]:
        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-1eli41thgg4jk
        A different default S3 bucket can be set in samconfig.toml and auto resolution of buckets turned off by setting resolve_s3=False

        Parameter "stack_name=apitest-app" in [default.deploy.parameters] is defined as a global parameter [default.global.parameters].
        This parameter will be only saved under [default.global.parameters] in /home/developer/test/apitest-app/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 apitest-app/d006400a8973740f15f568f9fd522821  547413 / 547413  (100.00%)
        Uploading to apitest-app/548c139a208e531d252a6f1827bd30e7  547414 / 547414  (100.00%)
        Uploading to apitest-app/a31e2576d4db1b6dcebe12cd4840c3ae  270 / 270  (100.00%)

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

Initiating deployment
=====================

        Uploading to apitest-app/9a3bcae16046c5ec82be9d9bd2aaa848.template  3113 / 3113  (100.00%)


Waiting for changeset to be created..

Error: Failed to create changeset for the stack: apitest-app, ex: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state: For expression "Status" we matched expected path: "FAILED" Status: FAILED. Reason: Unresolved resource dependencies [ServerlessRestApi] in the Outputs block of the template

上記の通り、デプロイが失敗する。
このエラーは Outputs 節で ${ServerlessRestApi} が参照できなくなっているため。
原因は MyApi 節を追加したことでデフォルトの ${ServerlessRestApi} が参照できなくなってるらしい。(暗黙のルールで決まってる)
なので、以下の修正を加えて再度デプロイを実施することにする
また、–guided オプションを付加してデプロイを実施するとsamconfig.tomlに以下の項目が追記されるので確認しておくこと

...
[default.deploy.parameters]
capabilities = "CAPABILITY_IAM"
confirm_changeset = true
resolve_s3 = true
s3_prefix = "apitest-app"   <--- ここ追加
region = "ap-northeast-1"   <--- ここ追加
profile = "XXX"             <--- ここ追加
image_repositories = []     <--- ここ追加
...

※ –profile オプションで指定した XXX プロファイル名も自動保持されているので、次回以降のsam deployコマンドは – profile オプション指定は不要であることがわかる。

メモ:

  • aws cloudformation  初回デプロイを実施すると aws cloudformation に以下のような aws-sam-cli-managed-defualt スタックが生成される
    以後、sam delop を実施すると、このスタックで変更差分を管理(s3バケットで管理)することになる
    (もちろん、今回追加した apitest-app というアプリ用のスタックも別途生成されるので注意)

template.yaml を修正 #

Outputs 節で ${ServerlessRestApi} を ${MyApi} に変更する

$ emacs template.yaml

   # Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/myget/"
   Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/myget/"
   ...  
   # Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/mypost/"
   Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/mypost/"
   ...

再度、sam deploy を実施 #

# なぜか template.yaml を指定しておかないといつまでもエラーが消えない様子。古い template.yaml をどこかで覚えている?
$ sam deploy -t template.yaml
...
Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y    # <--- 反映していいか聞いてきているので Y
...
CloudFormation outputs from deployed stack
---------------------------------------------------------------------------------------------------------------------------------------------
Outputs
---------------------------------------------------------------------------------------------------------------------------------------------
Key                 MyGetFunction
Description         My Get Lambda Function ARN
Value               arn:aws:lambda:ap-northeast-1:9999999999999:function:apitest-app-MyGetFunction-zzzz1

Key                 MyPostFunctionIamRole
Description         Implicit IAM Role created for My Post function
Value               arn:aws:iam::9999999999999:role/apitest-app-MyPostFunctionRole-zzzz2

Key                 MyPostApi
Description         API Gateway endpoint URL for Prod stage for my post function
Value               https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/mypost/

Key                 MyGetApi
Description         API Gateway endpoint URL for Prod stage for my get function
Value               https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/myget/

Key                 MyPostFunction
Description         My Post Lambda Function ARN
Value               arn:aws:lambda:ap-northeast-1:9999999999999:function:apitest-app-MyPostFunction-zzzz3

Key                 MyGetFunctionIamRole
Description         Implicit IAM Role created for My Get function
Value               arn:aws:iam::9999999999999:role/apitest-app-MyGetFunctionRole-zzzz4
---------------------------------------------------------------------------------------------------------------------------------------------

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

上記通りデプロイ成功する
また、Outputs節に指定したログから、api接続先情報を得ることができる

https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod

デプロイ成功したら git に登録

$ git add .
    $ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   samconfig.toml
        modified:   template.yaml
$ git commit -m "add deploy paramters"

aws にアクセスして動作確認する #

作成した local.html をコピーして aws.html としてapi接続先を変更する
api 接続先は、Outputs節で指定したログがデプロイ時に表示されているのでそれを使う。

https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod

$ cp client/local.html client/aws.html

emacs client/aws.html で以下の変更を実施

  • 「local」 を 「aws」 に変更
  • 「http://127.0.0.1:3000」 を 「https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod」 に変更

google-chrome で client/aws.html を起動して動作確認を行う。

$ google-chrome client/aws.html

以下の通り、すべてのリクエストが正常の完了することが確認できる。


正常動作したら git に登録

$ git add .
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   client/aws.html
$ git commit -m "add client/aws.html"

これで作成手順すべて完了。
以下にアプリディレクトリ階層の一覧を示しておく。

$ tree .
.
├── README.md
├── __init__.py
├── client
│   ├── aws.html
│   └── local.html
├── events
│   └── event.json
├── my_get_function
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── my_layer
│   └── python
│       └── db_info.py
├── my_post_function
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── samconfig.toml
├── template.yaml
└── tests
    ├── __init__.py
    ├── integration
    │   ├── __init__.py
    │   └── test_api_gateway.py
    ├── requirements.txt
    └── unit
        ├── __init__.py
        └── test_handler.py

9 directories, 20 files

その他 #

samconfig.toml に外部パラメータを追加する #

外部参照する情報は samconfig.toml に記載し、ここでは myget, mypost api に外部参照した情報をレスポンスとして返すようにする。
外部参照情報は以下の通り。
※文字列と数値をそれぞれ実験する

  • UserName = ’testman'
  • UserType = 1000

samconfig.toml に外部パラメータを追加する
※今回は default.global.parameters に追記しているが、他の項目に追加してもらっても問題ない

$ emacs samconfig.toml

...
[default.global.parameters]
stack_name = "apitest-app"
parameter_overrides = [
  "UserName=testman",
  "UserType=1111",
]
...

template.yaml に外部パラメータの参照設定を追加する

$ emacs template.yaml

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

  Sample SAM Template for apitest-app  

Parameters:  # <--- Paramters節を追記。外部参照パラメータを定義
  UserName:
    Type: String
  UserType:
    Type: Number
...
# myget/mypostに外部パラメータ参照定義追加
Resources:
  MyGetFunction:
    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: my_get_function/
      Handler: app.lambda_handler
      Runtime: python3.11
      Architectures:
        - x86_64
      Events:
        MyGet:
          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: /myget
            Method: get
            RestApiId:
              Ref: MyApi
      Environment:  # <--- Environment節を追記
        Variables:
          UserName: !Ref UserName
          UserType: !Ref UserType
      Layers:
        - !Ref MyLayer

  MyPostFunction:
    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: my_post_function/
      Handler: app.lambda_handler
      Runtime: python3.11
      Architectures:
        - x86_64
      Events:
        MyGet:
          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: /mypost
            Method: post
            RestApiId:
              Ref: MyApi
      Environment:  # <--- Environment節を追記
        Variables:
          UserName: !Ref UserName
          UserType: !Ref UserType
      Layers:
        - !Ref MyLayer

myget/mypost api に外部パラメータ参照を追加する

$ emacs my_get_function/app.py
$ emacs my_post_function/app.py

import os  # <--- 環境変数アクセスに必要
...
        "body": json.dumps({
            "message": "xxx api(host:{}/port:{}/)".format(dbinfo.connections['host'], dbinfo.connections['port']),
            "UserName": os.environ.get('UserName', "UserNameError"), # <-- ここに外部参照追加(UserName)
            "UserType": os.environ.get('UserType', 9999), # <-- ここに外部参照追加(UserType)
            # "location": ip.text.replace("\n", "")
        }),
...

上記設定で動作確認する
まずは、local.html で確認する

$ sam build -t template.yaml --config-file samconfig.toml
$ sam local start-api -t template.yaml --config-file samconfig.toml

# 別ターミナル
$ google-chrome client/local.html

以下の通り、レスポンスが期待通りとなることを確認できる。

次は aws.html で確認する

$ sam build -t template.yaml --config-file samconfig.toml
$ sam deploy -t template.yaml --config-fiele samconfig.toml
...
        Deploying with following values
        ===============================
        Stack name                   : apitest-app
        Region                       : ap-northeast-1
        Confirm changeset            : True
        Disable rollback             : False
        Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-1eli41thgg4jk
        Capabilities                 : ["CAPABILITY_IAM"]
        Parameter overrides          : {"UserName": "testman", "UserType": "1111"}  <--- ここに外部パラメータが表示される
        Signing Profiles             : {}
...
Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]:y <--- 変更があるので更新確認 y
...
Successfully created/updated stack - apitest-app in ap-northeast-1

# 上記、更新が成功した旨、表示されればOK。また接続先も変更されることはない
# 別ターミナル
$ google-chrome client/aws.html

以下の通り、レスポンスが期待通りとなることを確認できる。

sam deploy した内容を破棄する #

以下を実施することで、apitest-app スタックは削除される。
しかし、sam cli 用のスタック「aws-sam-cli-managed-default」が使用している s3バケット「apitest-app」 が破棄されないので注意
これは手動で削除するしかないらしい。謎仕様。。。

$ aws cloudformation delete-stack --stack-name apitest-app --profile XXX

破棄し再デプロイすると apigateway のエンドポイントURLが変更されるので、client/aws.html を改造してURL指定できるようにしておく

$ emacs client/aws.html

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>aws</title>
  </head>
  <body>
    <h1>aws</h1>
    <div>
      <label for="requestUrlInput" class="form-label">request url</label>
      <input type="text" class="form-control" id="requestUrlInput" placeholder="https://3k3wg0s3w4.execute-api.ap-northeast-1.amazonaws.com/Prod">
    </div>
    <button type="button" class="btn btn-primary" onclick="GetSimpleRequest()">GET単純リクエスト</button>
    <button type="button" class="btn btn-primary" onclick="GetNonSimpleRequest()">GET非単純リクエスト</button>
    <br>
    <button type="button" class="btn btn-warning" onclick="PostSimpleRequest()">POST単純リクエスト</button>
    <button type="button" class="btn btn-warning" onclick="PostNonSimpleRequest()">POST非単純リクエスト</button>

    <script>
    // ↓ここから追加
    let request_url = 'https://3k3wg0s3w4.execute-api.ap-northeast-1.amazonaws.com/Prod'
    // input要素を取得
    var inputElement = document.getElementById("requestUrlInput");
    inputElement.value = request_url
    // input要素の値が変化したときのイベントリスナーを設定
    inputElement.addEventListener("change", function() {
        // グローバル変数に新しい値を代入
        request_url = this.value;
    });
    // ↑ここまで追加。以下は request_url を参照するように変更している
    // GET 単純リクエスト
    function GetSimpleRequest() {
        axios({
            method: 'get',
            url: `${request_url}/myget`,
            headers: {
                'Content-Type': 'text/plain'
            },
        })
        .then(function (response) {
           console.log(response);
        })
        .catch(function (error) {
           console.log(error);
        });
     }
    // GET 非単純リクエスト
    function GetNonSimpleRequest() {
        axios({
            method: 'get',
            url: `${request_url}/myget`,
            headers: {
                'Content-Type': 'application/json'
            },
            data: {
                message: 'Hello, world!'
            }
        })
        .then(function (response) {
            console.log(response);
        })
        .catch(function (error) {
            console.log(error);
        });
     }

    // POST 単純リクエスト
    function PostSimpleRequest() {
        axios({
            method: 'post',
            url: `${request_url}/mypost`,
            headers: {
                'Content-Type': 'text/plain'
            },
            data: 'Hello, world!'
        })
        .then(function (response) {
            console.log(response);
        })
        .catch(function (error) {
            console.log(error);
        });
         // alert("Simple");
     }
    // POST 非単純リクエスト
    function PostNonSimpleRequest() {
        axios({
            method: 'post',
            url: `${request_url}/mypost`,
            headers: {
                'Content-Type': 'application/json'
            },
            data: {
                message: 'Hello, world!'
            }
        })
        .then(function (response) {
            console.log(response);
        })
        .catch(function (error) {
            console.log(error);
        });
         // alert("NonSimple");
     }
    </script>

    <!-- Optional JavaScript; choose one of the two! -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

    <!-- Option 1: Bootstrap Bundle with Popper -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>

    <!-- Option 2: Separate Popper and Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
  </body>
</html>

api gateway のエンドポイントタイプを変更する #

template.yaml に MyApi 節に EndpointConfiguration を追加して対応する
設定できる値:

  • EDGE … エッジ最適化
  • REGIONAL … リージョン別
  • PRIVATE … プライベート

$ emacs template.yaml

...
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      EndpointConfiguration: REGIONAL  # ここに追加
      Cors:
        AllowOrigin: "'*'"
        AllowMethods: "'OPTIONS,GET,POST'"
        AllowHeaders: "'Content-Type'"
...

メモ:
AWS API Gatewayのエンドポイントタイプ(エッジ最適化、リージョン別、プライベート)による料金の違いは以下の通りです¹:

  1. EDGE(エッジ最適化)
    このタイプのAPIでは、受信したAPIコール数と転送データ量に対してのみ料金が発生します¹。また、API Gatewayにはオプションのデータキャッシュ機能があり、選択するキャッシュサイズに応じて、時間単位で料金が計算されます。
  2. REGIONAL(リージョン別)
    このタイプのAPIでも、受信したAPIコール数と転送データ量に対してのみ料金が発生します¹。また、API Gatewayにはオプションのデータキャッシュ機能があり、選択するキャッシュサイズに応じて、時間単位で料金が計算されます。
  3. PRIVATE(プライベート)
    プライベートAPIのデータ転送には料金が発生しません。ただし、AWS PrivateLinkの料金は、API GatewayでプライベートAPIを使用するときに適用されます。

それぞれのエンドポイントタイプによる料金の違いは、主にデータ転送とキャッシュの使用に関連しています。具体的な料金は、AWSの公式ウェブサイトや料金計算ツールを参照してください。

  1. Amazon API Gateway Pricing | API Management | Amazon ….
    https://aws.amazon.com/api-gateway/pricing/.
  2. 料金 - Amazon API Gateway | AWS.
    https://aws.amazon.com/jp/api-gateway/pricing/.
  3. AWS API Gateway pricing 2023 – Cost Calculator.
    https://dataengineeracademy.com/blog/aws-api-gateway-pricing-factors-and-cost-structure/.
  4. AWS API Gateway: Create one API for both public access ….
    https://stackoverflow.com/questions/70942231/aws-api-gateway-create-one-api-for-both-public-access-and-private-access.
  5. Regional/Edge-optimized API Gateway VS Regional/Edge ….
    https://stackoverflow.com/questions/49826230/regional-edge-optimized-api-gateway-vs-regional-edge-optimized-custom-domain-nam.

vpc を追加する #

template.yaml にVpcConfigを追記する
追記する箇所は、全体に適用する場合と、各apiに追加する場合がある
なお、事前にVPC、セキュリティグループ、サブネットを作成しておく必要がある

# 全体に適用する場合:
Globals:
  Function:
    Timeout: 3
    VpcConfig: # <--- vpcはセキュリティグループとサブネットから自動判断される様子
      SecurityGroupIds:
        - sg-0abcd1234efgh5678 # <--- 事前にセキュリティグループ作成必要
      SubnetIds:
        - subnet-a1b2c3d4 # <--- 事前にサブネット1作成必要
        - subnet-e5f6g7h8 # <--- 事前にサブネット2作成必要

# API 単位に適用する場合:
MyGetFunction:
  Type: AWS::Serverless::Function
  Properties:
    ...
    VpcConfig: # <--- vpcはセキュリティグループとサブネットから自動判断される様子
      SecurityGroupIds:
        - sg-0abcd1234efgh5678 # <--- 事前にセキュリティグループ作成必要
      SubnetIds:
        - subnet-a1b2c3d4 # <--- 事前にサブネット1作成必要
        - subnet-e5f6g7h8 # <--- 事前にサブネット2作成必要

以下は試せてないが、この template.yaml で VPC、セキュリティグループ、サブネットを作成している。
動作するかは確認できてない。
また、VPC、セキュリティグループ、サブネットを作成するには Resources 節に追記する必要があるらしい
※AWS SAM テンプレートでは、各 Lambda 関数に対して VpcConfig を個別に設定する必要がある。これは、各関数が異なる VPC 設定を必要とする可能性があるため。例えば、一部の関数は特定の VPC 内でのみ実行する必要があり、他の関数は別の VPC または VPC 外で実行する必要があるかもしれません。したがって、各関数の Properties セクションに VpcConfig を追加することで、より柔軟な設定が可能となる。らしい。

Resources:
  MyVPC:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: 10.0.0.0/16

  MySubnet:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.1.0/24

  MySecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: My security group
      VpcId: !Ref MyVPC
    
    ...
    VpcConfig:
      SecurityGroupIds:
        - !Ref MySecurityGroup
      SubnetIds:
        - !Ref MySubnet
    ...

ドメインを追加する #

route53でドメイン設定した場合

$ emacs tempate.yaml

  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      Domain: # <--- Domain節追加
          DomainName: example.com # ここにドメイン名を指定
          CertificateArn: arn:aws:acm:us-east-1:123456789012:certificate/abc123def456 # ここに ACM 証明書の ARN を指定
      Cors:
        AllowOrigin: "'*'"
        AllowMethods: "'OPTIONS,GET,POST'"
        AllowHeaders: "'Content-Type'"

メモ:
お名前.comで取得したドメインをAWSのAPI Gatewayと連携するためには、以下の手順を実行:

  1. AWS Certificate Managerで証明書を取得
    まず、AWS Certificate Manager (ACM) を使用して、お名前.comで取得したドメイン名に対する証明書を取得。
    この証明書は、API Gatewayが安全なHTTPS接続を提供するために必要です。
  2. SAMテンプレートにドメイン情報を追加
    次に、SAMテンプレートの AWS::Serverless::Api リソースに Domain プロパティを追加。
    ここには、お名前.comで取得したドメイン名と、上記で取得したACM証明書のARNを指定します。
  3. お名前.comでDNS設定を行う
    API Gatewayは、新しいAPIのエンドポイントとして使用するドメイン名に対して一連のDNSレコードを提供。
    これらのレコードをお名前.comのDNS設定に追加することで、ドメイン名がAPI Gatewayのエンドポイントを指すようになる。

以上の手順を行うことで、お名前.comで取得したドメイン名をAPI Gatewayと連携することが可能です。 具体的な手順については、AWSの公式ドキュメンテーションをご覧ください。 なお、これらの設定は、ドメイン名と証明書が正しく設定されていること、そしてDNS設定が正しく行われていることを前提としています。 これらが未設定の場合、API Gatewayのカスタムドメイン設定は機能しません。ご注意ください。

簡単操作スクリプト追加 #

  • ローカル環境で起動(start-api)するスクリプト

    $ emacs local_start.fish

    #!/bin/fish
    sam build -t template.yaml --config-file samconfig.toml
    sam local start-api -t template.yaml --config-file samconfig.toml
    
  • aws にデプロイするスクリプト

    $ emacs deploy.fish

    #!/bin/fish
    sam build -t template.yaml --config-file samconfig.toml
    sam deploy -t template.yaml --config-file samconfig.toml
    
  • aws にデプロイしたapitest-appを削除するスクリプト

    $ emacs remove-apitest-app.fish

    #!/bin/fish
    aws cloudformation delete-stack --stack-name apitest-app --profile ns
    

api gateway のアクセスを vpc 内限定にする方法 #

このページで説明した template.yaml の VpcConfig 設定により、AWS SAM (Serverless Application Model) は、指定されたセキュリティグループとサブネットを使用して、Lambda関数をVPC内で動作する。
しかし、この設定はLambda関数のVPC設定を行うものであり、API Gateway自体がVPC内で動作するわけではない。
API Gateway はインターネットに公開されるサービスであり、VPC内からのみアクセス可能にするためには、追加の設定が必要
具体的には、プライベートAPIを作成し、VPCエンドポイントを使用してVPC内からのみアクセス可能にすることが可能。

API GatewayのアクセスをVPCに制限するための主な方法は以下の通り:

  1. VPCエンドポイントポリシー:
    VPCエンドポイントポリシーは、IAMリソースポリシーであり、インターフェースVPCエンドポイントにアタッチしてエンドポイントへのアクセスを制御。
    このポリシーを使用して、内部ネットワークからプライベートAPIへのトラフィックを制限することがでる。
    VPCエンドポイントポリシーは、API Gatewayリソースポリシーと一緒に使用できる。
    リソースポリシーは、APIにアクセスできるプリンシパルを指定するために使用され、エンドポイントポリシーは、VPCエンドポイント経由で呼び出すことができるプライベートAPIを指定する。
  2. リソースポリシー:
    リソースポリシーを使用すると、特定のソースIPアドレスまたはVPCエンドポイントからのAPIおよびメソッドへのアクセスを許可または拒否するリソースベースのポリシーを作成できる。
  3. IAMロールとポリシー:
    IAMロールとポリシーを使用すると、API全体または個々のメソッドに適用できる柔軟で堅牢なアクセス制御を提供。
    これらは、APIの作成と管理を誰が行うことができるか、また誰がそれらを呼び出すことができるかを制御するために使用できる。
  4. プライベートAPIの作成:
    プライベートAPIを作成すると、インターフェイスVPCエンドポイントを使用して、Amazon VPCの仮想プライベートクラウドからのみアクセスできるREST APIを作成できる。

これらの方法を使用することで、API GatewayへのアクセスをVPCに制限することが可能。

参考URL:

sam コマンドの挙動 #

  • samconfig.toml に記載したオプションよりも sam cli で指定したオプションが優先される
  • samconfig.toml は sam cli の各コマンドのオプション指定ができる。各コマンドに対応した節があるので注意すること。
    例えば以下のように組み合わせで対応している様子。
    [default.local_start_api.parameters] は sam local start-api が参照
    [default.local_start_lambda.parameters] は sam local start-labmda が参照
    [default.deploy.parameters] は sam deploy が参照
    [default.build.parameters] は sam build が参照
    ※なので sam build 時に –config-file が指定できる理由は samconfig.toml 内の [default.build.parameters] パラメータを参照するため。
  • –use-container オプションは、sam build 時に指定できるオプションで Lambda 関数のビルドを Docker コンテナ内で行う。
    なので、mac 環境だと必ず –use-container で sam build しないと aws 環境ではきっと動作しない。(nativeビルドが必要なやつが動作不良を起こすはず)
  • sam build や sam local で使うコンテナイメージは、デフォルトでは Amazon SAM は Amazon ECR Public からコンテナイメージを pull。
    別のリポジトリからコンテナイメージを pull したい場合は、--build-imageオプションを使用して代替のコンテナイメージのURI指定可能
  • –warm-containers EAGER オプションは Lambda 関数のコンテナを事前に起動し、それらを再利用することを可能にする。
    [default.local_start_api.parameters],[default.local_start_lambda.parameters] はデフォルトで指定されてある。
    ※リクエストの度にコンテナの起動が行われないため動作が早くなるはず。
  • –skip-pull-image オプションは、Docker イメージの取得をスキップする
    ※取得が行われないので起動が早くなるはず。また、イメージがない場合、勝手に落としてきてくれるみたいなので、デフォルトで指定してもいいかもしれない。
  • sam deploy 時は sam build が実行されてない場合、自動で sam build が行われる。
  • sam local start-api, start-lambda の実行手順はおそらく以下の通り。
    1. sam local start を実行したディレクトリ(プロジェクトルート)を確認
    2. -t オプションで template.yaml が指定されてある場合、その内容に従って起動を開始
      ようするに開発している作業ディレクトリがマウントされるのでホットリロード可能。ただし、実行に必要なパッケージ等は自分で配置(pip install等)する必要がある
    3. -t オプションがない場合、.aws-sam/buildディレクトリがある場合、そこにある template.yaml に従って起動。
      ようするに、.aws-sam/buildディレクトリがマウントされて起動することになるため、ホットリードする場合は、手動で sam build する必要がある。(npm等で監視ツール適用も可能)
    4. -t オプションがなく、.aws-sam/buildディレクトリもない場合、プロジェクトルートのtemplate.yamlに従って起動する(1と同じ)

まとめると、、、
環境が mac や windows host(wsl2 ubuntuでない)の場合だと sam build, sam local という手順を踏んで起動させるべき
理由としては linux 環境ならば、ほとんどのパッケージがそのままで利用可能であるため、sam build しなくてもほとんどのケースで問題なく開発環境で動作する
また、開発環境のソースをそのままホットリロード対応にすることも簡単に実現できるためである。

  • 今回のサンプルコードを開発ソースをホットリロード対応にする場合の構築手順
    #!/bin/fish
    rm -rf venv
    python -m venv venv
    source venv/bin/activate.fish
    
    # pytest, vscode 用にプロジェクトルートの requirements.txtを venv に反映
    pip install -r requirements.txt --upgrade
    # 各functionディレクトリのrequirements.txtを venv に反映
    pip install -r my_get_function/requirements.txt --upgrade
    pip install -r my_post_function/requirements.txt --upgrade
    pip install -r my_layer/requirements.txt --upgrade
    
    # ホットリロード対応にするため各ディレクトリにもパッケージをインストールしておく
    pip install -r my_get_function/requirements.txt -t my_get_function --upgrade
    pip install -r my_post_function/requirements.txt -t my_post_function --upgrade
    pip install -r my_layer/requirements.txt -t my_layer --upgrade
    # 面倒だけど start-api 時には必要っぽい...
    pip install -r my_layer/requirements.txt -t my_get_function --upgrade
    pip install -r my_layer/requirements.txt -t my_post_function --upgrade
    
    # レイヤをビルドしvenvに反映させておく
    sam build MyLayer
    cp -R .aws-sam/build/MyLayer/python/* venv/lib/python3.11/site-packages/
    # 各functionディレクトリにも反映させておく(こちらも start-api時には必要っぽい。。。)
    cp -R .aws-sam/build/MyLayer/python/* my_get_function/
    cp -R .aws-sam/build/MyLayer/python/* my_post_function/
    

参考URL #