サーバレスでExpressと再会

eslint-typescript (様子見)

qiita.com

tslintとtslint-config-airbnbを常用していますが、中の人による上記記事ではいずれ世の中はtslintからeslintに移行されていくってことが予見されています。eslintの方が設定をpackage.jsonの中に書けるってのが好き。IDEやビルドツール群との連携は元々eslintの方が強いので、ルールのTypeScript対応が成熟してくれば。現在困ってないので今はちょっと待ちで、近い将来に移行してみようと思う。過渡的にeslintでtslintのルールを使うブリッジが提供されているけど特に狙いもないので冗長なので、eslintネイティブにいい感じになるのを様子見ながら待ち。

serverless-webpack

github.com

今まで使ってたserverless-plugin-typescripitでは、コードのコンパイルについてゼロ設定で対応してくれるけど、node_modulesはそのままクラウドへ持ち上げるのでデプロイの際に無駄に大きなパッケージを作ります。Jestとか@types以下全部とか、明らかに不要なものが含まれています。webpackでまとめて小さくするととてもコンパクトになり、結果としてデプロイ速度もスピンアップも早くなりとても有用です。

SPAを作る時にはクソめんどくさいwebpack設定も、この用途ではTypeScriptをコンパイルするだけなのでシンプルです。特にプロジェクト毎に変わるものでは無いので一度作ったらしばらく使い回しできる。

// webpack.config.js
const path = require('path');
const serverlessWebpack = require('serverless-webpack');
const webpackNodeExternals = require('webpack-node-externals');

module.exports = {
    mode: serverlessWebpack.lib.webpack.isLocal ? "development" : "production",
    entry: serverlessWebpack.lib.entries,
    resolve: {
        extensions: [
            '.js',
            '.json',
            '.ts'
        ]
    },
    output: {
        libraryTarget: 'commonjs',
        path: path.join(__dirname, '.webpack'),
        filename: '[name].js'
    },
    target: 'node',
    externals: [webpackNodeExternals()],
    module: {
        rules: [
            {
                enforce: 'pre',
                test: /\.ts$/,
                use: [
                    {
                        loader: 'tslint-loader',
                        options: {
                            typeCheck: true,
                            emitErrors: true,
                        },
                    },
                ],
            },
            {
                test: /\.ts$/,
                use: [
                    {
                        loader: 'ts-loader'
                    }
                ],
            }
        ]
    }
};

さらに、serverless.ymlではプラグイン登録と、ちょっと設定が必要。

# serverless.ymlの該当部分
plugins:
    - serverless-webpack

custom:
    webpack:
        includeModules:
            forceExclude:
                - aws-sdk
        packager: 'yarn'

webpack設定の方でnode_modulesに入るものを取り除き、serverless設定の方でパッケージに追加しています。custom.webpack.includeModulesはtrueを設定すると依存パッケージを全部取り込みます。aws-sdkは入れなくてもAWS環境側で用意されてますので、forceExcludeにホワイトリスト登録するとパッケージから省く&ワーニングが無いとのこと。

パッケージマネージャーは惰性でyarnを使ってますが、その後の機能追随と速度向上および元々のデフォルト感から、折見てnpmへ戻して良いかもと思っています。

Express

Reactアプリの対となるAPIを作る際に、引き続きAWSのサーバーレス製品群でやろうと思っています。今回の手法としてはServerless Frameworkとaws-serverless-expressでExpressアプリをLambdaへ載せてみてます。

github.com

今まではLambda上でExpressを使うことはなんとなく遅くてダメなんじゃないかなと思い込んでたけど、よく考えたらExpressのミドルウェアによってちょっとぐらいコールスタックが増えてもパワフルな現代のコンピューターの前にはなんの影響もないだろうし、一個のLambdaでエンドポイントだけ面倒見てもらってそれ以下はExpressのコードで書いた方が見通しもよく、変更にも強く、テストもしやすく、他クラウドへの移植性もよいんじゃないかなと。さらにはServerless Framworkで管理対象が巨大になってくると、背後のCloudFormationの由来にてリソース上限数制限200個が来るから、Lambdaが一個になってれば当然制限を避けられる。

社内でちょっと話してた時に指摘されたのは、多数Lambaがそれぞれ立ち上がるよりはコールドスタートが掛かりにくいってメリットはあるかもと。そしてLambdaなのにExpressで書いて良いってことになればExpressの既存知識や膨大な世間のコード資産が有効活用できそう。

# serverless.yml
service: express

provider:
    name: aws
    runtime: nodejs8.10
    stage: dev
    region: ap-northeast-1

plugins:
    - serverless-webpack

custom:
    webpack:
        includeModules:
            forceExclude:
                - aws-sdk
        packager: 'yarn'

