Hono 🔥 WakuWaku AWS deploy
↑english ver
この記事はHono Advent Calendar 2024の10日目の記事です。(投稿がぎりぎりすぎてへんなタイトルになっちゃったかもです😭)
この記事ではHonoをベースにReactのminimumフレームワーク「Waku」をAWS Amplify上に構築します。WakuはNext.jsのようなReactのサーバーサイドの最新機能をHonoのミドルウェアとして実現できます。
最後に、Honoを通じてAWS AmplifyからVPC内のAWSリソースにアクセスする方法について記載します。
AWS Amplify Hostingのデプロイ仕様により、ExpressやHonoなど、Next.js以外のサーバーアプリケーションのデプロイもサポートしています。
AmplifyはNext.jsやNuxt.jsを自動的に検出し、いいかんじに設定してくれますが、他のサーバーフレームワークはAWS Amplifyのデプロイ仕様に従ってビルド後に調整する必要があります。
1. Deploying Waku with Hono on AWS Amplify
npm create waku@latest
コマンドを使用してプロジェクトを開始し、次の3つのファイルをリポジトリのルートディレクトリに保存することで、AWS Amplifyコンソールからデプロイすることができます。
AWS Amplifyは、Lambda内でaws-lambda-web-adapterのサイドカーをおそらく使用しており、HonoのHTTPオブジェクトとLambdaのイベントオブジェクトとの間のギャップを埋めることで、シンプルなnodeコマンドで動くHonoをAWS Lambdaにデプロイすることができます。
1.1. Create startServer.mjs
このコードはAWS AmplifyのAWS Lambda@Edge環境でnode startServer.mjs
コマンドによってサーバーを起動します。
Wakuによってビルドされたdist/entries.jsのコードファイルをHonoミドルウェアとして登録します。
// startServer.mjs import { Hono } from "hono"; import { serve } from "@hono/node-server"; import { serveStatic } from "@hono/node-server/serve-static"; import { serverEngine } from "waku/unstable_hono"; const app = new Hono(); app.use(serveStatic({ root: "./public" })); app.use( serverEngine({ cmd: "start", loadEntries: () => import("./entries.js"), env: process.env, }) ); app.notFound((c) => c.text("404 Not Found", 404)); console.log(`ready: Listening on http://localhost:3000/`); serve({ ...app, port: 3000 });
amplify.yml
1.2. Create AWS Lambdaでサーバーを動かすため、AWS AmplifyのCI/CDでビルドファイルのディレクトリ構造を調整します。
Wakuのビルドファイルを.amplify-hosting/compute/defaultに移動するコマンドが含まれています。
version: 1 frontend: phases: preBuild: commands: - npm ci --cache .npm --prefer-offline - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID build: commands: - npm run build postBuild: commands: - npm prune --production - rm -rf ./.amplify-hosting - mkdir -p ./.amplify-hosting/compute - cp -r ./dist ./.amplify-hosting/compute/default - cp -r ./node_modules ./.amplify-hosting/compute/default/node_modules - cp -r ./dist/public ./.amplify-hosting/static - cp -r ./dist/public ./.amplify-hosting/compute/default - cp deploy-manifest.json ./.amplify-hosting/deploy-manifest.json - cp startServer.mjs ./.amplify-hosting/compute/default/startServer.mjs - 'echo "{ \"type\": \"module\" }" > ./.amplify-hosting/compute/default/package.json' artifacts: baseDirectory: .amplify-hosting files: - '**/*'
deploy-manifest.json
1.3. Create .amplify-hosting/compute/default内のコードを動かすためのAWS Amplifyスタックを設定します。
computeResourcesのentrypointを変更してnodeによってstartServer.mjsが実行されるようにします。(その他は公式チュートリアル通りです)
{ "version": 1, "framework": { "name": "waku", "version": "0.21.7" }, "imageSettings": {}, "routes": [ { "path": "/_amplify/image", "target": { "kind": "ImageOptimization", "cacheControl": "public, max-age=3600, immutable" } }, { "path": "/*.*", "target": { "kind": "Static", "cacheControl": "public, max-age=2" }, "fallback": { "kind": "Compute", "src": "default" } }, { "path": "/*", "target": { "kind": "Compute", "src": "default" } } ], "computeResources": [ { "name": "default", "runtime": "nodejs18.x", "entrypoint": "startServer.mjs" } ] }
設定をGitHubにプッシュし、AWS AmplifyコンソールからGithubとCI/CDを接続するだけで、HonoやWakuなどのWebフレームワークをデプロイできます 🚀
2. Deploying Hono in AWS VPC using AWS CDK
AWS Amplify Backend はCI/CDを通じてCloudFront、CloudWatch、Lambda、S3などの構築を自動化してくれますが、現在AWS AmplifyをVPC内にAWS LambdaをデプロイしたりIPアドレス制限(#12)、WAF設定(#36)などの詳細な設定に対応していません。(今月中に改善されるかもしれません!)
次章では、aws-cdkとAWS AmplifyのCI/CDを使用してVPC Lambdaをデプロイし、VPC内のRDSや他のAWSスタックリソースに Honoでアクセスできるようにします。
AWS Amplify Backendを使用すると、サービス開発に必要な構成をすぐに構築してデプロイできます。
例えば、AWS Cognitoのユーザー認証やAWS DynamoDBのサーバーレスデータベースがあります。
次のコマンドでAWS Amplify Backendを初期化します:
npm create amplify@latest
2.1. Define the Handler
簡単な"Hello Hono!"テキストを返すAWS Lambdaハンドラーを定義します。
これは、将来的にVPC内のRDSやEC2にアクセスするために使われる想定です。
// amplify/handler.mjs import { Hono } from "hono"; import { handle } from "hono/aws-lambda"; const app = new Hono(); app.get("/", (c) => c.text("Hello Hono!")); export const handler = handle(app);
2.2. Custom Lambda in VPC
AWS CDKを使用してVPC内にAWS Lambdaカスタムスタックを定義します。
Honoのドキュメントのコードを参考に、aws-cdkでVPC Lambdaを構成します。
// amplify/custom.ts import * as cdk from "aws-cdk-lib"; import * as lambda from "aws-cdk-lib/aws-lambda"; import * as ec2 from "aws-cdk-lib/aws-ec2"; import { Construct } from "constructs"; import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; interface Props { vpc: ec2.Vpc; } export class VpcLambdaStack extends Construct { constructor(scope: Construct, id: string, props: Props) { super(scope, id); const { vpc } = props; const fn = new NodejsFunction(this, "VpcLambdaFunction", { entry: "amplify/handler.mjs", handler: "handler", runtime: lambda.Runtime.NODEJS_18_X, vpc, }); new cdk.CfnOutput(this, "VpcLambdaFunctionArn", { value: fn.functionArn, exportName: "VpcLambdaFunctionArn", }); } }
2.3. Configure Backend
backend.tsにVPC Lambdaのカスタムスタックを追加することで、AWS AmplifyのCI/CDを通じて構築およびデプロイされます。(以下のコードでは、CognitoとDynamodbのデプロイは一旦削除しています)
// amplify/backend.ts import { defineBackend } from "@aws-amplify/backend"; import * as ec2 from "aws-cdk-lib/aws-ec2"; import { auth } from "./auth/resource"; import { data } from './data/resource'; + import { VpcLambdaStack } from "./custom"; const backend = defineBackend({ - auth, - data, }); const stack = backend.createStack("CustomResources"); const vpc = new ec2.Vpc(stack, "CustomStackVPC"); new VpcLambdaStack(stack, "CustomStackLambda", { vpc });
これらの変更をGitHubにプッシュすることで、CI/CD上でVPC Lambdaをデプロイできます 🚀
3. Accessing VPC Lambda via AWS Amplify
VPC内のリソースを使用してサーバーサイドレンダリング(SSR)を実現するために、VPC内のLambda関数とAWS Amplifyを接続する手順を説明します。
3.1. Verify ARNs
最初に、AWS AmplifyアプリケーションとVPC Lambdaの両方のApp ARNをメモします:
-
AWS Amplify ARN:
Amplify console > General settings
ページにApp ARNがあります。後で参照するので、サービスロールもメモします。
-
VPC Lambda ARN:
CloudFormation > Stacks > {appname} > Outputs > value
にAmplifyカスタムリソースのデプロイログとして表示されるようにしています。(先ほどメモしたAmplify AppのARNっぽいCustomResourcesにあります)
3.2. Attach IAM for Invoker
VPC Lambdaを呼び出すためにIAM Userを作成します。
このIAM UserにはVPC Lambdaをinvokeすることを許可するポリシーが必要です。
-
Create IAM User: VpcLambdaInvokerなどのいいかんじの名前でIAM User を作成し、
IAM / Users / VpcLambdaInvoker / Create access key
ページでクレデンシャル(アクセスキーおよびシークレットアクセスキー)を作成します。
-
Policy Setup:
IAM / Users / VpcLambdaInvoker / Create policy
ページのJSONエディターでIAM UserにVPC Lambdaをinvokeする権限を付与するポリシーをアタッチします。ポリシーには以前メモしたVPC LambdaのApp ARNをリソースの値として指定します。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "lambda:InvokeFunction", "Resource": "<VPC_Lambda_App_ARN>" } ] }
3.3. Attach AWS Hosting's Service Role
AWS Amplifyが使用するサービスロールを更新して、VPC Lambdaを呼び出すための権限を含めます:
-
Role Selection:
IAM / Roles
ページでAmplifySSRLoggingRoleを検索し、以前にメモしたAWS Amplifyサービスロールに一致するものを選択します。
- Policy Configuration: パラメータを取得するために必要なSSM操作を許可する以下のカスタムポリシーを作成し、サービスロールにアタッチします。以前にメモしたAWS AmplifyのApp ARNの最後のほうにあるApp IDをAMPLIFY_APP_IDに入力します。(たぶんurlとかに使われているidです)
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowAmplifySSMCalls", "Effect": "Allow", "Action": [ "ssm:GetParametersByPath", "ssm:GetParameters", "ssm:GetParameter" ], "Resource": [ "arn:aws:ssm:*:*:parameter/amplify/<AMPLIFY_APP_ID>/*" ] } ] }
InvokeVpcLambdaPolicyみたいな名前を付けてポリシーを作成したら、IAMの設定は完了です!
以下の4つの環境変数の値を保存しておきます:
# .env.local VPC_AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx VPC_AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx VPC_LAMBDA_AWS_REGION=ap-northeast-1 VPC_LAMBDA_FUNCTION_NAME=amplify-xxxxxxxxxxxxxx-xx-xxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxx
4. Create App
IAMを設定したので、WakuのサーバーコンポーネントからVPC内のAWSリソースにアクセスできるようになりました。
最後に、AWS AmplifyおよびLambdaに環境変数を設定し、VPC Lambdaをinvokeする方法を説明します。
4.1. Invoke VPC Lambda in Server Component
VPC Lambdaをinvokeすることで、UI上でVPC内のデータを表示することができます。
以下のコードはServer Componentから VPC Lambdaを呼び出す例です:
import { Lambda } from "@aws-sdk/client-lambda"; const lambda = new Lambda({ region: process.env.VPC_LAMBDA_AWS_REGION!, credentials: { accessKeyId: process.env.VPC_AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.VPC_AWS_SECRET_ACCESS_KEY!, }, }); async function invokeLambda() { const args = { FunctionName: process.env.VPC_LAMBDA_FUNCTION_NAME!, Payload: JSON.stringify({}), }; const res = await lambda.invoke(args); const buf = Buffer.from(res.Payload!); return JSON.parse(buf.toString()); } export default async function HomePage() { const data = await invokeLambda(); return <p>{data.body}</p>; // Display Hello Hono! }
4.2. Manage Environment Variables
取得した環境変数をAmplifyコンソールのManage environment variables
ページで設定します。
4.3. Deploy
CI/CDプロセス中にAWS Lambdaに環境変数を渡すためにamplify.ymlファイルを更新します。
以上の手順でVPC内のAWSリソースに接続し、コンテンツをレンダリングできるようになりました🎉
version: 1 frontend: phases: preBuild: commands: - env | grep -e VPC_AWS_ACCESS_KEY_ID >> .env - env | grep -e VPC_AWS_SECRET_ACCESS_KEY >> .env - env | grep -e VPC_LAMBDA_AWS_REGION >> .env - env | grep -e VPC_LAMBDA_FUNCTION_NAME >> .env - npm ci --cache .npm --prefer-offline - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID build: commands: - npm run build postBuild: commands: - npm prune --production - rm -rf ./.amplify-hosting - mkdir -p ./.amplify-hosting/compute - cp -r ./dist ./.amplify-hosting/compute/default - cp -r ./node_modules ./.amplify-hosting/compute/default/node_modules - cp -r ./dist/public ./.amplify-hosting/static - cp -r ./dist/public ./.amplify-hosting/compute/default - cp deploy-manifest.json ./.amplify-hosting/deploy-manifest.json - cp startServer.mjs ./.amplify-hosting/compute/default/startServer.mjs - 'echo "{ \"type\": \"module\" }" > ./.amplify-hosting/compute/default/package.json' artifacts: baseDirectory: .amplify-hosting files: - '**/*'
Discussion
memo:
VPC_LAMBDA_FUNCTION_NAME
で指定する VPC Lambda の function name は aws-cdk で出力した functionArn の後ろのほうの文字列っぽいです 😭例)
AWS Lambda / functionArn
AWS Lambda / functionName