概要
1つのDockerイメージとして提供する、Java+React構成のアプリケーションについて、コンテナ起動時にビルド済みのReactコードを制御する方法を、具体例をもとに考えていきます。
はじめに
Twelve-Factor Appの「III. 設定」で述べられているように、アプリケーションの設定は環境変数に格納し、コードからは分離することが望ましいとされています。
つまり、Dockerコンテナとして動作するアプリケーションであれば、develop用、staging用、production用と各環境ごとで別のDockerイメージを用意するべきではなく、イメージ自体は1つにして、各環境ごとの差分はコンテナ実行時に与える環境変数で吸収するべきです。
しかし、コンテナ起動するアプリケーションに、環境変数を反映させることが容易でない場合も多々あります。
例えば、ビルド実施済みのフロント側コンテンツに対して、環境変数での制御を行う方法は、グーグルで検索を行ってもほとんどヒットしません。
そこで、本記事では、どうすればビルド済みのフロント側コンテンツに対して環境変数を反映させることができるかについて、Java+React構成のアプリケーションを一事例として説明していきます。
環境変数を反映させる戦略
コンテナ起動時にフロント側コンテンツに環境変数を反映させるために、Javaの起動時に読まれる環境変数を、HTMLのmetaタグ経由で、フロント資材に渡す、という戦略をとりました。
また、HTMLがレンダリングされるタイミングで、metaタグの内容が環境変数の値に書き換わるようにするために、Thymeleafを利用しました。
構成
ここから説明で使用するコードは、以下のようなthymeleaf/my-app
配下でフロント側のコードを管理し、thymeleaf/thymeleaf
配下でバック側のコードを管理する構成です。
└── thymeleaf
├── Dockerfile
├── my-app
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── ....
│ ├── src
│ │ └── ....
│ └── yarn.lock
└── thymeleaf
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── ....
│ └── resources
│ └── ....
└── test
└── java
└── ....
コードの全量は以下に配置しています。
https://github.com/nannany/thymeleaf
ここからどのようにDockerイメージを作成していくか示すために、まずDockerfileから説明していきます。
Dockerfile
Dockerfileは全体は以下のようです。
FROM node:13 AS front-build
WORKDIR /work1
ADD my-app .
RUN npx yarn && npx yarn build
FROM maven:3.6 AS back-build
WORKDIR /work2
ADD thymeleaf .
RUN mkdir -p /work2/src/main/resources/static
COPY --from=front-build /work1/build/ /work2/src/main/resources/static/
RUN mkdir -p /work2/src/main/resources/templates && \
sed -e "s!<meta name=\"from-environment\" content=\"\"/>!<meta name=\"from-environment\" th:content=\${@environment.getProperty('thymeleaf.test')}>!" \
/work2/src/main/resources/static/index.html > /work2/src/main/resources/templates/index.html && \
rm /work2/src/main/resources/static/index.html && \
mvn package
FROM adoptopenjdk/openjdk11:jdk-11.0.6_10-alpine
COPY --from=back-build /work2/target/thymeleaf-0.0.1-SNAPSHOT.jar .
ENTRYPOINT ["java", "-jar", "thymeleaf-0.0.1-SNAPSHOT.jar"]
このDockerfileは、以下の3ブロックに分かれています。
- Reactアプリケーションのビルド
- Javaアプリケーションのビルド
- 実行するイメージの作成
Reactアプリケーションのビルド
最初のブロックでは、Reactアプリケーションのビルドを行っています。
FROM node:13 AS front-build
WORKDIR /work1
ADD my-app .
RUN npx yarn && npx yarn build
特記することはありません。
Javaアプリケーションのビルド
次のブロックでは、Javaアプリケーションのビルドを行っています。
FROM maven:3.6 AS back-build
WORKDIR /work2
ADD thymeleaf .
RUN mkdir -p /work2/src/main/resources/static
COPY --from=front-build /work1/build/ /work2/src/main/resources/static/
RUN mkdir -p /work2/src/main/resources/templates && \
sed -e "s!<meta name=\"from-environment\" content=\"\"/>!<meta name=\"from-environment\" th:content=\${@environment.getProperty('thymeleaf.test')}>!" \
/work2/src/main/resources/static/index.html > /work2/src/main/resources/templates/index.html && \
rm /work2/src/main/resources/static/index.html && \
mvn package
Reactアプリケーションのビルド成果物を/work2/src/main/resources/static/
に配置して、/work2/src/main/resources/static/index.html
の
<meta name="from-environment" content=""/>
という記述を、
<meta name="from-environment" th:content=${@environment.getProperty('thymeleaf.test')}>
に書き換えて、Thymeleafにレンダリングさせるため/work2/src/main/resources/templates/index.html
に配置しています。
なぜ元のindex.htmlに置換後のように書いていないかというと、yarn build
時に、Thymeleaf記法で書いている部分のパースに失敗するからです。(おそらくwebpackが吐いているエラー)
そのあとでmvn package
をたたいてjarファイルを生成しています。
実行イメージの作成
最後のブロックで、コンテナ起動時にjava -jar 作ったjarファイル
のプロセスを立ち上げるイメージを作成しています。
FROM adoptopenjdk/openjdk11:jdk-11.0.6_10-alpine
COPY --from=back-build /work2/target/thymeleaf-0.0.1-SNAPSHOT.jar .
ENTRYPOINT ["java", "-jar", "thymeleaf-0.0.1-SNAPSHOT.jar"]
特記することはありません。
React
index.html
のhead内に、Dockerfile内で置換していたmetaタグを記述します。
<head>
....
<meta name="from-environment" content=""/>
....
</head>
Dockerイメージに固める前の開発段階では、.env
(もしくは.env.local
など)を使って環境変数を指定します。
.env
の記述は以下のようです。
REACT_APP_LOCAL_SUBSTITUTE=local
metaタグや.env
から値を取得する部分は、以下のようです。
getMetaData() {
const fromEnvironment = document.getElementsByName(
"from-environment"
)[0].content;
return fromEnvironment === '' ? process.env.REACT_APP_LOCAL_SUBSTITUTE : fromEnvironment;
}
from-environment
というnameのmetaタグのcontentが空文字であれば、.env
内の値を使い、それ以外の場合はmetaタグのcontentを使います。
このようにした理由としては、npm run start
で、Reactの動作のみ確かめたい場合にも、コードを書き換えたりせずに対応できるようにしたかったためです。
Java
バックエンド側に関して特記することはないです。
記述も少ないです。
https://github.com/nannany/thymeleaf/tree/master/thymeleaf
動かしてみる
上記のDockerfileをもとに作成したイメージについて、環境変数を与えてコンテナ起動してみます。(test:dev
という名前でイメージを作りました)
thymeleaf_test
という環境変数に応じて、フロント側コンテンツが変更されるようになっています。
まず、何も環境変数を与えないで動かしてみると、以下のように表示されます。
docker run --rm -p 80:8080 test:dev
次に、thymeleaf_test
にtest
という値を入れて、コンテナ起動してみます。
docker run --rm -p 80:8080 test:dev
反映が確認できました。
おわりに
そもそもこのような構成で、1つのDockerイメージにしてデプロイしようと考えるのが間違っているという説はあります。
フロントとバックは別のイメージにして、フロント側にNext.jsなどを導入すれば容易に環境変数で制御できるといった記事を見たので、きっとそういう構成にするのがいいのでしょう。
#参考
https://itnext.io/frontend-dockerized-build-artifacts-with-nextjs-9463f3da3362
https://qiita.com/shibukawa/items/6a3b4d4b0cbd13041e53
https://12factor.net/ja/config