Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ lede: "Next.jsやNuxt.jsなどのサーバーサイドレンダリング必須

Next.jsやNuxt.jsなどのサーバーサイドレンダリング必須なフレームワークであれば、Node.jsと一緒にコンテナ化するか、Vercelなどにデプロイする方法があります。こちらはJavaScriptのウェブアプリケーションなので実行環境を用意する必要があります。

一方、SPAとして作成したVue.jsなど、現代のフレームワークで作成したフロントエンドは、ビルドすると静的HTMLとJavaScriptコードになります。ただし、ファイルが存在指定なパスへのリクエストがあった場合にindex.htmlの内容をフォールバックとして返す必要があります
一方、SPAとして作成したVue.jsなど、現代のフレームワークで作成したフロントエンドは、ビルドすると静的HTMLとJavaScriptコードになります。ただし、物理的なファイルが存在しないパスへのリクエストがあった場合にindex.htmlの内容をフォールバックとして返す必要があるため、動作させるにはそのあたりが設定可能なウェブサーバーを使う必要があります。index.htmlをロードするとブラウザ上でJavaScriptのコードが動作しますが、そのコードが内部で持っているURLのパス情報をみて適切なページを表示したり、それでも存在なければJavaScriptがエラー画面を出力します

[Webフロントエンド設計ガイドラインのSPAのホスティング](https://future-architect.github.io/arch-guidelines/documents/forWebFrontend/web_frontend_guidelines.html#spaのホスティング)では、いくつか紹介しています
フューチャー作のガイドラインの[Webフロントエンド設計ガイドラインのSPAのホスティング](https://future-architect.github.io/arch-guidelines/documents/forWebFrontend/web_frontend_guidelines.html#spaのホスティング)では、いくつかホスティング方法を紹介しています

1. CloudFront+S3
2. LB+S3
Expand All @@ -31,17 +31,17 @@ Next.jsやNuxt.jsなどのサーバーサイドレンダリング必須なフレ

* classmethodブログ: [Application Load Balancer のリスナールールでトランスフォームを構成し、ターゲットにルーティングする前にホストヘッダーや URL パスを書き換えれるようになりました](https://dev.classmethod.jp/articles/application-load-balancer-url-header-rewrite/)

これ以外には、ウェブアプリケーション側に配信機能を持たせてしま鵜というのも過去に技術ブログで紹介しました。比較的高速なGoとかRustならありでしょう
これ以外には、ウェブアプリケーション側に配信機能を持たせてしまうというのも過去に技術ブログで紹介しました。比較的高速なGoとかRustならこれもありでしょう

* [Go 1.16のembedとchiとSingle Page Application](https://future-architect.github.io/articles/20210408/)

今回はDockerイメージを作る、3番目の方法を試そうと思います
今回はガイドラインではSPA用によいとしている1. 3のうち、Dockerイメージを作った3番目の方法を全力で試そうと思います

# なぜDockerにするか

S3とかオブジェクトストレージにおいて配信というのがお手軽ですが、コンテナにまとめておくことでデプロイ時にまとめてフロントエンド資材を入れ替えたりがしやすいのがメリットと考えています。また、CloudFrontはインターネット公開するサービスには良いのですが、社内システムでは使えません。また、ビルド済みフロントエンドを軽量なサーバーで配信すればリソース消費は少なくて済みます。開発時もフロントエンドを触らない人にとってはありがたいのではないでしょうか
S3とかオブジェクトストレージにおいて配信というのがお手軽ですが、コンテナにまとめておくことでデプロイ時にまとめてフロントエンド資材を入れ替えたり、戻したりがしやすいのがメリットと考えています。また、CloudFrontはインターネット公開するサービスには良いのですが、社内システムでは使えません。また、ビルド済みフロントエンドを軽量なサーバーで配信すればリソース消費は少なくて済みます。開発時もフロントエンドを触らない人がローカルで動作検証するにはありがたいでしょう。

せっかく作るのであればセキュリティを意識したコンテナを目指します。近年、ランサムウェアが流行っています。静的なHTML/JSでアプリを作りフロントエンドを配信するだけのコンテナにすることで攻撃面をかなり狭くできます。ですが不正なプログラムを配る踏み台にはされる可能性があるため、次の項目にもチャレンジしてみます
せっかく作るのであればセキュリティを意識したコンテナを目指します。近年、ランサムウェアが流行っています。静的なHTML/JSでアプリを作りフロントエンドを配信するだけのコンテナにしてバックエンドをプライベートネットワークの後ろ側に隠すことで攻撃面をかなり狭くできます。ですがHTMLなどが買い替えられると不正なプログラムを配る踏み台にされる可能性があるため、そうならないために次の項目にもチャレンジしてみます

* シェルがないDistroless
* フロントエンドのartifactは読み込み専用で実行ユーザーでは書き換えられない
Expand All @@ -56,7 +56,6 @@ Need to install the following packages:
[email protected]
Ok to proceed? (y) y


> npx
> "create-vite"

Expand All @@ -70,7 +69,7 @@ Ok to proceed? (y) y
:
```

シングルページアプリケーションで正しく動作することをテストするために、vue-routerを入れてページをいくつか足します。
シングルページアプリケーションとして正しく動作することをテストするために、vue-routerを入れてページをいくつか足します。

```ts router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
Expand Down Expand Up @@ -178,14 +177,13 @@ h1 { margin-bottom: 1rem }

## nginxの設定

Vueアプリができたところで次はサーバーです。Rust製の[static-web-server](https://crates.io/crates/static-web-server)とか安全そうだし良さそうだなとも思ったのですが、[APIサーバーへのリクエストをプロキシするような設定がなく、今後も入らなそう](https://github.com/static-web-server/static-web-server/issues/489)ということもあり見送りました。本番デプロイだけならALBがやってくれるはずなのでstatic-web-serverでも良いかと思います。まあありきたりですがnginxにします
Vueアプリができたところで次はサーバーです。Rust製の[static-web-server](https://crates.io/crates/static-web-server)とか安全そうだし良さそうだなとも思ったのですが、[APIサーバーへのリクエストをプロキシするような設定がなく、今後も入らなそう](https://github.com/static-web-server/static-web-server/issues/489)ということもあり見送りました。このプロキシ機能があればウェブフロントエンドとバックエンドが同じドメイン(ポート番号も含めて)動作するので、CORSを機にする必要がなくなります。もちろん、作ったイメージを本番デプロイするだけならALBがやってくれるはずなのでstatic-web-serverでも良いかと思います。ここはありきたりですがnginxにしておきます。なお、今回はイメージサイズは60MBほどになりました。static-web-serverはシングルバイナリで4MBほどらしいので小ささを極めたい場合はstatic-web-serverで試すと良いでしょう

設定ファイルとしては、SPAで必要なフォールバックを入れたのと、ログは`/var/log`とかではなく、コンソールに出力するようにしています。

設定ファイル上に`user www-data;`と書けばユーザーが設定できます。ただし設定しなくても`nobody`ユーザーで動作します。最初は非ルートでやるぞ!と設定していたのですが、`nobody`で十分です
設定ファイル上に`user www-data;`と書けばユーザーが設定できます。ただし設定しなくてもワーカーは`nobody`ユーザーで動作します。最初はセキュリティ強化のためにサーバーは非ルートユーザーで動かすぞ!と設定していたのですが、ワーカーさえルートでなければ実用上は問題ないため、無視底の`nobody`で十分だと判断しました。なお、別ユーザーで動かすと、特権ポートの1024以下は使えないため、80番ポートでサーバーを動かすことはできなくなります

```sh nginx.conf
# nginx.conf

worker_processes auto;

Expand Down Expand Up @@ -234,20 +232,18 @@ http {

## Dockerfile

Dockerfileは以下の通りです。最初Geminiに雛形をざっと作ってもらいましたが、いろいろ細かいところを後から修正しました。その際に心がけたポイントは以下の通りです
Dockerfileは以下の通りです。最初Geminiに雛形をざっと作ってもらいましたが、いろいろ細かいところを後から修正しました。その際に心がけたポイントは以下の通りです

1. ビルドステージ
* bindマウント、cacheマウントを駆使してキャッシュフレンドリーな高速ビルド
* bindマウント、cacheマウントを駆使してキャッシュフレンドリーな高速ビルド(生成AIはいつもやってくれない)
2. nginxの設定
* こちらもbindマウント、cacheマウントで効率化
* 最終イメージのDistrolessはシェルがなくてmkdirとかもできないので、こちらのステージですべての必要なフォルダを作ったり、ユーザーやグループの設定を引っこ抜いたり、ディレクトリの権限設定をお子なり必要ライブラリをコピーしたりも含めて全て行なっています
* 最終イメージのDistrolessはシェルがなくてmkdirとかもできないので、こちらのステージですべての必要なフォルダを作ったり、ユーザーやグループの設定を引っこ抜いたり、ディレクトリの権限設定を行ったり、nginxの動作に必要ライブラリをコピーしたりも含めて全て行なっています
3. 実行イメージ
* Debianの新しいバージョンのtrixie(13)が使いたい→まだベータ扱いなのでいったん保留
* ビルド済みのHTML/JSを持ってきたり、nginxの設定を持ってきたり
* 実行ユーザー(nobodyから書き換えられないユーザーでHTML/JSを配置

最初は実行イメージは別ユーザー、と思ったのですが、nginx自体、ワーカーを作ってそちらが実質的な処理を行うと言う個性になっており、それはnobodyで動きます。そのため、仮にプロセスが乗っ取られても被害はないかな、ということでやっています。

```Dockerfile Dockerfile
# syntax=docker/dockerfile:1

Expand Down Expand Up @@ -321,6 +317,8 @@ EXPOSE 80
ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]
```

実行するには次のようにします。

```sh
# ビルド
$ docker build -t vue-spa .
Expand All @@ -331,10 +329,13 @@ $ docker run --rm -it -p 8080:80 vue-spa

## デバッグ実行

実行イメージを`gcr.io/distroless/base-debian12`から`gcr.io/distroless/base-debian12:debug`にして、
設定を変えてみたい場合のデバッグ方法も紹介しておきます。Distrolessではセキュリティのためにシェルがイメージに含まれていませんが、デバッグ用のイメージが提供されています。

* ランタイムをイメージを`gcr.io/distroless/base-debian12`から`gcr.io/distroless/base-debian12:debug`に変更
* `docker run --rm -it -p 8080:80 --entrypoint=sh vue-spa` で、shをnginxの代わりに実行

# まとめ

Dockerfileは新旧の書き方がウェブには混在しているため、機会をみてはbind/cacheを使ったDockerfileをブログに書くようにしています。今回はSPAの静的HTMLのコンテナを考察して作ってみました。ビルドの効率と実行効率、セキュリティ、どれも妥協しないDockerfileを作りました。
Dockerfileは新旧の書き方がウェブには混在しているため、機会をみてはbind/cacheを使ったモダンなDockerfileの記法をブログに書くように日頃からしていました。今回はVue連載ということで、Vue製のSPAの静的HTMLのコンテナを作ってみました。単に作るだけでは世の中の有象無象の記事と変わらないので、最新の記法を使ったビルドの効率、実行効率、セキュリティ、どれも妥協しないDockerfileを作りました。Vue以外の方にも参考にしてもらえる記事になったと思います

明日は松本朝香さんです。
Loading