【Serverless Framework】nodejsで始めるサーバーレスチュートリアル

# この記事について

はじめにサーバーレスに対して、「サーバーレスに触れたいけどクラウドとか使うから面倒くさそう」、「興味あるけど難しそう」、「サクッと実装できるならいいけど実際どうなの？」などイメージや疑問があることでしょう。そのような方が**実際にサーバーレスに触れ、サーバーレスビギナーへの道の足がかりとなれるようチュートリアル記事です**。

# そもそもサーバーレスとは

**サーバーレスとは、サーバーがないという意味ではなく、サーバーの管理が不要という意味です**。

基本的に、サーバー周りの設定など一切なしで、**何らかのイベントに対してさ実行させたい(関数)コードを登録しておくことで、イベントが発火したら勝手に関数を実行してくれる代物です**。

サービスとしてFaaS(Function as a Service)と言われています。
AWSのlambda、AzureのAzure Functions、GCPのGoogle Cloud Functionsが有名です。

# チュートリアルを実施する前に
このチュートリアルでは、Nodejsで実装していきます。また今回はAWS(Amazon Web Service)のサーバーレス代表格であるlambdaを利用します。そのため、**Nodejsが使えることとAWSアカウントを持っていなければ進めることはできません**。

# チュートリアル

このチュートリアルでは、自身(もしくはkousaku-maron)のQiitaの記事情報を1時間おきに取得し、DBに保存するサーバーレスアプリケーションを作成します。

### Serverless導入

サーバーレスアプリケーションを作成するとき、実行させたいコードをデプロイし、AWSのコンソール画面(GUI)で実行タイミングやログ等の設定をしないといけません。

GUIでゴニョゴニョせずに、AWS側の設定を含めて実行させたいコードとともにデプロイできるフレームワークがあります。便利なフレームワークであるServerlessをインストールしましょう。

```bash
npm install -g serverless
```

Serverlessのインストールが完了したら、今回作成するサーバーレスアプリケーションを作成しましょう。

Serverlessは使用するクラウドサービス、言語に合わせたテンプレートが用意されているので、そちらを利用して作成していきます。

今回は、**lambdaをnodejsでアプリケーションを作るので、テンプレートを`aws-nodejs-ecma-script`で作成します**。ES6/ES7で書けなくてもいいのであれば、テンプレートは`aws-nodejs`でもよいです。

```bash
serverless create --template aws-nodejs-ecma-script --name serverless-sample --path serverless-sample
```

必要なパッケージがまだインストールされていない状態なので、インストールします。

```bash
cd serverless-sample
npm install
```

### Serverlessアプリケーションの構成について

Serverlessアプリケーションには「**関数(実行させたいコード)が書かれたファイル**」と「**クラウド側の設定が書かれたファイル**」があります。

「関数が書かれたファイル」は、各クラウドベンダーのFaaSサービスの書き方なので、説明は省きます。詳しく知りたい方は、各クラウドベンダーのFaaSサービスのドキュメントを読んでみてください。

**「クラウド側の設定が書かれたファイル」は`serverless.yml`に書かれています**。

重要なのは、`provider`と`functions`の項目です。

`provider`には利用するクラウドサービス、ランタイム(言語)等、アプリケーション全体の設定ができます。`functions`にはアプリケーション内で実行させるコード、そのコードを実行させるタイミング、そのコードで利用する環境変数等、関数(コード)個別の設定ができます。

```yml:serverless.yml
service:
  name: serverless-sample

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs6.10

functions:
  first:
    handler: first.hello
  second:
    handler: second.hello
    events:
      - http:
          method: get
          path: second
```

### Serverlessアプリケーションをデプロイしてみる

アプリケーションをAWSへデプロイするとき権限が必要になります。
AWSコンソール画面で、IAMよりServerless用のユーザーを作成します。

**ユーザーの追加からプログラムによるアクセスにチェックを入れ、既存のポリシーを直接アタッチよりAdministratorAccessをアタッチしユーザーを作成しましょう。作成完了後、`key`と`secret`をメモしといて下さい**。

<img width="785" alt="スクリーンショット 2019-02-23 11.59.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/7a5df0e9-ce09-f835-619e-92d0a26e77a8.png">

serverlessのコンフィグに`key`と`secret`を登録しましょう。

```bash
serverless config credentials --provider aws --key メモしたkey --secret メモしたsecret
```

デプロイしてみましょう。

