技術メモ

プログラミングとか電子工作とか

AWS LambdaをTypeScriptで開発する

f:id:ysmn_deus:20190318182949p:plain

JavaScriptはオワコン!とまでは言いたくないんですが、やはり普段からTypeScriptを利用していると極力JavaScriptは避けたいところ。
ということで基本JavaScriptで開発されているであろうAWS LambdaのNode環境ですが、TypeScriptで開発を始めるまでをまとめておきます。
Serverless Frameworkを使えば比較的容易にTypeScript環境を構築できるようですが、またそれは別途試してみます。
今回はSAM CLIで対応します。
(デプロイはJavaScriptと変わらないので割愛します)

SAM開発環境を整える

SAM CLIのインストール

まずはSAM CLIをインストールする必要があります。
Dockerのインストールも必要になってきますが、基本的には公式ドキュメント(Installing the AWS SAM CLI)に一通り書いてあるのでそちらを参照して下さい。

最終的に

sam --version

がきちんと動作すれば問題ないと思います。

(補足)IAMユーザー情報の登録

AWS CLIを普段利用していない方などはIAMユーザーの認証情報をPCにセットアップする必要があります。
AWSを利用していてLambdaだけしか使わない人も珍しいと思いますので、詳細は割愛しますが公式ドキュメント(Setting Up AWS Credentials)AWS CLIを利用しない認証情報のセットアップも掲載されておりますのでご参照ください。
個人的にはAWS CLIをセットアップすることをオススメしますが。

SAM CLIでプロジェクトを作成する

SAMでプロジェクトを作成します。
とりあえずはHello World的なものを生成する所まではJavaScriptで対応します。

sam init --runtime nodejs10.x --name hello-sam

上記コマンドのオプションをとりあえず説明しておきますと、
--runtime:Lambdaの実行ランタイムを選択します。(python3.7とか)ホントはv12を使っていきたいんですが、一応安牌をとってv10で進めて行きます。
--name:プロジェクトのディレクトリ名です。Lambdaの関数名などは別途修正する必要があります。

上記コマンドを実行すると対話形式でウィザードが進みます。
とりあえず今回は1, 1をチョイスしました。

Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Cloning app templates from https://github.com/awslabs/aws-sam-cli-app-templates.git

AWS quick start application templates:
        1 - Hello World Example
        2 - Quick Start: From Scratch
        3 - Quick Start: Scheduled Events
        4 - Quick Start: S3
        5 - Quick Start: SNS
        6 - Quick Start: SQS
        7 - Quick Start: Web Backend
Template selection: 1

-----------------------
Generating application:
-----------------------
Name: hello-sam
Runtime: nodejs10.x
Dependency Manager: npm
Application Template: hello-world
Output Directory: .

Next steps can be found in the README file at ./hello-sam/README.md

S3などの処理を書きたいときはその辺を選んだりするのが良いんでしょう。
とりあえず今回は無難なチョイスを選択しましたが、ウィザードが完了すると--nameで指定したディレクトリにファイルが出力されていると思います。

テストを実行する

SAM CLIでセットアップすると、mocha+chaiがセットアップできるようになっています。とりあえず走らせる為にpackage.jsonの情報でセットアップします。

cd .\hello-sam\
cd .\hello-world\
yarn

ドキュメントではnpmコマンドを利用している例が多いですが普段はyarnを利用しているのでそのまま使います。
基本問題無いのでそのままテストを走らせます。

yarn test

yarn run v1.22.0
$ mocha tests/unit/


  Tests index
    √ verifies successful response


  1 passing (7ms)

Done in 0.34s.

問題無さそうです。

ローカルで関数を実行する

SAMを利用すればローカルで関数を実行するのは非常に簡単です。
一応sam local invokeで関数を実行する方法と、sam local start-apiで実行する2種類の方法がありますが、大体後者かと思いますので後者で動かしてみます。
(Dockerが起動してないと怒られますので起動しておきましょう)

cd ..
sam local start-api

問題が無ければブラウザでhttp://127.0.0.1:3000/helloを入力するか、curlコマンドでGETリクエストを投げると{"message":"hello world"}が返ってきます。
基本的にJavaScriptのファイル群はここで不要となりますので、hello-worldディレクトリはこの段階で削除してしまって構いません。

