Help us understand the problem. What is going on with this article?

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

Dockerとは

コンテナベースのアプリケーションを仮想化したもの。軽量なVMの様に見えるがこれまでの(VirtualBoxなど)VMでは実現が難しい、不可能であったユースケースを解決してくれる。

  • ホストOSとリソースを共有するのでリソースの管理がVMより効率的
  • 基本的に状態を持たないのでポータビリティが非常に高く、特定の環境に依存することがない
  • 軽量なのでVMと比較し複数のインスタンスを実行することができる
  • DockerHubなどのレジストリを利用することで既存のイメージをダウンロードして実行することができる

コンテナとVM

VM

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

コンテナ

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

docker5_1.pngdocker5_2.png

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のあとにコマンドを記述することでコンテナの中でそのコマンドが実行される。コンテナ内のカレントディレクトリについては DockerfileWORKDIR で定義されたものになる(後述)。

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

DockerfileENTRYPOINT という構文を使う。

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 の例である。

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 は次のように記述する。

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を用いてテストを行う。

docker144_2.pngdocker144_1.png

まず、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 を実行することで必要なプラグインを事前にインストールすることができる。

docker-compose.yml
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などが必要となるほどプロビジョニングが複雑化することはあまりないと考えられる。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした