【ざっくり理解】Dockerのコンテナとイメージ

【ざっくり理解】Dockerのコンテナとイメージ

Dockerを適切に扱うためには、コマンドを暗記するのではなく、 きちんとしくみを理解する必要があります。 本記事では、下記の項目で開発者ファーストの環境を実現する裏側に迫ります。

  • Dockerのしくみ
  • ボリューム
  • ネットワーク
  • DockerCompose

アプリケーションをDockerで適切に扱うには、 dockerコマンドの暗記ではなく、 コンテナとイメージの違いを理解する必要があります。 ここではまず、基本概念であるイメージレイヤを学びます。 それから、コンテナ間やホストでファイルなどを共有するボリュームや、 コンテナ間で通信する際のネットワークについて理解を深めていきましょう。

Dockerのアーキテクチャ

第1章でも説明したとおり、 DockerコンテナやDockerイメージを扱うプログラム全体を

DockerEngineと呼びます。 Dockerが開発された初期は、 「docker」という名前の実行ファイルが、 コマンドラインツールとサーバ(デーモン)を兼ねていました。 現在のDockerEngineは、 複数のプロセス群で構成されています(図1)。 各要素の役割は、 以下のとおりです。

DockerEngine(dockerd)

Dockerサーバの中心的な存在です。 Linux上でDockerをデーモンとして管理する対象全体がDockerEngineであり、 これらのまとめ役がdockerdデーモンです。 DockerCLIからのAPIエンドポイントを受け付け、 処理内容をcontainerdに伝えます。 このDockerEngineは、 OSSプロジェクトmobyの成果物をベースにしています。

containerd *2

内部APIのエンドポイントを持ち、 複数のDockerコンテナやDockerイメージを管理する役割です。 dockerd-shimプロセスを通し、 コンテナランタイムを操作します。

runc *3

DockerEngineのデフォルトのコンテナランタイムです。 個々のコンテナ(プロセス)を隔離したり特別な権限を付与したりするために、 Linuxカーネルと通信する機能を担います。

Dockerのインストールと環境準備

Dockerのインストール作業とは、 DockerEngine本体だけでなく、 関連するパッケージや、 コマンドラインツールを含めたセットアップを指します。 各OSの環境により、 手順は異なります。

Linux

Linux環境で最も簡単なセットアップ方法は、 インストール用スクリプトの実行です。 このスクリプトは、 Debian、 Ubuntu、 CentOS、 Fedoraの利用可能なバージョンを自動判別し、 常に最新の安定バージョンをインストールします。

そのため、 プロダクション環境では推奨されていませんが、 ちょっとした開発環境を準備したい場合には、 とても手軽で便利です。 なお、 対応しているディストリビューションや、 それぞれの正確なセットアップ手順を知りたい場合は、 公式ドキュメントをご覧ください。

インストール用スクリプトはhttps://get.docker.com/で配布されています。 WebブラウザでURLを開くと、サイトの表示が「#bin/bash」で始まるため、 シェルスクリプトだとわかります。 そして、ページの冒頭を確認すると、 インストール用のコマンドが表示されています。

UbuntuServer20.04にDockerEngineの最新安定版をインストールするコマンドは、図2のとおりです。 上記のサイトの表示内容をgetdocker.shに出力し、 それをシェルスクリプトとして実行するコマンドです。

なお、CentOS7、8では、 aptの代わりにyumまたは.dnfコマンドを使います。 また自動でDockerEngineが起動せず、 ブート時にも自動実行されないため、 以下のコマンドも併せて実行する必要があります。


$systemctl start docker && systemctl enable docker

正常にインストールが完了していると、 docker versionコマンドの実行後、 クライアントとサーバの両方のバージョンを表示します。

一般ユーザーでdockerを操作するには

通常、一般ユーザー権限では、 dockerコマンドは使えず、sudoを利用する必要があります。 そこで、sudoを使わずに操作する、 「docker」グループに所属する方法を紹介します。


$sudo group add docker
$sudo usermod -aG docker ユーザー名

