🐳

Docker基本のキ再勉強

2022/02/28に公開

まえがき

この記事は
「Dockerは仮想環境の選択肢の一つ。ボリュームのとかネットワークの接続は都度調べて半コピペでどうにかしている気がするな…」
くらいの私がDockerをもう少し学び直すときに作成したメモ類をまとめたものです。

公式ドキュメントが必要十分ではありますが、いきなり公式のドキュメントを読むとそこそこ情報量が多く、また読む順番にも個人的に困ったため、

・正確性や網羅性を犠牲にしても、概観を掴む
・個人的に必要な理解順に記載する

を基本としてまとめています。

Docker概要

Dockerとは?

コンテナ型の仮想化によってアプリケーションの開発、実行を行うためのツール。

じゃあコンテナとは?

container。英語的な意味で言えば”入れ物”。その名の通りでコンテナ型仮想化はアプリケーションの実行に必要なファイルシステム(フォルダの構成など)とプロセスを、”入れ物”に入れて隔離するような感じで実現する。厳密では確実に無いが、プロセスとファイルシステムに上手いこと別名をつける技術くらいのイメージが近いのかもしれない。

従来の仮想サーバとの方式の違い

従来の仮想サーバは、ホストOSがあるにせよ、ハイパーバイザによる仮想化を行うにせよ、OS自体をエミュレーションしている。一方コンテナは実行空間は隔離するが、OS機能(ハードウェアへの利用など)についてはホストOSのものをそのまま利用する。

従来の仮想サーバとの機能非機能的な違い、ユースケース

  • コンテナの方が起動速度が早い
    →OSを立ち上げる必要がないため
  • コンテナ型仮想化ではホストと明らかに異なるOSは使用できない
    →例えば同じLinux系でディストリビューション違い程度ならばLinuxの互換性によってコンテナで起動できることも多い。一方、Linuxの上でWindowsコンテナを実行したりすることはできない。従来の仮想化はOS自体をエミュレーションするため、Linuxの上でWinでも起動できたし、LinuxとWinの仮想環境が共存することもできた。
  • ホストOS型仮想化に比べて、オーバーヘッドが少ない(ハイパーバイザとの比較では微妙そう)
    →ホストOS型の仮想化とコンテナではホストOS経由分のオーバヘッドでコンテナが性能上有利である。ただしハイパーバイザ型の仮想化との比較ではなんとも言い難いと思う。(ハードウェアレベルで最適化したハイパーバイザ型仮想化はかなりオーバヘッドが少ないと予想している。)
  • ハイパーバイザ型仮想化と比べてコストが安くなる可能性がある
    →ハードウェアにほぼ直接導入管理するハイパーバイザ型仮想化は技術的に難しく、その分コストがかかる。(ただし現時点ではこれまで主流だったハイパーバイザ型仮想化に知見をもつエンジニアが市場に多い可能性はあり、実際のコストはこの通りとならないかもしれない)

従来の仮想サーバとの棲み分け

Dockerが向いているケース

  • アクセス数の増減が予測されるなど素早いスケーリングが必要なケース
  • 開発者の開発環境など、個人端末レベルの計算力で仮想環境を動作させるケース
  • PoCなど初期投資にかけられる費用が限られているケース

多分あまり向いてないケース

  • 仮想デスクトップ環境を払い出すようなケース(高速スタートアップが必要ないのでハイパーバイザ型の方が自由度が高い分向いていそう)

おそらくBtoCのC側に近いアプリケーションの開発実行には向いてるが、例えば企業内部で払い出す仮想デスクトップのような用途には向いてなさそう。一口に仮想化といっても従来とはかなり毛色の違う仮想化で全くの別物と考えたほうが良さそう。
実際のところ、以下のような棲み分けは一定合理的と思います。

  • Dockerのホスト自体はハイパーバイザ型仮想化(クラウド利用)
  • その上のアプリケーション実行環境はDockerによるコンテナを利用

Dockerの構成要素

イメージ

公式より抜粋「Docker コンテナーを作成する命令が入った読み込み専用のテンプレートです。
使うだけであればいわゆる仮想OSのイメージと近い理解でおおよそ問題が無いはず。仮想OSのイメージと大きく違いがある点としては、イメージは層状に構成されること(後述)。Dockerfileからビルドして作成する。

コンテナ

公式の説明がわかりやすく十分なので抜粋。
コンテナーとは、イメージが実行状態となったインスタンスのことです。
「コンテナーは、複数のネットワークへの接続、ストレージの追加を行うことができ、さらには現時点の状態にもとづいた新たなイメージを生成することもできます。」
「コンテナーを削除すると、その時点での状態に対して変更がかかっていたとしても、永続的なストレージに保存されていないものは消失します。」

