Anket の Docker Image 作成を Packer + Ansible から Dockerfile にした

Docker Image を作る!!
といえば Dockerfile から作るケースが多いのではないでしょうか。
しかし Anket は Packer + Ansible を使って Docker Image を作っていました。

理由として元々 Anket は EC2(AmazonLinux) 上に構築していた歴史があってその時に AMI を Packer + Ansible で作っていました。
それから ECS に移行するにあたって Docker Image を作る必要があり、 AMI を作っていた時の資産を使うためにベースのイメージを centos にして作成しました。

課題

Packer + Ansible を使って centos ベースでイメージを使った時の課題は二つありました。

  • Image が大きい
  • Image を作成するのが遅い

Image が大きい

base の centos が 202MB くらいで Rails App の場合はそこから Ansible を使ってあれこれ入れるうちに 1G くらいになりました。

Image を作成するのが遅い

ビルド時間は 18m30s でした。
遅い。。。

image が大きい分には必要のないパッケージなどを最後に削除するなどしてスリムにしていくことができるかもしれません。
ただビルド時間はちょっと工夫する必要がありそうだったのでまずはここから着手しました。

どうやって早くするか

必要のないパッケージを確認したりソースからインストールしているものをパッケージマネージャーからインストールすることで早くはできそうですが、そこそこ playbook をいじる必要があるので、そこまでするなら Dockerfile から作りなおすかなぁと思ったので、別のアプローチを考えます。

最初に思いついたのが Ansible で任意のタスクを非同期で処理する方法でした。
docs.ansible.com

まず Ansible のディレクトリ構成はこんな感じになっています。

├── anket-app.yml
├── roles
│   ├── mysql
│   │   └── tasks
│   │       └── main.yml
│   ├── node
│   │   └── tasks
│   │       └── main.yml
│   ├── ruby
│   │   └── tasks
│   │       └── main.yml
└── vars
    ├── anket-app.yml

playbook はこんな感じになっています。

- hosts: all
  vars_files:
    - vars/anket-app.yml
  roles:
    - mysql
    - node
    ~~~
    - ruby

Ansible の async を使う場合には role に対して指定したいところですが、 role には指定できないようで個々のタスクに設定する必要がありそうでした。
role 内のタスクに async を設定していくのは骨が折れそうです。

次に思いついたのが playbook を並列で指定することを考えました。
まずは playbook で指定している role に対してタグを指定します。

- hosts: all
  vars_files:
    - vars/anket-app.yml
  roles:
    - mysql
      tags:
        - p1
    - node
      tags:
        - p1
    - ruby
      tags:
        - p2

通常は ansible-playbook playbook.yml のように指定しますが、これを

$ echo 'p1,p2' |  tr ',' '\n' | xargs -P 2 -I '{}' ansible-playbook playbook.yml --tags={}

のようにしています。
これにより tags で p1 を指定した playbook と p2 を指定した playbook が同時に実行されます。
これで結構早くなるのではと意気揚々と build します。

Another app is currently holding the yum lock; waiting for it to exit...

おっ。。。。
どうやら並列で playbook を指定した際にちょうど同じタイミングで yum install が入った際には先に入ったほうは問題ないのですが、 2番目に開始されたほうは1番目がinstall中なのでエラーを返してしまいます。

yum module には Ansible 2.8 から lock_timeout を指定することができて、これを設定すると指定された秒数分 lock をまってくれます。
これでエラーはなくなりました。

しかし実行結果は 17m21s となり最初よりも1分ほど早くなりました。
片方の yum install が終わるまでまったりしているので結局直列になるところが出てしまいそこまで改善できませんでした。

最終的に

Packer + Ansible を捨てて Dockerfile で作成しました。

理由は base image で Ruby の Image を使ったり multie stage build を行って必要なファイルだけ COPY することによってイメージサイズの削減しビルド時間も短縮できそうです。
Dockerfile はレイヤごとに?キャッシュしているので、一度実行されたものなどは変更などがない限り次 biuld した時はキャッシュを使用してくれるので1回目よりも早くビルドができるようになってるはずです。

Dockerfile はこんな感じになりました。
せっかく0から作るのでbase imageも alpine base を使いました。

FROM node:10.16.0-alpine as node

RUN apk --update --no-cache add yarn

FROM ruby:2.6.3-alpine3.9 as builder

ARG env
ARG secret_key_base

ENV RAILS_ENV ${env}
ENV NODE_ENV ${env}
ENV SECRET_KEY_BASE ${secret_key_base}

WORKDIR /app

RUN apk --update --no-cache add build-base mariadb-dev yarn tzdata

COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/include/node /usr/local/include/node
COPY --from=node /usr/bin/yarn /usr/bin/yarn

COPY ./src/anket-app/Gemfile ./src/anket-app/Gemfile.lock /app/
COPY ./src/anket-app/package.json ./src/anket-app/yarn.lock /app/

RUN bundle install --path vendor/bundle -j4 && \
  yarn install

COPY ./src/anket-app /app

RUN bundle exec rails assets:precompile

FROM ruby:2.6.3-alpine3.9

WORKDIR /app

RUN apk --update --no-cache add tzdata mariadb-dev

COPY --from=builder /app /app
COPY --from=builder /usr/local/bundle /usr/local/bundle

COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/include/node /usr/local/include/node

RUN addgroup anket && adduser -D -G anket anket
USER anket

ENTRYPOINT ["bundle", "exec"]
CMD ["puma"]

ポイントとかは特にないです。

実際にビルドしてみましたが初回は 7m33s
コードだけ変更があった時などは assets:precompile がはしって 1m17s

めっちゃ早くなった!!!!

きになる Docker Image のサイズは 478MB になりました。
最初が 1G なので半分くらいになりました!!

最後に

Dockerfile にする時に base の image で Ruby を使っていて Ansible の時はソースからいれていたので、時間とかは比較にならないかもしれないなぁと思いました。
ただ Dockerfile を使った場合はキャッシュがきいて 2回目移行はシュッと終わってくれるので、それだけでも移行したかいがあったかなぁと思いたい。