Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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に登録された

Screen Shot 2018-04-23 at 15.52.21.png

URLにアクセス

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

https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod

Screen Shot 2018-04-23 at 15.54.23.png

https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/users/user1234

Screen Shot 2018-04-23 at 15.55.47.png

感想

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

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

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

ソース

https://github.com/masaaki-uegaki/todo

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした