Serverless Frameworkを使ったAWS Lambdaの開発チュートリアル

# 概要
最近、LINE Botを開発しました。
そのプロジェクトで、サーバレスアーキテクチャの構築と管理を容易にするServerless Frameworkを使用しました。
Serverless Frameworkを利用することで、AWSへのデプロイや管理が簡単になり、開発プロセスがスムーズになりました。
この記事では、その設定やプロセスについて詳しく説明していきます。

# 実行環境
前提条件として、Node.jsおよびAWS CLIインストールが必要です。
私の実行環境は以下の通りです。
```bash
$ node -v
v20.0.0
```
```bash
$ aws --version
aws-cli/2.15.12 Python/3.11.7 Darwin/23.2.0 source/x86_64 prompt/off
```

# Serverless Framework のインストール
まずServerless Frameworkをインストールします。
```bash
$ npm install -g serverless
$ serverless --version
Framework Core: 3.38.0
Plugin: 7.2.0
SDK: 4.5.1
```
ちなみに、`serverless`のエイリアスとして`sls`が登録されているので、以下コマンドでも実行できます。
```bash
$ sls --version
Framework Core: 3.38.0
Plugin: 7.2.0
SDK: 4.5.1
```

# Serverless Frameworkが使うIAMユーザーの作成
次にAWSアカウントのセットアップを行います。
AWSコンソールにログインし、IAMの**ユーザーの作成**をクリックし、任意のユーザー名を入力します。
![click-create-iam-user.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458297/6ff2b44b-1f3d-4b12-1e3d-e199071bf473.png)

**ポリシーを直接アタッチする**を選択し、**AdministratorAccess**を選択し、ユーザーを作成します。
![create-iam-user.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458297/3b9ed1ad-c592-c096-8c56-0e91f9365968.png)
IAMロールの権限を制限したい場合は以下サイトを参考に独自でセットアップして下さい。

https://www.serverless.com/framework/docs/providers/aws/guide/iam

ユーザーが作成できたら、**アクセスキーを作成**から**アクセスキー**と**シークレットアクセスキー**を作成します。
![create-aws-access-key.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458297/beb62a19-505d-d9f0-0934-fe627adfded5.png)

# AWS CLI の設定
次にAWS CLIを設定します。
先ほど作成した**アクセスキー**と**シークレットアクセスキー**をAWSの認証情報としてAWS CLIに設定します。
```bash
$ serverless config credentials --provider aws --key "Your Access Key ID" --secret "Your AWS Secret Access Key"
✔ Profile "default" has been configured
```
上記コマンドは、`~/.aws/credentials`ファイルに認証情報を書き込む操作を内部で行います。
そのため、`aws configure`コマンドを使っても同じ設定が可能です。
```bash
$ aws configure
  AWS Access Key ID [None]: Your Access Key ID
  AWS Secret Access Key [None]: Your AWS Secret Access Key
  Default region name [None]:
  Default output format [None]:
```
もし`default`プロファイルが既に設定されている場合、次のようなエラーメッセージが表示されます。
```bash
Error:
Profile "default" is already configured in ~/.aws/credentials. Use the overwrite flag ("-o" or "--overwrite") to force the update.
```
その場合`--profile`オプションでプロファイル名を指定して新しいプロファイルを作成するか、`--overwrite`オプションで既存のプロファイルを上書く必要があります。
```bash
$ serverless config credentials --provider aws --key "Your Access Key ID" --secret "Your AWS Secret Access Key" --profile sample
```
```bash
$ serverless config credentials --provider aws --key "Your Access Key ID" --secret "Your AWS Secret Access Key" --overwrite
```
設定が完了したら、`~/.aws/credentials`ファイルに保存された認証情報を確認できます。
```bash
$ cat ~/.aws/credentials 
[default]
aws_access_key_id=Your Access Key ID
aws_secret_access_key=Your AWS Secret Access Key

[sample]
aws_access_key_id=Your Access Key ID
aws_secret_access_key=Your AWS Secret Access Key
```

# プロジェクトの作成
次に、プロジェクトを作成します。
`serverless`コマンドを実行するとプロジェクトのテンプレートを選択するように求められるので、今回は`AWS - Node.js - Express API`を選択します。

```bash
$ serverless

Creating a new serverless project

? What do you want to make? (Use arrow keys)
  AWS - Node.js - Starter 
  AWS - Node.js - HTTP API 
  AWS - Node.js - Scheduled Task 
  AWS - Node.js - SQS Worker
❯ AWS - Node.js - Express API
  AWS - Node.js - Express API with DynamoDB 
  AWS - Python - Starter 
  AWS - Python - HTTP API 
  AWS - Python - Scheduled Task 
  AWS - Python - SQS Worker 
  AWS - Python - Flask API 
  AWS - Python - Flask API with DynamoDB 
  Other 
```
プロジェクト名には任意の値を入力します。
今回は`serverless-sample`とします。
Serverless Frameworkへの登録やログイン、そして即時デプロイを行うかどうかの質問が表示されます。
これらに答えることでプロジェクトが作成されます。
```bash
$ serverless

Creating a new serverless project

? What do you want to make? AWS - Node.js - Express API
? What do you want to call this project? serverless-sample

✔ Project successfully created in serverless-sample folder

? Register or Login to Serverless Framework No

? Do you want to deploy now? No

What next?
Run these commands in the project directory:

serverless deploy    Deploy changes
serverless info      View deployed endpoints and resources
serverless invoke    Invoke deployed functions
serverless --help    Discover more commands
```
# プロジェクト構造
プロジェクトが作成されると基本的なフォルダ構造が自動的に生成されます。
![directory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458297/2d457236-a244-8d72-8eb9-c0e08bda344b.png)

