無印吉澤

Site Reliability Engineering(SRE)、ソフトウェア開発、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

Webhook の受信を契機として複数の API を叩く Lambda 関数を Node.js で書きたいときのためのメモ

f:id:muziyoshiz:20180630010936p:plain

ときどき AWS Lambda の Lambda 関数を Python で書いてます。AWS Lambda 便利ですよね。

しかし最近、他の人に Lambda 関数をいじってほしいけど Python での開発環境を作ってもらうのが面倒なので、一度書いた Lambda 関数を Node.js で書き換える、という機会がありました。そのときに Node.js の流儀がわからず困ってしまったので、あとで自分が読み返すためのメモを書いておきます。

このメモの対象読者は次のような人です。

  • Webhook の受信を契機として、複数の API を叩くような処理が書きたい
  • その処理を AWS Lambda に乗せて、楽に運用したい
  • AWS Lambda と API Gateway へのデプロイを楽にしたいので Serverless Framework を使いたい
  • 開発環境構築を楽にするために Node.js で書きたい(Serverless Framework を使うなら Node.js もあるはず)

※注意事項:私は Node.js 初心者で、Node.js の書き方はよくわかってません。なので、もっと良い書き方がわかったら、このメモをちょっとずつ直していきます。

目次

Serverless Framework のインストール

  • Node.js のインストール
    • brew install npm
  • Serverless Framework のインストール
    • npm install -g serverless

参考:Serverless Framework - AWS Lambda Guide - Installing The Serverless Framework

AWS クレデンシャルの設定

AWS クレデンシャルを設定していない場合、sls コマンドで作れます。~/.aws/credentials に設定済みであれば、この手順は不要です。デフォルトプロファイル以外でも大丈夫です。

  • Administrator Access 権限のある IAM ユーザーの作成
  • AWS クレデンシャルの設定
    • sls config credentials --provider aws --key アクセスキーID --secret シークレットアクセスキー

参考:Serverless Framework - AWS Lambda Guide - Credentials

新しい Serverless service (Node.js 8.10) の作成

Lambda 関数のランタイムには、async/await が使える Node.js 8.10 を選択します。現時点で、AWS Lambda で使える最新の Node.js はコレです。

ただ、6/30現在では sls create -t aws-nodejs でひな型(boilerplate)を作ると、Node.js 6.10 が選択されてしまいます。

$ sls create -t aws-nodejs -p example
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/myoshiz/example"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.27.3
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"

上記のコマンドを実行すると、以下のファイルが作られます。

  • .gitignore
  • handler.js
  • serverless.yml

この serverless.yml の

provider:
  name: aws
  runtime: nodejs6.10

となっている部分を

provider:
  name: aws
  runtime: nodejs8.10

に書き換えると、Node.js 8.10 が使えます。

参考:Node.js 8.10 runtime now available in AWS Lambda | AWS Compute Blog

デフォルトステージの設定

Serverless Framework には「ステージ」という概念があって、1個のコードを、ステージごとに Lambda 関数名や設定を変えてデプロイできます。ステージの主な用途は、開発環境と本番環境の切り替えです。

このステージを引数から指定できるようにするための設定を、serverless.yml に追加します。

provider:
  name: aws
  runtime: nodejs8.10
  stage: ${opt:stage, self:custom.defaultStage}
custom:
  defaultStage: dev

Serverless Framework のデフォルトのステージは dev なので、そのままで使うならこの設定は不要です。しかし dev が不要な場合は、デフォルトを production にしておくといいでしょう。

参考:Serverless Framework - AWS Lambda Guide - Deploying

デフォルトで使うAWSプロファイルおよびリージョンの設定

AWS プロファイルとリージョンも、引数から指定可能にしておきます。以下の設定を serverless.yml に追加します。

  profile: ${opt:profile, self:custom.defaultProfile}
  region: ${opt:region, self:custom.defaultRegion}
  defaultProfile: default
  defaultRegion: ap-northeast-1

ステージに応じて AWS プロファイルを切り替えたい場合は、以下の方法で実現できるようです。

参考:Serverless Frameworkで環境変数を外部ファイルから読み込み、環境毎に自動で切り替えてみる | Developers.IO

環境変数の定義

AWS Lambda で使いたい機密情報(パスワードや API キーなど)は、「環境変数」として AWS Lambda に登録できます。しかし、この環境変数は、git リポジトリには登録したくありません。

Serverless Framework では、

  • 環境変数を git リポジトリ管理外の YAML ファイルに書く
  • デプロイ時のみこの YAML ファイルを用意する

ということが可能です。

まず、以下の YAML ファイルを用意します。

conf/dev.yml - dev ステージで使う環境変数
conf/production.yml - production ステージで使う環境変数

そして、.gitignore に以下のように書いて、git リポジトリへの登録を禁止します。

# Secrets
conf/*.yml

最後に、serverless.yml に以下の設定(otherfile: 以下)を追加します。

custom:
  defaultStage: dev
  defaultProfile: default
  defaultRegion: ap-northeast-1
  otherfile:
    environment:
      dev: ${file(./conf/dev.yml)}
      production: ${file(./conf/production.yml)}

YAML ファイル(dev.yml, production.yml)には、以下のように記載します。環境変数に数値を代入したい場合は、ダブルクォートで囲んで、文字列にする必要があります。これは Serverless Framework の問題ではなく、AWS Lambda の制約です。

---
MESSAGE: Hello
YEAR: "2018"

参考:Serverless Frameworkで環境変数を外部ファイルから読み込み、環境毎に自動で切り替えてみる | Developers.IO

request-promise モジュールのインストール

API の呼び出しには request-promise モジュールを使います。このモジュールを使うためには request モジュールも必要です。

$ npm init
$ npm install --save request
$ npm install --save request-promise

package.json と package-lock.json が作成されます。このファイルは、ソースコードと一緒に git リポジトリにコミットします。

参考:request/request: Simplified HTTP request client., request/request-promise: The simplified HTTP request client 'request' with Promise support. Powered by Bluebird.

開発用プラグインの追加

Webhook を受け付けるためには、Amazon API Gateway と AWS Lambda を組み合わせて使うのが一般的です。

しかし、動作確認のためにいちいち API Gateway にデプロイするのは面倒なので、serverless-offline プラグインを使って、ローカルで動作確認できるようにします。

以下のコマンドでインストールできます。

$ npm install --save-dev serverless-offline

serverless.yml に以下の設定を追加。

plugins:
  - serverless-offline

sls offline start を実行すると、ローカルで HTTP サーバが動作します。

参考:dherault/serverless-offline: Emulate AWS λ and API Gateway locally when developing your Serverless project

handler の定義(Amazon API Gateway の設定)

sls create コマンドで作られた Lambda 関数 "hello" に handler の設定(events: 以下)を追加してみます。

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: hello
          method: post
    environment:
      MESSAGE: ${self:custom.otherfile.environment.${self:provider.stage}.MESSAGE}
      YEAR: ${self:custom.otherfile.environment.${self:provider.stage}.YEAR}

先ほど説明した環境変数を Lamnbda 関数内で使うには、environment: 以下で明示的に指定する必要があります。

そして、handler.js 内の hello 関数を以下のように修正してみましょう。リモートにデプロイすると、この関数は API Gateway から起動されます。

module.exports.hello = (event, context, callback) => {
    console.log(`MESSAGE: ${process.env.MESSAGE}`)
    console.log(`YEAR: ${process.env.YEAR}`)

    console.log(event);

    const body = event['body'];

    try {
        const json = JSON.parse(body);

        if (json && json.message) {
            callback(null, {
                statusCode: 200,
                body: JSON.stringify({message: `${process.env.MESSAGE} ${json.message} ${process.env.YEAR}`})
            })
        } else {
            callback(null, {statusCode: 400, body: JSON.stringify({error: 'No message'})});
        }
    } catch (e) {
        console.log(e);
        callback(null, {statusCode: 400, body: JSON.stringify({error: 'Invalid JSON'})});
    }
};

ローカルでの動作確認

この hello 関数をローカルで動作確認します。まず、以下のコマンドで HTTP サーバを起動します。

$ sls offline start
Serverless: Starting Offline: dev/ap-northeast-1.

Serverless: Routes for hello:
Serverless: POST /hello

Serverless: Offline listening on http://localhost:3000

他のターミナルから curl で JSON を POST し、応答が返されたら成功です。

$ curl -H 'Content-Type:application/json' -d '{"message":"world"}' http://localhost:3000/hello
{"message":"Hello world 2018"}

sls offline start を実行したターミナルには、console.log() に渡した event オブジェクトが以下のように出力されます。

Serverless: POST /hello (λ: hello)
MESSAGE: Hello
YEAR: 2018
{ headers:
   { Host: 'localhost:3000',
     'User-Agent': 'curl/7.54.0',
     Accept: '*/*',
     'Content-Type': 'application/json',
     'Content-Length': '19' },
  path: '/hello',
  pathParameters: null,
  requestContext:
   { accountId: 'offlineContext_accountId',
     resourceId: 'offlineContext_resourceId',
     apiId: 'offlineContext_apiId',
     stage: 'dev',
     requestId: 'offlineContext_requestId_27367216451093634',
     identity:
      { cognitoIdentityPoolId: 'offlineContext_cognitoIdentityPoolId',
        accountId: 'offlineContext_accountId',
        cognitoIdentityId: 'offlineContext_cognitoIdentityId',
        caller: 'offlineContext_caller',
        apiKey: 'offlineContext_apiKey',
        sourceIp: '127.0.0.1',
        cognitoAuthenticationType: 'offlineContext_cognitoAuthenticationType',
        cognitoAuthenticationProvider: 'offlineContext_cognitoAuthenticationProvider',
        userArn: 'offlineContext_userArn',
        userAgent: 'curl/7.54.0',
        user: 'offlineContext_user' },
     authorizer:
      { principalId: 'offlineContext_authorizer_principalId',
        claims: undefined },
     protocol: 'HTTP/1.1',
     resourcePath: '/hello',
     httpMethod: 'POST' },
  resource: '/hello',
  httpMethod: 'POST',
  queryStringParameters: null,
  stageVariables: null,
  body: '{"message":"world"}',
  isOffline: true }