こうすると、設定対象のユーザーはdockerグループ所属となり、 次回ログイン以降からsudoは不要です。


🔽 図2DockerEngine(最新安定版)のインストール

$sudo apt update
$sudo apt install curl-y
$curl -fsSL https://get.docker.com -o get -docker.sh
$sudo sh ./get-docker.sh

ただし、この設定は、セキュリティ上非常にリスクがあるため注意が必要です。 ここからは、どのようなリスクがあるのか説明します。

dockerコマンドは、 デフォルトで/var/run/docker.sockにあるUNIXソケットドメインを通してDockerEngineにアクセスします。 この所有権を確認しましょう(図3)。

ご覧のとおり、 所有者が「root」、 所属グループが「docker」で、 先頭に「s」のスティッキービットの属性を持ちます。 つまり「docker」グループに所属する一般ユーザーは、 このソケットに対して「root」としてアクセス可能であり、 事実上のroot権限を持ちます。 そのため、 複数のユーザーがサーバを共用する環境上では、 利用を避けるべきです。

システム全体の権限が不要であれば、 後述のRootlessモードをご検討ください。

Rootlessモード

Docker20.10)5GA(GeneralAvailability)となった機能にRootless(ルートレス)モードアがあります。

WE*DockerEnginelt.LinuxErootして実行する必要がありました。 そのため、 意図せぬセキュリティリスクが発生したり、 システム要件によってはDockerEngineを利用できなかったりするシーンもありました。

そこで導入されたのが、 Rootlessモードです。 Rootlessモードは、 Dockerデーモンをroot権限で実行するのではなく、 DockerデーモンとDockerコンテナをユーザー名前空間で実行します。 一般ユーザーでコンテナを扱えるようにする技術のため、root権限は不要です。 また、通常のDockerデーモンが扱うSETUIDビットやファイルケーパビリティも使用しません。

*6) 特殊なアクセス権の1つ。所有者とrootユーザー以外、ファイルを改名、削除できないようにするものです。
*7) https://docs.docker.com/engine/security/rootless/

ただし、動作条件としてnewuidmapとnewgidmapが利用できるシステムである必要があります。 CentOS8ではfuse-overlayfs日やiptablesも必要になります。 その他詳細は、ドキュメントをご覧ください。

要件を満たしている環境であれば、 Rootlessモードは次のコマンドでセットアップできます。 最後に、コマンド実行後に表示される環境変数PATHとDOCKER_HOSTを有効にします。


$ curl -fsSL https://get.docker.com
root less | sh

Windows と macOS

Windows や macOSでは、Linux用のDockerEngineは直接動きません。 各OS上でDockerを動かす方法は2つあります。

Docker Desktop for Windows macOSを使う方法

macOSを使う方法制約がなければDockerDesktopを使う方法が簡単です。 DockerDesktopは、 インストーラでセットアップするだけで、 各OS環境にあわせて自動的にDockerが利用可能な環境を提供します。 シェルやコマンドラインともシームレスに連携し、 Linux版のDockerEngineにはない、 Dockerコンテナやイメージなどのリソースを管理できるGUIもあります。 ダウンロードは配布サイトは10をご覧ください。

仮想マシンを使う方法

VirtualBoxやWSL2のような、 仮想マシン上にLinuxをインストールし、 その上にDockerEngineをインストールして使う方法もあります。

注8)https://github.com/containers/fuse-overlayfs*9)https://docs.docker.com/go/rootless/注10)https://www.docker.com/product/docker-desktop

ただし、 DockerEngineの諸設定はLinuxと変わらず、 さらにLinux仮想マシンそのものを管理する必要があるため、 慣れた方向けの手段です。

アプリケーションをパッケージ一化するDockerイメージ

あらためてDockerとは何かと言うと、 第1章でも説明したとおり、 アプリケーションを動かすために必要な依存関係をパッケージ化するものです。 このパッケージ化されたものをDockerイメージと呼び、 それをDockerコンテナとして実行できます。 ここからは、 イメージとコンテナに共通する重要な概念、 「イメージレイヤ」を説明します。

