7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Wano GroupAdvent Calendar 2024

Day 5

RemixアプリをALB Lambdaとしてデプロイする

Last updated at Posted at 2024-12-04

この記事はWano Group Advent Calendar 2024の5日目の記事となります。

ALB -> Lambda -> WebApp?

アプリケーションサーバーをAWS Lambdaでホストする、というのを仕事でも個人的にも最近たまにやっています。このへんでいつもお世話になっているのが aws-lambda-web-adpter ですね。
aws-lambda-web-adapterを仕込んだDocker Image を作ってデプロイ、Cloudfront からLambda Function URL や API Gateway へ...みたいなのが王道パターンかと思います。

実はこれだけでなく、ALBからもLambdaってルーティングできるらしいのです。
普通に aws-lambda-web-adapter で作ったLambdaアプリもアプリケーションサーバーとして起動できそうな気がしますが、個人的にやったことがありませんでした。

ということで、今回はそれをやってみた記事となります。

既存のWebサイトの特定パス以下にアプリをデプロイしたい

このアプリ専用でドメインを切るのではなく、既存のWebサイトの特定ディレクトリをルートとして使う想定でプロジェクトを作ることとします。

このおかげで本筋の ALB + Lambda とは別のルーティングノウハウへの言及が多くなってしまいました。Remix アプリ事例そのものとしてもなんらかの参考になるかと思うので、そのへんはご容赦ください。

Remixアプリの用意

通常、npx create-remix@latestで初期化すると、以下のような構成になります。
(デプロイ用スクリプトを格納するbuild_deployディレクトリを追加しています)

README.md
app
build_deploy // デプロイ
node_modules
package-lock.json
package.json
postcss.config.js
public
server.js // デプロイ用expressサーバー
tailwind.config.ts
tsconfig.json
vite.config.ts

インデックス

app/routes/
├── _index.tsx
├── sample_site1._index
│   └── index.tsx
sample_site1._index/index.tsx
export default function Index() {

  return (
    <div>
      <h1>sample_site1</h1>
      <p>このディレクトリがアプリルート</p>
    </div>
  )
}

今回は /sample_site1/へのリクエストを想定したWebページを実装します。

express設定

server.js はexpressサーバーとなっており、実際の稼働時はRemixはexpressのプラグインとして動作させます。

Remixは公式にExpressとの連携例が提供されているため、本実装でもExpressをWebサーバーとして使用しています。なお、最近ではHonoとの連携も容易になっているため、それも選択肢の一つとなるでしょう。

今回は既存Webサイトのサブディレクトリとしてアプリケーションをデプロイするため、URL /sample_site1/ 以下をアプリケーションのルートとして設定します。

(このアプリで専用のドメイン作るのであれば、SITE_PREFIX_DIRあたりの記載は無視してOKです。)

server.js
import { createRequestHandler } from "@remix-run/express";
import express from "express";
import * as build from "./build/server/index.js";
import path from 'path';
import { fileURLToPath } from 'url';

const SITE_PREFIX_DIR = "/sample_site1";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// eslint-disable-next-line no-undef
const _PORT = process.env.PORT || 7000;
// eslint-disable-next-line no-undef
const BUILD_VERSION = process.env.BUILD_VERSION || "0.0.0";

const app = express();

// 静的ファイルの提供
app.use(`${SITE_PREFIX_DIR}/assets`, express.static(path.join(__dirname, "build", "client", "assets"), {
  maxAge: 100000, // 100秒
  setHeaders: (res, /*path*/) => {
    res.setHeader('Cache-Control', 'public, max-age=100');
  }
}));

app.use(express.static(path.join(__dirname, `${SITE_PREFIX_DIR}/public`), {
  maxAge: 100000, // 100秒 
  setHeaders: (res,/* path*/) => {
    res.setHeader('Cache-Control', 'public, max-age=600');
  }
}));

// /img パスを build/client/images にマッピング
app.use(`${SITE_PREFIX_DIR}/images`, express.static(path.join(__dirname, "build", "client", "images"), {
  maxAge: 100000, // 100秒 
  setHeaders: (res) => {
    res.setHeader('Cache-Control', 'public, max-age=600');
  }
}));

// デバッグ用のログ
//app.use((req, res, next) => {
//  console.log(`Requested URL: ${req.url} をserver.jsで受信`);
//  next();
//});

// ALBのヘルスチェック用エンドポイント
let healthCheck = 0;
const healthCheckPrintMax = 5;
app.get(`${SITE_PREFIX_DIR}/health`, async (req, res) => {
  console.log(`ヘルスチェックを受信`)
  if (healthCheck < healthCheckPrintMax) {
    healthCheck++;
    console.log("Health check OK", BUILD_VERSION , `:` , healthCheck);
  }

  res.send("OK");
});

// Remixのリクエストハンドラ
app.all("*", createRequestHandler({ build }));

app.listen(_PORT, () => {
  console.log(`Server started on http://localhost:${_PORT}`);
});

console.log(`@@@@@@@ AWS Lambda COLD START @@@@@@@@@@@`);

ALBのヘルスチェックは /sample_site1/health エンドポイントで受け付けます。

Viteのアセットパス設定

サブディレクトリ運用のアセットパスを正しく設定するため、Viteの設定でベースパスを指定します。
このままでは /asset/index.jsとかにファイルを取りに行ってしまいますので,/sample_site1/asset/index.jsを見てもらうためです。

vite.config.ts
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

declare module "@remix-run/node" {
  interface Future {
    v3_singleFetch: true;
  }
}

