Edited at

コンテナイメージビルドが遅いなら、Kaniko使うと幸せになれる(全部入りRails Dockerfileを参考に)


結論


  • BuildKit対応してないCI/CD(サービス)使ってるならKaniko使えば早くなるよ

  • Google Cloud Build使ってるなら迷わず使っておくべき

  • Amazon Elastic Container Registry(ECR)でも問題なく使用できる

  • この記事の例でいうと未キャッシュからフルキャッシュなら 288秒→132秒に短縮(約2倍)


御託はいいのでどう書くか教えて

ここに書いてますので、自分の使ってるCI/CDサービスに当てはまるのを選んで参考にどうぞ

https://github.com/chimame/kaniko-example


本題

長いうんちくや説明を知りたい人向けです。

この記事で記載するのはAmazon Elastic Container Registry(ECR)を対象として記載です。


Kanikoって何?

Dockerデーモンを使用せずにコンテナイメージをビルドできるようになるものです。

GoogleがOSSとして公開し、開発されています。

コンテナイメージをビルドするのによくDocker in Docker(DinD)でビルドしますが、それをしなくてもできるようになります。


なんで使うの?

特に大きな利点が


  • 中間レイヤー単位でキャッシュする

これが非常に大きな利点をもたらします。

コンテナ運用で問題になるのが ビルド時間 です。キャッシュを何も考えないでビルドするとものすごく時間がかかります。(ビルドだけで平気で30分とか)

CI/CDの時間が長くなってしまうとリリースサイクルに影響が出てしまうので、時間を短縮する必要があります。

普通のコンテナイメージビルドでは以下のような問題がありました。


  1. Dockerfileを上から順(シングルタスク)で動作する

  2. 途中の行で変更があると以降はキャッシュが使用されず、再実行される

これを解決するため(特に1)に現在は BuildKit が導入され並列に処理できる状態となっていますが、2のキャッシュについてはまだまだ考える必要はあります。(某NTTの方はここの戦略を直近で発表されております)

さらに言うとCI/CDサービスが色々存在しますが、BuildKitが使えないということもまだ珍しくありません。

そこで今回はコンテナイメージビルドのキャッシュに有効な Kaniko を使用してコンテナイメージビルドを行います。

ただし、Google Cloud Platform(正確にはGoogle Cloud Build)ではこのKanikoを使う設定は難しくなく、公式に使用例がありますので、こちらを参考に使用するといいです。


使用方法

ここから具体的な使用方法を記載していきます。

まず最初にRailsを対象としたDockerfile例を以下に記載します。(webpacker使ってるのはこの記事書くのに手を抜いただけで、できるならwebpackの使用をオススメします)

# build JavaScript for webpack

FROM node:10.14.2-alpine AS build-webpack

RUN mkdir /app
WORKDIR /app

ENV NODE_ENV production

COPY package.json /app/package.json
COPY yarn.lock /app/yarn.lock
RUN yarn install

## webpack build
COPY ./app/javascript /app/app/javascript
COPY ./config/webpack /app/config/webpack
COPY ./config/webpacker.yml /app/config/webpacker.yml
COPY ./.browserslistrc /app/.browserslistrc
COPY ./babel.config.js /app/babel.config.js
COPY ./postcss.config.js /app/postcss.config.js
RUN yarn run webpack --config config/webpack/${NODE_ENV}.js

# install ruby gems
FROM ruby:2.6.3-alpine AS build-asset

RUN apk add --no-cache tzdata sqlite-dev && \
cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

RUN mkdir /app
WORKDIR /app

ARG BUNDLE_OPTIONS

ENV LANG C.UTF-8
ENV RAILS_ENV production
ENV WEBPACKER_PRECOMPILE=false

COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN apk add --no-cache --virtual .rails-builddeps alpine-sdk && \
bundle install -j4 --path vendor/bundle ${BUNDLE_OPTIONS} && \
apk del .rails-builddeps

## copy npm packages
COPY --from=build-webpack /app/node_modules /app/node_modules

## asset build
COPY ./app/assets /app/app/assets
COPY ./config/environments /app/config/environments
COPY ./config/initializers/assets.rb /app/config/initializers/assets.rb
COPY ./config/application.rb /app/config/application.rb
COPY ./config/boot.rb /app/config/boot.rb
COPY ./config/cable.yml /app/config/cable.yml
COPY ./config/credentials.yml.enc /app/config/credentials.yml.enc
COPY ./config/database.yml /app/config/database.yml
COPY ./config/environment.rb /app/config/environment.rb
COPY ./config/master.key /app/config/master.key
COPY ./config/webpacker.yml /app/config/webpacker.yml
COPY ./lib /app/lib
COPY ./config.ru /app/config.ru
COPY ./Rakefile /app/Rakefile
RUN bundle exec rake assets:precompile

