Dockerとは
コンテナベースのアプリケーションを仮想化したもの。軽量なVMの様に見えるがこれまでの(VirtualBoxなど)VMでは実現が難しい、不可能であったユースケースを解決してくれる。
- ホストOSとリソースを共有するのでリソースの管理がVMより効率的
- 基本的に状態を持たないのでポータビリティが非常に高く、特定の環境に依存することがない
- 軽量なのでVMと比較し複数のインスタンスを実行することができる
- DockerHubなどのレジストリを利用することで既存のイメージをダウンロードして実行することができる
コンテナとVM
VM
- VMはハイパーバイザを通してホストOSに対してのシステムコールを解釈させるなどの必要がある
- それぞれのVMには全て独立したOS・アプリケーション・ライブラリが必要
コンテナ
- ホストのカーネルは実行されるコンテナと共有される(コンテナは常にホストと同じカーネルを使う必要がある)
- この例ではアプリケーションYとZはライブラリBを共有している
- コンテナ内で実行されるプロセスはホストで実行されるプロセスと同等で、ハイパーバイザの実行に伴うオーバーヘッドが存在しない
Dockerの実行環境について
Dockerは純粋な仮想化は行わないため64bit Linuxでしか基本的に動作しないが、Docker for MacやDocker for Windowsなど透過的に使えるソフトウェアが開発されている。
内部ではVMを経由してDockerを実行しているためLinuxでDockerを直接利用するよりパフォーマンスの面で劣る。
https://www.docker.com/products/docker
Dockerの実行
インストール後に
$ docker run hello-world
でDockerについての情報を表示するイメージがダウンロードされ、イメージを元にしたコンテナが起動・実行される。
次に実用的な例として、
console
$ docker run -it ubuntu /bin/bash
でubuntuのbashが立ち上がる。runのあとにコマンドを記述することでコンテナの中でそのコマンドが実行される。コンテナ内のカレントディレクトリについては Dockerfile
の WORKDIR
で定義されたものになる(後述)。
-it
はtty付きのインタラクティブセッションを要求するオプションで、対話的なコマンドを実行したいときは付ける必要がある。
Ubuntuのイメージでbashを起動した状態で別のコンソールから docker ps
コマンドを実行することでコンテナの状態を確認することができる。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
50ffa6334a98 ubuntu "/bin/bash" 2 seconds ago Up 1 seconds reverent_bose
この場合reverent_boseが自動的に振られたコンテナの名前である。(--nameオプションで明示的に指定もできる。衝突した場合は起動できない)
次にコンテナ内でファイルを作成し
root@7d31a9f7251c:/# touch /tmp/hoge
外部からコンテナの状態を確認してみる。
docker diff
を実行すると
$ docker diff reverent_bose
C /tmp
A /tmp/hoge
と表示される。これはDockerがファイルシステムにUFS(union file system)を使用して複数のファイルシステムを階層的にマウントしており、ファイルシステム上の差分について管理されるようになっているからである。
更にこの状態で docker commit
コマンドを実行する。
$ docker commit reverent_bose test/create_file
sha256:e6816e42b5d88aa4fb03cfe5e20d56502134ae0e76cf0cb71239dbc0f6b56a16
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test/create_file latest e6816e42b5d8 4 seconds ago 127.1 MB
ubuntu latest c73a085dc378 3 hours ago 127.1 MB
docker images
を確認すると test/create_file
という新たなイメージが作成されていることがわかる。
この test/create_file
からコンテナを起動すると、ubuntuイメージから起動したコンテナに加えて /tmp/test
ファイルが存在する状態になっている。
Dockerfile
でのイメージの作成
上記のような手順でイメージを作成するのはDockerを使う上ではバッドノウハウとされている。
人が徐々に手を加えて作成されたイメージは再現性が低くなってしまい、イメージに変更を加えるのが難しくなってしまうためである。
そのため、Dockerのイメージは一般的に Dockerfile
を用いて作成される。
Dockerfile
の細かい構文については以下のドキュメントに書かれている。(https://docs.docker.com/engine/reference/builder/ )
独自DSLだが基本的にはシェルスクリプトの形式で記述する。
次に、上記で作ったイメージを作成する Dockerfile
を記述する。
適当なディレクトリを作成し、 Dockerfile
の作成
FROM ubuntu:latest
RUN touch /tmp/hoge
その後次のコマンドを実行する。
$ docker build -t test/create_file .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM ubuntu:latest
---> c73a085dc378
Step 2 : RUN touch /tmp/hoge
---> Running in ce59c6b29dae
---> 4275ce1b43b8
Removing intermediate container ce59c6b29dae
Successfully built 4275ce1b43b8
すると先ほど docker commit
で作成したものと同じイメージが作成される。
次は、 cowsay
を実行するためのイメージを別途作成する。新しい Dockerfile
を作成し以下のように記述する。
FROM ubuntu:latest
RUN apt-get update && apt-get install -y cowsay fortune
$ docker build -t test/docker-cowsay .
cowsay
を実行する。
$ docker run test/docker-cowsay /usr/games/cowsay hoge
______
< hoge >
------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
これで cowsay
が入ったイメージを実行できるが、更に簡単に cowsay
を実行させる方法がある。
Dockerfile
で ENTRYPOINT
という構文を使う。
FROM ubuntu:latest
RUN apt-get update && apt-get install -y cowsay fortune
ENTRYPOINT ["/usr/games/cowsay"]
Dockerfile
を変更した場合はイメージを再度作成する必要があるため docker build
を実行する。
$ docker build -t test/docker-cowsay .
$ docker run test/docker-cowsay hoge
______
< hoge >
------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
これで cowsay
コマンドを簡易に実行するイメージの作成ができた。
ただし、このままでは他のコマンドを実行したい場合などに不便である。
そのためDockerではよくあるパターンとして entrypoint.sh
などといったシェルスクリプトを ENTRYPOINT
に指定し、その中で実行する処理を分岐させることが多い。
以下が entrypoint.sh
の例である。
#!/bin/bash
if [ $# -eq 0 ]; then
/usr/games/fortune | /usr/games/cowsay
else
/usr/games/cowsay "$@"
fi
$ chmod +x entrypoint.sh
FROM ubuntu:latest
RUN apt-get update && apt-get install -y cowsay fortune
ADD entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]
$ docker build -t test/docker-cowsay .
Dockerのイメージを作成する際の注意点として、DockerはDockerfileの行ごとに状態の差分を保存しているためDockerfileに変更を加えた場合、その下に書かれている処理は全て再度実行される。
例えば最初の処理でapt-getコマンドを実行していた場合、それより下の行を変更して docker build
を行った際、apt-getコマンドが実行されるのは1回だけである。しかし、apt-getコマンドより上にADDなどが記述されており、対象ファイルが変更されていた場合はその後の処理が全て再度実行されてしまう。
上記で作成したイメージだが、このイメージからコンテナを起動すると
$ docker run test/docker-cowsay hoge
______
< hoge >
------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
$ docker run test/docker-cowsay
_______________________________________
/ "Life, loathe it or ignore it, you \
| can't like it." |
| |
| -- Marvin, "Hitchhiker's Guide to the |
\ Galaxy" /
---------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
と処理が分岐される。
公式イメージの利用
Dockerには公式イメージが提供されており(記事内で使っていたubuntuも公式イメージの1つ)、Dockerを実行する環境だけあればアプリケーションをホストマシンにインストールすることなく実行することができる。
今回はRedisのイメージを利用する。
$ docker run --name myredis -d redis
Unable to find image 'redis:latest' locally
latest: Pulling from library/redis
6a5a5368e0c2: Pull complete
2f1103ce5ca9: Pull complete
086a40c85e01: Pull complete
9a5e9d112ec4: Pull complete
dadc4b601bb4: Pull complete
b8066982e7a1: Pull complete
2bcdfa1b63bf: Pull complete
Digest: sha256:38e873a0db859d0aa8ab6bae7bcb03c1bb65d2ad120346a09613084b49185912
Status: Downloaded newer image for redis:latest
8d41189f1fab405166e4757fe09b697b09ba7654efd66a13626d395472cc4a61
-dオプションを付けてバックグラウンドでRedisコンテナを起動させる。
これに対して接続するクライアントとして、Redisイメージから別のコンテナを立ち上げる
$ docker run --rm -it --link myredis:redis redis /bin/bash
root@ef3efabd8d3f:/data# redis-cli -h redis -p 6379
redis:6379> PING
PONG
redis:6379> set "abc" 123
OK
redis:6379> get "abc"
"123"
redis:6379> exit
root@ef3efabd8d3f:/data# exit
exit
これで実際にRedisに接続し、実際にデータを保存することができる。
コンテナ同士が接続についてだが、Dockerは –link myredis:redis
というオプションでmyredisと名前を付けたコンテナに接続できるようになっており myredis:redis
という指定はmyredisコンテナにredisというホスト名でネットワークが解決できるようになるという指定をしている。(linkについて詳しくは後述)
データの永続化
Dockerは基本的にコンテナが終了したらデータが削除されるため、データの永続化はボリュームを使う必要がある。記述方法としてはDockerfileの中でVOLUMEを使うか、docker run に -v オプションを渡すの2通りがある。
VOLUME構文ではセキュリティ上の問題からDockerをインストールしたディレクトリにしかマウントできず、 -v はホストのディレクトリを指定しない場合Dockerをインストールしたディレクトリからマウントされ、ホストのディレクトリを指定した場合はホストの任意のディレクトリを指定することができる。( -v ホストディレクトリ:コンテナ内のディレクトリ)
$ docker run --name myredis -v /data -d redis
$ docker run --rm -it --link myredis:redis redis /bin/bash
root@ef3efabd8d3f:/data# redis-cli -h redis -p 6379
redis:6379> PING
PONG
redis:6379> set "abc" 123
OK
redis:6379> get "abc"
"123"
redis:6379> exit
root@ef3efabd8d3f:/data# redis-cli -h redis -p 6379 save
OK
root@ef3efabd8d3f:/data# exit
exit
$ docker run --rm --volumes-from myredis -v $(pwd)/backup:/backup ubuntu cp /data/dump.rdb /backup/
$ ls backup/
dump.rdb
これでコンテナの中からRedisのバックアップが取れているのが確認できる。
ネットワーク
Dockerのコンテナ同士を繋ぐネットワークには非推奨となっている–linkオプションを使うものと、現在推奨されている独自のネットワークを作る方式の2つがある。
link
--net
オプションを指定せずデフォルトのネットワークを使った場合、 -–link
オプションを使ってコンテナ同士を指定して繋ぐことができる。
前述の公式イメージの利用でRedisの公式イメージを使いサーバとクライアントを接続するのに使ったもので、クライアント側から --link myredis:redis
と書いているのがlinkの指定である。
具体的な処理としては -–link
オプションを指定したコンテナの中の /etc/hosts
にmyredisコンテナのIPが記述される。
このために、linkを指定する場合は事前にlinkされる側のコンテナを立ち上げておく必要がある。
Network
Dockerでは –-net
で使うネットワークを指定でき、デフォルトではbridgeという名前の仮想ネットワークを利用する。これらのネットワークは docker network ls で確認できる。
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a3d6eab2c163 bridge bridge local
989fd66a14a8 host host local
1a1dceb67c16 none null local
デフォルトではこの3つが存在しており、 -–net=host
を指定することでDockerホストのネットワークを利用することも可能である。
ネットワークは docker network create
コマンドで作成することができ、作成したネットワークを利用することでそのネットワークの中では「コンテナ名」もしくは「コンテナ名.ネットワーク名」の形でホスト名を解決することができる。
これは現在のDocker1.12ではDocker内部で独自に設定されたDNSで解決される。
$ docker network create testnet01
960970f91402b22d8f70976221aef6a70b55c46727a1272e4b44106ef394163d
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a3d6eab2c163 bridge bridge local
989fd66a14a8 host host local
1a1dceb67c16 none null local
960970f91402 testnet01 bridge local
$ docker run --net=testnet01 --name myredis -d redis
f3c088c27494c1144c74eff3ec6f525e497049d2cf66a86addb0e9fa4e11ff69
$ docker run --rm -it --net=testnet01 redis /bin/bash
root@5662a7cea39d:/data# redis-cli -h myredis -p 6379
myredis:6379> PING
PONG
myredis.testnet01:6379> exit
root@5662a7cea39d:/data# redis-cli -h myredis.testnet01 -p 6379
myredis.testnet01:6379> PING
PONG
先ほどはlinkを使ってコンテナ同士を繋いでいたが今回はコンテナ名を指定するだけで接続ができている。
以前は –-link
を使ってコンテナ同士のネットワークを構築するのメジャーであったが、今後はこちらのnetworkを作成する方法が推奨されている。
Docker Compose
Docker Composeは複数のコンテナを使うアプリケーションを定義・実行するためのツールである。
Docker Composeはアプリケーションのサービスの設定にComposeファイルと呼ばれるyaml形式のファイルを使う。そして、コマンドを実行した際にComposeファイルで設定されたサービスの作成・起動を行う。
Composeは開発環境・テストは当然、CI環境にも適していている(後述)。
docker-compose.yml
は次のように記述する。
version: '2'
services:
web:
build: .
depends_on:
- redis
ports:
- "8080:80"
redis:
image: redis
この例ではカレントディレクトリのDockerfileからイメージのビルドとコンテナの起動、ホストの8080番ポートをコンテナの80番ポートにフォワーディング、そのコンテナに連携するRedisコンテナを起動といった処理を行う。
また、docker-compose v2では起動時に新たなネットワークを自動的に作成するのでComposeファイルに定義したサービス名でそれぞれのコンテナにアクセスできる。
depends_onで依存するサービスを定義でき、この場合はRedisが起動してからwebで指定されたコンテナが立ち上がる。
参考にwebサービスにApacheをインストールしたコンテナを作成して実行する。
FROM centos:6.7
RUN yum -y update && yum -y install wget && yum clean all
RUN yum -y install httpd && yum clean all
EXPOSE 80
ENTRYPOINT ["/usr/sbin/httpd"]
CMD ["-D", "FOREGROUND"]
$ docker-compose up
upコマンドで定義しているコンテナを同時に起動することができる。イメージが存在しない場合は image の場合はネットワークから自動的にダウンロードし、 build の場合は指定した Dockerfile
を用いてビルドが実行される。
http://localhost:8080/
でApacheのトップ画面にアクセスができる。また、 docker network ls
で新たなネットワークができているのが確認できる。
そのネットワークを指定してコンテナを立ち上げることでRedisに接続できるのが確認できる。
$ docker run --rm -it --net=hoge_default redis /bin/bash
root@f29e0596cb01:/data# redis-cli -h redis
redis:6379>PING
PONG
立ち上げたDocker Composeは docker-compose down
で終了する。この際に自動的に作られたネットワークも削除される。
また、docker-compose run
コマンドを使うことで特定のサービスとそれに依存するコンテナを起動し、特定のサービスのexit codeを受け取ることができる。
これはテストなどに用いると便利である。
テストの際は上記、もしくは
$ docker-compose up -d
$ ./run_tests
$ docker-compose stop
$ docker-compose rm -f
などとして実行するのが推奨されている。
マルチステージビルド
Dockerの17.05 (ce)からmulti-stage buildという機能が追加された。
マルチステージビルドを用いることでDockerfileで複数のイメージの作成を定義・命名し、イメージ間でファイルのコピーなどができるようになる。
ビルド環境とランタイム環境でイメージのサイズが大きく変わるような場合には特に有用で、具体的にはgolangのベースイメージとalpine linuxベースのイメージではイメージのサイズの差が大きいので実行環境でのイメージの容量削減に繋がる。
DockerHubで公開されているgolangの公式イメージはtag 1.9で286MBである。一方alpine linuxのイメージのサイズは2MBであるため、golangのイメージでコンパイルしたソフトウェアのバイナリのみをalpine liuxにコピーすることで数百MB単位でのイメージのサイズの削減が期待できる。
Dockerfileの例としては以下のようになる
FROM golang:latest AS build
ADD . /go/src/github.com/yuki-ycino/go_sample
WORKDIR /go/src/github.com/yuki-ycino/go_sample
RUN go build -o app
FROM alpine AS execute
COPY --from=build /go/src/github.com/yuki-ycino/go_sample/app /app
ENTRYPOINT ["/app"]
複数の FROM
をDockerfileに記述し、ASを用いてDockerfile内で扱う名前を定義する。
その後、 COPY --from=image
と書くことで複数のイメージ間でのファイルのコピーが可能となる。
Dockerを使ったCIについて
Jenkinsのコンテナを使ってDockerコンテナのCIを回すことが多い(弊社でも利用しており、オライリー本にも載っている)ので紹介する。
プロジェクトに変更がpushされたタイミングでJenkinsがリポジトリのチェックアウト・イメージのビルド・テストの実行を行うよう設定する。
Jenkinsコンテナ内でDockerを実行するにはJenkinsコンテナにDockerホストのソケットをマウントする方法と、Docker in Dockerの2つがあるが、今回は前者について説明する。
ホストマシンのDockerのソケットファイルをDockerコンテナとして起動しているJenkinsコンテナにマウントすることで、JenkinsはホストマシンのDockerを使うことができるようになる。
その状態でJenkins上からホストマシンのDockerを用いてテストを行う。
まず、JenkinsのイメージにDockerをインストールする。
その後Jenkinsのコンテナを起動し、Docker Composeでソケットのマウントやデータの永続化を行う。
FROM jenkinsci/jenkins:latest
USER root
RUN echo "Asia/Tokyo" > /etc/timezone && dpkg-reconfigure -f noninteractive tzdata
RUN apt-get update && apt-get install -y locales
RUN echo ja_JP.UTF-8 UTF-8 >> /etc/locale.gen && locale-gen && update-locale LANG=ja_JP.UTF-8 LANGUAGE="ja_JP:ja"
ENV LANG ja_JP.UTF-8
RUN curl -sSL https://get.docker.com | sh
USER jenkins
ADD plugins.txt /usr/share/jenkins/plugins.txt
RUN /usr/local/bin/plugins.sh /usr/share/jenkins/plugins.txt
USER root
また、plugins.txt に必要なプラグインを定義しておいて plugins.sh を実行することで必要なプラグインを事前にインストールすることができる。
version: '3'
services:
jenkins:
restart: always
build: ./jenkins
ports:
- 8080:8080
- 50000:50000
volumes:
- ./docker_mount/jenkins:/var/jenkins_home/
- /var/run/docker.sock:/var/run/docker.sock
これで docker-compose up
を実行することで、8080番ポートでJenkinsが立ち上がり、CIが可能となる。
Jenkinsのイメージに対してDockerをインストールし、docker-compose.ymlで /var/run/docker.sock
をコンテナにマウントするよう定義することでコンテナ内でdockerコマンドを実行した際にホストのDockerが実行されるようになっている。
これでDocker化されたポータビリティの高いCI環境を構築することができる。
イメージのプッシュ
DockerHub
DockerHubは公式のレジストリサービスであり、Dockerは一般的にDockerHubを使ってイメージの公開を行う。
DockerHubでアカウントを取得すればdocker loginコマンドでサインインでき、その状態で docker push ユーザ名:イメージ名 コマンドを実行することでイメージを公開することができる。
DockerHubでリポジトリの設定でプライベートにする・GitHubが更新されたタイミングで自動的にDockerHubでイメージをビルドするなどといったこともできる。
DockerHub以外にも似たようなサービスにquery.ioなどいったものが存在する。
レジストリの構築
Dockerのレジストリはregistryイメージを使うことで自分で構築することができる。基本的にAPIはDockerHubと互換性があるものになっている。
デフォルトではGUIがなく、HTTPS化しないとクライアント側からinsecure registriesに登録しないと操作できない。
GUIについては konradkleine/docker-registry-frontend:v2 などいくつか個人が開発した別のイメージで提供されているものが存在する。
Packer
PackerはDockerとは直接の関係はないが、Vagrantを開発しているHashiCorpが開発しているマシンイメージに対する問題を解決するためのアプリケーションである。
VirtualBox・VMWare・Amazon EC2・DigitalOceanなどのマシンのイメージが必要な環境に対してJSONで設定を記述することで、それぞれのマシンイメージを元となるイメージからプロビジョニングを実行できる。
Packerの対象の1つにDockerがあり、既存のAnsibleなどプロビジョニングソフトウェアを用いてDockerイメージを作成することができる。
ただし、Dockerの利点であるUFSによるキャッシュが効かず、Ansibleでのプロビジョニングは基本的にDockerに対して最適化などをしていないためにイメージのサイズが肥大化してしまう。
試した結果相性はあまりよくないという結果となった。
また、Dockerは基本的に1コンテナ1プロセスで運用し、コンテナ同士がオーケストレーションをしてサービスを構築するという思想のため、Ansibleなどが必要となるほどプロビジョニングが複雑化することはあまりないと考えられる。