TL;DR
AWS も Lambda も自分で調べて〜なレベル感なのでなんも分かってません。
とりあえずガチャガチャやってみて、動いてるんじゃね?となったので、そこまでもプロセスをまとめただけのエントリーになります。なので、たぶん超長い記事になってると思います。 :pray:
Serverless 化する TypeScript な Express の構成
👇 前回までのあらすじ
構成はこんな感じ
/api |- /env | |- .env.dev | |- .env.prod |- /src | |- index.ts // Express server root |- /webpack // このディレクトリ下に webpack の設定ファイル | |- base.config.js | |- // … 略 |- package.json |- tsconfig.json
AWS Cli でユーザーを設定する
AWS cli をインストール
homebrew でインストールできるっぽい
$ brew install awscli
Docker 上で動かすこともできるみたいだけど、ちょっと面倒そうなので
ユーザーの設定
対話式で default ユーザーの設定ができる
$ aws configure AWS Access Key ID: AWS Secret Access Key: Default region name: Default output format:
AWS 上に作成した IAM ユーザーの AWS Access Key ID, AWS Secret Access Key を指定。
Default region は ap-northeast-1
, Default output format は json
を指定しました。
~/.aws/credentials
に Access Key ID, Secret Access Key の設定が、~/.aws/config
に region と output format の設定のファイルが作成される
~/.aws/credentials
[default] aws_access_key_id = DEFAULT_USER_ACCESS_KEY_ID aws_secret_access_key = DEFAULT_USER_SECRET_ACCESS_KEY
~/.aws/config
[default] region = ap-northeast-1 output = json
このファイルにある [default]
が以降で使われる serverless で使われるユーザーの profile 名になる
Serverless パッケージのインストール
$ npm install -g serverless $ sls -v
ドキュメントにあるように global に serverless コマンド (sls
) が使えるようにインストール。
あまり global に色々入れたくないので、local にインストールしても大丈夫なのかな?
aws-serverless-express で Serverless 化する
1つのエンドポイントで全てのパスを受け止めて Lambda 関数にパスして内部で動作している Express でそれぞれのURLに合う処理を行うようにするっぽい cf. AWS LambdaとNuxt.jsでServer Side Renderingする(2020年版) - Sweet Escape
パッケージのインストール
$ npm install --save aws-lambda aws-serverless-express $ npm install --save-dev @types/aws-lambda @types/aws-serverless-express
serverless.yml
severless コマンドやデプロイする際に使われる設定っぽい。(正確に理解できているわけではない)
service: my-serverless-app provider: name: aws runtime: nodejs12.x # デフォルトが us-east-1 なのでTOKYOリージョンを指定 region: ap-northeast-1 # --stage オプションの値をセット、オプションがない場合は dev を使用 stage: ${opt:stage, 'dev'} # .aws/credentials の [default] ユーザーを使用 profile: default # Lambda の設定 functions: app: # `./src/handler.ts` の handler 関数をエンドポイントにする handler: src/handler.handler timeout: 30 # 全ての URL を受け止めて Lambda に渡す events: - http: ANY / - http: 'ANY {proxy+}' # デプロイパッケージの設定 package: exclude: # デプロイパッケージから除外 - '**' include: # デプロイパッケージに含める - dist/**
TypeScript な Express を webpack でビルドしていたので、package 設定で、serverless.yml
のあるディレクトリを全て除外した後に webpack でビルドされる dist
ディレクトリ配下のみが対象となる設定にしました。
Lambda 用のエントリーポイントを作成する
API Gateway で受け止めた URL を Lmabda が ./src/hander.ts
の handler
関数に渡し、この関数を通じて Express アプリにアクセスします。
Express のファイルを分割する
local で作成していた際は app.listen(PORT)
で localhost を立ち上げてアプリを作成していましたが、Serverless にした際にこの app.listen(PORT)
で localhost が動作する処理が残っているとエラーが発生して上手く動作しなかったので、local での開発時だけ app.listen(PORT)
できるように ./src/index.ts
を分割します。
./src/app.ts
… local, Serverless 共通で使用する Express を export する
import express from 'express'; const app = express(); const router = express.Router(); // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.get('/', (req: express.Request, res: express.Response) => { const { method, path, query } = req; return res.send( JSON.stringify({ message: 'Hello World Serverless!', method, path, query, }), ); }); // Routing app.use('/', router); export default app;
./src/index.ts
… local 開発時のエントリーポイント
import app from './app'; const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`listening on port http://localhost:${PORT}`); });
app.ts
の Express を app.listen()
で動かすだけ
Lambda から呼び出される hander 関数を作成
./src/handler.ts
… API Gateway から渡されるイベント URL を hander 関数で受け止めて app.ts
の Express に渡す
import { APIGatewayEvent, Context, Handler } from 'aws-lambda'; import * as awsServerlessExpress from 'aws-serverless-express'; import app from './app'; const server = awsServerlessExpress.createServer(app); export const handler: Handler = (event: APIGatewayEvent, context: Context) => { awsServerlessExpress.proxy(server, event, context); };
serverless-offline で Local でも Serverless で実行できるようにする
パッケージのインストール
$ npm install --save-dev serverless-offline
serverless.yml
に下記を追加
plugins: - serverless-offline
offline での実行
$ sls offline … offline: [HTTP] server ready: http://localhost:3000 🚀
デフォルトの --stage dev
の場合は http://localhost:3000/dev
が Serverless のエンドポイントになっっている
Lambda で関数が呼べるように webpack する
TypeScript で書いていたので Serverless としてデプロイするにもビルドする必要があるだろうと思い Lambda 用に作成した handler.ts
をエントリーポイントにした webpack.config.js を作成してビルドしてみたのですが、通常の webpack でビルドすると出力されるプログラムは !function(){}
な無記名関数の中にカプセル化されてしまうようで、serverless-offline で試してみようとしたら Serverless の handler になる関数にアクセスできず 404 が返ってくるだけになってしまいました…
serverless-webpack を使う
Serverless で使えるようにいい感じに webpack してくれるっぽい
パッケージのインストール
$ npm install --save-dev serverless-webpack
serverless.yml
を編集
service: my-serverless-app plugins: - serverless-offline + - serverless-webpack + custom: + webpack: + webpackConfig: 'webpack.serverless.config.js' + includeModules: true provider: name: aws runtime: nodejs12.x region: ap-northeast-1 stage: ${opt:stage, 'dev'} profile: default # Lambda の設定 functions: app: # `./src/handler.ts` の handler 関数をエンドポイントにする handler: src/handler.handler timeout: 30 # 全ての URL を受け止めて Lambda に渡す events: - http: ANY / - http: 'ANY {proxy+}' - # デプロイパッケージの設定 - package: - exclude: # デプロイパッケージから除外 - - '**' - include: # デプロイパッケージに含める - - dist/**
custom.webpack.webpackConfig
… serverless-webpack で使用する webpack の設定ファイル。指定がない場合はwebpack.config.js
が使用されるcustom.webpack.includeModules
… webpack 側でwebpackl-node-externals
を使用してnode_modules
を除外する場合は、includeModules
をtrue
にしておく必要があるみたいです。cf. Serverless Webpack の使い方まとめ - Qiita
serverless-webpack でビルドされたものが sls offline
, sls deploy
の対象になるようだったので、package
の設定は不要なので (たぶん) 削除しました。
severless 用の webpack config ファイルの作成
custom.webpack.webpackConfig
で指定した設定ファイル webpack.serverless.config.js
を作成します
webpack.serverless.config.js
const webpack = require('webpack'); const slsw = require('serverless-webpack'); const path = require('path'); const nodeExternals = require('webpack-node-externals'); const Dotenv = require('dotenv-webpack'); const enviroment = process.env.NODE_ENV || 'dev'; module.exports = { mode: 'production', // serverless.yml で handler に指定したファイルが自動的にエントリーポイントに出来るっぽい entry: slsw.lib.entries, target: 'node', externals: [nodeExternals()], plugins: [ new Dotenv({ path: path.resolve(__dirname, `../env/.env.${enviroment}`), }), ], module: { rules: [ { test: /\.ts$/, exclude: /node_modules/, loader: 'ts-loader', options: { configFile: 'tsconfig.json', }, }, ], }, resolve: { extensions: ['.ts', '.js', '.json'], alias: { '@': path.join(__dirname, '/src/'), }, }, };
serverless-webpack でビルド
# stage prod でビルド $ sls webpack --stage prod # stage dev でビルド $ sls webpack --stage dev
.webpack
ディレクトリにビルドされたファイルが、.serverless
ディレクトリに zip 化されたファイル (serverless にデプロイされるファイル) が生成されます。
serverless-webpack でビルドする際にも process.env に値を渡したい
$ NODE_ENV=dev sls webpack --stage dev
上記のように NODE_ENV=
を先頭につければ sls webpack
でも process.env に値を渡すことができましたが、折角 stage オプションがあり二重っぽいので、serverless-webpack でビルドする際は stage オプションを NODE_ENV
としてプログラムに渡せるようにしたいと思います。
serverless.yml の値を webpack.config で取得してプログラムに渡す
slsw.lib.serverless.service
で webpack の設定で serverless.yml のプロパティにアクセスできるので、serverless.yml に NODE_ENV
プロパティを作成して、webpack で process.env にしてプログラムに渡せる方針にします。
serverless.yml
stage の値を NODE_ENV
プロパティに設定する
provider: name: aws runtime: nodejs12.x region: ap-northeast-1 stage: ${opt:stage, 'dev'} profile: default + environment: + NODE_ENV: ${self:provider.stage}
--stage
オプションで渡された値が provider.environment.NODE_ENV
にセットされる
webpack.EnvironmentPlugin を使って provider.environment
の値を process.env
にマージしてプログラムに渡す
webpack.serverless.config.js
const webpack = require('webpack'); const slsw = require('serverless-webpack'); // … const enviroment = slsw.lib.serverless.service.provider.environment.NODE_ENV || 'dev'; module.exports = { // … plugins: [ new Dotenv({ path: path.resolve(__dirname, `../env/.env.${enviroment}`), }), // serverless.yml の provider.environment の値をオブジェクトとして process.env にマージしてプログラムに渡す new webpack.EnvironmentPlugin( slsw.lib.serverless.service.provider.environment, ), ], //... };
これで severless でビルドする際も /src
内のプログラムで local で開発しているときと同様に process.env.NODE_ENV
が扱えるようになりました。
Deploy
$ sls deploy --stage prod -v
sls deploy
コマンドを実行すると sls webpack
が実行された後にビルドされた zip が AWS にデプロイされる。
-v
オプションを付けると途中経過がターミナル上で確認できる。
サービスの削除
デプロイしたサービスを削除する場合は sls remove
コマンドを実行すれば OK
$ sls remove -v
コマンドでは削除しきれないものもあるらしいので、最終的には AWS のコンソールで確認するのが良さそうです。(理解が浅いがゆえに謎課金が発生するの精神的気に結構辛い〜)
所感
検索 + タブ開きまくりでなんとか TypeScript な Express を Serverless 化して AWS にデプロイ・ローカルでも serverless-offline での実行が出来る状態にすることができました。 AWS なんもわからん + Serverless 初挑戦で理解が足りないことが二重・三重になってしまっていたので、この開発とデプロイができる環境を作ること自体にすごく時間がかかってしまって、肝心のサービスの中身はまだ何も手がつけられていないという状態… 取り敢えずサービスをリリースしたいという目的が優先なら知見のある技術選定でまずはサクッと作ってしまうのが大事だと感じました。
Next Step
- ビルドされた zip が node_modules を含んでいて巨大なので
serverless-layers
を使えば node_modules を s3 に分離してアップロードできそうなので試してみる - local / デプロイした状態で DynamoDB が使えるようにする
[参考]
- The Serverless Application Framework | Serverless.com
- Serverless Frameworkでローカル環境のセットアップからAWSへDeployまでを試してみる - au Commerce&Life Tech Blog
- serverless.yml の設定関連
- 設定ファイルと認証情報ファイルの設定 - AWS Command Line Interface
- GitHub - awslabs/aws-serverless-express: Run serverless applications and REST APIs using your existing Node.js application framework, on top of AWS Lambda and Amazon API Gateway
- GitHub - dherault/serverless-offline: Emulate AWS λ and API Gateway locally when developing your Serverless project
- GitHub - serverless-heaven/serverless-webpack: Serverless plugin to bundle your lambdas with Webpack
- AWS Lambdaの裏側をなるだけ詳しく解説してみる - Sweet Escape

AWS Lambda実践ガイド (impress top gear)
- 作者:大澤文孝
- 発売日: 2017/10/16
- メディア: 単行本(ソフトカバー)

サーバーレスシングルページアプリケーション ―S3、AWS Lambda、API Gateway、DynamoDB、Cognitoで構築するスケーラブルなWebサービス
- 作者:Ben Rady
- 発売日: 2017/06/23
- メディア: 単行本(ソフトカバー)