結論
- 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の時間が長くなってしまうとリリースサイクルに影響が出てしまうので、時間を短縮する必要があります。
普通のコンテナイメージビルドでは以下のような問題がありました。
- Dockerfileを上から順(シングルタスク)で動作する
- 途中の行で変更があると以降はキャッシュが使用されず、再実行される
これを解決するため(特に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つあり、各々やっていることは
- npm packageのインストールおよびwebpack(er)を使用してのビルド
- gemのインストール、1のnpm packageのコピー、assets:precompileを使用してのビルド
- 1のgem、1のビルド結果、2のビルド結果を取得して最終の実行コンテナイメージを作成
Railsにフロントまで含めてるのはどうなの?という議論はさて置いておいて、3つに分けている理由は以下です。
- webpackのビルドはインストールするnpm packageとビルド対象ファイルに影響するため
- 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
にて実行しています。
- KanikoのコンテナイメージはCMDが設定されていない
- 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
実行しているのは結構単純です。
- 対象ソースを取得
- Kanikoを動かすための設定を実施
- 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
というのがキャッシュの保存先になります。ですので予めキャッシュの保存先のリポジトリを作成しておく必要があります。
自分でキャッシュの保存先を変えたい場合は---cache-repo
という設定でリポジトリを指定することも可能です。
ECRを使用するとファイルキャッシュが使えない?
この記事の本題となるECRにpushする設定を入れるとキャッシュ保存先にファイルが使えません。(逆に私が知らないだけかも)
そうなると何がデメリットかというとKanikoは中間レイヤーごとにキャッシュの存在確認をするのですが、ECRにキャッシュがあると通信速度にも若干引っ張られます。通信するよりファイルI/Oの方が早いはずなのでできればファイルキャッシュを使用したいところです。
IAMの権限
なんでもかんでも権限を付けるとセキュリティ的によくないので必要最低限で絞るためにAmazoneEC2ContainerRegistryPowerUser
くらいあれば問題ないと思います。
本記事の各CI/CDサービスのenvironments設定
この記事の例では以下のenvironmentの設定が必要なので、ご自身で設定下さい。
Circle CI用
Travis CI用
GitLab CI用
さいごに
コンテナ運用で問題になるビルド時間を解決するためのツールを紹介しました。
Kaniko以外にもまだあるようですが、自身も業務でこのビルド時間がネックなってきており、開発するための知見を共有させて頂きました。
BuildKidのキャッシュ戦略がある程度固まってくるまでは有効なツールではないかなと考えています。