AstroのSSRをAWS Amplifyでホスティングするときの備忘録 220MB制限など

AstroのSSRをAWS Amplifyでホスティングするときの備忘録 220MB制限など
Astro Astro
AWS Amplify AWS Amplify
Node.js Node.js

AstroのSSRをAmplifyでホスティングしたい皆さんこんにちは。

SSGのAstroをAmplifyでホスティングする分にはらくちんなのですが、SSRをしようとすると途端にめんどくさくなります。

早く公式のアダプターを出しなさ~い!

ということでやり方や困ったことの対処法をメモします。

実際の経験を元に書いてるので、間違ったことがあれば指摘してください。まぁこの記事コメント欄無いんですけど。

この記事で紹介する環境はExampleとしてGitHub上に置いておきます。参考になればうれしいです。

これより下は2025年7月17日の情報です。

Astro v5のAmplifyのSSRやり方

Astro v4以前とv5では若干SSRのやり方が異なります。(自分の記憶が正しければ)

自分もあまり仕様は理解していないんですが、何かしらの仕様が変わってv4以前と同じやり方ではv5でできません。しかもビルドは成功するのにSSRが500エラーになって、CloudWatchにログも出ないという鬼畜な状態にされます。

v4以前のやり方

v4のやり方は「Astro Amplify SSR」とかでググったら大体出てくるやり方でできます。

自分はこちらの記事を参考にしてできました。Nodeアダプターを使用して自力で行うものです。この記事書いた人ありがとうございます。マジ感謝。

他にはコミュニティのアダプターも出ており、それを使うのもありだと思います。

自分は使ったことないので動くかはわかりませんが、AWSの公式ドキュメントでも紹介されてますしたぶん動きます。maybe。

AWS 公式ドキュメントの文言

メンテナンスAWSされていません。AWSする(動詞)

リポジトリのAboutに書いてあるサイト「www.astroawsamplify.com」にアクセスしたらBanggoodというサイトに飛ばされましたが、サイトは死んだのかな...?

v5のやり方

何かしらの仕様が変わったことによりv4のやり方ができなくなりました。コミュニティのアダプターが一応READMEに「an Astro site - v4.x or higher」と書いてあるので動くかもしれないですが、先ほどの述べた通り試してないので分からず。

(試せよという話は一旦置いておいて)

ということでやり方なんですが、ほとんどv4で紹介したこちらの記事とやり方は同じで、少し修正がいる感じになります。

v4では、Nodeアダプターをstandaloneモードで起動していましたが、これがどうやら動かないようで、middlewareモードにして別途サーバーを建ててSSRを処理する必要があるようです。

つまり、astro.config.mjsはこのような感じになります。

v4

import { defineConfig } from "astro/config";
import node from "@astrojs/node";
import moveServerFile from "./src/integrations/move-server-file";

const localPort = <ご自由に>;

// https://astro.build/config
export default defineConfig({
	server: {
		port: env.NODE_ENV === "production" ? 3000 : localPort
	},
	output: "static",
	adapter: node({
		mode: "standalone"
	})
});

v5

import { defineConfig } from "astro/config";
import node from "@astrojs/node";
import moveServerFile from "./src/integrations/move-server-file";

const localPort = <ご自由に>;

// https://astro.build/config
export default defineConfig({
	server: {
		port: env.NODE_ENV === "production" ? 3000 : localPort
	},
	integrations: [moveServerFile()], // 追加
	output: "static",
	vite: { // 後で解説
		define: {
			global: "window",
			"process.env.API_KEY": JSON.stringify(process.env.API_KEY)
		}
	},
	adapter: node({
		mode: "middleware" // 変更
	})
});

Nodeアダプターのmodeをmiddlewareにして、moveServerFileというインテグレーションを自作しました。

後で解説するところは後で解説するので一旦無視してください。

ビルド時にポート番号を3000にしてる件については、Amplifyのビルド出力設定ドキュメントにSSRは3000でみたいなこと書いてあったので一応ビルド時だけ3000にしてますが、たぶんポートはなんでもいいです。念のため。

