【Docker】1たす1から始めるクラスタ環境構築 ②Volume編

前回はDockerのごく基礎的な操作を実行し、イメージを作成したりコンテナを起動するということを行いました。 そのなかで、Dockerはデフォルトでは「コンテナ内での変更を保持することができない」ということを学びましたね。 今回はDockerでデータの永続化を可能にするための機能に焦点を当てて、実際のアプリケーションの環境構築を通してDockerの理解を深めましょう。

Dockerチュートリアル連載記事

  • 【Docker】1たす1から始めるクラスタ環境構築 ①基礎編
  • 【Docker】1たす1から始めるクラスタ環境構築 ②ボリューム編(←いまここ)
  • 【Docker】1たす1から始めるクラスタ環境構築 ③ネットワーク編(作成中)
  • 【Docker】1たす1から始めるクラスタ環境構築 ④Docker-Compose編(作成中)

Dockerにおけるデータバインディング

ボリューム」は、Dockerにおいて「データの永続化」を可能にするための機能です。

多くの記事で取り上げられているこの、「データの永続化」という機能。

非常に便利で開発の際には必須の機能なのですが、文字列のままだといまいちイメージがつきづらいですね🤔

まずは簡単にその種類を確認しておきましょう。

名前付きボリューム(Named Volume)

docker run -v <ボリューム名>:<バインドするファイルパス>

「ボリューム」は、ホストOSに新たなディレクトリを作成し、そこにコンテナ内の特定ディレクトリをマウントすることで、コンテナの削除後もデータを残存させることのできる機能です。

上記の記法でボリュームを作成すると、指定した名前でDockerがアクセスすることができるデータ領域が作成されます。

この「名前付きボリューム」は、複数のコンテナでのデータ共有することができ、特定のコンテナに依存しないデータの永続化が可能になります。

匿名ボリューム(Anonymous Volume)

docker run -v <バインドするファイルパス>

「匿名ボリューム」は名前を指定しないで作成するボリュームで、DockerEngineが作成した適当なハッシュがボリューム名として割り当てられます。

名前付きボリュームと違い、複数のコンテナと共有することはできませんが、そのコンテナ専用の一時的な保存領域が欲しい場合に利用します。

バインドマウント(Bind Mount)

docker run -v <ホスト側の絶対パス>:<バインドするファイルパス>

ホストOSのファイル・ディレクトリ構造をコンテナの指定したファイルパスのファイルシステムと同期するための機能です。

ホストOS側で編集した内容などをコンテナに反映させることが可能となります。

Dockerチュートリアル ボリューム編

コマンドベースだけだと退屈なので、ちょっとしたチュートリアルアプリケーションを作成してみました🎉

まずは、アプリケーションのかんたんな説明を、、、

①構成

シンプルなNode.jsのアプリケーションです。

  • サーバーサイド:Node.js
  • フロントエンド:Node.js

②機能

DBなどは使用せずに、ディレクトリ内にjsonファイルを生成してそこにデータを保存するという仕組みです。

①記事の作成  ブログのようにタイトルとコンテンツを記述して保存することができます。

②記事の閲覧  作成した記事は、一覧・詳細画面で参照することができます。

githubからソースを取得する

githubにソースをアップしているので、下記コマンドでソースをローカルにcloneしてください。

vagrant@vagrant:~$git clone git@github.com:y-hoshi-cs/docker-ls1.git node

アプリケーションをDocker化する

cloneしてきたアプリケーションをさっそく実行したいところですが、実は今回用意しているVagrant環境にはnode.jsは入っていません。

そこで、前回rubyコンテナでおこなったように、Dockerfileを作成しアプリケーションイメージを作成&コンテナ化していきましょう。

①Dockerfileの作成

cloneしてきたアプリケーションのディレクトリに移動し、コンテナ起動用のDockerfileを作成しましょう。

vagrant@vagrant:~$ cd node
vagrant@vagrant:~/node$ touch Dockerfile

Dockerfile

FROM node:14

WORKDIR /app

COPY package.json .

RUN npm i

COPY . .

EXPOSE 8080

CMD ["npm", "start"]

EXPOSE, CMDというコマンドが出てきましたね。

コマンドコマンドの概要
EXPOSEコンテナ立ち上げ時に公開するポート番号を指定する
CMDコンテナ立ち上げ時に引数のコマンドを実行する


EXPOSEに数値を指定すると、コンテナ内の該当ポートがホストOSの同じポートに割り当てられます。

結果として、コンテナ内でサーバーを起動している状態でブラウザからホストの該当ポートを見に行くと、コンテナで起動したサーバーにアクセスできるというわけです。

CMDには、コンテナ立ち上げ時に毎回実行するコマンドを記載します。

今回は、package.jsonであらかじめ定義した"npm start"というコマンドをコンテナ立ち上げ時に実行して、サーバーをスタートさせるという目的で記載しています。

