マルチコンテナーアプリケーション

読む時間の目安: 5 分

これまでは 1 つのコンテナーからなるアプリを使って作業を進めてきました。 ここからはアプリケーションに MySQL を加えることにします。 ところが以下の質問があがってきます。 「MySQL はどこで動かすの?」 「同じコンテナー内にインストール? それとも別に動かす?」 原則を言います。 1 つのコンテナーでは 1 つのことだけを行います。そうするのが一番うまくいきます。 その理由はいくつかあります。

  • データベースとは別にして、API やフロントエンドをスケーリングしなければならないなら、ちょうど良い機会です。
  • コンテナーを別々にすれば、バージョン管理や更新操作を個別に行うことができます。
  • 1 つのコンテナーをデータベース用としてローカルで利用し、本番環境ではサービスを利用してデータベースを管理することができます。 そうすればデータベースをアプリとともに提供する必要がありません。
  • プロセスを複数実行するにはプロセスマネージャーが必要です(コンテナーが起動するプロセスは 1 つです)。 そうなるとコンテナーの起動や停止が複雑になります。

理由はもっとあります。 そこでアプリケーションのアップデートは以下のようにします。

MySQL コンテナーに接続する Todo アプリ

コンテナーのネットワーク

コンテナーについてもう一度確認します。 デフォルトでコンテナーは分離された状態で動作するので、それが動作するマシン上の他のプロセスや他のコンテナーのことなどまったく知りません。 それならどうやってコンテナーどうしを互いにやりとりできるようにすればよいのでしょう。 その答えは ネットワーク です。 でもネットワークエンジニアである必要はありませんよ(よかった)。 そこで以下の単純なルールを覚えてください。

メモ

2 つのコンテナーが同一ネットワーク上にあれば互いにやりとりができます。 同一ネットワーク上にないときはできません。

MySQL の起動