moveServerFileの自作インテグレーションの中身はこれです。

import type { AstroIntegration } from "astro";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

export default function moveServerFile(): AstroIntegration {
	return {
		name: "astro-move-server-file",
		hooks: {
			"astro:build:done": async ({ dir, logger }) => {
				const sourceFileRelative = "src/server.mjs";
				const projectRoot = process.cwd();
				const sourcePath = path.join(projectRoot, sourceFileRelative);
				const destDir = path.join(fileURLToPath(dir), "../server");
				const destPath = path.join(destDir, "server.mjs");

				try {
					await fs.access(sourcePath);
					await fs.mkdir(destDir, { recursive: true });
					await fs.copyFile(sourcePath, destPath);
					logger.info(`✅ ${sourceFileRelative} を dist/server/server.mjs にコピーしました。`);
				} catch (e) {
					logger.error("❌ 失敗しました。:");
					logger.error(String(e));
				}
			}
		}
	};
}

何をしているかというと、「Nodeアダプターのmiddlewareを実行するサーバー」をsrc/server.mjsに書いており、それをビルド後にdistにコピペするインテグレーションです。

そのserver.mjsをAmplifyのSSRとして実行してもらう感じです。

server.mjsはこれです。

import { createServer } from "node:http";
import { handler as astroHandler } from "./entry.mjs";

createServer((req, res) => {
	try {
		res.on("error", (err) => {
			console.error("❌ レスポンスストリームでエラーが発生しました。", err);
		});

		astroHandler(req, res);
	} catch (e) {
		console.error("❌ AstroのSSRでエラーが発生しました。", e);

		if (!res.headersSent) {
			res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
			res.end("サーバーでエラーが発生しました。");
		}
	}
}).listen("3000", () => {
	console.log("🚀 ポート3000でサーバーが起動しました。");
});

とりあえずいろいろエラーキャッチできるかなぁと思ってエラーログ出すようにしてますが、今のとこエラーが出たことがないのでエラーをキャッチできているか分かりません。

(エラーを出して確かめろという話は一旦置いておいて)

上の方でimportしてるimport { handler as astroHandler } from "./entry.mjs";はNodeアダプターをmiddlewareモードでビルドした際に出来上がるファイルです。このserver.mjsを先ほどの自作インテグレーションでビルド後にdist/server/server.mjsにコピペし、生成されているdist/server/entry.mjsをimportしている形になります。

なので、deploy-manifest.jsonはserver.mjsを実行するように指定する必要があります。

{
	"version": 1,
	"routes": [
		{
			"path": "/_astro/*",
			"target": {
				"kind": "Static"
			}
		},
		{
			"path": "/*",
			"target": {
				"kind": "Static"
			},
			"fallback": {
				"kind": "Compute",
				"src": "default"
			}
		}
	],
	"computeResources": [
		{
			"name": "default",
			"entrypoint": "server.mjs", // ここ!
			"runtime": "nodejs20.x"
		}
	],
	"framework": {
		"name": "astro",
		"version": "5.9.3"
	}
}

他のamplify.ymlとかはv4と一緒です。一応載せときます。(親切)(優しい)(自画自賛)

version: 1
frontend:
  phases:
    preBuild:
      commands:
        - npm ci -omit=dev --ignore-scripts
        - du -sm node_modules # 後で解説
    build:
      commands:
        - env >> .env
        - npm run build
        - mkdir -p .amplify-hosting/compute
        - mv dist/client .amplify-hosting/static
        - mv dist/server .amplify-hosting/compute/default
        - cp deploy-manifest.json .amplify-hosting/deploy-manifest.json
        - bash delete-module.sh # 後で解説
        - cp -r node_modules .amplify-hosting/compute/default/node_modules
        - mv .env .amplify-hosting/compute/default/.env
        - find .amplify-hosting/compute/default \( -path ".amplify-hosting/compute/default/node_modules" \) -prune -o -type f -printf "%s bytes\t%p\n" # 後で解説
        - du -sm .amplify-hosting/compute/default # 後で解説
        - du -sm .amplify-hosting/compute/default/node_modules # 後で解説
  artifacts:
    baseDirectory: .amplify-hosting
    files:
      - "**/*"

