alpineでGoのバイナリ動かなくて焦った話(anaconda)

先に結論を言うとGoバイナリをalpineで動かしたい時はCA証明書が必要になることがあって、ca-certificatesをインストールすりゃ解決するってこと。

「Goはさいきょーだから、バイナリファイル1つありゃうごくやろー!」って思ってたぼくはチンパンジーでした。

過程

Go言語ではanacondaっていうライブラリを使うとTwitterAPIを簡単に叩くことができます。最近Streaming APIにも対応したそう。

github.com

これを使って以下のようなコードを書きます。このプログラムは簡単に説明すると環境変数にセットされたTwitterAPIキーを使用してStreaming APIを叩いて取得したツイートを表示し続けるだけのものです。

ツイートが取得できない(APIキーが間違ってるとか)と「ERROR」と出力してプログラムが終了します。

package main

import(
  "log"
  "github.com/ChimeraCoder/anaconda"
  "os"
)

func main(){
  anaconda.SetConsumerKey(os.Getenv("CONSUMER_KEY"))
  anaconda.SetConsumerSecret(os.Getenv("CONSUMER_SECRET"))
  api := anaconda.NewTwitterApi(os.Getenv("ACCESS_TOKEN"), os.Getenv("ACCESS_SECRET"))
  stream := api.PublicStreamSample(nil)

  for {
    x := <-stream.C
    switch tweet := x.(type) {
    case anaconda.Tweet:
      log.Println("tweet.Text: ",  tweet.Text)
    case anaconda.StatusDeletionNotice:
      //pass
    default:
      log.Fatal("ERROR")
    }
  }
}

これをビルドしてバイナリファイルを作り、Dockerコンテナで動かそうと思いました。コンテナは公式のalpineのDockerイメージを使用します。Linuxの64bit環境で動かすのでGOOS=linux GOARCH=amd64 go buildというようにビルドします。

あとはこんな感じのDockerfileを書いてdocker build

FROM alpine
COPY test /
CMD ["/test"]
mbp2016:test shibujibu$ docker build -t hoge .
Sending build context to Docker daemon  5.962MB
Step 1/3 : FROM alpine
 ---> 76da55c8019d
Step 2/3 : COPY test /
 ---> 2193b80c19bb
Removing intermediate container 8098e29f2a77
Step 3/3 : CMD /test
 ---> Running in 20cb9a6ef9b5
 ---> ba8189720e34
Removing intermediate container 20cb9a6ef9b5
Successfully built ba8189720e34
Successfully tagged hoge:latest
mbp2016:test shibujibu$

コンテナを動かします。test.envにはTwitterAPIのキーの環境変数を記述してあります。

mbp2016:test shibujibu$ docker run --env-file test.env --rm hoge
2017/09/18 12:42:45 ERROR
mbp2016:test shibujibu$

は?エラー???

問題

test.envに記述したAPIキーは全部合っています。意味がわからん。

とりあえずGoバイナリを動かすコンテナに使用するイメージにちょっと変更を加えてみました。具体的にはDockerfileを書き換えてgolang:alpineからイメージを作ってそれを使うこととしました。

FROM golang:alpine
COPY test /
CMD ["/test"]

再びイメージをビルドしてからコンテナを動かします。

mbp2016:test shibujibu$ docker build -t hoge .

(省略)

mbp2016:test shibujibu$ docker run --env-file test.env --rm hoge
2017/09/18 12:48:50 tweet.Text:  RT @AndhraPradeshCM: అమెరికాలోని న్యూయార్క్<200c>లో జరుగనున్న ఐక్యరాజ్య సమితి ప్రత్యేక సమావేశానికి భారతదే
శం తరపున ఆంధ్రప్రదేశ్<200c>ను పంపించాలని కేం…
2017/09/18 12:48:50 tweet.Text:  RT @jyotigaurmba: #Prophecies_Of_Nostradamus #HappyBirthdayPM
सभी भविष्यवाणी के अनुसार तो दुनिया का मुक्तिदाता भारत में…
2017/09/18 12:48:50 tweet.Text:  松岡さん…ペテ…テルくん…

動いた!!!!

原因

alpine:latest(ここで使ったバージョンは3.6)とgolang:alpine(バージョンは1.9.0-alpine3.6)の違いを見てみます。なんとなくインストールされているパッケージが気になりました。apk infoでインストールされているパッケージのリストを出力することができます。

mbp2016:test shibujibu$ docker run --rm alpine apk info
WARNING: Ignoring APKINDEX.84815163.tar.gz: No such file or directory
WARNING: Ignoring APKINDEX.24d64ab1.tar.gz: No such file or directory
musl
busybox
alpine-baselayout
alpine-keys
libressl2.5-libcrypto
libressl2.5-libssl
zlib
apk-tools
scanelf
musl-utils
libc-utils
mbp2016:test shibujibu$ docker run --rm golang:alpine apk info
WARNING: Ignoring APKINDEX.84815163.tar.gz: No such file or directory
WARNING: Ignoring APKINDEX.24d64ab1.tar.gz: No such file or directory
busybox
alpine-baselayout
alpine-keys
libressl2.5-libcrypto
libressl2.5-libssl
zlib
apk-tools
scanelf
libc-utils
ca-certificates
musl
musl-utils
mbp2016:test shibujibu$

golang:alpineに入っていてalpineに入っていないのはca-certificatesだけですね。

TwitterAPI使う」→「OAuth使う」→「認可…?もしかしたらHTTPS通信でCA証明書が要るんじゃねぇか?」と、ca-certificatesをインストールすりゃいけるんじゃないかと思いました。Dockerfileを書き換えます。

FROM alpine
RUN apk --update add ca-certificates
COPY test /
CMD ["/test"]
mbp2016:test shibujibu$ docker build -t hoge .
Sending build context to Docker daemon  5.962MB
Step 1/4 : FROM alpine
 ---> 76da55c8019d
(省略)
mbp2016:test shibujibu$ docker run --env-file test.env --rm hoge
2017/09/18 15:29:44 tweet.Text:  RT @NegritoNd: C'est simple. Il suffit d'un voyage en Asie dans la ville de Guangzhou tu trouve tes
fournisseurs dans le cosmétiques. Tissa…

動きました!

/var/lib/docker/imagesと/var/lib/docker/overlay2を彷徨った話

/var/lib/docker/の下にあるDockerイメージ関係のファイルを色々見たってだけの話です。目的としてはDockerイメージがどのように管理されているのかの理解を深めたいってことと、どんなファイルがどこにあるのかを把握したかったことでしょうか。

目次

環境

Docker for MacMoby OSにscreenコマンドで入ってます。環境の情報は以下。

/ # docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 17.06.1-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file

(省略)

/ # 

保存されているDockerイメージは一度全て削除しました。また、使用しているStorage Driverはoverlay2(OverlayFS)です。

docker pull

※Dockerイメージのアップデートによって下記と同様の結果を得られない可能性があります

docker pullはDockerイメージをダウンロードしてくるコマンドと思われがち(?)な気がしますが、実際は「ローカル環境にないレイヤ(イメージの差分)をダウンロードするコマンド」です。とりあえずhttpd:2.4というイメージをdocker pullでローカルに保存します。

/ # docker pull httpd:2.4
2.4: Pulling from library/httpd
ad74af05f5a2: Pull complete 
3d839585b9c7: Pull complete 
cf157792586a: Pull complete 
c620105f0566: Pull complete 
830b826a2e13: Pull complete 
ec2eb5743536: Pull complete 
eb53f3c09897: Pull complete 
Digest: sha256:5b35d13089db73df620f4c198f5a4bfa56b8fe45a0364f343df9a26d874fef6c
Status: Downloaded newer image for httpd:2.4
/ #

次にcassandra:3をpullしましょう。

/ # docker pull cassandra:3
3: Pulling from library/cassandra
ad74af05f5a2: Already exists 
3d839585b9c7: Already exists 
a87bb16b5ac4: Pull complete 
1d7e4aef1e06: Pull complete 
d60628bada83: Pull complete 
923dfced70a8: Pull complete 
2dcb172a5d25: Pull complete 
1fdf88044a29: Pull complete 
3b51736e19e9: Pull complete 
2fe35cc8fdbb: Pull complete 
8af4ba731cbc: Pull complete 
Digest: sha256:11405c2afbe5e1d7487b72eb6df1b0791d3f3ac644959427e311ba5d51272bd1
Status: Downloaded newer image for cassandra:3
/ # 

Already existsとなっているレイヤが2つあるのが確認できます。

ここでhttpd:2.4のDockerfileと、cassandra:3のDockerfileを見てみましょう。両方ともdebian:jessie-backportsというイメージを使用して作られていることがわかります。

Already existsとなっている2つのレイヤはdebian:jessie-backportsで使われているレイヤであり、cassandraとhttpdで共通していることがわかります。そしてcassandra:3のpull時にすでにこれらのレイヤはローカルに存在するので、レジストリからダウンロードをしていません。

docker buildによるDockerイメージの作成

httpd:2.4を使って新たにDockerイメージを作成します。今回はtestというイメージを作成しました。

/ # mkdir work; cd work
/work # cat > Dockerfile <<EOF
> FROM httpd:2.4
> RUN echo aaa
> ADD bbb /
> EOF
/work # echo "bbb" > bbb
/work # docker build -t test .
Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM httpd:2.4
 ---> e74fcb59d25b
Step 2/3 : RUN echo aaa
 ---> Running in c0a96a2abd2b
aaa
 ---> 5a611d17ce49
Removing intermediate container c0a96a2abd2b
Step 3/3 : ADD bbb /
 ---> a3af767eee5b
