Dockerについて基本から最近追加された機能までまとめ

  • 1248
    いいね
  • 0
    コメント

Dockerとは

コンテナベースのアプリケーションを仮想化したもの。一見すると軽量なVMの様に見えるがVMでは実現が難しい・不可能なユースケースを解決してくれる。

  • ホストOSとリソースを共有するのでVMに比べて遙かに効率的
  • ポータビリティが非常に高いので特定の環境に依存することがない
  • 軽量なので1つのマシンで複数のコンテナを実行できる
  • 構築に時間などをかけることがなく、ダウンロードするだけで実行することが可能

コンテナとVM

VM

  • VMはハイパーバイザを通してホストOSに対してのシステムコールを解釈させるなどの必要がある
  • それぞれのVMには全て独立したOS・アプリケーション・ライブラリが必要

コンテナ

  • ホストのカーネルは実行されるコンテナと共有される(コンテナは常にホストと同じカーネルを使う必要がある)
  • この例ではアプリケーションYとZはライブラリBを共有している
  • コンテナ内で実行されるプロセスはホストで実行されるプロセスと同等で、ハイパーバイザの実行に伴うオーバーヘッドが存在しない

docker5_1.pngdocker5_2.png

Dockerの実行環境について

Dockerは純粋な仮想化は行わないため64bit Linuxでしか基本的に動作しないが、Docker for MacやDocker for Windowsなどネイティブのように使えるソフトウェアが開発されている。 https://www.docker.com/products/docker

Dockerの実行

インストール後に

$ docker run hello-world

でDockerについての情報を表示するコンテナがダウンロードされて実行される。

$ docker run -it ubuntu /bin/bash

でubuntuのbashが立ち上がる。runのあとにコマンドを記述することでコンテナの中でそのコマンドが実行される。コンテナ内のカレントディレクトリについては DockerfileWORKDIR で定義されたものになる(後述)。

-it はtty付きのインタラクティブセッションを要求するオプションなので対話的に何かを実行したいときは基本的に付けたらよい。

その状態で別のターミナルから

$ 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オプションで明示的に指定もできる。衝突した場合は起動不可。)

今度はDocker内でファイルを作成し

root@7d31a9f7251c:/# touch /tmp/hoge

Docker外で docker diff を実行すると

$ docker diff reverent_bose
C /tmp
A /tmp/hoge

となる。これはDockerがファイルシステムにUFS(union file system)を利用しているので複数のファイルシステムを階層的にマウントしており、その差分が表示されるようになっている。

この状態で

$ 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

とすると、 test/create_file というコンテナのイメージが新たに作成されていることが分かる。この test/create_file を実行すると、実際に /tmp/test ファイルが作成されている。

Dockerfile でのイメージの作成

上記のような手順でイメージを作成するのは手順を覚えないといけなくなり再度同じイメージを作成するのが非常に難しくなるために Dockerのイメージは一般的に Dockerfile を使って作成される。

Dockerfile の細かい構文についてはドキュメントを参照(https://docs.docker.com/engine/reference/builder/

ここでは上記で作ったイメージを作成する 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

すると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 を実行させる方法がある。

DockerfileENTRYPOINT という構文を使う。

FROM ubuntu:latest

RUN apt-get update && apt-get install -y cowsay fortune

ENTRYPOINT ["/usr/games/cowsay"]

イメージを更新する必要があるため docker build を行う。

$ docker build -t test/docker-cowsay .
$ docker run test/docker-cowsay hoge
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ただし、このままでは他のコマンドを実行したい場合などに融通が効かなくなってしまう。

そのため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はUFSで行毎にファイルシステム上で差分を保存しているため、何度も実行してもapt-getが走るのは1回だけで済む。しかし、ADDしたファイルの内容が変更されていた場合その後の行が全て別のものとして認識されるため、 ADD entrypoint.sh の後に RUN apt-get をしていた場合 entrypoint.sh を変更する度に apt-get が走る事になってしまう。

このイメージを実行すると

$ 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つ)、アプリケーションをホストマシンにインストールすることなく簡単に実行することができる。

今回は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に接続し、実際にデータを保存することができる。

どうしてこれでコンテナ同士が接続できるかというと、 –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 は次のように記述する。

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 の場合はその時点でビルドが実行される。

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を使ったCIについて

Jenkinsのコンテナを使ってDockerコンテナのCIを回すことが多い(弊社でも利用されているしオライリー本にも載っている)ので紹介する。

プロジェクトに変更がpushされたタイミングでJenkinsがチェックアウト・イメージのビルド・テストの実行を行うようにする。

Jenkinsコンテナ内でDockerを実行するにはJenkinsコンテナにDockerホストのソケットをマウントする方法と、Docker in Dockerの2つがあるが、今回は前者を説明する。

DockerのソケットをJenkinsコンテナにマウントすることで、Jenkinsは自身に対する兄弟となるコンテナを作成することが可能となる。

docker144_2.pngdocker144_1.png

まず、JenkinsのコンテナにDockerをインストールしたイメージを作成する。その上で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 を実行することで必要なプラグインを事前にインストールすることができる。

docker-compose.yml
jenkins:
  build: ./jenkins
  ports:
    - "8080:8080"
    - "50000:50000"
  volumes:
    - ~/docker_mount/jenkins:/var/jenkins_home/
    - /var/run/docker.sock:/var/run/docker.sock
  environment:
    - JAVA_OPTS=-Dhudson.footerURL=http://mycompany.com

(上記は少し古いため記述がdocker-compose v1となっている)

これを実行することで、8080番ポートでJenkinsが立ち上がり、テストが可能となる。

テストのサンプルについては余裕があれば後々更新します・・・

イメージのプッシュ

DockerHub

DockerはデフォルトではDockerHubを使ってイメージの公開を行う。

DockerHubでアカウントを取得すればdocker loginコマンドでサインインでき、その状態で docker push ユーザ名:イメージ名 コマンドを実行することでイメージをpublicに公開することができる。

事前に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で設定を記述することでそれぞれのマシンイメージを元となるイメージからプロビジョニングすることができる。

その中の1つにDockerがあり、既存のAnsibleなどのプロビジョニングが存在する環境に対してそれらを用いたDockerイメージを作成することができる。

ただし、Dockerの利点であるUFSによるキャッシュが効かないのと、Ansibleでのプロビジョニングは基本的にDockerに対して最適化などをしていないためにイメージのサイズが肥大化するなど、相性があまり良くない。

また、Dockerは基本的に1コンテナ1プロセスでコンテナ同士がオーケストレーションをしてサービスを構築するという思想のため、Ansibleなどが必要となるほどプロビジョニングが複雑化することは多くはない。

基本的には既存の非常に複雑な環境構築が必要なものに対して消去法で適用することになると考えられる。