Serverless Framework の使い方を初心者にも分かりやすく説明する

2023/10/28現在、Node.jsのLTSは20.9.0になった。昔の記事でも見てくれている方がいましたので、最新の環境で動作するか確認し、記事を更新しました。

# はじめに

これは[Ateam Brides Inc. Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ateam-brides)(現:エイチームライフデザイン)の二日目の記事です。

さて、今回は業務で`Serverless Framework`を使ったので基本的な使い方を紹介したいと思います:zap: 

# Serverless Frameworkってどうやって動いてるの？

全体のイメージを持って取り掛かると理解が早いと思い、概要図を描きました:memo:

![Serverless Frameworkの概要図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/08fa9310-185d-3a02-5051-8c684cba2e07.png)

開発者は1〜3をするだけで、必要なAWSのリソースを作成してくれます:tada:<details><summary>補足</summary>概要図では最終的にLambdaとAPI Gatewayのリソースを作成していますが、API Gatewayの部分はLambdaが動くトリガーだったり使用するリソースになるので、作りたいものによって変わります。今回は、実際に作ってみる雛形に合わせたリソースを概要図に描いています。</details>

今までAWSコンソールでポチポチしてリソース作成したり、バラバラだったLambda等の**運用方法が統一され開発速度が上がることが Serverless Framework を導入するメリット**かと思います。



# 開発環境を準備する

それでは、早速、動かしてみましょう。私の実行環境は以下の通りです:eyes: 

```text:実行環境
OS: macOS Ventura v13.5.2
node: v18.16.0
```

## 1. Serverless Framework をインストール

まずは公式ドキュメントに従って、自分のマシンに`Serverless Framework`をインストールしましょう:wrench: 

```sh:ServerlessFrameworkをインストール
# インストール
$ npm install -g serverless

# バージョン確認
serverless --version

> Framework Core: 3.36.0
> Plugin: 7.1.0
> SDK: 4.4.0
```

## 2. Serverless Framework が使う IAMユーザーを作る

概要図にも書いた通り、`Serverless Framework`はデプロイコマンド一つでAWSの各リソースを作成してくれます。その際に、**リソースを作成する権限をもったIAMユーザーが必要になります**:santa:

それでは早速、AWSコンソールからIAMユーザーを作成してみましょう。


### AWSコンソールから IAMユーザーを追加
- ユーザー名：　`serverless-servicename-agent`
- AWS アクセスの種類を選択: アクセスキー - プログラムによるアクセス のみ
- 既存のポリシーから直接アタッチ
    - ポリシーの作成
        - JSON
            - [公式ドキュメントで紹介されている設定](https://gist.github.com/ServerlessBot/7618156b8671840a539f405dea2704c8)からコピペします（最終的には必要な権限のみ残して運用してください）
    - ポリシー名： `serverless-servicename-policy`
- ポリシーを作成後、リロードして作成したポリシーにチェックをつけてユーザを作成
![リロードして作成したポリシーを反映](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/b2ed18f2-bac5-f361-9bf2-923307553baa.png)
- ユーザーが作成されたら、ローカルコードから使用するためのアクセスキーを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/44084a82-1511-2942-010f-50a625d671f5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/8e5a63f5-2a9c-c86a-2976-87af9210780b.png)

**作成したら、「アクセスキー ID」と「シークレットアクセスキー」をメモしておいてください（後で使います）**

## 3. 作成した IAMユーザーのクレデンシャル情報を自分のマシンに設定する

手順2で作成した IAMユーザーを使って自分のマシンからデプロイするためには、クレデンシャル情報（「アクセスキーID」と「シークレットアクセスキー」）を認証情報ファイルに記載する必要があります:lock:

下記のコマンドの「アクセスキーID」と「シークレットアクセスキー」の箇所を先程メモした値に書き換えて実行してください。
`profile`オプションを指定することで名前付きプロファイルにしています。（今回は分かりやすいようにIAMユーザー名に合わせています。）

```sh:AWSクレデンシャル情報を認証情報ファイルに設定する
$ serverless config credentials --provider aws --key アクセスキーID --secret シークレットアクセスキー --profile serverless-servicename-agent

# 確認（コマンドqで終了）
less ~/.aws/credentials

# profileオプションに指定したプロファイル名のクレデンシャル情報が追加されていることを確認
> [default]
> aws_access_key_id=アクセスキーID
> aws_secret_access_key=シークレットアクセスキー
> 
> [serverless-servicename-agent]
> aws_access_key_id=アクセスキーID
> aws_secret_access_key=シークレットアクセスキー
```

# プロジェクトを作成

開発環境が準備できたら、峠は超えました。
下記のコマンドを実行してプロジェクトを作成しましょう:baby:

```sh:プロジェクトを作成
$ mkdir my-special-service
$ cd my-special-service
$ serverless create --template aws-nodejs-typescript --name my-special-service
$ npm install
```

- `name`オプションで指定したサービスを作成します
- <details><summary>サービス名にスネークケースは使わないほうがよいでしょう</summary>
    - 余談：私はサービス名を`my_special_service`のようにスネークケースで設定したのですが、デプロイでS3バケットが自動作成される際に、S3の命名規則（`小文字、数字、ドット (.)、およびハイフン (-) のみ`）に違反しておりエラーになってしまいました。もちろん、S3バケット名は任意の名称を設定できるので回避策もありますが、最初は公式ドキュメント通りがオススメです。</details>


下記のフォルダ構成でひな形が作成されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/595ea19f-06c8-caef-372a-8a2788cc61b0.png" alt="aws-nodejs-typescriptの雛形" width="350" />

## 知っておきたいserverless.tsファイル

このファイルが`Serverless Framework`のconfigファイルであり、とて〜も重要なファイルになります。
このファイルを自由に扱えたら`Serverless Framework`中級者なのかなと思います（私は初心者）。

ざっくりと設定項目を洗ってみたいと思います:point_up:

| key | 内容 | 使い所 |
|:-:|:-:|:-:|
| provider  | どこのクラウドサービスをどんな設定で動かすか(AWS以外もGCPとか対応してる） | slsコマンドのデフォルト値とか設定しておくとオプションで指定しなくてもよくなる|
| functions  | lambdaファンクションとそのトリガーの設定  | |
| layers  | lambdaファンクションが使うlayerの設定  | node_moduleや共通処理を切り出してくれる |
| resources  | lambdaファンクションが使うリソースを事前に定義 | 定義したリソースはデプロイ時に自動作成してくれる |
| plugins  | 機能拡張できるプラグインを設定します | 有名なのだとserverless-offlineプラグインを入れてローカルで動かしたり |

# サービスをデプロイする

概要図の`3.デプロイコマンドを実行`に該当します。
ここで紹介する`sls deploy`というコマンドはこれからいっぱい使うことになります。（`sls`は`servelessの`略）
しかし、**まだ下記のコマンドは実行しないでください**:hankey:


```sh:デプロイコマンド（実行しないで！）
$ sls deploy --aws-profile serverless-servicename-agent --region ap-northeast-1 --stage dev --verbose
```

- `aws-profile`オプションの値は先に設定した名前付きプロファイルの名前を指定します
- `aws-profile`や`region`、`stage`オプションは`serverless.ts`に設定しておけばCLIオプションで指定する必要がなくなります（後述します）

## serverless.ts の設定を調整

先述のとおり、デプロイのたびにCLIオプションを書くのは面倒ですね。
そこで、CLIオプションの値を`serverless.ts`に設定することで、毎回書く手間を省いてくれます:wrench: 

```diff:serverless.tsにオプション値を設定してCLIオプションを省略する
...
  provider: {
    name: 'aws',
    runtime: 'nodejs14.x',
+   profile: 'serverless-servicename-agent',
+   region: 'ap-northeast-1',
+   stage: "${opt:stage, 'dev'}",
    apiGateway: {
      minimumCompressionSize: 1024,
      shouldStartNameWithService: true,
    },
    environment: {
      AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
      NODE_OPTIONS: '--enable-source-maps --stack-trace-limit=1000',
    },
    lambdaHashingVersion: '20201221',
  },
...
```

- `stage`に指定した`${opt:stage, 'dev'}`ですが、これは`${可変のソース, デフォルト値}`の形になっています。`opt`（オプション）以外にも`env`（環境変数）など色んなソースを参照できます。

書いてみて分かると思うのですが、typescriptの型によってインテリセンスが効くので書きやすいですし、他にも用意されている設定を知ることもできて面白いです。

さて、これで先程のデプロイコマンドはだいぶスッキリ書くことができます。デプロイしてみましょう:rocket: 

```sh:すっきりしたデプロイコマンド
$ sls deploy --verbose
```

- `--verbose`オプションは任意です。つけておくと`Serverless Framework`がどうやって動いているか理解しやすくなるので最初はオススメです
- もし、デプロイする前に構文を検証したい場合は `deploy` の部分を `package` に変更して実行してください

## エラーがでるかも？（cloudformation:DeleteChangeSet権限がない）

最新バージョンでは下記のエラーが発生するかもしれません。

```
User: arn:aws:iam::99999999999:user/serverless-servicename-agent is not authorized to perform: cloudformation:DeleteChangeSet on resource: arn:aws:cloudformation:ap-northeast-1:99999999999:stack/my-special-service-dev/db85a570-7572-11re-9dc9-06df524be86t because no identity-based policy allows the cloudformation:DeleteChangeSet action
```

`serverless-servicename-agent`ユーザーに`cloudformation:DeleteChangeSet`権限が許可れれていないというエラーなので、下記のように更新してください。

```diff_json:serverless-servicename-policyポリシーを更新
{
    "Statement": [
        {
            "Action": [
                "apigateway:*",
+               "cloudformation:DeleteChangeSet",
                "cloudformation:CancelUpdateStack",
                "cloudformation:ContinueUpdateRollback",
                "cloudformation:CreateChangeSet",
                "cloudformation:CreateStack",
                "cloudformation:CreateUploadBucket",
                "cloudformation:DeleteStack",
                "cloudformation:Describe*",
                "cloudformation:EstimateTemplateCost",
                "cloudformation:ExecuteChangeSet",
                "cloudformation:Get*",
                "cloudformation:List*",
                "cloudformation:UpdateStack",
                "cloudformation:UpdateTerminationProtection",
                "cloudformation:ValidateTemplate",
                "dynamodb:CreateTable",
                "dynamodb:DeleteTable",
                "dynamodb:DescribeTable",
                "dynamodb:DescribeTimeToLive",
                "dynamodb:UpdateTimeToLive",
                "ec2:AttachInternetGateway",
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:CreateInternetGateway",
                "ec2:CreateNetworkAcl",
                "ec2:CreateNetworkAclEntry",
                "ec2:CreateRouteTable",
                "ec2:CreateSecurityGroup",
                "ec2:CreateSubnet",
                "ec2:CreateTags",
                "ec2:CreateVpc",
                "ec2:DeleteInternetGateway",
                "ec2:DeleteNetworkAcl",
                "ec2:DeleteNetworkAclEntry",
                "ec2:DeleteRouteTable",
                "ec2:DeleteSecurityGroup",
                "ec2:DeleteSubnet",
                "ec2:DeleteVpc",
                "ec2:Describe*",
                "ec2:DetachInternetGateway",
                "ec2:ModifyVpcAttribute",
                "events:DeleteRule",
                "events:DescribeRule",
                "events:ListRuleNamesByTarget",
                "events:ListRules",
                "events:ListTargetsByRule",
                "events:PutRule",
                "events:PutTargets",
                "events:RemoveTargets",
                "iam:AttachRolePolicy",
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:DeleteRolePolicy",
                "iam:DetachRolePolicy",
                "iam:GetRole",
                "iam:PassRole",
                "iam:PutRolePolicy",
                "iot:CreateTopicRule",
                "iot:DeleteTopicRule",
                "iot:DisableTopicRule",
                "iot:EnableTopicRule",
                "iot:ReplaceTopicRule",
                "kinesis:CreateStream",
                "kinesis:DeleteStream",
                "kinesis:DescribeStream",
                "lambda:*",
                "logs:CreateLogGroup",
                "logs:DeleteLogGroup",
                "logs:DescribeLogGroups",
                "logs:DescribeLogStreams",
                "logs:FilterLogEvents",
                "logs:GetLogEvents",
                "logs:PutSubscriptionFilter",
                "s3:CreateBucket",
                "s3:DeleteBucket",
                "s3:DeleteBucketPolicy",
                "s3:DeleteObject",
                "s3:DeleteObjectVersion",
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:ListAllMyBuckets",
                "s3:ListBucket",
                "s3:PutBucketNotification",
                "s3:PutBucketPolicy",
                "s3:PutBucketTagging",
                "s3:PutBucketWebsite",
                "s3:PutEncryptionConfiguration",
                "s3:PutObject",
                "sns:CreateTopic",
                "sns:DeleteTopic",
                "sns:GetSubscriptionAttributes",
                "sns:GetTopicAttributes",
                "sns:ListSubscriptions",
                "sns:ListSubscriptionsByTopic",
                "sns:ListTopics",
                "sns:SetSubscriptionAttributes",
                "sns:SetTopicAttributes",
                "sns:Subscribe",
                "sns:Unsubscribe",
                "states:CreateStateMachine",
                "states:DeleteStateMachine"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ],
    "Version": "2012-10-17"
}
```


## デプロイの実行結果

先程のデプロイコマンドの出力ログを見てみます:mag: 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/765cc835-d73a-ce94-48da-3076cd18da55.png)
〜
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/15b09664-2b4e-1b03-def9-99fb39c89a64.png)

改めて概要図を見ると、`--verbose`オプションで出力したログの流れと合っていることが確認できるかと思います。

![Serverless Frameworkの概要図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/08fa9310-185d-3a02-5051-8c684cba2e07.png)

- ローカルディレクトリを見るとデプロイ用に`.serverless`が作成されていることが分かります
- 実際にAWSコンソールを見ると、各リソースが作成されています（Lambda関数が見つからない場合は東京リージョンになっていないかもしれません）


### ローカルから実行してログを見る

デプロイしたLambda関数をローカルから実行してみましょう。

<details><summary>テンプレートにあるhello関数（`src/functions/hello/handler.ts`）を見ると、引数に`event`を受け取り、`event.body.name`を出力していることが分かります。</summary><div>

```typescript:src/functions/hello/handler.ts
const hello: ValidatedEventAPIGatewayProxyEvent<typeof schema> = async (event) => {
  return formatJSONResponse({
    message: `Hello ${event.body.name}, welcome to the exciting Serverless world!`,
    event,
  });
}
```

この`event`に同階層にある`mock.json`を渡したい

```json:src/functions/hello/mock.json
{
  "headers": {
    "Content-Type": "application/json"
  },
  "body": "{\"name\": \"Frederic\"}"
}
```

</div></details>

この`event`は、`--path`or`-p`オプションでjsonファイルを指定することで渡すことができます。コマンドは以下の通りです。

```sh:ローカルから実行するコマンド
$ sls invoke -f hello -p src/functions/hello/mock.json

> Running "serverless" from node_modules
> {
>     "statusCode": 200,
>     "body": "{\"message\":\"Hello Frederic, welcome to the exciting Serverless world!\",\"event\":{\"headers\":{\"Content-Type\":\"application/json\"},\"body\":{\"name\":\"Frederic\"},\"rawBody\":\"{\\\"name\\\": \\\"Frederic\\\"}\"}}"
> }
```

```sh:ログを見るコマンド
$ sls logs -f hello

> Running "serverless" from node_modules
> INIT_START Runtime Version: nodejs:14.v39       Runtime Version ARN: arn:aws:lambda:ap-northeast-1::runtime:78812e04a9860255e6713cb1e4f6e6092ec4c3cc2ad1a87256041a81269654fd
> START
> END Duration: 4.86 ms (init: 189.27 ms) Memory Used: 58 MB
```

- Lambdaの実行ログがローカルからも確認できました


## サービスを削除する

お金かかるしね、消しときましょう:joy:

```sh
$ sls remove --verbose
```

# おわりに

初めて業務で`Serverless Framework`を触りましたが、公式ドキュメントが丁寧に書いてあるので理解しやすかったです。サーバーに負荷の高い画像処理なんかを任せちゃえるし、今後も使っていきたいなと思いました:relaxed:

[Ateam Brides Inc. Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ateam-brides)の3日目は、
@Shuni がお送りします！！どんなネタを用意してくるのか楽しみです！！

## 追記

2025/01/22: Serverless Framework v4から有料になったので、SAMを使ってみたい方は下記の記事もご覧ください。
[2024年 Serverless Framework v3 から SAM に移行するときに考えたこと、やったこと](https://qiita.com/mkin/items/9684387591fa853e2c4a)



# 参考

https://aws.amazon.com/jp/blogs/apn/deploying-code-faster-with-serverless-framework-and-aws-service-catalog/

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