aws-serverless-express で API をサーバーレス化する

Node.js のフレームワークである Express の書き方で、デプロイすると AWS Lambda + API Gateway で動く aws-serverless-express を使ってみました。Express で作った既存の API を大幅に作り変えることなしにサーバーレス化することができます。

前提条件

  • AWS アカウントを作成し、aws-cli が設定済みの状態にしておいてください。

まずは、普通に Express で API を作成

  • パッケージをインストールします。
npm init
npm install express --save
npm install cors --save
  • index.js の中身は以下のようにしておきます。
const express = require('express');
const app = express();
const cors = require('cors');

app.use(express.json());
app.use(cors());

app.get('/api/info', (req, res) => {
  res.send({ application: 'sample-app', version: '1.0' });
});

app.post('/api/v1/getback', (req, res) => {
  res.send({ ...req.body });
});

app.listen(3000, () => console.log(`Listening on: 3000`));
  • 処理の内容は以下のとおりです:
    • /api/info への GET でのリクエストが有った場合に、API の情報を返却
    • /api/v1/getback への POST でのリクエストがあった場合に、リクエストの body の内容を返却
  • できたら動作確認をします。
node index.js
curl http://localhost:3000/api/info #=> {"application":"sample-app","version":"1.0"}
curl -X POST -H "Content-Type: application/json" -d '{"Name":"hoge", "Age":"100"}' http://localhost:3000/api/v1/getback #=> {"Name":"hoge","Age":"100"}

aws-serverless-express をセットアップ

  • ここから、先ほど作成した API をサーバーレス化していきます。
  • まず、aws-serverless-express をインストールします。
npm install --save aws-serverless-express
  • 次に、必要なスクリプトyaml ファイルをプロジェクト内にコピーしてきます。プロジェクトのルートで以下を実行します。
git clone https://github.com/awslabs/aws-serverless-express.git ase
cp -r ase/examples/basic-starter/scripts .
cp ase/examples/basic-starter/api-gateway-event.json .
cp ase/examples/basic-starter/cloudformation.yaml .
cp ase/examples/basic-starter/lambda.js .
cp ase/examples/basic-starter/simple-proxy-api.yaml .
rm -rf ase
  • package.json の script の部分を書き換えます。一から作成するのではなく、AWS で作成されている example を基に書き換えます。
    • 基にした例はこちら
    • Windows 以外の環境で開発している場合は、win-config 以下は不要です。
    • config の profile を指定することで、AWS CLI のプロファイルを切り替えられるようにしています。
"config": {
    "s3BucketName": "{S3バケット名}",
    "region": "{リージョン名}",
    "functionName": "{Lambda 関数名}",
    "cloudFormationStackName": "{CloudFormation のスタック名(自分でわかれば何でもOK)}",
    "accountId": "{アカウントID}",
    "profile": "{プロファイル名}"
 },
"scripts": {
    "start": "node app.local.js",
    "config": "node ./scripts/configure.js",
    "deconfig": "node ./scripts/deconfigure.js",
    "local": "node scripts/local",
    "invoke-lambda": "aws lambda invoke --function-name $npm_package_config_functionName --region $npm_package_config_region --payload file://api-gateway-event.json lambda-invoke-response.json && cat lambda-invoke-response.json",
    "create-bucket": "aws s3 mb s3://$npm_package_config_s3BucketName --region $npm_package_config_region",
    "delete-bucket": "aws s3 rb s3://$npm_package_config_s3BucketName --region $npm_package_config_region",
    "package": "aws cloudformation package --template ./cloudformation.yaml --s3-bucket $npm_package_config_s3BucketName --output-template packaged-sam.yaml --region $npm_package_config_region",
    "deploy": "aws cloudformation deploy --template-file packaged-sam.yaml --stack-name $npm_package_config_cloudFormationStackName --capabilities CAPABILITY_IAM --region $npm_package_config_region",
    "package-deploy": "npm run package && npm run deploy",
    "delete-stack": "aws cloudformation delete-stack --stack-name $npm_package_config_cloudFormationStackName --region $npm_package_config_region",
    "setup": "npm install && (aws s3api get-bucket-location --bucket $npm_package_config_s3BucketName --region $npm_package_config_region || npm run create-bucket) && npm run package-deploy",
    "win-config": "npm run config",
    "win-deconfig": "npm run deconfig",
    "win-local": "npm run local",
    "win-invoke-lambda": "aws lambda invoke --function-name %npm_package_config_functionName% --region %npm_package_config_region% --payload file://api-gateway-event.json lambda-invoke-response.json && cat lambda-invoke-response.json",
    "win-create-bucket": "aws s3 mb s3://%npm_package_config_s3BucketName% --region %npm_package_config_region%",
    "win-delete-bucket": "aws s3 rb s3://%npm_package_config_s3BucketName% --region %npm_package_config_region%",
    "win-package": "aws cloudformation package --template ./cloudformation.yaml --s3-bucket %npm_package_config_s3BucketName% --output-template packaged-sam.yaml --region %npm_package_config_region%",
    "win-deploy": "aws cloudformation deploy --template-file packaged-sam.yaml --stack-name %npm_package_config_cloudFormationStackName% --capabilities CAPABILITY_IAM --region %npm_package_config_region%",
    "win-package-deploy": "npm run win-package && npm run win-deploy",
    "win-delete-stack": "aws cloudformation delete-stack --stack-name %npm_package_config_cloudFormationStackName% --region %npm_package_config_region%",
    "win-setup": "npm install && (aws s3api get-bucket-location --bucket %npm_package_config_s3BucketName% --region %npm_package_config_region% || npm run win-create-bucket) && npm run win-package-deploy"
}
  • コピーしてきた yaml ファイルの simple-proxy-api.yaml の内容のうち、info -> title の箇所だけ書き換えておきます。

    • info -> title で設定した名前は Amazon API Gateway 上に登録される API の名前になります。
    • 今後、同じ手順で API を作成した場合に、名前がカブってしまうと上書きされてしまうため、任意の名前に変更しておきます。
  • コピーしてきたスクリプトを用いて、設定ファイルの書き換えを行います。