DockerEngine

イメージの作成や、イメージからのコンテナの立ち上げ、実行を行う。
dockerを使用する際に普段我々がやり取りしているのはDockerEngineの一味の中のdocker CLI。docke CLIでは、コンテナを立ち上げるために必要なプロセス空間の隔離等々長〜い手順をラッピングしたコマンドを提供している。CLIはその受け取ったコマンドに必要な各種操作をAPIを通じてdocker deamonに伝え、実際の仮想化処理はdocker deamon(と、その背後にある更にローレベルなコンポーネント群)がAPIを受け取って実行している。docker deamonとその裏にあるコンポーネント群にk8s関連で話題になったcontainerdなどが含まれる。

DockerDesktop

デスクトップクライアント。この記事読む人はあまり興味ない気がするので割愛

Dockerレジストリ

いわゆるリモートリポジトリ。イメージのpush,pullができる。

Dockerイメージ

本章ではもう少しだけDockerイメージについて深堀する。

イメージレイヤとコンテナでのファイル書き込み

Dockerイメージは層状になっていると言ったが、DockerFileの1命令ごとに、直前のレイヤからの差分を記録する形で生成される。

FROM hoge # layer0生成
COPY fuga # layer1生成
RUN  piyo # layer2生成

dockerfileによって生成されるレイヤはすべて読み取り専用である
ただし、これだとコンテナ実行中にファイルの作成などが全くできなくなってしまうため、
コンテナには追加で書き込み可能なレイヤ(container layerと呼ばれたりする)が追加される。
この部分はコンテナごとに独立していて、各コンテナで異なったデータなどを持てるようにしている。このコンテナレイヤに行われた変更はコンテナ終了時に破棄されるようになっている。

書き込み方法は、copy on write方式になっており、ファイルの書き込み操作を実行する場合、書き込みレイヤに変更対象となるファイルがcopyされた上で変更が記録されることになる。
そのため、コンテナは大量のデータを書き込むアプリケーションに向いていない(書き込み可能レイヤーへのデータコピーが頻発し、リソース上問題が出る)。

前述のコンテナレイヤへの書き込みがコンテナ終了時に破棄されることと合わせ、
コンテナでの永続的ファイルwriteと、大量のファイルwriteはコンテナ外部のストレージを利用することが望ましい。
外部ストレージの利用については後述

Dockerイメージのサイズ

docker imageは読み込み量に関わるため、性能上できるだけ容量が小さいほうが望ましい。そのため、イメージサイズの削減のために以下のような戦略が取られることがある。

  1. baseイメージを最小限のものに
    debianではなくalpin linuxを利用するなど、baseイメージを軽量化する
  2. Dockerfileの命令をまとめる
    RUN hoge; RUN fuga;ではなく RUN hoge&fuga;のように命令をまとめる。上述の通りimage layerはDockerfileでの命令ごとに作成されるため余分な変更が削減される。ただしこれは常に大きな効果があるわけではないので、可読性保守性も考慮して命令のまとめ方は決める必要がある。
  3. マルチステージビルドの利用
    例えばあるアプリケーションをビルドし実行する場合、アプリケーションのビルドと実行を一つのイメージで実行しようとすると、ビルドするためのツール、中間生成物など余計なリソースがイメージに含まれてしまうことがある。そこで、マルチステージビルドを利用すると一つのDockerfileの中でビルド環境も仮想化しつつ、最終的に実行するコンテナのサイズを削減することができる。

マルチステージビルドは、中間イメージを利用するような感覚。

FROM build_base as [Stage_name] #ビルド用のイメージに適当なステージ名をつけて使う
RUN apt install [some_build_tools] # ビルド用ツールのインストール
RUN [build some_applications] # アプリケーションのビルド

FROM runtime_base # アプリケーション実行用イメージ。これにはビルドツールは含まない
COPY --from=[Stage_name] [some_applications] ./ ... # 前のステージからビルド済みアプリを入手

コンテナのreadonly化

container layerは、コンテナ内でのroot file system(ホストのrootとは異なる)への書き込みを実施することができる。そのためセキュリティ上はcontainer layerへの書き込みも禁止(readonly化)したほうが良い。
ただし、コンテナをreadonly化すると、アプリケーションなどが生成する一時的なファイルが生成できなかったり、ログの書き込みができなかったりするため、動作上問題となることが多い。
これらの回避のためには、例えば以下のような手段を取ることができる。

  • ログについて:直接外部送信し、コンテナ内部には保持しない
    dockerコンテナのログドライバオプションで、logstashやcloudwatch agentなどを利用してログを外部に送信する。送信先でのログ検索、分析などを考えると有効な場面は多いと思う。
  • ログ、一時ファイル両方について:別パーティションのボリュームをマウントする
    readonly化はroot file systemに対して適用されるため、別パーティションのボリュームを/tmpなどにマウントした場合その領域は書き込み可能領域として使用できる。もしくは、Dockerではインメモリでのファイル作成も可能なため、そちらを利用することもできる。

ストレージとネットワーク

これまで触れたとおりで、コンテナは永続データを扱えないため実際にアプリを可動する際にはコンテナ外のストレージに接続する必要があることが多い。また、コンテナをせっかく起動してもコンテナへのリクエストの送信、コンテナ間でのやりとりができないと意味がない。

ストレージ

ストレージ概要

imageのところでも触れた通り、dockerコンテナでは永続的なデータの書き込みができず、また永続的でなくても書き込み量が多い場合にはdockerコンテナでの書き込み処理は推奨されない。
これらの用途ではdockerからみて外部のストレージに接続する必要がある。外部ストレージには、

  • 別ホストで可動するSQLやファイルサーバに接続する場合
  • コンテナを実行しているホストのファイルシステムを利用する場合

があるが、Dockerでいうストレージは後者のコンテナを実行しているホストのファイルシステムを利用する場合を指す(ことが多いと思う)。 以下ホストといえばコンテナを実行しているホストを指す。Dockerでのストレージの接続形態には以下の3つがある。

  1. ボリューム(docker volume)
    ホストのファイルシステムの中のdocker指定の場所にデータを保存する。これはdockerによって管理されるため、docker以外のプロセスやアプリによって変更してはいけない。
  2. バインドマウント
    ホストのファイルシステムの任意の場所を結びつけて利用する。まさにマウントであるので、docker以外のプロセスから変更しても問題ない。
  3. ホストのメモリを利用(tmpfs)
    ホストのメモリを間借りする。メモリの間借りなので当然永続化には向いていない。

ボリューム

docker volume createで明示的に生成するか、コンテナの作成時に生成される。ボリュームはまさにボリュームであり、作成したボリュームをコンテナの任意のパスにマウントできる。
複数のコンテナに同時にマウントでき(コンテナ間でのデータ共有に使える)、また、そのボリュームをマウントしているコンテナが一つもなくてもボリュームは削除されない(永続的)。
dockerボリュームは操作用のvolumeドライバ(と、そのコマンド)があるため、それを利用してリモートホストへのデータコピーなどを実行することができる。

バインドマウント

ホストが持っているフォルダなどをそのままマウントする。マウントしたものは、ホストでのフルパスでアクセスする。こちらもコンテナ間での共有が可能などメリットはあるが、ホストのファイルシステムをそのままマウントするため、コンテナからホストへの直接変更ができる状態となり、セキュリティ上の懸念が発生する。

tmpfsマウント

ホストのメモリ領域をマウントする。コンテナ間での共有ができず、またコンテナ終了と同時に削除される。

ストレージの使い分け

基本的にはボリュームを利用するべきであるが、他の2つは以下のような場合に使用できる。

  • バインドマウント
    ホストの設定ファイル(/etc/hoge.conf)などをコンテナに共有する場合
    ホスト側からもデータを変更する場合(例えば開発用コンテナであれば、追加ライブラリなどをホスト側からアップロードしたいなどのケースはあると思う)
  • tmpfsマウント
    読み書きの回数が多いが、総容量はそこまででもなく、かつ保存の必要がないもの。
    アプリケーションキャッシュや一時ファイルなどに利用できる。

ネットワーク概要

ネットワークモードには以下がある。

  • bridge(デフォルト)
    同一ホスト内のコンテナが相互に通信可能。 ネットワーク機器のブリッジを利用しているようなイメージ。
  • host
    コンテナのホストのNICをそのまま利用する。 例えばhostモードのコンテナで80番を利用すると、コンテナホストの80番への通信がコンテナへとルーティングされる。
  • overlay
    複数のホスト間で動作する複数のコンテナ間の通信を可能とする。 swarmを利用する場合は必須となる。swarmの話になってしまうので割愛させてください(swarmよくわからない)
  • ipvlan
    仮想ネットワークインターフェースを作成する。 ホストのネットワークインターフェースに関連付けた上で、サブネットマスクやゲートウェイを指定して仮想ネットワークを作成し、そのネットワークに所属させる形でコンテナを実行する。後述するmacvlanとの違いは一つのMACアドレスに複数の仮想インターフェースが結びついた状態となること。よほど特殊な要件以外では多分使わない。
  • macvlan
    こちらも仮想ネットワークインターフェースを作成する。ipvlanとの違いは仮想インターフェースごとに別のmacアドレスを割り当てること。 ゲートウェイに伝達されるMACアドレス数が多くなるため、ゲートウェイの制約によっては利用できないこともあるらしいが、MACアドレスが分割されている分通常のネットワークインターフェースと状況が近く、ipvlanでは発生するトラブルを回避できる場合もあるらしい。これもよほど特殊な要件以外では多分使わない。
  • none
    ネットワークをオフにする。
  • ネットワークプラグイン
    サードパーティのネットワークプラグインを利用することも可能らしい。

