20200324のdockerに関する記事は17件です。

Dockerで構築するEthereum PET-SHOP TRUFFLE BOXES(その1)

TruffleBoxs 入門(以下「入門記事」)
では、下記サイト(以下「React&Truffle記事」)をなぞらせていただきました。
TruffleBox (React&Truffle)を用いたDockerでのdapps(ブロックチェーンアプリ)の開発環境の構築

本記事では、より実践的なチュートリアルに挑戦ということで、ペットショップをやってみたいと思います。

ETHEREUM PET SHOP -- YOUR FIRST DAPP

ちなみに、このチュートリアルとはどのようなものなのか、今話題のDeepLで翻訳すると、このようになりました。

このチュートリアルでは、ペットショップのための養子縁組追跡システムを作成するプロセスを説明します。

このチュートリアルは、Ethereumとスマートコントラクトの基本的な知識があり、HTMLとJavaScriptの知識はあるが、ダップスには初めての方を対象としています。

開発環境構築

Docker環境

React&Truffle記事を参考に、使用するTruffleBox をペットショップに変更する形でDocker環境を用意します。

docker-compose.yml
version: '3'

services:
  truffle:
    build: 
      context: ./trufflebox/
      dockerfile: Dockerfile
    volumes:
      - ./trufflebox:/usr/src/app
    command: sh -c "cd client && yarn start"
    ports:
      - "8003:3000"
Dockerfile
FROM node:8-alpine  

RUN apk add --update alpine-sdk
RUN apk add --no-cache git python g++ make \
    && npm i -g --unsafe-perm=true --allow-root truffle 

WORKDIR /usr/src/app

この2ファイルを下記構成で作成。

pet-shop
├── docker-compose.yml
└── trufflebox
    └── Dockerfile

pet-shopフォルダで、下記を実行。

$ docker-compose build
Building truffle
Step 1/4 : FROM node:8-alpine
 ---> 2b8fcdc6230a
Step 2/4 : RUN apk add --update alpine-sdk
 ---> Using cache
 ---> 761342077e72
Step 3/4 : RUN apk add --no-cache git python g++ make     && npm i -g --unsafe-perm=true --allow-root truffle
 ---> Using cache
 ---> 82200b1b0c8f
Step 4/4 : WORKDIR /usr/src/app
 ---> Using cache
 ---> 4eb121f5853d
Successfully built 4eb121f5853d
Successfully tagged pet-shop_truffle:latest
$ docker-compose run truffle truffle unbox pet-shop
Creating network "pet-shop_default" with the default driver
You can improve web3's peformance when running Node.js versions older than 10.5.0 by installing the (deprecated) scrypt package in your project
This directory is non-empty...
? Proceed anyway? (Y/n) 
Starting unbox...
=================

? Proceed anyway? Yes
✔ Preparing to download box
✔ Downloading
npm WARN pet-shop@1.0.0 No description
npm WARN pet-shop@1.0.0 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

✔ cleaning up temporary files
✔ Setting up box

Unbox successful, sweet!

Commands:

  Compile:        truffle compile
  Migrate:        truffle migrate
  Test contracts: truffle test
  Run dev server: npm run dev

React&Truffle記事では、「unbox react」としていたところを、今回は、「unbox pet-shop」とするわけです。
これにより、
https://www.trufflesuite.com/boxes/pet-shop
を使用していることになります。

Ethereum ネットワーク

React&Truffle記事を参考に、Docke環境からローカルのGanache のテストネットワークに接続します。

まず、ローカル環境に接続可能なIPアドレスを割り振ります。
ローカルIPアドレスない場合は設定します。(React&Truffle記事、及び、入門記事参照)
そして、Ganache の設定画面で、ローカルIPアドレスで起動するように指定します。
スクリーンショット 2020-03-24 20.32.39.png
PORT NUMBER は7545としておきます。これは後で出てきます。

この状態で、METAMASKで接続できることを確認しておきます。
METAMASKは、このアドレス、ポートで接続するための設定を追加します。
スクリーンショット 2020-03-24 21.22.30.png

ここに接続するように、truffle-config.jsを構成します。
先ほど実行された、unbox pet-shop により、trufflebox フォルダに、ファイルが作成されています。

自動作成されたtruffle-config.js は下記のようになっていました。

truffle-config.js(自動作成)
module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // for more about customizing your Truffle configuration!
  networks: {
    development: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*" // Match any network id
    },
    develop: {
      port: 8545
    }
  }
};

truffleは、ネットワーク名を指定しなければ、development を使用します。
ですので、この中の設定を変更します。

truffle-config.js(変更)
module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // for more about customizing your Truffle configuration!
  networks: {
    development: {
      host: "10.200.10.1",
      port: 7545,
      network_id: "*" // Match any network id
    },
    develop: {
      port: 8545
    }
  }
};

そして、マイグレート=Ganache へのデプロイ。

$ docker-compose run truffle truffle migrate
You can improve web3's peformance when running Node.js versions older than 10.5.0 by installing the (deprecated) scrypt package in your project

Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.



Starting migrations...
======================
> Network name:    'development'
> Network id:      5777
> Block gas limit: 0x6691b7


1_initial_migration.js
======================

   Deploying 'Migrations'
   ----------------------
   > transaction hash:    0xd032d73ed143eeb44062f09d02fa64888d9c133d3addb2ff79ccef63d7a4f1ff
   > Blocks: 0            Seconds: 0
   > contract address:    0xF88b3D9805da39D094178f2ba0dCc38a0610d214
   > block number:        5
   > block timestamp:     1585053865
   > account:             0xc55F3d6C444ca88f529F3413EDEd85a39e38609C
   > balance:             99.98893326
   > gas used:            188483
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00376966 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00376966 ETH


Summary
=======
> Total deployments:   1
> Final cost:          0.00376966 ETH

起動。

$ docker-compose up
Creating pet-shop_truffle_1 ... done
Attaching to pet-shop_truffle_1
truffle_1  | sh: cd: line 1: can't cd to client: No such file or directory
pet-shop_truffle_1 exited with code 2

おっと失敗。フォルダ構成違うんですね。
docker-compose.ymlのcommandを修正します。

docker-compose.yml
version: '3'

services:
  truffle:
    build: 
      context: ./trufflebox/
      dockerfile: Dockerfile
    volumes:
      - ./trufflebox:/usr/src/app
    command: sh -c "npm run dev"
    ports:
      - "8003:3000"
$ docker-compose up
Recreating pet-shop_truffle_1 ... done
Attaching to pet-shop_truffle_1
truffle_1  | 
truffle_1  | > pet-shop@1.0.0 dev /usr/src/app
truffle_1  | > lite-server
truffle_1  | 
truffle_1  | ** browser-sync config **
truffle_1  | { injectChanges: false,
truffle_1  |   files: [ './**/*.{html,htm,css,js}' ],
truffle_1  |   watchOptions: { ignored: 'node_modules' },
truffle_1  |   server: 
truffle_1  |    { baseDir: [ './src', './build/contracts' ],
truffle_1  |      middleware: [ [Function], [Function] ] } }
truffle_1  | [Browsersync] Access URLs:
truffle_1  |  -----------------------------------
truffle_1  |        Local: http://localhost:3000
truffle_1  |     External: http://172.22.0.2:3000
truffle_1  |  -----------------------------------
truffle_1  |           UI: http://localhost:3001
truffle_1  |  UI External: http://localhost:3001
truffle_1  |  -----------------------------------
truffle_1  | [Browsersync] Serving files from: ./src
truffle_1  | [Browsersync] Serving files from: ./build/contracts
truffle_1  | [Browsersync] Watching files...
truffle_1  | [Browsersync] Couldn't open browser (if you are using BrowserSync in a headless environment, you might want to set the open option to false)

ブラウザで、
http://localhost:8003/
を開きます。画面が開きます!

スクリーンショット 2020-03-24 23.43.55.png

これにより、PET-SHOPトリュフボックスのInstallationがDocker環境にできたことになりまし、ETHEREUM PET SHOP -- YOUR FIRST DAPPのWriting the smart contract を始められる環境になったことになります。

いったんここで終了し、次の記事で、スマートコントラクトを作成していきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker勉強メモ④-コンテナ間通信

はじめに

Docker勉強メモ
- Docker勉強メモ① DockerインストールからHelloWorld
- Docker勉強メモ② Dockerイメージ作ってみる
- Docker勉強メモ③ Dockerfileを作ってDockerイメージ作成からコンテナ起動までやってみる
- Docker勉強メモ④-コンテナ間通信 ←ココ

やること

コンテナ間通信やってみる。

コンテナ間通信をする方法は2つ
- Dockerネットワークを作成してコンテナ名で接続できるようにする
- 「--link」オプションを使用する

「--link」オプションは、レガシーで削除の可能性あり、なので本記事ではスルー

豆知識

Docker のインストールは、自動的に3つのネットワークを作成
ネットワーク確認 : docker network ls
docker0 と表示されるブリッジ( bridge )ネットワーク
docker run --net=<ネットワーク名> と指定しないとdocker0になる
docker0情報は、ホストでifconfigで確認できる

コンテナ間通信 : Dockerネットワーク

Dockerネットワーク(bridgeタイプ)を作成

ホスト
$ docker network create wordpress-network

ネットワークを指定してコンテナを起動する

ホスト
$ docker run --name mysql --network wordpress-network -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7
$ docker run --name wordpress --network wordpress-network -e WORDPRESS_DB_PASSWORD=my-secret-pw -p 8080:80 -d wordpress

・DBとWebサーバのコンテナを作る
・同じネットワーク wordpress-network を指定する
・MySQLのコンテナ名は「mysql」とする
 ↑WordPressのDockerイメージは、MySQLへの接続先の指定のホスト名が「mysql」だから
→これで、WordPressコンテナ(wordpress)からMySQLコンテナ(mysql)に向けて通信可になる

ポート指定の注意点

ホスト
$ docker run --name mysql --network wordpress-network -p 13306:3306 -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7

図にするとこんな感じのはず

image.png

Dockerイメージを用いてコンテナ間で通信

次は、こんなの作ってみる

image.png

※Dockerイメージを用いてコンテナ間で通信するためには、定義ファイルなどに接続先としてコンテナ名を指定しておくことが必要

[centos@ip-172-31-0-62 ~]$ mkdir nginx
[centos@ip-172-31-0-62 ~]$ touch nginx/Dockerfile
[centos@ip-172-31-0-62 ~]$ mkdir nginx/files
[centos@ip-172-31-0-62 ~]$ touch nginx/files/tomcat.conf
[centos@ip-172-31-0-62 ~]$ mkdir tomcat
[centos@ip-172-31-0-62 ~]$ touch tomcat/Dockerfile
[centos@ip-172-31-0-62 ~]$ mkdir tomcat/files
[centos@ip-172-31-0-62 ~]$ cd tomcat/files
[centos@ip-172-31-0-62 ~]$ wget http://ftp.riken.jp/net/apache/tomcat/tomcat-9/v9.0.31/bin/apache-tomcat-9.0.31.tar.gz
nginx/Dockerfile
FROM nginx:latest

RUN rm -f /etc/nginx/conf.d/default.conf
COPY ./files/tomcat.conf /etc/nginx/conf.d/
nginx/files/tomcat.conf
server {
    location /tomcat/ {
        proxy_pass    http://tomcat-1:8080/;
    }
}
tomcat/Dockerfile
FROM centos:latest
RUN yum install -y java
ADD files/apache-tomcat-9.0.31.tar.gz /opt/
CMD [ "/opt/apache-tomcat-9.0.31/bin/catalina.sh", "run" ]
ホスト
[centos@ip-172-31-0-62 ~]$ tree
.
├── nginx
│   ├── Dockerfile
│   └── files
│       └── tomcat.conf
└── tomcat
    ├── Dockerfile
    └── files
        └── apache-tomcat-9.0.31.tar.gz

Nginxイメージ、Tomcatイメージの作成

ホスト
$ cd ${SOME_DIR}/nginx
$ docker build -t nginx-tomcat:1 .
$ cd ${SOME_DIR}/tomcat
$ docker build -t tomcat:1 .
ホスト
[centos@ip-172-31-0-62 tomcat]$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx-tomcat        1                   1e137080691f        14 minutes ago      127MB
tomcat              1                   5ad4b2b915cf        3 hours ago         523MB
 ・
 ・

Dockerネットワーク作成、コンテナ起動

ホスト
$ docker network create tomcat-network
$ docker run --name tomcat-1 --network tomcat-network -d tomcat:1
$ docker run --name nginx-tomcat-1 --network tomcat-network -p 10080:80 -d nginx-tomcat:1
ホスト
[centos@ip-172-31-0-62 tomcat]$ docker ps -a
                                            CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                   NAMES
ab41de63e013        nginx-tomcat:1      "nginx -g 'daemon of…"   11 seconds ago      Up 10 seconds       0.0.0.0:10080->80/tcp   nginx-tomcat-1
4949f5632a5a        tomcat:1            "/opt/apache-tomcat-…"   20 seconds ago      Up 19 seconds                               tomcat-1

ホストから以下サイトを開ければ、nginx経由でtomcatのサイトが開けたことになる
http://localhost:10080/tomcat/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker勉強メモ④ コンテナ間通信

はじめに

Docker勉強メモ
- Docker勉強メモ① DockerインストールからHelloWorld
- Docker勉強メモ② Dockerイメージ作ってみる
- Docker勉強メモ③ Dockerfileを作ってDockerイメージ作成からコンテナ起動までやってみる
- Docker勉強メモ④ コンテナ間通信 ←ココ
- Docker勉強メモ⑤ ネットワーク通信

やること

コンテナ間通信やってみる。

コンテナ間通信をする方法は2つ
- Dockerネットワークを作成してコンテナ名で接続できるようにする
- 「--link」オプションを使用する

「--link」オプションは、レガシーで削除の可能性あり、なので本記事ではスルー

豆知識

Docker のインストールは、自動的に3つのネットワークを作成
ネットワーク確認 : docker network ls
docker0 と表示されるブリッジ( bridge )ネットワーク
docker run --net=<ネットワーク名> と指定しないとdocker0になる
docker0情報は、ホストでifconfigで確認できる

コンテナ間通信 : Dockerネットワーク

Dockerネットワーク(bridgeタイプ)を作成

ホスト
$ docker network create wordpress-network

ネットワークを指定してコンテナを起動する

ホスト
$ docker run --name mysql --network wordpress-network -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7
$ docker run --name wordpress --network wordpress-network -e WORDPRESS_DB_PASSWORD=my-secret-pw -p 8080:80 -d wordpress

・DBとWebサーバのコンテナを作る
・同じネットワーク wordpress-network を指定する
・MySQLのコンテナ名は「mysql」とする
 ↑WordPressのDockerイメージは、MySQLへの接続先の指定のホスト名が「mysql」だから
→これで、WordPressコンテナ(wordpress)からMySQLコンテナ(mysql)に向けて通信可になる

ポート指定の注意点

ホスト
$ docker run --name mysql --network wordpress-network -p 13306:3306 -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7

図にするとこんな感じのはず

image.png

Dockerイメージを用いてコンテナ間で通信

次は、こんなの作ってみる

image.png

※Dockerイメージを用いてコンテナ間で通信するためには、定義ファイルなどに接続先としてコンテナ名を指定しておくことが必要

[centos@ip-172-31-0-62 ~]$ mkdir nginx
[centos@ip-172-31-0-62 ~]$ touch nginx/Dockerfile
[centos@ip-172-31-0-62 ~]$ mkdir nginx/files
[centos@ip-172-31-0-62 ~]$ touch nginx/files/tomcat.conf
[centos@ip-172-31-0-62 ~]$ mkdir tomcat
[centos@ip-172-31-0-62 ~]$ touch tomcat/Dockerfile
[centos@ip-172-31-0-62 ~]$ mkdir tomcat/files
[centos@ip-172-31-0-62 ~]$ cd tomcat/files
[centos@ip-172-31-0-62 ~]$ wget http://ftp.riken.jp/net/apache/tomcat/tomcat-9/v9.0.31/bin/apache-tomcat-9.0.31.tar.gz
nginx/Dockerfile
FROM nginx:latest

RUN rm -f /etc/nginx/conf.d/default.conf
COPY ./files/tomcat.conf /etc/nginx/conf.d/
nginx/files/tomcat.conf
server {
    location /tomcat/ {
        proxy_pass    http://tomcat-1:8080/;
    }
}
tomcat/Dockerfile
FROM centos:latest
RUN yum install -y java
ADD files/apache-tomcat-9.0.31.tar.gz /opt/
CMD [ "/opt/apache-tomcat-9.0.31/bin/catalina.sh", "run" ]
ホスト
[centos@ip-172-31-0-62 ~]$ tree
.
├── nginx
│   ├── Dockerfile
│   └── files
│       └── tomcat.conf
└── tomcat
    ├── Dockerfile
    └── files
        └── apache-tomcat-9.0.31.tar.gz

Nginxイメージ、Tomcatイメージの作成

ホスト
$ cd ${SOME_DIR}/nginx
$ docker build -t nginx-tomcat:1 .
$ cd ${SOME_DIR}/tomcat
$ docker build -t tomcat:1 .
ホスト
[centos@ip-172-31-0-62 tomcat]$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx-tomcat        1                   1e137080691f        14 minutes ago      127MB
tomcat              1                   5ad4b2b915cf        3 hours ago         523MB
 ・
 ・

Dockerネットワーク作成、コンテナ起動

ホスト
$ docker network create tomcat-network
$ docker run --name tomcat-1 --network tomcat-network -d tomcat:1
$ docker run --name nginx-tomcat-1 --network tomcat-network -p 10080:80 -d nginx-tomcat:1
ホスト
[centos@ip-172-31-0-62 tomcat]$ docker ps -a
                                            CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                   NAMES
ab41de63e013        nginx-tomcat:1      "nginx -g 'daemon of…"   11 seconds ago      Up 10 seconds       0.0.0.0:10080->80/tcp   nginx-tomcat-1
4949f5632a5a        tomcat:1            "/opt/apache-tomcat-…"   20 seconds ago      Up 19 seconds                               tomcat-1

ホストから以下サイトを開ければ、nginx経由でtomcatのサイトが開けたことになる
http://localhost:10080/tomcat/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker勉強メモ④-コンテナ間通信(1)

はじめに

Docker勉強メモ
- Docker勉強メモ① DockerインストールからHelloWorld
- Docker勉強メモ② Dockerイメージ作ってみる
- Docker勉強メモ③ Dockerfileを作ってDockerイメージ作成からコンテナ起動までやってみる
- Docker勉強メモ④-コンテナ間通信 ←今ここ

やること

コンテナ間通信やってみる。

コンテナ間通信をする方法は2つ
- Dockerネットワークを作成してコンテナ名で接続できるようにする
- 「--link」オプションを使用する

「--link」オプションは、レガシーで削除の可能性あり、なので本記事ではスルー

豆知識

Docker のインストールは、自動的に3つのネットワークを作成
ネットワーク確認 : docker network ls
docker0 と表示されるブリッジ( bridge )ネットワーク
docker run --net=<ネットワーク名> と指定しないとdocker0になる
docker0情報は、ホストでifconfigで確認できる

コンテナ間通信 : Dockerネットワーク

Dockerネットワーク(bridgeタイプ)を作成

ホスト
$ docker network create wordpress-network

ネットワークを指定してコンテナを起動する

ホスト
$ docker run --name mysql --network wordpress-network -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7
$ docker run --name wordpress --network wordpress-network -e WORDPRESS_DB_PASSWORD=my-secret-pw -p 8080:80 -d wordpress

・DBとWebサーバのコンテナを作る
・同じネットワーク wordpress-network を指定する
・MySQLのコンテナ名は「mysql」とする
 ↑WordPressのDockerイメージは、MySQLへの接続先の指定のホスト名が「mysql」だから
→これで、WordPressコンテナ(wordpress)からMySQLコンテナ(mysql)に向けて通信可になる

ポート指定の注意点

ホスト
$ docker run --name mysql --network wordpress-network -p 13306:3306 -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7

図にするとこんな感じのはず

image.png

Dockerイメージを用いてコンテナ間で通信

次は、こんなの作ってみる

image.png

※Dockerイメージを用いてコンテナ間で通信するためには、定義ファイルなどに接続先としてコンテナ名を指定しておくことが必要

[centos@ip-172-31-0-62 ~]$ mkdir nginx
[centos@ip-172-31-0-62 ~]$ touch nginx/Dockerfile
[centos@ip-172-31-0-62 ~]$ mkdir nginx/files
[centos@ip-172-31-0-62 ~]$ touch nginx/files/tomcat.conf
[centos@ip-172-31-0-62 ~]$ mkdir tomcat
[centos@ip-172-31-0-62 ~]$ touch tomcat/Dockerfile
[centos@ip-172-31-0-62 ~]$ mkdir tomcat/files
[centos@ip-172-31-0-62 ~]$ cd tomcat/files
[centos@ip-172-31-0-62 ~]$ wget http://ftp.riken.jp/net/apache/tomcat/tomcat-9/v9.0.31/bin/apache-tomcat-9.0.31.tar.gz
nginx/Dockerfile
FROM nginx:latest

RUN rm -f /etc/nginx/conf.d/default.conf
COPY ./files/tomcat.conf /etc/nginx/conf.d/
nginx/files/tomcat.conf
server {
    location /tomcat/ {
        proxy_pass    http://tomcat-1:8080/;
    }
}
tomcat/Dockerfile
FROM centos:latest
RUN yum install -y java
ADD files/apache-tomcat-9.0.31.tar.gz /opt/
CMD [ "/opt/apache-tomcat-9.0.31/bin/catalina.sh", "run" ]
ホスト
[centos@ip-172-31-0-62 ~]$ tree
.
├── nginx
│   ├── Dockerfile
│   └── files
│       └── tomcat.conf
└── tomcat
    ├── Dockerfile
    └── files
        └── apache-tomcat-9.0.31.tar.gz

Nginxイメージ、Tomcatイメージの作成

ホスト
$ cd ${SOME_DIR}/nginx
$ docker build -t nginx-tomcat:1 .
$ cd ${SOME_DIR}/tomcat
$ docker build -t tomcat:1 .
ホスト
[centos@ip-172-31-0-62 tomcat]$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx-tomcat        1                   1e137080691f        14 minutes ago      127MB
tomcat              1                   5ad4b2b915cf        3 hours ago         523MB
 ・
 ・

Dockerネットワーク作成、コンテナ起動

ホスト
$ docker network create tomcat-network
$ docker run --name tomcat-1 --network tomcat-network -d tomcat:1
$ docker run --name nginx-tomcat-1 --network tomcat-network -p 10080:80 -d nginx-tomcat:1
ホスト
[centos@ip-172-31-0-62 tomcat]$ docker ps -a
                                            CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                   NAMES
ab41de63e013        nginx-tomcat:1      "nginx -g 'daemon of…"   11 seconds ago      Up 10 seconds       0.0.0.0:10080->80/tcp   nginx-tomcat-1
4949f5632a5a        tomcat:1            "/opt/apache-tomcat-…"   20 seconds ago      Up 19 seconds                               tomcat-1

ホストから以下サイトを開ければ、nginx経由でtomcatのサイトが開けたことになる
http://localhost:10080/tomcat/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaアプリをDockerイメージにするならjibを使うと便利だよ

メリットの多いDocker

コンテナ化技術の一つであるDockerですが、使ってみると数多くのメリットがあります。
実際のメリットについてはネットで検索すると色々出てくるので、この記事ではDockerそのものに関する解説はいたしません。
ただ個人的に強調しておきたいのは、本番環境などへ直接Javaのモジュールやアーカイブなどを配置するようなデプロイしている場合、Dockerイメージをリポジトリにpushするという作業に変えることでたくさんの恩恵を得ることができると思います。

Dockerの面倒さ

メリットの多いDockerですが面倒な面もあります。
Dockerfileを書かないといけない。書くためのコマンドやベストプラクティスも学習するとなるとそれなりに面倒です。
ローカル開発環境がWindowsの場合にDocker環境を構築するのも面倒です。

jibが解決

