SAMを利用したLambda(Function URLs)デプロイ

## はじめに

[フューチャーAdvent Calendar 2022](https://qiita.com/advent-calendar/2022/future) の2日目です。
この記事は今年2022年4月にリリースされた「Lambda Function URLs」を実務で利用したところ非常に簡単だったので、そのやり方を紹介する記事です。
SAM初心者の方向けの内容になります。

こんにちは、Future CSIG所属の二瓶賢です。  
今年から[FutureVuls](https://vuls.biz/)という脆弱性管理サービスの開発チームで働いています。

さて、FutureVulsでは決済サービスにStripeや、メール送信サービスにSendgridといったwebサービスを利用しています。
そういったサービスではwebhookで異常を通知するケースが多いかと思いますが、それらwebhook受信用エンドポイントを都度用意するのが面倒という気持ちもあるかと思います。（私はそうです、特にサービスの検証段階だと猶更です。）

そこで、この記事では[AWS Lambda Function URLs](https://aws.amazon.com/jp/blogs/news/announcing-aws-lambda-function-urls-built-in-https-endpoints-for-single-function-microservices/) と[SAM](https://aws.amazon.com/jp/serverless/sam/)を利用し、(API Gatewayを構築せず、)シンプルで簡単に、エンドポイントを構築する流れをご紹介します。


## 手順

### SAM インストール

まずはAWS SAM CLIをインストールします
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html

### プロジェクト作成

下記コマンドを使うと良しなにテンプレートを作ってくれます。

```bash
sam init
```

<details>
<summary>全出力</summary>

```bash

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

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

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Multi-step workflow
        3 - Serverless API
        4 - Scheduled task
        5 - Standalone function
        6 - Data processing
        7 - Infrastructure event management
        8 - Serverless Connector Hello World Example
        9 - Multi-step workflow with Connectors
        10 - Lambda EFS example
        11 - Machine Learning
Template: 1

Use the most popular runtime and package type? (Python and zip) [y/N]: N

Which runtime would you like to use?
        1 - dotnet6
        2 - dotnet5.0
        3 - dotnetcore3.1
        4 - go1.x
        5 - graalvm.java11 (provided.al2)
        6 - graalvm.java17 (provided.al2)
        7 - java11
        8 - java8.al2
        9 - java8
        10 - nodejs16.x
        11 - nodejs14.x
        12 - nodejs12.x
        13 - python3.9
        14 - python3.8
        15 - python3.7
        16 - ruby2.7
        17 - rust (provided.al2)
Runtime: 4

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

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

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: y
X-Ray will incur an additional cost. View https://aws.amazon.com/xray/pricing/ for more details

Project name [sam-app]: sample-sam

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

    -----------------------
    Generating application:
    -----------------------
    Name: sample-sam
    Runtime: go1.x
    Architectures: x86_64
    Dependency Manager: mod
    Application Template: hello-world
    Output Directory: .

    Next steps can be found in the README file at ./sample-sam/README.md


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


SAM CLI update available (1.65.0); (1.60.0 installed)
To download: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html
```
</details>

作成されたディレクトリには`template.yaml`が作成されます。
これを修正し、（APIGatewayを使用せず）FunctionURLsのみを使う最小構成はこんな感じです。
デフォルトから変更している箇所にはコメントを入れてます。

```diff_yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-sample

  Sample SAM Template for sam-sample
 
Globals:
  Function:
    Timeout: 5
    Tracing: Active
  Api:
    TracingEnabled: True

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: hello-world
      Runtime: go1.x
-     Events:
-        CatchAll:
-          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
      Architectures:
        - x86_64
+     FunctionUrlConfig:
+       AuthType: NONE

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 environment for First Function"
-   Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "First Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn
+ HelloWorldFunctionUrl:
+   Description: "Function URLs endpoint"
+   Value: !GetAtt HelloWorldFunctionUrl.FunctionUrl
```

変更ポイントは下記です
1. `AWS::Serverless::Function`の`Properties`から`Events`を消す（これがあるとAPIGatewayが暗黙的に作成されてしまう）
1. `AWS::Serverless::Function`の`Properties`に`FunctionUrlConfig`を足す（認証にIAMリクエストを要求するか否かを選択できます [詳細](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-property-function-functionurlconfig.html)）
1.  `Outputs`からHelloWorldAPIを消す
1.  `Outputs`にFunctionUrlを足す（これがFunctionURLsのエンドポイントとなります）

※ goで実装する場合

goで実装する場合、デフォルトで生成されたソースにビジネスロジックを追記することかと思います。
デフォルトで生成されるソースではAPIGateway経由のリクエスト/レスポンスを想定されているので修正します。

具体的には[LambdaFunctionURLRequest](https://pkg.go.dev/github.com/aws/aws-lambda-go/events#LambdaFunctionURLRequest)と[LambdaFunctionURLResponse](https://pkg.go.dev/github.com/aws/aws-lambda-go/events#LambdaFunctionURLResponse)を使って下記のように変更します。

<details>
<summary>実装例</summary>

```diff_go
package main

import (
        "errors"
        "fmt"
        "io/ioutil"
        "net/http"

        "github.com/aws/aws-lambda-go/events"
        "github.com/aws/aws-lambda-go/lambda"
)

var (
        // DefaultHTTPGetAddress Default Address
        DefaultHTTPGetAddress = "https://checkip.amazonaws.com"

        // ErrNoIP No IP found in response
        ErrNoIP = errors.New("No IP in HTTP response")

        // ErrNon200Response non 200 status code in response
        ErrNon200Response = errors.New("Non 200 Response found")
)
- func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)
+ func handler(request events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) {
        resp, err := http.Get(DefaultHTTPGetAddress)
        if err != nil {
                return events.LambdaFunctionURLResponse{}, err
        }

        if resp.StatusCode != 200 {
                return events.LambdaFunctionURLResponse{}, ErrNon200Response
        }

        ip, err := ioutil.ReadAll(resp.Body)
        if err != nil {
                return events.LambdaFunctionURLResponse{}, err
        }

        if len(ip) == 0 {
                return events.LambdaFunctionURLResponse{}, ErrNoIP
        }

        return events.LambdaFunctionURLResponse{
                Body:       fmt.Sprintf("Hello, %v", string(ip)),
                StatusCode: 200,
        }, nil
}

func main() {
        lambda.Start(handler)
}
```

</details>

### ビルド

```bash
sam build
```

### ローカルテスト

ローカルでLambdaを起動し、動作確認をします。

```bash
sam local start-api
```

### デプロイ

```bash
sam deploy
```

これで終わりです。本当にシンプル。  
ロジックに修正・機能追加が入った場合も同様にビルド・デプロイするだけです。この時、FunctionURLsのエンドポイントは変わりません。

### 終わりに

SAMを利用するのは初めてだったのですが、簡単・シンプルにデプロイできたためかなり便利でした。
この記事が誰かの役に立てば幸いです。