Removing intermediate container e5057599e7b0
Successfully built a3af767eee5b
Successfully tagged test:latest
/work # docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
test                latest              a3af767eee5b        21 seconds ago      177MB
cassandra           3                   17c6f78ff576        6 days ago          385MB
httpd               2.4                 e74fcb59d25b        6 weeks ago         177MB
/work # 

DockerイメージのレイヤはDockerfileに書かれた"リソースの状態に変化を与える命令"の数だけ増えていきます。今回はRUN echo aaaADD bbbという命令を実行しています。echoコマンドはリソースの状態を変化させませんが、ADDは新たなファイルを追加します。 よってtestはhttpd:2.4のレイヤに新たなレイヤを1つ乗っけたDockerイメージになりました。

ただし、このRUN echo aaaによって、a3af767eee5b(test)の親となる5a611d17ce49というイメージの情報は作成されました。5a611d17ce49はレイヤの構造としてはe74fcb59d25b(httpd:2.4)と同じですが、異なるイメージIDです。

何を言ってるかわかんない人はdocker historyを実行すると理解できるんじゃないでしょう。

/ # docker history test
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
a3af767eee5b        2 minutes ago        /bin/sh -c #(nop) ADD file:506bbbe260d22ea...   4B                  
5a611d17ce49        2 minutes ago        /bin/sh -c echo aaa                             0B                  
e74fcb59d25b        6 weeks ago         /bin/sh -c #(nop)  CMD ["httpd-foreground"]     0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop)  EXPOSE 80/tcp                0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop) COPY file:761e313354b918...   133B                
<missing>           6 weeks ago         /bin/sh -c set -x  && buildDeps="   bzip2 ...   9.72MB              
<missing>           6 weeks ago         /bin/sh -c #(nop)  ENV HTTPD_ASC_FALLBACK_...   0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop)  ENV HTTPD_BZ2_FALLBACK_...   0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop)  ENV HTTPD_ASC_URL=https...   0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop)  ENV HTTPD_BZ2_URL=https...   0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop)  ENV HTTPD_SHA1=699e4e91...   0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop)  ENV HTTPD_VERSION=2.4.27     0B                  
<missing>           6 weeks ago         /bin/sh -c apt-get update  && apt-get inst...   44.2MB              
<missing>           6 weeks ago         /bin/sh -c {   echo 'deb http://deb.debian...   161B                
<missing>           6 weeks ago         /bin/sh -c #(nop)  ENV OPENSSL_VERSION=1.0...   0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop)  ENV NGHTTP2_VERSION=1.1...   0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop) WORKDIR /usr/local/apache2    0B                  
<missing>           6 weeks ago         /bin/sh -c mkdir -p "$HTTPD_PREFIX"  && ch...   0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop)  ENV PATH=/usr/local/apa...   0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop)  ENV HTTPD_PREFIX=/usr/l...   0B                  
<missing>           6 weeks ago         /bin/sh -c echo 'deb http://deb.debian.org...   55B                 
<missing>           6 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop) ADD file:a023a99f7d01868...   123MB      
/ # 

ちなみにDockerイメージのレイヤの数は、docker historyを実行したときに表示されるSIZEが0でないイメージの数と同じです。

docker inspectによるイメージの情報の取得

docker inspectはイメージやコンテナの様々な情報を表示するコマンドです。3つのイメージに対して実行し、結果を表にまとめました。

httpd:2.4 test cassandra:3
Id e74fcb59d25b a3af767eee5b 17c6f78ff576
Parent 5a611d17ce49
GraphDriver.Data.LowerDir 2db33dc1bcf8 2db33dc1bcf8 2db33dc1bcf8
92a02a0fd573 92a02a0fd573 92a02a0fd573
bf8eeee84ee1 bf8eeee84ee1 2f5f62573954
3907c84a2e8c 3907c84a2e8c 039eb2914846
9d7c5f5db931 9d7c5f5db931 f2045875c7ca
abddae19644c abddae19644c 0daf771f959a
849ba3b8deea 897896a0d4b6
8803b3ac7c28
6f0b9ede8868
908b9aa83622
GraphDriver.Data.MergedDir 849ba3b8deea 94289abd0b2d a497601c2f47
GraphDriver.Data.UpperDir 849ba3b8deea 94289abd0b2d a497601c2f47
GraphDriver.Data.WorkDir 849ba3b8deea 94289abd0b2d a497601c2f47
RootFS.Layers 2c40c66f7667 2c40c66f7667 2c40c66f7667
4552698f75ca 4552698f75ca 4552698f75ca
d0d2ea1af79e d0d2ea1af79e c15ba818508c
1cec7f0b11da 1cec7f0b11da e24d47802b79
485676c94e85 485676c94e85 1d83810d7a13
b1154a5677c9 b1154a5677c9 df00dc937d70
8dcb04aec53f 8dcb04aec53f 6de82d4abef4
2fae8ef74840 db42563f880d
fe10c6340d87
ac348eb6c295
c6f65dc602de
  • id
    • イメージのID
  • Parent
    • イメージを作る際に元となったイメージ(DockerfileのFROMで指定したイメージ)
    • pullしてきたイメージのParentは空になってる
      • docker historyで<missing>になっていたら空になる
      • 昔は空にならなかった?*1
  • GraphDriver.Data.*
    • よくわからんがoverlayfsに関係するものみたいです
    • LowerDirはDockerイメージを構成するもの
    • その他はコンテナに関係するもの?
  • RootFS.Layers
    • 文字通りの解釈をすると「ルートファイルシステムのレイヤ」
    • 各列のセルの個数とハッシュ値に注目するとGraphDriver.Data.LowerDirと対応関係がある気がします

/var/lib/dockerの探索

/var/lib/docker(Docker Root)はDocker関係の色々なデータが保存されている場所です。

/ # cd /var/lib/docker
/var/lib/docker # tree -L 1
.
├── containers
├── image
├── network
├── overlay2
├── plugins
├── swarm
├── tmp
├── trust
└── volumes

9 directories, 0 files
/var/lib/docker # 

主にDockerイメージに関係するのはimageとoverlay2のディレクトリです。

/var/lib/docker/image の探索

/var/lib/docker/imageには以下のようなディレクトリとファイルがあります

/var/lib/docker/image # tree -L 2
.
└── overlay2
    ├── distribution
    ├── imagedb
    ├── layerdb
    └── repositories.json

4 directories, 1 file
/var/lib/docker/image #

repositories.json

まずrepositories.jsonを表示してみます。

/var/lib/docker/image/overlay2 # cat repositories.json | jq
{
  "Repositories": {
    "cassandra": {
      "cassandra:3": "sha256:17c6f78ff576daa2f84e94eee62ca6a0d1bf4c35e41246a9ba0179b1cffc8000",
      "cassandra@sha256:11405c2afbe5e1d7487b72eb6df1b0791d3f3ac644959427e311ba5d51272bd1": "sha256:17c6f78ff576daa2f84e94eee62ca6a0d1bf4c35e41246a9ba0179b1cffc8000"
    },
    "httpd": {
      "httpd:2.4": "sha256:e74fcb59d25bdf03adcc8d89bcda8ae9456d2041c557c973072085708814e5c7",
      "httpd@sha256:5b35d13089db73df620f4c198f5a4bfa56b8fe45a0364f343df9a26d874fef6c": "sha256:e74fcb59d25bdf03adcc8d89bcda8ae9456d2041c557c973072085708814e5c7"
    },
    "test": {
      "test:latest": "sha256:a3af767eee5b9e96b12623e60c4bd71aad6613bbf127b0bb13e039dafa28e16a"
    }
  }
}
/var/lib/docker/image/overlay2 #

ここにはイメージのタグとIDの対応が書かれています。「cassandra@sha256:11405c2afbe5e…」というのはRepositoryIDで、イメージのリポジトリを表すハッシュ値のようです。たぶんDockerレジストリからpullした時に設定されるものでしょう。

distribution

/var/lib/docker/image/overlay2/distribution # tree -L 2
.
├── diffid-by-digest
│   └── sha256
└── v2metadata-by-diffid
    └── sha256

4 directories, 0 files
/var/lib/docker/image/overlay2/distribution # 

双方のsha256というディレクトリの下には数百個のファイルが置いてありました。よくわかんないですがDocker Registry(Docker Distribution)に関係するメタデータとかを保存してあるディレクトリのようです。ちらっとmoby/distributionのソースを読んで見たりもしましたが、わかったのはその程度。

diffid-by-digest/sha256のディレクトリの下のファイルにはハッシュ値が書いてあります。たとえばこんな感じです。

/var/lib/docker/image/overlay2/distribution/diffid-by-digest/sha256 # echo $(cat f814b22b783e01a2e2dce50c55554ca2777bb4f56098398d96d4532aa7642e1f)
sha256:b6a88c37a096ae33a00249244c5c057f18b996a65bd44c5033fbc8482c465cfc
/var/lib/docker/image/overlay2/distribution/diffid-by-digest/sha256 # 

b6a88c37a096ae33a002…というハッシュ値はv2metadata-by-diffid/sha256の下にファイル名として存在していました。その内容は以下です。

/var/lib/docker/image/overlay2/distribution/v2metadata-by-diffid/sha256 # cat b6
a88c37a096ae33a00249244c5c057f18b996a65bd44c5033fbc8482c465cfc | jq
[
  {
    "Digest": "sha256:f814b22b783e01a2e2dce50c55554ca2777bb4f56098398d96d4532aa7642e1f",
    "SourceRepository": "docker.io/library/mariadb",
    "HMAC": ""
  }
]
/var/lib/docker/image/overlay2/distribution/v2metadata-by-diffid/sha256 # 