Serverless: [200] {"statusCode":200,"body":"{\"message\":\"Hello world 2018\"}"}

デプロイ

ローカルで動作確認できたら、リモートの AWS Lambda と Amazon API Gateway にデプロイします。デプロイは sls deploy コマンドで行います。

$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (2.28 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............................
Serverless: Stack update finished...
Service Information
service: example
stage: dev
region: ap-northeast-1
stack: example-dev
api keys:
  None
endpoints:
  POST - https://aaaaaaaa.execute-api.ap-northeast-1.amazonaws.com/dev/hello
functions:
  hello: example-dev-hello

サービス名-ステージ名-関数名 という名前の Lambda 関数が作られました(この例では example-dev-hello)。本番環境のステージを指定するときは sls コマンドに --stage production を付けてください。

デプロイに成功すると、ローカルの場合と同じように動作確認できます。

$ curl -H 'Content-Type:application/json' -d '{"message":"world"}' https://aaaaaaaa.execute-api.ap-northeast-1.amazonaws.com/dev/hello

1回デプロイしたあとなら、sls deploy -f hello のようにして、特定の Lambda 関数だけ更新することもできます。

$ sls deploy -f hello

ちなみに、Lambda 関数がもう要らなくなったという場合は sls remove で削除できます。

リモートでの動作確認

Lambda 関数がうまく動かないときにはログを確認したくなると思いますが、その場合は console.log() でログ出力すると、CloudWatch Logs から確認できます。

ログは /aws/lambda/example-dev-hello のような名前のロググループに登録されます。

Webhook を受信したら複数の API を叩く

ここまでで、Webhook を受信したらなにかする、という Lambda 関数は書けました。次は、複数の API を叩くコードの例を紹介します。

API を1個叩くだけなら、request モジュールのみで実現できます。

しかし、ある API を叩き、その返り値を元に別の API を叩き、さらにその返り値を元に……と処理が続く場合は、コールバック地獄を避けるために request-promise を使ったほうが楽です。また、Promise を使うだけでなく、async/await も使ったほうがコードが読みやすくなります。

ここでは、試しに、先ほどの hello を3回呼び出すエンドポイント hello_hello_hello を作ってみます。

まず、serverless.yml に以下を追加します。これはローカルでの動作確認(後述)を簡単にするために method: get にしていますが、実際に Webhook から起動するときは method: post にしてください。

  hello_hello_hello:
    handler: handler.helloHelloHello
    events:
      - http:
          path: hello_hello_hello
          method: get

そして、handler.js に以下のコードを追加します。await で1個目の API からの応答が来るのを待ってから、次の API 呼び出しをしているのがわかるでしょうか。

module.exports.helloHelloHello = (event, context, callback) => {
    const callHello = (message) => {
        const request = require('request-promise');

        const params = {
            message: message
        };

        const options = {
            method: 'POST',
            uri: `http://localhost:3000/hello`,
            body: params,
            json: true
        };

        return request(options)
            .then(function (parsedBody) {
                return parsedBody;
            })
            .catch(function (err) {
                console.log(err);
                return null;
            });
    };

    (async () => {
        const response1 = await callHello('world');
        console.log(response1);

        const response2 = await callHello(response1.message);
        console.log(response2);

        const response3 = await callHello(response2.message);
        console.log(response3);

        callback(null, {statusCode: 200, body: response3.message});
    })();
};

この関数は、以下のように動作します。

  • http://localhost:3000/hello に "world" を送り、"Hello world 2018" を受け取る
  • http://localhost:3000/hello に "Hello world 2018" を送り、"Hello Hello world 2018 2018" を受け取る
  • http://localhost:3000/hello に "Hello Hello world 2018 2018" を送り、"Hello Hello Hello world 2018 2018 2018" を受け取る
  • 最終的な結果 "Hello Hello world 2018 2018" を呼び出し元に返す。

sls offline start で HTTP サーバを起動して、curl で以下のコマンドを実行してみてください。

$ curl http://localhost:3000/hello_hello_hello
Hello Hello Hello world 2018 2018 2018

これと同じ要領で、hello 以外の API も呼び出すことができます。

基本は以上です。あとは頑張ってください。

付録1. Windows の場合

Windows の場合は、Serverless Framework のインストール方法が違います。また、普通は curl コマンドが使えないので、別の方法を使う必要があります。

Serverless Framework のインストール

まず node.js から Windows 用のインストーラをダウンロードして、Node.js をインストールします。

次に、Windows のメニューから "Node.js command prompt" を選択して、プロンプトを開きます。このプロンプトで npm install -g serverless を実行すれば、Serverless Framework をインストールできます。

これ以降は、"Node.js command prompt" から、上記の sls コマンドを実行できます。

curl コマンドの代わり

curlコマンドの代わりに、Node.js の REPL からテスト用のリクエストを送信できる。

JSON を POST して、HTTP リクエストのボディを標準出力に表示。

C:\> node
> const request = require('request');
> const options = {url: 'http://localhost:3000/hello', headers: {'Content-type': 'application/json'}, json: {message: 'world'} };
> request.post(options, function(error, response, body){ console.log(body); });

GET して、HTTP リクエストのボディを標準出力に表示。

C:\> node
> const request = require('request');
> request.get('http://localhost:3000/hello_hello_hello', function(error, response, body){ console.log(body); });

付録2. IntelliJ IDEA Ultimate で Node.js 8.10 を使う

僕は IndelliJ IDEA Ultimate でコードを書いているのですが、Node.js 8.10 でエラーが出ない状態にするには以下の設定が必要でした。

  • Preferences > Plugins > Install JetBrains Plugin... を選択し、その画面から NodeJS プラグインをインストールする
  • Preferences > Languages & Frameworks > Node.js and NPM を選択し、その画面から Node.js Core library を有効にする(Enable ボタンを押す)
  • Preferences > Languages & Frameworks > JavaScript を選択し、その画面上で JavaScript language version を "ECMAScript 6" に変更する

参考:javascript - Arrow function "expression expected" syntax error - Stack Overflow

その他の参考ページ