Node.js のフレームワークである Express の書き方で、デプロイすると AWS Lambda + API Gateway で動く aws-serverless-express を使ってみました。Express で作った既存の API を大幅に作り変えることなしにサーバーレス化することができます。
前提条件
まずは、普通に 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`));
- 処理の内容は以下のとおりです:
- できたら動作確認をします。
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 をセットアップ
npm install --save aws-serverless-express
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 を基に書き換えます。
"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
の箇所だけ書き換えておきます。コピーしてきたスクリプトを用いて、設定ファイルの書き換えを行います。
npm run config -- \ --account-id="XXXXXXXXXXXX" \ --bucket-name="express-serverless-api-sample" \ --region="us-east-2" \ --function-name="ExpressServerlessExample"
- 各設定項目の意味は以下のとおりです:
- このコマンドにより、以下の3つのファイルが書き換わります:
API の内容変更
index.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 を開き、当該スタックを一度削除することで再デプロイが可能になります。
動かして試してみる
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 の方法から大幅に変更することなく実現できます。
- 既存のものを容易に活用することができそうです。