express-generatorで生成したテンプレートプロジェクトをTypeScript実装に置換|AWSでサーバレス化


express-generatorで生成したテンプレートプロジェクトをTypeScript実装に置換|リファクタリングの続きです。

AWSド初心者がNode.js,Express,API Gateway,Lambdaを使ったサーバレスに挑む

せっかくNode.jsを触りだしたので、AWSでサーバレスをやってみたい。
とは言え、自力で触った事があるのは、管理コンソールでEC2とS3の操作したぐらいしか。。。

AWS周りの環境設定は何も無い状態からスタートします。IAMユーザもありません。
まずはIAMユーザの作成からスタート。

IAMユーザ追加

1. IAM→ユーザ→ユーザを追加
2. ユーザー名を入力→プログラムによるアクセス:ON→AWS マネジメントコンソールへのアクセス:ON→カスタムパスワード入力→パスワードのリセットが必要:OFF
3. グループの作成→グループ名を入力
4. 権限付与(※この権限で問題無いかは不明。。。)

  • AmazonAPIGatewayAdministrator:Provides full access to create/edit/delete APIs in Amazon API Gateway via the AWS Management Console.
  • AmazonAPIGatewayInvokeFullAccess:Provides full access to invoke APIs in Amazon API Gateway.
  • AmazonAPIGatewayPushToCloudWatchLogs:Allows API Gateway to push logs to user's account.
  • AWSLambdaFullAccess:Provides full access to Lambda, S3, DynamoDB, CloudWatch Metrics and Logs.

5. ユーザの作成

  • 認証情報のcsvをダウンロード

6. ユーザ設定
・IAM→ユーザ→作成したユーザ→認証情報→MFA デバイスの割り当て→Google Authenticatorの2段階認証コードを2連続で設定

AWS CLIをインストール

Python3をインストール

brew install pyenv
pyenv install 3.6.5
pyenv local 3.6.5
python --version

cat << 'EOS' >> ~/.bash_profile
export PYENV_ROOT=$HOME/.pyenv
export PATH=$PYENV_ROOT/bin:$PATH
eval "$(pyenv init -)"
EOS

AWS CLIをインストール

curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
sudo python get-pip.py
sudo pip install -I awscli

AWS CLI の設定

aws configure

AWS Access Key ID : CSVのアクセスキー
AWS Secret Access Key : CSVのシークレットキー
Default region name : ap-northeast-1
Default output format : json

aws-serverless-expressを導入

aws-serverless-expressのインストール

yarn add aws-serverless-express
yarn add @types/aws-serverless-express --dev

aws-serverless-expressのexampleから設定ファイルをコピー

git clone https://github.com/awslabs/aws-serverless-express.git ase
mkdir todo/scripts
cp -r ase/example/scripts todo/scripts
cp ase/example/api-gateway-event.json todo
cp ase/example/cloudformation.yaml todo
cp ase/example/lambda.js todo
cp ase/example/simple-proxy-api.yaml todo
rm -rf ase

awsアカウントとs3バケットの設定

yarn run config --account-id=アカウントID --bucket-name=todo-bucket9 --region=ap-northeast-1

yarn run v1.6.0
$ node ./aws/configure.js --account-id=アカウントID --bucket-name=todo-bucket9 --region=ap-northeast-1
✨  Done in 0.11s.

package.jsonにAWSのタスク追加

  • configはyarn run configで自動追加される
  • scriptsはexampleからコピペで追加
package.json
{
  "name": "todo",
  "version": "0.0.0",
  "private": true,
+  "config": {
+    "s3BucketName": "todo-bucket9",
+    "region": "ap-northeast-1",
+    "cloudFormationStackName": "TodoServerlessExpressStack",
+    "functionName": "todo",
+    "accountId": "[CSVのアクセスキー]"
+  },
  "scripts": {
    "start": "nodemon ./bin/www",
    "debug": "nodemon --inspect ./bin/www",
    "build": "yarn run build-ts && yarn run copy-static-assets",
    "build-ts": "tsc",
    "tslint": "tslint -c tslint.json -p tsconfig.json",
    "copy-static-assets": "ts-node copyStaticAssets.ts",
+    "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": "yarn package && yarn deploy",
+    "delete-stack": "aws cloudformation delete-stack --stack-name $npm_package_config_cloudFormationStackName --region $npm_package_config_region",
+    "setup": "npm i && (aws s3api get-bucket-location --bucket $npm_package_config_s3BucketName --region $npm_package_config_region || yarn create-bucket) && yarn package-deploy"
  },
  "dependencies": {
    "aws-serverless-express": "^3.2.0",
    "cookie-parser": "^1.4.3",
    "debug": "^3.1.0",
    "express": "^4.16.3",
    "http-errors": "^1.6.3",
    "jade": "^1.11.0",
    "morgan": "^1.9.0",
    "path": "^0.12.7"
  },
  "devDependencies": {
    "@types/cookie-parser": "^1.4.1",
    "@types/moment": "^2.13.0",
    "@types/morgan": "^1.7.35",
    "@types/shelljs": "^0.7.8",
    "nodemon": "^1.17.3",
    "shelljs": "^0.8.1",
    "ts-node": "^5.0.1",
    "tslint": "^5.9.1",
    "typescript": "^2.8.3"
  }
}