②コンテナの起動

このDockerfileをイメージ化し、コンテナを立ち上げましょう。

vagrant@vagrant:~/node$ docker build -t node:docker01 .
vagrant@vagrant:~/node$ docker run -dit --rm --name node -p 8080:8080 node:docker01

ブラウザから192.168.33.10:8080にアクセスすると、冒頭のアプリケーション画面が見られるはずです!

せっかくなので、ちょっとデータを登録してみましょう。

アプリケーションのHome画面でタイトルを入力して"Create"ボタンを押下してみましょう。 記事の詳細画面に遷移します。

入力項目があるので、適当なことを入力して"Update"ボタンを押下します。

これで、記事が保存されました。

Blogs画面へ行くと、作成した記事が一覧に表示されていますか?

③コンテナを停止する

アプリケーションで記事が作成できたところで、このコンテナをいったん停止しちゃいます。(--rmフラグがついていたので自動的にコンテナが破棄されます)

で、また起動してみます。

vagrant@vagrant:~/node$ docker stop node
vagrant@vagrant:~/node$ docker run -dit --rm --name node -p 8080:8080 node:docker01

画面から再度アプリケーションにアクセス&Blogs画面に行くと、、、

おやぁ?🤔

先ほど作成した記事がなくなっていますね、、、

実はこれ、前回のrubyコンテナと同じ現象が起きています。

④問題を整理する

冒頭でお伝えしているように、このアプリケーションでは作成した記事はローカルのファイルシステム内(つまりDockerコンテナ内)にjsonファイルとして保存されます。

なので、コンテナを起動している間は期待どおりに作成したファイルを蓄積してくれます。

しかし、コンテナを一度削除してしまうとイメージ作成時点のディレクトリ構造に戻ってしまうのです!

さて、どう対処したものか。

Volumeという機能

今回立ち上げているコンテナで問題なのは、「コンテナを立ち上げるたびに、コンテナ内で編集した内容が初期化されてしまう」ということ。

それであれば、コンテナ内の編集内容をどこか別のところに退避させればよいわけです。

ここで登場するのが、冒頭で説明した「Volume」という機能です!

Volumeを使うと、コンテナのみに保持されていたデータを他の場所に退避させることができます。

①名前付きボリュームを使用してコンテナを立ち上げる

まずは「名前付きボリューム」から。

コマンドでコンテナを立ち上げます。

vagrant@vagrant:~/node$ docker run -dit --rm -p 8080:8080 -v node-data:/app --name node node:docker01

-vというオプションが出てきましたね!

このオプションを使用すると、ホストOSのディスク内の領域をDockerのコンテナにマウントすることができます。

vagrant@vagrant:~/node$ docker volume ls
DRIVER    VOLUME NAME
local     node-data

node-dataという、先ほど-vオプションの直後につけた文字列がVOLUME NAMEの欄に表示されています。

これが、このボリュームの名前なのですね。では続けて、、、

vagrant@vagrant:~/node$ docker volume inspect node-data
[
    {
        "CreatedAt": "2021-08-25T14:36:55Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/node-data/_data",
        "Name": "node-data",
        "Options": null,
        "Scope": "local"
    }
]

ボリュームの詳細が表示されました!

このMountpointというのがnode-dataボリュームがマウントされているところです。 (特に知らなくてもいいですが、、、)

ボリュームのマウントされていることがわかったところで、ブラウザからアプリケーションにアクセスし、再び記事を保存してみましょう。

そして、間髪入れずコンテナを削除します。そしてまた立ち上げます。

vagrant@vagrant:~/node$ docker stop node
vagrant@vagrant:~/node$ docker run -dit --rm -p 8080:8080 -v node-data:/app --name node node:docker01

アプリケーションを立ち上げると、前のコンテナで保存した内容がちゃんと引き継がれています!

名前付きボリュームを使用してホストOSのディスクにデータを保存することで、データの引継ぎに成功しました🎉

③現状のコンテナの問題点

名前付きボリュームを使用することで、データをホストOSのディスクにマウントして、変更内容を保持することができました。これは、実際のアプリケーション開発には必須の要件ですね。

しかし、開発する際にはもっと大事なことがあります。

アプリケーションを編集できなくてはいけないですね。

試しに、VSCodeで現状のアプリケーションのテンプレート部分に変更を加えてみます。

~/node/views/_header.pug

header.header 
  div.header__toggler
  div.header__title JsonBlog Yeah!

アプリケーション左上のタイトルを変更してみました。画面をリロードしてみましょう。

...

当然といえば当然ですが、コンテナはホストOSのファイルシステムから独立しているので、ホストから変更を加えてもコンテナ内のアプリケーションには変更が反映されません。

これじゃコードの変更ができないですね!困った。。。

③ボリュームマウントを使用してコンテナを立ち上げる

