- 投稿日:2020-03-24T23:55:25+09:00
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.ymlversion: '3' services: truffle: build: context: ./trufflebox/ dockerfile: Dockerfile volumes: - ./trufflebox:/usr/src/app command: sh -c "cd client && yarn start" ports: - "8003:3000"DockerfileFROM 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 └── Dockerfilepet-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 devReact&Truffle記事では、「unbox react」としていたところを、今回は、「unbox pet-shop」とするわけです。
これにより、
https://www.trufflesuite.com/boxes/pet-shop
を使用していることになります。Ethereum ネットワーク
React&Truffle記事を参考に、Docke環境からローカルのGanache のテストネットワークに接続します。
まず、ローカル環境に接続可能なIPアドレスを割り振ります。
ローカルIPアドレスない場合は設定します。(React&Truffle記事、及び、入門記事参照)
そして、Ganache の設定画面で、ローカルIPアドレスで起動するように指定します。
PORT NUMBER は7545としておきます。これは後で出てきます。この状態で、METAMASKで接続できることを確認しておきます。
METAMASKは、このアドレス、ポートで接続するための設定を追加します。
ここに接続するように、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.ymlversion: '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/
を開きます。画面が開きます!これにより、PET-SHOPトリュフボックスのInstallationがDocker環境にできたことになりまし、ETHEREUM PET SHOP -- YOUR FIRST DAPPのWriting the smart contract を始められる環境になったことになります。
いったんここで終了し、次の記事で、スマートコントラクトを作成していきます。
- 投稿日:2020-03-24T23:27:36+09:00
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図にするとこんな感じのはず
Dockerイメージを用いてコンテナ間で通信
次は、こんなの作ってみる
※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.gznginx/DockerfileFROM nginx:latest RUN rm -f /etc/nginx/conf.d/default.conf COPY ./files/tomcat.conf /etc/nginx/conf.d/nginx/files/tomcat.confserver { location /tomcat/ { proxy_pass http://tomcat-1:8080/; } }tomcat/DockerfileFROM 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.gzNginxイメージ、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/
- 投稿日:2020-03-24T23:27:36+09:00
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図にするとこんな感じのはず
Dockerイメージを用いてコンテナ間で通信
次は、こんなの作ってみる
※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.gznginx/DockerfileFROM nginx:latest RUN rm -f /etc/nginx/conf.d/default.conf COPY ./files/tomcat.conf /etc/nginx/conf.d/nginx/files/tomcat.confserver { location /tomcat/ { proxy_pass http://tomcat-1:8080/; } }tomcat/DockerfileFROM 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.gzNginxイメージ、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/
- 投稿日:2020-03-24T23:27:36+09:00
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図にするとこんな感じのはず
Dockerイメージを用いてコンテナ間で通信
次は、こんなの作ってみる
※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.gznginx/DockerfileFROM nginx:latest RUN rm -f /etc/nginx/conf.d/default.conf COPY ./files/tomcat.conf /etc/nginx/conf.d/nginx/files/tomcat.confserver { location /tomcat/ { proxy_pass http://tomcat-1:8080/; } }tomcat/DockerfileFROM 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.gzNginxイメージ、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/
- 投稿日:2020-03-24T22:00:15+09:00
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の紹介でした。
- 投稿日:2020-03-24T20:18:52+09:00
【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参考書籍
環境構築
(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はなんなんだ。。。)
ただ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)
これでやっとアプリ制作に取り掛かれるようになった。。。泣
- 投稿日:2020-03-24T20:13:56+09:00
【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の実装ができなかったので嬉しい!!
- 投稿日:2020-03-24T18:08:24+09:00
【環境構築】dockerでvue.js+Typescript+vuetify+express+Sequelizeの環境構築
dockerファイル作成
ディレクトリを作成
consolemkdir node
consolecd node vim Dockerfile
node.jsを準備
dockerファイルを作成し、そこから各種プログラムを実行できるようにする。
ここではexpress,suquelize-cliの実行環境の構築ができるよう記載。DockerfileFROM node:12.13 RUN npm install -g express-generator sequelize-cliFROM nodeでノードのベースイメージ
RUNコマンドでnpm installを実行しexpressとsepulize-cliをインストールするコマンドdocker イメージを作成
これによりDockerfileが実行される。
consoledocker build node/. -t serverapp:latest
-tオプションを付けていることで、名前(serverapp)とタグ名(latest)を指定している。
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をインストールしていきます。
コンテナにロングインする。consoledocker exec -it serverapp /bin/bash
docker exec -it <コンテナ名>/bin/bash コンテナにログイン
-it コンテナを継続的に動かすために必要rootcd /node express .
destination is not empty, continue?(空ファイルじゃないけど大丈夫?)と聞かれますが、中にはDockerfaileがあるだけなので[y]で続行する。
sequelizeなどの準備
ここで色々必要になるものの準備を行います。
rootnpm install --save sequelize sqlite3 cors nodemon npm install
それぞれ簡単解説
ここでは詳しい説明はしませんが、別記事をそれぞれ作成しようと思います。
名称 説明 sequelize データベースを管理するツール sqlite3 簡易版データベース cors セキュリティ上のルール nodemon 自動でサーバーを再起動してくれるツール これでローカルフォルダのnode/にファイルが作成できたはず。
sequelizeをセットアップ
rootsequelize init
ターミナルに戻る
rootexit
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ファイルを作成。
consolemkdir node/data
もう一度dockerコンテナにログイン
consoledocker exec -it serverapp /bin/bash
Unable to resolve sequelize package in
sequleize model:createコマンドを使いデータベースに雛形を作成
sequelize model:createコマンドとは?rootsequelize model:create --name goal --underscored --attributes goalname:string
Unable to resolve sequelize package inのエラーが出る場合はこちら
マイグレートする
そもそもマイグレートとは、アプリケーションで使うデータベースの定義を自動的に作成・管理する機能です。
rootsequelize db:migrate
マイグレートが完了したら一度dockerを停止する。
terminaldocker stop serverapp
vueの準備
ディレクトリを作成
consolemkdir vue
consolevim vue/Dockerfile
作成したfrontapp内のDockerfileに以下の記述をする。
ここではvue/cliの実行環境の構築ができるよう記載。DockerfileFROM node:12.13 RUN npm install -g @vue/cli
Dockerfileを元にコンテナイメージを作成。起動し、ローカルのフォルダをマウント。
consoledocker build vue/. -t frontapp:latest
consoledocker run -itd --rm --name frontapp -v $PWD/vue:/vue frontapp:latest
docker run コマンドが正常に動いているか確認。
consoledocker ps
consoledocker exec -it frontapp /bin/bash
コンテナにログイン後以下を実行する。
rootcd /vue vue create frontapp
以下のように標準のyarnか高速のnpmどちらかで実行
npmで実行したいため[y]を選択する。rootYour connection to the default yarn registry seems to be slow. Use https://registry.npm.taobao.org for faster installation?
以下のメッセージに従いvueをインストールしていく。
今回はマニュアルでtypescriptなどもインストールしていく。これでvueのインストールは完了です。
vuetifyの環境
frontappに移動し、vuetifyのプラグインを追加する。
インストールはデフォルトで行った。
インストール終了後はexitで一度コンテナからログアウトするconsoleroot@701c15dfea18:/# cd vue/frontapp root@701c15dfea18:/vue/frontapp# vue add vuetify exitdocker-compose.ymlファイルを準備する。
Node.jsとVue.jsそれぞれのコンテナを起動する際、composeファイルがあると起動/終了が楽なので、
docker-compose.ymlを下記のように記入。docker-compose.ymlversion: "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 downlocalhost:8080でアクセス
localhost:3000でアクセス
参考記事
とてもお世話になりました。ありがとうございました
Vue.js + Express + Sequelize + DockerでCRUD搭載のTodoリストを作ってみる
【環境構築】Docker + Rails6 + Vue.js + Vuetifyの環境構築手順
GitHub PagesにDocker+Vue.js+Vuetifyでページを公開
- 投稿日:2020-03-24T17:13:37+09:00
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でも起動できた)
- 投稿日:2020-03-24T16:39:31+09:00
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環境向け)がラインナップされています。
3. インスタンスのアップグレード方法
それでは、Flexソフトウェア上にあるNetBackupインスタンスをアップグレードする方法についてご紹介します。
① 左側のFlex アプライアンス コンソールのインターフェイスから、[SystemTopology] のアイコンをクリックします。
③ [Manage]から[Upgrade Instance]を選択します。
④ メッセージを確認し、[Continue]をクリックします。
⑤ アップグレードしたいバージョンを選択し、[Precheck]を押下します。
互換性のプリチェックが行われます。
⑥ [Precheck was successful]のメッセージが表示されます。Precheckが完了したら、[Next]ボタンを押下します。
⑦ Smart Meter Customer Registration Keyを入力します。
※Smart Meter Customer Registration KeyとはSmart Meterを利用するため必要なファイルです。Registration KeyはSmart Meterポータル(https://taas.veritas.com/) から入手可能です。
⑨ アップデートが実行され、進捗状況が表示されます。
5分~10分程度で作業は完了します。
⑩ [Progress]が100%, [Status]がSuccessfulとなりました。
⑪ アップグレードを確定したい場合、[Manage]>[Upgrade Instance]>[Commit]を選択します。 元のバージョンに戻したい場合はRolebackをクリックします。(Commit前で、且つ24時間以内であればロールバックが可能です)
⑫ Commitを選択した場合、メッセージが表示されますのでCommitをクリックしてアップグレード完了となります。
<参考> RoleBackを選択した場合、以下が表示されますのでRoleBackボタンを押下します。
<参考> ロールバックステイタスが表示されます。[Status]にSuccessfulが表示されたらロールバック完了になります。
4. まとめ
いかがだったでしょうか、通常のNetBackupのアップグレードの場合、1~2時間程度かかる作業工程が、GUIの直観的な操作でわずか5分~10分程度で簡単にアップグレードできます。また、アップグレード作業後であっても24時間以内であれば元のバージョンに戻せますのでなにか手違いがあっても安心です。
バックアップの運用負荷、煩雑な管理でお悩みの方は活用を検討されてみてはいかがでしょうか。商談のご相談はこちら
本稿からのお問合せをご記入の際には「お問合せ内容」に#GWCのタグを必ずご記入ください。ご記入いただきました内容はベリタスのプライバシーポリシーに従って管理されます。
- 投稿日:2020-03-24T16:21:30+09:00
GAMESSプログラムで第一原理計算を実行可能なDockerコンテナを作る方法
はじめに
こんにちは,(株)日立製作所 研究開発グループ サービスコンピューティング研究部の露木です。
材料開発にAI・機械学習を適用する際に常に問題となるのがデータ数の少なさです。材料候補の物質を変えながら,大量の実験を繰り返して教師データとなる物性値を測定することは困難であり,少量の実験値から学習を行わなければなりません。しかし幸運なことに,計算化学・計算物理学の理論にもとづいてシミュレーション計算を行えば,第一原理的に物性値を得ることができます。このようにデータセットを補う観点から,機械学習を適用する立場でもシミュレーション計算を実行できる環境を整えることは非常に重要といえます。
本記事でとりあげるGAMESSプログラムは,量子力学にもとづいた繰り返し計算により電子の波動関数を計算し,分子の物性を計算するためのプログラムです。GAMESSはアイオワ州立大学により開発されているプログラムであり,理論化学・計算化学分野における有力なプログラムといえます。GAMESSプログラムは主要な波動関数法 (HF法やMP2法,結合クラスター法,配置間相互作用法など) や 密度汎関数法 (DFT) ベースの電子状態計算,相対論補正や振動状態計算等にも対応しています。同種のプログラムにはGAUSSIANや MOLPRO,TURBOMOLE などがあります。ご興味ある方は,これらプログラムと比較検討してみてください。
本稿ではGAMESSを利用して第一原理的な電子状態計算を行うことを目標に,Dockerコンテナとして実行環境を構築する方法を記します。
手順
コンパイル環境の準備
インターネットには DrSnowbird/hpc-app-gamess や saromleang/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 basebaseコンテナを起動して,シェルにログインします。
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
- 投稿日:2020-03-24T15:46:00+09:00
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/DownloadsMacの場合は、「OS X Hosts」をクリックします。
Ubuntuを入手してインストール
Ubuntu Desktop 18.04.4 LTS
https://jp.ubuntu.com/downloadVirtualboxのホストオンリーネットワークを作成
ホストOSで通信するために、Virtualboxのホストオンリーネットワークを作成します。
ホストOSの設定ではないのでクリックする場所に注意。
クリックするだけで設定等は自動で行われます。
自動入力される設定の例(環境によって設定内容は変わります、デフォルトのままでOK、修正不要):
DHCPの設定も勝手に入ると思います。
VirtualboxのゲストOS用ネットワークを作成
今度はゲストOS用のネットワーク設定です。クリックする場所に注意。
ネットワーク の アダプター2 に 割り当て ホストオンリーアダプター を選択します。
名前 で、先ほど作成したホストオンリーアダプターを選択できるはずです。
ゲストOS(Ubuntu)でインターネット通信が可能かを確認
Firefox(Webブラウザ)等で、インターネットに接続可能になったことを確認してください。
ゲストOS(Ubuntu)のアップデート
アップデートを行います。
sudo apt update sudo apt upgradeNet-toolsのインストール
IPの確認にifconfigを利用したいので、インストールしておきます。
sudo apt-get install net-toolssshのインストール
ホスト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 sshsshが起動中ですので、ゲスト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 0Macから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-commonDocker’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 upgradeDOCKER ENGINE - COMMUNITY のインストール
sudo apt-get install docker-ce docker-ce-cli containerd.ioDOCKER 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:~$
- 投稿日:2020-03-24T15:28:58+09:00
[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.tmlversion: '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.gopackage 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使えばできることがわかったので記事にしてみました。
- 投稿日:2020-03-24T11:54:06+09:00
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=utf8mb4database.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 mysqlmysqlコンテンに入り文字コードを確認する
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の文字化けしていた部分を確認すると、日本語で表示されていることが確認できました
- 投稿日:2020-03-24T09:27:04+09:00
コーディング未経験のPO/PdMのためのRails on Dockerハンズオン、Rails on Dockerハンズオン vol.12 - TDDでPost機能をコーディング part1 -
はじめに
お待たせしました!第12回にしてやっとつぶやき機能を実装してまいります!ここではPost機能といいますね。
前回のソースコード
前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。
どんなの実装するの?
最初に今回のゴールを見据えておきましょう。
画面遷移
新たにポストページを作ります。
ポストページはポストを投稿するフォームと、今までのユーザー全員のポストが投稿日時降順で表示される機能を具備しています。
ポストページはサインイン済のユーザーしかアクセスできません。
ポストの投稿ユーザーをクリックしたらそのポストのプロフィールページに遷移できます。
ユーザーのプロフィールページでは、そのユーザーの過去のポストが投稿日時降順で表示されています。ER図
テストシナリオ
さて、これを踏まえて今回のテストシナリオを考えてみましょう。
- 未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること
- サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること
- 未サインインのユーザーは、トップページでヘッダーにポストページへのリンクを見つけられないこと
- 未サインインのユーザーは、サインアップページでヘッダーにポストページへのリンクを見つけられないこと
- 未サインインのユーザーは、サインインページでヘッダーにポストページへのリンクを見つけられないこと
- 未サインインのユーザーは、ユーザー詳細ページでヘッダーにポストページへのリンクを見つけられないこと
- サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
- サインイン済のユーザーが、ポストページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
- サインイン済のユーザーは、ポストページでポストを入力できること
- ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
- ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
- ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
- サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
- 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
- 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
- サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
- サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
こんな感じでしょう。
では元気にデベロップしてまいりましょう!今回はTDDで開発をしていくので、テストコードを書いて、アプリコードを書いて、を繰り返していきます。開発スタート
まずは、それぞれのテストシナリオをテストコードに落とし込みましょう。
開発はTDDで進めるので
- テストをコーディングする(Red)
- テストコードがパスするようにアプリケーションをコーディングする(Green)
- 非効率な記述があれば、Greenをキープしながらコーディングし直す(Refectoring)
です。
ではコンテナを起動しておきましょう!
$ docker-compose up -d $ docker-compose exec web ash未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること
ポストページについては以下の仕様で実装していきます。ポストページのURLパスは
/posts
とします。まずはテストコードを書いていきます。今回のテストシナリオ用に新しいテストシナリオファイルを作りましょう。
# touch spec/system/07_posts_spec.rb07_posts_spec.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbRails.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.rbclass PostsController < ApplicationController def index # 未サインインの場合、トップページにリダイレクトする + redirect_to root_path unless signed_in? end end以前、サインイン済ならプロフィールページにリダイレクトさせる、という機能を作りましたね。今回はそれの条件が逆バージョンです。
今回使っているunless
はif
の逆の分岐です。つまり、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.rbfeature "ユーザーとして、ポストを投稿したい", 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_link
のid
を付与することにします。spec/system/07_posts_spec.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbfeature "ユーザーとして、ポストを投稿したい", 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 Smith
をcreate
したテストケースがありました。
これを簡易に使いまわせるように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.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbfeature "ユーザーとして、ポストを投稿したい", 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を確認してみましょう。
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.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbclass 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.rbclass User < ApplicationRecord ... + has_many :posts ... end
ちなみにマイグレーションファイルの中身もみておきましょう。
db/migrate/YYYYMMDDhhmmss_create_posts.rbclass 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: true
がuser_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.rbclass 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の技術ですが、input
やtextarea
に何も入力がない時に限り、そのフォームの補助の役割でどういうものを入力すればいいかを表示してあげる機能です。
autofocus: true
はページが表示された時に自動的にフォーカスされるフィールドを指定できるHTML5の機能です。便利なのでつけときます。ここまでで再度テストを実行してみましょう。
# 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
- 投稿日:2020-03-24T04:20:48+09:00
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]
- 投稿日:2020-03-24T02:35:30+09:00
開発コンテナで快適!<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
- バージョン管理システム
- 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 をダウンロードしてインストールします。
インストールしたら起動して、拡張機能をインストールします。
以下の拡張機能をそれぞれ名前で検索して [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# というように、プロジェクトフォルダーを分けることは多い。後からプロジェクトフォルダーが増えることもよくある。
ということで、ワークスペースを作成しましょう。
マルチルートワークスペースの作成
- Windows Explorer で任意の場所にワークスペースフォルダー (本記事では
C:\Workspaces\MoroMoro.Sample
) を作成します。- VSCode を起動します。
- メニューバーの [File] - [Close Folder] が無効化されていることを確認します。有効化されていたり [Close Workspace] が表示されている場合はそれを押してフォルダー(もしくはワークスペース) を必ず閉じてください。
- メニューバーの [File] - [Save Workspace As...] を押します。
- ファイル保存ダイアログが開かれるので、ワークスペースフォルダーに移動し、ファイル名にワークスペースフォルダーと同じ名前 (本記事では
MoroMoro.Sample
) を入力して [Save] ボタンで保存します。<注意>
本記事ではワークスペース名やプロジェクト名をMoroMoro.Sample.Frontend
のようにアッパーキャメルケースで命名していますが、Node.js パッケージ (後述) の命名規則に合わせてmoromoro.sample.frontend
のように全て小文字で命名しても構いません。(本記事でも Node.js パッケージ名については全て小文字で命名します。)Git Init
バージョン管理はワークスペースレベルで行います。
Git Bash を開きワークスペースに移動してから次のコマンドを実行します。git init<注意>
「使い捨てだからバージョン管理しなくていいや」という人も必ず行ってください。
非常に厄介なことに、Git Init の有無で Remote-Container の挙動の一部が大きく変わってしまうためです。(後述)プロジェクト
続いて、ワークスペースにフロントエンドのプロジェクトを作成します。
プロジェクトフォルダーの追加
- Windows Explorer でワークスペースフォルダーにプロジェクトフォルダー (本記事では
MoroMoro.Sample.Frontend
) を作成します。- VSCode のサイドメニューバーから (Explorer) を開き、[Add Folder] ボタンを押します。
- フォルダー選択ダイアログが開かれるので、プロジェクトフォルダーを選択して [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
フォルダーを作成してください。
大事なことなので画像貼っておきます。
このフォルダーの中にDockerfile
とdevcontainer.json
を作成します。
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...
を選択します。
フォルダー選択ダイアログが開きますのでプロジェクトフォルダーを選択して開きます。
すると右下にメッセージが表示されコンテナイメージの作成が開始します。
作成が完了するとメッセージが消えます。
問題無ければこの時点でコンテナの中に入れているはずです。コンテナの中にいる時はステータスバーの左端にDev Container: Node.js 12
と表示されます。
コンテナの中でプロジェクトフォルダーを開いたことにより、コンテナ内ではプロジェクトフォルダーがワークスペースそのものとなります。
本記事では、コンテナ内でのワークスペースのことをコンテナワークスペースと呼ぶことにします。Docker ソケットのマウント確認
ターミナルから次のコマンドを実行し、正常にイメージ一覧を取得できることを確認してください。
docker imagesエラーが発生してしまう場合は開発コンテナの準備に不備があったということですので設定を見直してください。
修正したら、開発コンテナをリビルドします。node_modules のマウント確認
コンテナワークスペースに空の node_modules フォルダーが作成されていることを確認してください。
node_modules フォルダが作成されていない場合は開発コンテナの準備に不備があったということですので設定 (特にワークスペース名やプロジェクト名の誤字脱字、workspaces
がWorkspaces
やworkspace
になっていないかなど) を見直してください。 (或いは前述の 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
を実行するとリビルドが行われます。完了後は自動でコンテナが開きなおされます。
コンテナのリビルドはコンテナ内にいる時にしか実行できません。
コンテナ外にいるときは単純にコンテナを削除してしまえば、次回コンテナを開くときにコンテナが自動で作成されます。
開発コンテナを閉じる
VSCode を終了すればコンテナは停止されます。(ただし、devcontainer.json で
"shutdownAction": "none"
を設定している場合は停止されません。)
次回 VSCode 起動時には自動で開発コンテナが開かれます。VSCode を終了させずにコンテナを閉じる時は、メニューバーの [File] - [Close Remote Connection] を実行します。(Connection といいつつ接続だけでなくコンテナも停止します。)
再び開発コンテナを開くには、1回目と同じ方法でも開くことができるのですが、もっと楽な手順で開くこともできます。Remote Explorer にコンテナとフォルダーが登録されていますので、フォルダーの右端の (Open Folder in Container) ボタンを押せば一発で開けます。
無事開発コンテナを開けたら
ここからはもう、ずっと、最後まで、とことん、開発コンテナにひきこもります。
パッケージの作成
さて、まずはプロジェクトのパッケージ化を行いましょう。
パッケージ化すると、パッケージのビルドや実行などのスクリプトを登録できたり、パッケージが依存する 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 の
dependencies
とdevDependencies
に記録されます。
更に、追加でインストールされた間接的な依存モジュールも含む全ての依存モジュールの情報が 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
環境変数 (production
かdevelopment
のいずれか) に従い、設定を切り替える- development モード時はデバッグ補助用にソースマップ (トランスパイル前後のソースコードの紐付け情報) を生成する
- バンドル前のエントリーポイントは
./src/client/index.tsx
- バンドル後のエントリーポイントは
bundle.js
- 出力フォルダは
dist
- バンドルするソースコードが配置されているフォルダーは
src/client
とnode_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 をテストできるようにするため
preset
にts-jest
を設定します。
また、本来 WebPack (CSS Loader) を通さなければ処理できない CSS Modules (後述) という特殊なインポート方法を、WebPack を介さない Jest でも最低限エラー発生を回避して処理できるよう、moduleNameMapper
でjest-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 ページを表示させることもできます。
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
の時 (本番) はfatal
、error
、warn
、info
のログを出力します。サーバーサイド: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.ico
やApp.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 (
useState
やuseEffect
) を利用する必要があります。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.json
のscripts
で定義したスクリプトの一覧が並んでいますので、run:hmr
を実行します。
実行するとサーバー起動とビルドが行われます。
Terminal に[1] ℹ 「wdm」: Compiled successfully.
と出力されたら成功です。
ブラウザを立ち上げてhttp://localhost:3000
にアクセスしてみてください。
Home と HomeWork がうまく切り替わったでしょうか。クライアントサイドコードの変更
HMR 用補助サーバーの設定で説明しましたとおり、クライアントサイドコードの変更は HMR 機能によってブラウザをリロードすることなく即座に反映されます。
試しにHome.tsx
の "Hello World." を "Hello HMR." に変えてみたり、App.styl
をいじってみてください。
ちなみにこの動画では、撮影の都合上 Browser Preview という拡張機能を使用して VSCode 内にブラウザを表示させていますが、もちろん普通のブラウザでも HMR はちゃんと動作します。サーバーサイドコードの変更
サーバーサイドコードの変更は ts-node-dev によって検出され、サーバーが自動で再起動されます。
サーバーの停止
基本的にサーバーは起動しっぱなしで良いのですが、ターミナル上で Ctrl + C を押せばサーバーが停止します。
デバッグ
特にバグがあるわけでもないのですが、次はデバッグをしてみます。
クライアントサイドコードのデバッグ
HomeWork 関数をデバッグしてみます。
- 準備
- サーバーを起動しておきます。
- Run サイドバーの上部にあるドロップダウンリストから
launch.json
で定義したデバッグ設定を選べますので、Debug Client (Chrome)
もしくはDebug Client (Edge)
を選択しておきます。HomeWrok.tsx
の5行目にカーソルを移動し、F9 キーにてブレークポイントを設置します。- デバッグ
- F5 キーにてデバッグを開始します。
- 自動起動されたブラウザにて HomeWork ページを表示すると、ブレークポイント (
HomeWork.tsx
の5行目) でブレーク (一時停止) します。ブレーク後は下記の操作が行えます。
操作 キー ステップオーバー3 F10 ステップイン4 F11 再開 F5 停止5 6 Shift + F5 他にも、変数にカーソルをあてて変数の中身を確認したり、DEBUG CONSOLE から変数の中身を書き換えたりすることも可能です。
サーバーサイドコードのデバッグ
hello API をデバッグしてみます。
- 準備
- サーバーを起動しておきます。
- Run サイドバーの上部にあるドロップダウンリストから、
Debug Server (Attach)
を選択しておきます。server.ts
の16行目にカーソルを移動し、F9 キーにてブレークポイントを設置します。- デバッグ
- F5 キーにてデバッグを開始します。
- ブラウザ (自動起動はしません) にて HomeWork ページを表示すると、ブレークポイント (
Server.ts
の16行目) でブレーク (一時停止) します。ブレーク後は、クライアントサイドコードのデバッグ時と同じ操作が可能です。
なお、サーバーの起動時の処理をデバッグしたい場合には、サーバーを停止し
Debug Server (Launch)
を使用してデバッグを開始してください。ユニットテスト
次はユニットテストを用意して実行してみます。本当はテスト駆動開発で実装より先にテストを用意したかったのですが、記事の構成の都合から諦めて後回しにしました。
各テストケースの基本的な流れは次のようになります。
- testing-library の
render
関数でテスト対象コンポーネントを疑似的にレンダーする- テストしたいシナリオを
fireEvent
でエミュレートする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 を実行します。
Run Jest ではなく Debug Jest を実行することでデバッグも可能です。
テストの全体実行とカバレッジ
テスト全体を実行するには、Explorer サイドバーの NPM Scripts から、
test
を実行します。
結果と共にカバレッジも出力するようにしてあります。
カバレッジの詳細レポートはコンテナワークスペースの coverage フォルダーに出力されています。coverage/lcov-report/index.html
をブラウザで開けば、どの行が何回実行されたかといった情報も確認できます。
本番用コンテナ
本記事もいよいよ大詰めです。
本番用コンテナは 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/iconsApp コンポーネントの実装
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実行
テスト
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(); }); });おまけ
本編に挟み込めなかった小ネタをいくつか。
ワークスペース外のファイルを開く
開発コンテナ内のファイルは、コンテナワークスペースには含まれていなくても VSCode で開くことができます。
下記のコマンドを{File Path}
を置き換えて実行するとファイルが開かれます。code {File Path}Source Control サイドバーが git を検出しなくなった場合の対処法
ワークスペースをコピーして開発コンテナを開くなどしていると、Source Control サイドバーが git を検出できなくなる場合があります。
.git フォルダーの権限周りがおかしくなっている可能性がありますので、下記のコマンドで .git フォルダーの権限を再設定することで解決するか確認してみてください。chmod -R 644 ../.gitダメな場合は開発コンテナをリビルドしましょう。数分で解決です。
変更履歴
- 2020/03/24: .dockerignore セクションを追加