今年度に入った頃から複数の開発案件に並行で関わるようになり、さらにこのところ私事においても煩忙を極めていたため、物事をじっくりアウトプットできずにいた。
ようやっと4連休を手に入れたので、リハビリも兼ねて簡単な演習を行ってみよう。
今回のテーマ
今回、何をやりたいかというと、大きくは以下2点です。
- Node.js 製のアプリケーションと MySQL をそれぞれコンテナ上で動かす
- それぞれのコンテナを docker-compose で連携させる
動機づけ
そもそもコンテナとは、隔離されたアプリケーション実行環境であり、「よそのコンテナを意識しなくてよい」という思想が根幹にあると考えています。
一方で、システムの規模が大きくなると、アプリケーションやデータベースなど、役割ごとにコンテナを切り分け、それぞれを相互に連携させたいと思うのも自然な発想です。
Dockerfile はなんとか書けるようになったが、はてさて複数のコンテナ同士を連携させるにはどうしたらよいだろうか、という課題に対する自分なりのメモです。
……本来は桜の季節に投稿するはずでしたが、モチベーションが足りずに今頃になってしまいました。
目次
実験環境
- MacBook Pro (13-inch, M1*1, 2020)
はじめの Node.js アプリケーション
まずはコンテナ化を考えず、普通に Node.js で簡単な REST API をサクッと作っていきます。
適当な場所に node-app
というディレクトリを作り、カレントディレクトリを変更しましょう。
- projects
- node-app
$ mkdir node-app; cd $_
Node.js プロジェクトの作成
Node.js プロジェクトを作成します。
npm init
コマンドを入力すると対話形式でプロジェクトの初期設定ができますが、ここでは簡単のため、対話を省略するための -y
オプションを付けています。
$ npm init -y
すると、node-app
直下に package.json
が生成されます。
- projects
- node-app
- package.json
- node-app
package.json
{ "name": "app", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
Express のインストール
続いて、Express というライブラリをインストールします。
$ npm install --save express
コマンド実行後のディレクトリ階層はこのような感じになります。
また、インストールに成功すると、package.json
が自動的に書き換えられます。
Express は、4.17.1 が入りました。
package.json
{ "name": "app", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "express": "^4.17.1" } }
index.js の作成
それでは、「Hello, World」を応答するバカ API を作ってみましょう。
package.json
と同じ階層に index.js
を作成します。
空行を除けば10行にも満たない簡素なコードです。
index.js
'use strict'; const express = require('express'); const PORT = 8080; const HOST = '0.0.0.0'; const app = express(); app.get('/', (req, res) => { res.send('Hello, World'); }); app.listen(PORT, HOST);
Node.js アプリのテスト実行
この段階でテスト実行してみましょう。
$ node index.js
http://localhost:8080 にアクセスすると、「Hello, World」が返ってきます。
ここからは、このアプリケーションをコンテナ上で動作させてみます。
Dockerfile, .dockerignore ファイルの作成
package.json
と同じ階層に、Dockerfile
、.dockerignore
という名前のファイルをそれぞれ作成します。 これらのファイルの名前は変えてはいけません。
Dockerfile
FROM node:14 WORKDIR /user/src/app COPY package*.json ./ RUN npm install COPY . . EXPOSE 8080 CMD ["node", "index.js"]
.dockerignore
node_modules npm-debug.log
Docker イメージの作成
$ docker build . -t node-app
しばらく待つと、イメージが出来上がります。
コンテナ版 Node.js アプリケーションの実行
$ docker run -p 80:8080 -dit node-app
ここで、docker ps
コマンドを叩くと、コンテナが一つ実行中になっていることが確認できます。
ブラウザで http://localhost にアクセスしてみます。
先ほどは 8080番ポートを指定していましたが、今回は80番ポートに変わっていることに注意しましょう。
MySQL をコンテナで動かす
続いて、データベースのコンテナを作ります。 DBMS は MySQL にしましょう。
また、コンテナを作るだけでなく、併せて以下の考慮が必要になります。
- コンテナが停止してもデータが揮発しないよう、ボリューム(永続化領域)を作成し、マウントする
node-app
コンテナと通信できるよう、Docker にネットワークを作る
コンテナ起動時に指定できる環境変数
はじめに、予備知識として、MySQL をベースとしたコンテナを docker run
する際には、-e
オプションを使用して以下の環境変数を指定できます。
環境変数名 | 意味 |
---|---|
MYSQL_ROOT_PASSWORD |
MySQLのrootユーザのパスワード |
MYSQL_DATABASE |
デフォルトのデータベース名 |
MYSQL_USER |
データベースにアクセスできる一般ユーザ名 |
MYSQL_PASSWORD |
上記ユーザのパスワード |
MYSQL_ALLOW_EMPTY_PASSWORD |
root パスワードを空欄にできるかどうか。yes を設定すると空にする |
MYSQL_RANDOM_ROOT_PASSWORD |
yes を設定するとランダムなパスワードを設定する |
MYSQL_ONETIME_PASSWORD |
yes を設定すると、root ユーザが初回ログインしたときにパスワードの変更を要求される |
ボリュームの作成
コンテナ内に保存したデータは、そのコンテナが停止すると失われてしまいます。
これを防ぐためには、ボリュームと呼ばれる永続化領域を Docker の実行環境内に作成し、MySQL コンテナを起動する際にマウントします。
まずは、適当な名前(mysqlvolume
)でボリュームを作りましょう。
$ docker volume create --name mysqlvolume
データベースのデータは、/var/lib/mysql
ディレクトリに保存されます。
ここを、今作ったボリュームにマウントすることで、コンテナを破棄してもデータベースの内容が失われないようにします。
今回は簡単のため、Dockerfile
を作らず、MySQL のベースイメージから直接コンテナを起動しましょう。
$ docker run --name mydb -dit \ -v mysqlvolume:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=aaa \ -e MYSQL_DATABASE=potato \ --platform linux/x86_64 \ mysql:5.7
ここで、-v
オプションを使用してボリュームマウントを行っています。
また、環境変数も同時に設定しています。
環境変数名 | 意味 | 設定値 |
---|---|---|
MYSQL_ROOT_PASSWORD |
MySQLのrootユーザのパスワード | aaa |
MYSQL_DATABASE |
デフォルトのデータベース名 | potato |
M1 Mac における注意点
ちなみに、--platform
は M1 Mac で実行する場合に必要となるオプションです(Ubuntuなどで試す際には不要です)。
もしも --platform
を明示的に指定しない場合、以下のようなエラーが発生してしまいます。
--platform
オプションを指定すると、正常に Docker イメージの取得が始まります。
docker ps
で、無事にコンテナが起動したことが分かります。
MySQL コンテナに入る
せっかくですので、起動した MySQL コンテナの中に入ってみましょう。
docker ps
でコンテナ ID を確認したのち、以下のコマンドを打ち込みます。
$ docker exec -it [ContainerID] /bin/bash
続いて、MySQL にログインしましょう。
$ mysql -p
── と入力すると、パスワードを求められますので、先ほど設定した aaa
を入力します。
無事にデータベースが動いているようです。
とりあえず、このコンテナは落としてしまって構いません。
ネットワークを作る
さて、初めに作った Node.js コンテナと、MySQL コンテナを相互に通信させたいとします。
これを実現するため、以下を行います。
- Docker にネットワークを作る
- それぞれのコンテナを、同じネットワーク内で実行する
各コンテナにはそれぞれ異なる IP アドレスが割り振られますが、同じネットワークに所属しているコンテナ同士であれば、--name
で指定したコンテナ名から IP アドレスを引くことが可能になります。
mynodenet
という名前のネットワークを作ります。
$ docker network create mynodenet
なお、作成したネットワークは docker network ls
コマンドで確認できます。
Node.js アプリケーションを MySQL に繋ぐ
いよいよ、Node.js アプリケーションを MySQL と通信させます。
ここで、改めてカレントディレクトリを確認します。 node-app
ならば OK です。
もし異なる場合は、node-app
に cd
しましょう。
MySQL に接続するためのパッケージのインストール
MySQL に接続するため、追加のパッケージを導入します。 以下のコマンドを入力します。
$ npm install --save mysql
成功すると、package.json
が以下のように自動で書き換わります。
package.json
{ "name": "app", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "express": "^4.17.1", "mysql": "^2.18.1" } }
index.js の編集
続いて、index.js
の編集です。
ここで、接続に必要な情報は、起動時に環境変数として与えるようにします。
環境変数名 | 意味 | 設定値 |
---|---|---|
MYSQL_HOST |
MySQLのコンテナ名 | mydb |
MYSQL_ROOT_PASSWORD |
MySQLのrootユーザのパスワード | aaa |
MYSQL_DATABASE |
デフォルトのデータベース名 | potato |
index.js
'use strict'; const express = require('express'); const mysql = require('mysql'); const PORT = 8080; const HOST = '0.0.0.0'; const MYSQL_HOST = process.env.MYSQL_HOST; const MYSQL_ROOT_PASSWORD = process.env.MYSQL_ROOT_PASSWORD; const MYSQL_DATABASE = process.env.MYSQL_DATABASE; const app = express(); const pool = mysql.createPool({ host: MYSQL_HOST, user: 'root', password: MYSQL_ROOT_PASSWORD, database: MYSQL_DATABASE }); app.get('/', (req, res) => { try { pool.getConnection((err, connection) => { if (err) throw err; try { connection.query( 'SELECT * FROM mysql.user', (error, results) => { res.send(results); } ); } catch (e) { throw e; } finally { connection.release(); } }); } catch (e) { res.send('error'); } }); app.listen(PORT, HOST);
急激に実装量が増えました。
簡単のため、コネクションプールを使わない実装例を載せようか迷いましたが、どのみち必要になるため、きちんと書きました。
Node.js アプリケーションのコンテナイメージのビルドと起動
今ふたたび、Node.js アプリのコンテナイメージを docker build
でビルドし、docker run
でコンテナを起動します。
MySQL イメージ起動の際に指定した環境変数と合わせる形で -e
オプションを指定し、さらに、--net
オプションで MySQL と同じネットワーク名を指定します。
$ docker build . -t node-app $ docker run -p 80:8080 \ -dit \ -e MYSQL_HOST=mydb \ -e MYSQL_ROOT_PASSWORD=aaa \ -e MYSQL_DATABASE=potato \ --net mynodenet \ node-app
これで、node-app
コンテナと mydb
コンテナが同じネットワーク(mynodenet
)に参加したことになります。
ふたたび、ブラウザで http://localhost にアクセスしてみましょう。
なにやら DB ユーザ情報の一覧のような JSON が表示されたら成功です。
docker-compose によるオーケストレーション
ここまでで、だいたいやりたいことは終わりました。
ただ、Node.js アプリのコンテナにしても、MySQL のコンテナにしても、起動時にいちいち大量のオプションを付けるのは非常に面倒です。
一つでも間違うと正しく連携できなくなりますし、そもそも、MySQL が起動していない状態で Node.js アプリケーションを先に起動してしまうと、コネクションがうまく張れずにエラーを吐いてしまうかもしれません。
また、MySQL をマウントするためのボリュームだったり、複数のコンテナをまとめるネットワークだったり、これらの依存関係を人間が主導で管理するのは限界がありそうです。
そこで登場するのが、docker-compose というツールです。
docker-compose のインストール
docker-compose は、Docker とは別にインストールが必要な場合があります(と思ったら、最新の Docker には普通に docker-compose がバンドルされていることがこの記事を書いている途中で判明しました。チクショー*3)。
pip
を使ってインストールしましょう。
$ sudo pip3 install docker-compose
docker-compose.yaml の作成
ここで、作業ディレクトリを変えます。
node-app
と同階層に、新たに compose
というディレクトリを作り、docker-compose.yaml
というファイルを作ります。
- projects
- compose
- docker-compose.yaml
- node-app
- compose
docker-compose.yaml
version: "3" services: mydb: image: mysql:5.7 networks: - mynodenet volumes: - mysqlvolume:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: aaa MYSQL_DATABASE: potato myap: depends_on: - mydb image: node-app networks: - mynodenet ports: - 80:8080 restart: always environment: MYSQL_HOST: mydb MYSQL_ROOT_PASSWORD: aaa MYSQL_DATABASE: potato networks: mynodenet: volumes: mysqlvolume:
docker-compose の実行
docker-compose.yaml
が配置されているディレクトリに cd
して、以下のコマンドを実行します。
$ docker compose up -d
すると、なんということでしょう*4。
ボリュームやネットワークが生成され、さらに依存関係をもつ複数の Docker イメージが順番に起動したではありませんか。
この状態で、docker ps
を叩くと、確かに docker-compose によってコンテナが起動されたことが確認できます。
また、ブラウザで http://localhost にアクセスすると、わけのわからない JSON が表示されることも確認できます。
遊び終わったら、以下のコマンドでコンテナを停止させましょう。
コンテナやネットワークはリムーブされますが、ボリュームは生き残るため、再び docker-compose up
したときもデータが失われることはありません。
$ docker-compose down
ここまでのまとめ
- コンテナは本来、互いに影響しない独立したプログラムの実行環境である
- コンテナ同士を相互に連携させたい場合、起動順や設定内容の整合などの考慮が必要である
- これらを人間が手動管理するのは現実的ではないため、docker-compose などを用いて複数コンテナを統括するのがよい
以上。
思ったより話が長くなってしまった。