後で解説と書いてあるやつは、220MB制限に関係する箇所です。後で解説します。

以上!v5でSSRだ~!

SSRで環境変数読めない問題

問題が発生した!

まぁこれはv5関係ない問題ですが、SSRで何も設定しないとSSR上でPUBLIC_がついていない環境変数が読めません!

これはCloudflare Workersなどでも同様の問題が発生しており、未だに直ってない様子。直らないのかも?よくわかりません。

それのissueがこれ。(closeしている...?)

これの解決方法が先ほど紹介したastro.config.mjsで書いてあった後で解説のところです。

import { defineConfig } from "astro/config";
import node from "@astrojs/node";
import moveServerFile from "./src/integrations/move-server-file";

const localPort = <ご自由に>;

// https://astro.build/config
export default defineConfig({
	server: {
		port: env.NODE_ENV === "production" ? 3000 : localPort
	},
	integrations: [moveServerFile()], // 追加
	output: "static",
	vite: { // 今解説!
		define: {
			global: "window",
			"process.env.API_KEY": JSON.stringify(process.env.API_KEY)
		}
	},
	adapter: node({
		mode: "middleware" // 変更
	})
});

vite.defineにSSRで読んでほしい環境変数一個ずつ書く必要があるようです。めんどくさいですね。

vite: {
	define: {
		global: "window",
		"process.env.API_KEY": JSON.stringify(process.env.API_KEY),
		"process.env.SECRET_KEY": JSON.stringify(process.env.SECRET_KEY),
		"process.env.SUPER_SECRET_KEY": JSON.stringify(process.env.SUPER_SECRET_KEY),
		"process.env.ULTRA_SUPER_SECRET_KEY": JSON.stringify(process.env.ULTRA_SUPER_SECRET_KEY)
	}
}

理由は知りません。issue読んだらたぶん分かります。自分も前読みましたが忘れました。人はこうやって記憶が薄れていきます。

解説以上!(先ほども言いましたが、PUBLIC_は普通に読めます。)

220MB制限の対処法

来ました。Amplify SSR一番の敵です。

AmplifyのSSRはLamdaで動いているんですが、Lamdaみたく220MBまでという制限があります。(Lamdaは250MBくらいだっけ?)

Lamdaはレイヤーでなんとかできますが、AmplifyのSSRは自分の知る限りできません。

公式ドキュメントにも書いてあります。これがなかなかきつく、ちょ~っとライブラリ使ったり、画像最適化するとすぐ終わります。

そのほとんどを占めているのが、node_modulesです。amplify.ymlにも書いてありますが、SSR処理でライブラリを使用するためにcp -r node_modules .amplify-hosting/compute/default/node_modulesしています。こいつが諸悪の根源1です。

諸悪の根源1: node_modules

これの解決方法があまりにも力づくなんですが、ビルド後にnode_modulesから必要のないファイルを消します。

力づくすぎて気持ち悪いですが、公式ドキュメントにもそう書いてあります。

ということでそれを行っているのが、先ほど紹介したamplify.ymlのbash delete.shです。

version: 1
frontend:
  phases:
    preBuild:
      commands:
        - npm ci -omit=dev --ignore-scripts
        - du -sm node_modules # 今解説!
    build:
      commands:
        - env >> .env
        - npm run build
        - mkdir -p .amplify-hosting/compute
        - mv dist/client .amplify-hosting/static
        - mv dist/server .amplify-hosting/compute/default
        - cp deploy-manifest.json .amplify-hosting/deploy-manifest.json
        - bash delete-module.sh # 今解説!
        - cp -r node_modules .amplify-hosting/compute/default/node_modules
        - mv .env .amplify-hosting/compute/default/.env
        - find .amplify-hosting/compute/default \( -path ".amplify-hosting/compute/default/node_modules" \) -prune -o -type f -printf "%s bytes\t%p\n" # 今解説!
        - du -sm .amplify-hosting/compute/default # 今解説!
        - du -sm .amplify-hosting/compute/default/node_modules # 今解説!
  artifacts:
    baseDirectory: .amplify-hosting
    files:
      - "**/*"

