Syntax Error.

[Sy] Express で作った API をまるっとサーバレスにする手順(aws-serverless-express使用)

2019/05/15 (更新: 2019/05/17)

これまで Express を使ってサーバサイド(API)を開発した場合、そのまま EC2 にデプロイしてたんですが、今年に入ってサーバレスに取り組んでます。今回は aws-serverless-express という AWS公式のモジュールを使って Express をまるっとサーバレス化する手順をまとめました。

2019/05/17 追記:この記事で作った API を git で管理する手順はこちらにまとめてます。

⇒ [Sy] aws-serverless-express を使ってサーバレス化した API を git で管理する

1. expressを使って通常どおりローカルでAPIとして動く状態まで作る

まずはローカルで普通に Express を使って、以下のように API を作っていきます。

1-1. Express と必要なパッケージをインストール

Express をインストール。

$ yarn add express

CORS対応したいので、その辺を簡単にできる cors をインストール。

$ yarn add cors

ローカル開発時の環境変数設定のために dotenv をインストール。

$ yarn add dotenv

1-2. .env を作成

今回はポート番号のみになりますが、 .env を作成して以下のようにしておきます。

(.env)

PORT=3333

1-3. app.js を作成

app.js を作って処理を書いていきます。

(app.js)

const express = require("express");
const cors = require('cors');
const dotenv = require('dotenv');

const app = express();

dotenv.config();

app.use(cors());

app.get("/", (req, res) => {
  console.log("-- / --");
  res.send({ status: true });
});

app.listen(process.env.PORT, () =>
  console.log("Listening on port " + process.env.PORT + " ...")
);

1-4. 起動して確認

app.js を実行します。

$ node app.js
Listening on port 3333 ...

3333ポートで動き続けるので、もう一つターミナルを起動して、APIをコールします。(ブラウザやhttpクライアントからURLを打ち込んでもOKです)

$ curl http://localhost:3333
{"status":true}%

できました。これをサーバレスにしていきます。

起動していた app.js は一旦 ctrl + C で終了しておきましょう。

$ node app.js
Listening on port 3333 ...
^C ←(ctrl + C で終了)

ここまでのディレクトリ構成はこうなってます。(yarn.lockpackage-lock.json は省いてます)

/
├ .env
├ app.js
├ node_modules
└ package.json

ひとまず、Express を使って API を作るところまで完了です。

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

ここからが本題です。 aws-serverless-express をインストールして、必要な設定ファイルを作ったりしていきます。

今回使った aws-serverlss-express のバージョンは 3.3.6 です。

こちらを参考にさせていただきました。ありがとうございます。

2-1. aws-serverless-express をインストール

まずは aws-serverless-express をインストールします。

$ yarn add aws-serverless-express

続けて、以下のように必要なyamlファイル等を awslabs のリポジトリからいただいて配置していきます。

$ 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

2-2. package.jsonに追記

configscripts を以下のように package.json に追記します。(参考記事で紹介されている設定に加えて、 profile によって AWS CLI の profile の設定を切り替えられるようにしています)

(package.json)