Dockerイメージはイメージレイヤの重なり

仮想マシンの場合、 「仮想マシンの起動」とはOSそのものを仮想的なハードウェア環境上で動かすことを指します。 そのため、 仮想マシンのイメージは、 OS全体のファイルやストレージ容量が必要ですので、 サイズが大きなファイルになるのが一般的です。

一方のDockerは、 アプリケーションをコンテナと呼ぶ特別な状態で起動します。 そのため、 Dockerイメージに必要なのは対象アプリケーションを動かすための最小のファイル群で済みます。 たとえば、 Ubuntuのファイルシステムを実行できるubuntu:20.04イメージの容量は約70MBです

し、 AlpineLinuxのalpine:3.14は約5MBしかありません。 そのうえ、 OSの起動ではないため、 OS全体を動かすファイルの準備は不要です。

なお、 Dockerイメージはファイルではなく、 ファイルシステムを扱うための抽象的な概念です。 そして、 実体としてのファイルやディレクトリは、 抽象的なイメージレイヤの積み重ねで構成されます。

各イメージレイヤに、 Linuxファイルシステムを構成するファイルやディレクトリと、 Dockerイメージ実行時に必要となるコマンド、 ・メタ情報を含みます(図4)。 このイメージレイヤは、 読み込み専用であり、 変更できません。 そのため、 Dockerイメージ全体も、 DVDROMメディアのように読み込み専用です。

なお、 DockerにはDockerイメージを自動構築するしくみとして、 Dockerfileと呼ぶファイルを利用できます。 このファイル中でFROM命令を使うと、 親イメージ(parentimage)の情報をイメージレイヤに記録します。 さらに、 親イメージ内に含まれるファイルシステムが、 イメージを構築処理するためのファイルシステム上からも存在するかのように見えます。 親イメージ内のファイルシステムは読み込み専用ですが、 構築時ま"には通常のファイルと同様に処理(コピーや編集や削除)ができます。 その 注11)イメージ構築時、 一時的に自動起動する中間コンテナ (intermediatecontainer)から見えるファイルシステム。

ため、 あたかも元のイメージレイヤを共有しながら、 新しいイメージレイヤを「差分」として派生しているかのように扱えます(図5)。

結果として、 Dockerを使う環境上では不要なディスク容量の増加を招きません。 さらに、 別のDocker動作環境にアプリケーションを移動する場合も、 差分となるイメージレイヤだけを移動できます。 これにより、 いわゆる、 Dockerを使ったアプリケーションのポータビリティや、 軽量性が実現されています。

DockerfileでDockerイメージ構築

これまで説明したとおり、 Dockerイメージはイメージレイヤを概念として積み重ねたものです。 また、 Dockerイメージ用のイメージレイヤはすべて読み込み専用で、 変更できません。

前述のとおり、 Dockerコンテナ作成時のイメージレイヤだけ読み書き可能です。 このコンテナ用のイメージレイヤは、 dockercontainerdiffコマンドの実行によって見える元イメージとの差分情報であり、 dockercontainerpsーコマンドで容量を確認できます。 この内容を、 dockercontainercommitコマンドを使用してDockerイメージ用のレイヤに変換可能です。

しかしながら、 Dockerイメージを作るために、 毎回このコマンドを実行するのは大変です。 Docker本来の目的の1つは、 すばやいアプリケーションの開発体験ですが、 これでは本末転倒になってしまいます。

この問題を解決するのがDockerfileを使った自動構築です。 Dockerfileはテキスト形式のファイルです。 DockerfileにはDockerイメージを自動構築する命令を書きます。 各行は「ディレクティブ値」の構文で構成されており、 これは(概念としての)イメージレイヤに相当します。

以下はAlpineLinuxイメージ、 alpine:latestのDockerfileです。

FROMscratchADDalpine-minirootfs-3.14.2-x86_64.tar.gz/CMD[/bin/sh"]

FROM

Dockerイメージ構築時、 元になるイメージ名とタグを指定します。 seratchは特殊な名前で、 空っぽで何もない、 親イメージがない状態を意味します。

ADD

Dockerイメージ内のファイルシステムに、

ファイルを追加する命令です。 ここではAlpineLinuxのファイルシステムが入ったtar形式のファイルを展開しています。

CMD

Dockerコンテナの実行時、 デフォルトで実行するコマンドを指定するメタ情報です。 ここでは/bin/shを自動的に実行します。

dockerimagebuildの裏側

dockerimagebuildコマンドの実行時、 このDockerfileを読み込むと、 行を処理するたびに中間コンテナを起動し、 イメージレイヤを作成する処理(手動でのコマンドdockercontainercommit実行と同じ状態)を行います(図6)。

ちなみに、 このイメージレイヤの構造はdockerimagehistoryコマンドでも確認できます。 図7では、 新しく追加コミットされたレイヤが上に表示されています。

なお、 この実行結果はFROM命令に相当する列がなく、 2行しか見えません。 前述のとおり、 FROMscratchで、 何も存在しないイメージレイヤを指定したからです。 このようなイメージをベースイメージ(baseimage)と呼びます。 また、 各行がDockerfileで指定した命令と対応しているのがわかります。

ここではAlpineLinuxを例に取り上げましたが、 すべての公式DockerイメージはこのようなDockerfileを通して構築されています注1た。 つまり、 Ansibleなどの構成管理ツールのように、 Dockerfileを設定ファイルとして、 メンバー間で共有したり、 再利用したりできます。 また、

注12)公式イメージのDockerfileはGitHubで公開されています。 https://github.com/docker-library/official-images

DockerfileがあればDockerイメージの中身を確認できるため、 透明性の確保にもつながります。

あらためてDockerコンテナの実行とは?

Dockerコンテナとは、 名前空間の分離やcgroupの利用による、 特別な状態でプロセスを実行するものを指します。 Dockerの場合は、 Dockerイメージの中に含まれているアプリケーションを、 このコンテナという、 特別な状態のプロセスとして起動するものです。

このDockerコンテナも、 Dockerイメージと同じようにイメージレイヤを持ちますが、 読み書き可能です。 それ以外は、イメージレイヤとしての性質は同じです。 イメージレイヤは親イメージの情報を持つため、 1つのイメージレイヤを複数のイメージレイヤ間で共有できます。

Dockerイメージが1つあれば、 ホスト上でDockerアプリケーションを複数、 互いに影響を与えずに実行できます。 これは、 前述のイメージレイヤのしくみがDockerコンテナにもあるからです。 このしくみに加え、 Dockerコンテナを実現するために名前空間の分離やgroupによるリソース制限を行うので、 結果としておスト上で複数のアプリケーションをすばやく実一行できる環境を実現しています(図8)。 「これまで説明したとおり、 通常の仮想サーバとは異なり、 Dockerコンテナは単なるプロセスです。 そのため、 コンテナの起動とは、 特別な状態としてプロセスを起動するだけで、 コンテナの停止とは対象プロセスの停止を指します。 コンテナの削除とは、 正確にはコンテナ用のイメージレイヤと関連ファイルの削除を指します。

CentOS LinuxのDockerイメージの起動とは?

Dockerイメージには、 centosPubuntuのように、 Linuxディストリビューション名を持つものがあります。 これらのイメージでコンテナを実行する際の挙動を、 centos:8 イメージを例に考えましょう。 dockerrun-itdcentos:8の 実行が意味するのは、 CentOS8のDockerイメージを使い、 その中の/bin/bashをPID1として動作するコンテナを起動することです。 *注意が必要なのは、 DockerはCentOS8の仮想マシンを起動しているのではないことです。 あくまでもCentOS8のファイルシステムに含まれる/bin/bashをコンテナとして起動しているに過ぎません。 /bin/bashをホスト上で起動していますが、 コンテナ内のPID名前空間では/bin/shのプロセスしか存在しないように見えます。 また、 コンテナ内のプロセス空間から見えるファイルシステムも、 centos:8用のDockerイメージを基に、 このコンテナ用に追加されたイメージレイヤしか見えません。