このスクリプトでnode_modulesからいらないやつをごっそり消してます。途中挟まるduとかfindコマンドはnode_modulesのファイル容量確認コマンドです。

delete-module.shの紹介したいんですが、その前に一旦package.jsonを紹介します。

amplify.ymlの先頭にも書いてありますが、npm ci -omit=dev --ignore-scriptsでpackage.jsonに書いてあるdevDependenciesを跳ね除け、dependenciesだけをインストールしてくれます。node_modulesに直接手を下す前に一旦package.json側で整理します。

{
	"name": "koreha-ichirei",
	"type": "module",
	"version": "1.0.0",
	"engines": {
		"node": "20.15.0"
	},
	"volta": {
		"node": "20.15.0"
	},
	"scripts": {
		"dev": "astro dev",
		"build": "panda codegen && astro build",
		"preview": "astro preview",
		"format": "prettier --write ./**/*",
		"lint": "biome lint",
		"prepare": "panda codegen && husky"
	},
	"dependencies": {
		"@astrojs/node": "9.2.2",
		"@astrojs/react": "4.3.0",
		"@astrojs/sitemap": "3.4.1",
		"@aws-sdk/client-s3": "3.828.0",
		"@pandacss/dev": "0.51.1",
		"@splidejs/splide": "4.1.4",
		"@types/luxon": "3.6.2",
		"@types/pg": "8.15.2",
		"@types/react": "19.0.7",
		"@types/react-dom": "19.0.3",
		"@types/ws": "8.18.1",
		"astro": "5.9.3",
		"better-auth": "1.2.8",
		"glob": "11.0.3",
		"gsap": "3.13.0",
		"kysely": "0.28.2",
		"luxon": "3.6.1",
		"mime": "4.0.7",
		"pg": "8.16.0",
		"react": "19.0.0",
		"react-dom": "19.0.0",
		"rough-notation": "0.5.1",
		"sass-embedded": "1.83.4"
	},
	"devDependencies": {
		"@astrojs/check": "0.9.4",
		"@biomejs/biome": "1.9.4",
		"@prisma/client": "6.8.2",
		"dotenv": "16.5.0",
		"dotenv-cli": "8.0.0",
		"husky": "9.1.7",
		"prettier": "3.4.2",
		"prettier-plugin-astro": "0.14.1",
		"prettier-plugin-prisma": "5.0.0",
		"prettier-plugin-sh": "0.14.0",
		"prettier-plugin-toml": "2.0.5",
		"prisma": "6.8.2",
		"prisma-kysely": "1.8.0",
		"tsx": "4.19.4",
		"typescript": "5.7.3"
	}
}

これは一例です。とあるサイトで自分が使ってたものです。

環境はこうです。

  • フロントエンドフレームワーク / ライブラリ
    • Astro
    • Nodeアダプター(SSR)
    • React
    • GSAP(なめらかに動くやつ)
    • RoughNotation(手書きっぽい線引くやつ)
    • luxon(時扱うやつ)
    • splide(スライドするやつ)
  • CSS-in-JS
    • Panda CSS
  • DB
    • PostgreSQL(Neon)
  • ORM
    • Prisma(マイグレーションとかだけ)
    • Kysely
  • 認証
    • Better Auth
  • リンタ― / フォーマッター
    • Biome
    • Prettier
  • その他
    • AWS SDK(AWS触るやつ)
    • husky(コミット時にフォーマットするやつ)

いやこんなにあったら220MB無理~~~~~!

とりあえずリンタ―、フォーマッター、TypeScript(Astroに同梱)とPrismaはスキーマ要因なのでdevDependenciesに押し込みます。これでnpm ci -omit=dev --ignore-scriptsすると、dependenciesだけが入ります。