```bash
serverless deploy
```

デプロイが完了すれば、AWSコンソール画面のlambdaで関数がアップロードされたことが確認できるはずです。

<img width="1384" alt="スクリーンショット 2019-02-23 12.14.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/e3cd34df-bb2c-c34c-9529-40ecc961afe7.png">

`serverless-sample-dev-second`を開いてみましょう。
**`serverless.yml`で`second`のイベントトリガーはhttpになっていたので、httpリクエストに対してこの関数は実行されるようになっています**。

※_ブラウザでAPI GatewayのAPI エンドポイントに書いてるURLにアクセスすると実際に関数が実行されます。_

<img width="1334" alt="スクリーンショット 2019-02-23 12.19.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/ee3556ef-111b-f478-c2ed-21dd5d88a948.png">

### 関数内でQiita APIを叩いてみる

関数内でhttpリクエストを送信するため、`axios`をインストールします。

```bash
npm install axios --save
```

functionsにデフォルトで登録されていた`first`と`second`はもう使わないので削除しましょう。ファイルも削除してしまって大丈夫です。代わりに新しく`qiita`を登録し、`qiita.js`を作成しておきましょう。

**async/awaitを使いたいので、ランタイムをnodejs8.10に変更します。また、Qiita APIのエンドポイントを環境変数としてendpointに設定します**。

※_qiitaアカウントを持っているかつ投稿したことがあるのであれば、`kousaku-maron`を自身のアカウント名に変えてください。_

```yml:serverless.yml
service:
  name: serverless-sample

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs8.10

functions:
  qiita:
    handler: qiita.hello
    environment:
      endpoint: https://qiita.com/api/v2/users/kousaku-maron/items
```

`qiita.js`で実行させる関数を書いていきます。

中身は**`axios`で環境変数に登録した`endpoint`へhttpリクエストを送信し、帰ってきたデータを`callback`で返しているだけです**。

```js:qiita.js
import axios from 'axios'

export const hello = async (event, context, callback) => {
  const res = await axios({
    method: 'get',
    url: process.env.endpoint,
    params: {
      page: 1,
      per_page: 100,
    }
  })

  const result = []
  if(res.data) {
    Promise.all(res.data.map(async element => {
      const record = {
        id: element.id,
        title: element.title,
        url: element.url,
        likes_count: element.likes_count,
        created_at: element.created_at,
        updated_at: element.updated_at,
        tags: element.tags,
      }
      result.push(record)
    }))
  }

  callback(null, {
    message: 'qiita article data success.',
    data: result,
    event,
  })
}
```

serverlessにはローカルで関数を実行させる機能があります。
ローカルで実行させて結果が正しく出ているか確認してみましょう。

※_ちなみに`local`を省けば、デプロイされたlambda関数を実行させることができます。_

```bash
serverless invoke local qiita
```

こんな感じで、結果がコンソールに表示されていれば成功です。

```json
{
  "message": "qiita article data success.",
  "data": [
    {
      "id": "3887e57c5c5519abb46d",
      "title": "【Unity】音ゲーの仕組みを学び「〇〇の達人」をUnityで作る　パート3",
      "url": "https://qiita.com/kousaku-maron/items/3887e57c5c5519abb46d",
      "likes_count": 1,
      "created_at": "2019-02-16T15:45:47+09:00",
      "updated_at": "2019-02-16T15:46:29+09:00",
      "tags": [
        {
          "name": "C#",
          "versions": []
        },
        {
          "name": "Unity",
          "versions": []
        },
        {
          "name": "ゲーム制作",
          "versions": []
        }
      ]
    }
    ...
  ]
  ...
}
```

### 取得したデータをDynamoDBに保存してみる

`aws-sdk`を使用するのでインストールします。

```bash
npm install aws-sdk --save
```

Qiita APIで取得したデータをDynamoDBに保存するため、まずは`serverless.yml`にDynamoDBの設定を書き加えます。

※_難しく見えますが、細かく説明しますのでご安心を、、、_

```yml:serverless.yml
service:
  name: serverless-sample

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs8.10
  timeout: 60
  environment: 
    DYNAMODB_TABLE: qiitaTable
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      # Resource: arn:aws:dynamodb:${opt:stage, self:provider.stage}:*:table/${self:provider.environment.DYNAMODB_TABLE}
      Resource: arn:aws:dynamodb:us-east-1:*:table/qiitaTable


functions:
  qiita:
    handler: qiita.hello
    environment:
      endpoint: https://qiita.com/api/v2/users/kousaku-maron/items
      tableName: ${self:provider.environment.DYNAMODB_TABLE}

resources:
  Resources:
    qiitaTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
```