なんかぼくが結構昔にpullした覚えのあるイメージのリポジトリの名前がでてきました。docker pullの履歴…でしょうか。

imagedb

/var/lib/docker/image/overlay2/imagedb # tree -L 4
.
├── content
│   └── sha256
│       ├── 17c6f78ff576daa2f84e94eee62ca6a0d1bf4c35e41246a9ba0179b1cffc8000
│       ├── 5a611d17ce49bcb851f8888f76bfaabedd01e6f72a3285542c0c4bd428412240
│       ├── a3af767eee5b9e96b12623e60c4bd71aad6613bbf127b0bb13e039dafa28e16a
│       ├── e74fcb59d25bdf03adcc8d89bcda8ae9456d2041c557c973072085708814e5c7
│       └── temp
└── metadata
    └── sha256
        ├── 5a611d17ce49bcb851f8888f76bfaabedd01e6f72a3285542c0c4bd428412240
        │   └── parent
        └── a3af767eee5b9e96b12623e60c4bd71aad6613bbf127b0bb13e039dafa28e16a
            └── parent

6 directories, 7 files
/var/lib/docker/image/overlay2/imagedb # 

おっ!見たことあるハッシュ値が出てきましたね。

まずcontent/sha256の下にあるハッシュ値のファイルはイメージのIDのハッシュ値と一致します。5a611d17ce49…ってファイルはtestイメージのParentのIDと一致します。これらのファイルはjson形式のファイルで、docker inspectのコマンドを実行した時のようなものが表示されました。まぁイメージの情報が記載されたファイルと言っていいのかなと思います。

特に興味深かったのは"history"という項目で、ここではいつどのようなコマンドでレイヤが作られたのかが全てわかります。