もしDockerコンテナ上で動くアプリケーションがJavaで開発されている場合、Googleが提供しているjibを利用することでDockerにまつわるいくつかの面倒さから解放されます。

jibを利用するにあたって必要なのは(gradleを使っている場合) build.gradle

plugins {
  id 'com.google.cloud.tools.jib' version '2.1.0'
}

という設定だけです。

そして仮に docker-image-to-push/1.0.0 というイメージ名でpushしたい場合は

jib.to.image = 'docker-image-to-push/1.0.0'

build.gradle に記述するだけです。
これで gradle jib と実行すればDockerイメージが作成されpushされます。
驚くべきはたったこれだけの設定でいいことと、ローカルにDocker環境が必要でないことです。
もしDocker Hubにアカウントを持っているなら

jib.to {
  auth {
    username 'account'
    password 'pass'
  }
  image 'account/repository:1.0.0'
}

のように build.gradle に設定することですぐにjibを試すことができます。
(account, pass, repositoryの部分は環境に合わせて変更してください)

上記の例でわかるようにほとんど設定のいらないjibですが、色々設定することも可能です。
まず設定しておいてほしいのは(JavaのソースファイルのエンコーディングがUTF-8の場合)

jib.container.environment = [JAVA_TOOL_OPTIONS: '-Dfile.encoding=UTF-8']

という設定です。
これがないとソースファイル中に日本語がある場合にエラーになってしまいます。
設定できる内容については configurationから確認できます。

以上、jibの紹介でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】Docker for WindowsでLaradock(+MySQL)に挑戦(※格闘)してみた。【備忘録】

はじめに

Docker for WindowsでLaradockの環境構築をして、Webアプリが作れるところまで記載しております。

DBに関してはMySQLでphpmyadminも利用できるようにしています。

※まだできていませんが、PostgreSQLでの環境構築の格闘記録はこちらにあります。(なんでこんなに難しいの。。。)

https://chobimusic.com/laradock_postgresql/

※僕の備忘録でもあり、構築できるようになるまでにかなり苦労したのでとても長文です。かなりのエラーを潰してきたので、部分部分かいつまんで参考にしていただければと思います。どなたかの参考になれば幸いです。※

参考記事


Windows10でLaradockを使ってLaravel 5.5環境を作る
https://qiita.com/sket88/items/4de708ce394179c61d8a


DockerでMySQL複数バージョンを共存させる
https://qiita.com/tanakaworld/items/427b94ea0435b5dccfa2


LaradockのMySQLに接続できなくてはまった話
https://qiita.com/dnrsm/items/4bd078c17bb0d6888647


laradockの環境設定からMySQL接続まで
https://qiita.com/yknsmullan/items/dea4102cf14b1b66e5af


docker起動でportが確保できないエラーの解決
https://nijoen.net/blog/773/


Dockerでコンテナの停止・削除ができなくなった時の対処法
https://qiita.com/musatarosu/items/31d6293a93e75ca6073e


docker docker-compose コマンド
https://qiita.com/souichirou/items/6e701f6469822a641bdd

参考書籍

https://www.amazon.co.jp/PHP%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF-Laravel-Web%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E9%96%8B%E7%99%BA-%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B35-5-LTS%E5%AF%BE%E5%BF%9C/dp/4802611846

環境構築


(Dockerのインストールは、書籍を参考にインストール)

フォルダ作成。

$ mkdir laravel_app 
$ cd laravel_app

git cloneコマンドでLaradockのダウンロード後、.envファイルの作成
$ git clone https://github.com/laradock/laradock.git
$ cd laradock
$ cp env-example .env

.envファイルを編集(※MySQLのバージョンが8.0以上になっているとセキュリティの関係でDockerがうまく動作しないらしい)
MYSQL_VERSION=5.7

APP_CODE_PATH_HOST=../laravel-practice/ //後でインストールするlaravelの名前

コンテナの初期化(実行コマンド※Dockerアプリは起動した状態※)
$ docker-compose up -d nginx mysql workspace phpmyadmin

ここでエラー発生。

どうやら3306ポートが使えないらしい。.envファイル上で3306と記載の部分を3307に変更して再度compose upするも同様のエラー。(追記:XAMPPを起動していたのを見落としていました。それが原因。)

XAMPPでも3307のポートを使用しているので、念のためその後ポートは3306に戻す。

ためしに以下のコマンドで起動してみる。

docker-compose up

バカみたいに時間がかかったのでctrl+cで離脱。
docker-compose up -d nginx mysql

上記と同様(3306ポートが使えない。)のエラーが発生。

一応下記のコマンドでstatusがupになっていれば起動しているらしい。

$ docker ps

どうやらnginx,php-fpm,workspace,docker:dindは起動してる。記載のないphpmyadminとmysqlが起動していない。

$ docker-compose exec --user=laradock workspace bash
laradock@0b80605539aa:/var/www$

ただnginxとworkspaceは起動しているのでとりあえずログインを試みたところログインはできた。
composer create-project laravel/laravel laravel-practice --prefer-dist "5.5.*"

さらにLaravelプロフェクトの作成を試みる。下記ディレクトリに作成されていることを確認。
(ここで最低限、Docker自体は起動していることは確認。MySQLのエラー解決に関しては後述。)

exitでコンテナからログアウト。.envファイル(laradock側の共有ディレクトリ)に以下を追記。

DB_HOST=mysql

サービスの終了
docker-compose stop

再起動
docker-compose up -d nginx mysql workspace phpmyadmin

やはり3306ポートエラーになる。

XAMPPを終了して3307に変更して再起動。(ここでXAMPPを切り忘れていたことに気付く。)

mysqlが起動した!!どうやらXAMPPと干渉しあってたっぽい。。。(3306はなんなんだ。。。)

Dockerを使う際はXAMPPを停止しましょう。

ただphpmyadminはエラーで起動できていないので、laravel-practiceの.envファイルを編集して再起動。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

⇓以下のように編集
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3307
DB_DATABASE=default
DB_USERNAME=default
DB_PASSWORD=secret

再起動してみてもphpmyadminのエラーは変わらず。

ダメもとでコンテナにログインしてmigrateしたところ下記のようなエラー。
そもそもartisanコマンドすら利かないことが判明。。。

$ docker-compose exec --user=laradock workspace bash
laradock@0b80605539aa:/var/www$ php artisan migrate
Could not open input file: artisan

localhostへアクセスしてもnot found。hostsファイルやらDockerの初期設定にも間違いがあるかもしれない。

戦いは続く…

再挑戦編


phpmyadminとの格闘


PCを再起動してからDockerを再起動してみる。

phpmyadminは変わらず起動しない。
※0.0.0.0:8080のバインドに失敗しました:ポートは既に割り当てられています

netstat -ano | find ":8080"
find: ‘:8080’: No such file or directory

このコマンドでポートで何が使用されているかわかるらしいが何も反応せず。。。
docker ps -a

調べると以前のプロセスもこのコマンドでチェックできるとのことで実行。

7 weeks agoとめちゃくちゃ怪しいログを発見。

docker rm docker ps -a -q

こちらで停止できるとのことだが
Error response from daemon: You cannot remove a running container
(デーモン(メモリ上の常駐ソフトウェア)からのエラー応答:実行中のコンテナを削除できません)

終了できそうなコマンドを一通り入力。

docker-compose kill
docker stop $(docker ps -a -q)
docker rm $(docker ps -a -q)

再起動するも変わらないので、まだ削除されていないコンテナがあるっぽい。。。
docker ps //起動中のコンテナを表示。
docker rm --force <コンテナID> //指定のコンテナを強制終了

見つけたコンテナを強制終了して再起動するもport被りの状況は変わらず。。。
docker-compose up -d

ためしに上記コマンド。(鬼時間かかる。。。おそらく3時間くらい待った。)長すぎるので割愛。最後の文が赤字の時点でアウト。。。

以下の通り何も表示されない。待った意味。。。一旦ステイ。。。

$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

MySQLとの格闘


たまたまこんなものを見つける。(いつ入れたのか全く覚えていない。これが干渉している??)

見つけた。どうやらLaravelを学習する際に入れた模様。

一応MAMPインストールした過去があったのでこちらの共存も気になったが、起動してなければ特にコンフリクト(衝突)することはないそうなので追求せず。

とりあえずこいつを停止してみた。

おそらくこいつのせいでXAMPPのポートも3306で起動できず、3307に変更せざるを得なかったんだなと気づく。

とりあえず3306にDocker内のファイルを変更してみる。mysqlは3306でも起動するようになった!!

残る問題はphpmyadminとartisanコマンドの実行だ。。。

artisanコマンドとの格闘


laradock@48ebb9bcd534:/var/www$ php artisan serve
Could not open input file: artisan
$ cd laravel-practice
$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>

ディレクトリが違うだけだった。。。

が、http://127.0.0.1:8000にアクセスしてもエラー。

コンテナからexitしてもしやと思いコンテナに入らずにアクセス。

表示された。(これであってるのか。。。??) 追記:コンテナで環境構築してるのでartisan serveは不要なのを後に知る。

ためしにdockerを終了してからアクセスしたところ表示されないので合ってるっぽい。とりあえず構築成功??

マイグレートも失敗。これはテーブル作成などもしてないからなんとなく理解できる。

php artisan migrate

phpmyadminとの格闘②


docker-compose.ymlのphpmyadminのポート番号を8081に変更。
//ポートのみ変更した場合
phpmyadmin:
      build: ./phpmyadmin
      environment:
        - PMA_ARBITRARY=1
        - MYSQL_USER=root
        - MYSQL_PASSWORD=password
        - MYSQL_ROOT_PASSWORD=password
    ports:
    - 8081:80

//サーバ名を調べるのに試行錯誤したパターン。※ログインエラー。
mysql:
      build:
        context: ./mysql
        args:
          - MYSQL_VERSION=${MYSQL_VERSION}
      environment:
        - MYSQL_DATABASE=DB
        - MYSQL_HOST=DB
        - MYSQL_USER=root
        - MYSQL_PASSWORD=password
        - MYSQL_ROOT_PASSWORD=password
        - TZ=${WORKSPACE_TIMEZONE}

phpmyadmin:
      build: ./phpmyadmin
      environment:
        - PMA_ARBITRARY=1
        - PMA_HOST=mysql
        - PMA_USER=root
        - PMA_PASSWORD=password
      ports:
        - 8081:80

全てきれいにdoneになった。泣 (そして一体8080ポートは何に使用しているんだろうか。。。)

http://localhost:8081でphpmyadminへアクセス。

サーバ名。。。わからずログインできず。。。

(その後かなり格闘した末。。。)laravel-practiceの.envに従い入力したところログインに成功。
サーバ:mysql
ユーザー:default
パスワード:secret

以下参照

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3307
DB_DATABASE=default
DB_USERNAME=default
DB_PASSWORD=secret

喜びも束の間。データベースを作成する特権がありません。と表示。

.envファイルにこんな記載があるのを思い出す。ユーザーrootでパスワードrootを入力したところログイン。編集もできた◎

MYSQL_ROOT_PASSWORD=root

mysql -u root -p root //こちらからもアクセス可能

create database default; //DB作成

ところがコンテナ内でmigrateはできず。。。

次はちゃんと教材見ながらトライしてみよう。。。挑戦はまだまだ続く。。。

日を改め再チャレンジ


PostgreSQLでサイトの表示まではできたので、再度チャレンジ。(以下参考。)

https://chobimusic.com/laradock_postgresql/

compose upの前に.envファイルを編集。

APP_CODE_PATH_HOST=../laravel #laravelのプロジェクトファイル名に書き換え
DATA_PATH_HOST=../data #複数データを参照してしまう可能性があるため
COMPOSE_PROJECT_NAME=docker_mysql #dockerのコンテナ名を変更
MYSQL_VERSION=5.7 #latestから変更
DB_HOST=mysql #追記

さらにdocker-compose.ymlのphpmyadminのポート番号を8081に変更。(MySQL Notifierも毎回起動するので停止させておく。)

コマンド実行。もはや1つもdoneされない状況に。。。泣 前は表示されなかったのに。

$ docker-compose up -d nginx mysql workspace phpmyadmin

【ERROR: Service 'php-fpm' failed to build: The command '/bin/sh -c if [ ${INSTALL_IMAGEMAGICK} = true ]; then apt-get install -y libmagickwand-dev imagemagick && pecl install imagick && docker-php-ext-enable imagick ;fi' returned a non-zero code: 100】でサービス 'php-fpm'の構築に失敗しているとのこと。


以下の記事を参考に、php-fpmディレクトリのDockerfileにて以下をImageMagickの欄に追記。再度試したところdoneと表示◎

http://domwp.hatenablog.com/entry/2019/01/30/151146

apt-get update && \ 

laravelのバージョンを教本だと5.5のところあえて6.8で今回はチャレンジ。成功。

$ docker-compose exec workspace composer create-project --prefer-dist laravel/laravel . "6.8.*"
Application key set successfully.

Laravelアプリ トップ画面 http://localhost/
phpmyadmin トップ画面 http://localhost:8081/

以下でphpmyadminにログイン。DBにdocker_mysqlがあること確認。

サーバ名:mysql
ユーザー名:root
パスワード:root

Laravel側の.envファイルを編集。

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=docker_mysql
DB_USERNAME=root
DB_PASSWORD=root

コントローラー作成


コントローラー作成はOK
$ docker-compose exec workspace php artisan make:controller ArticleController
Controller created successfully.

DB作成


マイグレーションファイルの作成。
$ docker-compose exec workspace php artisan make:migration create_articles_table --create=articles
Created Migration: 2020_03_24_092458_create_articles_table