**`provider`には、lambdaのタイムアウト、保存するテーブル名、DynamoDBに保存できるようにするための権限の設定を追記しています**。

タイムアウトはDB保存処理に時間がかかるため余裕をみて適当に設定します。

権限は`iamRoleStatements`で設定でき、どのリソースに対して何のアクションを許可するという形で設定しています。

※_`${self:provider.stage}`の書き方でymlに記載している値を参照できます。しかし、`iamRoleStatements`の`Resource`で使用するとエラーになったため直接値を入力しました。_

```yml
provider:
  name: aws
  runtime: nodejs8.10
  timeout: 60
  environment: 
    DYNAMODB_TABLE: qiitaTable
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      # Resource: arn:aws:dynamodb:${opt:stage, self:provider.stage}:*:table/${self:provider.environment.DYNAMODB_TABLE}
      Resource: arn:aws:dynamodb:us-east-1:*:table/qiitaTable
```

`functions`には、`tableName`の環境変数を設定します。この値は`qiita.js`で利用します。

```yml
functions:
  qiita:
    handler: qiita.hello
    environment:
      endpoint: https://qiita.com/api/v2/users/kousaku-maron/items
      tableName: ${self:provider.environment.DYNAMODB_TABLE}
```

**`recources`という項目を追加し、デプロイ時、DynamoDBのテーブルを作成するように設定します**。

**ここではDynamoDBだけでなく、S3などのリソースも作成するように設定できます**。

DynamoDBのテーブルは必ずプライマリーキーを設定しないといけないので、`AttributeDefinitions`と`KeySchema`で設定します。

```yml
resources:
  Resources:
    qiitaTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
```

qiita.jsで、DynamoDBに保存する処理を書きます。
`aws-sdk`で`putItem`というDynamoDBに保存する関数をPromiseを利用して作成し、Qiita APIで取得したデータを保存させています。

```js:qiita.js
import axios from 'axios'
import AWS from 'aws-sdk'

const documentClient = new AWS.DynamoDB.DocumentClient()

export const hello = async (event, context, callback) => {
  const res = await axios({
    method: 'get',
    url: process.env.endpoint,
    params: {
      page: 1,
      per_page: 100,
    }
  })

  if(res.data) {
    Promise.all(res.data.map(async element => {
      const params = {
        TableName: process.env.tableName,
        Item:{
          'id': element.id,
          'title': element.title,
          'url': element.url,
          'likes_count': element.likes_count,
          'created_at': element.created_at,
          'updated_at': element.updated_at,
          'tags': element.tags,
        }
      }

      await putItem(params)
    }))
  }

  callback(null, {
    message: 'write qiita article data to dynamoDB success.',
    event,
  })
}

const putItem = (params) => {
  return new Promise((resolve, reject) => {
    documentClient.put(params, (err, data) => {
      if (err) reject(err)
      else resolve(data)
    })
  })
}
```

ここまで作成できたら、一度デプロイし、実行させてみましょう。

※_serverless invoke --function qiitaでコマンドラインからlambda関数を実行できます。_

```bash
serverless deploy
serverless invoke --function qiita
```

AWSコンソール画面でDynamoDBを見にいくと、データが入っていることが確認できるはずです。

<img width="889" alt="スクリーンショット 2019-02-23 16.18.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/8c6413ee-bdd6-8c7f-e2ed-78a1cdc02d0a.png">


### 1時間おきに関数を実行させるようにスケジューリングしてみる

`serverless.yml`にちょこっと加えるだけで、作成した関数を1時間おきに実行させるようできます。

`events`を作成し、`schedule`を追加します。
たった２行です、むちゃくちゃ簡単。

※_scheduleの細かい設定は[Rate または Cron を使用したスケジュール式](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html)をみてください。_