...
  "history": [
    {
      "created": "2017-07-24T16:51:25.302466408Z",
      "created_by": "/bin/sh -c #(nop) ADD file:a023a99f7d01868b164d63bfaf8aabc7f271659c69939c3854f041f5a3217428 in / "
    },
    {
      "created": "2017-07-24T16:51:25.711083944Z",
      "created_by": "/bin/sh -c #(nop)  CMD [\"bash\"]",
      "empty_layer": true
    },
    {
      "created": "2017-07-24T16:51:28.195514863Z",
      "created_by": "/bin/sh -c echo 'deb http://deb.debian.org/debian jessie-backports main' > /etc/apt/sources.list.d/backports.list"
    },
...

tempディレクトリは空でした。

metadata/sha256ディレクトリにはハッシュ値の名前のディレクトリの下にparentというファイルが存在します。1つずつその内容を見てみましょう。

/var/lib/docker/image/overlay2/imagedb/metadata/sha256 # echo $(cat 5a611d17ce49bcb851f8888f76bfaabedd01e6f72a3285542c0c4bd428412240/parent )
sha256:e74fcb59d25bdf03adcc8d89bcda8ae9456d2041c557c973072085708814e5c7
/var/lib/docker/image/overlay2/imagedb/metadata/sha256 # echo $(cat a3af767eee5b9e96b12623e60c4bd71aad6613bbf127b0bb13e039dafa28e16a/parent )
sha256:5a611d17ce49bcb851f8888f76bfaabedd01e6f72a3285542c0c4bd428412240
/var/lib/docker/image/overlay2/imagedb/metadata/sha256 # 

これは単純にイメージの親子関係を示したものな気がします。なんとなく「a3…の親は5a…で、5a…の親はe7…」というのを表してる気がします。ていうかdocker historyを実行すればわかるんじゃないか。

/var/lib/docker/image/overlay2/imagedb/metadata/sha256 # docker history test
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
a3af767eee5b        5 hours ago         /bin/sh -c #(nop) ADD file:506bbbe260d22ea...   4B                  
5a611d17ce49        5 hours ago         /bin/sh -c echo aaa                             0B                  
e74fcb59d25b        6 weeks ago         /bin/sh -c #(nop)  CMD ["httpd-foreground"]     0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop)  EXPOSE 80/tcp                0B                  
<missing>           6 weeks ago         /bin/sh -c #(nop) COPY file:761e313354b918...   133B    
...

推測は当たってましたね。

layerdb

/var/lib/docker/image/overlay2/layerdb # tree -L 2
.
├── mounts
├── sha256
│   ├── 0b5f5b5372687dc3ee654a390291ff3fad4a16453982324c7a1516b5fdad3344
│   ├── 16b24c982e9af95d8901216f85a88a619b388afcdaffafe7fee4dfd297a3cdd2
│   ├── 2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc
│   ├── 307d16ce704ed0b8e116087ee1fba751ee2b37ba9f4ffd0c66bc113290316e58
│   ├── 401d4edd5b986b47951ab29b2eaff57d54b95af3087e03e67948fa65de99043d
│   ├── 45f77e6fb6f40c7ae6d8ab47a2fb68aeb22366daa8203bfce4156a8121341b1b
│   ├── 4965e73361fe8100273897579149913ecb5a8429721656c5c999542adda3abc2
│   ├── 58c80d31b95aece5164c036a124a7c5c9d4a255cd0401243412d2d6a54fe5368
│   ├── 5a850e1dce73e5ee3487cc6db12a7382d8850b7a8cdc48dc4d4c27baba8f3219
│   ├── 68ba39668b7fee407c39a6b62559ffea45fbd53af01b913f59544a01fc341ca7
│   ├── 74baa82fa7b49621f26d837d1946b239eab368714791e47dbc9fef63e808d563
│   ├── ae203f0b5fb6d009c5497b5f38411087d2fa50d0e7c3e7b5a707c8f83d761104
│   ├── bd88d85b29f57a5c4f6f0a32117750257dcb07d0c823e5aafc75b5eb83cacd7e
│   ├── c27aaeeaafc3fa91ad11673c6bd1ded2a6dab88ecf5c0cdb29cb7a800523e2f7
│   ├── cac57b9fa5f9ee5d8f76ead54ce9d3192ebe5ef7e5b42d4977dbccefb0df3799
│   ├── d29fdb354f8c5359ef8fc3eb92bad413be6593a500ccbd536a50386bdb5098ef
│   └── d88f40d9b23524a317ed26592089b884d6f3d1135761233b5b3fa6d175e3b4b2
└── tmp

20 directories, 0 files
/var/lib/docker/image/overlay2/layerdb # 

mountsとtmpというディレクトリは空ですね…。sha256の下にはハッシュ値の名前のディレクトリが17個あります。17ってことは…docker inspectの表のRootFS.Layersの種類の数と一致してますね。

2c40c66f7667…というハッシュ値は、現在保存してある3つのイメージに共通するベースイメージのIDと同じですね。しかし、それ以外のハッシュ値は見たことがありません。とりあえず2c40c66f7667…ディレクトリの下を調べます。

/var/lib/docker/image/overlay2/layerdb/sha256/2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc # ls -l
total 472
-rw-r--r--    1 root     root            64 Sep  8 07:45 cache-id
-rw-r--r--    1 root     root            71 Sep  8 07:45 diff
-rw-r--r--    1 root     root             9 Sep  8 07:45 size
-rw-r--r--    1 root     root        470368 Sep  8 07:45 tar-split.json.gz
/var/lib/docker/image/overlay2/layerdb/sha256/2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc # 

ファイルが4つあって、その内1つはgzipで圧縮されたjsonファイルのようですね。

/var/lib/docker/image/overlay2/layerdb/sha256/2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc # echo $(cat cache-id )
2db33dc1bcf8c874324675de0c9a919ce9f3abbc9cfe947840fa33ea7ef38a8d
/var/lib/docker/image/overlay2/layerdb/sha256/2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc # echo $(cat diff)
sha256:2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc
/var/lib/docker/image/overlay2/layerdb/sha256/2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc # echo $(cat size)
123450406
/var/lib/docker/image/overlay2/layerdb/sha256/2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc # 

うーんよくわからん。他のディレクトリの中も見てみましょう。

/var/lib/docker/image/overlay2/layerdb/sha256/0b5f5b5372687dc3ee654a390291ff3fad4a16453982324c7a1516b5fdad3344 # ls -l
total 20
-rw-r--r--    1 root     root            64 Sep  8 07:45 cache-id
-rw-r--r--    1 root     root            71 Sep  8 07:45 diff
-rw-r--r--    1 root     root            71 Sep  8 07:45 parent
-rw-r--r--    1 root     root             2 Sep  8 07:45 size
-rw-r--r--    1 root     root           381 Sep  8 07:45 tar-split.json.gz
/var/lib/docker/image/overlay2/layerdb/sha256/0b5f5b5372687dc3ee654a390291ff3fad4a16453982324c7a1516b5fdad3344 # echo $(cat cache-id )
92a02a0fd5736b9fb0f2e5daa315bfd56cc753a50f38dcc34cf4c60304159016
/var/lib/docker/image/overlay2/layerdb/sha256/0b5f5b5372687dc3ee654a390291ff3fad4a16453982324c7a1516b5fdad3344 # echo $(cat diff)
sha256:4552698f75ca2ab79d3a88725c9749f3994085abbf462011892b034a5ead2813
/var/lib/docker/image/overlay2/layerdb/sha256/0b5f5b5372687dc3ee654a390291ff3fad4a16453982324c7a1516b5fdad3344 # echo $(cat size)
55
/var/lib/docker/image/overlay2/layerdb/sha256/0b5f5b5372687dc3ee654a390291ff3fad4a16453982324c7a1516b5fdad3344 # echo $(cat parent)
sha256:2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc
/var/lib/docker/image/overlay2/layerdb/sha256/0b5f5b5372687dc3ee654a390291ff3fad4a16453982324c7a1516b5fdad3344 # 

んん!?parentってファイルが現れましたね。もう1つ他のディレクトリの中も…

/var/lib/docker/image/overlay2/layerdb/sha256/16b24c982e9af95d8901216f85a88a619b388afcdaffafe7fee4dfd297a3cdd2 # ls -l
total 20
-rw-r--r--    1 root     root            64 Sep  8 07:47 cache-id
-rw-r--r--    1 root     root            71 Sep  8 07:47 diff
-rw-r--r--    1 root     root            71 Sep  8 07:47 parent
-rw-r--r--    1 root     root             2 Sep  8 07:47 size
/var/lib/docker/image/overlay2/layerdb/sha256/16b24c982e9af95d8901216f85a88a619b388afcdaffafe7fee4dfd297a3cdd2 # echo $(cat cache-id )
0daf771f959a4fb1f987cdce446ad84fbfd4924eaf9ac8558d287cc25f76d32d
/var/lib/docker/image/overlay2/layerdb/sha256/16b24c982e9af95d8901216f85a88a619b388afcdaffafe7fee4dfd297a3cdd2 # echo $(cat diff)
sha256:df00dc937d70bdb82f35892405fcd19420a9c75d6cfa070537c316b7e3327746
/var/lib/docker/image/overlay2/layerdb/sha256/16b24c982e9af95d8901216f85a88a619b388afcdaffafe7fee4dfd297a3cdd2 # echo $(cat size)
85
/var/lib/docker/image/overlay2/layerdb/sha256/16b24c982e9af95d8901216f85a88a619b388afcdaffafe7fee4dfd297a3cdd2 # echo $(cat parent)
sha256:307d16ce704ed0b8e116087ee1fba751ee2b37ba9f4ffd0c66bc113290316e58
/var/lib/docker/image/overlay2/layerdb/sha256/16b24c982e9af95d8901216f85a88a619b388afcdaffafe7fee4dfd297a3cdd2 # 

いろんなハッシュ値見てきて流石に疲れてきました。

sizeはレイヤのサイズが書かれたファイルに間違いないでしょう。cache-idのハッシュ値はGraphDriver.Data.LowerDirのハッシュ値の中に一致するものがあります。diffのファイルのハッシュ値はGraphDriver.Data.LowerDirのハッシュ値に対応するレイヤでしょうか。前にも言いましたがdocker inspectの表を見るとGraphDriver.Data.LowerDirのセルとRootFS.Layersのセルは対応関係があるように見えるんですよね。

parentファイルについては「なるほど、imagedbのparentファイルはイメージの親子関係を示すものでしたが、layerdbのparentファイルはレイヤの親子関係を示すものっぽい…」と言いたかったんですが、307d16ce704e…というハッシュ値は初めて見ました。なんだこりゃ。ていうかディレクトリの名前のハッシュ値の意味もよくわかんないですよね…

さて、スルーしてきたtar-split.jsonなんですが、こいつはクソでかくて内容も意味不明なんですが非常に興味深いファイルでした。解凍してheadで出力してみるとこんな感じになっています。

/var/lib/docker/image/overlay2/layerdb/sha256/2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc # gzip -d tar-split.json.gz 
/var/lib/docker/image/overlay2/layerdb/sha256/2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc # head tar-split.json
{"type":2,"payload":"YmluLwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwNDA3NTUAMDAwMDAwMAAwMDAwMDAwADAwMDAwMDAwMDAwADEzMTM0NzYzNDAwADAxMDAyMQAgNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","position":0}
{"type":1,"name":"bin/","payload":null,"position":1}
{"type":2,"payload":"YmluL2Jhc2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAxMDA3NTUAMDAwMDAwMAAwMDAwMDAwADAwMDAzNzMyNzcwADEzMDA3NDQ2NTcxADAxMDcxMgAgMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","position":2}
{"type":1,"name":"bin/bash","size":1029624,"payload":"Zm7whxqHhHs=","position":3}
{"type":2,"payload":"AAAAAAAAAABiaW4vY2F0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDEwMDc1NQAwMDAwMDAwADAwMDAwMDAAMDAwMDAxNDUzMTAAMTI1MDEwNTM3NzAAMDEwNTE2ACAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHVzdGFyADAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDAwMDAAMDAwMDAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==","position":4}
{"type":1,"name":"bin/cat","size":51912,"payload":"yjb1ezeiycI=","position":5}
{"type":2,"payload":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYmluL2NoYWNsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAxMDA3NTUAMDAwMDAwMAAwMDAwMDAwADAwMDAwMDM0MzQwADEyNDAzMjUyMTQxADAxMTAxMwAgMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","position":6}
{"type":1,"name":"bin/chacl","size":14560,"payload":"1nko8F2nz90=","position":7}
{"type":2,"payload":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYmluL2NoZ3JwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAxMDA3NTUAMDAwMDAwMAAwMDAwMDAwADAwMDAwMTY1MjUwADEyNTAxMDUzNzcwADAxMTA1NwAgMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","position":8}
{"type":1,"name":"bin/chgrp","size":60072,"payload":"BDG8jvA7tS4=","position":9}
/var/lib/docker/image/overlay2/layerdb/sha256/2c40c66f7667aefbb18f7070cf52fae7abbe9b66e49b4e1fd740544e7ceaebdc # 

「"name"はDockerイメージに格納されているファイルかな〜」とか「"payload"はファイルの内容かな〜」って推測ができます。"position"はtailで確認したところ16517までありました。

調べて見るとこれはtar-splitというもの使用して作られたファイルのようです。tarファイルを逆アセンブルしてjsonのオブジェクトとして分けている???用途はいまいちよくわかってません。

/var/lib/docker/overlay2の探索

overlay2(OverlayFS)についてはDocker技術入門Docker-docs-jaが参考になります。

/var/lib/docker/overlay2 # tree -L 1
.
├── 039eb29148465722868d44cca608f800468b4fee3e17656fefe8c6849ba8a273
├── 0daf771f959a4fb1f987cdce446ad84fbfd4924eaf9ac8558d287cc25f76d32d
├── 2db33dc1bcf8c874324675de0c9a919ce9f3abbc9cfe947840fa33ea7ef38a8d
├── 2f5f62573954383a3198c873dec271ace28a90305417849784783d53e573e04e
├── 3907c84a2e8cea3b5c4f8936769381a9b1c8517e1a102f1c1ca601ef84775658
├── 6f0b9ede88685f011e22f57cdc3128928648a262a26bfbb88ce5a28f991ced5b
├── 849ba3b8deead52bbb555dfd3003da9cc3cf1b5ed947574a945962f615b3288e
├── 8803b3ac7c2854f462495405210e1b37571755ae03a3dd401bc5eedcf0ef9ed1
├── 897896a0d4b68945de1e16fa69a29be337822bf3d9be72f3807fd6c762a19a6d
├── 908b9aa836221d528df4039a8972c78cee8f62362239a340197851441095f270
├── 92a02a0fd5736b9fb0f2e5daa315bfd56cc753a50f38dcc34cf4c60304159016
├── 94289abd0b2d381a14b20a0801cb59f38ebdbb1f61e88f7ab6a0a931b4e46f28
├── 9d7c5f5db93144797926396ab64b3ef9e9af755bb1ae1c374f1bc125ef8d62ee
├── a497601c2f47a3d26189522e10097592728df133d6418ac4b97f575fd1a087fd
├── abddae19644c1d09d312ce1dbe6c1b95815d5f9917cc0ec2fd55b7554bed6813
├── bf8eeee84ee100f366d677e19fbea7680f87ca431f8634f14ce08bf490a013e8
├── f2045875c7cad61ac4f5c694865daa780825b064569835b48857f6fab180fc57
└── l

18 directories, 0 files
/var/lib/docker/overlay2 # 

ディレクトリ内にはGraphDriver.Data.LowerDirで見たハッシュ値ディレクトリがいくつもあり、lという名前のディレクトリも存在します。まず初めにlの中を見ます。

/var/lib/docker/overlay2 # ls -l l
total 68
lrwxrwxrwx    1 root     root            72 Sep  8 07:45 6CJN3N2LQUWFFE5JXD76TSGXJA -> ../2db33dc1bcf8c874324675de0c9a919ce9f3abbc9cfe947840fa33ea7ef38a8d/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:45 7QZA73LSD7ZBNOEM24DB3JYCL7 -> ../849ba3b8deead52bbb555dfd3003da9cc3cf1b5ed947574a945962f615b3288e/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:45 DFYUB2X43MSKOPR6KPWA5NDXZS -> ../abddae19644c1d09d312ce1dbe6c1b95815d5f9917cc0ec2fd55b7554bed6813/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:45 GIS437CEOFWNTOUFLYZ6Y5VXGV -> ../3907c84a2e8cea3b5c4f8936769381a9b1c8517e1a102f1c1ca601ef84775658/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:48 HP2UA5JBG5VRENFVGX2HQPFYO5 -> ../6f0b9ede88685f011e22f57cdc3128928648a262a26bfbb88ce5a28f991ced5b/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:48 IGLBERN35IOHTKT4IHQWN7QU4Y -> ../a497601c2f47a3d26189522e10097592728df133d6418ac4b97f575fd1a087fd/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:45 IW4UTGZLJQFFL7YIJOORJXBOMC -> ../9d7c5f5db93144797926396ab64b3ef9e9af755bb1ae1c374f1bc125ef8d62ee/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:47 KJAF45SHPEBMO7YLF2TXNE7IHJ -> ../f2045875c7cad61ac4f5c694865daa780825b064569835b48857f6fab180fc57/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:48 MG4BLYAE6ANVWDLEITBAF3IQZD -> ../908b9aa836221d528df4039a8972c78cee8f62362239a340197851441095f270/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:47 OOQ3TV5QD5GMPWTBUZ3OVADX66 -> ../897896a0d4b68945de1e16fa69a29be337822bf3d9be72f3807fd6c762a19a6d/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:47 P5LQ54M2LA4RHYIWUTFZ7UWREN -> ../039eb29148465722868d44cca608f800468b4fee3e17656fefe8c6849ba8a273/diff
lrwxrwxrwx    1 root     root            72 Sep  8 10:02 QZL3Z4CKLPVJAKAS2FVQEZV5OY -> ../94289abd0b2d381a14b20a0801cb59f38ebdbb1f61e88f7ab6a0a931b4e46f28/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:47 R6ICSXFWWKVF5COWGMVK6V7FIE -> ../2f5f62573954383a3198c873dec271ace28a90305417849784783d53e573e04e/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:47 ULLIFMMDGK4L7GRHG5K342KB3N -> ../0daf771f959a4fb1f987cdce446ad84fbfd4924eaf9ac8558d287cc25f76d32d/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:45 Z3HSFRAFYXCHT3NTSHGLPXNG5M -> ../bf8eeee84ee100f366d677e19fbea7680f87ca431f8634f14ce08bf490a013e8/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:47 ZABQTUIS3WWBV5IF65IGFTYZ6O -> ../8803b3ac7c2854f462495405210e1b37571755ae03a3dd401bc5eedcf0ef9ed1/diff
lrwxrwxrwx    1 root     root            72 Sep  8 07:45 ZSG73ARDTW3UFT47AW4DJWQFAC -> ../92a02a0fd5736b9fb0f2e5daa315bfd56cc753a50f38dcc34cf4c60304159016/diff
/var/lib/docker/overlay2 # 

ハッシュ値ディレクトリの下のdiffというディレクトリのシンボリックリンクがありました。ファイル名は大文字の英字と数字からなっています。

次にハッシュ値ディレクトリの下を見てみます。試しに039eb2914846572…のディレクトリの中身を表示します。

/var/lib/docker/overlay2/039eb29148465722868d44cca608f800468b4fee3e17656fefe8c6849ba8a273 # ls -l
total 20
drwxr-xr-x    6 root     root          4096 Sep  8 07:47 diff
-rw-r--r--    1 root     root            26 Sep  8 07:47 link
-rw-r--r--    1 root     root            86 Sep  8 07:47 lower
drwx------    2 root     root          4096 Sep  8 07:47 merged
drwx------    2 root     root          4096 Sep  8 07:47 work
/var/lib/docker/overlay2/039eb29148465722868d44cca608f800468b4fee3e17656fefe8c6849ba8a273 # 

3つのディレクトリと2つのファイルが確認できました。ここで、DockerイメージtestのGraphDriver.Data.LowerDirのハッシュ値の名前がつけられたディレクトリを見ていきます。見る時の順番はイメージのレイヤの中で低いところに位置すると考えられるものからです。mergedとworkの中身はどのディレクトリも空なので省略します。

こんな感じのシェルスクリプトを書いて

#!/bin/sh

dirs="2db33dc1bcf8c874324675de0c9a919ce9f3abbc9cfe947840fa33ea7ef38a8d
92a02a0fd5736b9fb0f2e5daa315bfd56cc753a50f38dcc34cf4c60304159016
bf8eeee84ee100f366d677e19fbea7680f87ca431f8634f14ce08bf490a013e8
3907c84a2e8cea3b5c4f8936769381a9b1c8517e1a102f1c1ca601ef84775658
9d7c5f5db93144797926396ab64b3ef9e9af755bb1ae1c374f1bc125ef8d62ee
abddae19644c1d09d312ce1dbe6c1b95815d5f9917cc0ec2fd55b7554bed6813
849ba3b8deead52bbb555dfd3003da9cc3cf1b5ed947574a945962f615b3288e
"

echo "---link---"

for dir in $dirs
do
        echo $(cat $dir/link)
done

echo -e "\n---lower---"

for dir in $dirs
do
        if [ -e $dir/lower ]; then
                echo $(cat $dir/lower)
        fi
done

echo -e "\n---diff---"

for dir in $dirs
do
        if [ -e $dir/diff ]; then
                echo $(ls $dir/diff)
        fi
done

exit 0

スクリプトを実行します

/var/lib/docker/overlay2 # ./show.sh
---link---
6CJN3N2LQUWFFE5JXD76TSGXJA
ZSG73ARDTW3UFT47AW4DJWQFAC
Z3HSFRAFYXCHT3NTSHGLPXNG5M
GIS437CEOFWNTOUFLYZ6Y5VXGV
IW4UTGZLJQFFL7YIJOORJXBOMC
DFYUB2X43MSKOPR6KPWA5NDXZS
7QZA73LSD7ZBNOEM24DB3JYCL7

---lower---
l/6CJN3N2LQUWFFE5JXD76TSGXJA
l/ZSG73ARDTW3UFT47AW4DJWQFAC:l/6CJN3N2LQUWFFE5JXD76TSGXJA
l/Z3HSFRAFYXCHT3NTSHGLPXNG5M:l/ZSG73ARDTW3UFT47AW4DJWQFAC:l/6CJN3N2LQUWFFE5JXD76TSGXJA
l/GIS437CEOFWNTOUFLYZ6Y5VXGV:l/Z3HSFRAFYXCHT3NTSHGLPXNG5M:l/ZSG73ARDTW3UFT47AW4DJWQFAC:l/6CJN3N2LQUWFFE5JXD76TSGXJA
l/IW4UTGZLJQFFL7YIJOORJXBOMC:l/GIS437CEOFWNTOUFLYZ6Y5VXGV:l/Z3HSFRAFYXCHT3NTSHGLPXNG5M:l/ZSG73ARDTW3UFT47AW4DJWQFAC:l/6CJN3N2LQUWFFE5JXD76TSGXJA
l/DFYUB2X43MSKOPR6KPWA5NDXZS:l/IW4UTGZLJQFFL7YIJOORJXBOMC:l/GIS437CEOFWNTOUFLYZ6Y5VXGV:l/Z3HSFRAFYXCHT3NTSHGLPXNG5M:l/ZSG73ARDTW3UFT47AW4DJWQFAC:l/6CJN3N2LQUWFFE5JXD76TSGXJA

---diff---
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
etc
usr
etc
etc lib tmp usr var
bin etc lib tmp usr var
usr
/var/lib/docker/overlay2 # 

なかなか面白い結果が得られたんじゃないかなと思います。

linkファイルには大文字の英字と数字からなる文字列が書かれていますが、これは自身のレイヤのdiffディレクトリを指すシンボリックリンクの名前です。

イメージの最下位のレイヤと考えられる2db33dc1bcf8…のディレクトリの下のdiffには、Debianの標準的なディレクトリ階層が存在します。これはベースイメージ(Dockerイメージの祖となるイメージ。ちなみにhttpd:2.4とcassandra:3のベースイメージはdebian:jessie)に含まれるレイヤです。これに他のディレクトリにあるdiffの内容を上乗せしていき、マージしてコンテナのディレクトリを構成するのでしょう。

lowerは上乗せしていくレイヤの順番を示しているようです。大文字の英字と数字からなる文字列はlディレクトリにあったシンボリックリンクであり、これはそれぞれdiffディレクトリのリンクとなっています。レイヤが上層になるほどlowerファイルに書かれた文字列は長くなっていきます。また、lowerファイルの文字列の一番後ろには自身のレイヤのdiffディレクトリを指すシンボリックリンクの名前を追加していっています。

まとめ

Dockerイメージはqcowやrawのように「イメージファイル」というひとまとまりにはなっておらず、分割されてレイヤとして管理されていることをこの目で確認できました。また、/var/lib/docker/images、/var/lib/docker/overlay2にどのようなファイルが置かれているのかもわかりました。今度はOverlayFSについての理解を深めるためにコンテナを起動して/var/lib/docker/overlay2ディレクトリの変化を見たいと思います。

それと、少し不思議に思ったのはdokcer pullを実行したときに「ad74af05f5a2: Pull complete 」というようなメッセージが出ますが、このハッシュ値の名前がついたファイルやディレクトリ、またこのハッシュ値が書かれたファイルがどこにも見当たらなかったことです。このハッシュ値はいったいなんなんでしょうか。

参考

ブリッジに接続したらDHCPからIPが割り当てられない(VMware)

ゲストOSのインタフェースをVMwareのブリッジネットワークに接続したけど、一向にIPが割り当てられない時の対策。Windows 8.1の話です。

[ コントロールパネル ]

[ ネットワークとインターネット ]

[ ネットワーク接続 ]

Wi-Fiを使っているならWi-Fi以外のやつのプロパティを開いて「VMware Bridge Protocol」のチェックを外す

Dockerコンテナのタイムゾーンの変更は環境変数を設定すればいいと思っている方へ

コンテナのタイムゾーンはデフォルトではだいたいUTC(協定世界時)になってるんですが、これをJST(日本標準時)に変更したい場合、よく知られている方法として

$ docker run -e TZ=Asia/Tokyo hoge

というように環境変数TZを設定してやればいいというのがあります。ただしAlpine Linuxはこれだけではうまくいきません。

実験

dockerイメージalpine:3.6でコンテナを作ります。そしてdateコマンドを実行します。

mbp2016:~ shibujibu$ docker run alpine:3.6 date
Mon Aug 28 05:44:58 UTC 2017
mbp2016:~ shibujibu$ docker run -e TZ=Asia/Tokyo alpine:3.6 date
Mon Aug 28 05:45:02 GMT 2017
mbp2016:~ shibujibu$ 

タイムゾーンGMT(グリニッジ標準時)になってる???

原因

答えはほとんどここのQiitaの記事に書いてあります

qiita.com

AlpineにはUbuntuとかCentOSみたいなディストリと違って/usr/share/zoneinfoが存在しません。

mbp2016:~ shibujibu$ docker run -it alpine:3.6 /bin/sh
/ # ls /usr/share/
apk   man   misc
/ # 

というわけでLinuxのTime Zone Databaseをインストールします。

/ # apk --update add tzdata
fetch http://dl-cdn.alpinelinux.org/alpine/v3.6/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.6/community/x86_64/APKINDEX.tar.gz
(1/1) Installing tzdata (2017a-r0)
Executing busybox-1.26.2-r5.trigger
OK: 7 MiB in 12 packages
/ # ls /usr/share/
apk       man       misc      zoneinfo
/ # export TZ=Asia/Tokyo
/ # date
Mon Aug 28 15:52:41 JST 2017
/ # 

おお!JSTになった!

ちなみにfluent/fluentdなんかはベースイメージがAlpine Linuxなので注意です。

DockerでEFKの環境を作って、fluentdのコンテナでTZが異なる外部ホストのrsyslogからデータを取得していた時、kibanaでsyslogのデータのtimestampを見たら未来の時間になってました。fluentdのTZをJSTにしたら解決しましたね。

とりあえずdocker-composeでEFK動かした時のメモ

初めてfluentd触った時設定ファイルのあれやこれやがちんぷんかんぷんだったのでメモっておきました。ほぼ自分用。

とりあえず何も考えずにここのサンプルを実行 docs.fluentd.org

このサンプルは何やってんのか

  • httpdのコンテナの出力をDockerのlogdriverを使ってfluentdに転送
  • fluentdでデータの整形をした後elasticsearchに転送
  • kibanaで可視化

fluent.confについて

# fluentd/conf/fluent.conf
<source>
  @type forward
  port 24224
  bind 0.0.0.0
</source>
<match *.**>
  @type copy
  <store>
    @type elasticsearch
    host elasticsearch
    port 9200
    logstash_format true
    logstash_prefix fluentd
    logstash_dateformat %Y%m%d
    include_tag_key true
    type_name access_log
    tag_key @log_name
    flush_interval 1s
  </store>
  <store>
    @type stdout
  </store>
</match>

<source>

データの入力元

  • @type forward

  • port 24224

    • リッスンポート
  • bind 0.0.0.0

    • 入力を受け付ける送信元IP
    • 0.0.0.0なら全てのIPからの入力を受け付ける

<match>

データの解析・出力

  • 正規表現に合致するタグの入力に対して処理を行う
  • **.*ならピリオド1個を含むタグの入力ならどんなものでも処理する
  • ちなみにこのサンプルでsourceから渡されてくる入力のタグはhttpd.accessしかない(ここのタグ名は後述のlogdriverのオプションで設定している)

  • @type copy

    • 使用するfluentdの出力プラグインをここで指定する
    • @type copyout_copyプラグインを指定している
    • out_copyは出力先が複数ある場合に使う。最低でも一つの<store>ディレクティブを要する
    • 出力プラグインについてはここを参照

<store>

出力先の設定とか出力するデータのフォーマットを決める

  • @type elasticsearch

    • fluent-plugin-elasticsearchの使用
    • これはgem installで追加したプラグイン。fluentdに元から入ってない
  • host elasticsearch

  • port 9200

    • 出力先のポート
  • logstash_format true, logstash_prefix fluentd

    • elasticsearchに登録するドキュメントのindexの名前のプレフィックスをlogstashからfluentdに変更する
    • ちなみにデフォルトのindexはlogstash-2017.08.01みたいなものになるが、変更後はfluentd-2017.08.01になる
    • 「fluentd使うのにlogstash_◯◯なのか…」っていう違和感めっちゃ感じるけど大丈夫
  • logstash_dateformat %Y%m%d

    • indexの日付のフォーマットを変更する
    • logstash_prefix fluentdと組み合わせるとfluentd-20170801ってな感じの形式にできる
  • include_tag_key true, tag_key @log_name

    • tagkeyの追加と設定
    • サンプルではelasticsearchに登録されたドキュメントに"@log_name": "httpd.access"ってフィールドが追加されるはず
  • type_name access_log

    • ドキュメントのtypeの名前の設定
  • flush_interval 1s

    • 出力にどんだけの時間の間隔を空けるか

2つ目の<store>

elasticsearchの他に端末にもデータの出力を行うってわけ!

docker-compose.ymlのlogdriverについて

3行目〜

web:
    image: httpd
    ports:
      - "80:80"
    links:
      - fluentd
    logging:
      driver: "fluentd"
      options:
        fluentd-address: localhost:24224
        tag: httpd.access
  • fluentdログドライバを使用している
    • ここから送出されるログデータはin_forwardで受け取れる。ってことはデータの形式としてはout_forwardのものと同じ?
  • fluentd-address
    • fluentdのアドレスとポート
    • 当たり前なんだけどfluentdのポートはpublishしてやる必要がある
  • tag httpd.access
    • fluentdのsourceのタグ名を設定する
    • ちなみにこいつを設定しないとタグ名がhttpdのコンテナのコンテナIDになってしまう↓ f:id:tckw_aya:20170829024122p:plain

まとめ

elasticsearchいいよねfluentdいいよねkibanaいいよねEFKいいよね。

そういえばRancherのCommunity Catalogを見てる時にSematext Docker Agenetってやつを見つけたんですが、あいつもlogstashとかfluentdみたいなことができるんですかね。

面白そうなので使ってみたいんですがググっても日本語のドキュメントが皆無でつらい。

Dockerを0から勉強し直す(1) 「コンテナ技術とは?」

今日から「Dockerを0から勉強し直す」という記事をいくつか書いていきます。これはDockerを構成する技術とか内側の部分を調べてまとめていくものです。自分は今までDockerを使ってCI/CDのパイプラインとか、ちょっとしたWebアプリケーションの環境とかを構築してきましたが、Dockerを構成する部分や、そもそもDocker イメージとはどういうものなのかをちゃんと理解していませんでした。なんというか、このままではまずい気がしたので「0から」学び直したいと思ったわけです。

というわけで第1回は「コンテナ技術とは?」というテーマで書いていきます。(たぶん誤記があると思います)

目次

コンテナ技術の概要

Linuxのユーザーランド上では複数のプロセスを走らせることが可能です。たとえばWebサーバーやデータベースサーバーを一緒に稼働させることができます。ただし、同じLinux上で稼働するプロセスはOSレベルの環境設定については基本的にすべて共通になってしまいます。ディスク(ファイルやディレクトリ)、NICIPアドレス、プロセステーブルなども共通です。この場合、たとえば同じTCP80番ポートを使用するプロセスを複数作れないですし、2つのアプリケーションが必要とするライブラリのバージョンが異なる場合、バージョンによってディレクトリを分けてライブラリのファイルを配置してやる必要があり面倒です。

Linuxコンテナはこのような問題を解決します。Linuxカーネルのnamespace(名前空間)の機能により、Linux上で稼働するプロセスを複数のグループで分割して、それぞれのグループに異なる環境設定を適用できます。複数のユーザーランドを作成し、それぞれに異なるディレクトリの内容を見せたり、異なるネットワークの設定をすることが可能です。このように実行環境が分離されたユーザーランドを「コンテナ」と呼びます。コンテナではプロセステーブルもそれぞれ作られるので、コンテナの外で走っているプロセスを見ることができなくなります。*1

f:id:tckw_aya:20170829024248p:plain

http://www.school.ctc-g.co.jp/columns/nakai/nakai41.html

LXC(Linux Contianer)

コンテナ技術の歴史としてよく聞くのは「chrootFreeBSDのjail → LXC → Docker」という流れで発展していったという話です。Dockerのことを書く前にchrootやLXCとはどういうもなのかということと、LXCを構成するcgroupやnamespaceについて整理して書きます。

chroot

chrootは古くからUNIXのシステムに備わっているファイルシステムの隔離機能です。chrootの特徴は以下の通りです。

  • ディレクトリーツリーの分離を行い、プロセスのルートディレクトリを変更する
  • chrootすると指定したディレクトリ以下に閉じ込めることが出来る。その場合、指定されたディレクトリが/(ルートディレクトリ)として見えるため、それより上のディレクトリにアクセスすることができない
  • chrootはプロセスやネットワークなどの隔離はしないので、プロセスリストが見えるし、ユーザーIDやネットワークインターフェイスも共通になる
  • root権限で使える
  • プロセスのルートを一旦変更すると当該プロセスと(その子孫)は元のルートディレクトリに戻れなくなる

1998年からFreeBSDにはjailという仮想化機構があり、これはchrootの拡張版のようなものでファイルシステムだけでなくプロセス空間の隔離も行います。LXC(Linux Container)はさらなる拡張版とも言え、cgroup、namespace、chrootを使ってコンテナを作成します。

namespace

namespaceはプロセスのグループに対して6種のOSのリソースを分離させて割り当てるというものです。ここで言うリソースには以下のようなものがあります。

名前空間 分離対象
IPC System V IPC, POSIX メッセージキュー
Network ネットワークデバイス、スタック、ポートなど
Mount マウントポイント
PID プロセス ID
User ユーザー ID とグループ ID
UTS ホスト名と NIS ドメイン

namespaceを設定すると別の名前空間のリソースには干渉できなくなります。

cgroup

cgroupはプロセスをグループ化し、グループに対して物理的なリソース(CPU、メモリ、ディスク I/O、ネットワーク等)を制限・隔離・監視するものです。cgroupのリソースを制御するにはサブシステムというものを使います。サブシステムには以下のようなものがあります。

サブシステム 機能
blkio 物理ドライブ (例: ディスク、ソリッドステート、USB) などのブロックデバイスの入出力アクセスの制限を設定する
cpu スケジューラーを使用してcgroupのタスクに割り当てるCPU時間を制御する
freezer cgroupのタスクを一時停止または再開する
memory cgroupのタスクによって使用されるメモリーに対する制限を設定し、それらのタスクによって使用されるメモリーリソースについての自動レポートを生成する

LXC

LXCは環境(プロセスの集合)ごとにリソースを分ける仮想化ソフトウェアで、cgroupやnamespace、chrootなどの技術を使っています。環境毎にプロセスやファイルシステムの隔離だけでなく、CPUやメモリの制限も行えます。LXCの関連技術には、LXDというLXCコンテナを操作するためのAPIを提供するデーモンもあるようです。

Dockerとは

超簡単に言えばDockerはlibcontainerという独自のドライバを利用してcgroupやnamespaceなどの技術を間接的に使い、OSレベルで仮想化を行うソフトウェアです。Dockerはすこし前まではlxcやlibvirtというライブラリを使っていましたが、Docker 0.9からlibcontainerを使用するように変更されたようです。*2

ちなみにsystemd-nspawnはchrootの強化版みたいなものらしいです。

f:id:tckw_aya:20170819171726p:plain

libcontianer

Go言語のネイティブな実装であり、名前空間・cgroup・機能・ファイルシステムへのアクセス管理を持つコンテナを作成します。コンテナを作成後、コンテナに対してライフサイクル上の追加操作を可能にします。

Dockerの特徴

LXCでは実現できていない、Dockerの利点をいくつか書いていきます。

ポータビリティ

Dockerはポータビリティに優れており、簡単に言えば「Dockerさえインストールされていればどこでもアプリケーションコンテナを作成・実行可能」です。ユーザーが注意するべきなのはアプリケーションレベルの依存関係だけです。Dockerイメージにまとまったファイルシステムを使い、Docker上でコンテナを作動させるだけでサービスのデプロイは完了します。コンテナはホストの環境から独立しており共有するのはカーネルだけですが、たとえばWindowsであればDocker for WindowsMacであればDocker for Macがインストールされていれば「カーネルの違い」という問題は解決できます。

Dockerレジストリの存在

これの存在はかなり重要だと思います。たとえば私たちがdocker run centosを実行するとDocker HubというパブリックレジストリからCentOSのDockerイメージをpullし、簡単にコンテナを作成・実行することができます。Dockerレジストリリポジトリを作成し、イメージをpushすればあらゆる人々とイメージの共有ができます。

バージョン管理

DockerはDockerイメージに「タグ」をつけることでバージョンを分けることができます。そもそもDockerイメージはいくつもの層(ファイルやメタ情報)が重なったミルフィーユのような構造をしており、ここでいうバージョンの違いとは「積み重なっている層の違い」を意味します。DockerfileでDockerのイメージをビルドする際にレイヤのキャッシュが起こります。このDockerfileに何か命令を追加して新たにイメージをビルドする際には、このキャッシュを使って差分のレイヤを乗せるだけでできてしまいます。イメージ間で共通する層は共有できるので、ディスク容量の削減が可能です。docker tagによるロールバックも可能です。

Dockerをサポートする技術の多さ

有名なのだとDocker Composeがあります。これは複数のコンテナを使用するアプリケーション環境をdocker-compose.ymlというYAML形式のファイルに記述し、それを読み込むだけで環境のデプロイができるというものです。他にもk8sやSwarmといったオーケストレーションツールがあり、これらを使用することでコンテナを使ったクラスタリングも容易にできます。

ドキュメントの多さ

流行と共にDockerについて書かれたドキュメントがどんどん増えていっています。日本語のドキュメントもいっぱいあるのがいいですよね。

まとめ

コンテナとは「プロセスを実行するために隔離され、リソースが制限されたユーザーランド」です。そして、namespaceやcgroupっていう技術を用いてコンテナ化を行います。

コンテナ技術って聞くと「LXCかDocker」っていうところがあったんですが、FreeBSDのjalisや、OpenVZ、SolarisのZoneなんていうのもコンテナ技術の1種としてあるようです。

課題

ネットで1.5日かけて色々調べてこの記事を書いたんですが、曖昧な部分とか整理しきれてない部分が結構あります。これから勉強会などに出向いて、コンテナ技術や仮想化について詳しい方々に話を聞いて「コンテナ完全に理解した」と言えるようになりたいと思いました。この記事はこれから少しずつ編集・改良を加えていくつもりです。

参考

*1:Docker実践入門――Linuxコンテナ技術の基礎から応用まで」に書いてあることとほぼ同じことを書いてしまいましたがお許しください。この本は「Dockerの勉強をこれから始める」という方にオススメなので是非購入してみてください!

*2:DOCKER 0.9: INTRODUCING EXECUTION DRIVERS AND LIBCONTAINER

Docker + Kerberos認証入門

「Dockerを使ってKerberosの環境を作ろう!」という内容です。Kerberos認証のことは参考書でちょっと読んだ程度で全然わかってないので、手を動かしながら勉強したいなと思った次第です。

目次


環境

単一ホスト上に合計3つのDockerコンテナを立てます。それぞれサービス、クライアント、KDCの役割があります。クライアントがサービスにSSHで接続する時、Kerberos認証をかけ、ログインまでできるようにするのが到達目標です。

以下、今回作成するコンテナです。

  • サービス(ホスト名: myservice)
  • クライアント(ホスト名: myclient)
  • KDC(ホスト名: mykdc)

また全てのコンテナはcentos:7.3.1611のイメージを使用して作り、新たに定義したネットワークkrb_networkに繋ぎます。DNSは使用しません。ちなみにDocker for Macを使ってます。

コンテナの起動まで

Kerberos用のネットワークの定義

コンテナに静的IPを割り当てたかったんですけどDockerのデフォルトのブリッジネットワークではそれができないので、新たにネットワークを作ってそこでIPを割り当てることにしました。

$ docker network create --subnet=192.168.1.0/24 krb_network

コンテナ作成

$ docker run -itd --name service1 -e TZ=Asia/Tokyo --net=krb_network --ip=192.168.1.10 --hostname myservice.mydomain.com --add-host=myclient.mydomain.com:192.168.1.11 --add-host=mykdc.mydomain.com:192.168.1.12 centos:7.3.1611

$ docker run -itd --name client1 -e TZ=Asia/Tokyo --net=krb_network --ip=192.168.1.11 --hostname myclient.mydomain.com --add-host=myservice.mydomain.com:192.168.1.10 --add-host=mykdc.mydomain.com:192.168.1.12 centos:7.3.1611 

$ docker run -itd --name kdc1 -e TZ=Asia/Tokyo --net=krb_network --ip=192.168.1.12 --hostname mykdc.mydomain.com --add-host=myservice.mydomain.com:192.168.1.10 --add-host=myclient.mydomain.com:192.168.1.11 centos:7.3.1611

このdocker runコマンドでは以下のようなことをやっています。

タイムゾーンは別に変更しなくてもいいです。

確認

コンテナがちゃんと動いているか

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
27e8658076c9        centos:7.3.1611     "/bin/bash"         5 minutes ago       Up 5 minutes                            kdc1
ec5a5306a0c3        centos:7.3.1611     "/bin/bash"         5 minutes ago       Up 5 minutes                            client1
db937ce4d851        centos:7.3.1611     "/bin/bash"         5 minutes ago       Up 5 minutes                            service1

hostsファイルと時刻の確認 (以下はmykdcで確認した場合)

$ docker attach kdc1 
[root@mykdc /]# cat /etc/hosts
127.0.0.1   localhost
::1    localhost ip6-localhost ip6-loopback
fe00::0    ip6-localnet
ff00::0    ip6-mcastprefix
ff02::1    ip6-allnodes
ff02::2    ip6-allrouters
192.168.1.10    myservice.mydomain.com
192.168.1.11    myclient.mydomain.com
192.168.1.12    mykdc.mydomain.com mykdc
[root@mykdc /]# date
Mon Jul 17 10:03:45 JST 2017
[root@mykdc /]# 

Kerberos関係のパッケージのインストール

krb5-workstationのインストール

myserviceとmyclientで行います

[root@myclient /]# yum -y install krb5-workstation

krb5-serverのインストール

mykdcで行います

[root@mykdc /]#  yum -y install krb5-server

/etc/krb5.confの編集

全てのコンテナで編集し、以下のようにします。default_ccache_name = KEYRING:persistent:%{uid}コメントアウトも忘れずに。

/etc/krb5.conf

(省略)

[libdefaults]
 dns_lookup_realm = false
 ticket_lifetime = 24h
 renew_lifetime = 7d
 forwardable = true
 rdns = false
# default_realm = EXAMPLE.COM                                                                                                                                  
 default_realm = MYDOMAIN.COM
# default_ccache_name = KEYRING:persistent:%{uid}

[realms]
# EXAMPLE.COM = {                                                                                                                                              
#  kdc = kerberos.example.com                                                                                                                                  
#  admin_server = kerberos.example.com                                                                                                                         
# }                                                                                                                                                            

 MYDOMAIN.COM = {
  kdc = mykdc.mydomain.com
  admin_server = mykdc.mydomain.com
 }

[domain_realm]
# .example.com = EXAMPLE.COM
# example.com = EXAMPLE.COM
.mydomain.com = MYDOMAIN.COM
mydomain.com = MYDOMAIN.COM

KDCの設定

各種設定ファイルの編集

mykdcで/var/kerberos/krb5kdc/kdc.conf/var/kerberos/krb5kdc/kadm5.aclの編集をします。

/var/kerberos/krb5kdc/kdc.conf

[kdcdefaults]
 kdc_ports = 88
 kdc_tcp_ports = 88

[realms]
# EXAMPLE.COM = {                                                                                                                                              
  MYDOMAIN.COM = {
  #master_key_type = aes256-cts                                                                                                                                
  acl_file = /var/kerberos/krb5kdc/kadm5.acl
  dict_file = /usr/share/dict/words
  admin_keytab = /var/kerberos/krb5kdc/kadm5.keytab
  supported_enctypes = aes256-cts:normal aes128-cts:normal des3-hmac-sha1:normal arcfour-hmac:normal camellia256-cts:normal camellia128-cts:normal des-hmac-sh\
a1:normal des-cbc-md5:normal des-cbc-crc:normal
 }

/var/kerberos/krb5kdc/kadm5.acl

# */admin@EXAMPLE.COM     *
*/admin@MYDOMAIN.COM *  

Kerberosデータベースの作成

[root@mykdc /]# kdb5_util create -s
Loading random data
Initializing database '/var/kerberos/krb5kdc/principal' for realm 'MYDOMAIN.COM',
master key name 'K/M@MYDOMAIN.COM'
You will be prompted for the database Master Password.
It is important that you NOT FORGET this password.
Enter KDC database master key: 
Re-enter KDC database master key to verify: 

root/adminプリンシパル作成

[root@mykdc /]# kadmin.local
Authenticating as principal root/admin@MYDOMAIN.COM with password.
kadmin.local:  addprinc  root/admin
WARNING: no policy specified for root/admin@MYDOMAIN.COM; defaulting to no policy
Enter password for principal "root/admin@MYDOMAIN.COM": 
Re-enter password for principal "root/admin@MYDOMAIN.COM": 
Principal "root/admin@MYDOMAIN.COM" created.
kadmin.local:  exit
[root@mykdc /]# 

KDC起動

ではKDC管理サーバーとKDCサーバーを起動しましょう

[root@mykdc /]# kadmind
[root@mykdc /]# krb5kdc

出力が何もなくてめっちゃ不安になったので、プロセスが動いているか、何のポートが開いているかを確認しました。

ssnetstatも使えなかったので、とりあえず追加でnet-toolsをインストールしました。

[root@mykdc /]# ps ax
  PID TTY      STAT   TIME COMMAND
    1 pts/0    Ss     0:00 /bin/bash
   85 ?        Ss     0:00 kadmind
   87 ?        Ss     0:00 krb5kdc
   95 pts/0    R+     0:00 ps ax
[root@mykdc /]# netstat -atun
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:88              0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.11:41085        0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:749             0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:464             0.0.0.0:*               LISTEN     
tcp6       0      0 :::88                   :::*                    LISTEN     
tcp6       0      0 :::749                  :::*                    LISTEN     
tcp6       0      0 :::464                  :::*                    LISTEN     
udp        0      0 0.0.0.0:88              0.0.0.0:*                          
udp        0      0 127.0.0.11:50525        0.0.0.0:*                          
udp        0      0 0.0.0.0:464             0.0.0.0:*                          
udp6       0      0 :::88                   :::*                               
udp6       0      0 :::464                  :::*   

OKですね。

サービスプリンシパルの作成

mysericeのコンテナで設定します。

[root@myservice /]# kadmin -p root/admin
Authenticating as principal root/admin with password.
Password for root/admin@MYDOMAIN.COM: 
kadmin:  addprinc -randkey host/myservice.mydomain.com
WARNING: no policy specified for host/myservice.mydomain.com@MYDOMAIN.COM; defaulting to no policy
Principal "host/myservice.mydomain.com@MYDOMAIN.COM" created.
kadmin:  ktadd host/myservice.mydomain.com
Entry for principal host/myservice.mydomain.com with kvno 2, encryption type aes256-cts-hmac-sha1-96 added to keytab FILE:/etc/krb5.keytab.
Entry for principal host/myservice.mydomain.com with kvno 2, encryption type aes128-cts-hmac-sha1-96 added to keytab FILE:/etc/krb5.keytab.
Entry for principal host/myservice.mydomain.com with kvno 2, encryption type des3-cbc-sha1 added to keytab FILE:/etc/krb5.keytab.
Entry for principal host/myservice.mydomain.com with kvno 2, encryption type arcfour-hmac added to keytab FILE:/etc/krb5.keytab.
Entry for principal host/myservice.mydomain.com with kvno 2, encryption type camellia256-cts-cmac added to keytab FILE:/etc/krb5.keytab.
Entry for principal host/myservice.mydomain.com with kvno 2, encryption type camellia128-cts-cmac added to keytab FILE:/etc/krb5.keytab.
Entry for principal host/myservice.mydomain.com with kvno 2, encryption type des-hmac-sha1 added to keytab FILE:/etc/krb5.keytab.
Entry for principal host/myservice.mydomain.com with kvno 2, encryption type des-cbc-md5 added to keytab FILE:/etc/krb5.keytab.
kadmin:  exit

klistでKerberos のキーテーブルを確認します。

[root@myservice /]# klist -e -k /etc/krb5.keytab 
Keytab name: FILE:/etc/krb5.keytab
KVNO Principal
---- --------------------------------------------------------------------------
   2 host/myservice.mydomain.com@MYDOMAIN.COM (aes256-cts-hmac-sha1-96) 
   2 host/myservice.mydomain.com@MYDOMAIN.COM (aes128-cts-hmac-sha1-96) 
   2 host/myservice.mydomain.com@MYDOMAIN.COM (des3-cbc-sha1) 
   2 host/myservice.mydomain.com@MYDOMAIN.COM (arcfour-hmac) 
   2 host/myservice.mydomain.com@MYDOMAIN.COM (camellia256-cts-cmac) 
   2 host/myservice.mydomain.com@MYDOMAIN.COM (camellia128-cts-cmac) 
   2 host/myservice.mydomain.com@MYDOMAIN.COM (des-hmac-sha1) 
   2 host/myservice.mydomain.com@MYDOMAIN.COM (des-cbc-md5) 

ユーザープリンシパルの作成

どこかのコンテナ内で以下のコマンドを実行します。

[root@myclient /]# kadmin -p root/admin
Authenticating as principal root/admin with password.
Password for root/admin@MYDOMAIN.COM: 
kadmin:  addprinc user1
WARNING: no policy specified for user1@MYDOMAIN.COM; defaulting to no policy
Enter password for principal "user1@MYDOMAIN.COM": 
Re-enter password for principal "user1@MYDOMAIN.COM": 
Principal "user1@MYDOMAIN.COM" created.
kadmin:  exit
[root@myclient /]# 

TGTの取得

myclientで行います。kinitコマンドでTGT(Ticket Granting Ticket)を取得します

[root@myclient /]# kinit user1
Password for user1@MYDOMAIN.COM: 
[root@myclient /]# 

klistでチケットを表示してみます。

[root@myclient /]# klist
Ticket cache: FILE:/tmp/krb5cc_0
Default principal: user1@MYDOMAIN.COM

Valid starting     Expires            Service principal
07/17/17 11:08:28  07/18/17 11:08:28  krbtgt/MYDOMAIN.COM@MYDOMAIN.COM

SSH接続まで

ログイン用ユーザー追加

myserviceでsshでログインするユーザーを作成します。ここで作成するユーザーはさっき作成したユーザープリンシパルと同じ名前にしてください。

 [root@myservice /]# adduser user1  

SSHサーバーのインストール

openssh-serverをインストールします。

[root@myservice /]# yum -y install openssh-server

このままsshdを実行するとCould not load host keyのエラーが発生するので鍵を作ります。

[root@myservice /]# ssh-keygen -A
ssh-keygen: generating new host keys: RSA1 RSA DSA ECDSA ED25519 

次に/etc/ssh/sshd_configを編集します。

/etc/ssh/sshd_config

(省略)
# Kerberos options
KerberosAuthentication yes
#KerberosOrLocalPasswd yes
KerberosTicketCleanup yes
#KerberosGetAFSToken no
#KerberosUseKuserok yes

# GSSAPI options
GSSAPIAuthentication yes
GSSAPICleanupCredentials yes
#GSSAPIStrictAcceptorCheck yes
#GSSAPIKeyExchange no
#GSSAPIEnablek5users no
(省略)

sshdを実行してサーバーを立てます。

[root@myserver /]# /usr/sbin/sshd

SSHクライアントのインストール

myclientで行います

[root@myclient /]# yum -y install openssh-clients

SSH接続

オプションで-o GSSAPIAuthentication=yesをつけてsshを実行します(オプションつけなくてもログインできるかも)

[root@myclient /]# ssh -o GSSAPIAuthentication=yes user1@myservice.mydomain.com
Last login: Mon Jul 17 11:09:37 2017 from myclient.mydomain.com
[user1@myservice ~]$ cat /etc/hostname
myservice.mydomain.com
[user1@myservice ~]$ 

ログインに成功しました〜!再度チケットを表示してみましょう。

[root@myclient /]# klist
Ticket cache: FILE:/tmp/krb5cc_0
Default principal: user1@MYDOMAIN.COM

Valid starting     Expires            Service principal
07/17/17 11:08:28  07/18/17 11:08:28  krbtgt/MYDOMAIN.COM@MYDOMAIN.COM
07/17/17 11:09:37  07/18/17 11:08:28  host/myservice.mydomain.com@MYDOMAIN.COM
[root@myclient /]# 

新たなチケットが追加されています。

これでDockerを使ったKerberos環境の構築を終わりたいと思います。お疲れ様でした。

参考