EY-Office ブログ

AWS Lambda用TypeScript開発ツールを調べた

仕事の関係でAWS Lambdaを使い簡単なAPIサーバーを作る事になりました。Lambdaはいくつかのプログラム処理系(言語)をサポートしていますが、当然Node.jsを使います😆

JavaScriptで開発するのであれば、ブラウザーでAWSマネジメントコンソールを開きLambdaの関数作成・変更のエディター(IDE)を使う事で開発・デバッグできます。

しかし、TypeScriptで開発したと思うとAWSマネジメントコンソールでは無理で、何らかの開発環境が必要になります。

AWS Lambda

開発環境の要求仕様

今回の開発環境に必要とされるものは以下です

  • 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-httpServerless 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
  ]
}

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