```yml:serverless.yml
service:
  name: serverless-sample

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs8.10
  timeout: 60
  environment: 
    DYNAMODB_TABLE: qiitaTable
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      # Resource: arn:aws:dynamodb:${opt:stage, self:provider.stage}:*:table/${self:provider.environment.DYNAMODB_TABLE}
      Resource: arn:aws:dynamodb:us-east-1:*:table/qiitaTable


functions:
  qiita:
    handler: qiita.hello
    events:
      - schedule: cron(0 * * * ? *)
    environment:
      endpoint: https://qiita.com/api/v2/users/kousaku-maron/items
      tableName: ${self:provider.environment.DYNAMODB_TABLE}

resources:
  Resources:
    qiitaTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

```

デプロイしてみましょう。

```bash
serverless deploy
```

AWSコンソール画面でデプロイしたlambda関数をみると、Cloudwatch Eventsがトリガーとして設定されているはずです。

<img width="1308" alt="スクリーンショット 2019-02-23 16.31.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/d09158b3-2d5c-a7b0-3fe8-36c7904b1c9d.png">


### デプロイしたlambda関数を削除しましょう

下のコマンドを実行するだけです。

```bash
serverless remove
```

# プラグインについて(オプション)

このセクションはオプションですので、みなくても特に問題はありません。

チュートリアルでは触れませんでしたが、**ServerlessにはプラグインというServerlessを容易に拡張できる機能があります**。

このセクションではプラグインを使った拡張例をいくつか紹介します。

### DynamoDBを連携した関数をローカルで実行させてみる

ローカルで動くDynamoDBをServerlessで操作するプラグインをインストールします。

※自分の環境では最新のプラグインがちゃんと動作しなかったためバージョンを指定しています。githubの[issue](https://github.com/99xt/serverless-dynamodb-local/issues/210)に上がっていました。


```bash
npm install --save-dev serverless-dynamodb-local@0.2.35
```

`serverless.yml`にインストールしたプラグインを設定します。

```yml:serverless.yml
service:
  name: serverless-sample

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack
  - serverless-dynamodb-local

...
```

プラグインを利用して、ローカルで動くdynamoDBをインストールします。

```bash
serverless dynamodb install
```

dynamoDBのインストールが完了したら、`serverless.yml`にローカルのdynamoDBで使用するテーブルの設定をしましょう。

**`custom`を追加し、`port`を`8000`で起動するように設定しています**。

```yml:serverless.yml
...

custom:
  dynamodb:
    start:
      port: 8000
      inMemory: true
      migrate: true

resources:
  Resources:
    qiitaTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
```

dynamoDBを起動してみましょう。

```bash
serverless dynamodb start
```

`http://localhost:8000/shell/`にアクセスするとdynamoDBを操作できる画面がみれるはずです。

<img width="1437" alt="スクリーンショット 2019-02-26 0.53.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/3e58ad22-8a40-6327-b867-0b9828bcf15b.png">

qiita関数で使用するdynamoDBをローカルのdynamoDBに変更します。
`region`を`endpoint`を指定してあげるだけでローカルのdynamoDBに切り替え可能です。

```js:qiita.js
...

const documentClient = new AWS.DynamoDB.DocumentClient({
  region: 'localhost',
  endpoint: 'http://localhost:8000'
})

...
```

ローカル環境でqiita関数を実行させましょう。

```bash
serverless invoke local --function qiita
```

ローカルのdynamoDBにデータが保存されているか確認します。
dynamoDBを操作できる画面の左側に下記のコードを貼り付けて実行させてみてください。

```js
const params = {
    TableName: 'qiitaTable',
}
dynamodb.scan(params, (err, data) => {
    if (err) ppJson(err)
    else ppJson(data)
})
```

ちゃんとデータが保存されていることが確認できると思います。

<img width="1417" alt="スクリーンショット 2019-02-26 1.05.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/993f19a2-8083-b61c-1fb3-c1cf6431b2ef.png">

最後に8000ポートをしようしているプロセスをキルするコマンドでローカルで起動しているdynamoDBを落としましょう。

```bash
lsof -i :8000 -t | xargs kill
```

### 古いバージョンのコードを整理し容量を削減させてみる

後日、追記

# 最後に

この記事ではQiita APIで取得したデータをDynamoDBに保存させる関数を作成しました。

APIの利用とAWSのリソース操作が学べたはずなので、Twitter APIを使ってBotを作成してみたり、DBのバックアップをとるバッチを作成したり、サーバーレスで色々できるようになったのではないでしょうか。

サンプルコード公開してます。
[https://github.com/kousaku-maron/qiita-sample/tree/master/serverless-sample](https://github.com/kousaku-maron/qiita-sample/tree/master/serverless-sample)