bridgeモード

最も基本的なbridgeモードについてもう少しだけ記載する。

bridgeはbridge(で接続されたネットワーク)をまず定義し、そこにコンテナを参加させていくような接続方法を取る。bridgeには最初から作成されているデフォルトbridgeと、ユーザがあとから作成するユーザー定義bridgeの2つがある。以下の理由で、基本的にはユーザー定義bridgeを使用するべきである。

  • デフォルトbridgeでは--linkによって明示的に接続しないとbridge内でのDNS解決ができない。ユーザー定義bridgeではデフォルトでDNS解決が可能である。
  • ユーザー定義ネットワークであれば、コンテナーの動作中にアタッチ、デタッチが行える。
  • ユーザー定義の名の通り設定が柔軟である。

コンテナ間ではなく、コンテナとホストの間の通信はコンテナのポートをホストのポートで公開することができるため、localhostを利用しての通信が可能である。以下にbridgeモードでのコンテナのイメージ図を示す。

おわりに

ここまでの内容とあとはコマンドを調べることで、私的合格最低点のコンテナが作れると思っています。以下に今回の記事で触れなかった、発展的内容、個人的宿題事項をまとめて終わります。

  • コンテナイメージの脆弱性対策
    Docker Scanの利用、DevSecOpsの実現など
  • Dockerfileのより高度な最適化
    Buildkitでの並列性や、キャッシュを意識したDockerfileの構成方法等
  • Dockerイメージの管理
    イメージ間の依存をどう整理するか、どうタグ付けをしていくか
  • k8s,swarm,ECS,ACIなどのコンテナオーケストレーション
    需要や障害に合わせてどうコンテナを立ち上げるか?それらをどう通信させるか?
  • ログの収集、利用戦略
    どのようにログを集めるのか?
  • dockerの各CLIコマンドの詳細
    どのようなオプションが利用可能か?どんな機能があるか?
  • containerdなどのランタイムの詳細
    ランタイムは何をしているのか?containerd以外にもランタイムがあるがどのような差異があるのか?今後のトレンドは?

参考文献

Docker概要

・Docker: Docker概要
https://matsuand.github.io/docs.docker.jp.onthefly/get-started/overview/
・VmWare:基礎から学ぶ サーバ仮想化とは?
https://juku-jp.vmware.com/whatsvm/whatsvm02/
・Container Runtime Meetup(inductor):コンテナランタイムはじめの一歩
https://speakerdeck.com/inductor/container-runtime-101
・Qiita(kirikunix):なぜDockerではホストOSと違うOSベースのコンテナイメージが動くのか
https://qiita.com/kirikunix/items/33414240b4cacee362da
・CodeZine:最短で使うDocker入門~Dockerを体験しよう
https://codezine.jp/article/detail/12830

イメージ

Docker:イメージ、コンテナ、ストレージ・ドライバについて
https://docs.docker.jp/v17.06/engine/userguide/storagedriver/imagesandcontainers.html
Docker:マルチステージビルドの利用
http://docs.docker.jp/v19.03/engine/userguide/eng-image/multistage-build.html#
InfoQ(Hrishikesh Barua):Dockerコンテナを安全に運用するには
https://www.infoq.com/jp/news/2017/01/containers-secure-production/
コンテナイメージを軽くする方法と、その原理原則を考える
https://blog.mosuke.tech/entry/2020/07/09/container-image-size/

ストレージとネットワーク

Docker:Docker におけるデータ管理
https://matsuand.github.io/docs.docker.jp.onthefly/storage/
Docker:ネットワーク概要
https://matsuand.github.io/docs.docker.jp.onthefly/network/
[翻訳] 仮想ネットワークのための Linux network interface まとめ
https://markunet.github.io/blog/Intro_Linux_interfaces_JP/#macvlan
RedHat:第41章 IPVLAN の使用
https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/getting-started-with-ipvlan_configuring-and-managing-networking

Discussion