なぜ Docker を使うのか？ 小規模シス管向けの Docker のススメ

この記事は、 [Docker Advent Calendar 2020](https://qiita.com/advent-calendar/2020/docker) 24日目の記事のつもりで準備していました。

**ごめんなさい、大遅刻しました。**
せめて年が明けてしまう前に… ということで供養します。

どのカレンダーも、 24日だけ空白になっていることって多くない？ そういうことなの？

## 導入

さてみなさん、 Docker を使っていますか？

使っていない人は、あんまり Docker Advent Calendar なんて見に来ないと思いますが、それはさておき。

Docker とかコンテナ系の技術のことを、なんとなーくは知ってはいても、 **本格的に使うまでに至っていない** という人って、少なくない気がするんですよね。
私も最初はそんな感じでした。

今でこそ、 Docker なしの生活には戻れませんが、初めて Docker について知ったときのことを思い出すと、 『なぜ Docker を使うのか？』 がいまいちピンとこなくて、積極的に使おうとまでは思わなかったことをよく覚えています。

そんな中、古き良き (←良くない) 組織の中で、簡単なシステムを置き換えたり新しく建てる必要が出て来ました。
『Docker 使おうよ！』 なんて言ってはみたものの、『それのどこがいいの？』という当然の疑問に、簡潔に答えらている気がしません。

Docker の仕組みや導入方法については、もっと文才のある先人たち、がたくさん記事を残してくれています。
ただ、 Docker に積極的にではない人に『これを読んでみてね』というには、どれもなんかちょっと違う感じがしています。

そこで、自分の頭の中の整理がてら、『なぜ Docker が良いのか』を、 **なんとなくわかるレベルで、なるべく簡潔に** まとめてみようと思います。

あらかじめ断っておきますが、 開発者向けではなくて、 **システム運用者向け** の話が中心です。

あくまで、初めて触れる人に「Docker, いいかも？」と思ってもらえるようにしたい、というのが目的なので、以下のような話はあえて省いています。

* Windows, Linux 以外の利用についての話
* 具体的なコードやコマンド例
* Windows Server Core / Nano Server コンテナの話
* K8s とか OpenShift などでスケーリングするような、大規模なデプロイの話

編集リクエストも大歓迎です。

## 改めて Docker とは？

サーバーでとあるアプリケーションをホストして動かしたい！ ……となったとき、多くの場合は

* アプリケーション
* アプリケーションが依存する フレームワーク, ミドルウエア, ライブラリ
* OS
* 仮想/物理マシン

等々をセットアップする必要があります。

さらに、実用的なアプリケーションでは、アプリケーション本体に加えて、データベースや Web サーバーなどが別途必要になったりするでしょう。

どんな環境でも問題なく動く魔法のようなアプリは別として、少なくないアプリケーションではライブラリや OS との相性で、不具合が出てしまうことがしばしばあります。
こういった相性の問題は、 **『開発環境では動いていたのに、本番環境では動かない！』** とか、 **『単体では動いていたのに、○○のアプリと組み合わせると動かない』** といったようなことを引き起こしやすく、開発者や運用技術者たちの頭を悩ますことになるでしょう。
**セキュリティパッチを当てるために、 OS やライブラリを更新する場合なんかは特に。**

そこで、依存するライブラリや OS の設定などを、 システムリソースの消費を抑えながら、 アプリケーションの単位でまるっと仮想化して、できるだけどこでも同じように動かせるようにしようとするのが、 **コンテナ型仮想化** という技術です。
そしてそれを、 **簡単に作成・実行・管理・配布できる** ような環境を整えてくれるのが **Docker** というエコシステムです。

### VM との違い

『仮想化』というと、まず思いつくのが VM, つまり仮想マシンですよね。
OS ごとまるっと仮想化してしまう VM では、 一つ VM を起動するために、 サーバー一台分の大量のリソース (メモリやストレージ等) を消費してしまいますし、起動にも時間がかかります。

一方のコンテナ型では、 OS を、中核となる「カーネル」と、それ以外の「ユーザーランド」に分けて考えて、その「カーネル」側の部分をホストやコンテナ同士で共有しています。
そうすることによって、**消費するリソースを劇的に抑えたり、一瞬で起動できる**ようにしています。

それでも OS の主要な部分は仮想化されているため、 例えばファイルシステムや、ネットワーク・ポートなども、 コンテナとベースOS間や、コンテナ間で分離されています。

> ![docker-birthday-3-intro-to-docker-slides-18-1024.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308278/6ce1b58d-8961-ec2c-38e9-a6cc3cd82171.jpeg)
> Source: <https://www.slideshare.net/Docker/docker-birthday-3-intro-to-docker-slides#18>

一方で、コンテナ型仮想化では、コンテナとホスト OS は、同じカーネルである必要がある、という制限があります。
例えば、 Ubuntu と CentOS は同じ Linux カーネルを使っているので混在できますが、 同じ Linux でもカーネルのバージョンが異なる場合や、 そもそもカーネルの種類が異なる Windows と Ubuntu をまたぐような組み合わせでは、コンテナを直接動かせません。

…まぁ実際には、 Windows OS 上で Linux のコンテナは扱えるのですが…、それについては後述します。

とりあえず、 **コンテナ型仮想化である Docker は、リソースの消費量が少なく、1台の物理サーバーに大量のコンテナを稼働させることができる** とう特徴を知っておいてください。

## Docker と イメージ と Docker Hub

Docker が素晴らしいのは、コンテナ技術を使っているからだけではありません。
Docker Hub との統合され、コンテナ用のイメージを非常に簡単に管理できる点の恩恵がとても大きいです。

………

と、ここで「Docker のイメージ」とは何なのかについて軽く説明をば。

Docker でいう「イメージ (image)」とは、「コンテナの初期状態のスナップショット」と考えることができます。

コンテナを起動する前に、Docker Hub から公式や第三者によってビルド済み「イメージ」をプル（ダウンロード）したり、 `Dockerfile` を記述して自分でビルドすることで、イメージファイルを構成します。
そして、そのイメージを使ってコンテナを起動すると、そのイメージ自体は読み取り専用のまま、コンテナには「イメージ」との差分が記録されていきます。

この「イメージ」と「コンテナ」の関係性は、オブジェクト指向型のプログラミング言語で言うような、クラスとインスタンスの関係に似ているでしょうか。 [^1]
一つのクラス（イメージ）から複数のインスタンス（コンテナ）を作ることができたり、クラス（イメージ）が変更されるとインスタンス（コンテナ）は流用できずに作り直す必要があったり…

[^1]: また、「イメージ」同士にも継承関係というか、親子関係が存在していて、小さな「イメージレイヤー」が積み重なることで一つの「イメージ」という概念を構成しています。例えば、公式の[Rubyのイメージ](https://hub.docker.com/_/ruby)と、[Pythonのイメージ](https://hub.docker.com/_/python)は、どちらも[buildpack-depsイメージ](https://hub.docker.com/_/buildpack-deps)を元にしており、Ruby,Pythonそれぞれのイメージは、buildpack-depsイメージとの差分の情報だけを持っています。それの何が嬉しいかと言うと、RubyとPythonの両方のイメージをプルした時、OSのユーザーランドのファイルや、curl,git,gcc等のパッケージは共通になっているので、DockerHubからのダウンロード量や、ストレージの容量効率が高くなります。

イメージ同士や、イメージとコンテナの関係については、 [Dockerイメージの理解を目指すチュートリアル - Qiita](https://qiita.com/zembutsu/items/24558f9d0d254e33088f) あたりの記事に細かくまとめられているので参照ください。

……

また、 Docker イメージは、 `Dockerfile` というファイルにソースコードとして記述してビルドします。
つまり、以下のような関係性と言えるのではないでしょうか。

|オブジェクト指向＆コンパイル型言語|Docker|
|---|---|
|ソースコード|`Dockerfile`|
|↓コンパイル↓|↓BUILD↓|
|クラスを含んだバイナリ|イメージ|
|↓実行↓|↓RUN↓|
|インスタンス|コンテナ|

**Java や .NET でいう Maven や NuGet といったパッケージマネージャーに当たるものが Docker Hub です。**
Docker Hub を使えば、自分でイメージをビルドしなくても、ビルド済みのイメージを入手できます。

Docker Hub と `docker` コマンドが、上記のようなイメージやコンテナをいい感じに管理してくれるため、利用者はあまり細かいことを考えなくても、最低限のリソースでコンテナを起動させることができるようになっています。

……

さて、そんな便利な Docker イメージ と Docker Hub ですが、 Docker Hub には Docker のコミュニティが管理しているイメージだけでなく、 非公式のイメージもたくさん公開されています。
（Docker に限った話ではありませんが）非公式のビルド済みイメージには、悪意のあるユーザーによって不適切なコードが含まれている可能性が否定できません。  [^2]

[^2]: 悪意のコードが混入しているか確認しやすくする仕組みも用意されてはいますが。

とりあえずまずは、多くの人が使っていて信頼でき、セキュリティ更新プログラムがタイムリーに適用される、公式イメージ ([Docker Official Images](https://docs.docker.com/docker-hub/official_images/)) と、それを基に自分で書いた Dockerfile でビルドしたイメージを使ってみるところから始めましょう。 


## データのマウントと永続化

コンテナ内で作成されたファイルは、コンテナ内に保存されます。

しかし、 Docker のエコシステムの基本的な考え方として、 **コンテナ内に保存されたデータはいつ何時消えてもよいようにしておく** と考えるのが一般的です。

なぜなら、 Docker のコンテナはコマンドでかなり簡単に削除できるようになっている上、 将来ベースイメージを変更する必要が出てきたときに、古いコンテナ上にあるデータを新しいコンテナに移動させることが難しくなっているからです。

このため、 DB のデータなどの失われては困るデータは、 あらかじめビルド時点でイメージに埋め込んでおいたり、 bind mount や volume という仕組みを使ってデータの一部をコンテナ外に保存する（コンテナ内にマウントする）ことが一般です。

> ![types-of-mounts.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308278/b7c83f95-666b-5458-0430-96b39d0239e4.png)
> Source: <https://docs.docker.com/storage/>

コンテナ起動後は、イメージ自体の中身やマウントの場所を変更できません。
つまり、 **コンテナを作る段階で、どのデータが永続化され、どこが消えてもよいのかといった設計を、嫌でも考える必要がある**ということです。

**それだけ聞くと、不便極まりない仕組みですよね。**

しかし、将来的にサービスを運用し続けるために、プログラムやライブラリを更新していくことを考えると、**この制限はメリット**です。

まず、 利用するプログラムの設定ファイル (conf ファイルや、プログラムのプラグイン・パッチなど) は、コンテナを起動したまま書き換える必要はないため、**あらかじめそれらを埋め込んだイメージをビルド**させます。
それにより、**開発サーバーから本番サーバーに移行したり、本番サーバーを作り直した場合などに、設定忘れなどの発生を防げます。**

また、イメージに埋め込むファイルを Dockerfile に記述することになるため、それがどのファイルを意図的に変更しているかの記録になります。
それによって、**後任者や、物覚えの悪い将来の自分でも、何を意図して設定ファイルを変えたのかが記録として残りやすくなるといえる**でしょう。
結果的に、運用中にいろんな人が設定を書き換えていて、どの設定が何の目的なのかよくわからなくなるみたいなことに陥りにくくなります。

一方で、コンテナ起動後に書き換わって、かつ永続化が必要なデータは、**マウントされた bind mount や volume に保存します。**
それにより、マウントされたフォルダを直接別コンテナでマウントしてみたり、コピーした上で別コンテナでマウントすることで、同じデータを持った別のコンテナを簡単に作ることができます。
これは、コンテナ起動するときのイメージのバージョンを変更するだけで、アプリケーションのバージョンアップの環境を簡単に構築することができ、**検証が劇的に簡単になります。**
さらに、本番環境のバージョンアップ実施のタイミングにおいても、 あらかじめバージョンアップ済みのイメージをビルドしておき、 いったん古いコンテナを停止(削除)して、すぐに新しいイメージでコンテナを起動させることで、**ダウンタイムを短く更新することも可能です。**

**ほら、億劫になりがちな、アップデートとその検証が、簡単になる気がしてきませんか？**

## Docker Compose

Docker の特徴を最大限に生かすため、Docker でコンテナを動かす場合は、**1コンテナ1プロセスとする**ことが一般的です。
(ちなみに、必須ではない)

冒頭で挙げたような、アプリケーション、データベース、Web サーバーといった組み合わせで動作させる場合、それぞれ別々のコンテナで動かすことになるでしょう。

そうすると面倒になってくるのがイメージやコンテナの管理。

Docker の CLI だけだと、コンテナの数が増えていくにつれて、コンテナ同士の関係性の設定や、コンテナの起動終了などの管理が大変になってきます。

そこで活躍するのが、複数のコンテナの管理を簡単にしてくれる **Docker Compose** です。

Compose では、複数のコンテナ間の依存関係や、各コンテナのネットワーク・マウント島の設定、 イメージをビルドするのか Docker Hub のイメージを使うのか… といった設定を、以下のような一つの YAML ファイルにまとめて記述します。

```yaml
version: '3'
services:
  web:
    build: .
    ports:
    - "5000:5000"
    volumes:
    - .:/code
    - logvolume01:/var/log
    links:
    - redis
  redis:
    image: redis
volumes:
  logvolume01: {}
```

ファイルの書き方については細かく述べませんが、上記のようなファイルを用意して `docker-compose up` などと**コマンドをひとつ叩くだけ**で、簡単にコンテナを管理できるようになります。

より大規模に、複数のマシンで Dokcer を運用するような場合は、 Kubernetes や OpenShift といったソリューションが必要になってきますが、 とりあえず**サーバー一台でいくつものコンテナを動かすだけ**であれば、 この Docker Compose は外せません。

ぜひ、併せて覚えてみてください。

## まとめ

このように、 **コンテナによる高い隔離性 (isolation) という特徴** によって、環境の入れ替えに非常に強い設計となることが **半ば強制** されます。

サーバーを立てた人が、詳細なドキュメントを残していなくても、コードを見ればどのように構築されているかがわかって、手元でも簡単に再現できるので、 小さい組織でありがちな、**「作った人がもういなくて誰も触れない」ようなことを減らせます。**

加えて、ビルド・デプロイといった部分が「仕組み化」されることで、少ない労力で自動化させることもできるでしょう。
(まぁ、冒頭に挙げているような環境下では、そこまで踏み込むことはなかなかないかもしれませんが……)

ぜひ `Dockerfile` と `Docker Compose` の書き方を覚えて、コードで全てが管理される世界を作りましょう！！


## おまけ

### Docker と IaC

Docker を利用した利点は、 Ansible や Chef などをはじめとした IaC (Infrastructure as Code) と通ずるところがあり、実際、それらと組み合わせて導入すると、相乗効果が高まります。

具体的には、 Docker のインストールや、ファイアウォールの設定、マウント用のフォルダの作成、そしてイメージの取得やコンテナの起動などといった Docker, Docker Compose コマンドの実行を Ansible によって行わせることで、サーバーの立ち上げから更新まで全てコードで管理できます。


### Windows で Docker

> そもそもカーネルの種類が異なる Windows と Ubuntu をまたぐような組み合わせでは、コンテナを直接動かせません。

と書いていましたが、実際のところ Windows 上で Linux 系の OS のコンテナを動かせます。

これは、 Windows 10 の WSL2 という仕組みで、 超軽量な Linux カーネルの仮想マシンが動いているためです。 [^3]

[^3]: Windows 10 1909 以上が必要なので、 Enterprise エディションや LTSC/LTSB で古いバージョンを使っている場合は注意。

この WSL2 上の Linux に、手動で Docker をインストールもできますが、実用上は Docker Desktop for Windows というツールを Windows 側にインストールして、 Windows から透過的に Docker を使えるようにすると便利です。

導入手順は、 Advent Calendar 13日目の [貧弱なWindows PCでもDockerを動かしたい！（WSL2）](https://qiita.com/y9k8i/items/ed6804de727aa332e8ab) や、 21日目 の [Windows 10 HomeへのDocker Desktopインストールが何事もなく簡単にできるようになっていた (2020.12時点)](https://qiita.com/zaki-lknr/items/db99909ba1eb27803456) で詳しく書かれているので、そちらを参照ください。

## 参考

* [docker初心者の方が知っておいた方がよい基礎知識](https://www.osscons.jp/cloud/%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89/?action=common_download_main&upload_id=698)
* [Docker ドキュメント | Dockerドキュメント日本語 暫定公開リポジトリ](https://matsuand.github.io/docs.docker.jp.onthefly/) (最新版に準拠)
* [Docker ドキュメント日本語化プロジェクト](http://docs.docker.jp/) (2020年現在、内容がちょっと古い)