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
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
region: ap-northeast-1
stage: ${opt:stage, 'dev'}
profile: default
functions:
app:
handler: src/handler.handler
timeout: 30
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();
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,
}),
);
});
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
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',
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 でビルド
$ sls webpack --stage prod
$ 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}`),
}),
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 が使えるようにする
[参考]