serverlessFWにこんにちは　serverlessでローカル上でAWS Lambdaを起動・呼び出してみる

## はじめに
[AWS SAMを使ってLambda(GO)のCICDをGitHub Actionsで構築](https://qiita.com/yuta-katayama-23/items/d5c786aaa2a439a3cb17)でサーバーレスアプリケーションを構築するフレームワークとして[AWS SAM](https://aws.amazon.com/jp/serverless/sam/)は触った事があったが、[serverless](https://www.serverless.com/)は触った事がなかったので、serverlessでHello World（Node.js）をやってみたので備忘録を残す。

やってみた事としては、以下の3つ。

- cli上で`severless`コマンドからHello World的なLambda関数を作成するseverless.ymlの作成
- ローカル環境でそのLambda関数を実行し、Lambdaを呼び出し
- 【おまけ】実際にAWS環境にDeploy

ソースコード全体は以下。

https://github.com/yuta-katayama-23/learn-serverless/commit/b0d69c83991fe69a635aa4bc89a3a02ad6d0875c

## はじめてのserverless
### プロジェクトを作成
今回はVirtualboxのCentOS7.9上でserverlessのセットアップを行った（環境構築方法の詳細は[Windows上にLinux(CentOS)のWebアプリ開発環境をvirtualboxで構築する](https://qiita.com/yuta-katayama-23/items/6baf167464eca3376ea0)を参照）。

```console
[root@localhost serverless]# yarn init
yarn init v1.22.17
question name (serverless):
question version (1.0.0):
question description: serverlessFW-learning
question entry point (index.js):
question repository url:
question author (Yuta Katayama xxx@xxx.com>): yuta-katayama-23
question license (MIT):
question private:
success Saved package.json
Done in 36.32s.
```

### serverlessをインストール
[Get started with Serverless Framework Open Source & AWS](https://www.serverless.com/framework/docs/getting-started)にはグローバルにインストールする方法が書かれているが、グローバルだと他の人がプロジェクトを利用する際に前提条件が増えるので、素直に`package.json`で管理する方法を取る。

```console
yarn add --dev serverless
```

こうする事で、[npx](https://docs.npmjs.com/cli/v8/commands/npx)コマンドで`npx sls --version`のように`serverless`（省略形で`sls`）コマンドが使えるようになる。

※npxはローカルインストールしたコマンドを実行するために使うもの（この辺りは[npxコマンドとは？ 何ができるのか？](https://zenn.dev/ryuu/articles/what-npxcommand)などを参照）

### `npx serverless`でserverlessプロジェクトを作成

[公式](https://www.serverless.com/framework/docs/getting-started#getting-started)に書かれている通り、`serverless`コマンドを実行（ローカルパッケージの場合には`npx serverless`）する事でCLI上で対話をしながらプロジェクトを作成できる。

```console
[root@localhost serverless]# npx serverless

Creating a new serverless project

? What do you want to make? AWS - Node.js - Starter
? What do you want to call this project? aws-node-project

✔ Project successfully created in aws-node-project folder

? Do you want to login/register to Serverless Dashboard? No

? No AWS credentials found, what credentials do you want to use? Skip

You can setup your AWS account later. More details available here: http://slss.io/aws-creds-setup
```

上記のようにする事でserverless.ymlや`handler.js`が作成され、後はdeployすればLambda関数が作成できる状態が作られる。

※上記においてAWSのアカウント設定をskipしているが、これはAWS環境に実際にDeployする時になってから改めて設定すれば問題ない（serverlessは[Using AWS Access Keys](https://www.serverless.com/framework/docs/providers/aws/guide/credentials/#using-aws-access-keys)の[Setup with the aws-cli](https://www.serverless.com/framework/docs/providers/aws/guide/credentials/#setup-with-the-aws-cli)に書かれている通り、aws-cliの設定内容（実態は`~/.aws`以下のconfigとcredential）からユーザ情報を読み取ってくれる）

※今回のように既にプロジェクトを作成済みの場合、[`create`コマンド](https://www.serverless.com/framework/docs/providers/aws/cli-reference/create)を用いてserverlessプロジェクトを作成する事もできる。createコマンドの場合、serverless.yamlやhandler.jsの出力先を`--path`オプションで指定できるので、既にプロジェクトがある時には便利だろう。

```console
[root@localhost serverless]# npx serverless create --template aws-nodejs --path myService

✔ Project successfully created in "myService" from "aws-nodejs" template (6s)

[root@localhost serverless]# cd myService/
[root@localhost myService]# tree -a
.
├── .gitignore
├── handler.js
└── serverless.yml
```

### ローカルでserverlessアプリケーションを実行できるようにする
今回はserverlessアプリケーションと言ってもLambda単体しかないのでLambdaをローカルで実行できるようにする。

方法は簡単で公式の[Plugins](https://www.serverless.com/framework#items-list)にも書かれている[serverless-offline](https://github.com/dherault/serverless-offline)をインストールし、serverless.yamlに以下のように追記するだけ。

```yaml
service: aws-node-project

frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs14.x

# 以下を追記
plugins:
  - serverless-offline

functions:
  hello:
    handler: handler.hello
```

後は、`npx sls offline`コマンドを叩けばLambdaをローカルで起動できる。

```console
[root@localhost serverless]# npx sls offline

Starting Offline at stage dev (us-east-1)

Offline [http for lambda] listening on http://localhost:3002
Function names exposed for local invocation by aws-sdk:
           * hello: aws-node-project-dev-hello
(λ: hello) RequestId: cl1suu1g00000f0pvg2b8cg52  Duration: 4.05 ms  Billed Duration: 5 ms
```

※上記では実際にLambda関数を実行した際のログも出でいる（`(λ: hello) RequestId: cl1suu1g00000f0pvg2b8cg52  Duration: 4.05 ms  Billed Duration: 5 ms`）が、それは次の章で見ていく。

※serverless-offlineを入れずとも、[AWS - Invoke Local](https://www.serverless.com/framework/docs/providers/aws/cli-reference/invoke-local)に書かれているように、以下のようにローカルでLambdaを実行する事は可能。API Gatewayなど他のAWSサービスと連携するサーバーレスアプリケーションをローカルでエミュレーションするにはserverless-offlineが必要になる。

```console
study@localhost:~/workspace/serverless (main *)
$ yarn invoke:local
yarn run v1.22.19
$ npx sls invoke local --function hello
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v3.0! Your function executed successfully!\",\n  \"input\": \"\"\n}"
}
Done in 3.00s.
```

### aws-cliでLambdaを実行する（呼び出す）
まずはaws-cliのインストールが必要なので[AWS CLI の最新バージョンをインストールまたは更新します。](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html)を参照しインストールする。

あとは[invoke](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/invoke.html)に書かれているようにコマンドを実行するだけ。

```console
[root@localhost serverless]# aws lambda invoke --payload '{ "name": "Bob" }' --endpoint-url http://localhost:3002 --function-name aws-node-project-dev-hello --cli-binary-format raw-in-base64-out response.json
{
    "StatusCode": 200
}
```
```json:response.json
{
	"statusCode": 200,
	"body": "{\n  \"message\": \"Go Serverless v3.0! Your function executed successfully!\",\n  \"input\": {\n    \"name\": \"Bob\"\n  }\n}"
}
```

※2点、注意点があるのでそれについて補足する。

#### aws-cliを利用する際には、ダミーでもAWSのクレデンシャル情報が必要
AWSクレデンシャルがない状態でコマンドを実行すると、以下のようにエラーになる。

```console
[root@localhost serverless]# aws lambda invoke --payload '{ "name": "Bob" }' --endpoint-url http://localhost:3002 --function-name aws-node-project-dev-hello --cli-binary-format raw-in-base64-out response.json

You must specify a region. You can also configure your region by running "aws configure".
```

という事でダミーでもいいので以下のようにしてAWSクレデンシャルを設定する。

```console
[root@localhost serverless]# aws configure
AWS Access Key ID [None]: dumy
AWS Secret Access Key [None]: dumy
Default region name [None]: ap-northeast-1
Default output format [None]: json
```

※後々AWS環境にDeployする（serverless.ymlに基づいてAWS環境にLambdaを作成する）際には、aws-cliのprofileを切り替えて本物のクレデンシャルを利用する必要があるので注意（詳細は[AWS CLI の名前付きプロファイル](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-profiles.html)を参照）

#### aws-cli v2 から、cliの引数のパラメータはbase64エンコードされた文字列を渡す必要がある
詳細は[AWS CLI バージョン 2 では、デフォルトでバイナリパラメータが base64 でエンコードされた文字列として渡されるようになりました](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cliv2-migration.html#cliv2-migration-binaryparam)を参照。

やる事としては、cliのコマンド実行時に`--cli-binary-format raw-in-base64-out`オプションを追加する or `~/.aws/config`に`cli_binary_format=raw-in-base64-out`を追記すればいい。
ちなみに、何もしないで実行すると以下のようにエラーになる。

```console
[root@localhost serverless]# aws lambda invoke --payload '{ "name": "Bob" }' --endpoint-url http://localhost:3002 --function-name aws-node-project-dev-hello response.json

Invalid base64: "{ "name": "Bob" }"
```

## まとめとして
今回はserverlessコマンドで初期プロジェクトを作成して、それをローカルで起動・実行するだけをやってみた。今後はNode.jsでもES6で実装したり、ESLintを導入したり、色々やってみたいと思う。

## おまけ
### AWS環境にDeployしてみる
#### aws-cliのクレデンシャルを追加で設定する
上記の[aws-cliを利用する際には、ダミーでもAWSのクレデンシャル情報が必要](#aws-cliを利用する際にはダミーでもawsのクレデンシャル情報が必要)ではダミーの実際のAWSアカウントで払い出したものではないクレデンシャル情報を設定していたので、今度は本物のAWSアカウントのクレデンシャル情報を設定する。

やり方は簡単で、公式の[名前付きプロファイルを使用する](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-profiles.html#cli-configure-profiles-create)に書かれている通り、`aws configure --profile {新しく作成するprofile名}`というコマンドで設定できる。

> 追加のプロファイルを設定するには、--profile オプションで aws configure を使用する

```console
[root@localhost serverless]# aws configure --profile yuta_katayama
AWS Access Key ID [None]: ******************** ← マスクのために*で表記している
AWS Secret Access Key [None]: **************************************** ← マスクのために*で表記している
Default region name [None]: ap-northeast-1
Default output format [None]: json
```

設定すると以下のように新しくprofileが作成できている事が確認できる。

```console
[root@localhost serverless]# aws configure list-profiles
default
yuta_katayama
```

profileを切り替えるには[名前を指定されたプロファイルを使用する](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-profiles.html#using-profiles)に書かれているようにLinuxであれば環境変数を設定すればいい。設定後に`aws configure list`を実行すれば、現在aws-cliで使用されているクレデンシャル情報が確認できる。

```console
[root@localhost serverless]# export AWS_PROFILE=yuta_katayama
[root@localhost serverless]# aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile            yuta_katayama              env    ['AWS_PROFILE', 'AWS_DEFAULT_PROFILE']
access_key     ****************CQID shared-credentials-file    
secret_key     ****************NJo8 shared-credentials-file    
    region           ap-northeast-1      config-file    ~/.aws/config
```

#### `npx sls deploy`でAWS環境にDeployしてみる
公式の[AWS - deploy](https://www.serverless.com/framework/docs/providers/aws/cli-reference/deploy)に書かれている通り、`npx sls deploy --region ap-northeast-1`コマンドを実行するだけ（`--region`オプションがないとデフォルトの`us-east-1`にDeployされる）。実行すると以下のようになる。

```console
[root@localhost serverless]# yarn deploy
yarn run v1.22.17
$ npx sls deploy --region ap-northeast-1

Deploying aws-node-project to stage dev (ap-northeast-1)

✔ Service deployed to stack aws-node-project-dev (185s)

functions:
  hello: aws-node-project-dev-hello (108 kB)

Toggle on monitoring with the Serverless Dashboard: run "serverless"
Done in 189.37s.
```

この状態になると実際にAWS環境のLambdaを呼び出す事もできる。
※注意として`--function`オプションに渡すLambda関数名はservrless.ymlに定義している関数名を指定する必要がある

```console
[root@localhost serverless]# npx serverless invoke --function hello --data "hello world" --stage dev --region ap-northeast-1
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v3.0! Your function executed successfully!\",\n  \"input\": \"hello world\"\n}"
}
```

#### `npx sls deploy`でやっている事
[AWS - deploy](https://www.serverless.com/framework/docs/providers/aws/cli-reference/deploy)に

> The sls deploy command deploys your entire service via CloudFormation.（sls deployコマンドは、CloudFormation経由でサービス全体をデプロイします。）

と書かれているように、裏で実行されている事としては、CloudFormationのスタックを作成しAWSサービスの各リソースを構築するという事が行われている。

実際にマネジメントコンソールからCloudFormationのスタックを確認してみると、以下のようにserverlessによって作成されたスタックが確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/bd4df138-c3fd-59be-ea03-9b27e2c74554.png)

また、スタックの中を見てみると以下の画像のようにLambda関数以外にも複数のリソースが自動で作成されている事が分かる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/7f670d29-8a00-4d50-3d26-182dd53a943c.png)

これがserverlessフレームワークがサーバレスアプリケーションの構築のためのフレームワークと言われる所以。つまり、アプリケーションとして成り立つために必要になるログ出力など最低限のものを自動で構築してくれ、一般的にLambdaのDeployというとS3にzipを格納してそれをLambda関数に適用させるがそれに必要になるS3も自動で作成してくれる。
この辺りが便利になっている部分だろう。

ちなみに、自分でLambda関数のDeployをするためにCloudFormationを書くと[Cloud Formationを使ってGolang製LambdaのCICDをGitHub Actionsで構築](https://qiita.com/yuta-katayama-23/items/19cce34f8c5b1837ba37)でやってみたように色々AWSリソースの定義を書く必要があり大変。

※今回serverlessの方で自動的に作成してくれたCloudFormationのtemplate.yamlの全体としては以下（これだけの定義を[serverless.yml
](https://github.com/yuta-katayama-23/learn-serverless/commit/b0d69c83991fe69a635aa4bc89a3a02ad6d0875c#diff-8cf16232f3b7094ea431234555d38ad71e6e62764c3e29a5841e25172aea5049)に書いたたった数行でできる）。

<details><summary>template.yaml（クリックで開きます）</summary><div>

```yaml:template.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: The AWS CloudFormation template for this Serverless application
Resources:
  ServerlessDeploymentBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
  ServerlessDeploymentBucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref ServerlessDeploymentBucket
      PolicyDocument:
        Statement:
          - Action: 's3:*'
            Effect: Deny
            Principal: '*'
            Resource:
              - !Join 
                - ''
                - - 'arn:'
                  - !Ref 'AWS::Partition'
                  - ':s3:::'
                  - !Ref ServerlessDeploymentBucket
                  - /*
              - !Join 
                - ''
                - - 'arn:'
                  - !Ref 'AWS::Partition'
                  - ':s3:::'
                  - !Ref ServerlessDeploymentBucket
            Condition:
              Bool:
                'aws:SecureTransport': false
  HelloLogGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: /aws/lambda/aws-node-project-dev-hello
  IamRoleLambdaExecution:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: !Join 
            - '-'
            - - aws-node-project
              - dev
              - lambda
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'logs:CreateLogStream'
                  - 'logs:CreateLogGroup'
                Resource:
                  - !Sub >-
                    arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/aws-node-project-dev*:*
              - Effect: Allow
                Action:
                  - 'logs:PutLogEvents'
                Resource:
                  - !Sub >-
                    arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/aws-node-project-dev*:*:*
      Path: /
      RoleName: !Join 
        - '-'
        - - aws-node-project
          - dev
          - !Ref 'AWS::Region'
          - lambdaRole
  HelloLambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        S3Bucket: !Ref ServerlessDeploymentBucket
        S3Key: >-
          serverless/aws-node-project/dev/1649769206618-2022-04-12T13:13:26.618Z/aws-node-project.zip
      Handler: handler.hello
      Runtime: nodejs14.x
      FunctionName: aws-node-project-dev-hello
      MemorySize: 1024
      Timeout: 6
      Role: !GetAtt 
        - IamRoleLambdaExecution
        - Arn
    DependsOn:
      - HelloLogGroup
  HelloLambdaVersioncUWuCRlBIs9VT7ZlYopUEw3ADBvd3GjqQ5P9jF0YN8:
    Type: 'AWS::Lambda::Version'
    DeletionPolicy: Retain
    Properties:
      FunctionName: !Ref HelloLambdaFunction
      CodeSha256: ragOnPdr8ZNOSxsHRpByhvtsKyMt7qYT6H3gJcsuBoM=
Outputs:
  ServerlessDeploymentBucketName:
    Value: !Ref ServerlessDeploymentBucket
    Export:
      Name: sls-aws-node-project-dev-ServerlessDeploymentBucketName
  HelloLambdaFunctionQualifiedArn:
    Description: Current Lambda function version
    Value: !Ref HelloLambdaVersioncUWuCRlBIs9VT7ZlYopUEw3ADBvd3GjqQ5P9jF0YN8
    Export:
      Name: sls-aws-node-project-dev-HelloLambdaFunctionQualifiedArn
```

</div></details>