仕事の関係でAWS Lambdaを使い簡単なAPIサーバーを作る事になりました。Lambdaはいくつかのプログラム処理系(言語)をサポートしていますが、当然Node.jsを使います😆
JavaScriptで開発するのであれば、ブラウザーでAWSマネジメントコンソールを開きLambdaの関数作成・変更のエディター(IDE)を使う事で開発・デバッグできます。
しかし、TypeScriptで開発したと思うとAWSマネジメントコンソールでは無理で、何らかの開発環境が必要になります。
開発環境の要求仕様
今回の開発環境に必要とされるものは以下です
- TypeScript
- Express.js(なぜ必要なのかは後で説明します)
- ローカル(Mac)上で開発・デバッグできる
- AWS Lambdaへデプロイできる
基本フレームワーク・ツール
ネットを調べるとnode.jsを使ったLambda用の開発フレームワーク・ツールとしては、
- まずserverlessの情報が大量に出てきます。serverlessはServerless, Incがオープンソースとして開発・サポートしているフレームワークで、AWS LambdaだけではなくGoogle Cloud FunctionsやAzure Functionsをサポートしています。
- そしてAmazon純正のAWS SAM(Serverless Application Model)の情報も見つかりました。
そこで両方を使い開発環境のプロトタイプを作ってみました。その感想としては
serverless
- 大量な情報があり、あまりつまずかずにTypeScript+Expressでプロトタイプが作れました
- ただし、serverlessインストール時に大量にdeprecatedが表示されるのが気になりました(バージョンアップで解決されると思いますが・・・)
- 開発コミュニティも充実しているようです
AWS SAM
- 有用な情報が少なく、AWS固有の謎設定ファイルに何度かハマりました
- TypeScriptサポートはなく、webpack + ts-loaderを使って乗り切りました
- ローカル開発環境はDockerなのでDocker環境が必要ですが、今時は普通かもしれませんね
sam init
が作るnode.js開発環境は2重のプロジェクトになっていて気に入らない😅- しかし、Amazon(AWS)純正の謎安心感
serverlessのホームページにSAMとの比較記事がありました。
なぜExpress.jsが必要なのか
Lambdaを使いAPIサーバーを作るのに、Node.js用Web/APIサーバーの定番Express.jsは必須ではありません。
しかし、Lambdaだけで作ると1つのAPIエンドポイントに対し、1つのAWS Lambda関数を作り、1つのAmazon API Gatewayを定義する必要があります。今回作るAPIさーばーではList, Create, Update, Deleteの4個必要になります。
それに対しLambdaでExpressを動かすと1つのAWS Lambda関数に対しワイルドカードのAmazon API Gatewayを定義する事で、複数のAPIサポートはExpressのコード内で行えます。またExpress用に作られたプラグイン等も使え、なれたExpressでLambdaが開発できます。
Expressは本来スタンドアロンのサーバーですが serverless-http や Serverless Express by Vendia を組み合わせるとLamndaで動かす事ができます。
結論
今回はAmazon(AWS)純正という誘惑からAWS SAMを使う事にしました。
サンプルコード
プロジェクトはsam init
で作り、以下のようなファイルを作成しました。
- src/app.ts
import express, {Request, Response} from "express"
import serverlessExpress from "@vendia/serverless-express"
import cors from "cors"
const app = express()
app.use(cors())
app.get('/hello', (req: Request, res: Response) => {
res.json({message : 'Hello World !!'});
});
app.post('/users', (req: Request, res: Response) => {
res.json([{name: 'Taro'}, {name: 'Hanako'}]);
})
export const lambdaHandler = serverlessExpress({app});
- package.json
{
"name": "sam-ts-express",
"version": "1.0.0",
"description": "Sample code for SAM + TypeScript + Express",
"main": "app.js",
"author": "SAM CLI",
"license": "MIT",
"scripts": {
"dev-server": "sam local start-api &",
"dev": "NODE_ENV=development webpack --watch --mode=development",
"deploy": "NODE_ENV=production webpack --mode=production && sam deploy"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.72",
"@types/cors": "^2.8.10",
"@types/express": "^4.17.11",
"aws-sam-webpack-plugin": "^0.9.0",
"ts-loader": "^8.0.17",
"typescript": "^4.2.3",
"webpack": "^5.24.4",
"webpack-cli": "^4.5.0"
},
"dependencies": {
"@vendia/serverless-express": "^4.3.4",
"cors": "^2.8.5",
"express": "^4.17.1"
}
}
- template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
sam-ts-express
Sample code for SAM + TypeScript + Express
Globals:
Function:
Timeout: 3
Resources:
HelloFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: app.lambdaHandler
Runtime: nodejs14.x
Events:
Api:
Type: Api
Properties:
Path: /{proxy+}
Method: ANY
Outputs:
HelloApi:
Description: "API Gateway endpoint URL for Prod stage for API function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
HelloFunction:
Description: "API Lambda Function ARN"
Value: !GetAtt HelloFunction.Arn
HelloFunctionIamRole:
Description: "Implicit IAM Role created for API function"
Value: !GetAtt HelloFunctionRole.Arn
- samconfig.toml
version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "sam-ts-express"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-xxxxxx"
s3_prefix = "sam-ts-express"
region = "ap-northeast-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
- tsconfig.json
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
"allowJs": true,
"checkJs": true,
"sourceMap": true,
"strict": true,
"noImplicitAny": false,
"esModuleInterop": true
},
"include": ["src/**/*.ts"]
}
- webpack.config.js
const AwsSamPlugin = require("aws-sam-webpack-plugin");
const awsSamPlugin = new AwsSamPlugin();
module.exports = {
entry: awsSamPlugin.entry(),
output: {
filename: "[name]/app.js",
libraryTarget: "commonjs2",
path: __dirname + "/.aws-sam/build/"
},
devtool: "source-map",
resolve: {
extensions: [".ts", ".js"]
},
target: "node",
externals: process.env.NODE_ENV === "development" ? [] : [],
mode: process.env.NODE_ENV || "production",
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader"
}
]
},
plugins: [
awsSamPlugin
]
}