🦊

【AWS】IDE上からSAMでLambda×APIGatewayを実装する備忘録

2024/03/28に公開

はじめに

本記事は備忘録としてのwikiです。
各ファイルの役割や記載内容の詳しい説明は割愛しております。
本記事内では以下の2点を行います。

  • TypeScriptでサーバーレスのAPI環境を構築します。
  • その環境に対して任意のAPIを実装します。

※ 最終的なyml全文は『最後に』の追記後を参照してください。

環境

使用IDE

IntelliJ WebStorm
(私は未確認だが無料の『IntelliJ IDEA CE』でも同じ操作感のはず)
→ IntelliJ IDEA CEは対応していなかったです。。。

OS

Mac 14.3

前提

  • AWSDevelopperのアカウントを既に作成している
  • IDEに対してAWSアカウントの認証が許可されている

理解の助けになる知識

LambdaとAPIGatewayについてのある程度の理解
RESTAPIについてのある程度の理解
AWSのマイクロサービス等の構成についての理解
.ymlファイルの構文を知っている

手順

  1. 自分のマシンにsamをインストールする
  2. IDEで新しいAWSサーバーレスアプリのプロジェクトを作成する
  3. デプロイを実行する
  4. 自分の任意のAPIを作成する
  5. 更新のデプロイを実行する

1.自分のマシンにsamをインストールする

公式にインストーラがあるのでそこからとってくる。
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html
各OSに合ったものが用意されているのでインストールする。

2.IDEで新しいAWSサーバーレスアプリのプロジェクトを作成する

公式に書いてある通りです。
https://docs.aws.amazon.com/ja_jp/toolkit-for-jetbrains/latest/userguide/deploy-serverless-app.html

以下のキャプチャ画像の通りに設定していきます。

SAMのパスが設定されていないと先に進めないので、
設定されていない場合は以下の方法で設定を済ませてください。

既にパスが設定されている場合は『create』を押してプロジェクトを作成してください。

samの実行パスを設定する

SAM CLI excutable:の項目にパスを設定します。
パスはMacの場合、whichコマンドで取得できます。

which sam

3. デプロイを実行する

プロジェクトが作成されたらデフォルトでディレクトリが構成されています。
デフォルトではHelloWorldAPIが実装されます。
まずはデフォルトの挙動を見るために、READMEに従ってデプロイを完了させます。
以下のコマンドはすべてプロジェクトルートの階層で実行します。
ローカル用でのデプロイ方法などもREADMEに記載されているので、目を通しておくといいです。
今回は普通にビルドします。

ビルド

sam build

デプロイ
--guidedによって対話形式で環境変数を設定してデプロイができます。

sam deploy --guided

完了したら、stackという単位でAWS環境が構築されます。
実際にブラウザでAWSのコンソール画面にアクセスして『CloudFormation』を確認してみると、
新しくstackが作成されていることが確認できます。

4. 自分の任意のAPIを作成する

最終的なyml全文は『最後に』の追記後を参照してください。

template.yamlを編集する

デフォルトで書かれているHelloWorldFunctionのブロックの下に以下を追加する。

  MyCustomFunction: # 新しく追加したLambda関数
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/ # TypeScriptのソースのパス
      Handler: app.myCustomFunctionHandler # 任意のモジュール
      Runtime: nodejs18.x
      Architectures:
        - x86_64
      Events:
        MyCustomEndpoint:
          Type: Api
          Properties:
            Path: /MyCustomFunction
            Method: get # 任意のHTTPメソッド
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        Sourcemap: true
        EntryPoints:
          - app.ts

app.tsxにコードを追加する

your-aws-project/hello-world/app.tsxにTypeScriptのコードを追記する。
これはLambda関数の『コード』にあたる、いわゆる処理の部分。
myCustomFunctionHandlerとしてエクスポートする。
以下のコードを記述。
めんどくさかったので、デフォルトのHelloWorldのコピペですが。。。(レスポンスの文字列だけ変えたよ)

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

test-handler.test.tsにテストを追加する

your-aws-project/hello-world/tests/unit/test-handler.test.tsにテストを追加する
めんどくさかったので、デフォルトのHelloWorldのコピペですが。。。(実際はちゃんと書いてね)

it('verifies successful response', async () => {
        const event: APIGatewayProxyEvent = {
            httpMethod: 'get',
            body: '',
            headers: {},
            isBase64Encoded: false,
            multiValueHeaders: {},
            multiValueQueryStringParameters: {},
            path: '/hello',
            pathParameters: {},
            queryStringParameters: {},
            requestContext: {
                accountId: '123456789012',
                apiId: '1234',
                authorizer: {},
                httpMethod: 'get',
                identity: {
                    accessKey: '',
                    accountId: '',
                    apiKey: '',
                    apiKeyId: '',
                    caller: '',
                    clientCert: {
                        clientCertPem: '',
                        issuerDN: '',
                        serialNumber: '',
                        subjectDN: '',
                        validity: { notAfter: '', notBefore: '' },
                    },
                    cognitoAuthenticationProvider: '',
                    cognitoAuthenticationType: '',
                    cognitoIdentityId: '',
                    cognitoIdentityPoolId: '',
                    principalOrgId: '',
                    sourceIp: '',
                    user: '',
                    userAgent: '',
                    userArn: '',
                },
                path: '/hello',
                protocol: 'HTTP/1.1',
                requestId: 'c6af9ac6-7b61-11e6-9a41-93e8deadbeef',
                requestTimeEpoch: 1428582896000,
                resourceId: '123456',
                resourcePath: '/hello',
                stage: 'dev',
            },
            resource: '',
            stageVariables: {},
        };
        const result: APIGatewayProxyResult = await myCustomFunctionHandler(event);

        expect(result.statusCode).toEqual(200);
        expect(result.body).toEqual(
            JSON.stringify({
                message: 'hello world',
            }),
        );
    });

5. 更新のデプロイを実行する

プロジェクトルートの階層で次のコマンドを実行する。

sam deploy cloudformation deploy --template-file ./template.yaml --stack-name your-aws-project --capabilities CAPABILITY_IAM 

各オプションの意味は以下の通り。
--stack-nameにはすでにデプロイ済みのスタック名を指定する。
これで既にあるstackの更新だと認識される。
--template-fileにはtemplateファイルのパスを渡す。
--capabilities CAPABILITY_IAMでIAMリソースの変更を許可する。

これでAPIの実装は完了。

最後に

オーソライザーの設定の仕方やCORS対応などもSAMで行っていきたい。

---追記---
同じエンドポイント上に異なるメソッド(GETとかOPTIONSとか)を設定していくうえで以下のような書き方になりました。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  your-aws-project

  Sample SAM Template for your-aws-project
  
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  MyApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Dev
      DefinitionBody:
        swagger: '2.0'
        info:
          title: your-aws-project
          version: '1.0'
        paths:
          /MyCustomFunction:
            # OPTIONSメソッド
            options:
              summary: OPTIONS Method
              responses:
                '200':
                  description: 200 response
              x-amazon-apigateway-integration:
                type: mock
                requestTemplates:
                  application/json: '{"statusCode": 200}'
                responses:
                  default:
                    statusCode: 200
            # GETメソッド
            get:
              responses:
                '200':
                  description: 200 response
              x-amazon-apigateway-integration:
                type: aws_proxy   # Lambda統合
                uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyCustomFunction.Arn}/invocations  # Lambda関数との紐付け
                httpMethod: GET

          /hello:
            get:
              responses:
                '200':
                  description: 200 response
              x-amazon-apigateway-integration:
                type: aws_proxy
                uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations
                httpMethod: GET

  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs18.x
      Architectures:
        - x86_64
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get
            RestApiId: !Ref MyApiGateway

  MyCustomFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: app.myCustomFunctionHandler
      Runtime: nodejs18.x
      Architectures:
        - x86_64
      Events:
        MyCustomEndpoint:
          Type: Api
          Properties:
            Path: /MyCustomFunction
            Method: get
            RestApiId: !Ref MyApiGateway

Discussion