AWS Amplify HostingでHonoを使ったフルスタックウェブアプリケーションを構築する
AWS Amplify HostingはNext.jsやNuxt.jsのフレームワークのSSRを独自にサポートしていますが。この機能(Compute)の動作仕様は公開されています。
これを利用して、Node.jsとHonoで作ったAPIサーバーをデプロイしてフルスタックなウェブアプリケーションを作ることができます。
必要なファイルだけを最小限に配置した例が以下になります。
├── amplify.yml
└── app
├── compute
│ └── default
│ ├── index.js
│ ├── node_modules
│ ├── package-lock.json
│ └── package.json
├── deploy-manifest.json
└── static
├── index.html
└── styles.css
※実際の開発ではビルドツールを使ってバンドルした結果が上記app/
ディレクトリになるのが理想です。説明のためにこのような変則的な構成にしています。
amplify.yml
amplify.ymlはAmplifyのビルド設定ファイルです。git pushするとこの設定どうりにAWS環境でビルドが行われます。
version: 1
frontend:
phases:
build:
commands:
- 'cd app/compute/default/ && npm install'
artifacts:
baseDirectory: app
files:
- '**/*'
cache:
paths:
- '.npm/**/*'
アップロード対象のディレクトリにnode_modulesを配置する必要があるため 'cd app/compute/default/ && npm install'
というコマンドを実行しています。
deploy-manifest.json
これが最も重要なファイルです。deploy-manifest.jsonのにはAWSビルド環境でリソースをどのように配置するかの設定が書かれています。
{
"version": 1,
"framework": { "name": "hono", "version": "4.6.3" },
"routes": [
{
"path": "/api/*",
"target": {
"kind": "Compute",
"src": "default"
}
},
{
"path": "/*",
"target": {
"kind": "Static"
}
}
],
"computeResources": [
{
"name": "default",
"runtime": "nodejs20.x",
"entrypoint": "index.js"
}
]
}
上記では、/api/*
のリクエストは app/compute/default/index.js
で実行した APIにルーティングされます。
それ以外のリクエストは app/static
以下の静的ファイルを返します。これはCloudFrontから静的なファイルとして配信されます。今回はここにindex.htmlとstyles.cssを配置しました。
(もちろん全てのページ単位のリクエストをHonoで動かしているNode.jsのサーバーに向け、そこでHTMLを返してMPAとして画面を作ることもできます)
app/compute/default/
Computeリソースの起点となるディレクトリです。このディレクトリ以下がAWS Lambda(もしくは@Edge)にデプロイされます。このLambda関数はAWS側で管理されているのでユーザーからは直接編集することはできません。
const { Hono } = require('hono');
const { serve } = require('@hono/node-server');
const api = new Hono().basePath('/api');
// TODOアプリのためのインメモリストレージ
const todos = [];
// TODOの取得
api.get('/todos', (c) => c.json(todos));
// TODOの追加
api.post('/todos', async (c) => {
const { title } = await c.req.json();
const newTodo = { id: todos.length + 1, title, completed: false };
todos.push(newTodo);
return c.json(newTodo, 201);
});
// TODOの更新
api.put('/todos/:id', async (c) => {
const id = parseInt(c.req.param('id'));
const { title, completed } = await c.req.json();
const todo = todos.find(t => t.id === id);
if (!todo) {
return c.json({ error: 'Todo not found' }, 404);
}
todo.title = title || todo.title;
todo.completed = completed !== undefined ? completed : todo.completed;
return c.json(todo);
});
// TODOの削除
api.delete('/todos/:id', (c) => {
const id = parseInt(c.req.param('id'));
const index = todos.findIndex(t => t.id === id);
if (index === -1) {
return c.json({ error: 'Todo not found' }, 404);
}
todos.splice(index, 1);
return c.json({ message: 'Todo deleted successfully' });
});
const port = 3000
console.log(`Server is running on port ${port}`)
serve({
fetch: api.fetch,
port
})
なんの変哲もないHonoのAPIサーバーです。サーバー側で状態を保つことを表すためにインメモリの配列にデータを保存しました。
app/static/
静的ファイルを配置するディレクトリです。上記のAPIを使うためにindex.htmlとstyles.cssで簡単なUIを作りました。
(ソースコードは省略)
デプロイ
Amplifyのコンソールで設定したリポジトリにプッシュすると、Amplifyが自動的にデプロイを開始します。
これらを通して実際にデプロイされたウェブアプリケーションは以下のURLで見ることができます。
https://main.d34bg7pb0ejo8j.amplifyapp.com/
UIは特に見るべき部分はないので、開発者コンソールでAPI通信をしている部分と入力したデータがサーバー側に保持されていることを確認してください。
発展的な話題
static/index.html
がホストできるということはここからSPAが構築できるということです。Vite+ReactでSPAを構築して、複数の画面に遷移するするアプリケーションにすることができます。
それに、APIを動かしているサーバー部分でAmplifyの関連サービスのSDKを組み込み、依存をサーバーサイドのみに分離する設計も作れます。
また、本来baseDirectory
はgitリポジトリには含めず、ビルド時に生成してバンドルした結果を配置するべきです。そうするとTypeScriptを使うことができます。
注意点としてはcompute/default サブディレクトリは、圧縮して 220 MB を超えることはできません。
参考文献
似た話題
Discussion