ホストOSのファイルシステムをコンテナにマウントすることのできる機能も、Dockerには存在します。

それが「ボリュームマウント」です。

ボリュームマウントする際のフォーマットがこちら。

docker run -v <ホストOSのフルパス>:<コンテナ内のパス> <イメージ名>

ホストOSのフルパスというのはlinuxでは"pwd"コマンドを実行した際に表示されるパスを指します。今の環境でいえば、コマンドはこんな感じになります。

docker run -v /home/vagrant/node:/app node:docker01

これはまだ短いほうですが、いちいちフルパスを記述しているとやたらコマンドが長くなってしまうケースがあります。ので、こういう便利な記法があります。

docker run -v "$(pwd):/app" node:docker01

だいぶ短くなりました!これでコンテナを立ち上げましょう、、、

vagrant@vagrant:~/node$ docker run -dit --rm -p 8080:8080 -v "$(pwd):/app" --name node node:docker01

どうですか? いかがでしょう?

アプリケーションがクラッシュしましたね😇

vagrant@vagrant:~/node$ docker ps -a
CONTAINER ID   IMAGE                     COMMAND                   CREATED        STATUS                      PORTS     NAMES

コンテナの起動に失敗したのでそのまま削除されてしまっています。

--rmフラグを削除して再度起動してみます。

vagrant@vagrant:~/node$ docker run -dit -p 8080:8080 -v "$(pwd):/app" --name node node:docker01

同じく起動に失敗しますが、今度はdocker ps -aでコンテナがEXITEDになっていることが確認できます。

ログを確認してみましょう。

vagrant@vagrant:~/node$ docker logs node
> hoge@1.0.0 start /app
> nodemon server.js

sh: 1: nodemon: not found
npm ERR! code ELIFECYCLE
npm ERR! syscall spawn
npm ERR! file sh
npm ERR! errno ENOENT
npm ERR! hoge@1.0.0 start: `nodemon server.js`
npm ERR! spawn ENOENT
npm ERR!
npm ERR! Failed at the hoge@1.0.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm WARN Local package.json exists, but node_modules missing, did you mean to install?

npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2021-08-26T12_15_19_711Z-debug.log

何やら、コンテナ起動時のnpm startコマンド時点で必要な依存パッケージが見つかっていないようです。

先ほどまでは問題なく起動できていたのに、なぜか??

③匿名ボリュームを併用してコンテナを立ち上げる

結論を言ってしまうと、問題点は「コンテナのファイル群をホストからマウントしてきたファイル群で上書きしている」ということです。

確かに、ホストでファイルを編集した際にその変更がコンテナに反映されるようにするためには、ホストのファイルをコンテナのファイルシステムにマウントする必要があります。

しかし、今回の場合、ホストOSにはnode.jsの依存パッケージが格納された"node_modules"がありません。

その状態でコンテナのファイルシステムにマウントをしているため、コンテナ内の"node_modules"が削除されている→依存パッケージが解決できない→エラーという結果になっていたわけです。

こういう時の解決策となるのが、最後に紹介する「匿名ボリューム」です。

コンテナを、以下のコマンドで起動してみましょう。

vagrant@vagrant:~/node$ docker rm node
vagrant@vagrant:~/node$ docker run --rm -dit --name node -p 8080:8080 -v "$(pwd):/app" -v /app/node_modules node:docker01

...コンテナが無事起動したのではないでしょうか?

今回のコマンドでは新しく下記が追加されています。


-v /app/node_modules

匿名ボリュームは、コンテナ内のマウント対象を指定するだけで作成でき、名前付きボリュームと同様に、ホストOSのディスク内に一時領域を確保することができます。(名前付きボリュームと違い、ボリュームの名前を付ける必要はありません)

ボリュームのマウントを行う際、今回のように「バインドマウントより深い階層のディレクトリをマウントする」と、バインドマウントでマウントされるホストOSのファイルよりも、コンテナのファイルのほうが優先されます。

今回の場合、バインドマウントで/appをマウントしていますが、匿名ボリュームは/app/node_moduleをマウントしているため、node_moduleについてはコンテナ内で作成されたものが優先されるということです。

匿名ボリュームは、名前付きボリュームとは異なり「コンテナ同士のデータの共有」ができません。そのため、基本コンテナと一対一で作成するだけであまり使い勝手はよくないのですが、今回のようなケースでは役立てることができます。

複数コンテナの連携について...

さて、単体でのコンテナ起動ができたところで、この連載の主眼となっているのは「クラスタ環境構築」であることを忘れてはいけません、、、

次回は「Network」編。

Dockerでは複数のコンテナで連携するために、各コンテナを同一のネットワーク内でハンドリングするための機能があります。

また少し長めの記事になりますが、ぜひお付き合いくださいませ、、、

前へ

【Docker】1たす1から始めるクラスタ環境構築①基礎編

次へ

Googleに学ぶコードレビューのポイント