概要
Web, APP, DBの3つのコンテナを作成し、それぞれのコンテナにWeb三層構造のそれぞれの責務に分割する。
Nginx ⇄ APP ⇄ DB
背景
この構成に変更する目的は、以下の二つ
- 元の構成では、
app.jar
に静的リソースを含める必要があった - 負荷分散を実現したい
それぞれについて具体的に見ていく。
1. 元の構成では、app.jar
に静的リソースを含める必要があった
元々は、SpringBootのThymeleafでフロントエンド部分を作成していた。
しかし、SpringBootのフロントエンド部分をThymeleafで実現した場合、Javaのビルド後の成果物(app.jar
)に静的ファイルが含まれてしまうことになる。
この場合、フロントエンドのソースを変更した場合にも、app.jar
を作り直す必要があり、ホットリロードが非効率になってしまう。
フロントエンドのソースの変更後の具体的な手順
- Vue, TypeScriptのファイルをJavaScriptにトランスパイルし、適切なディレクトリに配置する
- アプリケーションのビルドを行い、
app.jar
を作成する
フロントエンドのソースのみを変更した場合には、フロントエンドのトランスパイルのみで済ませたい。
2. 負荷分散を実現したい
元の構成の場合には、フロントエンドのソースもapp.jar
に含まれている。
そのため、フロントエンドとバックエンドとでサーバーを分割することができず、単純な画面描画などの場合にも同じサーバーへのアクセスが必要になってしまう。
Web三層構造を実現する
point
- Webブラウザからのリクエストは、Nginxコンテナが受け付ける
- 必要であれば、APPやDBコンテナへのリクエストが行われる
- フロントエンドのソースは、Node.jsコンテナ(仮名)でトランスパイルし、Nginxコンテナに配置する
対応手順
- Node.jsのコンテナを作成する
- Nginxコンテナを作成し、ソースを配置する
- APPコンテナとの連携を図る
- DBコンテナとの連携を図る
- CORS問題を対応する
APP ⇄ DB
については、以下で対応している。
Node.jsのコンテナを作成する
Node.jsのコンテナの責務は、フロントエンドのソースをトランスパイルして、Nginxコンテナに配置すること。
フロントエンドのビルドは、Viteを使用している。
本番環境と開発環境とで、若干の挙動の違いがあるので、整理しておく。
本番環境
コンテナ内でフロントエンドのソースをトランスパイルし、Nginxコンテナとリソースを共有する。
トランスパイル後、コンテナは停止する。
開発環境
コンテナ内でフロントエンドのソースをトランスパイルし、Nginxコンテナとリソースを共有する。
ソースの変更に応じて、ホットデプロイを行うため、コンテナは起動し続ける。
Dockerfile
Node.jsのDockerfileを示す。
FROM node:21.7.1-bullseye-slim
WORKDIR /app
# キャッシュ機能を有効活用するためにパッケージ関連のものを先にCOPYしている。
COPY yarn.lock package.json .yarnrc.yml .
COPY .yarn/ ./.yarn/
RUN yarn install
COPY . .
CMD yarn run ${NODE_ENV}
ベースイメージ
Node.jsの公式イメージを使用する。
- 本番環境のサーバーとして利用するには向いていない
- ディストリビューションは、bullseye(Debian 11)
キャッシュの有効化
キャッシュの機能を有効に使うために、依存ファイルとソースファイルとでCOPY
の位置を変えている。
依存ファイルの変更は稀なので、通常時(ソースのみを修正した場合)には、6行目より前はキャッシュが利用される。
CMD
から実行されるスクリプト
package.json
は、以下のようになっている。
- 開発環境の場合は
Watch
モードでビルドを実行している - 本番環境の場合は、ソースのビルドだけを実施し、スクリプトの実行が完了し次第、コンテナが停止する
{
"scripts": {
"dev": "vite build --watch --mode dev",
"production": "vue-tsc --noEmit && vite build --mode production",
},
}
docker-compose.yml
Node.jsのコンテナのdocker-compose.yml
を示す。
web:
container_name: web_container
build: ./web
volumes:
- type: bind
source: ./web/src
target: /app/src
environment:
NODE_ENV: ${ENVIRONMENT}
バインドマウント
コンテナ上に、フロントエンドのソースをバインドマウントしている。
開発サーバーでは、ここで最新のフロントエンドのソースを検知し、フロントエンドのリビルドを実施する。
Nginxコンテナを作成し、リソースを配置する
Nginxコンテナの責務は、Node.jsのコンテナからビルド済みのリソースを受け取り、配信すること。
Dockerfile
NginxのDockerfileを示す。(Nginxについてはあまり深掘りしていないので、設定内容も軽め...)
FROM nginx:latest
COPY ./default.conf /etc/nginx/conf.d/default.conf
docker-compose.yml
Nginxのdocker-compose.yml
を示す。
web:
container_name: web_container
build: ./web
volumes:
- type: bind
source: ./web/src
target: /app/src
- type: volume # 追加
source: web-source
target: /app/dist
environment:
NODE_ENV: ${ENVIRONMENT}
nginx:
container_name: nginx_container
build: ./nginx
volumes:
- type: volume
source: web-source
target: /app
ports:
- 3000:80
depends_on: # Nginxコンテナが先に起動しても問題ないため、depends_onとしている。
- web
volumes:
web-source:
ボリュームマウント
NginxとNode.jsのコンテナ間でのビルド済みのフロントエンドのソースの共有は、ボリュームマウントで行なっている。
そのため、Node.jsのコンテナでビルドしたリソースは、Nginxコンテナから参照できる。
コンテナの起動順
一応、Node.jsコンテナ、Nginxコンテナの順で起動するように設定している。
(Nginxコンテナでは、リソースが配置されるまでは404となるだけなので、あまり重要ではないが...)
APPコンテナとの連携を図る
Axiosを使用して、フロントエンドからバックエンドへのリクエストを可能にする。
APPコンテナの設定については、こちらを参照。(TODO)
以下のように、RestControllerのパスにリクエストを投げるだけ。
onMounted(() => {
axios
.get('/sample') // エンドポイントにリクエストを投げる。
.then((response: AxiosResponse) => {
console.log(response);
})
.catch((err: AxiosError) => {
console.log(err);
});
});
CORS問題を対応する
CORSとは、ブラウザが別のオリジンに対してJavaScriptによるリクエストを送信した場合に、そのリクエストをブロックするかどうかを設定するためのもの。(詳細)
特に設定をしない場合、Axiosによるリクエスト時に以下のエラーとなった。
Access to XMLHttpRequest at 'http://localhost:8080/sample' from origin 'http://localhost' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
API側(SpringBoot)とNginx側のそれぞれで対応する必要がある。
- Webブラウザは、プリフライトリクエストを送信する(
method: OPTIONS
) - レスポンスヘッダーからCORSの設定を確認し、元のリクエストを送信するかどうかを決める
SpringBootでCORS設定を行う
CORSは、API側を保護するためのものであり、アクセス許可についてはAPI側で設定を行う。
リクエスト元では、この許可しているCORS設定に適合するようなリクエストを送る必要がある。
こちらを参考にした。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* CORSの設定を許可する。
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**").allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE").allowedHeaders("*");
}
}
NginxでCORSを対応する
Nginxの設定ファイル(default.conf
)に以下を追加した。
/api
へのリクエストの場合には、APPサーバーへのリクエストを中継している。
location /api/ {
proxy_pass http://localhost:8080;
}