コンテナーをネットワークに加えるには 2 つの方法があります。 1) そのコンテナーの起動時にネットワークを割り当てる、あるいは 2) 既存のコンテナーを接続する、です。 ここからはまずネットワークを生成して MySQL コンテナーの起動時に割り当てることにします。

  1. ネットワークを生成します。

     $ docker network create todo-app
    
  2. MySQL コンテナーを起動してネットワークに割り当てます。 同時に環境変数をいくつか定義して、データベースの初期化に利用します。 (MySQL Docker Hub 一覧 の「Environment Variables」の節を参照してください。)

     $ docker run -d \
         --network todo-app --network-alias mysql \
         -v todo-mysql-data:/var/lib/mysql \
         -e MYSQL_ROOT_PASSWORD=secret \
         -e MYSQL_DATABASE=todos \
         mysql:5.7
    

    ARM ベースのチップ、たとえば Macbook M1 Chips / Apple Silicon を利用している場合は、以下のコマンドを実行します。

     $ docker run -d \
         --network todo-app --network-alias mysql \
         --platform "linux/amd64" \
         -v todo-mysql-data:/var/lib/mysql \
         -e MYSQL_ROOT_PASSWORD=secret \
         -e MYSQL_DATABASE=todos \
         mysql:5.7
    

    PowerShell を利用している場合は以下のコマンドとします。

     PS> docker run -d `
         --network todo-app --network-alias mysql `
         -v todo-mysql-data:/var/lib/mysql `
         -e MYSQL_ROOT_PASSWORD=secret `
         -e MYSQL_DATABASE=todos `
         mysql:5.7
    

    ここで--network-aliasフラグというものを利用しています。 これについてはすぐに説明します。

    ヒント

    上ではボリューム名としてtodo-mysql-dataを指定し/var/lib/mysqlにマウントしました。 このディレクトリは MySQL がデータを保存する場所です。 だからといってdocker volume createコマンドは実行していません。 Docker が名前つきボリュームが指定されたことを認識して、これを自動的に生成してくれます。

  3. データベースが起動され実行していることを確認するため、データベースに接続して接続確認を行います。

     $ docker exec -it <mysql-container-id> mysql -u root -p
    

    パスワードプロンプトが表示されたら secret と入力します。 MySQL シェルにおいてデータベース一覧を表示し、todosデータベースがあることを確認します。

     mysql> SHOW DATABASES;
    

    出力は以下のようになるはずです。

     +--------------------+
     | Database           |
     +--------------------+
     | information_schema |
     | mysql              |
     | performance_schema |
     | sys                |
     | todos              |
     +--------------------+
     5 rows in set (0.00 sec)
    

    MySQL シェルを終了して、マシン上のシェルに戻ります。

    mysql> exit
    

    やりました。 todosデータベースは間違いなくありますので、利用していきましょう。

MySQL への接続

MySQL が起動し実行されていることが確認できたので、さっそく使ってみます。 ただし疑問があります ... どうやって? 同一ネットワーク上に別のコンテナーを実行したとき、そのコンテナーをどうやって認識したらよいのでしょう(各コンテナーにはそれぞれに IP アドレスがありますが)。

このことを理解するために、ここでは nicolaka/netshoot というコンテナーを利用することにします。 これは 実に多くの ツールを提供してくれるもので、ネットワークに関するトラブルシューティングやデバッグに活用できます。

  1. nicolaka/netshoot というイメージを使ったコンテナーを新たに起動します。 ネットワークは同一のものに接続するようにします。

     $ docker run -it --network todo-app nicolaka/netshoot
    
  2. コンテナー内部にてdigコマンドを実行することにします。 これは便利な DNS ツールです。 ホスト名mysqlに対する IP アドレスを探し出してみます。

     $ dig mysql
    

    そうすると以下のような出力となります...

     ; <<>> DiG 9.14.1 <<>> mysql
     ;; global options: +cmd
     ;; Got answer:
     ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32162
     ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
    
     ;; QUESTION SECTION:
     ;mysql.				IN	A
    
     ;; ANSWER SECTION:
     mysql.			600	IN	A	172.23.0.2
    
     ;; Query time: 0 msec
     ;; SERVER: 127.0.0.11#53(127.0.0.11)
     ;; WHEN: Tue Oct 01 23:47:24 UTC 2019
     ;; MSG SIZE  rcvd: 44
    

    「ANSWER SECTION」において、mysqlにおけるAという 1 つのレコードが172.23.0.2を持っているのがわかります(この IP アドレスはお手元では、まず間違いなく別の値になっているはずです)。 mysqlというのは、普通は適正なホスト名ではないので、Docker はこの名前から IP アドレスを解決しています。 この IP アドレスはこのコンテナーのものであって、ネットワークエイリアスを持っていたからです(前の手順において--network-aliasフラグを用いたことを思い出してください)。

    これはどういうことでしょう... アプリとしては単にホスト名mysqlに接続できて、データベースとやりとりができさえすればよいのです。 こんな簡単にできるなんて。

MySQL を使ったアプリ実行

Todo アプリは環境変数をいくつか設定することによって MySQL への接続をサポートしています。 それは以下のようなものです。

  • MYSQL_HOST - 実行している MySQL サーバーのホスト名。
  • MYSQL_USER - 接続に利用するユーザー名。
  • MYSQL_PASSWORD - 接続に利用するパスワード。
  • MYSQL_DB - 接続後の利用データベース。

環境変数を利用した接続設定

環境変数を使って接続設定を行うことは、開発環境においては問題ありません。 しかし本番環境において動作させるアプリケーションに、そうした設定を行うことは 極めて不適切 です。 Docker の前セキュリティリーダー Diogo Monica 氏が すばらしいブログ投稿 においてその理由を説明してくれています。

コンテナーオーケストレーションのメカニズムにおいては、よりセキュアなものとして Secret 機能がサポートされています。 たいていの場合、この Secret というものは実行コンテナーに対してファイルの形でマウントされます。 多くのアプリケーション(MySQL イメージや Todo アプリを含む)でも、環境変数の末尾に_FILEをつけて、変数を含んだファイルを指し示しているものがあります。

たとえばMYSQL_PASSWORD_FILEという変数を設定することで、そこから参照できるファイル内容が接続時のパスワードであるものとして、アプリが利用するようにできます。 Docker はそういった変数の受け渡しをサポートする機能はありません。 この変数を求めたりファイル内容を得たりするのは、アプリが行わなければならないことです。

ここまで説明してきましたので、開発向けコンテナーを起動させましょう。

  1. メモ: MySQL バージョン 8.0 およびそれ以降では、mysqlでは以下のコマンドが含まれていることを確認してください。
     mysql> ALTER USER 'root' IDENTIFIED WITH mysql_native_password BY 'secret';
     mysql> flush privileges;
    
  2. 上で説明した環境変数をそれぞれ設定して、コンテナーへの接続を通じてネットワークにアクセスします。

     $ docker run -dp 3000:3000 \
       -w /app -v "$(pwd):/app" \
       --network todo-app \
       -e MYSQL_HOST=mysql \
       -e MYSQL_USER=root \
       -e MYSQL_PASSWORD=secret \
       -e MYSQL_DB=todos \
       node:12-alpine \
       sh -c "yarn install && yarn run dev"
    

    PowerShell を利用している場合は以下のコマンドとします。

     PS> docker run -dp 3000:3000 `
       -w /app -v "$(pwd):/app" `
       --network todo-app `
       -e MYSQL_HOST=mysql `
       -e MYSQL_USER=root `
       -e MYSQL_PASSWORD=secret `
       -e MYSQL_DB=todos `
       node:12-alpine `
       sh -c "yarn install && yarn run dev"
    
  3. コンテナーのログ(docker logs <container-id>)を見てみると、MySQL データベースの利用を示すメッセージが出力されているはずです。

     $ nodemon src/index.js
     [nodemon] 1.19.2
     [nodemon] to restart at any time, enter `rs`
     [nodemon] watching dir(s): *.*
     [nodemon] starting `node src/index.js`
     Connected to mysql db at host mysql
     Listening on port 3000
    
  4. ブラウザー上においてアプリを開き、Todo リストに 2、3 のアイテムを追加します。

  5. MySQL データベースに接続してみて、間違いなくアイテムがデータベースに書き込まれたことを確認します。 パスワードは secret です。

     $ docker exec -it <mysql-container-id> mysql -p todos
    

    そして MySQL シェルから以下を実行します。

     mysql> select * from todo_items;
     +--------------------------------------+--------------------+-----------+
     | id                                   | name               | completed |
     +--------------------------------------+--------------------+-----------+
     | c906ff08-60e6-44e6-8f49-ed56a0853e85 | Do amazing things! |         0 |
     | 2912a79e-8486-4bc3-a4c5-460793a575ab | Be awesome!        |         0 |
     +--------------------------------------+--------------------+-----------+
    

    みなさんのテーブルにはみなさんのアイテムが書き込まれていますから、上と完全に同じものにはなっていないはずです。 しかし確かにデータが保存されたことがわかったはずです。

Docker ダッシュボードで確認してみると、2 つのアプリコンテナーが実行されています。 ただしこの 2 つが 1 つのアプリを通じてグルーピングされていることを示す情報はどこにもありません。 このことを改善していく方法について、この先で見ていきます。

グループ化されていない 2 つのコンテナーが示された Docker ダッシュボード

まとめ

ここまでにアプリケーションが保存するデータを外部データベースとし、それも別々に動作するコンテナー内としました。 コンテナーにおけるネットワーク機能に関してちょっとだけ学び、DNS を通じてサービス検出が行われる様子も見てきました。

もっとも、アプリケーションの起動に必要となる手順に、少々戸惑いを覚えてきている頃かもしれません。 必要となることがネットワークの生成、コンテナーの起動、環境変数の設定、ポートの公開、などなどでした。 覚えることがたくさんになってしまったので、他の人に教えようと思ってもなかなか大変なことになってきました。

次の節では Docker Compose について説明します。 Docker Compose を利用すると、もっと簡単な方法でアプリケーションを構成することができます。 誰でも簡単に 1 コマンドで実現できるようにしましょう。

get started, setup, orientation, quickstart, intro, concepts, containers, docker desktop