# exec docker image
FROM ruby:2.6.3-alpine

RUN apk add --no-cache tzdata sqlite-dev && \
cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

ENV LANG C.UTF-8
ENV RAILS_ENV production

RUN mkdir /app
WORKDIR /app

COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
COPY --from=build-asset /app/vendor/bundle /app/vendor/bundle
RUN bundle install -j4 --path vendor/bundle ${BUNDLE_OPTIONS}

COPY --from=build-asset /app/public /app/public
COPY --from=build-webpack /app/public /app/public

COPY . /app

EXPOSE 3000
CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"]

何を書いているのかわからないと困るで少しだけ解説します。

「ステージは全部で3つ」

FROMの数を見れば一目瞭然なんですが、ステージが3つあり、各々やっていることは


  1. npm packageのインストールおよびwebpack(er)を使用してのビルド

  2. gemのインストール、1のnpm packageのコピー、assets:precompileを使用してのビルド

  3. 1のgem、1のビルド結果、2のビルド結果を取得して最終の実行コンテナイメージを作成

Railsにフロントまで含めてるのはどうなの?という議論はさて置いておいて、3つに分けている理由は以下です。


  1. webpackのビルドはインストールするnpm packageとビルド対象ファイルに影響するため

  2. assets:precompileはインストールするgemおよびnpm packageとビルド対象ファイルに影響するため

要は何に影響されるかということでこのような形になっています。

仮に使用するgemのバージョン変更や追加があったとしてもwebpackのビルドには影響がないはずです。そのような場合にはwebpackのビルドはキャッシュが効いてほしいためにこのような構成になります。

このようなステージ構成にして、Multi Stage Buildを実行すると影響ない部分はキャッシュが使用されます。ですが、gemの追加して普通にdocker buildをすると、その行以降はキャッシュが効かなくなります。

これに開発環境用のステージも追加となると…😭

(そもそもassets:precompileとwebpack両方必要ってどうなの?って感じですが、あくまで例ですので)

Kanikoではdocker buildとは違い、BuildKit同様に影響対象さえ変わらなければキャッシュが適用されます。それを実現しているのが記載した 中間レイヤー単位でキャッシュする ということで実現しているわけです。


Circle CI

まず最初にCircle CIでの使用例を記載します。まずCircle CIではimageを指定して使用できるのですが、Kanikoのイメージでは以下の理由により使用できず、やむえずdockerにて実行しています。


  1. KanikoのコンテナイメージはCMDが設定されていない

  2. Circl CIのCMD使用には /bin からのものしか実行できない

逆にもっとスマートに使用できたって方は編集リクエストなりコメントなりくれると嬉しいです。

version: 2

jobs:
build:
branches:
only:
- master
machine: true
steps:
- checkout
- run:
name: set up
command: |
docker pull gcr.io/kaniko-project/executor:debug
echo "$RAILS_MASTER_KEY" > ./config/master.key
echo "{\"credHelpers\":{\"${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com\":\"ecr-login\"}}" > /tmp/config.json
- run:
command: |
docker run -it -v /tmp/config.json:/kaniko/.docker/config.json -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY -v $(pwd):/build gcr.io/kaniko-project/executor:debug --cache=true --context /build --dockerfile /build/Dockerfile --destination ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/kaniko-example:$CIRCLE_SHA1

実行しているのは結構単純です。


  1. 対象ソースを取得

  2. Kanikoを動かすための設定を実施

  3. Kanikoを実行

が実施していることです。

checkout部分に関しては特に何もないので2以降の説明をします。

まずはdocker pullでKanikoの実行イメージを取得してきています。(正直docker runで取得するので必要にないっちゃないです。)

その次にRailsのcredentials用のためのmaster.keyの設定ですが、これもRails依存の設定なのでRailsに関係ない人は無視していいです。

大事なのがその次の

echo "{\"credHelpers\":{\"${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com\":\"ecr-login\"}}" > /tmp/config.json

です。これは何をやっているかというとKanikoでビルドした結果および中間レイヤーをECRにpushできるようにする設定です。Kanikoではamazon-ecr-credential-helperが内包されており、docker loginなしにECRにアクセスできるようになっております。その「amazon-ecr-credential-helperを使用する」という設定です。

これを一旦/tmpに吐いて、後のdocker run時にvolume設定で設定しています。

