AWS lambdaでステートフルWEBアプリを使う方法


はじめに

AWS lambdaでWEBアプリを動かしたいけど、API Gatewayの仕様が窮屈で諦めた。
ステートフルなWEBアプリをステートレスに改修するのは面倒。
そのような方に、API Gatewayを使わない構成をご紹介します

AWS lambdaでWEBアプリは無理?

AWS lambdaの主な特徴

  • サーバの面倒をみなくてよい
  • 使わないと料金が発生しない
  • 急なリクエスト増にも標準で対応できる

この最初の2つの特徴を知って、多くの人が驚き、喜び、そして、

サーバーの面倒みなくていいし、使わない時間の料金は請求されなくて安いし、
月に2, 3回数人が数分使うだけの社内の簡易WEBアプリを動かすのに最適じゃない?

と、ごくごく自然に考えます。

じゃ、さっそく試そうとすると、

AWS lambdaをHTTPSで使うには、図のようにAPI Gatewayを前段に使う必要があることがわかり、
じゃ、API Gatewayを使おうとすると、

  • API Gatewayには最大30秒のタイムアウト制約があり、AWS lambdaを30秒しか動かせない
  • API Gatewayが、常に同じlambdaインスタンスを使ってくれる保証がない
  • API Gatewayがヘッダーを書き換えてしまうので、WEBアプリに実装されているBASIC認証が使えない。API Gateway専用のlambdaを作る必要がある
  • websocketが使えない

と、いろいろ面倒(仕様上の制約)な壁にぶつかります。

WEBアプリを改修すればAWS lambda + API Gatewayで動かせますが、
そもそも利用頻度も低く手間をかけたくないという動機なので、
わざわざWEBアプリをステートレスに改修する手間はかけたくありません。

できないじゃん、、、残念

と私も諦めていました。この方法を発見するまでは。

この方法でAWS lambdaのWEBアプリにHTTPSアクセスできる

Ngrok(エングロック)というサービスをご存知でしょうか。
URLのエンドポイントを提供してくれる、開発者に人気のリバースプロキシ―サービスです。
PC上で開発中のアプリを起動し、Ngrokで一時的なURLを取得してインターネットに公開。
インターネットからのアクセスを開発中アプリに流すことができます。

ある時思い立って、AWS lambdaからNgrokに接続しやると、
Ngrokで取得したURLで、ステートフルなWEBアプリが普通に使えるではありませんか!!

この図の構成が普通に動くのです。

この構成であれば、

  • 30秒のタイムアウト制約はありません。300秒AWS lambdaの実行時間最大まで使えます
  • 同じインスタンスがリクエストを処理してくれるので、ステートフルWEBアプリがそのまま動きます
  • WEBアプリに実装されているBASIC認証がそのまま使えます
  • websocketも使えます

今まで、WEBアプリをlambdaで動かすには、次の記事のように

AWS Serverless Expressなどを使って改修して動かすことができましたが、
この構成では、Ngrokに接続するラッパーだけ書けばいいのでとても簡単です。

サンプル

この構成のサンプルアプリ(Node.js + express)をGithubに登録しましたので試してみてください。

'use strict';

const app = require('./app');
const PORT = 3000;
const ngrok = require('ngrok');
const EventEmitter = require('events').EventEmitter;
const ev = new EventEmitter();

const slack_url = process.env.SLACK_URL;
const https = require('https');
const url = require('url');

const postSlack = (msg) => {
  const opts = url.parse(slack_url);
  opts.method = 'POST';
  opts.headers = {'Content-Type': 'application/json'};
  const req = https.request(opts, (res) => {
    console.log("Send a slack message: " + res.statusCode);
  });
  req.on('error', (err) => {console.log(err)});
  req.write(JSON.stringify({text: msg}));
  req.end();
}

module.exports.server = (event, context, callback) => {
  let URL;
  // (1)
  const server = app.start(PORT, context, (req, res) => { // (4)
    ngrok.kill();
    console.log('Receive Bye request');
    postSlack('Closed ' + URL);
    ev.emit('kill');
  })
  // (2)
  ngrok.connect(PORT, (err, url) => {
    if (err) {
      callback(err, null);
      return;
    }
    URL = url;
    ev.once('kill', () => {
      const response = {
        statusCode: 200,
        body: JSON.stringify({ message: 'Good bye' }),
      };
      server.close();
      console.log('App closed.');
      callback(null, response);
    })
    postSlack('Go ' + URL);  //(3)
    console.log('Connect to %s', URL);
  })
};

簡単に解説すると

  1. アプリをPort 3000で起動する
  2. NgrokでPort 3000を公開して、一時URLを取得する
  3. 一時URLをSLACKで通知する
  4. 特定のリクエストがきたら終了する
  • 4の終了処理をきれいにやるためにコードが少し多くなっていますが、lambdaのタイムアウト終了でよしと割り切ればシンプルにできます

おわりに

AWS lambdaの中からリバースプロキシ(Ngrokなど)との間に接続を張り、リバースプロキシ経由でWEBアプリを利用する。これでAPI Gatewayの仕様上の制約から解放されて、AWS lambda上でステートフルなWEBアプリを使うことができました。

AWS lambdaの最大実行時間300秒の制約の範囲内でですが、
社内のちょっとした処理など一回2,3分アプリが動けば十分なケースで使える構成かと思います。試してみてください。

補足

  • この記事はServerless Meetup Tokyo #8のLTで話した内容です
  • 参考 LT資料@SlideShare
  • 他の言語や他のWEBサーバーでも同様のアプローチでWEBアプリを使う事ができるようになります
  • Ngrokはセキュリティーが気になるという場合は、自前でlocaltunnelサーバーを建てるか、SSHポート転送を使う事でも問題を解決できます。