npm run config -- \
--account-id="XXXXXXXXXXXX" \
--bucket-name="express-serverless-api-sample" \
--region="us-east-2" \
--function-name="ExpressServerlessExample"
  • 各設定項目の意味は以下のとおりです:
    • account-id(必須):AWS のアカウントの ID です。
    • bucket-name(必須):コードなどをアップロードする S3 のバケット名です。事前に作成する必要はありません。
    • region(オプション):リージョンの指定です。
    • function-name(オプション):作成される Lambda の関数名です。
  • このコマンドにより、以下の3つのファイルが書き換わります:

API の内容変更

  • aws-serverless-express が使えるように、API の内容を修正します。

index.js の修正

  • 修正の内容は以下のとおりです:
    • aws-serverless-express の Middleware を有効化する
    • listen メソッドは削除する
      • aws-serverless-express では UNIX ドメインソケットで別の箇所で listen しているため
    • 変数 app を module 化する
      • lambda.js で読み込む必要があるため
  • 修正後の index.js は以下のとおりです:
const express = require('express');
const app = express();
const cors = require('cors');
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware') // 追加した行

app.use(express.json());
app.use(cors());
app.use(awsServerlessExpressMiddleware.eventContext()) // 追加した行

app.get('/api/info', (req, res) => {
  res.send({ application: 'sample-app', version: '1.0' });
});

app.post('/api/v1/getback', (req, res) => {
  res.send({ ...req.body });
});

// app.listen(3000, () => console.log(`Listening on: 3000`));

module.exports = app // 追加した行
  • ローカルでも Lambda 上でも動く API にする場合は、 Lambda 上で動いているか否かを判定する必要があります。
  • Lambda 上で実行される際には、環境変数 AWS_REGION が設定されるため、これを利用して判定する関数を以下のように作っておきます。
const isProd = () => {      
  return !!process.env.AWS_REGION
}
  • この isProd を用いて修正した index.js は以下のとおりです:
const express = require('express');
const app = express();
const cors = require('cors');

const isProd = () => {
  return !!process.env.AWS_REGION
}

app.use(express.json());
app.use(cors());

if (isProd()) {
  console.log('Environment: Production');
  const awsServerlessExpressMiddleware = require("aws-serverless-express/middleware");
  app.use(awsServerlessExpressMiddleware.eventContext());
} 

app.get('/api/info', (req, res) => {
  res.send({ application: 'sample-app', version: '1.0' });
});

app.post('/api/v1/getback', (req, res) => {
  res.send({ ...req.body });
});

if (isProd()) {
  module.exports = app;
} else {
  app.listen(3000, () => console.log(`Listening on: 3000`));
}

lambda.js の修正

  • example からコピーした lambda.js を修正します。上で修正した index.js のパスを指定して import します。
const awsServerlessExpress = require(process.env.NODE_ENV === 'test' ? '../../index' : 'aws-serverless-express')
// const app = require('./app')
const app = require('./index')
(以下略)

API をデプロイする

  • デプロイには、example からコピーしたスクリプトを利用します。
    npm run setup
  • 最終的に、下記のようなメッセージが出れば成功です。
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - aws-express-sample-stack

このデプロイ時にエラーが発生(2020年01月09日)

  • 原因は、コピーしてきた設定では、 Node.js 8.10 のランタイムで関数を作成しようとするため
    • ここ でアナウンスがある通り、2020年1月6日以降、新規で Node.js 8.10 の Lambda 関数は作成不可能になりました。
  • よって、cloudformation.yaml を修正する必要があります。
    • Resources -> ExpressServerlessExample -> Properties -> Runtime の部分を下記のように修正しました。
ExpressServerlessExample:
   Type: AWS::Serverless::Function
   Properties:Runtime: nodejs12.x
  • また、エラーが発生した場合、CloudFormation で ROLLBACK_FAILED などの状態になり、再度デプロイができなくなる状態になります。
    • その場合、AWS マネジメントコンソールから CloudFormation を開き、当該スタックを一度削除することで再デプロイが可能になります。

動かして試してみる

  • AWS マネジメントコンソールから API Gateway を開き、作成した API の「ステージ」でエンドポイントの URL を確認できます。

f:id:linkode-okazaki:20200109121945p:plain

  • index.js で書いてあるルーティングの定義に基づいてエンドポイントがそれぞれ作成されているわけではなく、あくまで index.js の処理の内部で振り分けされているようです。

  • これを基に、先程と同様に動作確認をしてみます。

curl  https://xxxxxx.us-east-2.amazonaws.com/prod/api/info
#=> {"application":"sample-app","version":"1.0"}
curl -X POST -H "Content-Type: application/json" -d '{"Name":"hoge", "Age":"100"}'  https://xxxxxx.us-east-2.amazonaws.com/prod/api/v1/getback
#=> {"Name":"hoge","Age":"100"}⏎  
  • ローカルで確認したときと同様の動作になっていれば OK です。

まとめ

  • aws-serverless-express を用いて、AWS Lambda + API Gateway でサーバーレスな API を実現することができました。
    • Node.js で Web アプリケーションや API を作成する際に広く使われている Express の方法から大幅に変更することなく実現できます。
    • 既存のものを容易に活用することができそうです。

参考資料