Reactの開発環境(WSL2+docker)構築から本番環境(DockerHub+kubernetes)へのデプロイ手順まとめ
最近は、開発環境をDockerで構築することが多くなってきたので、Reactアプリの開発環境構築手順をまとめました。
また、本番デプロイ用にDockerHubへのPushとkubernetesへのデプロイ用YAMLの作成手順についてもまとめました。
- 1. 環境
- 2. フォルダ構成
- 3. [開発環境] Dockerfileとdocker-compose.ymlを作成する
- 4. [開発環境] コンテナを起動する
- 5. [開発環境] 起動したdockerコンテナにReact環境を構築する
- 6. [開発環境] Dockerfileを修正してパッケージがインストールされるようにする
- 7. [開発環境] Dockerコンテナの停止と開始
- 8. [本番環境] Dockerfileを作成する(マルチステージ・ビルド)
- 9. [本番環境] docker buildとdocker push
- 10. [本番環境] kubernetesのデプロイ用YAML作成
- 11. [本番環境] kubectl applyコマンドでデプロイする
- 12. [本番環境] kubectl deleteコマンドでリソースを削除する
環境
環境は以下を前提としていますが、Dockerが利用できる環境であればMacでもLinuxでも同じです。
- Windows 10 Home
- WSL2(Ubuntu 20.04)
- Docker
- docker-compose
Windows10 HomeにWSL2とDocker環境を構築する手順については以下の記事にまとめています。
フォルダ構成
React開発環境の大まかなフォルダ構成です。
Reactアプリ用のディレクトリ「app」はcreate-react-appコマンドで作成するため、手動で作成する必要はありません
react-app ・・・ プロジェクトフォルダ
├── app ・・・ create-react-appで作成したフォルダ
│ ├── node_modules
│ ├── public
│ └── src
│
├── docker ・・・ docker関連
│ ├── dev
│ │ ├── Dockerfile ・・・ 開発環境用Dockerファイル
│ │ └── docker-compose.yml ・・・ 開発環境用docker-composeファイル
│ │
│ └── prod
│ ├── Dockerfile ・・・ 本番環境用Dockerファイル
│ ├── dockerhub-push.sh ・・・ ビルドとDockerHubへのPushシェル
│ └── nginx
│ └── default.conf ・・・ 実行用コンテナ(nginx)のコンフィグ
│
└── kube
└── react-app.yml ・・・ kubernetesのServiceとDeployment
[開発環境] Dockerfileとdocker-compose.ymlを作成する
Dockerfile作成
/docker/dev ディレクトリに開発環境用のDockerfileを作成します。
FROM node:12.18-buster
WORKDIR /workspace/app
# -------------------------------------------------
# [1] 初期構築
# -------------------------------------------------
RUN apt-get update
RUN apt-get -y install locales && \
localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8 \
LANGUAGE ja_JP:ja \
LC_ALL ja_JP.UTF-8 \
TZ JST-9
RUN apt-get install -y vim
ベースとなるDockerイメージには安定したバージョンの「buster」を指定します。
「alpine」は軽量ですがapt-getコマンドも利用できないなど開発環境としては色々と面倒なので、一通りの機能が揃っている「buster」がおすすめです。
docker-compose.yaml作成
Dockerfileと同じディレクトリにdocker-compose.yamlファイルを作成します。
version: '3.8'
services:
app:
build:
context: ../../
dockerfile: ./docker/dev/Dockerfile
volumes:
- "../../:/workspace"
ports:
- "3000:3000"
tty: true
buildはcontextをプロジェクトのルートディレクトリを指定するために「../../」とします。
そのため、dockerfileもプロジェクトのルートディレクトリからのパスを指定します。
volumesでコンテナの作業ディレクトリとローカルのディレクトリを指定しておくことで、ローカル上のファイルを作成・修正するとコンテナ内のファイルに反映されるようになります。
後ほど、create-react-appで環境構築するため、volumesではプロジェクトのルートディレクトリをコンテナの「/app」に紐付けます。
portsはReactのnpm startの標準ポートである3000ポートをそのまま転送しておきます。
ttyはコンテナにログインした状態で色々と操作するためにtrueにします。
[開発環境] コンテナを起動する
docker-compose up
開発用の docker-compose.yml ファイルのあるディレクトリに移動し、以下のコマンドを実行し開発環境を構築します。
$ cd docker/dev
$ docker-compose up --build -d
「-d」はバックグラウンド実行のオプションです。
docker-compose ps
docker-compose ps コマンドで起動状態を確認します。
$ docker-compose ps
Name Command State Ports
----------------------------------------------------------------------
dev_app_1 docker-entrypoint.sh node Up 0.0.0.0:3000->3000/tcp
[開発環境] 起動したdockerコンテナにReact環境を構築する
Dockerコンテナが起動したら、DockerコンテナにログインしReact環境を構築していきます。
Dockerコンテナにログイン
以下のコマンドを実行するとDockerコンテナにログインできます。
$ docker-compose exec app /bin/bash
「app」のところは、「docker-compose.yaml」の「services」で指定した名前です。
ログインが成功すると、作業ディレクトリで指定したディレクト(/app)にrootユーザでログインした状態になります。
プロジェクトのルートディレクトリがコンテナの「/app」に紐付けられていることがわかります。
$ docker-compose exec app /bin/bash
root@cb3d7faff0ed:/app# ls -al
total 24
drwxr-xr-x 6 node node 4096 Sep 12 05:31 .
drwxr-xr-x 1 root root 4096 Sep 12 05:39 ..
drwxr-xr-x 4 node node 4096 Sep 12 04:05 docker
drwxr-xr-x 2 node node 4096 Sep 12 04:55 kube
React環境を構築
create-react-appインストール
Reactの環境構築に「create-react-app」を利用したいため、npmでグローバル領域にインストールします。
root@cb3d7faff0ed:/app# npm install -g create-react-app
create-react-appでReact環境を構築
typescrptを利用したい場合は「–template typescript」オプションを指定しましょう。
また、ここでは yarn ではなく npm を利用したいため「–user-npm」オプションを指定します。
root@cb3d7faff0ed:/app# npx create-react-app app --template typescript --use-npm
create-react-appのv4.0.0では「–typescript」は動作しませんでしたので、「–template typescript」を指定する必要があります。
ローカル環境側でプロジェクトのルートディレクトリに「app」ディレクトリが作成されていることが確認できます。
docker-composeのvolumesを設定しましたので、ローカル環境側でも「create-react-app」で作成したファイルを編集できます。
Reactアプリ実行
DockerコンテナでReactアプリを「npm start」で実行します。
root@cb3d7faff0ed:/app/react-tasks# npm start
Compiled successfully!
You can now view react-typescript in the browser.
Local: http://localhost:3000
On Your Network: http://172.18.0.2:3000
Note that the development build is not optimized.
To create a production build, use npm run build.
ブラウザで「http://localhost:3000」を開くと起動しているはずです。
[開発環境] Dockerfileを修正してパッケージがインストールされるようにする
create-react-appコマンドでReact環境を初期構築すると、以下のファイルが作成されます。
- package.json
- package-lock.json
- tsconfig.json
2回目以降に開発環境用のコンテナを作成した際にcreate-react-appコマンドや追加でインストールしたパッケージなどを自動でインストールできるようにDockerfileを修正します。
FROM node:12.18-buster
WORKDIR /workspace/app
# -------------------------------------------------
# [1] 初期構築
# -------------------------------------------------
RUN apt-get update
RUN apt-get -y install locales && \
localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8 \
LANGUAGE ja_JP:ja \
LC_ALL ja_JP.UTF-8 \
TZ JST-9
RUN apt-get install -y vim
# -------------------------------------------------
# [2] パッケージのインストール
# -------------------------------------------------
COPY app/package*.json /workspace/app
COPY app/tsconfig.json /workspace/app
RUN npm install
「[2] パッケージのインストール」以下の4行を追加すると、次回以降コンテナを再作成した場合でもパッケージがインストールされた状態でコンテナが起動します。
[開発環境] Dockerコンテナの停止と開始
PCの再起動時など、Dockerコンテナの停止と開始が必要になりますし、コンテナを再作成の場合などは削除も行うこともありますのでコマンドを覚えておきましょう。
Dockerコンテナの停止
$ docker-compose stop
Stopping dev_app_1 ... done
$ docker-compose ps
Name Command State Ports
--------------------------------------------------------
dev_app_1 docker-entrypoint.sh node Exit 137
Dockerコンテナの削除
$ docker-compose down
Removing dev_app_1 ... done
Removing network dev_default
docker-compose downコマンドはコンテナが起動状態では実行できませんので、docker-compose stopコマンドで停止後に実行する必要があります。
Dockerコンテナの開始
$ docker-compose start
Starting app ... done
$ docker-compose ps
Name Command State Ports
----------------------------------------------------------------------
dev_app_1 docker-entrypoint.sh node Up 0.0.0.0:3000->3000/tcp
[本番環境] Dockerfileを作成する(マルチステージ・ビルド)
/docker/prod ディレクトリに本番環境用のDockerfileを作成します。
FROM node:12.18-buster AS builder
WORKDIR /app
RUN apt-get update
COPY app/package*.json /app/
COPY app/tsconfig.json /app/
RUN npm install
COPY app/src /app/src/
COPY app/public /app/public/
RUN npm run build
FROM nginx:1.18-alpine
COPY docker/prod/nginx/default.conf /etc/nginx/conf.d/
COPY --from=builder /app/build /usr/share/nginx/html
WORKDIR /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
開発環境用のコンテナイメージには開発作業を行うため機能豊富な「node:xx.xx-buster」といった容量の大きいコンテナイメージを利用しましたが、Reactアプリの場合、本番環境用のコンテナではビルドした静的コンテンツが稼働する環境であればよいため、容量の小さな「nginx:xx.xx-apline」を利用したいと考えます。
ですが、デプロイするためにビルドする必要がありますので、ビルド時には「node:xx.xx-buster」を利用したいことになります。
この問題についてはDockerのマルチステージ・ビルド機能を利用して解決します。
上記のDockerfileでDockerイメージファイルが2か所で設定されているところに注目してください。
- 1行目:FROM node:12.18-buster AS builder
- 15行目:FROM nginx:1.18-alpine
「node:12.18-buster」はビルド専用のコンテナとして利用し、実際にデプロイするコンテナは「nginx:1.18-alpine」を利用しています。
もう少し処理内容を具体的に説明すると
- 12行目:RUN npm run build
でReactアプリをビルドしコンテナ内の「/app/build」ディレクトリにビルド済みコンテンツが配置されます。
ビルド済みコンテンツを
- 17行目:COPY –from=builder /app/build /usr/share/nginx/html
で「nginx:1.18-alpine」の「/app/build /usr/share/nginx/html」にコピーします。
このようにコンテナ間でファイルをコピーすることを利用して容量の軽い本番用コンテナを作成します。
[本番環境] docker buildとdocker push
本番環境用のDockerfileが作成できれば、
- docker build コマンドで docker image を作成する
- docker image を DockerHub へ pushする
の2つの作業を行います。
手動で実行する
手動で行う場合は以下のコマンドを順に実行します。
docker build
プロジェクトのルートディレクトリに移動し実行します。
コンテナ名を「react-app」、バージョンを「1.0.0」とした場合のコマンド例です。
$ docker build --no-cache -t [DockerHubのアカウント]/react-app:1.0.0 -f ./docker/prod/Dockerfile .
「–no-cache」オプションを実行することで、時間はかかりますがキャッシュを利用せずにビルドされます。
docker push
docker buildコマンドで作成したコンテナイメージをDockerHubへpushします。
$ docker push [DockerHubのアカウント]/react-app:1.0.0
初めてDockerHubを利用する場合はdocker loginコマンドで事前にログインしておきましょう。
shellでbuildとpushを半自動化する
本格的にCI/CD環境を構築し完全に自動化してもよいのですが、テスト開発用のアプリなどの場合には、もう少し簡単に半自動化させるためにshellを作成しました。
本番環境用のDockerfileと同じディレクトリ(/docker/prod/)に「dockerhub-push.sh」というファイル名で作成した場合を例にします。
dockerhub-push.sh
#!/bin/bash
#
# dockerhubへdocker imageを登録
#
# React App
#
# bashのスイッチ
set -euC
# グローバル変数
CONTEXT=../../.
DOCKER_FILE=./docker/prod/Dockerfile
DOCKER_ID=[DockerHubのアカウント]
CONTAINER_NAME=react-app
VERSION=
#
# 引数parse処理
#
function usage() {
cat <<EOS >&2
Usage: $0 -v VERSION
-c CONTEXT CONTEXTパス [規定値:$CONTEXT]
-f DOCKER_FILE Dockefileのファイル名 [規定値:$DOCKER_FILE]
-i DOCKER_ID DockehubのDocker Id [規定値:$DOCKER_ID]
-t CONTAINER_NAME Dockeコンテナ名 [規定値:$CONTAINER_NAME]
-v VERSION Dockeイメージのバージョン
EOS
exit 1
}
# 引数のパース
function parse_args() {
while getopts c:f:i:t:v: OPT
do
case $OPT in
c) CONTEXT=$OPTARG ;;
f) DOCKER_FILE=$OPTARG ;;
i) DOCKER_ID=$OPTARG ;;
t) CONTAINER_NAME=$OPTARG ;;
v) VERSION=$OPTARG ;;
?) usage;;
esac
done
if [[ "$VERSION" == "" ]]; then
usage
fi
}
#
# 関数定義
#
#
# docker buildコマンドでキャッシュを使用せずdocker image作成
#
function docker_build() {
local docker_id="$1"
local container_name="$2"
local version="$3"
local docker_file="$4"
echo "docker build --no-cache -t "${docker_id}/${container_name}:${version}" -f "$docker_file" ."
docker build --no-cache -t "${docker_id}/${container_name}:${version}" -f "$docker_file" .
}
#
# dockerhubへ作成済みのdocker imageをpush
#
function docker_push() {
local docker_id="$1"
local container_name="$2"
local version="$3"
echo "docker push "${docker_id}/${container_name}:${version}""
docker push "${docker_id}/${container_name}:${version}"
}
#
# main
#
function main() {
local context="$1"
local container_name="$2"
local docker_file="$3"
local docker_id="$4"
local version="$5"
cd "$context"
docker_build "$docker_id" "$container_name" "$version" "$docker_file"
docker_push "$docker_id" "$container_name" "$version"
}
# エントリー処理
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
parse_args "$@"
main "$CONTEXT" "$CONTAINER_NAME" "$DOCKER_FILE" "$DOCKER_ID" "$VERSION"
fi
コマンドラインヘルプなどを実装しているので長くなっていますが、やっていることはdocker buildとdocker pushだけです。
利用方法
バージョン番号以外はグローバル変数に初期値として格納しているので、基本はバージョン番号のみ指定して実行します。
$ cd [シェルを配置したディレクトリ]
$ ./dockerhub-push.sh -v 1.0.0
無事成功すれば指定したバージョンのDockerイメージがDockerHubに登録されます。
[本番環境] kubernetesのデプロイ用YAML作成
デプロイ対象のコンテナは今回1つですので、外部への公開用にNodePortを利用したServiceとDeploymentを、それぞれ1つ作成します。
ServiceとDeploymentのYAMLを分けて作成してもよいのですが、1つにまとめるとkubectlコマンドが1回で済みますので、1つで作成しました。
ファイルを作成する場所はどこでも大丈夫です。
ここでは「/kube/」に「react-app.yml」というファイル名で作成しています。
apiVersion: v1
kind: Service
metadata:
name: react-app-svc
labels:
app: react-app
spec:
type: NodePort
ports:
- name: http-port
protocol: TCP
port: 80
targetPort: 80
nodePort: 30080
selector:
app: react-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: react-app
spec:
selector:
matchLabels:
app: react-app
replicas: 2
template:
metadata:
labels:
app: react-app
spec:
containers:
- name: react-app
image: [DockerHubのアカウント]/react-app:1.0.0
ports:
- containerPort: 80
[本番環境] kubectl applyコマンドでデプロイする
作成したkubernetesのYAMLファイルでデプロイしましょう。
kubernetesへのデプロイはkubectlでYAMLファイルを指定することで簡単にデプロイできます。
新規にデプロイする場合
新規にデプロイする場合は以下のコマンドを実行します。
$ kubectl apply -f react-app.yml
service/react-app-svc created
deployment.apps/react-app created
serviceとdeploymentが作成できました。
念のため作成されたserviceやPodを確認するとNodePortで30080番ポートで公開されていることがわかります。
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/react-app-67c6dc68d7-6bmnq 1/1 Running 0 2m23s
pod/react-app-67c6dc68d7-h9w2x 1/1 Running 0 2m23s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 14d
service/react-app-svc NodePort 10.111.128.19 <none> 80:30080/TCP 2m23s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/react-app 2/2 2 2 2m23s
NAME DESIRED CURRENT READY AGE
replicaset.apps/react-app-67c6dc68d7 2 2 2 2m23s
kubernetesクラスタの30080ポートにブラウザでアクセスするとReactアプリが表示されます。
更新したDockerイメージをデプロイする場合
更新した場合は、通常Dockerコンテナイメージのバージョンを変更します。
「1.0.0」から「1.1.0」といった具合です。
プログラムの改修後にDockerHubに1.1.0にバージョンアップしたコンテナイメージがアップした後に、kubernetesのYAMLファイルでPullするイメージに対して「1.1.0」を指定します。
Deploymentの一部を抜粋するとこんな感じです。
spec:
containers:
- name: react-app
image: [DockerHubのアカウント]/react-app:1.1.0
ports:
- containerPort: 80
YAMLファイルの修正が完了したら、更新分をデプロイします。
更新する場合のデプロイも簡単で、新規のときと同じくkubectl applyコマンドで稼働中のコンテナを差し替えしてくれます。
$ kubectl apply -f react-app.yml
service/react-app-svc unchanged
deployment.apps/react-app configured
今回の場合、Serviceには変更がなくDeploymentのみ変更されたとありますので、想定通りの結果です。
[本番環境] kubectl deleteコマンドでリソースを削除する
kubernetesクラスタへデプロイしたserviceやdeploymentを削除する場合はkubectl deleteコマンドを使用します。
$ kubectl delete -f react-app.yml
service "react-app-svc" deleted
deployment.apps "react-app" deleted
kubectl get allコマンドで状態を確認すると、関連するserviceやdeploymentが削除されていることを確認できます。
$ kubectl get all
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 14d