Dockerコンテナ起動時に、フロント側コンテンツを制御する一事例


概要

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ブロックに分かれています。

  1. Reactアプリケーションのビルド
  2. Javaアプリケーションのビルド
  3. 実行するイメージの作成

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_testtestという値を入れて、コンテナ起動してみます。

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