functions:
    app:
        handler: lambda.default
        events:
            - http:
                  method: ANY
                  path: '{proxy+}'
                  cors: true

serverless.ymlの方では、methodが「ANY」で、pathが「{proxy+}」を設定します。これはAWSのドキュメントではプロキシ統合と呼んでいる。これでAPI GatewayではルーティングをせずにExpressの方に全てが回って来ます。

// lambda.ts
import { createServer, proxy } from 'aws-serverless-express';
import { eventContext } from 'aws-serverless-express/middleware';
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import * as express from 'express';

// 普通にExpressのインスタンスを作る
const app = express();

// aws-serverless-express/middlewareでAPI Gatewayのイベントを搭載してみる
// http.Requestですでにアプリを作るのに十分な情報があるので、実戦だったらいらない機能かなと思います。
app.use(eventContext());
type Event = {
    apiGateway: {
        event: any;
    };
};
const dumpEvent: express.Handler = (req, res) => {
    res.json((req as unknown as Event).apiGateway.event);
};

// ここでは例なのでワイルドカードによる全部受けハンドラ登録をExpressに対して行っている。
// 実戦では、app.get('/dump', dumpEvent); というようにメソッドとパスの組み合わせを具体的に設定する。
app.all('*', dumpEvent);

// aws-serverless-expressでLambdaと直結する。
const server = createServer(app);
export default function (event: APIGatewayProxyEvent, context: Context) {
    proxy(server, event, context);
}

上記ではとりあえず例としてExpressでもワイルドカード利用でリクエスト全部受けのコード書いてますが、実戦ではExpressのルーティング作法で細かく普通にやればよく、そうすれば想定外パスへのリクエストはExpressの機能として404が返ります。

Serverless Components (様子見)

github.com

Serverless Frameworkが定義ファイルからCloudFormationテンプレートを生成してクラウド操作するのではなく、定義ファイルからコンポーネントインスタンスが生成されてそれらがAPIを直接操作する仕組みになっていた。これは良いアイディアで良いアーキテクチャだと思うけど、実装として今はまだ未成熟。Serverless Frameworkの先としてうまく融合 or 進化してほしい。

CLIだけでなく コードでデプロイなどの操作を行う機能 も素敵。そのほかそここことなく、Gulpのようなものを目指してるのかなと思った。

Amplify (お蔵入り)

amplify-cliのfunctionやapi機能がTypeScriptにまだ対応していない。また吐き出すScaffoldの構造がちょっと好きじゃないかな。そもそもにExpressでいいじゃないかと思ったのは、AmplifyでAPIを定義する際にCLIが表示する選択肢の中にExpress利用を見つけたからなので、もうちょいTypeScript対応が進めば個人的な見え方は変わると思います。でも今日ではなかった。

LoopBack (お蔵入り)

5年ぐらい前に、自分的には第1期としてNodeに触れていた時期に、チューンドNodeバイナリを提供していたStrongLoopを見つけました。StrongLoopは当時住んでた近所のSan Mateoに会社が所在していたので、そこで開催されたNodeのMeetupに参加するのにオフィスに入ったこともあります。連想的に彼らが作ってたNode上でRESTful-APIを作るCLI/フレームワークとしてLoopBackを触ったことがありましたが当時APIはGo言語で書くことになったのと、モノとして時期尚早感があったので使うことはありませんでした。ニューストピックとしてその後すぐにStrongLoopがIBMに買収されたことまで追いましたが、しばらく忘れていました。Expressを検討する中で懐かしい名前としてStrongLoopとLoopBackがまた出てきた。

ちょうど新しいバージョン4を出す前夜ぐらいなタイミングで、しかもその4はそれまでのコードベースを捨ててTypeScriptで書き直しているということで、俄然興味がでてきます。

v4.loopback.io

CLIで吐き出すのはTypeScriptオンリーでそれは素敵なのですが、フレームワークの作法としてクラスを書いてデコレータでDIするなど昔のJavaフレームワークかのような。製品WEBでGraphQLとかも語ってたのでこれまでの実績捨ててゼロから作り直した動機はこの辺かと期待しましたが。。。分厚い。。。スタックが深いよ、これは。また4はそれまでと異なりExpressベースではなくなっていました。そしてさすがan IBM Company。IBMクラウドを基本に考えているので、サーバレスにはちょっと背を向けてる風な感じ。各クラウドベンダーの独自なマネージドサービスを多用するサーバレスは現時点での競争負け組であるIBMの戦略には合わないみたい。そして、4が出てくるこのタイミングでわざわざExpressベースとはいえ古い3を使うかというと、なんか気が進まない。よってまたLoopBackはそっと閉じて心の隅にしまう。