マイグレート。初めてマイグレートに成功◎laravel側の.envファイルをrootユーザーで記入したからっぽい。
$ docker-compose exec workspace php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (0.13 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table (0.13 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated: 2019_08_19_000000_create_failed_jobs_table (0.07 seconds)
Migrating: 2020_03_24_092458_create_articles_table
Migrated: 2020_03_24_092458_create_articles_table (0.05 seconds)

これでやっとアプリ制作に取り掛かれるようになった。。。泣
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】Docker for WindowsでMailHogの実装をしてみた。

はじめに

Dockerの環境構築を頑張ろうと思ったきっかけである以下の教材。

https://chobimusic.com/laravel_vue_sns/

Dockerの環境構築ができたので、当時できなかったMailHogの実装に挑戦してみました。

※ちなみに以下が死ぬほど格闘したLaradock(+MySQL)の環境構築の備忘録です。

https://chobimusic.com/laradock_mysql/

MailHogの実装


以下の記事と上記記載の教材を参考に。

https://tech.windii.jp/backend/laravel/laravel-mailhog-docker-compose

MailHogのコンテナを起動。http://localhost:8025/にアクセスするとMailHogが表示される。

$ docker-compose up -d mailhog
WARNING: Image for service mailhog was built because it did not already exist. To rebuild this image you must use docker-compose build or docker-compose up --build.
Creating docker_mysql_mailhog_1 ... done

以降、再起動する場合はコマンドが以下になる。
docker-compose up -d workspace nginx php-fpm postgres mailhog

laravel側の.envを編集。
MAIL_DRIVER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=user
MAIL_PASSWORD=password
MAIL_ENCRYPTION=null

Mailableクラスを作成
$ docker-compose exec workspace php artisan make:mail Test --markdown=emails.test
Mail created successfully.

routes/web.phpを編集。
use Illuminate\Support\Facades\Mail;
use App\Mail\Test;

Route::get('/', function () {
return view('welcome');
});

Route::get('/test', function () {
Mail::to('test@example.com')->send(new Test);
return 'メール送信しました!';
});

http://localhost/test
にアクセス。

http://localhost:8025
にアクセス。無事確認できました◎

XAMPP環境下では、MailHogの実装ができなかったので嬉しい!!

 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【環境構築】dockerでvue.js+Typescript+vuetify+express+Sequelizeの環境構築

dockerファイル作成

ディレクトリを作成

console
mkdir node
console
cd node
vim Dockerfile

node.jsを準備

dockerファイルを作成し、そこから各種プログラムを実行できるようにする。
ここではexpress,suquelize-cliの実行環境の構築ができるよう記載。

Dockerfile
FROM node:12.13
RUN npm install -g express-generator sequelize-cli

FROM nodeでノードのベースイメージ
RUNコマンドでnpm installを実行しexpressとsepulize-cliをインストールするコマンド

docker イメージを作成

これによりDockerfileが実行される。

console
docker build node/. -t serverapp:latest

-tオプションを付けていることで、名前(serverapp)とタグ名(latest)を指定している。
スクリーンショット 2020-02-29 15.02.42.png

docker run -itd --rm --name serverapp -v $PWD/node:/node serverapp:latest

オプションの説明

-itd コンテナを継続的に動かすために必要
--rm コンテナ終了時自動的に削除。
--name serverappというコンテナ名前で作成
-v ホスト側のディレクトリ:コンテナ側のマウントポイント
今回の場合は$PWDで現在いるディレクトリの/nodeがホスト側、/node serverapp:latestがマウントポイントとなる。

express,sequelizeをインストールする

ドッカーコンテナにログインしexpressをインストールしていきます。
コンテナにロングインする。

console
docker exec -it serverapp /bin/bash

docker exec -it <コンテナ名>/bin/bash コンテナにログイン
-it コンテナを継続的に動かすために必要

root
cd /node
express .

destination is not empty, continue?(空ファイルじゃないけど大丈夫?)と聞かれますが、中にはDockerfaileがあるだけなので[y]で続行する。
スクリーンショット 2020-02-29 16.21.32.png

sequelizeなどの準備

ここで色々必要になるものの準備を行います。

root
npm install --save sequelize sqlite3 cors nodemon
npm install
それぞれ簡単解説

ここでは詳しい説明はしませんが、別記事をそれぞれ作成しようと思います。

名称 説明
sequelize データベースを管理するツール
sqlite3 簡易版データベース
cors セキュリティ上のルール
nodemon 自動でサーバーを再起動してくれるツール

これでローカルフォルダのnode/にファイルが作成できたはず。

sequelizeをセットアップ

root
sequelize init

スクリーンショット 2020-02-29 16.54.25.png

ターミナルに戻る

root
exit

node/config/config.jsonの記載を変更する。
変更点
database mysql  → sqliteへの変更
storage "./data/development.sqlite3"の記載をそれぞれに追加

config.config.json
{
  "development": {
    "username": "root",
    "password": null,
    "database": "database_development",
    "host": "127.0.0.1",
    "dialect": "sqlite",
    "storage": "./data/development.sqlite3",
    "operatorsAliases": false
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "sqlite",
    "storage": "./data/test.sqlite3",
    "operatorsAliases": false
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "sqlite",
    "storage": "./data/production.sqlite3",
    "operatorsAliases": false
  }
}

nodeファイルの中にconfig.jsonで指定したdataファイルを作成。

console
mkdir node/data

もう一度dockerコンテナにログイン

console
docker exec -it serverapp /bin/bash

Unable to resolve sequelize package in
sequleize model:createコマンドを使いデータベースに雛形を作成
sequelize model:createコマンドとは?

root
sequelize model:create --name goal --underscored --attributes goalname:string

Unable to resolve sequelize package inのエラーが出る場合はこちら

マイグレートする

そもそもマイグレートとは、アプリケーションで使うデータベースの定義を自動的に作成・管理する機能です。

root
sequelize db:migrate

マイグレートが完了したら一度dockerを停止する。

terminal
docker stop serverapp

vueの準備

ディレクトリを作成

console
mkdir vue
console
vim vue/Dockerfile

作成したfrontapp内のDockerfileに以下の記述をする。
ここではvue/cliの実行環境の構築ができるよう記載。

Dockerfile
FROM node:12.13
RUN npm install -g @vue/cli

Dockerfileを元にコンテナイメージを作成。起動し、ローカルのフォルダをマウント。

console
docker build vue/. -t frontapp:latest

スクリーンショット 2020-03-08 05.19.39.png

console
docker run -itd --rm --name frontapp -v $PWD/vue:/vue frontapp:latest

docker run コマンドが正常に動いているか確認。

console
docker ps

https://www.dropbox.com/s/d767il7jbm3yprk/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202020-03-08%2005.21.25.png?dl=0

console
docker exec -it frontapp /bin/bash

コンテナにログイン後以下を実行する。

root
cd /vue
vue create frontapp

スクリーンショット 2020-03-08 05.21.33.png

以下のように標準のyarnか高速のnpmどちらかで実行
npmで実行したいため[y]を選択する。

root
Your connection to the default yarn registry seems to be slow.
Use https://registry.npm.taobao.org for faster installation?

以下のメッセージに従いvueをインストールしていく。
今回はマニュアルでtypescriptなどもインストールしていく。

スクリーンショット 2020-03-08 05.33.47.png
スクリーンショット 2020-03-08 05.34.35.png

これでvueのインストールは完了です。

vuetifyの環境

frontappに移動し、vuetifyのプラグインを追加する。
インストールはデフォルトで行った。
インストール終了後はexitで一度コンテナからログアウトする

console
root@701c15dfea18:/# cd vue/frontapp
root@701c15dfea18:/vue/frontapp# vue add vuetify
exit

docker-compose.ymlファイルを準備する。

Node.jsとVue.jsそれぞれのコンテナを起動する際、composeファイルがあると起動/終了が楽なので、
docker-compose.ymlを下記のように記入。

docker-compose.yml
version: "3"
services:
  node:
    build: node/.
    volumes:
      - ./node:/node
    working_dir: /node
    command: ["npm", "start"]
    ports:
      - "3000:3000"
  vue:
    build: vue/.
    volumes:
      - ./vue:/vue
    working_dir: /vue/frontapp
    command: ["npm", "run", "serve"]
    ports:
      - "8080:8080"

一度バックグランドで実行しそれぞれ表示を確認する。

docker-compose up -d
# コンテナ終了は docker-compose down

localhost:8080でアクセス
スクリーンショット 2020-02-24 23.31.55.png
localhost:3000でアクセス
スクリーンショット 2020-02-24 23.34.35.png

参考記事

とてもお世話になりました。ありがとうございました:bow:
Vue.js + Express + Sequelize + DockerでCRUD搭載のTodoリストを作ってみる
【環境構築】Docker + Rails6 + Vue.js + Vuetifyの環境構築手順
GitHub PagesにDocker+Vue.js+Vuetifyでページを公開

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Memory不足でElasticSearchのDockerコンテナ立ち上げが「exited with code 137」で終了したときの対応方法

DockerでElasticSearchのコンテナを起動しようとしたら起動できなくて、「exited with code 137」というエラー文を吐いてて原因を調べてたら時間取られたのでメモ。

DockerでElasticSearchのコンテナを起動しようとしてエラー

$ docker-compose up <container名>
...
<container名> exited with code 137

原因

メモリの上限値が少ないからOut Of Memoryエラーになっているよう。
Dockerの初期設定のメモリは2GBになっているけど、ElasticSearchのコンテナはそれ以上にメモリ使うのかな。

cf. https://github.com/10up/wp-local-docker/issues/6

対応方法

Dockerのメモリ設定の上限値を上げる。2GBから8GBに上げたら起動できた。
(4GBでも起動できた)

Screen Shot 2020-03-24 at 17.10.44.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Veritas Flexアプライアンス上のNetBackupインスタンスをアップグレードする手順

1. はじめに

本稿ではVeritas FlexアプライアンスのNetBackupインスタンスをアップグレードする方法についてご紹介します。
FlexアプライアンスはDockerコンテナを基盤としてNetBackupを構築しているため、非常に可搬性・アジリティに優れており、ブラウザインターフェースから短時間(5分~10分程度で完了)、且つ容易な操作でバージョンアップ作業を完了することができます。

2. Veritas Flex アプライアンス とは?

Veritas Flex アプライアンスは Docker コンテナの環境を利用することで複数からなるNetBackupインスタンスを1つのFlexアプライアンスに統合可能です。Flexに統合することでマルチドメイン環境の管理を可能にし、運用効率性の向上、コスト(Capex, Opex)の最適化を実現します。現在、Flex 5150(Edge・SMB環境向け)、Flex5340(統合バックアップ・Enterprise環境向け)がラインナップされています。
image.png

3. インスタンスのアップグレード方法

それでは、Flexソフトウェア上にあるNetBackupインスタンスをアップグレードする方法についてご紹介します。

① 左側のFlex アプライアンス コンソールのインターフェイスから、[SystemTopology] のアイコンをクリックします。
image.png

② アップグレードするインスタンスをクリックします。
image.png

③ [Manage]から[Upgrade Instance]を選択します。
image.png

④ メッセージを確認し、[Continue]をクリックします。
image.png

⑤ アップグレードしたいバージョンを選択し、[Precheck]を押下します。
 互換性のプリチェックが行われます。
image.png

⑥ [Precheck was successful]のメッセージが表示されます。Precheckが完了したら、[Next]ボタンを押下します。
image.png

⑦ Smart Meter Customer Registration Keyを入力します。
 ※Smart Meter Customer Registration KeyとはSmart Meterを利用するため必要なファイルです。Registration KeyはSmart Meterポータル(https://taas.veritas.com/) から入手可能です。
image.png

⑧ [Upgrade]ボタンを押下します。
image.png

⑨ アップデートが実行され、進捗状況が表示されます。
 5分~10分程度で作業は完了します。
image.png

⑩ [Progress]が100%, [Status]がSuccessfulとなりました。
image.png

⑪ アップグレードを確定したい場合、[Manage]>[Upgrade Instance]>[Commit]を選択します。 元のバージョンに戻したい場合はRolebackをクリックします。(Commit前で、且つ24時間以内であればロールバックが可能です)
image.png

⑫ Commitを選択した場合、メッセージが表示されますのでCommitをクリックしてアップグレード完了となります。

image.png

<参考> RoleBackを選択した場合、以下が表示されますのでRoleBackボタンを押下します。
image.png

<参考> ロールバックステイタスが表示されます。[Status]にSuccessfulが表示されたらロールバック完了になります。
image.png

4. まとめ

いかがだったでしょうか、通常のNetBackupのアップグレードの場合、1~2時間程度かかる作業工程が、GUIの直観的な操作でわずか5分~10分程度で簡単にアップグレードできます。また、アップグレード作業後であっても24時間以内であれば元のバージョンに戻せますのでなにか手違いがあっても安心です。
バックアップの運用負荷、煩雑な管理でお悩みの方は活用を検討されてみてはいかがでしょうか。

商談のご相談はこちら

本稿からのお問合せをご記入の際には「お問合せ内容」に#GWCのタグを必ずご記入ください。ご記入いただきました内容はベリタスのプライバシーポリシーに従って管理されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GAMESSプログラムで第一原理計算を実行可能なDockerコンテナを作る方法

はじめに

こんにちは,(株)日立製作所 研究開発グループ サービスコンピューティング研究部の露木です。

材料開発にAI・機械学習を適用する際に常に問題となるのがデータ数の少なさです。材料候補の物質を変えながら,大量の実験を繰り返して教師データとなる物性値を測定することは困難であり,少量の実験値から学習を行わなければなりません。しかし幸運なことに,計算化学・計算物理学の理論にもとづいてシミュレーション計算を行えば,第一原理的に物性値を得ることができます。このようにデータセットを補う観点から,機械学習を適用する立場でもシミュレーション計算を実行できる環境を整えることは非常に重要といえます。

本記事でとりあげるGAMESSプログラムは,量子力学にもとづいた繰り返し計算により電子の波動関数を計算し,分子の物性を計算するためのプログラムです。GAMESSはアイオワ州立大学により開発されているプログラムであり,理論化学・計算化学分野における有力なプログラムといえます。GAMESSプログラムは主要な波動関数法 (HF法やMP2法,結合クラスター法,配置間相互作用法など) や 密度汎関数法 (DFT) ベースの電子状態計算,相対論補正や振動状態計算等にも対応しています。同種のプログラムにはGAUSSIANMOLPROTURBOMOLE などがあります。ご興味ある方は,これらプログラムと比較検討してみてください。

本稿ではGAMESSを利用して第一原理的な電子状態計算を行うことを目標に,Dockerコンテナとして実行環境を構築する方法を記します。

手順

コンパイル環境の準備

インターネットには DrSnowbird/hpc-app-gamesssaromleang/docker-gamess などのGAMESS用のDockerfileが既に公開されています。しかし,これらには開発者の環境 (大学) に固有のコードが入っていますので,そのままでは使えません。そこで,今回は汎用的なDockerfileを新たに作りました。

まずは,このリポジトリを clone してDockerfileを入手してください。

git clone <リポジトリのURL> (注. 現時点では未公開。準備完了したら公開します)

次に,GAMESSの公式サイトからそのソースコード gamess-current.tar.gz を入手します。Gordon Group/GAMESS Homepage をWebブラウザで開いてライセンスに合意できるか確認したら,メールアドレスを登録してください。大抵は数時間以内にダウンロード用のURLとパスワードがメールで送付されます。

gamess-current.tar.gz を入手したら解凍します。

tar xzf gamess-current.tar.gz

次に,GAMESSのコンパイルオプションを対話的に生成していきます。まずはその準備としてbaseコンテナをビルドします。

docker-compose build base

baseコンテナを起動して,シェルにログインします。

docker run --rm -it -v `pwd`/gamess:/opt/gamess base /bin/bash

gamess-current.tar.gz に同梱されている config スクリプトを実行してコンパイルオプションを生成します。 config スクリプトを実行したら,質問が表示されていきますので環境とお好みに合わせて設定してください。

./config

ご参考までに先程のDockerfileで動作確認済みの設定例を以下に記します。

質問1

まず,configスクリプトを実行後,最初に表示される質問は単なるイントロダクションなので,読み終わりましたらEnterキーを押してください。

This script asks a few questions, depending on your computer system,
to set up compiler names, libraries, message passing libraries,
and so forth.

You can quit at any time by pressing control-C, and then <return>.

Please open a second window by logging into your target machine,
in case this script asks you to 'type' a command to learn something
about your system software situation.  All such extra questions will
use the word 'type' to indicate it is a command for the other window.

After the new window is open, please hit <return> to go on.

質問2

2つ目の質問はコンパイルする環境を選ぶものです。今回は 64-bit CPU のLinux として linux64 を選択します。

After the new window is open, please hit <return> to go on.
   GAMESS can compile on the following 32 bit or 64 bit machines:
axp64       - Alpha chip, native compiler, running Tru64 or Linux
cray-xt     - Cray's massively parallel system, running CNL
cray-xc     - Cray's XC40, with KNL nodes
hpux32      - HP PA-RISC chips (old models only), running HP-UX
hpux64      - HP Intel or PA-RISC chips, running HP-UX
ibm32       - IBM (old models only), running AIX
ibm64       - IBM, Power3 chip or newer, running AIX or Linux
ibm64-sp    - IBM SP parallel system, running AIX
ibm-bg      - IBM Blue Gene (Q model), these are 64 bit systems
linux32     - Linux (any 32 bit distribution), for x86 (old systems only)
linux64     - Linux (any 64 bit distribution), for x86_64 or ia64 chips,
              using gfortran, ifort, or perhaps PGI compilers.
mac32       - Apple Mac, any chip, running OS X 10.4 or older
mac64       - Apple Mac, any chip, running OS X 10.5 or newer
sgi32       - Silicon Graphics Inc., MIPS chip only, running Irix
sgi64       - Silicon Graphics Inc., MIPS chip only, running Irix
sun32       - Sun ultraSPARC chips (old models only), running Solaris
sun64       - Sun ultraSPARC or Opteron chips, running Solaris
win32       - Windows 32-bit (Windows XP, Vista, 7, Compute Cluster, HPC Edition)
win64       - Windows 64-bit (Windows XP, Vista, 7, Compute Cluster, HPC Edition)
winazure    - Windows Azure Cloud Platform running Windows 64-bit
singularity - GAMESS Singularity container image

    type 'uname -a' to partially clarify your computer's flavor.

please enter your target machine name: 

質問3

3つ目の質問はGAMESSのソースコードが置かれている場所を問うものです。正しい答えは config スクリプトが保存されているカレントディレクトリですので,そのままEnterキーを押してください。

Where is the GAMESS software on your system?
A typical response might be /u1/mike/gamess,
most probably the correct answer is /opt/gamess

GAMESS directory? [/opt/gamess]

質問4

4つ目の質問はコンパイルしたGAMESSプログラムを配置するディレクトリです。こちらもそのままEnterキーを押してください。

Setting up GAMESS compile and link for GMS_TARGET=linux64
GAMESS software is located at GMS_PATH=/opt/gamess

Please provide the name of the build locaation.
This may be the same location as the GAMESS directory.

GAMESS build directory? [/opt/gamess]

質問5

5個目の質問はGAMESSのバージョンを問うものです。このバージョンはコンパイルする人が区別するために決めるものなので,任意の値で構いません。ここではデフォルトの 00 のままEnterキーを押します。

Please provide a version number for the GAMESS executable.
This will be used as the middle part of the binary's name,
for example: gamess.00.x

Version? [00]

質問6

6つ目の質問はコンパイラを選択するものです。今回は gfortran と入力して Enterキーを押します。今回のDockerfileでは利用不可能ですが,もしもお手元に intelのfortranコンパイラ (ifort) のライセンスがあり,利用可能であればそちらのほうが高速になります。

Linux offers many choices for FORTRAN compilers, including the GNU
compiler suite's free compiler 'gfortran', usually included in
any Linux distribution.  If gfortran is not installed, it can be
installed from your distribution media.

To check on installed GNU compilers, for RedHat/SUSE style Linux,
   type 'rpm -aq | grep gcc' for both languages,
and for Debian/Ubuntu style Linux, it takes two commands
   type 'dpkg -l | grep gcc'
   type 'dpkg -l | grep gfortran'

There are also other compilers (some commercial), namely Intel's 'ifort',
Portland Group's 'pgfortran', Pathscale's 'pathf90', AMD's 'AOCC',
and ARM's armflang.
The last four are not common, and aren't as well tested.

type 'which gfortran'  to look for GNU's gfortran   (a good choice),
type 'which ifort'     to look for Intel's compiler (a good choice),
type 'which pgfortran' to look for Portland Group's compiler,
type 'which pathf90'   to look for Pathscale's compiler.
type 'which aocc'      to look for AMD's compiler.
type 'which armflang'  to look for ARM compiler.
Please enter your choice of FORTRAN:

質問7

次の質問は gfortran のバージョンを問うものです。 gfortran -v コマンドを実行すればバージョンを確認できますが,今回のDockerfileであれば 7.4 を入力してEnterキーを押してください。

Linux offers many choices for FORTRAN compilers, including the GNU
compiler suite's free compiler 'gfortran', usually included in
any Linux distribution.  If gfortran is not installed, it can be
installed from your distribution media.

To check on installed GNU compilers, for RedHat/SUSE style Linux,
   type 'rpm -aq | grep gcc' for both languages,
and for Debian/Ubuntu style Linux, it takes two commands
   type 'dpkg -l | grep gcc'
   type 'dpkg -l | grep gfortran'

There are also other compilers (some commercial), namely Intel's 'ifort',
Portland Group's 'pgfortran', Pathscale's 'pathf90', AMD's 'AOCC',
and ARM's armflang.
The last four are not common, and aren't as well tested.

type 'which gfortran'  to look for GNU's gfortran   (a good choice),
type 'which ifort'     to look for Intel's compiler (a good choice),
type 'which pgfortran' to look for Portland Group's compiler,
type 'which pathf90'   to look for Pathscale's compiler.
type 'which aocc'      to look for AMD's compiler.
type 'which armflang'  to look for ARM compiler.
Please enter your choice of FORTRAN: gfortran

gfortran is very robust, so this is a wise choice.

Please type 'gfortran -dumpversion' or else 'gfortran -v' to
detect the version number of your gfortran.
This reply should be a string with at least two decimal points,
such as 4.9.4 or 6.3.0.
The reply may be labeled as a 'gcc' version,
but it is really your gfortran version.

Please enter only the first decimal place, such as 4.9:

質問8

このコメントはgfortarnのバージョンが適合していることを教えてくれているだけですので,そのままEnterキーを押して次に進んでください。

   Good, the newest gfortrans can compile REAL*16 data type.
   Please report any numerical issues you encounter.
hit <return> to continue to the math library setup.

質問9

数値計算を高速化するためのライブラリとして,何をつかうか質問されています。今回は atlas をつかうので, atlas と入力してから Enterキーを押してください。

Linux distributions do not include a standard math library.

There are several reasonable add-on library choices,
       MKL from Intel           for 32 or 64 bit Linux (very fast)
      ACML from AMD             for 32 or 64 bit Linux (free)
     LibFLAME from AMD          for 64 bit Linux (free)
     ATLAS from www.rpmfind.net for 32 or 64 bit Linux (free)
  PGI BLAS from Portland Group  for 32 or 64 bit Linux
     ArmPL from ARM             for 64 bit Linux
and one very unreasonable option, namely 'none', which will use
some slow FORTRAN routines supplied with GAMESS.  Choosing 'none'
will run MP2 jobs 2x slower, or CCSD(T) jobs 5x slower.

Some typical places (but not the only ones) to find math libraries are
Type 'ls /opt/intel/mkl'                 to look for MKL
Type 'ls /opt/intel/Compiler/mkl'        to look for MKL
Type 'ls /opt/intel/composerxe/mkl'      to look for MKL
Type 'echo $MKLROOT'                     to look for MKL
Type 'ls -d /opt/acml*'                  to look for ACML
Type 'ls -d /usr/local/acml*'            to look for ACML
Type 'ls /usr/lib64/atlas'               to look for Atlas
Type 'ls /opt/pgi/linux86-64/*/lib/*     to look for libblas.a from PGI
Type 'ls /opt/pgi/osx86-64/*/lib/*       to look for libblas.a from PGI
Type 'echo $ARMPL_DIR'                   to look for ArmPL

Enter your choice of 'mkl' or 'atlas' or 'acml' or 'libflame' or 'openblas' or 'pgiblas' or 'armpl' or 'none':

質問10

次の質問は atlas をインストールしたディレクトリを問うものです。 /usr/lib/x86_64-linux-gnu/atlas を入力して Enterキーを押してください。

Where is your Atlas math library installed?  A likely place is
   /usr/lib64/atlas

  **BOLT: /shared/math/atlas/3.10.3-gnu-4.8.5-skylake
  **BOLT: /shared/math/atlas/3.10.3-gnu-4.8.5-amd-epyc
  **BOLT: /shared/math/atlas/3.10.3-gnu-4.8.5-skylake-lapack
  **BOLT: /shared/math/atlas/3.10.3-gnu-4.8.5-amd-epyc-lapack
Please enter the Atlas subdirectory on your system:

質問11

この質問は注意事項を述べているだけですので,そのままEnterを押します。

The linking step in GAMESS assumes that a softlink exists
within the system's /usr/lib/x86_64-linux-gnu/atlas
   from libatlas.so   to a specific file like libatlas.so.3.0
   from libf77blas.so to a specific file like libf77blas.so.3.0
config can carry on for the moment, but the 'root' user should
   chdir /usr/lib/x86_64-linux-gnu/atlas
   ln -s libf77blas.so.3.0 libf77blas.so
   ln -s libatlas.so.3.0   libatlas.so
prior to the linking of GAMESS to a binary executable.

Math library 'atlas' will be taken from /usr/lib/x86_64-linux-gnu/atlas

please hit <return> to compile the GAMESS source code activator

質問12

このメッセージは自動設定用プログラムのコンパイルに成功したことを示すものです。そのままEnterを押してください。

gfortran -o /opt/gamess/tools/actvte.x actvte.f
unset echo
Source code activator was successfully compiled.

please hit <return> to set up your network for Linux clusters.

質問13

並列計算の方法に関する質問です。今回はDockerであり,ノード間並列計算 (mpi) の利用は困難ですので単一のノードに閉じた並列計算のみを有効化します。sockets を入力してEnterキーを押してください。

If you have a slow network, like Gigabit Ethernet (GE), or
if you have so few nodes you won't run extensively in parallel, or
if you have no MPI library installed, or
if you want a fail-safe compile/link and easy execution,
     choose 'sockets'
to use good old reliable standard TCP/IP networking.

If you have an expensive but fast network like Infiniband (IB), and
if you have an MPI library correctly installed,
     choose 'mpi'.

If you wish to use a combination of TCP/IP networking for small
messages and MPI for large messages in a 'mixed' fashion,
     choose 'mixed'.

communication library ('serial','sockets' or 'mpi' or 'mixed')?

質問14

クラスター結合法に関するベータ版のコードをコンパイルするかという質問です。時間がかかる上に収束が難しい計算手法ですし,コンパイルも異様に遅くなりますから特別な事情が無い限りは no にしておきます。

Users have the option of compiling the beta version of the
active-space CCSDt and CC(t;3) codes developed at Michigan
State University (CCTYP = CCSD3A and CCT3, respectively).

These builds take a considerable amount of time and memory for
compilation due to the amount of machine generated source code.
We recommend that users interested in installing these codes
compile GAMESS in parallel using the Makefile generated during
the initial configuration ('make -j [number of cores]').

This option can be manually changed later by modifying install.info

Optional: Build Michigan State University CCT3 & CCSD3A methods?  (yes/no):

質問15

最後の質問はGPUによる高速化ライブラリの利用に関する質問です。今回はGPUを利用する予定は無いので no を選びます。

64 bit Linux and IBM builds can attach a special LIBCCHEM code for fast
MP2 and CCSD(T) runs.  The LIBCCHEM code can utilize nVIDIA GPUs,
through the CUDA libraries, if GPUs are available.
Usage of LIBCCHEM requires installation of HDF5 I/O software as well.
GAMESS+LIBCCHEM binaries are unable to run most of GAMESS computations,
and are a bit harder to create due to the additional CUDA/HDF5 software.
Therefore, the first time you run 'config', the best answer is 'no'!
If you decide to try LIBCCHEM later, just run this 'config' again.

Do you want to try LIBCCHEM?  (yes/no):

GAMESSプログラムのコンパイル

すべての質問に答えたら,GAMESSプログラムを含むDockerイメージをビルドします。下記コマンドを実行するとGAMESSプログラムをソースコードからコンパイルするため,しばらく時間がかかります。

docker-compose build docker-gamess

おわりに

以上でコンパイルは終わりです。環境準備はできましたので,いよいよGAMESSを使えるようになりましたが記事が長くなってしまいましたので,使い方は次回に解説します。

参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MacOS の VirtualBox Ubntu上にDocker環境を構築

MacOS の VirtualBox Ubntu上にDocker環境を構築

下記の手順で作業を行います。
1. MacOS上のVirtualBoxにゲストOSとしてUbntuをインストール
2. ゲストOSのUbntu上にDocker環境を構築

MacOS の VirtualBox にUbntuをゲストOSとしてインストール

VirtualBoxを入手する

VirtualBox binaries
https://www.virtualbox.org/wiki/Downloads

Macの場合は、「OS X Hosts」をクリックします。

スクリーンショット 2020-03-21 18.51.31.png

Ubuntuを入手してインストール

Ubuntu Desktop 18.04.4 LTS
https://jp.ubuntu.com/download

Virtualboxのホストオンリーネットワークを作成

ホストOSで通信するために、Virtualboxのホストオンリーネットワークを作成します。
ホストOSの設定ではないのでクリックする場所に注意。
スクリーンショット 2020-03-23 17.20.37.png

クリックするだけで設定等は自動で行われます。
自動入力される設定の例(環境によって設定内容は変わります、デフォルトのままでOK、修正不要):
スクリーンショット 2020-03-23 17.20.55.png

DHCPの設定も勝手に入ると思います。

スクリーンショット 2020-03-23 17.21.47.png

VirtualboxのゲストOS用ネットワークを作成

今度はゲストOS用のネットワーク設定です。クリックする場所に注意。
スクリーンショット 2020-03-23 17.32.13.png

ネットワーク の アダプター2 に 割り当て ホストオンリーアダプター を選択します。
名前 で、先ほど作成したホストオンリーアダプターを選択できるはずです。
スクリーンショット 2020-03-23 17.33.30.png

ゲストOS(Ubuntu)でインターネット通信が可能かを確認

Firefox(Webブラウザ)等で、インターネットに接続可能になったことを確認してください。

ゲストOS(Ubuntu)のアップデート

アップデートを行います。

sudo apt update
sudo apt upgrade

Net-toolsのインストール

IPの確認にifconfigを利用したいので、インストールしておきます。

sudo apt-get install net-tools

sshのインストール

ホストOSからゲストOSへの作業時にsshを利用したいので、インストールしておきます。
セキュリティ対策等は各自でお願いします。

sudo apt-get install ssh
systemctl start sshd

私の環境でのssh例(当然、IPやI/F等は各自の環境で変わってきます)

ゲストOSにログインし、sshが起動していることを確認します。

tagucchan@tagvirtualbox01:~$ ps -ef | grep ssh
tagucch+  1752  1675  0 10:50 ?        00:00:00 /usr/bin/ssh-agent /usr/bin/im-launch env GNOME_SHELL_SESSION_MODE=ubuntu gnome-session --session=ubuntu
root      4945     1  0 10:55 ?        00:00:00 /usr/sbin/sshd -D
root      6000  4945  0 10:57 ?        00:00:00 sshd: tagucchan [priv]
tagucch+  6090  6000  0 10:58 ?        00:00:00 sshd: tagucchan@pts/1
tagucch+  6117  6091  0 11:00 pts/1    00:00:00 grep --color=auto ssh

sshが起動中ですので、ゲストOS(Ubuntu)のIPを調べます。

tagucchan@tagvirtualbox01:~$ ifconfig
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.0.2.15  netmask 255.255.255.0  broadcast 10.0.2.255
        inet6 fe80::3306:660:8dc9:8a3e  prefixlen 64  scopeid 0x20<link>
        ether 08:00:27:e9:85:d9  txqueuelen 1000  (Ethernet)
        RX packets 19747  bytes 19014822 (19.0 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 7266  bytes 488706 (488.7 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

enp0s8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.56.101  netmask 255.255.255.0  broadcast 192.168.56.255
        inet6 fe80::ff08:5ca7:204b:8b9c  prefixlen 64  scopeid 0x20<link>
        ether 08:00:27:88:8e:37  txqueuelen 1000  (Ethernet)
        RX packets 83  bytes 12617 (12.6 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 119  bytes 16465 (16.4 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 254  bytes 20458 (20.4 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 254  bytes 20458 (20.4 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

ホストOSのIPも確認し、通信可能なI/Fは上から2つ目の「enp0s8」、IPが「192.168.56.101」になります。
割愛しますが、ホストOSとゲストOSで「共通のIPではない方のIP」でsshできます。
詳細の説明は、Virtualboxのドキュメントを探して参照してみて下さい。

ホストOS(MacOS)のIP確認

enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.0.2.15  netmask 255.255.255.0  broadcast 10.0.2.255
        inet6 fe80::3306:660:8dc9:8a3e  prefixlen 64  scopeid 0x20<link>
        ether 08:00:27:e9:85:d9  txqueuelen 1000  (Ethernet)
        RX packets 19768  bytes 19016712 (19.0 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 7290  bytes 490675 (490.6 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

enp0s8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.56.101  netmask 255.255.255.0  broadcast 192.168.56.255
        inet6 fe80::ff08:5ca7:204b:8b9c  prefixlen 64  scopeid 0x20<link>
        ether 08:00:27:88:8e:37  txqueuelen 1000  (Ethernet)
        RX packets 208  bytes 25051 (25.0 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 196  bytes 27251 (27.2 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 267  bytes 21659 (21.6 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 267  bytes 21659 (21.6 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

Macからsshしてみます。

tagucchan@tagvirtualbox01:~$ ssh tagucchan@192.168.56.101
The authenticity of host '192.168.56.101 (192.168.56.101)' can't be established.
ECDSA key fingerprint is SHA256:rgLFPu10ZCNlOSnLROddIRtxfxYtqkzn38auX/c1rmk.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.56.101' (ECDSA) to the list of known hosts.
tagucchan@192.168.56.101's password: 
Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 5.3.0-42-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage


 * Canonical Livepatch is available for installation.
   - Reduce system reboots and improve kernel security. Activate at:
     https://ubuntu.com/livepatch

11 個のパッケージがアップデート可能です。
0 個のアップデートはセキュリティアップデートです。

Your Hardware Enablement Stack (HWE) is supported until April 2023.
Last login: Tue Mar 24 10:58:01 2020 from 192.168.56.1
tagucchan@tagvirtualbox01:~$ 

ゲストOSのUbntu上にDocker環境を構築

公式ドキュメントdocker docs 「Install using the repository」通りに作業します。

aptがhttps経由でリポジトリを使用できるようにする

必要なパッケージをインストール。

sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common

Docker’s official GPG key を追加

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

鍵の指紋(finger print)で正しい鍵かをチェック

の指紋(finger print)を確認

sudo apt-key fingerprint 0EBFCD88

以下が表示されればOKです。
「9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88」

出力例:

tagucchan@tagvirtualbox01:~$ sudo apt-key fingerprint 0EBFCD88
pub   rsa4096 2017-02-22 [SCEA]
      9DC8 5822 9FC7 DD38 854A  E2D8 8D81 803C 0EBF CD88
uid           [ unknown] Docker Release (CE deb) <docker@docker.com>
sub   rsa4096 2017-02-22 [S]

リポジトリを追加( ここでは x86_64 / amd64 用 stable )

sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

DOCKER ENGINE - COMMUNITY のインストール

DOCKER ENGINEのCOMMUNITY(コミュニティエディション)をインストールします。

最新に更新しておきます。

sudo apt-get update
sudo apt-get upgrade

DOCKER ENGINE - COMMUNITY のインストール

sudo apt-get install docker-ce docker-ce-cli containerd.io

DOCKER ENGINEの動作確認

動作するか hello-world で確認します。

sudo docker run hello-world

実行例:
Hello from Docker!
This message shows that your installation appears to be working correctly.
の表示が出ればOKです。

tagucchan@tagvirtualbox01:~$ sudo docker run hello-world
[sudo] password for tagucchan: 
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete 
Digest: sha256:f9dfddf63636d84ef479d645ab5885156ae030f611a56f3a7ac7f2fdd86d7e4e
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

tagucchan@tagvirtualbox01:~$ 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Windows] Dockerを使用してMosquittoでMQTTサーバを構築する際に別コンテナにloalhostで接続できるようにする

TL;DR

  • Mosquittoを使ってMQTTサーバを立てた際にコンテナ間でlocalhostで接続できないかと調査
  • docker-composeでnetwork_modeを「host」にすることでホスト端末と同じIPアドレスにすることで可能とわかった
  • GO言語のpaho.mqtt.golangライブラリを使って接続テストを実施

環境

  • Windows10
  • Docker Desktop 2.2.0.3
  • docker-composeはDocker Desktopに同梱
  • visual studio code 1.42.1[拡張機能Remote - Containers使用]
  • Mosquitto 1.6.9

完成したリポジトリ

https://github.com/MegaBlackLabel/mqtt-docker-sample

ファイル

docker-compose.tml
version: '3'

services:
    api:
        build:
            dockerfile: Dockerfile
            context: ./containers/api
        volumes:
            - ./containers/api/src:/go/api
        tty: true
        network_mode: "host"
    mqtt:
        build:
            dockerfile: Dockerfile
            context: ./containers/mqtt
        ports: 
            - "1883:1883"
        volumes:
            - mosquittodata:/mosquitto/data
            - mosquittolog:/mosquitto/log
        tty: true
        network_mode: "host"

volumes:
    mosquittodata:
        driver: "local"
    mosquittolog:
        driver: "local"
  • apiコンテナとmqttコンテナにnetwork_modeで「host」を設定することでホスト端末と同じIPアドレスを使用する
main.go
package main

import (
    "crypto/tls"
    "flag"
    "fmt"
    "os"
    "strconv"
    "time"

    MQTT "github.com/eclipse/paho.mqtt.golang"
)

// Que Strut.
type Que struct {
    Server    string
    Sendtopic string
    Resvtopic string
    Qos       int
    Retained  bool
    Clientid  string
    Username  string
    Password  string
    Client    MQTT.Client
    Callback  MQTT.MessageHandler
}

// Connect func .
func (q *Que) Connect() error {
    connOpts := MQTT.NewClientOptions().AddBroker(q.Server).SetClientID(q.Clientid).SetCleanSession(true)
    if q.Username != "" {
        connOpts.SetUsername(q.Username)
        if q.Password != "" {
            connOpts.SetPassword(q.Password)
        }
    }
    tlsConfig := &tls.Config{InsecureSkipVerify: true, ClientAuth: tls.NoClientCert}
    connOpts.SetTLSConfig(tlsConfig)

    client := MQTT.NewClient(connOpts)
    if token := client.Connect(); token.Wait() && token.Error() != nil {
        return token.Error()
    }
    q.Client = client

    if q.Callback != nil {
        if token := q.Client.Subscribe(q.Resvtopic, byte(q.Qos), q.Callback); token.Wait() && token.Error() != nil {
            return token.Error()
        }
        fmt.Printf("[MQTT] Subscribe to %s\n", q.Sendtopic)
    }

    fmt.Printf("[MQTT] Connected to %s\n", q.Server)

    return nil
}

// Publish func .
func (q *Que) Publish(message string) error {
    if q.Client != nil {
        token := q.Client.Publish(q.Sendtopic, byte(q.Qos), q.Retained, message)
        if token == nil {
            return token.Error()
        }
        fmt.Printf("[MQTT] Sent to %s\n", q.Sendtopic)
    }

    return nil
}

// SetSubscribe - .
func (q *Que) SetSubscribe(callback MQTT.MessageHandler) error {
    if callback != nil {
        if token := q.Client.Subscribe(q.Resvtopic, byte(q.Qos), callback); token.Wait() && token.Error() != nil {
            return token.Error()
        }
        fmt.Printf("[MQTT] Subscribe to %s\n", q.Sendtopic)
    }

    return nil
}

func onMessageReceived(client MQTT.Client, message MQTT.Message) {
    fmt.Printf("[MQTT] Received to %s [Received Message: %s]\n", message.Topic(), message.Payload())
}

func main() {
    hostname, _ := os.Hostname()

    server := flag.String("server", "tcp://localhost:1883", "The full URL of the MQTT server to connect to")
    sendtopic := flag.String("sendtopic", "MQTT/Client/Update/TEST", "Topic to publish the messages on")
    resvtopic := flag.String("resvtopic", "MQTT/+/Update/#", "Topic to publish the messages on")
    qos := flag.Int("qos", 0, "The QoS to send the messages at")
    retained := flag.Bool("retained", false, "Are the messages sent with the retained flag")
    clientid := flag.String("clientid", hostname+strconv.Itoa(time.Now().Second()), "A clientid for the connection")
    username := flag.String("username", "", "A username to authenticate to the MQTT server")
    password := flag.String("password", "", "Password to match username")
    flag.Parse()

    q := &Que{
        Server:    *server,
        Sendtopic: *sendtopic,
        Resvtopic: *resvtopic,
        Qos:       *qos,
        Retained:  *retained,
        Clientid:  *clientid,
        Username:  *username,
        Password:  *password,
        Callback:  onMessageReceived,
    }

    err := q.Connect()
    if err != nil {
        fmt.Println(err)
        os.Exit(2)
    }

    if err := q.SetSubscribe(onMessageReceived); err != nil {
        fmt.Println(err)
        os.Exit(2)
    }

    for {
        time.Sleep(5000 * time.Millisecond)
        if err := q.Publish("test massage"); err != nil {
            fmt.Println(err)
            os.Exit(2)
        }
    }
}
  • MQTTサーバの接続先に「tcp://localhost:1883」を指定しているが、お互いのコンテナがnetwork_modeで「host」を設定しているので接続できる

MQTT接続実行結果

実行結果
root@docker-desktop:/go/api# go run main.go 
go: downloading github.com/eclipse/paho.mqtt.golang v1.2.0
go: downloading golang.org/x/net v0.0.0-20200320220750-118fecf932d8
[MQTT] Subscribe to MQTT/Client/Update/TEST
[MQTT] Connected to tcp://localhost:1883   
[MQTT] Subscribe to MQTT/Client/Update/TEST
[MQTT] Sent to MQTT/Client/Update/TEST
[MQTT] Received to MQTT/Client/Update/TEST [Received Message: test massage]
[MQTT] Sent to MQTT/Client/Update/TEST
[MQTT] Received to MQTT/Client/Update/TEST [Received Message: test massage]
[MQTT] Sent to MQTT/Client/Update/TEST
[MQTT] Received to MQTT/Client/Update/TEST [Received Message: test massage]

まとめ

MQTTサーバをDockerで構築する際にサーバをlocalhostにできないかな、というので調査していてnetwork_mode使えばできることがわかったので記事にしてみました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laradockで構築したMySQLの日本語化けを解決

はじめに

LaradockでLaravelの環境構築をしました
ところが、マイグレーションを実行しRegisterを実行しDBを確認したところ
日本語が???と文字化けしてしまっていました
色んな記事を参考に試したものの、どれもうまく行かなかったので
備忘録として残しておきます

初期のDBの設定

まずは初期状態のDBの文字コードの設定は下記でした

+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | latin1                     |
| character_set_connection | latin1                     |
| character_set_database   | utf8                       |
| character_set_filesystem | binary                     |
| character_set_results    | latin1                     |
| character_set_server     | utf8                       |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+

これをutf8mb4に変更していきます

my.cnfの編集

laradock/mysql/my.cnfを下記に書き換えます

my.cnf
 # The MySQL  Client configuration file.
 #
 # For explanations see
 # http://dev.mysql.com/doc/mysql/en/server-system-variables.html

 [mysql]

[mysqld]
skip-character-set-client-handshake
character-set-server = utf8mb4
# collation-server = utf8mb4_general_ci
collation-server = utf8mb4_unicode_ci
init-connect = SET NAMES utf8mb4

[client]
default-character-set=utf8mb4

database.phpの編集

config/database.phpを下記のように変更します

        'mysql' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',    //変更
            'collation' => 'utf8mb4_unicode_ci',    //変更
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ]

編集はこれで終了

buildする

--buildオプションをつけてコンテナを起動します
下記コマンドを実行

$ docker-compose up -d --build workspace nginx mysql

mysqlコンテンに入り文字コードを確認する

mysql> show variables like '%char%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8mb4                    |
| character_set_connection | utf8mb4                    |
| character_set_database   | utf8                       |
| character_set_filesystem | binary                     |
| character_set_results    | utf8mb4                    |
| character_set_server     | utf8mb4                    |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+

utf8mb4に変更することができました

DBの文字化けしていた部分を確認すると、日本語で表示されていることが確認できました

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コーディング未経験のPO/PdMのためのRails on Dockerハンズオン、Rails on Dockerハンズオン vol.12 - TDDでPost機能をコーディング part1 -

はじめに

お待たせしました!第12回にしてやっとつぶやき機能を実装してまいります!ここではPost機能といいますね。

前回のソースコード

前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。

どんなの実装するの?

最初に今回のゴールを見据えておきましょう。

画面遷移

screen_transition_diagram.png
新たにポストページを作ります。
ポストページはポストを投稿するフォームと、今までのユーザー全員のポストが投稿日時降順で表示される機能を具備しています。
ポストページはサインイン済のユーザーしかアクセスできません。
ポストの投稿ユーザーをクリックしたらそのポストのプロフィールページに遷移できます。
ユーザーのプロフィールページでは、そのユーザーの過去のポストが投稿日時降順で表示されています。

ER図

entity_relation_diagram.png
user:post = 1:nの関係です。

テストシナリオ

さて、これを踏まえて今回のテストシナリオを考えてみましょう。

  1. 未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること
  2. サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること
  3. 未サインインのユーザーは、トップページでヘッダーにポストページへのリンクを見つけられないこと
  4. 未サインインのユーザーは、サインアップページでヘッダーにポストページへのリンクを見つけられないこと
  5. 未サインインのユーザーは、サインインページでヘッダーにポストページへのリンクを見つけられないこと
  6. 未サインインのユーザーは、ユーザー詳細ページでヘッダーにポストページへのリンクを見つけられないこと
  7. サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
  8. サインイン済のユーザーが、ユーザー詳細ページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
  9. サインイン済のユーザーが、ポストページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
  10. サインイン済のユーザーは、ポストページでポストを入力できること
  11. ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
  12. ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
  13. ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
  14. サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること
  15. サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
  16. 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  17. 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  18. サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  19. サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  20. サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
  21. サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

こんな感じでしょう。
では元気にデベロップしてまいりましょう!今回はTDDで開発をしていくので、テストコードを書いて、アプリコードを書いて、を繰り返していきます。

開発スタート

まずは、それぞれのテストシナリオをテストコードに落とし込みましょう。
開発はTDDで進めるので

  1. テストをコーディングする(Red)
  2. テストコードがパスするようにアプリケーションをコーディングする(Green)
  3. 非効率な記述があれば、Greenをキープしながらコーディングし直す(Refectoring)

です。

ではコンテナを起動しておきましょう!

$ docker-compose up -d
$ docker-compose exec web ash

未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること

ポストページについては以下の仕様で実装していきます。ポストページのURLパスは/postsとします。

まずはテストコードを書いていきます。今回のテストシナリオ用に新しいテストシナリオファイルを作りましょう。

# touch spec/system/07_posts_spec.rb
07_posts_spec.rb
feature "ユーザーとして、ポストを投稿したい", type: :system do
  scenario "未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること" do
    # ポストページにアクセスする
    visit posts_path

    # 現在のパスがトップページのパスであることを検証する
    expect(current_path).to eq root_path
  end
end

ポストページへのルーティングの名前付きルートをposts_pathとしています。
今までも何度か書いてきましたが、posts_pathにアクセスしようとしたのに現在のパスはroot_pathです、というリダイレクトの検証です。

この時点ではアプリケーションのコーディングを行っていないのでテストは失敗します。

# rspec spec/system/07_posts_spec.rb
...
Failures:

  1) ユーザーとして、ポストを投稿したい 未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること
     Failure/Error: visit posts_path

     NameError:
       undefined local variable or method `posts_path' for #<RSpec::ExampleGroups::Nested:0x0000555f60b1ccf8>

     # ./spec/system/07_posts_spec.rb:3:in `block (2 levels) in <main>'

Finished in 2.41 seconds (files took 8.4 seconds to load)
1 example, 1 failure
...

RSpecではこのような形でテストの失敗の理由が示されるので、それが解消されるようにアプリケーションをコーディングしていきましょう。
今回はposts_pathという変数もしくはメソッドがアプリケーションに定義されていないことがテスト失敗の理由として示されているので、まずはルーティングの設定をする必要がありそうです。

rails g controllerコマンドでポストページに必要な設定・ファイルを揃えましょう。

# rails g controller posts index

今回はポストページのためのアクションとしてposts#indexを用意することにしました。
ルーティングを定義します。

config/routes.rb
  Rails.application.routes.draw do
-   get 'posts/index'
    root 'static_pages#home'

    get   '/sign_up', to: 'users#new',    as: :sign_up
    post  '/sign_up', to: 'users#create', as: :create_user
    resources :users, only: [:show]

    get     '/sign_in',   to: 'sessions#new',     as: :sign_in
    post    '/sign_in',   to: 'sessions#create',  as: :create_session
    delete  '/sign_out',  to: 'sessions#destroy', as: :sign_out
+
+   get '/posts', to: 'posts#index', as: :posts
  end

これで名前付きルートposts_pathが定義されたのでテスト結果が変わるはずです。もう一度テストを実行してみましょう。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい 未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること
     Failure/Error: expect(current_path).to eq root_path

       expected: "/"
            got: "/posts"

       (compared using ==)



     # ./spec/system/07_posts_spec.rb:5:in `block (2 levels) in <main>'

Finished in 7.94 seconds (files took 16.58 seconds to load)
1 example, 1 failure

以前テストは失敗していますが、エラー理由が変わっていますね。
今回は/にリダイレクトされることが期待されていたけど/postsにアクセスできてしまっていることがテスト失敗の理由のようです。
今コントローラーで何も制御をしていないので誰でもポストページにアクセスできてしまいますね。
では、未サインインのユーザーがposts#indexにルーティングされた場合、root_pathにリダイレクトするようにアプリをコーディングしていきましょう!

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    # 未サインインの場合、トップページにリダイレクトする
+   redirect_to root_path unless signed_in?
  end
end

以前、サインイン済ならプロフィールページにリダイレクトさせる、という機能を作りましたね。今回はそれの条件が逆バージョンです。
今回使っているunlessifの逆の分岐です。つまり、trueの場合は何もなし、falseの場合に実行する、という挙動をとります。

またテストを実行してみましょう!

# rspec spec/system/07_posts_spec.rb

Finished in 4.35 seconds (files took 7.28 seconds to load)
1 example, 0 failures

テストがパスしました!
では次のテストシナリオの実装に移りましょう!

サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること

まずはテストシナリオをコーディングします。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること" do
      # テストシナリオ用のユーザーを作成
+     user = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
+
      # サインインページにアクセスする
+     visit sign_in_path
      # サインインページで作成したユーザーのメールアドレスを入力する
+     fill_in :user_email, with: user.email
      # サインインページで作成したユーザーのパスワードを入力する
+     fill_in :user_password, with: user.password
      # サインインボタンをクリックする(サインインする)
+     click_on :sign_in_button
+     
      # ポストページにアクセスする
+     visit posts_path
+ 
      # 現在のページがポストページであることを検証する
+     expect(current_path).to eq posts_path
+   end
  end
# rspec spec/system/07_posts_spec.rb

Finished in 6.5 seconds (files took 13.64 seconds to load)
2 examples, 0 failures

テストがパスしていますね。ちゃんとサインイン前後でリダイレクト機能の出しわけができているようです。

未サインインのユーザーは、トップページでヘッダーにポストページへのリンクを見つけられないこと

こちらもテストから書き始めます。ヘッダーのポストページへのリンクはheader_posts_linkidを付与することにします。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "未サインインのユーザーは、トップページでヘッダーにポストページへのリンクを見つけられないこと" do
      # トップページにアクセスする
+     visit root_path
+     
      # ページ内に"header_posts_link"をid属性に持つ要素がないことを検証する
+     expect(page).not_to have_selector "#header_posts_link"
+   end
  end
# rspec spec/system/07_posts_spec.rb

Finished in 7.61 seconds (files took 6.77 seconds to load)
3 examples, 0 failures

まだリンクをコーディングしていないので当然見つからないですね。テストをパスできています。

未サインインのユーザーは、サインアップページでヘッダーにポストページへのリンクを見つけられないこと

一つ前とほぼ同じテストですね。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "未サインインのユーザーは、サインアップページでヘッダーにポストページへのリンクを見つけられないこと" do
      # サインアップページにアクセスする
+     visit sign_up_path
+     
      # ページ内に"header_posts_link"をid属性に持つ要素がないことを検証する    
+     expect(page).not_to have_selector "#header_posts_link"
+   end
  end
# rspec spec/system/07_posts_spec.rb

Finished in 6.83 seconds (files took 6.03 seconds to load)
4 examples, 0 failures

未サインインのユーザーは、サインインページでヘッダーにポストページへのリンクを見つけられないこと

また、ほぼ同じです。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "未サインインのユーザーは、サインインページでヘッダーにポストページへのリンクを見つけられないこと" do
      # サインインページにアクセスする
+     visit sign_in_path
+ 
      # ページ内に"header_posts_link"をid属性に持つ要素がないことを検証する    
+     expect(page).not_to have_selector "#header_posts_link"
+   end    
  end
# rspec spec/system/07_posts_spec.rb

Finished in 9.04 seconds (files took 6.57 seconds to load)
5 examples, 0 failures

どんどん進みましょう。

未サインインのユーザーは、ユーザー詳細ページでヘッダーにポストページへのリンクを見つけられないこと

ほぼ同じテストケース最後です。ユーザー詳細ページなので、Userモデルが1つ必要なのですが、前のテストでもJohn Smithcreateしたテストケースがありました。
これを簡易に使いまわせるようにJohn Smithを作成するコードをメソッド化してみましょう。メソッド化はとてもシンプルなRubyコードでdefを使うだけです。メソッド外で変数を使うことになるのでインスタンス変数を使う必要があります。

spec/system/07_posts_spec.rb
  # ユーザー「John Smith」をDBに作成する
+ def create_john
+   User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
+ end

  feature "ユーザーとして、ポストを投稿したい", type: :system do
  ...
    scenario "サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること" do
      # テストシナリオ用のユーザーを作成
-     user = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
+     user = create_john
      ...
    end
  end

これでテストがパスするか一度確認しておきましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 7.8 seconds (files took 6.24 seconds to load)
5 examples, 0 failures

これでJohn Smithのユーザー作成を単純なメソッド
呼び出しで必要なテストシナリオからのみ呼び出すことができるようになりました!
では今回のテストシナリオを追加していきます。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "未サインインのユーザーは、ユーザー詳細ページでヘッダーにポストページへのリンクを見つけられないこと" do
      # テスト用のユーザーを作成する
+     user = create_john
+
      # テストユーザーのユーザー詳細ページにアクセスする
+     visit user_path(user)
+
      # ページ内に"header_posts_link"をid属性に持つ要素がないことを検証する
+     expect(page).not_to have_selector "#header_posts_link"
+   end
  end
# rspec spec/system/07_posts_spec.rb

Finished in 8.91 seconds (files took 6.91 seconds to load)
6 examples, 0 failures

今回もテストをパスできていることがわかります。メソッド
の呼び出しもうまくいっていますね。

サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること

次はサインイン後にヘッダーにポストページへのリンクが表示されており、クリックするとポストページに行けるようになる機能を作っていきます。今までと同様に、テストからコーディングしていきましょう。
サインイン済のユーザーでテストをするシナリオは前にもあったので、まずはサインイン済の状態にする操作をメソッド化してみます。

spec/system/07_posts_spec.rb
  ...
  # 与えられたユーザーでサインインする
+ def sign_in(user)
    # サインインページにアクセスする
+   visit sign_in_path
    # サインインページで作成したユーザーのメールアドレスを入力する
+   fill_in :user_email, with: user.email
    # サインインページで作成したユーザーのパスワードを入力する
+   fill_in :user_password, with: user.password
    # サインインボタンをクリックする(サインインする)
+   click_on :sign_in_button
+ end
  ...
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
    scenario "サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること" do
      user = create_john
-     visit sign_in_path
-     fill_in :user_email, with: user.email
-     fill_in :user_password, with: user.password
-     click_on :sign_in_button
+     sign_in(user)
      ...
    end
    ...
  end

再度、メソッド化してもテストがパスするかを確認します。

# rspec spec/system/07_posts_spec.rb

Finished in 8.96 seconds (files took 8 seconds to load)
6 examples, 0 failures

メソッド化成功です!
ではこのメソッドを使って、今回のテストコードを記述してみます。

spec/system/07_posts_spec.rb
feature "ユーザーとして、ポストを投稿したい", type: :system do
  ...
+   scenario "サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること" do
      # テスト用のユーザーを作成する
+     user = create_john
      # テスト用のユーザーでサインインする
+     sign_in(user)
+
      # プロフィールページにアクセスする
+     visit user_path(user)
      # "header_posts_link"をid属性に持つ要素(=ヘッダーのポストリンク)をクリックする
+     click_on :header_posts_link
+
      # 現在のページがポストページであることを検証する
+     expect(current_path).to eq posts_path
+   end
  end

はい。これでテストを回してみましょう。まだヘッダーにポストリンクを作っていないのでRedになるはずです。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい After sign in サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
     Failure/Error: click_on :header_posts_link

     Capybara::ElementNotFound:
       Unable to find link or button :header_posts_link

Finished in 11.92 seconds (files took 6.78 seconds to load)
7 examples, 1 failure

header_posts_linkをクリックしようとしたけどそんな要素見つからなかった、という失敗理由が表示されていますね。
サインイン後のリンクにポストリンクを追加してあげましょう。

app/views/layouts/application.html.erb
  <% if signed_in? %>
+   <li class="nav-item"><%= link_to "Posts", posts_path, class: "nav-link", id: :header_posts_link %></li>
    <li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link", id: :header_profile_link %></li>
    <li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link", id: :header_sign_out_link %></li>
  <% else %>

1行、ポストページへのリンクを追加しました。

# rspec spec/system/07_posts_spec.rb

Finished in 10.38 seconds (files took 6.17 seconds to load)
7 examples, 0 failures

今回はテストがパスしています。今まで実装もしていなかったのでパスしてた、未サインインユーザーにはポストページへのリンクがヘッダーに表示されないテストもパスしているのでデグレなく機能実装ができましたね。

ちょっとViewを確認してみましょう。
image.png
Viewを確認してみても、Postsリンクが追加されたことがわかりますね。

サインイン済のユーザーが、ユーザー詳細ページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること

サインインしているユーザーとは別のユーザーを作成し、そのユーザーのユーザー詳細ページからのポストページへの遷移をテストしてみましょう。
別のユーザーも他のテストシナリオでも使えるようにメソッド化しておきたいところです。create_johnメソッドを少し改良して、引数によって異なるユーザーをDB作成できるように改良して使ってみましょう。

spec/system/07_posts_spec.rb
- def create_john
-   User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
- end
  # user_typeに応じて、ユーザーをDBに作成する
  # 1: John Smith
  # 2: Taro Tanaka
+ def create_user(user_type = 1)
+   case user_type
+   when 1
+     User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
+   when 2
+     User.create(name: "Taro Tanaka", email: "taro@sample.com", password: "taro1234")
+   end
+ end
  ...
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ... 
    scenario "サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること" do
-     user = create_john
+     user = create_user(1)
      ...
    end
    ...
    scenario "未サインインのユーザーは、ユーザー詳細ページでヘッダーにポストページへのリンクを見つけられないこと" do
-     user = create_john
+     user = create_user(1)
      ...
    end
    ...
    scenario "サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること" do
-     user = create_john
+     user = create_user(1)
      ...
    end
  end

create_userメソッドではcase文を使ってみました。case文はある変数の値に応じて動作を変える分岐を作ることができます。

case target
when value1
  # target == value1 の場合のコード
when value2
  # target == value2 の場合のコード
...
else
  # どれにも当てはまらなかった場合のコード
end

では、メソッドがうまく置き換われたかを確認してみます。

# rspec spec/system/07_posts_spec.rb

Finished in 9.62 seconds (files took 6.84 seconds to load)
7 examples, 0 failures

ちゃんとテストがパスしていますので、新しいメソッドは機能しています。
ではこのメソッドを使って二人のユーザーを作成して実行するテストコードを記述してみましょう。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること" do
      # テスト用のユーザーを2人作成する
+     user1 = create_user(1)
+     user2 = create_user(2)
      # user1でサインインする
+     sign_in(user1)
+
      # user2のユーザー詳細ページにアクセスする
+     visit user_path(user2)
      # "header_posts_link"をid属性に持つ要素(=ヘッダーのポストリンク)をクリックする
+     click_on :header_posts_link
+    
      # 現在のページがポストページであることを検証する
+     expect(current_path).to eq posts_path
+   end
    ...
  end

テストを実行してみましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 12.04 seconds (files took 5.41 seconds to load)
8 examples, 0 failures

問題なくGreenです。

サインイン済のユーザーが、ポストページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること

これも一つ前と同じようなケースです。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーが、ポストページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること" do
      # テスト用のユーザーを1人作成する
+     user = create_user(1)
      # userでサインインする
+     sign_in(user)
+
      # ポストページにアクセスする
+     visit posts_path
      # "header_posts_link"をid属性に持つ要素(=ヘッダーのポストリンク)をクリックする    
+     click_on :header_posts_link
+
      # 現在のページがポストページであることを検証する
+     expect(current_path).to eq posts_path
+   end
    ...
  end
# rspec spec/system/07_posts_spec.rb

Finished in 13.63 seconds (files took 6.56 seconds to load)
9 examples, 0 failures

遷移系はここまでですね。全てGreenをキープしています。

サインイン済のユーザーは、ポストページでポストを入力できること

いよいよポストページの機能に入っていきます。
でもやり方は変わりません。まずはテストをコーディングしましょう!

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーは、ポストページでポストを入力できること" do
      # テスト用のユーザーを作成する
+     user = create_user(1)
      # このテストシナリオで使うポスト内容を定義する
+     content = "Hello world."
      # userでサインインする
+     sign_in(user)
+ 
      # ポストページにアクセスする
+     visit posts_path
      # ポスト入力欄(#post_content)にcontentを入力する
+     fill_in :post_content, with: content 
+ 
      # ポスト入力欄(#post_content)にcontentが入力されていることを検証する
+     expect(find("#post_content").value).to eq content
+   end
    ...
  end

前回のハンズオンでも出てきた入力してちゃんと入力できているかを確認するテストコードですね。
今回はポストを投稿するための入力エリアであるpost_contentに「Hello world.」を入力できるかをチェックしています。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい After sign in サインイン済のユーザーは、ポストページでポストを入力できること
     Failure/Error: fill_in :post_content, with: content

     Capybara::ElementNotFound:
       Unable to find field :post_content that is not disabled

Finished in 20.46 seconds (files took 7.82 seconds to load)
10 examples, 1 failure

このテストは失敗します。なぜならまだポストを投稿するための入力エリアであるpost_contentを作っていないからです。

post_contentはPostモデルオブジェクトを作るためのフォームです。
なのでまずはPostモデルを作成しましょう。

# rails g model post content:string user:references
# rm -rf spec/models

ここで見かけたことのないreferences型が出てきました。
これは外部キーを定義するための型です。今回のようにuser:referencesとするとuser_idという項目が定義され、ここにはUserモデルの主キーであるidが入るようになります。

Postモデルファイルをみてみましょう。

app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

今まではclass定義しかされていませんでしたが、今回はbelongs_to :userというコードがデフォルトで記述されています。
これはモデルの関連付けです。(参考: Active Record の関連付け - Railsガイド

belongs_toは指定するモデルを1つに特定できることを表します。つまり、あるPostから見ると紐づくUserが一意に決まることを示しています。

逆にUserは複数のPostを行います。これを表す関連付けがhas_manyです。
UserモデルにはまだPostモデルとの関連付けがコーディングされていないので、自分で記述しておきましょう。

app/models/user.rb
  class User < ApplicationRecord
    ...
+   has_many :posts
    ...
  end

ちなみにマイグレーションファイルの中身もみておきましょう。

db/migrate/YYYYMMDDhhmmss_create_posts.rb
class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts do |t|
      t.string :content
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

t.references :user, null: false, foreign_key: trueuser_idを外部キーとして利用できるようにDBにSQLを発行してくれます。

では、マイグレーションファイルを適用します。

# rails db:migrate

== YYYYMMDDhhmmss CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.1530s
== YYYYMMDDhhmmss CreatePosts: migrated (0.1536s) =============================```

Postモデルの準備ができたので、今までと同じようにPostモデルを作成するためのフォームを作っていきましょう。
Userモデルの時と同じように、コントローラーで空のPostモデルオブジェクトを作成し、Viewでform_withヘルパーを使ってフォームを作ってみます。

app/controllers/posts_controller.rb
  class PostsController < ApplicationController
    def index
      redirect_to root_path unless signed_in?
+     @post = Post.new
    end
  end
app/views/posts/index.html.erb
- <h1>Posts#index</h1>
- <p>Find me in app/views/posts/index.html.erb</p> %>
+ <div class="container my-5">
+   <%= form_with model: @post, url: nil, local: true do |form| %>
+     <div class="form-group">
+       <%= form.text_area :content, class: "form-control", placeholder: "いまどうしてる?", autofocus: true %>
+     </div>
+   <% end %>
+ </div>

まだリクエスト先のルーティングを決めていないのでurlにはnilを定義しています。
また、今までと違う点としてはform.text_areaを使っています。text_areaヘルパーは<textarea>タグを生成するヘルパーです。
ポストは今までのように1行の短い文字列ではなく、改行などを含んだ140文字の文章になるのでそちらを選択してます。
さらに、placeholderを使っています。これはHTML5の技術ですが、inputtextareaに何も入力がない時に限り、そのフォームの補助の役割でどういうものを入力すればいいかを表示してあげる機能です。
autofocus: trueはページが表示された時に自動的にフォーカスされるフィールドを指定できるHTML5の機能です。便利なのでつけときます。

今回の場合は、以下のようなフォームができあがっています。
image.png

ここまでで再度テストを実行してみましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 16.43 seconds (files took 6.9 seconds to load)
10 examples, 0 failures

これでテストをパスすることができました!

今回はこの辺りで時間切れなので、残りのテスト&コーディングは次回に回したいと思います!

まとめ

今回はポスト機能をTDD/BDDでコーディングしてきました。なんかいよいよコーディングしている感じが高まってきましたね。
次回は残りのユーザー詳細ページ側でそのユーザーの投稿に絞ってポストを確認できる機能をコーディングしていきます。
次回以降もBDDで実装を進めていくので、是非ともテストコードも振り返っておいてくださいね。

後片付け

いつものようにコンテナを落としておきます。

# exit
$ docker-compose down

本日のソースコード

Other Hands-on Links

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerの基礎からOpenFOAMのインストールまで(MacOS)

モチベーション

Macを使っていると,大抵の場合は問題ないのですが,一部のアプリケーションをインストールする際,どうしてもネイティブのlinux環境がほしいときがあります.僕の場合,OpenFOAM-extendをインストールしようとした際,MacPortsしかサポートされておらず,Homebrewとのバッティングが嫌で,どうしてもLinux環境がほしいなという状況になりました.
そこでDockerを使ってインストールしてみようとした際に勉強したことを残しておこうと思います.

Dockerfileの作成

ここに書くと長くなるので,GitHubに置いておきました.

OpenFOAMのインストール

The Complete Guide to Docker & OpenFOAM · CFD Engine

上記記事が非常に丁寧に説明してあり,参考になります.通常のOpenFOAMをインストールする分にはこれで事足ります.

以下で上記記事で採用されているDockerfileを確認できます.

自分の場合OpenFOAM-extend-4.0をインストールする必要があったため,これをベースにしつつ,公式のinstall manualに則ってDockerfileを自作しました.

Dockerfile作成時の注意点(1)

デフォルトではRUNで行われるコマンドはすべてshのため,bashや他のshellで行う必要のあるコマンドは下記のgithub issuesのスレッドを参考に変更しなければなりません.

Dockerfile作成時の注意点(2)

基本的に毎回のRUNコマンドごとにディレクトリがもとの位置に戻ります.また,$HOMEの位置にも注意が必要です.
cdを複数回使用する場合は注意して,テストしながらDockerfileを作成する必要があります.
必要応じてWORKDIRコマンドでカレントディレクトリを変更すれば書く量が減るかも知れません.

イメージのビルド

current directoryにDockerfileを用意.イメージのビルドは以下のコマンドで行います.

$ docker build -t image_name:tag_name directory-of-Dockerfile
# for example
$ docker build -t nishiys/simple_openfoam-extend-4 .

コンテナの作成とコマンドの実行

「コマンドを叩いたときにコンテナを作成し,コマンドが終わればコンテナを削除する.」というのがDockerの本来の良い使い方らしいです.
毎回コマンドを覚えなくていいようにシェルスクリプト化するか,エイリアスを設定しておくのが良いと思います.

シェルスクリプトの例です.$ chmod +x docker.shで実行権限を付与するのを忘れずに.

docker.sh
#!/bin/sh
docker container run -ti --rm -v $PWD:/data -w /data image_name /bin/bash

# -ti : enable to use the container as an interactive terminal
# --rm : delete the container when you exit the container
# -v : mount our current working directory ($PWD) as /data in the container
# -w : tell Docker that we’d like to be in /data when the container starts.
# /bin/bash : run bash after entering the container

その他必須コマンド

# show all images
$ docker images -a

# show all containers
$ docker ps -a

# delete a docker image
$ docker rmi image_name

# delete docker images whose REPOSITORY is <none>
$ docker image prune

# delete all containers
$ docker rm `docker ps -a -q`

# delete all inactive containers
$ docker container prune

コンテナを抜けたり出たりをする場合は以下のコマンドも必要です.

# start a docker container
$ docker start <container ID or container NAME>
# enter a container
$ docker attach <container ID or container  NAME>

その他イメージの管理関係で必要なコマンド

# add a tag to an existing image
$ docker image tag original_image_name[:tag_name] new_image_name[:tag_name]

# share your image on Docker Hub
$ docker image push [options] repository_name[:tag_name]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

開発コンテナで快適!<del>ひきこもり生活</del>フロントエンド開発

どうも、よこけんです。
Web アプリ開発の現場から離れて10年くらい経つのですが、思うところあって最近のフロントエンド開発についてプライベートで勉強しました。今日はその成果をアウトプットしようと思います。

本記事では主に、VSCode の Remote-Container を使ったフロントエンド (React) 開発を行うための環境構築方法を解説します。
オールインワンのためかなり長い記事になってしまいましたが、大半の作業はファイルのコピペとコマンドのコピペなので作業自体はシンプルです。ただし、各要素の理解こそが重要なので、この記事をきっかけに各要素の理解を深めていっていただければと思います。

本記事では特に下記の要素を押さえています。

  • 開発環境のコンテナ化
    • 常にコンテナの中で 生活 開発していきます。
    • 開発者間での開発環境の統一ができます。
    • 開発環境のリセットが容易です。
    • 本番環境に (構成の面で) 近い環境を使用して開発できます。
  • 本番環境のコンテナ化
    • Docker in Docker によって、開発コンテナからも気軽に起動できます。
    • 本番環境と (構成の面で) 同一の環境を使用して動作確認できます。
  • React 開発を始めるにあたって重要になってくる (手堅い) 周辺技術
    • 技術選定を簡略化もしくは省略できます。
    • create-react-app を使わないので、各技術がブラックボックス化されず制御しやすくなります。
    • 導入時に手を焼くであろうポイントを回避できます。
    • 概略を押さえることで各要素の学習のハードルが下がり、スムーズに進めやすくなります。
  • デバッグ方法
    • 取っつきにくくややこしい設定に手を焼くことなく、容易にデバッグを行えます。

反対に、下記については本記事では扱いません。

  • 言語について
  • React そのものの詳細な開発テクニック・テストテクニック
  • バージョン管理 (Git) の詳細
  • ひきこもりの是非
  • アトミックデザインなどのコンポーネント設計手法
  • マテリアルデザインなどの Web デザイン手法
  • Cloud などへのデプロイメント

余談ですが、個人的にはアトミックデザインについてはやや懐疑的です。
コンポーネントの再利用性を高めること自体は重要と思いますが、ボトムアップなアプローチは過剰設計を招きがちです。
フロントエンド開発においても、トップダウンなアプローチで必要に応じてコンポーネントの再利用性を高めていく進化的設計が望ましいと考えています。

まぁそれはさておき、本題に入りましょう。
完成品は GitHub にあげてありますのでご活用ください。

開発環境構成

まずはこの記事で構築する開発環境の構成を見ていきます。

ホスト構成

  • OS
    • Windows 10 Pro (※)
  • IDE
    • VSCode
      • Remote Development (Remote-Container)
      • Docker
      • EditorConfig
      • Git Lens (この記事では扱いませんがお勧めです)
      • Git Graph (この記事では扱いませんお勧めです)
  • バージョン管理システム
    • Git
  • コンテナツール
    • Docker Desktop

※ 私の環境が Windows 10 Pro なので、Windows 10 Home や Mac だとどうなるのかはよくわかりません。特に Windows 10 Home は Hyper-V 非対応のため Docker Desktop が使えないかと思います。(Virtual Box + Docker Toolbox や WSL2 + Docker Desktop で Docker が動かせるという噂ですが。)

開発用コンテナ構成

  • OS
    • Debian
  • ランタイム
    • Node.js
  • パッケージマネージャー
    • npm
  • コンテナツール
    • Docker CE CLI
    • Docker Compose
  • IDE
    • VSCode (ホスト環境からリモート接続)
      • language-stylus
      • Debugger for Chrome
      • Debugger for Edge
      • ESLint
      • Stylint
      • Manta's Stylus Supremacy
      • Jest Runner
      • EditorConfig 1
      • Git Lens 1
      • Git Graph 1
  • バージョン管理システム
    • Git 1

コンテナ化した本番環境を Docker in Docker で起動できるよう、コンテナツールもインストールしておきます。 (基本的には本番環境は起動せず開発環境内で直接アプリを実行しますが。)

開発用パッケージ構成

  • クライアントサイド
    • 言語
      • HTML
      • TypeScript
      • Stylus
    • Lint
      • ESLint
        • ESLint Plugin React
      • Stylint
    • フレームワーク
      • React
        • React DOM
        • React Hooks
        • React Router DOM
        • React Hot Loader
    • モジュールバンドラ
      • WebPack
        • WebPack CLI
        • WebPack Merge
        • TS Loader
        • CSS Loader
        • Style Loader
        • Stylus Loader
        • URL Loader
        • File Loader
        • Clean WebPack Plugin
        • HTML WebPack Plugin
    • テストフレームワーク
      • Jest
      • Jest CSS Modules
      • Fetch Mock
      • React Testing Library
    • その他
      • concurrently
  • サーバーサイド
    • ランタイム
      • ts-node
      • ts-node-dev
    • 言語
      • TypeScript
    • Lint
      • ESLint
    • Web サーバー
      • Express
      • WebPack Dev Server
    • ロギング
      • log4js
    • リバースプロキシ
      • node-fetch

多過ぎ…

うん。多いですね。
これだけ見ると、フロントエンド開発をこれから学ぼうとしている方は尻込みしてしまうかもしれません。
ただ、これらは大きなものから小さなものまで全て洗い出して記載しており、主要技術としては太字で記載したものに限定されます。
下記は主要技術を抜き出したリストとなります。

  • IDE
    • VSCode
      • Remote Development (Remote-Container)
  • バージョン管理システム
    • Git
  • ランタイム
    • Node.js
  • パッケージマネージャー
    • npm
  • コンテナツール
    • docker-ce-cli
    • docker-compose
  • 言語
    • TypeScript
    • Stylus
  • Lint
    • ESLint
    • Stylint
  • Web サーバー
    • Express
    • WebPack Dev Server
  • フレームワーク
    • React
  • モジュールバンドラ
    • WebPack
  • テストフレームワーク
    • Jest
    • React Testing Library
  • ロギング
    • log4js

まぁ、それでも多いんですが。
だからこそ、まとめて押さえられるようにこの記事を書こうと思い立ったわけです。

といっても、この記事だけで全て完璧に習得できる、なんてことは全くありません。特にバージョン管理、言語、フレームワーク、テストフレームワーク、そして CSS フレームワークについては、確実に追加学習が必要となります。

何を学べば良いかがある程度固まって、その取っ掛かりになるくらいの情報は得られる、というのがこの記事の目指すところです。

本番環境構成

続いて本番環境です。

本番用コンテナ構成

  • OS
    • Debian
  • ランタイム
    • Node.js
  • パッケージマネージャー
    • npm

本番用パッケージ構成

  • クライアントサイド
    • 言語
      • (HTML)
      • (JavaScript)
      • (CSS)
  • サーバーサイド
    • ランタイム
      • ts-node
    • 言語
      • TypeScript
    • Web サーバー
      • Express
    • ロギング
      • log4js
    • リバースプロキシ
      • node-fetch

クライアントサイドはブラウザ上で実行されますので HTML + JavaScript + CSS になっています。TypeScript や Stylus がビルド時に自動的にこれらにトランスパイル (変換) されます。
React などのライブラリ群は、ビルド時にモジュールバンドラによってまとめられるため本番環境へのインストールは不要です。

事前準備

では、ここからは実際に作業を行っていきます。

VSCode のインストール

VSCode をダウンロードしてインストールします。

インストールしたら起動して、拡張機能をインストールします。

image.png

以下の拡張機能をそれぞれ名前で検索して [Install] ボタンでインストールしてください。

  • Remote Development (Remote-Container)
  • Docker
  • EditorConfig
  • Git Lens (この記事では扱いませんがお勧めです)
  • Git Graph (この記事では扱いませんお勧めです)

Git のインストール

Git をダウンロードしてインストールします。

重要な設定は改めて行うので、インストール時に行う設定はとりあえず適当で良いです。よくわからない項目はそのままで。

インストールが完了したら Git Bash を開き、下記のように設定を行います。
{User Name}{Mail Address} は自分の情報で置き換えてください。

git config --global user.name "{User Name}"
git config --global user.email "{Mail Address}"
git config --global push.default "simple"
git config --global core.autocrlf "false"
git config --global core.ignorecase "false"
git config --global core.quotepath "false"

Hyper-V の設定確認

デフォルトで有効化されていると思いますが、一応確認しておいてください、

仮想マシンを自分で作成する必要はありません。

Docker Desktop のインストール

Docker Desktop (Community Edition) をダウンロードしてインストールします。インストーラのオプション設定は変更せずにインストールします。

インストールが完了すると自動で起動します。Docker アカウントのログイン画面が表示されますが、アカウント登録やログインは不要ですので閉じてしまってください。

ワークスペース

VSCode にはマルチルートワークスペースという機能があります。これは一つのワークスペースに、関連する複数のプロジェクトフォルダーをまとめる機能です。
この記事では一つのプロジェクトフォルダーしか作成しませんが、下記の理由からこのマルチルートワークスペースを採用します。

  • 常にマルチルートワークスペースで構築することで、一貫して同じ操作方法・動作となる。
    • プロジェクトフォルダー を直接開く場合はプロジェクトフォルダー = ワークスペースだが、マルチルートワークスペースではプロジェクトフォルダー ≠ ワークスペースとなり、一部の操作方法や動作に違いがある。
    • フロントエンドは Node.js、バックエンドは Python や C# というように、プロジェクトフォルダーを分けることは多い。後からプロジェクトフォルダーが増えることもよくある。

ということで、ワークスペースを作成しましょう。

マルチルートワークスペースの作成

  1. Windows Explorer で任意の場所にワークスペースフォルダー (本記事ではC:\Workspaces\MoroMoro.Sample) を作成します。
  2. VSCode を起動します。
  3. メニューバーの [File] - [Close Folder] が無効化されていることを確認します。有効化されていたり [Close Workspace] が表示されている場合はそれを押してフォルダー(もしくはワークスペース) を必ず閉じてください。
  4. メニューバーの [File] - [Save Workspace As...] を押します。
  5. ファイル保存ダイアログが開かれるので、ワークスペースフォルダーに移動し、ファイル名にワークスペースフォルダーと同じ名前 (本記事では MoroMoro.Sample) を入力して [Save] ボタンで保存します。

<注意>
本記事ではワークスペース名やプロジェクト名を MoroMoro.Sample.Frontend のようにアッパーキャメルケースで命名していますが、Node.js パッケージ (後述) の命名規則に合わせて moromoro.sample.frontend のように全て小文字で命名しても構いません。(本記事でも Node.js パッケージ名については全て小文字で命名します。)

Git Init

バージョン管理はワークスペースレベルで行います。
Git Bash を開きワークスペースに移動してから次のコマンドを実行します。

git init

<注意>
「使い捨てだからバージョン管理しなくていいや」という人も必ず行ってください。
非常に厄介なことに、Git Init の有無で Remote-Container の挙動の一部が大きく変わってしまうためです。(後述)

プロジェクト

続いて、ワークスペースにフロントエンドのプロジェクトを作成します。

プロジェクトフォルダーの追加

  1. Windows Explorer でワークスペースフォルダーにプロジェクトフォルダー (本記事では MoroMoro.Sample.Frontend) を作成します。
  2. VSCode のサイドメニューバーから image.png (Explorer) を開き、[Add Folder] ボタンを押します。
  3. フォルダー選択ダイアログが開かれるので、プロジェクトフォルダーを選択して [Add] ボタンで追加します。

EditorConfig 設定

EditorConfig はファイルの文字コードや改行コード、インデントなどのエディタ設定をファイルにまとめる仕組みです。設定ファイルをソースコードと一緒に管理することで、メンバー間でのエディタ設定を統一することができます。
VSCode では直接はこの仕組みをサポートしていませんが、事前準備でインストールした EditorConfig 拡張機能によってこの仕組みが利用できるようになります。
VSCode の settings.json などでも同様のことを実現できるのですが、下記の理由から EditorConfig を採用することにします。

  • EditorConfig をサポートする別のエディタを併用できる
  • 適用対象ファイルをパターンで指定することができる
  • フォルダ単位で設定ファイルを用意することができる (基本的にはルートフォルダーで全体設定するだけで事足りますが)

では、プロジェクトフォルダーに .editorconfig というファイルを作成し、下記の内容で保存してください。 (必要に応じて独自にカスタマイズしてください。)

root = true

[*]
end_of_line = lf
charset = utf-8
indent_size = 4
indent_style = space
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

一行目の root = true という記述は特別な設定です。これによって「この設定ファイルはルートフォルダーに配置されている」ということが宣言され、これより上位のフォルダーに EditorConfig 設定ファイルがあったとしても無視されるようになります。
あとはシンプルでわかりやすいと思うので説明は省きます。

なお、VSCode 独自の設定や拡張機能の設定は EditorConfig では設定できませんので settings.json で管理します。(後述)

Git Ignore の設定

Git Ignore は下記の gitignore.io という Web サイトで作成するのが手っ取り早いです。

今回は Node, react, Linux, VisualStudioCode を入力して作成しました。 (Stylus を含めると *.css が登録されてしまうのであえて除外)

では、プロジェクトフォルダーに .gitignore というファイルを作成し、下記の内容で保存してください。 (必要に応じて独自にカスタマイズしてください。)

内容
# Created by https://www.gitignore.io/api/node,react,linux,visualstudiocode
# Edit at https://www.gitignore.io/?templates=node,react,linux,visualstudiocode

### Linux ###
*~

# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*

# KDE directory preferences
.directory

# Linux trash folder which might appear on any partition or disk
.Trash-*

# .nfs files are created when an open file is removed but is still being accessed
.nfs*

### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# next.js build output
.next

# nuxt.js build output
.nuxt

# rollup.js default build output
dist/

# Uncomment the public line if your project uses Gatsby
# https://nextjs.org/blog/next-9-1#public-directory-support
# https://create-react-app.dev/docs/using-the-public-folder/#docsNav
# public

# Storybook build outputs
.out
.storybook-out

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# Temporary folders
tmp/
temp/

### react ###
.DS_*
**/*.backup.*
**/*.back.*

node_modules

*.sublime*

psd
thumb
sketch

### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

### VisualStudioCode Patch ###
# Ignore all local history of files
.history

# End of https://www.gitignore.io/api/node,react,linux,visualstudiocode

開発コンテナ

さあ、お待ちかねの コンテナハウス 開発コンテナです。
快適な環境を整えていきましょう。

開発コンテナの作成

開発コンテナは Remote-Container 拡張機能が用意しているコマンドで手っ取り早く作成することもできるのですが、下記の理由から本記事では手作業で作成します。

  • ベースイメージは本番環境と揃えたい
  • 拡張機能が用意する Node.js 開発用 Dockerfile と Docker in Docker 用 Dockerfile の2つの良いとこどりをしたい
  • 拡張機能が用意する Node.js 開発用 Dockerfile では、今回使用する一部の Node.js モジュールとの相性が悪い
  • 拡張機能が用意する Docker in Docker 用 Dockerfile では、イメージサイズが無駄に大きくなる

やはり 生活空間 開発環境はこだわらないと。
まずはプロジェクトフォルダーに .devcontainer フォルダーを作成してください。
大事なことなので画像貼っておきます。
image.png
このフォルダーの中に Dockerfiledevcontainer.json を作成します。
image.png

Dockerfile

Dockerfile は下記の内容で保存してください。

FROM node:12.16-buster-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
    #
    # Verify git, process tools installed
    && apt-get -y install --no-install-recommends git openssh-client iproute2 procps \
    #
    #####
    # https://github.com/Microsoft/vscode-dev-containers/tree/master/containers/docker-in-docker#how-it-works--adapting-your-existing-dev-container-config
    # Note that no recommended packages are required, except for gnupg-agent.
    #
    # Install Docker CE CLI
    && apt-get install -y --no-install-recommends apt-transport-https ca-certificates curl software-properties-common lsb-release jq \
    && apt-get install -y gnupg-agent \
    && curl -fsSL https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/gpg | apt-key add - 2>/dev/null \
    && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]') $(lsb_release -cs) stable" \
    && apt-get update \
    && apt-get install -y --no-install-recommends docker-ce-cli \
    #
    # Install Docker Compose
    && curl -sSL "https://github.com/docker/compose/releases/download/$(curl https://api.github.com/repos/docker/compose/releases/latest | jq .name -r)/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose \
    && chmod +x /usr/local/bin/docker-compose \
    #####
    #
    # Clean up
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*
ENV DEBIAN_FRONTEND=dialog

これが開発コンテナの素です。
Docker がこの定義に従ってイメージを構築し、コンテナとして立ち上げてくれるのです。

ベースイメージは node:12.16-buster-slim です。後述の本番環境のベースイメージと揃えています。buster は Debian 10 のことで、Docker イメージ向けにスリムになったものが buster-slim です。これに Node.js 12.16 がインストールされています。

開発コンテナには更に、Git など開発に必要となるツールと Docker CE CLI、Docker Compose を追加インストールします。これらの追加ツールは、製品コードに直接影響しない補助ツールなのでバージョン指定を行っていません。Docker Compose についても、下記の記事を参考に最新版が自動で選択されるよう細工しています。

devcontainer.json

devcontainer.json は下記の内容で、{Workspace Name} 1箇所と {Project Name} 2箇所を適切に置き換えた上で保存してください。この際、大文字・小文字もしっかり合わせる必要があります。
本記事の場合、{Workspace Name}MoroMoro.Sample に、{Project Name}MoroMoro.Sample.Frontend になります。

// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.101.1/containers/javascript-node-12
{
    "name": "Node.js 12",
    "dockerFile": "Dockerfile",
    // Set *default* container specific settings.json values on container create.
    "settings": {
        "terminal.integrated.shell.linux": "/bin/bash"
    },
    // Add the IDs of extensions you want installed when the container is created.
    "extensions": [
        "editorconfig.editorconfig",
        "mhutchie.git-graph",
        "eamodio.gitlens",
        "sysoev.language-stylus",
        "msjsdiag.debugger-for-chrome",
        "msjsdiag.debugger-for-edge",
        "dbaeumer.vscode-eslint",
        "haaleo.vscode-stylint",
        "thisismanta.stylus-supremacy",
        "firsttris.vscode-jest-runner"
    ],
    // Use 'forwardPorts' to make a list of ports inside the container available locally.
    "forwardPorts": [
        3000, // Main server
        8080, // HMR server
    ],
    "mounts": [
        // Use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-in-docker.
        "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind",
        // https://code.visualstudio.com/docs/remote/containers-advanced#_use-a-targeted-named-volume
        "source={Project Name}-node_modules,target=/workspaces/{Workspace Name}/{Project Name}/node_modules,type=volume"
    ],
    // Use 'postCreateCommand' to run commands after the container is created.
    "postCreateCommand": "npm install"
}

重要な設定項目についてだけ詳しく説明しておきます。

extensions

インストールする VSCode の拡張機能です。
コンテナイメージ作成時に自動でインストールしてくれます。
ホスト側にはインストールされませんので開発コンテナ内でのみ使用可能です。
拡張機能は ID で指定する必要がありますが、拡張機能サイドバーで拡張機能を右クリックして Copy Extension Id を実行すれば簡単に ID をコピーできます。

forwardPorts

ポートフォワーディング設定です。
コンテナ内でポート3000を使用してサーバーを立ち上げますので、ホスト側のブラウザからアクセスできるようにこの設定が必要となります。また、直接アクセスすることはありませんが、後述の HMR 用補助サーバーがポート8080を使用してこっそりブラウザとやり取りしますのでこちらも設定が必要です。

mounts

マウント設定です。二つ設定しています。

一つ目のマウント設定
Docker in Docker を実現するために必要です。
これにより、開発コンテナ内の Docker CLI がホストの Docker Desktop と接続され、正常に動作するようになります。

二つ目のマウント設定
node_modules フォルダー (Node モジュールが大量にインストールされるフォルダ) を Docker Desktop の名前付きボリュームという特別な領域にマウントするための設定です。
通常、ワークスペース内のファイルはホストとコンテナの間で自動的に共有されるのですが、ファイルアクセス速度はやや遅めです。基本的には全く問題無いレベルの遅延なのですが、node_modules フォルダーには大量のファイル作成が行われるため、この遅延によるパフォーマンス低下が顕著に現れてしまいます。そこで、node_modules フォルダーだけは例外的にホストではなく名前付きボリュームにマウントしてしまうわけです。

名前付きボリュームはコンテナの外の領域なので、コンテナを再作成しても削除されません。その代わり、別コンテナからも同じ名前付きボリュームにマウントすることができてしまいます。なので、名前が衝突しないよう MoroMoro.Sample.Frontend-node_modules というように面倒な名前付けを行う必要があります。

ちなみに、マウント先のパスの先頭 (パーティション名) は workspaces 固定です。workspaceFolder 設定及び workspaceMount 設定で変更することもできますが、非常にややこしいことになるのでやらないでください。本記事の、『マルチルートワークスペース』+『ワークスペース丸ごと Git 管理』+『コンテナ内から Git 操作』という構成は恐らく成立しなくなります。
ホスト側のワークスペース直下で Git Init を行い、workspaceFolder 設定及び workspaceMount 設定を変更していない2場合に限り、ホストのプロジェクトフォルダではなくワークスペースフォルダがマウントされ、コンテナ内から Git 操作できるようになります。

postCreateCommand

コンテナイメージ作成後に自動実行されるコマンドです。コンテナイメージはコンテナを初めて開くときに作成されます。
npm install というコマンドは package.json に従って Node モジュールのインストールを行うコマンドです。この後コンテナを開きますが、その時点では package.json がないのでこのコマンドはあまり意味がありません。しかし、package.json 作成完了後であれば、チームメンバーが Git からソースコード一式を手に入れて開発コンテナを開くとそれだけで最初から開発環境が完璧に揃って提供されることになります。

開発コンテナを開く

ついに開発コンテナが用意できましたね…。早速開きましょう。
開き方はいくつかあるのですが、本記事では Remote Explorer から開く方法をお勧めします。他の方法より手数が多い方法なのですが、Remote Explorer に慣れておけば後々便利です。
まだ一度も開いていないコンテナを開くには、Remote Explorer の右上の [+] ボタンを押し、Open Folder in Container... を選択します。
image.png
フォルダー選択ダイアログが開きますのでプロジェクトフォルダーを選択して開きます。
すると右下にメッセージが表示されコンテナイメージの作成が開始します。
image.png
作成が完了するとメッセージが消えます。
問題無ければこの時点でコンテナの中に入れているはずです。コンテナの中にいる時はステータスバーの左端に Dev Container: Node.js 12 と表示されます。
image.png

コンテナの中でプロジェクトフォルダーを開いたことにより、コンテナ内ではプロジェクトフォルダーがワークスペースそのものとなります。
本記事では、コンテナ内でのワークスペースのことをコンテナワークスペースと呼ぶことにします。

Docker ソケットのマウント確認

ターミナルから次のコマンドを実行し、正常にイメージ一覧を取得できることを確認してください。

docker images

エラーが発生してしまう場合は開発コンテナの準備に不備があったということですので設定を見直してください。
修正したら、開発コンテナをリビルドします。

node_modules のマウント確認

コンテナワークスペースに空の node_modules フォルダーが作成されていることを確認してください。
node_modules フォルダが作成されていない場合は開発コンテナの準備に不備があったということですので設定 (特にワークスペース名やプロジェクト名の誤字脱字、workspacesWorkspacesworkspace になっていないかなど) を見直してください。 (或いは前述の Git Init を行っていない場合にもマウントが正常に行えません。Git Init を行っていないと、コンテナワークスペースが /workspaces/{Workspace Name}/{Project Name} ではなく /workspaces/{Project Name} に配置されてしまいます。使い捨てで Git 管理しない場合でも Git Init だけは必ず行ってください。)
修正したら、開発コンテナをリビルドします。

なお、前述の通りこの node_modules フォルダーはホスト側にはファイルを一切作成しません。逆にホスト側で node_modules フォルダー内にファイルを作成しても、コンテナ側には共有されません。ただし、node_modules フォルダーそのものの削除を行ってしまうとお互い連動して削除されてしまいます。ホスト側では中身が無いからといって、くれぐれもフォルダーそのものを削除しないよう注意しましょう。

開発コンテナのリビルド

開発コンテナの準備に不備があった場合には、修正してリビルドを行ってください。
リビルドする方法もいくつかあるのですが、やはり Remote Explorer で行う方法をお勧めします。
Remote Explorer にコンテナとフォルダーが登録されていますので、コンテナを右クリックして Rebuild Container を実行するとリビルドが行われます。完了後は自動でコンテナが開きなおされます。
image.png
コンテナのリビルドはコンテナ内にいる時にしか実行できません。
コンテナ外にいるときは単純にコンテナを削除してしまえば、次回コンテナを開くときにコンテナが自動で作成されます。
image.png

開発コンテナを閉じる

VSCode を終了すればコンテナは停止されます。(ただし、devcontainer.json で "shutdownAction": "none" を設定している場合は停止されません。)
次回 VSCode 起動時には自動で開発コンテナが開かれます。

VSCode を終了させずにコンテナを閉じる時は、メニューバーの [File] - [Close Remote Connection] を実行します。(Connection といいつつ接続だけでなくコンテナも停止します。)
image.png
再び開発コンテナを開くには、1回目と同じ方法でも開くことができるのですが、もっと楽な手順で開くこともできます。Remote Explorer にコンテナとフォルダーが登録されていますので、フォルダーの右端の image.png (Open Folder in Container) ボタンを押せば一発で開けます。
image.png

無事開発コンテナを開けたら

ここからはもう、ずっと、最後まで、とことん、開発コンテナにひきこもります。

パッケージの作成

さて、まずはプロジェクトのパッケージ化を行いましょう。
パッケージ化すると、パッケージのビルドや実行などのスクリプトを登録できたり、パッケージが依存する Node モジュールを簡単に管理できたりします。

package.json の作成

コンテナワークスペースに package.json ファイルを作成し、下記の内容で {project name} 2箇所と {Your Name} 1箇所を適切に置き換えた上で保存してください。{project name} では大文字が禁止されているので全て小文字で記述します。
本記事の場合、{project name}moromoro.sample.frontend に、{Your Name}Kenji Yokoyama になります。
(npm init は今回使いません。下記を貼り付けて置換した方が手っ取り早いので。)

{
    "name": "{project name}",
    "version": "1.0.0",
    "description": "{project name}",
    "scripts": {
        "start": "     export NODE_ENV=production  && ts-node ./src/server/server.ts",
        "build": "     export NODE_ENV=production  && webpack --config ./webpack.config.ts",
        "build:dev": " export NODE_ENV=development && webpack --config ./webpack.config.ts",
        "run": "       export NODE_ENV=production  && npm run build     && npm start",
        "run:dev": "   export NODE_ENV=development && npm run build:dev && ts-node-dev --nolazy --inspect=9229 ./src/server/server.ts",
        "run:hmr": "   export NODE_ENV=development && export HMR=true   && concurrently \"ts-node-dev --nolazy --inspect=9229 ./src/server/server.ts\" \"webpack-dev-server --config ./webpack.config.hmr.ts\"",
        "test": "      export NODE_ENV=development && jest --coverage"
    },
    "author": "{Your Name}",
    "license": "UNLICENSED",
    "private": true
}

フロントエンド開発のための非公開パッケージですので、"license": "UNLICENSED""private": true を設定しておきます。
scripts に登録した各スクリプトについては後ほど適宜説明していきます。

Node モジュールのインストール

続いて Node モジュールのインストールです。VSCode のターミナルにて、モジュール名を指定して npm install コマンドを実行します。依存モジュールがある場合は、基本的に全て自動で追加インストールされます。

まずは本番環境用モジュールをインストールします。

本番環境用モジュールのインストールには -S オプションを使用します。

npm install -S typescript ts-node express @types/express log4js node-fetch @types/node-fetch

次は開発環境用モジュールのインストールです。

開発環境用モジュールのインストールには -D オプションを使用します。
実行環境用にインストールしたモジュールを改めてインストールする必要はありません。

npm install -D ts-node-dev stylus @types/stylus eslint eslint-plugin-react @typescript-eslint/eslint-plugin @typescript-eslint/parser stylint react @types/react react-dom @types/react-dom react-router-dom @types/react-router-dom react-hot-loader @hot-loader/react-dom webpack @types/webpack webpack-cli webpack-merge @types/webpack-merge webpack-dev-server @types/webpack-dev-server ts-loader style-loader css-loader stylus-loader url-loader file-loader @types/file-loader clean-webpack-plugin html-webpack-plugin @types/html-webpack-plugin concurrently @types/concurrently jest @types/jest ts-jest fetch-mock @types/fetch-mock @testing-library/react @testing-library/react-hooks @testing-library/jest-dom jest-css-modules

手動インストールが必要なモジュール (代替モジュールがあるもの) がいくつか報告されましたので追加でインストールします。

npm install -D react-test-renderer @types/react-test-renderer canvas bufferutil utf-8-validate

インストールは node_modules フォルダーに対して行われます。
コンテナワークスペースの node_modules フォルダーに大量のモジュールフォルダーが追加されていることを確認しておきましょう。
この時、ホスト側の node_modules には一切ファイルが追加されていないことが重要です。せっかくひきこもったのですが、念のため 偵察ドローンを飛ばして外の様子を Windows Explorer でホスト側の node_modules フォルダーが空であることを確認しておいてください。

なお、インストールに成功するとインストールされたモジュールのバージョン情報が package.json の dependenciesdevDependencies に記録されます。
更に、追加でインストールされた間接的な依存モジュールも含む全ての依存モジュールの情報が package-lock.json に記録されます。
package.json と package-lock.json によって依存モジュールのバージョンが固定されるので、モジュールの再インストールをしても全く同じ環境を復元することができます。

コンテナワークスペース用 VSCode 設定

コンテナワークスペースに .vscode フォルダを作成し、下記の内容の settings.json ファイルを作成してください。

{
    "editor.formatOnSave": true,
    "editor.formatOnPaste": true,
    "editor.formatOnType": true,
    "[stylus]": {
        "editor.formatOnType": false
    },
    "files.associations": {
        "*.stylintrc": "jsonc"
    },
    "eslint.format.enable": true,
    "jestrunner.debugOptions": {
        "skipFiles": [
            "<node_internals>/**/*.js",
            "node_modules/"
        ]
    }
}

上から3つの設定はフォーマッタの基本設定です。セーブ時、ペースト時、タイピング時 (主に改行時) に自動フォーマットが行われるように設定しています。
残りの設定は ESLint、Stylint、Jest の 設定となりますが、これらについては後述します。

TypeScript の設定

TypeScript の設定を行っておきます。
コンテナワークスペースに tsconfig.json を作成し、下記の内容で保存してください。

{
    "compilerOptions": {
        "baseUrl": "src",
        "jsx": "react",
        "moduleResolution": "node",
        "noEmitOnError": true,
        "noFallthroughCasesInSwitch": true,
        "noImplicitReturns": true,
        "noUnusedLocals": true,
        "noUnusedParameters": false, // Resolving with an underscore when a parameter cannot be removed, may leave the one when the parameter is used again.
        "removeComments": true,
        "resolveJsonModule": true,
        "strict": true,
        /* Applied only to client scripts. */
        "module": "CommonJS",
        "target": "es5",
        "sourceMap": true
    },
    "exclude": [
        "node_modules"
    ]
}

本記事では各項目の説明は省略させていただきますが、ソースコードチェックができるだけしっかり行われるよう設定してあります。例えば使われていないローカル変数があったり型の指定がされていなかったりしたらコンパイルエラーになります。
ただし、引数については使われていないものがあってもエラーにならないようにしてあります。引数は削除できない場合が多々あるからです。(その場合、引数名にアンダースコアを付けることでエラー回避できるのですが、後から引数を使うようになった時にアンダースコアを削除するようメンバーに徹底しきれなかったりするので。)

Lint の設定

Lint は設定した独自のコーディングルールに基づいてソースコードの詳細なチェックを行ってくれるツールです。命名規則や空白の使い方、1ファイルあたりの最大行数など、様々なルールを設定できます。
本記事では私が考えるコーディングルールを設定していますが、これを叩き台に適宜ルールを変更していただければと思います。

ESLint

コンテナワークスペースに .eslintrc ファイルを作成し、下記の内容で保存してください。

内容
{
    "env": {
        "browser": true,
        "es6": true,
        "node": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:@typescript-eslint/eslint-recommended"
    ],
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    "rules": {
        "max-len": [
            "off",
            80
        ],
        "max-lines": [
            "warn",
            300
        ],
        "max-statements": [
            "warn",
            30
        ],
        "max-params": [
            "warn",
            5
        ],
        "max-depth": [
            "warn",
            4
        ],
        "max-nested-callbacks": [
            "warn",
            {
                "max": 5
            }
        ],
        "require-jsdoc": [
            "warn",
            {
                "require": {
                    "FunctionDeclaration": true,
                    "MethodDefinition": true,
                    "ClassDeclaration": true,
                    "ArrowFunctionExpression": false,
                    "FunctionExpression": false
                }
            }
        ],
        "indent": [
            "error",
            4,
            {
                "SwitchCase": 1
            }
        ],
        "quotes": [
            "error",
            "double",
            {
                "avoidEscape": true
            }
        ],
        "semi": [
            "error",
            "always"
        ],
        "no-multiple-empty-lines": [
            "error",
            {
                "max": 2,
                "maxBOF": 0,
                "maxEOF": 0 // It allows one empty line.
            }
        ],
        "brace-style": [
            "error",
            "1tbs",
            {
                "allowSingleLine": false
            }
        ],
        "max-statements-per-line": [
            "error",
            {
                "max": 1
            }
        ],
        "one-var": [
            "error",
            "never"
        ],
        "one-var-declaration-per-line": [
            "error",
            "always"
        ],
        "comma-style": [
            "error",
            "last"
        ],
        "dot-location": [
            "error",
            "property"
        ],
        "no-useless-computed-key": [
            "error",
            {
                "enforceForClassMembers": true
            }
        ],
        "object-property-newline": [
            "error",
            {
                "allowAllPropertiesOnSameLine": true
            }
        ],
        "padded-blocks": [
            "error",
            "never"
        ],
        "wrap-iife": [
            "error",
            "inside"
        ],
        "camelcase": "error",
        "no-unused-vars": "off",
        "yoda": "error",
        "curly": "error",
        "arrow-spacing": "error",
        "arrow-parens": [
            "error",
            "as-needed",
            {
                "requireForBlockBody": true
            }
        ],
        "prefer-arrow-callback": "error",
        "object-curly-spacing": [
            "error",
            "always"
        ],
        "rest-spread-spacing": [
            "error",
            "never"
        ],
        "template-curly-spacing": "error",
        "block-spacing": "error",
        "array-bracket-spacing": "error",
        "semi-spacing": "error",
        "space-before-blocks": "error",
        "space-in-parens": "error",
        "key-spacing": "error",
        "keyword-spacing": "error",
        "space-infix-ops": "error",
        "comma-spacing": "error",
        "func-call-spacing": "error",
        "space-unary-ops": "error",
        "spaced-comment": "error",
        "use-isnan": "error",
        "new-parens": "error",
        "constructor-super": "off", // It is not needed, because VSCode already has the checker.
        "no-fallthrough": "error",
        "no-iterator": "error",
        "no-new-wrappers": "error",
        "no-path-concat": "error",
        "no-self-compare": "error",
        "no-throw-literal": "error",
        "no-undef-init": "error",
        "no-unreachable": "error",
        "no-unsafe-finally": "error",
        "no-unsafe-negation": "error",
        "no-useless-call": "error",
        "no-whitespace-before-property": "error",
        "eqeqeq": "error"
    }
}

設定できるルールについては下記のドキュメントから確認できます。

ESLint はソースコードのチェックだけでなく、一部のルールに対する自動修正機能を含んでいます。
コンテナワークスペース用 VSCode 拡張機能の設定で行った下記の設定によって、ファイル保存時や改行時などに ESLint による自動修正が行われるようになります。

    "eslint.format.enable": true,

Stylint

コンテナワークスペースに .stylintrc ファイルを作成し、下記の内容で保存してください。

内容
{
    "blocks": false,
    "brackets": "never",
    "colons": "always",
    "colors": "always",
    "commaSpace": "always",
    "commentSpace": "always",
    "cssLiteral": "never",
    "customProperties": [],
    "depthLimit": false,
    "duplicates": true,
    "efficient": false,
    "exclude": [],
    "extendPref": "@extends",
    "globalDupe": false,
    "groupOutputByFile": true,
    "indentPref": 4,
    "leadingZero": "always",
    "maxErrors": false,
    "maxWarnings": false,
    "mixed": true,
    "mixins": [],
    "namingConvention": "camelCase",
    "namingConventionStrict": true,
    "none": "never",
    "noImportant": true,
    "parenSpace": "never",
    "placeholders": false,
    "prefixVarsWithDollar": "always",
    "quotePref": "double",
    "reporterOptions": {
        "columns": [
            "lineData",
            "severity",
            "description",
            "rule"
        ],
        "columnSplitter": "  ",
        "showHeaders": false,
        "truncate": true
    },
    "semicolons": "never",
    "sortOrder": false,
    "stackedProperties": "never",
    "trailingWhitespace": "never",
    "universal": false,
    "valid": true,
    "zeroUnits": false,
    "zIndexNormalize": false
}

設定できるルールについては下記のドキュメントから確認できます。

Stylint のルールチェックは Stylint 拡張機能、自動フォーマットは Manta's Stylus Supremacy 拡張機能が行ってくれます。ただ、自動フォーマットはやや強力すぎる (ルール違反を一瞬たりとも許さず、単に次の項目を入力するために改行しただけでも消し去られます) ので、コンテナワークスペース用 VSCode 拡張機能の設定で行った下記の設定によってタイピング時の自動フォーマットを無効化しています。

    "[stylus]": {
        "editor.formatOnType": false
    },

また、.stylintrc ファイルは、そのままでは VSCode が JSON ファイルとして認識してくれないため、コンテナワークスペース用 VSCode 拡張機能の設定で行った下記の設定によって言語モードに JSON with Comments を設定しています。

    "files.associations": {
        "*.stylintrc": "jsonc"
    },

<注意>
stylint とよく似た名前の stylelint という Lint 系ツールがありますが、これは Stylus ではなく CSS や Sass の Lint ツールです。本記事の構成では Stylus しか使用しないため不要です。

WebPack の設定

WebPack はモジュールバンドラです。
開発したソースコードや画像などのファイル群をリリース用にバンドルしてくれます。
特にソースコードについては、TypeScript からコンパクトな JavaScript へのトランスパイルや、トランスパイルされた JavaScript を呼び出すコードを HTML に埋め込んでくれたりします。
更に、画像ファイルなども全て JavaScript コード化してひとまとめにすることができます。(ひとまとめにせず独立したファイルのまま含めることもできます。)

設定に使用できるファイルフォーマットは複数ありますが、本記事では強力なコード補完機能の恩恵を受けられる TypeScript にて記述します。

基本設定

コンテナワークスペースに webpack.config.ts ファイルを作成し、下記の内容で保存してください。

import * as webpack from "webpack";
import { CleanWebpackPlugin } from "clean-webpack-plugin";
import * as HtmlWebpackPlugin from "html-webpack-plugin";
import * as path from "path";

const IS_DEV = (process.env.NODE_ENV === "development");

const config: webpack.Configuration = {
    mode: !IS_DEV ? "production" : "development",
    devtool: !IS_DEV ? false : "source-map",
    entry: [
        "./src/client/index.tsx"
    ],
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "dist")
    },
    resolve: {
        extensions: [".js", ".ts", ".tsx", ".styl"],
        modules: [
            path.resolve(__dirname, "src/client"),
            path.resolve(__dirname, "node_modules")
        ],
        alias: {
            "react-dom": "@hot-loader/react-dom",
        },
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: [
                    "react-hot-loader/webpack",
                    "ts-loader"
                ]
            },
            {
                test: /\.styl$/,
                use: [
                    "style-loader",
                    {
                        loader: "css-loader",
                        options: {
                            importLoaders: 1,
                            sourceMap: IS_DEV,
                            modules: {
                                localIdentName: !IS_DEV ? "[hash:base32]" : "[path][name]__[local]",
                            }
                        }
                    },
                    "stylus-loader"
                ]
            },
            {
                test: {
                    not: [
                        /\.html?$/,
                        /\.jsx?$/,
                        /\.tsx?$/,
                        /\.styl$/
                    ]
                },
                use: {
                    loader: "url-loader",
                    options: {
                        /* Every file exceeding the size limit is deployed as a file with a name of the indicated rule. */
                        limit: 51200,
                        name: !IS_DEV ? "[hash:base32].[ext]" : "[path][name].[ext]"
                    }
                }
            },
            {
                test: /favicon\.ico$/,
                use: "file-loader?name=[name].[ext]"
            },
        ],
    },
    plugins: [
        new CleanWebpackPlugin({
        }),
        new HtmlWebpackPlugin({
            template: "./src/client/index.html",
            filename: "./index.html"
        })
    ]
};

export default config;

細かく説明すると非常に長くなってしまうので、要点をまとめておきます。

  • ビルド時にセットされる NODE_ENV 環境変数 (productiondevelopment のいずれか) に従い、設定を切り替える
  • development モード時はデバッグ補助用にソースマップ (トランスパイル前後のソースコードの紐付け情報) を生成する
  • バンドル前のエントリーポイントは ./src/client/index.tsx
  • バンドル後のエントリーポイントは bundle.js
  • 出力フォルダは dist
  • バンドルするソースコードが配置されているフォルダーは src/clientnode_modules
  • react-dom モジュールをインポートしようとすると代わりに @hot-loader/react-dom モジュールがインポートされる (後述の HMR で必要となる)
  • TypeScript ファイル (.ts or .tsx) は次のローダーを使ってバンドルを行う
    • ts-loader : TS から JS へのトランスパイル
    • react-hot-loader/webpack : 後述の HMR で必要となる
  • Stylus ファイル (.styl) は次のローダーを使ってバンドルを行う
    • stylus-loader : Stylus から CSS へのトランスパイル
    • css-loader : CSS のクラス名を衝突回避のためユニークな名前に変換する (後述の CSS Modules)
    • style-loader : CSS を JS で動的に出力する
  • HTML、JavaScript、TypeScript、Stylus 以外のファイルは、50KB 以下なら JS に直接埋め込み、50KB 以上ならファイル名を一意に変更した上で独立ファイルとしてバンドルする
  • favicon.ico はブラウザが名指しで直接取得しにくるので、ファイル名を維持して独立ファイルとしてバンドルする
  • CleanWebpackPlugin を使用し、バンドル処理開始時に前回の出力結果を全て削除する
  • HtmlWebpackPlugin を使用し、バンドル後の JS ファイル (bundle.js) を呼び出すコードを index.html に埋め込む

HMR 用補助サーバーの設定

HMR (Hot Module Replacement) というのは、開発時、ブラウザで Web アプリの動作を確認している時にソースコードを変更しても、サーバーの再起動もブラウザのリロードも行うことなく変更内容がブラウザに自動反映されるという機能です。(複雑な変更は追従しきれない場合があり、その場合は手動でリロードするようブラウザに表示されます。)

HMR の実現を補助する開発用サーバーが WebPack に用意されていますので、ここではそのサーバー設定を行います。HMR 利用時以外は不要なサーバーなので、基本設定とは別のファイルにします。(webpack-merge を使用して基本設定を HMR 用設定にマージします。)

コンテナワークスペースに webpack.config.hmr.ts ファイルを作成し、下記の内容で保存してください。

import * as webpack from "webpack";
import * as merge from "webpack-merge";
import config from "./webpack.config";
import "webpack-dev-server";

const hmrConfig: webpack.Configuration = merge(config, {
    devServer: {
        host: "localhost",
        port: 8080,
        contentBase: "src/client",
        historyApiFallback: true,
        inline: true,
        hot: true,
        open: false
    }
});

export default hmrConfig;

CSS Modules や画像ファイルを TypeScript で利用可能にする

TypeScript では型定義のないモジュールをインポートして使用するとエラーになってしまいます。
解決方法はいくつかありますが、本記事では手っ取り早く下記の定義を追加します。

  • 全ての Stylus ファイルに対して、string 配列がエクスポートされたモジュールとして型定義を追加
  • 全てのファイル (型定義が見つからなかった場合に限る) に対して、Any 型の値がデフォルトエクスポートされたモジュールとして型定義を追加

コンテナワークスペースに modules.d.ts ファイルを作成し、下記の内容で保存してください。

declare module "*.styl" {
    const classNames: {
        [className: string]: string
    };
    export = classNames;
}

declare module "*" {
    const value: any;
    export default value;
}

Jest の設定

Jest はテスティングフレームワークです。
本記事では Jest と React Testing Framework を組み合わせることで React コンポーネントのユニットテストを行います。

コンテナワークスペースに jest.ts ファイルを作成し、下記の内容で保存してください。

{
    "preset": "ts-jest",
    "moduleNameMapper": {
        "\\.(css|styl)$": "<rootDir>/node_modules/jest-css-modules"
    }
}

Jest で TypeScript をテストできるようにするため presetts-jest を設定します。
また、本来 WebPack (CSS Loader) を通さなければ処理できない CSS Modules (後述) という特殊なインポート方法を、WebPack を介さない Jest でも最低限エラー発生を回避して処理できるよう、moduleNameMapperjest-css-modules を設定しています。

デバッグ設定

次の5種類のデバッグを行えるよう設定を行います。

  • Chrome 上で動作しているクライアントサイドコードのデバッグ
  • Edge 上で動作しているクライアントサイドコードのデバッグ
  • サーバーサイドコードのデバッグ (既に起動しているサーバープロセスにアタッチしてデバッグ)
  • サーバーサイドコードのデバッグ (サーバープロセスを起動してデバッグ)
  • Jest でテスト実行しながらデバッグ

コンテナワークスペースに .vscode フォルダを作成し、下記の内容の launch.json ファイルを作成してください。

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug Client (Chrome)",
            "type": "chrome",
            "request": "launch",
            "trace": true,
            "sourceMaps": true,
            "url": "http://localhost:3000",
            "webRoot": "${workspaceFolder}",
            "sourceMapPathOverrides": {
                "webpack:///*": "${workspaceFolder}/*"
            },
            "skipFiles": [
                "<node_internals>/**/*.js",
                "node_modules"
            ]
        },
        {
            "name": "Debug Client (Edge)",
            "type": "edge",
            "request": "launch",
            "trace": true,
            "sourceMaps": true,
            "url": "http://localhost:3000",
            "webRoot": "${workspaceFolder}",
            "sourceMapPathOverrides": {
                "webpack:///*": "${workspaceFolder}/*"
            },
            "skipFiles": [
                "<node_internals>/**/*.js",
                "node_modules"
            ]
        },
        {
            "name": "Debug Server (Attach)",
            "type": "node",
            "request": "attach",
            "cwd": "${workspaceFolder}",
            "port": 9229,
            "protocol": "inspector",
            "internalConsoleOptions": "openOnSessionStart",
            "skipFiles": [
                "<node_internals>/**/*.js",
                "node_modules"
            ]
        },
        {
            "name": "Debug Server (Launch)",
            "type": "node",
            "request": "launch",
            // "preLaunchTask": "npm: build:dev",
            "runtimeArgs": [
                "--nolazy",
                "-r",
                "ts-node/register"
            ],
            "args": [
                "${workspaceFolder}/src/server/server.ts"
            ],
            "cwd": "${workspaceFolder}",
            "protocol": "inspector",
            "internalConsoleOptions": "openOnSessionStart",
            "env": {
                "TS_NODE_IGNORE": "false"
            },
            "skipFiles": [
                "<node_internals>/**/*.js",
                "node_modules"
            ]
        }
    ]
}

Jest のデバッグ実行は Jest Runner 拡張機能を使用して行うため launch.json では設定できません。代わりにコンテナワークスペース用 VSCode 拡張機能の設定で行った下記の設定によって Jest のデバッグ設定を行っています。

    "jestrunner.debugOptions": {
        "skipFiles": [
            "<node_internals>/**/*.js",
            "node_modules/"
        ]
    },

実装

ようやくここまで辿り着きました。(本記事を書き始めて1週間)
ここからは実装を行っていきます。

本記事では、ハードコーディングされた "Hello World." というメッセージを表示する Home ページと、サーバーに実装した hello API から取得した "Hello World!" というメッセージを動的に表示する Home Work ページを用意し、これらをメインページ内でタブ切り替えのように行き来できるようにします。二つのページには異なる URL を割り当てますので、URL 直打ちで最初から Home Work ページを表示させることもできます。
ezgif-1-7ea4b3d7020e.gif
Home Work ページではメッセージを動的に取得していることがわかりやすいよう、待機中は "Loading..." と表示しています。
あとデザインがクソダサいですが気にしないように。

ソースコードフォルダーの作成

コンテナワークスペースに下記のフォルダー構造を作成しておきます。

  • コンテナワークスペース
    • src
      • client
      • server

本記事ではこれ以上深いフォルダーはあえて作成しませんが、実開発ではフォルダー構成は重要です。
基本的な考え方として下記のページが参考になるかと思います。(後半を読み飛ばさないように)

サーバーサイド:loggers.ts

log4js を使用してシステムログ用のロガーとアクセスログ用のロガーを実装します。 (クライアントサイドはブラウザ上で実行されるためログは取れません。)
log4js の公式ドキュメントは下記にあります。

src/server フォルダーに loggers.ts ファイルを作成し、下記の内容で保存してください。

import * as log4js from "log4js";

const IS_DEV = process.env.NODE_ENV === "development";

log4js.configure({
    appenders: {
        "system_console": {
            type: "console",
            layout: {
                type: "pattern",
                pattern: "%[[%d] [%p]%] %c - %m [%f:%l:%o]"
            },
        },
        "system_file": {
            type: "file",
            filename: "logs/system/system.log",
            maxLogSize: 5 * 1024 * 1024,
            backups: 5,
            compress: true,
            layout: {
                type: "pattern",
                pattern: "[%d] [%p] %c - %m [%f:%l:%o]"
            },
        },
        "access_console": {
            type: "console",
        },
        "access_file": {
            type: "dateFile",
            filename: "logs/access/access.log",
            pattern: "yyyy-MM-dd",
            alwaysIncludePattern: true,
            keepFileExt: true,
            compress: true,
            daysToKeep: 5,
        }
    },
    categories: {
        "default": {
            appenders: ["system_console"],
            level: !IS_DEV ? "info" : "all",
            enableCallStack: true,
        },
        "system": {
            appenders: ["system_console", "system_file"],
            level: !IS_DEV ? "info" : "all",
            enableCallStack: true,
        },
        "access": {
            appenders: !IS_DEV ? ["access_file"] : ["access_console", "access_file"],
            level: !IS_DEV ? "info" : "all",
        }
    }
});

export const defaultLogger = log4js.getLogger();
export const systemLogger = log4js.getLogger("system");
export const accessLogger = log4js.getLogger("access");
export const accessLogConnector = log4js.connectLogger(accessLogger, { level: "auto" });

システムログは logs/system フォルダーに保存されます。
ログサイズが 5MB を超えたらログを圧縮してローテーションを行うように設定しています。

アクセスログは logs/access フォルダーに保存されます。
毎日ログを圧縮してローテーションを行うように設定しています。

また、NODE_ENV 環境変数が development の時 (開発時) は全てのレベルのログを出力し、production の時 (本番) は fatalerrorwarninfo のログを出力します。

サーバーサイド:server.ts

Express を使用してサーバーを実装します。
Express の公式ドキュメントは下記にあります。

src/server フォルダーに server.ts ファイルを作成し、下記の内容で保存してください。

import * as express from "express";
import * as process from "process";
import * as path from "path";
import fetch from "node-fetch";
import { systemLogger as logger, accessLogConnector } from "./loggers";

const clientRootPath = "dist";
const clientRootAbsolutePath = path.join(process.cwd(), clientRootPath);

const server = express();

server.use(accessLogConnector);
server.use(express.static(clientRootPath));

server.get("/api/hello", (req, res) => {
    res.send({ message: "Hello World!" });
});

server.get("*", (req, res) => {
    if (process.env.HMR === "true") {
        fetch(
            `http://localhost:8080${req.originalUrl}`,
            {
                method: req.method,
                headers: req.headers as { [key: string]: string }
            }
        ).then(innerRes => new Promise((resolve, reject) => {
            innerRes.body.pipe(res);
            res.on("close", resolve);
            res.on("error", reject);
        }));
        return;
    }
    res.sendFile("index.html", { root: clientRootAbsolutePath });
});

server.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
    logger.error(err);
    next(err);
});

server.listen(3000, () => {
    logger.info("server running");
});

dist フォルダー内の静的ファイルへのアクセスについてはそのまま該当のファイルを返します。

サンプル実装として、/api/hello という URL にアクセスされたら { message: "Hello World!" } という JSON データを返すようにしています。実際の開発では、リバースプロキシ化してバックエンドサービスに処理を委譲することが多いかと思います。

上記のいずれにも当てはまらない場合、通常は index.html を返します。ここまで特に触れませんでしたが、本記事で構築するのは SPA (Single Page Application) と呼ばれる形式のアプリケーションで、クライアント内で完結するルーティングを行えるため、サーバーはとにかく index.html を返してあげる必要があります。
ただし、HMR 環境変数が true の場合には HMR 用補助サーバーへの簡易リバースプロキシとして動作します。HMR 用補助サーバーが静的コンテンツをホスティングするためです。

また、エラーハンドラを追加してエラーをシステムログに記録するようにしてあります。

そして最後にポート番号 3000 を使用してサーバーを起動しています。

クライアントサイド:index.html

コンテンツは React で実装していきますので index.html は非常にコンパクトです。

src/client フォルダーに index.html ファイルを作成し、下記の内容で保存してください。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sample</title>
</head>

<body>
    <div id="root"></div>
</body>

</html>

body には id="root" を設定した div 要素を配置するのみです。
React がこの div 要素に対して動的にコンポーネントをレンダリングします。

クライアントサイド:index.tsx

index.tsx はエントリーポイントです。ここからクライアントサイドの処理が開始されます。

src/client フォルダーに index.tsx ファイルを作成し、下記の内容で保存してください。

import { hot } from "react-hot-loader/root"; // Must be imported before "react" and "react-dom".
import * as React from "react";
import * as ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

const Root = () => {
    return (
        <BrowserRouter>
            <App />
        </BrowserRouter>
    );
};

ReactDOM.render(<Root />, document.getElementById("root"));

export default hot(Root);

エントリーポイントでは、HMR に対応するための細工と、React Router (後述) に対応するための細工を行います。具体的なコンテンツの実装は、次に実装する App コンポーネントから行っていきます。
具体的には、React Router DOM の BrowserRouter という特殊なコンポーネントで App コンポーネントをラップし Root コンポーネントとして定義し、更にその Root コンポーネントを React Hot Loader の hot 関数でラップしたものを、先ほど index.html に配置した div 要素に対してレンダリングしています。

なお、NODE_ENV 環境変数が production の時には hot 関数は何も行わず引数で受け取ったコンポーネントをそのまま返しますので、hot 関数は除去せずそのままリリースして大丈夫です。

<補足>
TypeScript の中に HTML のタグのような記述が混ざっていることに戸惑う人もいるかと思います。これは React の JSX という機能で、HTML のようなタグ構文を使用してオブジェクト (仮想 DOM コンポーネント) の生成を行うことができます。(トランスパイルすると React.createElement() を呼び出す普通の JavaScript コードに変換されます。その関係で、import * as React from "react"; を必ず記述しておく必要があります。)
上述の Root 関数の場合、App コンポーネントを生成し、さらにそれを子要素として渡して BrowserRouter コンポーネントを生成しています。この Root 関数も仮想 DOM を生成して返す関数ですので、ReactDOM.render() の引数部分のように JSX 構文で Root コンポーネントを生成することができます。

クライアントサイド:App.tsx

ここからが UI を作りこんでいくメインプログラミングとなります。

App コンポーネント (メインページ) は、 自コンポーネント内に Home コンポーネント (Home ページ) を表示する "Home" リンクと、同じく自コンポーネント内に HomeWork コンポーネント (Home Work ページ) を表示する "Home Work" リンクを持ちます。リンクをクリックしても App コンポーネント自体は消えたり再読み込みされたりせず、Home コンポーネントと HomeWork コンポーネントの切り替えだけが行われます。どちらのコンポーネントも JavaScript コード自体は最初からブラウザに読み込まれていますので、切り替え時にサーバー通信は発生しません。(HomeWork コンポーネントの実装がサーバーの API を叩くのでそれに関しての通信は発生しますが。)

また、Home は "/" に、HomeWork は "/homework" にルーティングしています。これによりユーザーが URL をブックマークに登録してショートカット表示するということが可能になります。

src/client フォルダーに App.tsx ファイルを作成し、下記の内容で保存してください。

import * as React from "react";
import { Switch, Route, Link } from "react-router-dom";

import "./favicon.ico";
import * as styles from "./App.styl";
import Home from "./Home";
import HomeWork from "./HomeWork";

const App = () => {
    return (
        <>
            <Link className={styles.menuButton} to="/">Home</Link>
            <Link className={styles.menuButton} to="/homework">Home Work</Link>
            <Switch>
                <Route exact path="/" component={Home} />
                <Route exact path="/homework" component={HomeWork} />
            </Switch>
        </>
    );
};

export default App;

コンポーネントのルーティングや切り替えには React Router DOM を使用しています。

favicon.icoApp.stylus がインポートできるのは WebPack のおかげです。
favicon.ico はブラウザが決め打ちでアクセスしてくるのでインポートするだけで良いですが、例えば img タグで読み込ませたい場合は import favicon from "/favicon.ico"; として <img src={favicon} /> というように指定します。
App.stylus もインポートするだけでスタイルが適用されるのですが、クラスセレクタはそのままでは適用されませんので、className={styles.menuButton} というようにして CSS クラス名をコンポーネントにセットする必要があります。CSS クラス名はバンドル時に、衝突回避のため一意な名前に変換されます。このように、CSS ファイルや Stylus ファイルを JavaScript/TypeScript モジュールのように扱う機能のことを CSS Modules と言います。

<補足>
JSX 構文で仮想 DOM を作成する際には、必ず単一の親要素を用意する必要があります。
今回のように特に親要素に該当するコンポーネントや HTML 要素が無い場合には、Fragment コンポーネント (<> または <Fragment>) を使用します。

クライアントサイド:favicon.ico

src/client フォルダーに favicon.ico ファイルを作成してください。
用意しないとビルドエラーが発生しますので中身は空でも良いので作成しておいてください。

クライアントサイド:App.styl

src/client フォルダーに App.styl ファイルを作成し、下記の内容で保存してください。

$basicForegroundColor = #0000A0
$basicBackgroundColor = #A0A0FF

body
    color: $basicForegroundColor
    background-color: $basicBackgroundColor

.menuButton
    margin-right: 16px

<注意>
スタイルシートのファイル間の依存方向が、コンポーネントの依存方向と逆行しないよう気を付けてください。
本記事では App.styl しか用意しませんが、例えば Home.styl や HomeWork.styl を用意する場合、Home.styl や HomeWork.styl から App.styl を参照 (インポート) してはいけません。base.styl を用意してそちらを参照させるなど、適切に設計しましょう。

クライアントサイド:Home.tsx

単純に "Hello World." と表示するだけのページです。

src/client フォルダーに Home.tsx ファイルを作成し、下記の内容で保存してください。

import * as React from "react";

const Home = () => {
    return (
        <h1>Hello World.</h1>
    );
};

export default Home;

クライアントサイド:HomeWork.tsx

サーバーに実装した hello API から取得した "Hello World!" というメッセージを動的に表示するページです。

src/client フォルダーに HomeWork.tsx ファイルを作成し、下記の内容で保存してください。

import * as React from "react";
import { useState, useEffect } from "react";

const HomeWork = () => {
    const [message, setMessage] = useState("Loading...");
    useEffect(() => {
        fetch("/api/hello")
            .then(response => response.json())
            .then(json => setMessage(json.message));
    }, []);
    return (
        <h1>{message}</h1>
    );
};

export default HomeWork;

hello API との通信にはfetch 関数 (Web 通信を行う非同期関数) を使っていますが、取得したデータを動的にコンテンツに埋め込むには React Hooks (useStateuseEffect) を利用する必要があります。

HomeWork 関数は h1 タグに message 変数の中身を埋め込んで返すようになっていて、この message 変数は通常の変数ではなく useState によって用意されたステート変数という特殊な変数です。
"Loading..." を初期値として与えていますので、HomeWork ページ表示直後はこの "Loading..." が表示され、その後 hello API からデータが取得されると、setMessage 関数を通じて message ステート変数が書き換えられ、"Hello World!" と表示されます。
このように、動的な書き換えを行うには必ずステート変数を利用する必要があります。

そしてここからが少しややこしいのですが、実は HomeWork 関数は初期化時に1回だけ呼ばれるわけではなく、レンダー時 (ステート変数書き換え時) に毎回呼びなおされます。
ですので hello API との通信を HomeWork 関数にべた書きしてしまうと、ステート変数書き換え後に再び hello API 呼び出しが行われ、またステート変数が書き換えられ…と無限ループしてしまいます。
このように、DOM のレンダーとは独立して動作するべき処理は副作用と呼ばれ、useEffect 関数を通じてレンダー後に処理する必要があります。
といっても、単に処理のタイミングをレンダー後に移動しただけでは、ループすることに変わりありません。今回の場合は特に useEffect 関数の第二引数が重要です。ここには、値が更新されるたびに副作用を再実行する必要のあるステート変数のリストを渡すようになっています。そしてここに空のリストを渡すと、副作用の実行を初回レンダー後の1回のみに制限することができます。ちなみに、第二引数を省略した場合はレンダー毎に副作用が実行されてしまうので、空のリストをしっかりと渡す必要があります。

React Hooks のより詳しい解説については下記のドキュメントを参照してください。

実行

一通りの実装が完了しましたので実行させてみましょう。
Explorer サイドバーの NPM Scripts に、package.jsonscripts で定義したスクリプトの一覧が並んでいますので、run:hmr を実行します。
image.png
実行するとサーバー起動とビルドが行われます。
ezgif-1-98b27c5cd798.gif
Terminal に [1] ℹ 「wdm」: Compiled successfully. と出力されたら成功です。
ブラウザを立ち上げて http://localhost:3000 にアクセスしてみてください。
ezgif-1-7ea4b3d7020e.gif
Home と HomeWork がうまく切り替わったでしょうか。

クライアントサイドコードの変更

HMR 用補助サーバーの設定で説明しましたとおり、クライアントサイドコードの変更は HMR 機能によってブラウザをリロードすることなく即座に反映されます。
試しに Home.tsx の "Hello World." を "Hello HMR." に変えてみたり、App.styl をいじってみてください。
ezgif-1-832e51b56c30.gif
ちなみにこの動画では、撮影の都合上 Browser Preview という拡張機能を使用して VSCode 内にブラウザを表示させていますが、もちろん普通のブラウザでも HMR はちゃんと動作します。

サーバーサイドコードの変更

サーバーサイドコードの変更は ts-node-dev によって検出され、サーバーが自動で再起動されます。

サーバーの停止

基本的にサーバーは起動しっぱなしで良いのですが、ターミナル上で Ctrl + C を押せばサーバーが停止します。

デバッグ

特にバグがあるわけでもないのですが、次はデバッグをしてみます。

クライアントサイドコードのデバッグ

HomeWork 関数をデバッグしてみます。

  • 準備
    1. サーバーを起動しておきます。
    2. Run サイドバーの上部にあるドロップダウンリストから launch.json で定義したデバッグ設定を選べますので、Debug Client (Chrome) もしくは Debug Client (Edge) を選択しておきます。
    3. HomeWrok.tsx の5行目にカーソルを移動し、F9 キーにてブレークポイントを設置します。
  • デバッグ
    1. F5 キーにてデバッグを開始します。
    2. 自動起動されたブラウザにて HomeWork ページを表示すると、ブレークポイント (HomeWork.tsx の5行目) でブレーク (一時停止) します。

ブレーク後は下記の操作が行えます。

操作 キー
ステップオーバー3 F10
ステップイン4 F11
再開 F5
停止5 6 Shift + F5

他にも、変数にカーソルをあてて変数の中身を確認したり、DEBUG CONSOLE から変数の中身を書き換えたりすることも可能です。

サーバーサイドコードのデバッグ

hello API をデバッグしてみます。

  • 準備
    1. サーバーを起動しておきます。
    2. Run サイドバーの上部にあるドロップダウンリストから、Debug Server (Attach) を選択しておきます。
    3. server.ts の16行目にカーソルを移動し、F9 キーにてブレークポイントを設置します。
  • デバッグ
    1. F5 キーにてデバッグを開始します。
    2. ブラウザ (自動起動はしません) にて HomeWork ページを表示すると、ブレークポイント (Server.ts の16行目) でブレーク (一時停止) します。

ブレーク後は、クライアントサイドコードのデバッグ時と同じ操作が可能です。

なお、サーバーの起動時の処理をデバッグしたい場合には、サーバーを停止し Debug Server (Launch) を使用してデバッグを開始してください。

ユニットテスト

次はユニットテストを用意して実行してみます。本当はテスト駆動開発で実装より先にテストを用意したかったのですが、記事の構成の都合から諦めて後回しにしました。

各テストケースの基本的な流れは次のようになります。

  1. testing-library の render 関数でテスト対象コンポーネントを疑似的にレンダーする
  2. テストしたいシナリオを fireEvent でエミュレートする
  3. expect でコンポーネントの状態を検証する

ユニットテストフレームワークの詳細は下記を参照してください。

App コンポーネントのテスト

App コンポーネントに対して次の3つのテストケースを用意します。

  • 最初に Home が表示されること
  • メニューから Home を表示できること
  • メニューから Home Work を表示できること

src/client フォルダーに App.spec.tsx ファイルを作成し、下記の内容で保存してください。

import * as React from "react";
import { MemoryRouter } from "react-router-dom";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

const homeMock = jest.fn(() => <></>);
jest.mock("./Home", () => ({ __esModule: true, default: homeMock }));

const homeworkMock = jest.fn(() => <></>);
jest.mock("./HomeWork", () => ({ __esModule: true, default: homeworkMock }));

import App from "./App";

afterEach(cleanup);
afterEach(jest.clearAllMocks);
afterAll(() => {
    jest.unmock("./Home");
    jest.unmock("./HomeWork");
});

describe("App", () => {
    it("最初に Home が表示されること", () => {
        render(<MemoryRouter><App /></MemoryRouter>);
        expect(homeMock).toBeCalled();
    });

    it("メニューから Home を表示できること", () => {
        const app = render(<MemoryRouter><App /></MemoryRouter>);
        expect(homeMock).toBeCalled();
        homeMock.mockClear();

        const menuHomeButton = app.getByText("Home", { exact: true });
        fireEvent.click(menuHomeButton);

        expect(homeMock).toBeCalled();
    });

    it("メニューから Home Work を表示できること", () => {
        const app = render(<MemoryRouter><App /></MemoryRouter>);
        expect(homeworkMock).not.toBeCalled();

        const menuHomeWorkButton = app.getByText("Home Work", { exact: true });
        fireEvent.click(menuHomeWorkButton);

        expect(homeworkMock).toBeCalled();
    });
});

MemoryRouter コンポーネントについて
App コンポーネントは React Router DOM を使用しているため、Router コンポーネントでラップする必要があります。
実行時は index.tsx にて BrowserRouter コンポーネントでラップしていますが、テスト時は MemoryRouter コンポーネントでラップします。

コンポーネントのモック化について
App コンポーネントのテストに専念するため、App.tsx をインポートする前に Home コンポーネントと HomeWork コンポーネントを下記のようにしてモック化しています。

const homeMock = jest.fn(() => <></>);
jest.mock("./Home", () => ({ __esModule: true, default: homeMock }));

const homeworkMock = jest.fn(() => <></>);
jest.mock("./HomeWork", () => ({ __esModule: true, default: homeworkMock }));

これは Home.tsx や HomeWork.tsx を下記のようなコードで一時的に上書きしているかのような効果を持ちます。

export default () => {
    return (
        <></>
    );
};

モックは更に、自分が呼び出しされたかどうか等を調べられるようになっています。

expect(homeMock).toBeCalled();

これにより、App コンポーネントが Home コンポーネントや HomeWork コンポーネントを適切なタイミングで呼び出しているかどうかをテストできるわけです。

モック化が Home.spec.tsx 以外のテストケースに影響しないよう、クリーンアップ処理の登録も忘れずに行います。

afterAll(() => {
    jest.unmock("./Home");
    jest.unmock("./HomeWork");
});

Home コンポーネントのテスト

Home コンポーネントに対して次の1つのテストケースを用意します。

  • メッセージが表示されること

src/client フォルダーに Home.spec.tsx ファイルを作成し、下記の内容で保存してください。

import * as React from "react";
import { render, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import Home from "./Home";

afterEach(cleanup);
afterEach(jest.clearAllMocks);

describe("HomeWork", () => {
    it("メッセージが表示されること", () => {
        const app = render(<Home />);
        const heading = app.getByRole("heading");
        expect(heading).toHaveTextContent("Hello World.");
    });
});

HomeWork コンポーネントのテスト

HomeWork コンポーネントに対して次の1つのテストケースを用意します。

  • サーバーから取得したメッセージが表示されること

src/client フォルダーに HomeWork.spec.tsx ファイルを作成し、下記の内容で保存してください。

import * as React from "react";
import { render, cleanup, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import * as fetchMock from "fetch-mock";

import HomeWork from "./HomeWork";

afterEach(cleanup);
afterEach(jest.clearAllMocks);
afterEach(fetchMock.restore);

describe("HomeWork", () => {
    it("サーバーから取得したメッセージが表示されること", async () => {
        fetchMock.get("/api/hello", (url, opts) => ({
            status: 200,
            body: {
                message: "Hello Mock."
            }
        }));
        const app = render(<HomeWork />);
        const heading = app.getByRole("heading");
        expect(heading).toHaveTextContent("Loading...");
        await waitFor(() => {
            expect(heading).toHaveTextContent("Hello Mock.");
        });
    });
});

fetch 関数のモック化について
HomeWork コンポーネントのテストに専念するため、fetch 関数 (hello API への通信) を下記のようにしてモック化しています。

        fetchMock.get("/api/hello", (url, opts) => ({
            status: 200,
            body: {
                message: "Hello Mock."
            }
        }));

今回のモック化は各テストケースがテストシナリオに合わせて用意している形ですので、他のテストケースに影響しないようテストケース実行のたびにクリーンアップを行います。

afterEach(fetchMock.restore);

waitFor 関数について
HomeWork コンポーネントはレンダー後に副作用として hellow API 呼び出しとメッセージ更新を行います。副作用はレンダースレッドがフリーになってから、つまりテストケースの実行が完了してからでないと本来実行されません。そこで、テストケースの中で waitFor 関数を使用し、スレッドを一旦フリーにしてあげる必要があります。
waitFor 関数はレンダーされた仮想 DOM の変更を一定 (50ms) 間隔で監視しながら待機する関数です。仮想 DOM が更新されると、引数に指定されたコールバック関数を実行して waitFor 関数が完了します。

<注意>
現行の Testing Library では、型定義ファイルに不備があるため waitFor 関数のインポートが行えません。近いうちに Testing Library のアップデートで修正されると思いますが、それまでの間は、node_modules/@types/testing-library__dom/index.d.ts に下記の型定義を追加して保存しておくことで回避することができます。

export function waitFor<T>(
    callback: () => void,
    options?: {
        container?: HTMLElement;
        timeout?: number;
        interval?: number;
        mutationObserverOptions?: MutationObserverInit;
    },
): Promise<T>;

テストの個別実行とデバッグ

テストを個別に実行するには、テストコードを右クリックして Run Jest を実行します。
ezgif-1-b04e1c98b6b7.gif
Run Jest ではなく Debug Jest を実行することでデバッグも可能です。
ezgif-1-bb2c8e5571b7.gif

テストの全体実行とカバレッジ

テスト全体を実行するには、Explorer サイドバーの NPM Scripts から、test を実行します。
image.png
結果と共にカバレッジも出力するようにしてあります。
image.png
カバレッジの詳細レポートはコンテナワークスペースの coverage フォルダーに出力されています。coverage/lcov-report/index.html をブラウザで開けば、どの行が何回実行されたかといった情報も確認できます。
image.png

本番用コンテナ

本記事もいよいよ大詰めです。
本番用コンテナは Dockerfile + docker-compose.yml で作成します。

Dockerfile

コンテナワークスペースに Dockerfile を作成し、下記の内容で保存してください。

FROM node:12.16-buster-slim AS base
WORKDIR /app
COPY ["package.json", "package-lock.json", "tsconfig.json", "./"]
RUN npm install --production --silent

FROM base AS build
WORKDIR /app
RUN npm install --silent
COPY . .
RUN npm run build

FROM base AS product
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/src/server ./src/server
EXPOSE 3000

このコードでは、マルチステージビルドで次の3つのイメージを作成しています。

イメージ名 概要
base ビルド用イメージと本番イメージのベースイメージ。
build ビルド用の中間イメージ。開発環境用の Node モジュールのインストール、プロジェクトフォルダ内の全てのファイルのコピーを行い、package.json に定義した build スクリプトを使用してビルドを行う。
product 本番イメージ。build イメージで生成したビルド成果物が配置される。開発環境用の Node モジュールはインストールされない。

大元のベースイメージには node の 12.16-buster-slim を使用しています。これは、現時点での Node.js の安定バージョンと Debian の最新バージョンのスリム版の組み合わせです。
node の Docker イメージは下記ページから確認できます。

.dockerignore

コンテナワークスペースに .dockerignore を作成し、下記の内容で保存してください。

**/coverage
**/dist
**/logs
**/node_modules

ビルド時や実行時に生成されるディレクトリがイメージ内にコピーされないようにしています。
他にもビルドに不要なファイルはありますが、特別大きなファイルであったりしない限り、そこまで厳密にリストアップする必要はありません。

docker-compose.yml

docker-compose.yml は下記の内容で、{project name} 1箇所を適切に置き換えた上で保存してください。{project name}package.json に合わせて小文字で記述します。

version: '2.1'

services:
    app:
        image: {project name}
        build: .
        ports:
            - 3001:3000
        command: npm start

コンテナ起動設定を app というサービス名で登録しています。
開発コンテナで既にポート3000をそのまま使用しているため、こちらはポート3001にフォワーディングしておきます。
また、コンテナ起動時にサーバーが起動するよう npm start コマンドを設定しておきます。

イメージビルドとコンテナ起動

ターミナルで下記のコマンドを実行すると、イメージがビルドされ、コンテナが起動します。(ビルド済みのイメージをそのまま起動する場合は --build オプションを付けずに実行します。)

docker-compose up --build app

ブラウザを立ち上げて http://localhost:3001/ にアクセスできることを確認してください。

コンテナにシェルで接続

コンテナ起動後、別ターミナルで下記のコマンドを実行すると、コンテナにシェルで接続することができます。

docker-compose exec app /bin/bash

本番用コンテナ内の調査などで役立ちますが、本番用コンテナにはツール類がほとんどインストールされていませんので、状況に応じて apt-get でツールのインストールを行う必要があります。

コンテナ停止

コンテナ起動後、別ターミナルで下記のコマンドを実行すると、コンテナが停止します。

docker-compose down

終わりに

コンテナ生活、如何でしたでしょうか。
といっても、コンテナに引きこもっていることを忘れてしまうくらいに普通に開発が行えてしまうので、あまり実感は無いかもしれません。
開発コンテナの利点は環境周りのトラブルの低減です。
今後は是非、コンテナにひきこもって快適なフロントエンド開発を満喫していただければと思います。


番外編:CSS フレームワークについて

本編では扱いませんでしたが、実開発ではデザインについても考えなければいけません。
世の中には Bootstrap をはじめ様々な CSS フレームワークがありますので、それらから選択するのが無難です。ちなみに React 開発においては、Material UI という CSS フレームワークが一番人気だそうです。
参考までに、本記事の構成に Material UI の AppBar コンポーネントを取り入れた例をご紹介しておきます。

Material UI のインストール

npm で開発環境にインストールします。
Material UI 本体の他に、アイコン集もインストールします。

npm install -D @material-ui/core @material-ui/icons 

App コンポーネントの実装

src/client/App.tsx ファイルを下記の内容に書き換えます。

import * as React from "react";
import { Switch, Route, Link } from "react-router-dom";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import MenuIcon from "@material-ui/icons/Menu";
import Drawer from "@material-ui/core/Drawer";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import Container from "@material-ui/core/Container";
import HomeIcon from "@material-ui/icons/Home";
import HomeWorkIcon from "@material-ui/icons/HomeWork";

import "./favicon.ico";
import * as styles from "./App.styl";
import Home from "./Home";
import HomeWork from "./HomeWork";

const App = () => {
    const [drawerState, setDrawerState] = React.useState(false);
    const toggleDrawer = (state: boolean) => (event: any) => {
        if (event.type === "keydown" && (event.key === "Tab" || event.key === "Shift")) {
            return;
        }
        setDrawerState(state);
    };

    return (
        <>
            <AppBar position="static">
                <Toolbar>
                    <IconButton edge="start" className={styles.menuButton} color="inherit" aria-label="menu" onClick={toggleDrawer(true)}>
                        <MenuIcon />
                    </IconButton>
                    <Typography variant="h6" className={styles.title}>
                        Sample
                    </Typography>
                </Toolbar>
            </AppBar>
            <Drawer open={drawerState} role="presentation" className={styles.list} onClose={toggleDrawer(false)} onClick={toggleDrawer(false)} onKeyDown={toggleDrawer(false)}>
                <List>
                    <ListItem button key="home" component={Link} to="/">
                        <ListItemIcon><HomeIcon /></ListItemIcon>
                        <ListItemText primary="Home" />
                    </ListItem>
                    <ListItem button key="homework" component={Link} to="/homework">
                        <ListItemIcon><HomeWorkIcon /></ListItemIcon>
                        <ListItemText primary="Home Work" />
                    </ListItem>
                </List>
            </Drawer>
            <Container maxWidth="sm">
                <Switch>
                    <Route exact path="/" component={Home} />
                    <Route exact path="/homework" component={HomeWork} />
                </Switch>
            </Container>
        </>
    );
};

export default App;

src/client/App.styl ファイルを下記の内容に書き換えます。

$spacing = 8px
$basicBackgroundColor = #A0A0FF
$basicForegroundColor = #0000A0

$heading
    color: $basicForegroundColor


:global(body)
    background-color: $basicBackgroundColor

.root
    flex-grow: 1

.menuButton
    margin-right: $spacing * 2

.title
    flex-grow: 1

.list
    width: 250

実行

実行すると見慣れたバーが追加されています。
image.png

メニューボタンを押すとメニューが開きます。
image.png

テスト

src/client/App.spec.tsx ファイルを下記の内容に書き換えます。

import * as React from "react";
import { MemoryRouter } from "react-router-dom";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

const homeMock = jest.fn(() => <></>);
jest.mock("./Home", () => ({ __esModule: true, default: homeMock }));

const homeworkMock = jest.fn(() => <></>);
jest.mock("./HomeWork", () => ({ __esModule: true, default: homeworkMock }));

import App from "./App";

afterEach(cleanup);
afterEach(jest.clearAllMocks);
afterAll(() => {
    jest.unmock("./Home");
    jest.unmock("./HomeWork");
});

describe("App", () => {
    it("最初に Home を表示すること", () => {
        render(<MemoryRouter><App /></MemoryRouter>);
        expect(homeMock).toBeCalled();
    });

    it("メニューを開けること", () => {
        const app = render(<MemoryRouter><App /></MemoryRouter>);

        const menuButton = app.getByLabelText("menu");
        fireEvent.click(menuButton);

        expect(app.getByRole("presentation")).not.toBeNull();
    });

    it("メニューをクリックするとメニューが閉じること", () => {
        const app = render(<MemoryRouter><App /></MemoryRouter>);

        const menuButton = app.getByLabelText("menu");
        fireEvent.click(menuButton);

        const menu = app.getByRole("presentation");
        fireEvent.click(menu);

        expect(() => app.getByRole("presentation")).toThrow();
    });

    it("キーを押下するとメニューが閉じること", () => {
        const app = render(<MemoryRouter><App /></MemoryRouter>);

        const menuButton = app.getByLabelText("menu");
        fireEvent.click(menuButton);

        const menu = app.getByRole("presentation");
        fireEvent.keyDown(menu, { key: "a", code: 65 });

        expect(() => app.getByRole("presentation")).toThrow();
    });

    it.each([
        ["Tab", 9],
        ["Shift", 16]
    ])("一部のキーを押下してもメニューが閉じないこと [%s, %d]", (key, code) => {
        const app = render(<MemoryRouter><App /></MemoryRouter>);

        const menuButton = app.getByLabelText("menu");
        fireEvent.click(menuButton);

        const menu = app.getByRole("presentation");
        fireEvent.keyDown(menu, { key, code });

        expect(app.getByRole("presentation")).not.toBeNull();
    });

    it("メニューから Home を表示できること", () => {
        const app = render(<MemoryRouter><App /></MemoryRouter>);
        expect(homeMock).toBeCalled();
        homeMock.mockClear();

        const menuButton = app.getByLabelText("menu");
        fireEvent.click(menuButton);

        const menuHomeButton = app.getByText("Home", { exact: true });
        fireEvent.click(menuHomeButton);

        expect(() => app.getByRole("presentation")).toThrow();
        expect(homeMock).toBeCalled();
    });

    it("メニューから Home Work を表示できること", () => {
        const app = render(<MemoryRouter><App /></MemoryRouter>);
        expect(homeworkMock).not.toBeCalled();

        const menuButton = app.getByLabelText("menu");
        fireEvent.click(menuButton);

        const menuHomeWorkButton = app.getByText("Home Work", { exact: true });
        fireEvent.click(menuHomeWorkButton);

        expect(() => app.getByRole("presentation")).toThrow();
        expect(homeworkMock).toBeCalled();
    });
});

いい感じです。
image.png

おまけ

本編に挟み込めなかった小ネタをいくつか。

ワークスペース外のファイルを開く

開発コンテナ内のファイルは、コンテナワークスペースには含まれていなくても VSCode で開くことができます。
下記のコマンドを {File Path} を置き換えて実行するとファイルが開かれます。

code {File Path}

Source Control サイドバーが git を検出しなくなった場合の対処法

ワークスペースをコピーして開発コンテナを開くなどしていると、Source Control サイドバーが git を検出できなくなる場合があります。
.git フォルダーの権限周りがおかしくなっている可能性がありますので、下記のコマンドで .git フォルダーの権限を再設定することで解決するか確認してみてください。

chmod -R 644 ../.git

ダメな場合は開発コンテナをリビルドしましょう。数分で解決です。


変更履歴


  1. ホスト環境とコンテナ環境の両方に入れる必要があります。 

  2. もしくは絶対パスでホストのワークスペースを指定する。でも絶対パスは論外。 

  3. 一行ずつ進めていき、関数を呼び出している場合には関数内に入らず飛び越していく形式 

  4. 一行ずつ進めていき、関数を呼び出している場合には関数内に進んでいく形式 

  5. デバッグは停止しますが、サーバーは停止しません。 

  6. ブレークしていない時でも停止は可能です。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む