ちなみに、Dockerをセットアップしただけの状態だとC:\Users以下以外のディレクトリは権限がなくてマウントされないので、プロジェクトのディレクトリがC:\Users以下でない場合はsam local Error: Cannot find module 'app' docker toolboxのようなエラーが発生します。
対処法としましてはDokcer Toolboxでは下記のサイトを参考にさせていただきました。

coffee-nominagara.com

ToolboxでないDokcerでは警告が出てきちんと設定していた気がします。

TypeScript

tsconfig.jsonの作成

まずはプロジェクトのルートディレクトリにtsconfig.jsonを作っておきます。
特にこだわりのない人は

tsc --init

tsconfig.jsonを作成するのが良いと思います。
一応今回用意しているtsconfig.jsonは下記の通りにしました。

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "outDir": "./built",
    "allowJs": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "esModuleInterop": true
  },
  "include": [
    "./src/**/*"
  ],
    "exclude": ["node_modules"]
}

入力対象を./src/以下、出力先を./builtにしています。

パッケージを追加

TypeScriptを使っているので型が必要です。開発用の型をまずは追加していきます。

yarn add -D @types/aws-lambda

最小限のパッケージはこれだけでOKです。

index.tsを編集

index.tsを編集します。基本的にはデフォルトのapp.jsを踏襲して作成してみます。

import {APIGatewayEvent, Context} from "aws-lambda"

const lambdaHandler = async (event: APIGatewayEvent, context: Context) => {
    let response: any
    try {
        response = {
            'statusCode': 200,
            'body': JSON.stringify({
                message: 'hello ts world',
            })
        }
    } catch (err) {
        console.log(err)
        return err
    }

    return response
}

export {lambdaHandler}

とりあえずという意味ではeventcontextanyで受けてしまえばいいんですが、まぁそんぐらいは・・・
これでコンパイルしてみます。tsconfig.jsonを設定してるのでルートディレクトリでtscコマンドを実行します。

tsc

これでbuiltディレクトリが作成され、index.jsが生成されたかと思います。

template.yamlの修正

JavaScriptの時との差異はコンパイル後のファイル名ファイルの場所です。
これをtemplate.yamlに修正していきます。

~
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: built/
      Handler: index.lambdaHandler
~

CodeUriHandlerを修正しました。

ローカルで関数を実行する

これでローカル関数を実行してみます。

sam local start-api

JavaScriptの時と同様にブラウザかcurlコマンドで確認してみて下さい。
返ってくるのが{"message":"hello world"}のままの人はもしかするとsam buildして.aws-samディレクトリに履歴が残ってる可能性がありますので消しちゃって下さい。
無事{"message":"hello ts world"}が返ってきたら最低限度の環境構築はできたのではないでしょうか。

その他

ESLint + Prettier

基本的にはReactの設定と同じなのですが、ReactのJSXなどは不要です。
一応eslintrcなどを掲載しておきます。

パッケージの追加

yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
yarn add -D prettier @types/prettier @types/eslint-plugin-prettier eslint-config-prettier eslint-plugin-prettier

eslintrc.js

module.exports = {
  env: {
    browser: true,
    node: true,
    es6: true
  },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: "module"
  },
  plugins: [
    'prettier',
    '@typescript-eslint'
  ],
  extends: [
    "eslint:recommended",
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
    'prettier/@typescript-eslint'
  ],
  rules: {
    'prettier/prettier': [
      'error',
      {
        'semi': false,
        'trailingComma': "all"
      }
    ]
  },
};

.eslintignore

node_modules/
*.config.js
built/

webpack

今回はwebpack+ts-loaderで構成します。

パッケージの追加

yarn add -D ts-loader webpack webpack-cli dotenv

今回は.env環境変数を管理する想定でdotenvを利用しています。
直で書いたりする場合はこの辺は必要無いと思います。
とりあえず.envを作っておきます。

NODE_ENV=development

webpack.config.js

const path = require("path")
const dotenv = require("dotenv")
dotenv.config()

const BUILT_PATH = path.resolve(__dirname, "./built")
const BUILD_VARIANT = process.env.NODE_ENV

module.exports = {
  target: "node",
  mode: BUILD_VARIANT === "production" ? "production" : "development",
  resolve: {
    extensions: [".ts", ".js"],
  },
  entry: "./src/index.ts",
  output: {
    filename: `${BUILD_VARIANT}.js`,
    path: BUILT_PATH,
    library: "[name]",
    libraryTarget: "commonjs2",
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: "ts-loader",
      },
    ],
  },
}

これで、ファイルを分割して開発することができます。