|ファイル|説明|
|-|-|
|`serverless.yml`|Serverless Frameworkの設定ファイルです。<br>サービス名や使用するクラウドプロバイダー、Lambda関数の設定などを記述します。|
|`index.js`|Lambda関数の具体的なロジックを実装するファイルです。|

# serverless.yml の初期設定
`serverless.yml`の初期設定は以下の通りです。
```yml:serverless.yml
service: serverless-sample
frameworkVersion: '3'

provider: 
  name: aws
  runtime: nodejs18.x

functions:
  api:
    handler: index.handler
    events:
      - httpApi: '*'
```
|セクション|説明|
|-|-|
| `service`|Serverless プロジェクトの名前を指定します。<br>この例では`serverless-sample`としています。|
|`frameworkVersion`|使用するServerless Frameworkのバージョンを指定します。<br>`'3'` はバージョン 3 を示します。|
|`provider`|使用するクラウドプロバイダーを指定します。<br>この例では、プロバイダー`aws` と、Node.jsのランタイムバージョン`nodejs18.x`を設定しています。|
|`functions`|Lambda関数とその設定を指定します。<br>このセクションで関数のハンドラー、イベントトリガーなどを設定しています。|

今回の設定では、`functions`セクションで`api`という名前のLambda関数を設定しています。
`handler`はその関数がどのファイルに記述されているかを指定します。
今回の設定では`index.js`の`handler`関数にそのロジックが実装されています。
`events`はどのイベントがこの関数をトリガーするかを定義しています。
今回の設定では`httpApi`を使用しAPI Gateway経由で受け取る全てのHTTPリクエストをこの関数にルーティングするように設定されています。

# index.jsの初期設定
`index.js`の初期設定は以下の通りです。
```JavaScript:index.js
const serverless = require("serverless-http");
const express = require("express");
const app = express();

app.get("/", (req, res, next) => {
  return res.status(200).json({
    message: "Hello from root!",
  });
});

app.get("/path", (req, res, next) => {
  return res.status(200).json({
    message: "Hello from path!",
  });
});

app.use((req, res, next) => {
  return res.status(404).json({
    error: "Not Found",
  });
});

module.exports.handler = serverless(app);
```
上記の`index.js`はExpressフレームワークを使ったNode.jsのコードです。
このコードは、ルートパス("/")と"/path"へのGETリクエストに応じて、異なるJSONメッセージを返します。
また、未定義のパスへのリクエストに対しては404エラーとエラーメッセージが返されます。
`serverless(app)`により、Expressで作成されたWebアプリケーション`app`をServerless Frameworkを通じてAWS Lambda関数としてデプロイすることが可能です。

# ローカルのテスト
Serverless Frameworkをローカルでテストする方法は2通りあります。

### serverless offline
1つ目は`serverless offline`を使用する方法です。
この方法では、ローカル環境でAPI GatewayやLambda関数の動作をエミュレートし、実際のAWS環境を模倣してテストを行うことができます。
開発中のAPIの挙動を確認したり、フロントエンドの開発と連携させて動作テストを行うのに便利です。
`serverless offline`プラグインをプロジェクトに追加し、設定を行うことで、簡単にローカル環境でのAPIテスト環境を構築できます。

以下コマンドで`serverless-offline`を導入します。
```bash
$ npm install serverless-offline --save-dev
```
その後`serverless.yml`の`plugins`に以下を追記します。
```yml:serverless.yml
plugins:
  - serverless-offline
```
`serverless offline`コマンドを実行すると、ローカルエンドポイントが出力されます。
このエンドポイントに対して`curl`コマンドなどを用いることで、関数の動作をテストできます。
```bash
$ serverless offline
（中略）
   ┌───────────────────────────────────────────────────────────────────────┐
   │                                                                       │
   │   ANY | http://localhost:3000/{default*}                              │
   │   POST | http://localhost:3000/2015-03-31/functions/api/invocations   │
   │                                                                       │
   └───────────────────────────────────────────────────────────────────────┘

Server ready: http://localhost:3000 🚀
```
### serverless invoke local
2つ目は`serverless invoke local`コマンドを使用する方法です。
このコマンドを使うと、特定のLambda関数をローカル環境で直接実行することができます。
デプロイする前に個々の関数の挙動をテストしたい場合に便利です。
```bash
$ serverless invoke local --function api
{
    "statusCode": 200,
    "headers": {
        "x-powered-by": "Express",
        "content-type": "application/json; charset=utf-8",
        "content-length": "30",
        "etag": "W/\"1e-n7v8BWYZG0cayfAnplsH5ynO6ec\""
    },
    "isBase64Encoded": false,
    "body": "{\"message\":\"Hello from root!\"}"
}
```