export default defineConfig({
  base: '/sample_site1/', // ここだけ追加
  plugins: [
    remix({
      future: {
        // React Router v7にアップデートするために必要なフラグ群
        v3_fetcherPersist: true,
        v3_relativeSplatPath: true,
        v3_throwAbortReason: true,
        v3_singleFetch: true,
        v3_lazyRouteDiscovery: false, // https://github.com/remix-run/remix/issues/10124
      },
    }),
    tsconfigPaths(),
  ],
});

注意点として、202412時点で上記のv3_lazyRouteDiscoveryにはバグが有ります。

これを true にすると、ルーティング時にmanifestファイルというものをまずRemixが探しに行くようになりますが、これが Vite の baseもRemixのbasenameも尊重してくれません。

/sample_site1/ではなく/にルーティングマニフェストファイルを要求してしまい、404エラーが発生します。

次期 Remix である React Router v7のリリースまでにこの問題が解決されることが期待されます。

Docker Image /Lambdaのデプロイ

build_deploy以下は

build_deploy
└── site1
    ├── Dockerfile
    └── Makefile

としています。

アプリケーションのビルドとaws-lambda-web-adapterの設定を含むDockerfileを実装します。

DockerFile
FROM arm64v8/node:22.3-alpine3.19 AS build-deps
WORKDIR "/var/task"
COPY package.json package-lock.json ./
RUN npm ci

# Build stage
FROM build-deps AS build
COPY . .
RUN npm run build

# Production dependencies stage
FROM arm64v8/node:22.3-alpine3.19 AS prod-deps
WORKDIR "/var/task"
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Final stage
FROM arm64v8/node:22.3-alpine3.19 AS runner
WORKDIR "/var/task"
RUN apk update && apk add ca-certificates tzdata && rm -rf /var/cache/apk/*
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
ENV PORT=7000

# Copy only necessary files from previous stages
COPY --from=prod-deps /var/task/node_modules ./node_modules
COPY --from=build /var/task/build ./build
COPY --from=build /var/task/public ./public
COPY --from=build /var/task/package.json ./package.json
COPY --from=build /var/task/package-lock.json ./package-lock.json
COPY server.js ./server.js

ENV NODE_OPTIONS=--enable-source-maps
ENV VITE_ENV=production


CMD ["node", "server.js"]

MakefileでDocker Imageの作成、ECRへのプッシュ、Lambdaの更新を自動化します。
LambdaとECRは事前にsample_site1という名前で作成されている前提です。

Makefile
LAMBDA_FUNC_NAME = sample_site1
ECR_COM = xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com
REPOSITORY_URI_LAMBDA=${ECR_COM}/sample_site1
LOCAL_DOCKER_IMAGE=vkjp/sample_site1:latest

build:
	mkdir -p .build

	docker build \
		-f ./Dockerfile \
		--platform linux/arm64 \
		--progress=plain \
		-t ${LOCAL_DOCKER_IMAGE} ../..
	rm -rf .build


deploy_frontend:
	export AWS_PAGER="" && \
	aws ecr get-login-password | docker login --username AWS --password-stdin ${ECR_COM} && \
	docker tag ${LOCAL_DOCKER_IMAGE} ${REPOSITORY_URI_LAMBDA}:latest && \
	docker push ${REPOSITORY_URI_LAMBDA}:latest

	# UPDATE
	@echo "Lambdaを更新して伝播を待機中...."
	aws lambda update-function-code \
		--function-name ${LAMBDA_FUNC_NAME} \
		--image-uri "${REPOSITORY_URI_LAMBDA}:latest" \
		--publish

	@echo "更新の伝播を待機中...."
	@while [ "$$(aws lambda get-function --function-name ${LAMBDA_FUNC_NAME} --query 'Configuration.LastUpdateStatus' --output text)" = "InProgress" ]; do \
		echo "更新中... 待機しています"; \
		sleep 5; \
	done

このディレクトリでmake buildmake deploy_frontendを実行することでデプロイが完了します。

ALBターゲットグループ/ALBリスナールール作成

ここまでの実装はAPI Gateway + LambdaLambda Function URL + LambdaでのRemixアプリケーションのデプロイと同様です。以下がALB特有の実験となります。

ALBのターゲットグループ、「samplesite1」を作り、先程のLambdaを指定します。

image.png

今回はあまりALBのヘルスチェック設定する意味もないのですが、server.jsで設定したヘルスチェック関数にルーティングしておきます。

image.png

あとはALBリスナールールに特定ホスト、特定パス以下の挙動を先程のターゲットグループ 「samplesite1」に当てれば完成です。

image.png

動作確認

/sample_site1にアクセスしてみます。

image.png

/sample_site1にアクセスすると、CSSとJavaScriptが正しく読み込まれ、アプリケーションが正常に動作することが確認できます。'ちょっとわからない見た目ですね...)

考察

ALB + Lambda(aws-lambda-web-adapter)でも問題なくサイト構築ができることがわかりました。

ここまでALBでやってきてなんですが、キャッシュのことなどを考えると普通に 前段のCloudfront 側で Lambda Function URLにルーティングするかな... という気はします。ALBからルーティングされているからといってこれが自動的に VPC Lambda になるってわけでもないですからね。

ALB Lambda、用途がすぐには思い浮かばない...。

いろいろ試せましたということで、本稿を終わりたいと思います。

明日は @kotobuki5991 さんの「OpenFeature x AWS AppConfigでフィーチャーフラグ」となります。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?