{
  "config": {
    "s3BucketName": "YOUR_UNIQUE_BUCKET_NAME",
    "region": "YOUR_AWS_REGION",
    "cloudFormationStackName": "YOUR_STACK_NAME",
    "functionName": "YOUR_SERVERLESS_EXPRESS_LAMBDA_FUNCTION_NAME",
    "accountId": "YOUR_ACCOUNT_ID",
    "profile": "YOUR_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 --profile $npm_package_config_profile && cat lambda-invoke-response.json",
    "create-bucket": "aws s3 mb s3://$npm_package_config_s3BucketName --region $npm_package_config_region --profile $npm_package_config_profile",
    "delete-bucket": "aws s3 rb s3://$npm_package_config_s3BucketName --region $npm_package_config_region --profile $npm_package_config_profile",
    "package": "aws cloudformation package --template ./cloudformation.yaml --s3-bucket $npm_package_config_s3BucketName --output-template packaged-sam.yaml --region $npm_package_config_region --profile $npm_package_config_profile",
    "deploy": "aws cloudformation deploy --template-file packaged-sam.yaml --stack-name $npm_package_config_cloudFormationStackName --capabilities CAPABILITY_IAM --region $npm_package_config_region --profile $npm_package_config_profile",
    "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 --profile $npm_package_config_profile",
    "setup": "npm install && (aws s3api get-bucket-location --bucket $npm_package_config_s3BucketName --region $npm_package_config_region --profile $npm_package_config_profile || npm run create-bucket) && npm run package-deploy"
  },
・
・

ここで、 cloudFormationStackNameprofile は、直接書き直しておきます。

cloudFormationSatckName は、 CloudFormation に作成されるスタックの名前の指定になります。自分がわかる名前であればなんでも良いかと。

profile は、 AWS CLI の config で設定している profile のうち、どれを使うかの指定になります。

例えばこんな感じになります。

(package.json)

{
  "config": {
    ・
    ・
    "cloudFormationStackName": "ExpressServerlessTestApiStack",
    ・
    ・
    "profile": "utano320"
  },
・
・

2-3. simple-proxy-api.yaml変更

先程 awslabs からコピーしてきた yamlファイルのうち、 simple-proxy-api.yaml は一箇所手を入れておく必要があるので、変更しておきます。

変更が必要な箇所は、 info -> title です。ここで設定した名前は Amazon API Gateway 上に登録される API の名前になります。

コピーしてきた段階だと、ここが AwsServerlessExpressApi となってます。このままでも動くんですが、今後同じ手順で複数の API を作っていく時に名前がかぶってしまうと上書きされてしまうので、名前を変えておく方が良いです。

例えば、こんな感じですね。(cloudFormationStackNameと合わせて自分なりにルールを決めておくとわかりやすいと思います)

---
swagger: 2.0
info:
  title: ExpressServerlessTestApi
・
・

これで aws-serverless-experss まわりでやることは終わりです。

3. ローカルでも Lambda 上でも動くように app.js を変更

いよいよ AWS 上(API Gateway + Lambda)で動くようにしていきます。

今後の開発はローカルで普通どおりやりたいので、 ローカルでもサーバレスでも同じコードで動く 状態に app.js を変更します。

以下の「追記」「変更」の箇所を修正してください。

const app = express();

// ↓追記
const isProd = () => {
  return !!process.env.AWS_REGION
}
// ↑追記


// ↓変更
// dotenv.config();

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

・
・

// ↓変更
// app.listen(process.env.PORT, () =>
//   console.log("Listening on port " + process.env.PORT + " ...")
// );

if (isProd()) {
  module.exports = app;
} else {
  app.listen(process.env.PORT, () =>
    console.log("Listening on port " + process.env.PORT + " ...")
  );
}
// ↑変更

一つずつ説明を。

isProd() は、Lambda上で実行される際に環境変数 AWS_REGION が設定されてたので、それを利用して設定されていればProduction(Lambda上で実行)と判定する関数です。

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

次に、以下の部分では、ローカルの場合 dotenv を使ってポートの設定など .env に書かれた設定を有効化し、Lambdaで実行された場合( isProd()true の場合)は aws-serverless-express/middleware を読み込んでミドルウェアをセットしてます。

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

最後にこの部分は、ローカルの場合はWebサーバとして起動、Lambdaで実行された場合は app をモジュール化してます。

if (isProd()) {
  module.exports = app;
} else {
  app.listen(process.env.PORT, () =>
    console.log("Listening on port " + process.env.PORT + " ...")
  );
}

4. 設定の書き換え用スクリプト実行

awslabs から落としてきたスクリプトを以下のコマンドで実行し、 yamlファイルと package.json を書き換えていきます。

コマンドに渡すパラメータは、それぞれ自分の環境に合わせて変えてください。

account-id

AWSのアカウントIDです。12桁くらいの数字になります。AWSコンソールにログインして「マイアカウント」で確認できます)

bucket-name

S3上に作るバケット名です。全ユーザでユニークである必要があります。事前に作っておく必要はありません。このバケットにコードなどがアップロードされます。

region

リージョンの指定です。今回は例として東京リージョン(ap-northeast-1)としてます。(わからない場合は、AWSコンソールにログインして、リージョンを切り替えた際にURLにある region=xx-xxxxxxxx-xx の部分を参考にしてください)

function-name

作成される Lambda の関数名になります。

上記の値がそろったら、以下のコマンドを実行します。

$ npm run config -- \
--account-id="XXXXXXXXXXXX" \
--bucket-name="utano320-express-serverless-test-api" \
--region="ap-northeast-1" \
--function-name="ExpressServerlessTest"

5. サーバレス化

すべての準備が整ったので、以下のコマンドを実行してAWS上にAPIを作ります。

$ npm run setup

・
・
・

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - ExpressServerlessTestApiStack

これで Express を使った API をサーバレス化することができました。

エンドポイントは、以下の手順で AWSコンソールの Amazon API Gateway から取得できます。

Amazon API Gateway作成したAPIステージprodURLの呼び出し