最後の実行コマンドの説明です。

docker run -it \

-v /tmp/config.json:/kaniko/.docker/config.json \
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
-v $(pwd):/build \
gcr.io/kaniko-project/executor:debug \
--cache=true \
--context /build \
--dockerfile /build/Dockerfile \
--destination ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/kaniko-example:$CIRCLE_SHA1

まずは以下の2つ

  -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \

-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \

ここは実行するコンテナにAWS用の環境変数を設定しています。さきほどECRにpushする設定をしましたが、pushするIAMの情報が必要です。それがここになります。

次に

  --cache=true \

Kanikoのコンテナを実行する場合にデフォルトではキャッシュの保存を行ってくれません。ですので、キャッシュを保存するようにするためのフラグを設定します。

最後に

  --destination ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/kaniko-example:$CIRCLE_SHA1

これは最終できあがったコンテナイメージをどこにpushするかの設定をしています。ECRのレジストリを指しておりますが、付けるtagは各々あると思うので、あくまで参考程度に。

これを実行するすることで、Kanikoによるキャッシュを考慮したビルドを実行してくれます。

キャッシュの保存先については最後の注意点に記載します。


Travis CI

次にTravis CIでの使用方法です。ほぼCircle CIと同様なので説明は省きたいと思います。

language: bash

services:
- docker

before_install:
- docker pull gcr.io/kaniko-project/executor:debug
- echo "$RAILS_MASTER_KEY" > ${TRAVIS_BUILD_DIR}/config/master.key
- echo "{\"credHelpers\":{\"${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com\":\"ecr-login\"}}" > /tmp/config.json

script:
- docker run -it -v /tmp/config.json:/kaniko/.docker/config.json -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY -v ${TRAVIS_BUILD_DIR}:/build gcr.io/kaniko-project/executor:debug --cache=true --context /build --dockerfile /build/Dockerfile --destination ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/kaniko-example:$TRAVIS_PULL_REQUEST_SHA

branches:
only:
- master


GitLab CI

最後にGitLab CIでの使用方法です。これは他の2つと違い、公式にGitLab registoryを使う方法の使用例があります。

ただし、ドキュメントに書いてある通り11.2以上を対象としているため、もしそれ未満のバージョンの方は上記のCircle CIやTravis CIのようにdocker runで使用することになります。

stages:

- build

build:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
script:
- echo "{\"credHelpers\":{\"${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com\":\"ecr-login\"}}" > /kaniko/.docker/config.json
- echo "$RAILS_MASTER_KEY" > ${CI_PROJECT_DIR}/config/master.key
- /kaniko/executor --cache=true --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/kaniko-example:$CI_COMMIT_SHA
only:
- master

上記2つのCI/CDサービスと違うのはKanikoのイメージの中で動かしています。entrypointを上書きしてクリアしており、後で/kaniko/executorを実行しているところが味噌です。

他の実行に関しては特に上記2つのCI/CDサービスと同様のことを実行しております。


注意点


キャッシュの保存先

上記の設定ではキャッシュの保存先は登録ECRリポジトリ/cacheというのがキャッシュの保存先になります。ですので予めキャッシュの保存先のリポジトリを作成しておく必要があります。

image.png

自分でキャッシュの保存先を変えたい場合は---cache-repoという設定でリポジトリを指定することも可能です。


ECRを使用するとファイルキャッシュが使えない?

この記事の本題となるECRにpushする設定を入れるとキャッシュ保存先にファイルが使えません。(逆に私が知らないだけかも)

そうなると何がデメリットかというとKanikoは中間レイヤーごとにキャッシュの存在確認をするのですが、ECRにキャッシュがあると通信速度にも若干引っ張られます。通信するよりファイルI/Oの方が早いはずなのでできればファイルキャッシュを使用したいところです。


IAMの権限

なんでもかんでも権限を付けるとセキュリティ的によくないので必要最低限で絞るためにAmazoneEC2ContainerRegistryPowerUserくらいあれば問題ないと思います。

image.png


本記事の各CI/CDサービスのenvironments設定

この記事の例では以下のenvironmentの設定が必要なので、ご自身で設定下さい。


Circle CI用

image.png


Travis CI用

image.png


GitLab CI用

image.png


さいごに

コンテナ運用で問題になるビルド時間を解決するためのツールを紹介しました。

Kaniko以外にもまだあるようですが、自身も業務でこのビルド時間がネックなってきており、開発するための知見を共有させて頂きました。

BuildKidのキャッシュ戦略がある程度固まってくるまでは有効なツールではないかなと考えています。