maybe daily dev notes

私の開発日誌

AWS Lambdaだけでアンケートフォームを作れる

Lambda function URL活用案件として、Lambdaだけを使ってアンケートフォームを作ってみた。 実際は回答の閲覧用にSlackも使ってるが、ほかは本当にLambdaだけ。

アーキテクチャ

コード

冗長な部分もあるので、要点だけ抜粋。フルのコードはこちらに載せた

これをLambdaのマネコンでデプロイし、Function URLを設定すれば良い。 また、 SLACK_WEBHOOK_URL 環境変数を設定すれば、アンケート結果をSlackに送信できる。

const https = require('https')

exports.handler = async (event) => {
  console.log(event)

  const req = event.requestContext.http;
  const sourceIp = req.sourceIp;
  // can perform IP address restriction here
  // e.g. if (sourceIp != "11.4.51.4") throw new Error()

  if (req.method == 'GET') {
    if (!(req.path == '/')) {
      return {
        statusCode: 404,
        body: 'Not found',
      };
    }
    return {
      statusCode: 200,
      body: getHTML(),
      headers: {
        'content-type': 'text/html'
      }
    };
  }
  else if (req.method == 'POST') {
    const response = JSON.parse(event.body);
    await processResponse(response);
    return {
      statusCode: 200,
      body: 'success',
    };
  }
  return {
    statusCode: 400,
    body: 'Bad Request',
  }
};

const processResponse = async (response) => {
  const webhookUrl = process.env.SLACK_WEBHOOK_URL;
  await post(webhookUrl, response);
};

const post = async (url, data) => {
  // c.f. https://stackoverflow.com/questions/40537749/how-do-i-make-a-https-post-in-node-js-without-any-third-party-module
}

const getHTML = () => {
  return `
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  </head>
  <body>
    <div class="flex container mx-auto py-10">
      <div class="grow">
        <form id="myForm">
          <div class="mb-6">
            <label for="q1" class="block mb-2 text-sm">
              Q1. 名前を教えて下さい。</label>
            <input
              type="text"
              id="q1"
              name="q1"
              placeholder="もつ太郎"
              required
            />
          </div>
          <div>
            <button
              type="submit"
            >
              Submit
            </button>
          </div>
        </form>
      </div>
    </div>

    <script>
      $('#myForm').submit(function (event) {
        event.preventDefault();
        var $form = $(this);
        var $button = $form.find('button');
        $.ajax({
          url: '/',
          type: 'POST',
          data: JSON.stringify(
            $form.serializeArray().reduce((json, { name, value }) => {
              json[name] = value;
              return json;
            }, {}),
          ),
          headers: {
            'Content-Type': 'application/json',
          },
        });
      });
    </script>
  </body>
</html>
`;
}

Function URLをブラウザで開くと、以下のようなアンケートフォームが表示される。

送信すると、Slackに連携される。

基本的な機能が揃っていることはお分かりいただけただろうか。

ポイント

1. ホスティングが単一のLambdaで完結する

今回はLambdaをHTTPアクセス可能にするために、Lambda Function URLと用いている。Amazon API Gatewayは使っていないので、複雑なAWSリソースの設定は不要。この程度の要件であればLambda単体で十分サービス提供可能である。

このため、Lambdaを1つだけマネコンでデプロイすれば完結する単純さが利点となる。 何らかの事情でGoogleフォームなどサードパーティー製ツールを利用できない方は、検討してみてはいかが。

ちなみに今は結果をSlackに連携しているが、上記の processResponse の実装を変えれば、任意の連携を実現可能。 例えばDynamoDBに保存したり、Amazon SNSに送信したりといったように。

2. index.js 1つで完結させるために

今回は完全手作業でのデプロイを想定し、可能な限りLambdaの構成をシンプルにしている。

例えば、ファイルが複数になると急激に手作業でのデプロイが面倒になる。 HTMLなどは別ファイルに分けたほうが本来見通しが良いが、getHTML 関数に押し込めている。

また、JavaScriptのライブラリを使うとバンドルが必要になり、これも単純な手作業ではデプロイできなくなる原因。 このため、バックエンドの実装はライブラリを一切使っていない。

フロントエンドについては CDN経由でライブラリの読み込みが可能なので、 DOM操作のために jQuery (初めて触った!) だけ使っている。jQuery、もう死んだものだと思ってたが、ここ1年リリースがなくたしかに死んでそうだった。

ReactなどモダンなフレームワークはWebpackに類するツールの利用を前提としている感があり、案外超Lightweightな用途では使いづらい気がした (preactもつらそう。) jQueryだけだと古風すぎるので、HTMLの色付け用にTailwindを使っている。

この方法はフォーム画面を作るためにHTML/CSSの知識が必須なので、若干ハードルの高い方法ではある。

3. セキュリティ面

Lambda Function URLは現状ビルトインの認証が IAM 認証しかないが、IAM認証 はブラウザからのアクセスでは使いづらい。

今回は認証なしでPublicアクセスを前提としている。 一応イベントからクライアントのIPアドレスを取れるので、IPアドレス制限程度であれば容易に実現できる。

exports.handler = async (event) => {
  const req = event.requestContext.http;
  const sourceIp = req.sourceIp;
  // 簡易なIPアドレス制限
  if (sourceIp != "11.4.51.4") throw new Error()

Basic認証を実装したら便利そうだと思ったが、なぜか現状は使えないようだった。 (www-authenticate ヘッダーが自動でDropされるような挙動を示す)

まとめ

Lambdaだけでアンケートフォームを作ってみた。

メリットは、

  • インフラは管理不要
  • 極限まで簡単な構築手順
  • 画面は無限にカスタマイズ可能

何らかの理由でこの手のSaaSが使えないという方は、検討してみても良いかも。