一方で、 コンテナはLinuxカーネルとホスト上の一部が共通しているため、 cat/proc/meminfoなどを実行すれば、 /proc/上の情報も制限された状態としてアクセス可能です。

ですので、 結果として1つのLinux上で、 被数のCentOSのバージョンが混在しているかのように見え、 UbuntuやDebianのように異なるディストリビューション環境との混在もできるのです。

Dockerとは仮想化技術ではないというのは、 このような実装からも確認できます。

コンテナ間やホスト上でファイルを共有するボリューム

さて、これまではイメージレイヤを通して、 DockerイメージとDockerコンテナの違いを説明してきました。 ところで、隔離されたコンテナ間でファイルを共有したい場合や、 ホスト上のファイルにアクセスしたい場合は、 どのようにしたらよいのでしょうか。

Dockerには、 Dockerボリュームというコンテナ内における特別なディレクトリまたはファイルがあります。 例えると、 DockerイメージがDVD-ROMのようにデータが固定された内容であるのに対し、 ボリュームは外部ストレージのように動的なデータを保管するものです。

このボリュームは、 コンテナのライフサイクルと切り離された設計思想です。 Dockerコンテナからイメージを作成しても、 ボリュームはイメージの中に反映されません。 また、 コンテナを削除しても、 ボリュームは残り続けます。 そのうえ、 図9のとおり、 ボリュームはコンテナ間でディレクトリやファイルを共有できるほか、 ホスト側のディレクトリやファイルも、 コンテナ内と共有できます(バインドマウントと呼びます)。

Dockerボリュームは、 「名前付き」「名前なし」「ホストのマウント(bindmount)」の3種類です。 dockervolume系のサブコマンドで操作できます。 いずれも、 先述のCopyonWriteを経由せず、 ダイレクトにホスト上のファイルシステムとアクセス可能な機能を提供するため、 読み書き性能は劣化しません。 また、 Dockerイメージ用のレイヤと、 コンテナ用のイメージレイヤ、 そしてボリュームの各領域は、 コンテナの名前D空間内ではaufsの実装により、 1つに統合されて見えます(図10)。

つまり、 いったんコンテナが起動さえしてしまえば、 そのコンテナ用のプロセス空間内からアクセス可能なファイルシステムとは、 Dockerコンテナ用のイメージレイヤかボリュームか見分けがつきません。 結果的に、 一見すると通常のLinuxディストリビューションと何ら変わらないかのように利用できます。

通常このDockerボリュームは、 Dockerイメージの中に保管する必要のない、 動的なデータ用に使います。 ただし、 Dockerイメージに含むアプリケーションやデータは、 ホスト間でのポータビリティを実現していますが、 Dockerボ

リュームはポスト間での移動は前提としていません。 ボリュームにあるデータをほかのホストに移動する場合は、 OS上のファイルをコピーするなど、 Dockerのしくみ以外を使う必要があります。

Dockerコンテナはネットワークも隔離。

これまでプロセスやファイルシステムの隔離を中心に解説しました。 なお、 Dockerコンテナからは、 デフォルトではネットワークも隔離されています。

通常dockercontainerrunを実行して起動するアプリケーションは、 デフォルトの「bridge」という名称のブリッジネットワークに接続します。 このとき、 コンテナ内で見えるネットワークは、 ホスト上のインターネットインターフェースにルーティングされているため、 コンテナの中から外への通信は可能です。 しかし、 このコンテナ内で何らかのアプリケーションがポートをリッスンしていても、 外部のホストから通信はできません。 コンテナ側のボートが外部と通信するには、 起動時にpまたは-Pオプションでポートを公開する設定が必要です。 このDockerネットワークはdockernetwork