後はビルド後になんとかします。

ということでdelete-module.shがこれです。

rm -rf node_modules/esbuild
rm -rf node_modules/@esbuild
rm -rf node_modules/@swc
rm -rf node_modules/@pandacss
rm -rf node_modules/@splidejs
rm -rf node_modules/gsap
rm -rf node_modules/rough-notation
rm -rf node_modules/sass-embedded
rm -rf node_modules/sass-embedded-*
rm -rf node_modules/typescript
rm -rf node_modules/tsx
rm -rf node_modules/@types
rm -rf node_modules/@astrojs/react
rm -rf node_modules/@astrojs/sitemap
rm -rf node_modules/@aws-sdk
rm -rf node_modules/glob
rm -rf node_modules/mime
rm -rf node_modules/.astro/assets # 諸悪の根源2 後で解説
find node_modules -type d -name "test" -exec rm -rf {} +
find node_modules -type d -name "tests" -exec rm -rf {} +
find node_modules -type d -name "doc" -exec rm -rf {} +
find node_modules -type d -name "docs" -exec rm -rf {} +
find node_modules -type d -name "example" -exec rm -rf {} +
find node_modules -type d -name "examples" -exec rm -rf {} +
find node_modules -type d -name ".bin" -exec rm -rf {} +
find node_modules -type f -name "*.md" -delete
find node_modules -type f -name "*.map" -delete
find node_modules -type f -name "*.ts" -delete
find node_modules -type f -name "*.tsx" -delete
find node_modules -type f -iname "LICENSE*" -delete

削りました。

とりあえずgsapやrough-notationなどクライアント側で動くやつはビルド時にバンドルされるので消しました。Sass, Panda CSS, その他ビルド時に必要なAstroインテグレーションも削除します。TypeScriptはAstroに同梱されてインストールされるので消します。

という感じで地道に消していきます。

また、findコマンドで不要なexample, test, ts, mdなどは全部消します。これ結構重要です。

これ消しすぎたりすると動かなくなるので、注意が必要です。このdelete-module.shも間違ってるところあるかもしれないので、間違ってたら教えてください。まぁこの記事コメント欄無いんですけど。

これで解決できればいいんですが、まだあります。諸悪の根源2です。

諸悪の根源2: 最適化された画像たち

Astroは画像最適化機能があります。

大変便利で良く使います。ビルドするとdist/client/_astro/配下に収まります。こちらはSSGのフォルダなので220MB制限の適用外です。(たぶん)

なので、良いんですが、

なぜかビルドするとdist配下に収まりつつ、node_modules/.astro/assets/配下にも全部入ります。WTF。

これではとんでもねぇサイズの画像たち×最適化された数だけnode_modulesの中に納まってしまいます。

これなんでなんですか?(無知)

キャッシュ?

ということでdelete-module.shにある通りrm -rf node_modules/.astro/assetsが必要ということですね。

npm ci後とビルド後にduコマンドで容量を見ていたのは、ビルド後にnode_modules内の容量が変わるからです。

最後に

とにかく経験を元に雑に書いてみました。

Amplifyえぐいわって感じなんですが、ある程度安く、AstroでSSRって考えると自分の浅はかな知識ではAmplifyになる気がしています。

Cloudflare Pages / Workersだと無料の場合実行時間が10msという制限があり、DBなどを使っていると接続がタイムアウトするときがあります。Netlify, Vercelにするとこれらの問題は解決しますが、Netlifyは無料だとFunctionsが海外サーバーになりとても遅いです。ギリ耐えられないくらい遅いです。Vercelは商用利用が禁止なのでそこらへんがちょっと...。

ってなると、Amplifyかなぁっていう。あとCloud Run?でも無料枠ないしな...。

金払えって話ですね。

間違ってるとこがあったら指摘ください。自分はTwitterで生息しています。

冒頭でも紹介しましたが、今回使用した環境はExampleとしてGitHub上に置いてあります。