🚀

Hono × Lambda@Edgeで、開発環境にBasic認証かけるやつ

2023/08/24に公開

はじめに

HonoがAWSのCDN Edgeで動いたら嬉しいな、と思って、色々あってLambda@Edge用のAdaptorを作りました!

https://github.com/honojs/hono/releases/tag/v3.3.0

HonoをLambda@Edgeで採用するメリット

Lambda@Edgeのコードを書く際に、そもそもフレームワークを採用するか?という判断はありますが、少なくとも以下のメリットを得ることができます。

  • パスルーティングやビルドインされたMiddlewareなど、WEBフレームワークの恩恵を受けられる。
  • ライブラリが軽量(20KB前後)のため、Viewer-request/responseの1MB制限の環境にもマッチする。
  • Cloudflare Workersなどの各種Edge環境にも対応しているため、他のランタイムへコードを移行しやすい。
    • また、他のランタイムでのノウハウをそのまま輸入しやすい。
    • ちなみにHonoはAPI Gateway + AWS LambdaやLambda Functions URLにも対応しています!
      https://hono.dev/getting-started/aws-lambda

動かす

知名度を上げるためにも(?) 、Adaptor作者の私が簡単に動かしてみましょう。

以下、公式のGetting Startedを下敷きにお話しします。

https://hono.dev/getting-started/lambda-edge

1. 環境

最初にGetting Startedの1~3までを実施し、CloudFront+S3+Lambda@Edgeの環境を作ります。

https://<CloudFrontのURL>/にアクセスすると、S3に何も置かなくても、Lambda@Edgeがレスポンスを返すのがわかります。

https://<CloudFrontのURL>/hono/にアクセスすると404が返ります。

サラッとやってますが、これ地味に便利です。

というのもS3をオリジンにする場合、存在しないパスを指定すると、404でなく403が返却されて気持ち悪いのですが、Lambda@EdgeにHonoを設定することで、対象外のパスを404として簡単にコントロールできます。

2. HTMLの配置

先ほど作成したS3に雑なHTMLを配置します。場所は適当に、s3://<バケット>/test/hono.htmlなどに置きます。

test/hono.html
<h1>Hello!!</h1>

3. Hono × Lambda@Edge

デモ用なので簡単な要件を設定してみます。

  • /hono/ -> /test/hono.html とルーティングする。
  • シンプルなBasic認証をかける。

viewer-requestに関連づけられたLambda@Edgeを以下のイメージで更新し、cdk deployを行います。

import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'

import type { Callback, CloudFrontRequest } from 'hono/lambda-edge'
import { handle } from 'hono/lambda-edge'

type Bindings = {
  callback: Callback
  request: CloudFrontRequest
}

const app = new Hono<{ Bindings: Bindings }>()

app.use(
  '*',
  basicAuth({
    username: 'hono',
    password: 'acoolproject',
  })
)

app.get('/hono/', async (c, next) => {
  await next();
  c.env.request.uri = "/test/hono.html"
  c.env.callback(null, c.env.request)
})

export const handler = handle(app)

これでS3のパス書き換えも、Basic認証もLambda@Edgeで出来ちゃいました。簡単ですね。

え? パス書き換えならCloudFront Functionsでも実現できるし、AWS WAFでのBasic認証を行えばLambda@Edgeの必要はなさそう? この例だと確かにそうかも。せっかくだからもう少し捻ってみましょう。

4. 商用ドメイン以外だけBasic認証

こんな記事があるように、商用環境以外は一般に公開したくないというニーズがありますね。

https://analyzegear.co.jp/blog/767

で、AWS WAFで作りこむと、商用とそれ以外でのテンプレート差分が出てしまって、ちょっと面倒。

ですが、このように、Honoのミドルウェアの利用条件を追加してあげると、Lambda@Edgeのコードを簡単に一本化できます!

import { Hono } from 'hono';
import { basicAuth } from 'hono/basic-auth';

import type { Callback, CloudFrontRequest } from 'hono/lambda-edge';
import { handle } from 'hono/lambda-edge';

type Bindings = {
  callback: Callback;
  request: CloudFrontRequest;
};

const app = new Hono<{ Bindings: Bindings }>();

// app.use(
//  '*',
//  basicAuth({
//    username: 'hono',
//    password: 'acoolproject',
//  })
// )

// 商用ドメイン「以外」はBasic認証
app.use('*', (c, next) => {
  const host = c.req.header('host')
  const prodDomein = 'aaaaaaaaaaaaa.cloudfront.net'
  if (host !== prodDomein) {
    return basicAuth({
      username: 'hono',
      password: 'acoolproject',
    })(c, next);
  }

  next();
});

app.get('/hono/', async (c, next) => {
  await next();
  c.env.request.uri = "/test/hono.html";
  c.env.callback(null, c.env.request);
});

export const handler = handle(app);

こんな感じでLambda@Edgeのカスタマイズ性を生かしながら、フレームワークの生産性を発揮できるのが嬉しいところです。

まとめ

Honoが使えるようになったことで、Lambda@Edgeが書きやすく、またライブラリのエコシステムを活用することでLambda@Edge自体の可能性も広がるでしょう。グローバルなサーバレスAPIや新たなフロントエンド基盤としても使えるようになるのが楽しみです。

Appendix. Hotswap Deployment

cdk deployでLambda@Edgeを更新すると10分弱ほどかかるため、デバッグしずらいのが難点。
開発時にこの待ち時間は時間はちょっと許容できないので、おまじないを使います。

具体的にはCDKのTemplateで作ったpackage.jsonにデプロイ用のスクリプトを追記します。インストール方法は割愛しますが、jqとAWS CLIv2に依存している手順なので入ってない場合は導入してください。

package.json
...
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk",
    "honoBuild": "esbuild --bundle --outfile=./dist/index.js --platform=node --target=node18 ./lambda/index_edge.ts",
    "zip": "zip -j lambda.zip dist/index.js",
    "update": "aws lambda update-function-code --zip-file fileb://lambda.zip --function-name $FUNCTION_NAME --region us-east-1",
    "publish": "aws lambda publish-version --function-name $FUNCTION_NAME --region us-east-1 > publish-output.json",
    "get-arn": "jq -r '.FunctionArn' publish-output.json > new-lambda-arn.txt",
    "get-config": "aws cloudfront get-distribution-config --id $DISTRIBUTION_ID > current-config.json",
    "update-config": "jq --arg newArn $(cat new-lambda-arn.txt) '.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations.Items[0].LambdaFunctionARN=$newArn | .DistributionConfig' current-config.json > updated-config.json",
    "associate": "aws cloudfront update-distribution --id $DISTRIBUTION_ID --if-match $(cat current-config.json | jq -r '.ETag') --distribution-config file://updated-config.json",
    "deploy": "run-s honoBuild zip update publish get-arn get-config update-config associate"
  },
...

必要なパッケージをいれ、LambdaとCloudFrontの情報を環境変数に設定する。

npm install --save-dev npm-run-all esbuild
export FUNCTION_NAME=<Lambdaの名前>
export DISTRIBUTION_ID=<CloudFormationのDistribution>

これで簡単にdeploy出来る。

npm run deploy

CDK的にはCloudFrontに紐づけているLambda@Edgeのバージョンがどんどんズレてしまうので、くれぐれもプロダクション環境では注意してください。

Discussion