読者です 読者をやめる 読者になる 読者になる

AWS LambdaをTypeScriptで開発する

プログラミング

今のプロジェクトでは、AWS LambdaをメインにすえたServerless Architectureを採用しています。
AWS LambdaはWebのコンソールからソースコードを編集したり、イベントソースを定義したりできるのですが、やはりリポジトリで管理したりデプロイの自動化を行いたくなります。
いくつか管理ツールがあるのですが、今回はServerless Frameworkを利用しています。
関数やイベントソース、関連するリソースをYAMLファイルで定義でき、また構成の自由度も高く使いやすく感じます。
さらに、プラグイン形式で機能を拡張でき例えば、今回のようにTypeScriptを利用する場合でも容易に対応することができます。

ServerlessでTypeScriptを扱う際の基本的な設定などを、serverless-typescript-starterにまとめてみました。

serverless-webpack

プラグインとして、serverless-webpackを利用しています。
このプラグインは、その名の通りServerless FrameworkにWebpackを組み込むもので、デプロイコマンド実行時にビルド後のファイルをデプロイできるようになり、またビルドされた関数をローカル環境で実行することができるようになります。
ビルドタスク自体はWebpackの機能をそのまま使っているため、ts-loaderやbabel-loaderなど任意のローダを利用することで、TypeScriptやES2015で開発を行うことができます。

TypeScriptとWebbpackの設定

AWS LambdaのNodeランタイムは、現時点では4.3.2となっています。
そのため、ES2015のいくつかのシンタックスや機能は利用することができません。
TypeScriptやWebpackを利用する場合は、そのことを考慮した設定を行う必要があります。

{
  "compileOnSave": false,
  "compilerOptions": {
    "declaration": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "target": "es5",
    "lib": ["es6", "dom"],
    "typeRoots": [
      "./node_modules/@types"
    ]
  }
}

tsconfig.jsonではtargetをnode v4.3.2で動作するes5に設定。さらにbabelなどを通す場合はes6でも大丈夫だと思います。
modulecommonjs形式で出力するように。Webpackはデフォルトではcommonjs形式のみでimport/exportを処理できないため。

'use strict';
const webpack = require('webpack');
module.exports = {
  entry: './handler.ts',
  output: {
    libraryTarget: 'commonjs',
    path: '.webpack',
    filename: 'handler.js'
  },
  externals: {
    'aws-sdk': true
  },

  target: 'node',
  resolve: {
    extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx', '.ts', '.tsx'],
  },
  module: {
    loaders: [
      { test: /\.(jsx?|tsx?)$/, loaders: ['ts-loader'], exclude: [/node_modules/] }
    ]
  },
  devtool: "#source-map"
};

serverless-webpackでは、webpack.config.jsはServiceごとに配置されることがデフォルトになっています。
output.libraryTargetcommonjs形式に。AWS Lambdaは、module.exports =のcommonjs形式で関数を定義するため。
targetにはnodeを指定。
AWS Lambdaではaws-sdkImageMagicがデフォルトで利用できるため、もし利用する場合は、externalsに設定してビルド後のコードに含まれないように。

テストや関数の分割などの方針について

まだ試行錯誤中ではあるのですが、テストや関数の分割などの現在採用している方針について記載します。

AWSリソースのフェイクの利用

関数内でaws-sdkを利用する場合は少なからずあると思いますが、Docker上にAWSリソースを模したイメージを立てて利用するようにしています。
現在は以下のimageを利用しています。

基本的にローカルでのテスト時にaws-sdkのメソッドを呼び出したときに期待するレスポンスが帰ってくることを確認するためで、イベントソースとしては利用していません。

環境変数の利用

AWS Lambdaにはビルドされたコードがデプロイされるため、当然環境変数は利用することができません。
けれども、環境に応じて値を変更したい場合というのは当然あるため、Webpackを用いてビルド時に環境変数を設定できるようにしています。

new webpack.DefinePlugin({
  'process.env': {
    'ENV': JSON.stringify(process.env.ENV),
    'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
  }
})

webpack.config.jsプラグイン定義の箇所で、必要な環境変数を利用できるようにしています。
今回はDockerイメージを立ち上げるときに環境変数を渡すようにしているため、このような手法をとっていますが、Webpackのプラグインを利用するなどの手法もあると思います。

依存するコンポーネントを受け取る関数に分割

Lambdaで実行される関数をそのままテストすることは難しいため、必要に応じて分割を行います。
特に、aws-sdkを利用するような箇所は、sdkのオブジェクトを受け取るような関数に分割を行っています。

export function uploadFile(s3: AWS.S3, key: String, body: String) {}

例えばS3にファイルをアップロードするような場合、実際の処理は上記のような関数に分割して実装しています。
ユニットテスト時にはフェイクのS3にアクセスするS3オブジェクトを利用したり、sinonのstubを利用して挙動の確認を行います。
AWS Lambda上では起動される関数内でS3オブジェクトを生成し、上記の関数を利用するようにします。
責務を明確にした上で、テスト可能な単位まで関数を分割することで、AWS上での試行錯誤を減らし、安全に開発を行うことができると思います。


まだ、いろいろと検証中で固まっているわけではないのですが、現在のAWS Lambda + TypeScriptの開発環境や考え方について、簡単に紹介してみました。