Reactの開発環境(WSL2+docker)構築から本番環境(DockerHub+kubernetes)へのデプロイ手順まとめ

2020年9月13日Docker,kubernetes,React,Development

最近は、開発環境をDockerで構築することが多くなってきたので、Reactアプリの開発環境構築手順をまとめました。

また、本番デプロイ用にDockerHubへのPushとkubernetesへのデプロイ用YAMLの作成手順についてもまとめました。

環境

環境は以下を前提としていますが、Dockerが利用できる環境であればMacでもLinuxでも同じです。

環境

  • Windows 10 Home
  • WSL2(Ubuntu 20.04)
  • Docker
  • docker-compose

Windows10 HomeにWSL2とDocker環境を構築する手順については以下の記事にまとめています。

フォルダ構成

React開発環境の大まかなフォルダ構成です。

point

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」を指定します。

point

「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」ディレクトリが作成されていることが確認できます。 

 

point

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
point

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 .
point

「–no-cache」オプションを実行することで、時間はかかりますがキャッシュを利用せずにビルドされます。

docker push

docker buildコマンドで作成したコンテナイメージをDockerHubへpushします。

$ docker push [DockerHubのアカウント]/react-app:1.0.0
point

初めて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