Node.jsのバージョンをAWS Lambdaで利用可能な最新バージョンに合わせる

  • npm iをyarn installにしたい所だが、yarnだと後々、ClooudFormationデプロイ時にエラーが出るのでnpmでいく
docker-compose.yml
version: '3'

services:
  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "80:80"
    volumes:
      - "./conf.d:/etc/nginx/conf.d"
    links:
      - node_express

  node_express:
-    image: node:9.11.1-alpine
+    image: node:8.11.1-alpine
    container_name: node_express
    hostname: node_express
    volumes:
      - ".:/src"
    working_dir: /src
    command: > 
      sh -c
      "yarn global add typings
      && npm i
      && typings i
      && yarn build
      && yarn start"
    ports:
      - "3000:3000"

ソース修正

  • app.jsをTypeScript化したソースにaws-serverless-express/middlewareの利用設定を追加
src/app.ts
import { Router, NextFunction, Request, Response } from 'express';

import * as createError from 'http-errors';
import * as express from 'express';
import * as path from 'path';
import * as cookieParser from 'cookie-parser';
import * as logger from 'morgan';
+ import * as awsServerlessExpressMiddleware from 'aws-serverless-express/middleware';

import { IndexController } from './controllers/index';
import { UserController } from './controllers/user';

/**
 * Application.
 *
 * @class App
 */
export class App {
  public app: express.Application;

  /**
   * Bootstrap the application.
   *
   * @static
   * @return {ng.auto.IInjectorService} Returns the newly created injector for this app.
   */
  public static bootstrap(): App {
    return new App();
  }

  /**
   * Constructor.
   *
   * @constructor
   */
  constructor() {
    this.app = express();
    this.setConfig();
    this.setRoutes();
    this.setApiRoutes();
    this.setErrorHandler();
  }

  /**
   * Configure application
   *
   */
  private setConfig(): void {
+    this.app.use(awsServerlessExpressMiddleware.eventContext());

    this.app.set('views', path.join(__dirname, 'views'));
    this.app.set('view engine', 'jade');

    this.app.use(logger('dev'));

    this.app.use(express.json());
    this.app.use(express.urlencoded({ extended: false }));
    this.app.use(cookieParser());
    this.app.use(express.static(path.join(__dirname, 'public')));
  }

  /**
   * Create and return Router.
   *
   */
  private setRoutes(): void {
    this.app.use('/', new IndexController().create());
    this.app.use('/users', new UserController().create());
  }

  /**
   * Create REST API routes
   *
   */
  private setApiRoutes(): void {
  }

  /**
   * Create Error handler
   *
   */
  private setErrorHandler(): void {
    // Catch 404 and forward to error handler
    this.app.use((req: Request, res: Response, next: NextFunction) => {
      next(createError(404));
    });

    // Error handler
    this.app.use((err: app.Error, req: Request, res: Response, next: NextFunction) => {
      // Set locals, only providing error in development
      res.locals.message = err.message;
      res.locals.error = req.app.get('env') === 'development' ? err : {};

      // Render the error page
      res.status(err.status || 500);
      res.render('error');
    });
  }
}
  • Lambdaで利用するソースのapp取得方法をbin/wwwと同様に修正
lambda.js
'use strict'
const awsServerlessExpress = require('aws-serverless-express');
- const app = require('./app')
+ const application = require('dist/app');
+ const app = application.App.bootstrap().app;

// NOTE: If you get ERR_CONTENT_DECODING_FAILED in your browser, this is likely
// due to a compressed response (e.g. gzip) which has not been handled correctly
// by aws-serverless-express and/or API Gateway. Add the necessary MIME types to
// binaryMimeTypes below, then redeploy (`npm run package-deploy`)
const binaryMimeTypes = [
  'application/javascript',
  'application/json',
  'application/octet-stream',
  'application/xml',
  'font/eot',
  'font/opentype',
  'font/otf',
  'image/jpeg',
  'image/png',
  'image/svg+xml',
  'text/comma-separated-values',
  'text/css',
  'text/html',
  'text/javascript',
  'text/plain',
  'text/text',
  'text/xml'
]
const server = awsServerlessExpress.createServer(app, null, binaryMimeTypes);

exports.handler = (event, context) => awsServerlessExpress.proxy(server, event, context);

デプロイ

yarn run setup

・・・・

Successfully created/updated stack - todo
✨  Done in 55.88s.

デプロイ結果

  • API Gatewayに登録された

URLにアクセス

  • API Gateway→ステージ→URL の呼び出しのURLにアクセス

感想

AWSド初心者の私には、少々最初のハードルが高くて1日ぐらい掛かっちゃいました。

AWS関連のスクリプトと設定ファイルを/awsディレクトリを作成して、まとめて突っ込んだ結果、
何度やってもInternal Server Errorが表示されてしまった。
パスがおかしいんだろうけど、何度直してもエラーで表示できなかったので、
素直にaws-serverless-express/exampleと同じように、プロジェクト直下に置く構成にしたら動きました。。。

元々作ってたExpressのルーティング処理があれば、簡単にサーバレス化できそうですね。
2回目は大丈夫です。

ソース