# AWS へのデプロイ
`serverless deploy`コマンドを使用してアプリケーションを AWS にデプロイする事が出来ます。
```bash
$ serverless deploy

Deploying serverless-sample to stage dev (us-east-1)

✔ Service deployed to stack serverless-sample-dev (117s)

endpoint: ANY - https://XXXXXXXXX.execute-api.us-east-1.amazonaws.com
functions:
  api: serverless-sample-dev-api (894 kB)
```
デプロイ後、エンドポイントURLにアクセスすることで、API Gateway経由でLambda関数を呼び出す事ができます。

# デプロイしたサービスの削除
デプロイしたサービスは、`serverless remove`コマンドを使ってAWSから削除できます。

```bash
$ serverless remove
Removing serverless-sample from stage dev (us-east-1)

✔ Service serverless-sample has been successfully removed (29s)
```
プロジェクトが完了したり、不要になったサービスは削除しておきましょう。

# serverless.ymlのカスタマイズ

最近、LINE Botの実装に使用した`serverless.yml`を紹介します。
定期実行、もしくはメッセージに対する応答時にLambda関数が実行されます。

```yml:serverless.yml
service: amazon-wishlist-to-line
frameworkVersion: "3"

useDotenv: true

provider:
  name: aws
  runtime: nodejs18.x
  region: ap-northeast-1
  environment:
    AMAZON_WISHLIST_URL: ${env:AMAZON_WISHLIST_URL}
    CHANNEL_ACCESS_TOKEN: ${env:CHANNEL_ACCESS_TOKEN}
    CHANNEL_SECRET: ${env:CHANNEL_SECRET}
    USER_ID: ${env:USER_ID}
  timeout: 600

functions:
  lineWebhookHandler:
    handler: src/index.handler
    events:
      - http:
          method: post
          path: "/webhook"
          cors: true
  scheduledHandler:
    handler: src/handlers/scheduledHandler.handleScheduledEvent
    events:
      - schedule:
          rate: cron(0 22 * * ? *)
          enabled: true

plugins:
  - serverless-offline
```

### サービスの設定
サービス名を`amazon-wishlist-to-line`と指定し、Serverless Frameworkのバージョン3を使用します。
`useDotenv`を使用する事で、`.env`ファイルから環境変数を読み込むように設定します。

### プロバイダーの設定
AWSをクラウドプロバイダーとして、Node.js 18.xをランタイムとして使用します。
必要な環境変数とLambda関数のタイムアウト時間も設定しています。

### 関数の定義
#### lineWebhookHandler
LINEのWebhookからのリクエストを処理する関数です。
`src/index.handler`にそのロジックが実装されています。
`/webhook`のパスで待機していて、HTTP POSTリクエストを受け取ります。
CORSを有効にして、外部のドメインからのリクエストも受け入れるようにしています。

#### scheduledHandler
定期的に実行される関数です。
`src/handlers/scheduledHandler.handleScheduledEvent`にそのロジックが実装されています。
この関数はAWS EventBridgeを利用しており、`cron(0 22 * * ? *)`により、UTC時間で毎日22時（日本時間の朝7時）に実行されるようスケジュールされています。

### ローカル開発のサポート
`serverless-offline`プラグインを使用してローカルでの開発を容易にします。
このプラグインを導入することでAPIのローカルテストを可能にします。

### AWS へのデプロイ
`serverless deploy`コマンドを使用してアプリケーションを AWS にデプロイできます。
この実装では、API Gatewayを通じてLambda関数（`lineWebhookHandler`のみ）が呼び出されます。
そのため、HTTP POSTリクエスト用のエンドポイントURLのみが出力されます。
`scheduledHandler`はAWS EventBridgeを使用して定期的に実行されるため、エンドポイントURLは出力されません。
```bash
$ serverless deploy
 （中略）
 Deploying amazon-wishlist-to-line to stage dev (ap-northeast-1)
 ✔ Service deployed to stack kindle-wishlist-to-line-dev (49s)

 endpoint: POST - https://XXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/webhook
  functions:
   lineWebhookHandler:  amazon-wishlist-to-line-dev-lineWebhookHandler (71 MB)
   scheduledHandler:  amazon-wishlist-to-line-dev-scheduledHandler (71 MB)
```
詳細なコードは、GitHubのリポジトリでも公開しています。
興味のある方は是非チェックしてみてください。

https://github.com/GorillaSwe/kindle-wishlist-to-line

# まとめ

この記事では、Serverless Frameworkを利用したAWSへのデプロイや管理について解説しました。
間違っている箇所や疑問に思ったことがあれば、ぜひコメントをください！

最後まで読んでいただき、ありがとうございました。

上記Serverless Frameworkを基にしたLINE Botの開発について記事を書いているので、こちらも読んでいただけると嬉しいです。

https://qiita.com/GorillaSwe/items/fc207dd6abc1cd0589ef