🔥

Hono 🔥 WakuWaku AWS deploy

2024/12/10に公開
1

https://github.com/tseijp/waku-amplify-with-lambda-test
https://github.com/tseijp/note/blob/main/2024-12-10.md
↑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のデプロイ仕様に従ってビルド後に調整する必要があります。

deploy

https://docs.amplify.aws/nextjs/start/quickstart/nextjs-app-router-client-components/

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 });

1.2. Create amplify.yml

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:
      - '**/*'

1.3. Create deploy-manifest.json

.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でアクセスできるようにします。

https://github.com/aws-amplify/amplify-hosting/issues/794
https://aws.amazon.com/jp/blogs/mobile/accessing-resources-in-a-amazon-virtual-private-cloud-amazon-vpc-from-next-js-api-routes/

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をメモします:

  1. AWS Amplify ARN: Amplify console > General settings ページにApp ARNがあります。後で参照するので、サービスロールもメモします。

arn

  1. VPC Lambda ARN: CloudFormation > Stacks > {appname} > Outputs > value にAmplifyカスタムリソースのデプロイログとして表示されるようにしています。(先ほどメモしたAmplify AppのARNっぽいCustomResourcesにあります)

output

3.2. Attach IAM for Invoker

VPC Lambdaを呼び出すためにIAM Userを作成します。
このIAM UserにはVPC Lambdaをinvokeすることを許可するポリシーが必要です。

  1. Create IAM User: VpcLambdaInvokerなどのいいかんじの名前でIAM User を作成し、IAM / Users / VpcLambdaInvoker / Create access keyページでクレデンシャル(アクセスキーおよびシークレットアクセスキー)を作成します。

iam

  1. 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を呼び出すための権限を含めます:

  1. Role Selection:IAM / RolesページでAmplifySSRLoggingRoleを検索し、以前にメモしたAWS Amplifyサービスロールに一致するものを選択します。

roles

  1. 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ページで設定します。

env

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

tseitsei

memo: VPC_LAMBDA_FUNCTION_NAME で指定する VPC Lambda の function name は aws-cdk で出力した functionArn の後ろのほうの文字列っぽいです 😭

例)

  • AWS Lambda / functionArn
    • arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:amplify-☆☆☆
  • AWS Lambda / functionName
    • amplify-☆☆☆