読者です 読者をやめる 読者になる 読者になる

パラボラアンテナと星の日記

あることないこと

細かすぎて伝わらないかもしれない、CircleCIでDockerをごにょごにょするときのスピードアップテク

前回、「Dockerコンテナにcookしserverspecでテストをする」ということをやりました。 キャッシュ活用などによる高速化などが課題でした。今回はいくつかの課題を解決させ、テスト時間の短縮を図りました。

(「スピードアップテク」とか書きましたが、勝手に自分がハマってたところを改善したりしてるだけの箇所もあります。)

先に結論:対策後のビルド時間

対策前(build#9) docker imageキャッシュ有効時(build#55) docker imageキャッシュ無効時(build#54)
load docker image 1m41s 0m20s 1m00s
knife solo cook(nginxのインストール) 2m56s 0m12s 0m21s
(other) (1m03s) (0m41s) (1m06s)
TOTAL 5m40s 1m13s(!!!!!) 2m27s

結構早くなりました(5m40s -> 1m13s)!! 大勝利の予感!!!!

実施した5個の細かい対策

  1. chefをdocker imageに含める
  2. docker imageのキャッシュ
  3. gemのキャッシュ
  4. docker imageのレイヤー数を最小限にする
  5. docker runでホスト側に空けたポートを利用しない

それぞれ説明していきます

1. chefをdocker imageに含める (差分)

もともとknife solo bootstrap(prepare + cook)でコンテナ側にchefをインストールしていたのですが、docker imageにchefも含めるように修正しました。

これにより、docker imageをキャッシュする場合、chefのインストールがスキップできるので、多少早くなります。

Dockerfile

  • rsyncとchefを入れています。rsyncも実はknife solo prepare時に無ければインストールされるっぽい
diff --git a/Dockerfile b/Dockerfile
index 57936bb..05cdedb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,7 +3,7 @@
 FROM centos:6.6
 MAINTAINER Tsuyoshi HOSHINO
 RUN yum update -y
-RUN yum -y install openssh-server sudo
+RUN yum -y install openssh-server sudo rsync
 
 ## Create user
 RUN useradd docker; passwd -f -u docker
@@ -23,6 +23,11 @@ RUN sed -ri 's/#PermitRootLogin yes/PermitRootLogin yes/g' /etc/ssh/sshd_config
 RUN sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config
 RUN sed -ri 's/#UsePAM no/UsePAM no/g' /etc/ssh/sshd_config
 
+## Prepare chef
+RUN cd /tmp && \
+    curl -LO https://opscode-omnibus-packages.s3.amazonaws.com/el/6/x86_64/chef-12.0.3-1.x86_64.rpm && \
+    rpm -ivh ./chef-12.0.3-1.x86_64.rpm
+
 ## Init SSHD
 RUN /etc/init.d/sshd start
 RUN /etc/init.d/sshd stop

circle.yml

  • prepareは行わず、cookだけにします
diff --git a/circle.yml b/circle.yml
index 816ffb9..a7b4863 100644
--- a/circle.yml
+++ b/circle.yml
@@ -16,7 +16,7 @@ test:
     - cp ./ssh-config.circleci ~/.ssh/config
   override:
     - docker run -d -p 40022:22 hoshinotsuyoshi/centos-sshd; sleep 10
-    - bundle exec knife solo bootstrap centos-sshd:
+    - bundle exec knife solo cook centos-sshd:
         timeout: 900
     - bundle exec rake:
         timeout: 900

2. docker imageのキャッシュ (差分)

docker imageの実体は/var/lib/dockerの下とかにあると思うのですが、これをキャッシュするのは試さず、docker savedocker loadを使ってキャッシュしてみました。

docker savedocker loadは、それぞれimageをtarに固める・tarからimageを展開するコマンドです。使い方はこんな感じ。

$ docker save [IMAGE名] > image.tar
$ docker load < image.tar

Dockerfileに変更があった場合はキャッシュimageを利用せずに再buildしてほしいので、直近のDockerfileのダイジェスト値も保存しておき、docker imageを再buildするかどうかを判定するようにしました。(何かを再発明してる感がすごいですが気にしない)

DockerhubのAutomated buildも良いのですが、pullにかかる時間と、Dockerfileに変更があった場合に結局すぐビルドできないということがあるので、今回は見送りました。

circle.yml と docker buildのスクリプト

こんな感じ。

diff --git a/circle.yml b/circle.yml
index a7b4863..d73eecf 100644
--- a/circle.yml
+++ b/circle.yml
@@ -3,11 +3,11 @@ machine:
     Asia/Tokyo
   services:
     - docker
-
 dependencies:
+  cache_directories:
+    - "~/cache"
   override:
-    - docker info
-    - docker build -t hoshinotsuyoshi/centos-sshd .
+    - ./docker-build.sh
 test:
   pre:
     - bundle -j4 --path=vendor/bundle
diff --git a/docker-build.sh b/docker-build.sh
new file mode 100755
index 0000000..1f00b75
--- /dev/null
+++ b/docker-build.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+set -xe
+
+docker info
+
+if [ -e ~/cache/centos-sshd.tar ] && [ $(md5sum Dockerfile | cut -d' ' -f1) = $(cat ~/cache/dockerfile.digest) ]
+then
+  docker load < ~/cache/centos-sshd.tar
+else
+  mkdir -p ~/cache
+  docker build -t hoshinotsuyoshi/centos-sshd .
+  md5sum Dockerfile | cut -d' ' -f1 > ~/cache/dockerfile.digest
+  docker save hoshinotsuyoshi/centos-sshd > ~/cache/centos-sshd.tar
+fi

3. gemのキャッシュ (差分)

これはそのまんまですがvendor/bundleをキャッシュするようにしました。これでbundleにかかってた時間16秒ほどが不要になる。

circle.yml

diff --git a/circle.yml b/circle.yml
index d73eecf..26df530 100644
--- a/circle.yml
+++ b/circle.yml
@@ -6,11 +6,12 @@ machine:
 dependencies:
   cache_directories:
     - "~/cache"
+    - "vendor/bundle"
   override:
     - ./docker-build.sh
+    - bundle -j4 --path=vendor/bundle
 test:
   pre:
-    - bundle -j4 --path=vendor/bundle
     - cp ./id_rsa_insecure ~/.ssh/id_rsa
     - sudo chown 600 ~/.ssh/id_rsa
     - cp ./ssh-config.circleci ~/.ssh/config

4. docker imageのレイヤー数を最小限にする (差分)

「RUNを1つにまとめたりしてDockerfileでの命令の数を少なくするとビルドが早くなる」のは直感として知っていたのですが、 なんでもイメージサイズも小さくなるそうです。

極端かもしれませんがこれも限界までやってしまいます。&&を多用してRUNを1つにまとめます。これでdocker buildも早くなりましたし、docker loadも20秒以内に終わるようになりました。

before

つまりはこういうDockerfileの

# Dockerfile
# inspired from http://www.nerdstacks.net/2014/03/ssh-ready-centos-dockerfile/

FROM centos:6.6
MAINTAINER Tsuyoshi HOSHINO
RUN yum update -y
RUN yum -y install openssh-server sudo rsync

## Create user
RUN useradd docker; passwd -f -u docker

## Set up SSH
RUN mkdir -p /home/docker/.ssh; chown docker /home/docker/.ssh; chmod 700 /home/docker/.ssh
ADD id_rsa_insecure.pub /home/docker/.ssh/authorized_keys

RUN chown docker /home/docker/.ssh/authorized_keys
RUN chmod 600 /home/docker/.ssh/authorized_keys

## setup sudoers
RUN echo "docker ALL=(ALL) ALL" >> /etc/sudoers.d/docker

## Set up SSHD config
RUN sed -ri 's/#PermitRootLogin yes/PermitRootLogin yes/g' /etc/ssh/sshd_config
RUN sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config
RUN sed -ri 's/#UsePAM no/UsePAM no/g' /etc/ssh/sshd_config

## Prepare chef
RUN cd /tmp && \
    curl -LO https://opscode-omnibus-packages.s3.amazonaws.com/el/6/x86_64/chef-12.0.3-1.x86_64.rpm && \
    rpm -ivh ./chef-12.0.3-1.x86_64.rpm

## Init SSHD
RUN /etc/init.d/sshd start
RUN /etc/init.d/sshd stop

CMD /usr/sbin/sshd -D

after

RUNは1回にしてしまえ! という感じです

# Dockerfile
# inspired from http://www.nerdstacks.net/2014/03/ssh-ready-centos-dockerfile/

FROM centos:6.6
MAINTAINER Tsuyoshi HOSHINO
RUN yum update -y && \
    yum -y install openssh-server sudo rsync && \
    yum clean all && \
    useradd docker && \
    passwd -f -u docker && \
    mkdir -p /home/docker/.ssh && \
    chown docker /home/docker/.ssh && \
    chmod 700 /home/docker/.ssh && \
    echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQTEOxQRimMlKlE67ofwnbLsvaNPO7VydzSQIzrFTDxa3wH5wPxLymA9zqpXiVAymZIaWOUiFP5+TKoMSBpaaQirv9fbstB7CMH/NYLA5ureJvuII91RkSKVKQn0ddHBtqbDFvHXCFsdoh/gNCN0/6joKZQjQFiCCT9jjwwkroK7NPnUlmLv+g6M02orCwEohd4CBqSKzKcvJ7+rrQlPPQlRVyLUrXiYcHutIphxcU5Ls04cGsXhcMu/LmNzfM7RaFee/02JodZ14QWDHlw8KxTdzqBP6rod4G3319aFdJ8t2SBCGqVUGP0uPpKxGkt2CKZCc43qNPPVZVCJtacBxX berlin@berlin.local' >> /home/docker/.ssh/authorized_keys && \
    chown docker /home/docker/.ssh/authorized_keys && \
    chmod 600 /home/docker/.ssh/authorized_keys && \
    echo "docker ALL=(ALL) ALL" >> /etc/sudoers.d/docker && \
    sed -ri 's/#PermitRootLogin yes/PermitRootLogin yes/g' /etc/ssh/sshd_config && \
    sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config && \
    sed -ri 's/#UsePAM no/UsePAM no/g' /etc/ssh/sshd_config && \
    cd /tmp && \
    curl -LO https://opscode-omnibus-packages.s3.amazonaws.com/el/6/x86_64/chef-12.0.3-1.x86_64.rpm && \
    rpm -ivh ./chef-12.0.3-1.x86_64.rpm && \
    /etc/init.d/sshd start && \
    /etc/init.d/sshd stop

CMD /usr/sbin/sshd -D

Dockerfileを育てるのが目的ではないので、こんなノリでもいいはずです。

5. docker runでホスト側に空けたポートを利用しない (差分)

これはうまく説明できないし、もともとハマっていただけ?、という話なのですが、もともとdocker runするときに

$ docker run -d -p 40022:22

みたいにしてホスト側のポート(40022)も指定し、cookするときは「localhostのポート40022にsshする」という感じにしていました。

理由は、「dockerコンテナのIPアドレスを調べるのがめんどいから」ホスト側のポートで固定してしまおう、という思いからでした。

しかしどうやら、、これのせいで、sshがめっちゃ遅い」という謎の事態に陥っていたようでした。

で、やめたところ、遅さが解消されました。cook(nginxのインストールだけ)に2分以上かかってたのが10秒くらいになりました。

「dockerコンテナのIPアドレスを調べるのがめんどい」に関しては、しぶしぶ対応します。docker run後にdocker inspectIPアドレスを調べ、.ssh/configに書き込むようにしました。

docker inspectJSONを吐くのですが、rubyで適当にパースして欲しいもの(IPアドレス)だけ取り出します。 こんな感じでいけます。

$ ruby -rjson -e 'puts JSON.parse(%x(docker inspect [コンテナ名])).first["NetworkSettings"]["IPAddress"]'
#=> 172.x.x.x # 的なIPアドレスが返るはず

最終的に以下の様な感じになりました。

circle.yml

diff --git a/circle.yml b/circle.yml
index 26df530..9f08ab6 100644
--- a/circle.yml
+++ b/circle.yml
@@ -16,7 +16,8 @@ test:
     - sudo chown 600 ~/.ssh/id_rsa
     - cp ./ssh-config.circleci ~/.ssh/config
   override:
-    - docker run -d -p 40022:22 hoshinotsuyoshi/centos-sshd; sleep 10
+    - docker run -d -p 22 --name sshd hoshinotsuyoshi/centos-sshd; sleep 10
+    - host=$(ruby -rjson -e 'puts JSON.parse(%x(docker inspect sshd)).first["NetworkSettings"]["IPAddress"]'); echo '  HostName '$host >> ~/.ssh/config
     - bundle exec knife solo cook centos-sshd:
         timeout: 900
     - bundle exec rake:
diff --git a/ssh-config.circleci b/ssh-config.circleci
index f0b5396..8ee4139 100644
--- a/ssh-config.circleci
+++ b/ssh-config.circleci
@@ -1,7 +1,5 @@
 Host centos-sshd
-  HostName 0.0.0.0
   User docker
-  Port 40022
   UserKnownHostsFile /dev/null
   StrictHostKeyChecking no
   PasswordAuthentication no

(これほんと説明面倒でうまく説明できない)

以上です

まだ本格的なcookbookで試してないので、まだまだ壁はあるかもしれませんが、実用度はだいぶ増してきた気がします!