Kosaku Kurino

Kosaku Kurino

【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をインストールしましょう。

npm install -g serverless

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

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

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

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

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

cd serverless-sample
npm install

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

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

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

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

重要なのは、providerfunctionsの項目です。

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

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をアタッチしユーザーを作成しましょう。作成完了後、keysecretをメモしといて下さい

スクリーンショット 2019-02-23 11.59.32.png

serverlessのコンフィグにkeysecretを登録しましょう。

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

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

serverless deploy

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

スクリーンショット 2019-02-23 12.14.56.png

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

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

スクリーンショット 2019-02-23 12.19.55.png

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

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

npm install axios --save

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

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

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

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で返しているだけです

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関数を実行させることができます。

serverless invoke local qiita

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

{
  "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を使用するのでインストールします。

npm install aws-sdk --save

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

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

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に記載している値を参照できます。しかし、iamRoleStatementsResourceで使用するとエラーになったため直接値を入力しました。

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で利用します。

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のテーブルは必ずプライマリーキーを設定しないといけないので、AttributeDefinitionsKeySchemaで設定します。

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-sdkputItemというDynamoDBに保存する関数をPromiseを利用して作成し、Qiita APIで取得したデータを保存させています。

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関数を実行できます。

serverless deploy
serverless invoke --function qiita

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

スクリーンショット 2019-02-23 16.18.52.png

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

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

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

scheduleの細かい設定はRate または Cron を使用したスケジュール式をみてください。

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

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

serverless deploy

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

スクリーンショット 2019-02-23 16.31.35.png

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

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

serverless remove

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

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

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

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

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

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

※自分の環境では最新のプラグインがちゃんと動作しなかったためバージョンを指定しています。githubのissueに上がっていました。

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

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

service:
  name: serverless-sample

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

...

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

serverless dynamodb install

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

customを追加し、port8000で起動するように設定しています

...

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を起動してみましょう。

serverless dynamodb start

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

スクリーンショット 2019-02-26 0.53.35.png

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

...

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

...

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

serverless invoke local --function qiita

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

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

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

スクリーンショット 2019-02-26 1.05.41.png

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

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