系のサプコマンドで操作できます。 たとえば、 dockernetworkIsコマンドを実行すると、 3つのネットワーク(図11)を表示します。 これらは、 Dockerをインストールした直後からデフォルトで存在します。

$dockernetworkisNETWORKIDNAME01866fcc6836bridge1a22ad41aa65host50056204921anone DRIVERbridgehostnull_ SCOPElocalLocallocal

●bridgeネットワーク

Dockerコンテナ実行時デフォルトで適用されるネットワークです。 コンテナ内から外のネットワークに対して通信するには、 Linuxのbridge機能を通して利用可能にします。 また、 このデフォルトのbridgeネットワークとは疎通しない、 別のbridgeネットワークを、 ユーザー定義bridgeネットワークとして作成できます。 複数のコンテナ間での通信に欠かせない、 コンテナ名(サービス名)での名前解決が、 ユーザー定義bridgeネットワークでは利用できます。

●hostネットワーク

ホスト側のインターフェース情報がそのまま見えるモードです。 正確には、 ネットワーク機能を備離せずにコンテナを起動していると言えるでしょう。

●noneネットワーク

ネットワークインターフェースを持ちません。 コンテナ外からの通信を受け付けないだけでなく、 コンテナ内からネットワーク側での通信がいっさいできません。

●Dockerネットワークの特徴

特徴は、 コンテナはパブリックなIPv4アドレスを直接持ちませんが、 bridgeネットワークでは、 プライベートなIPアドレスを持つ点です。 コンテナ上で動いているサービスを公開するには、 ホスト側のポートに割り当て(マッピング)する必要があります。 また、 ホスト側では、 コンテナおよび複数のプロセスが、 同時に1つのポートをリッスンできません。 ホスト外からの通信は、 docker-proxyというプログラムが、 Dockerコンテナ内でリッスンしているプロセスに対して、 1対1で処理します。 デフォルトのbridgeネットワークでは、 Dcockerコンテナ以外のプロセスとの通信における負荷分散や、 プロキシ機能はありません。

このように、 Dockerコンテナはアプリケーション問でプロセスやファイルシステムを隔離

するだけでなく、 Dockerネットワークも分けることで、 お互い影響を与えず起動できます。

Docker Compose と Docker

これまで説明してきたDockerコンテナ、 Dockerイメージ、 Dockerボリューム、 Dockerネットワークを、 YAMLファイル(dockercompose.yml)1つで管理できるツールがDockerComposeィ13です。

アプリケーションの実行時、 必要となるネットワークやボリュームは、 Composeプロジェクトの名前(デフォルトではディレクトリ名)を接頭語として使います。 そのため、 1つのホスト上でも、 複数のアプリケーションを、 相互に影響を与えず起動するのが簡単です。

近年は、 GitHub上でさまざまなプロジェクトのdocker-compose.ymlが公開されるようになってきました。 これまで課題だった、 アプリ に役立ちます。 また、 自分が作ったアプリケーションもDockerComposeでの利用を目指すことが、 チームのメンバーでの共有や、 第三者への公開時に役立つと言えるでしょう注い。

13)https://docs.docker.com/compose/

Dockerとはアプリケーションを簡単に開発・共有・実行するためのプラットフォームです。 その後ろ側ではLinuxカーネルの名前空間、 cgroup、 ケーパビリティの各機能を利用して、 特別な状態としてプログラムを実行します。

Dockerコンテナを開発・デプロイしやすくするため、 イメージレイヤによって構成されるDockerイメージの実装や、 イメージレイヤとホスト上のファイルを1つに見せるボリューム機能が組み合わされています。 さらにネットワークの開催によって、1つのホスト上で、 複数のプログラムが同一のポートをコンテナ内でリッスンできるようになりました。

一見すると複雑に思えるかもしれませんが、 いかに簡単かつすばやい開発体験を行えるかという、 開発者ファーストの設計思想によるものです。 優劣の話ではなく、 仮想マシンとは開発・運用ワークフローの異なる場合が多いため、 導入する環境においてDockerが適しているかどうかを熟慮する必要があります。