kaniko が解決したいこと

https://qiita.com/Daniel_Nakano/items/4e4d33ed11b31616bc46 からの転記

この記事は Kubernetes2 Advent Calendar の20日目の記事です。

ここで書いたことは 2019/12/20 時点での情報を元にしており、今後新しいバージョンで仕様が変更になる可能性があるのでご注意ください。

はじめに

kaniko とはコンテナビルドツールの一つで、docker のセキュリティやキャッシュの問題を解決するために開発されました。 この記事では、 docker のセキュリティの問題について軽く紹介した後に、それを kaniko がどのように解決したかを説明します。 また、最後に使用中に発生した問題もご紹介します。

kaniko とは?

Kaniko-Logo.png https://github.com/GoogleContainerTools/kaniko

kaniko とは docker daemon に依存せずにコンテナイメージがビルドができるツールです。

コンテナイメージとして提供されているので、コンテナや kubernetes 上で実行することが可能となっています。 また、docker daemon に依存しないのでルート権限が必要となることで引こ起こるセキュリティ問題が発生しないのと、キャッシュ機能も改善されており docker build に比べビルド時間が短縮されるのが大きな特徴です。

なぜ kaniko が作られたのか?

kubernetes が本番環境などで広く利用されるようになりイメージのビルドも kubernets 上でコンテナイメージをビルドしたいという要望が増えてきたからのようです。 https://github.com/kubernetes/kubernetes/issues/1294 https://github.com/kubernetes/kubernetes/issues/4018

(GKE を使ってると Cloud Build 上でイメージをビルドすることが多いと思われるので、kubernetes 上でイメージをビルドしたい場面はあまり思いつきませんでした。。。)

また、yaml 内では、イメージは名前とタグで指定されていおり、ビルドによって新しくイメージが作られた場合以外では変更されないという信頼ベースの運用になっているために何かの拍子で変更されてしまうと Pod の起動に失敗する可能性があります。kubernetes 上でビルドする仕組みがあれば、より確実な参照の仕組みが構築出来るかもという提案もありました。 https://github.com/kubernetes/kubernetes/issues/503

上記で述べた要望と受けて議論された結果、これらの課題を解決するツールとして kaniko が開発されました。 https://github.com/kubernetes/kubernetes/issues/1806

ちなみに Openshift ではすでにイメージのビルドも可能でした。 https://docs.openshift.com/container-platform/4.2/builds/understanding-image-builds.html#builds-strategy-docker-build_understanding-image-builds v3 までは docker daemon に依存することによる危険性はありましたが、v4 で Buildah という docker daemon に依存しないビルドツールが採用されています。

docker build の問題

kubernetes 上でも docker を使えばイメージをビルドすること自体は可能でした。 やり方としては、 kubernetes上に立てた docker daemon に対して /var/run/docker.sock (docker daemon が REST API を待ち受けるソケット) をマウントしたコンテナを起動して docker build を実行する方法や、 Docker-in-Docker を利用する方法がありました。

しかし、 /var/run/docker.sock をマウントする方法は docker daemon がルート権限で稼働していることホスト上の任意のファイルを読み書きできてしまうというセキュリティの問題がありました。

docker run -t -i -v /var/run/docker.sock:/var/run/docker.sock debian:jessie /bin/bash

Docker-in-Docker についても、ベースとなる docker を --privileged で起動する必要がありホストのデバイスに触れてしまう問題がありました。

また、 docker build の実行の際に Dockerfile の途中のコマンドで変更(差分が有るファイルを COPY など)があるとそれ以降のコマンドではキャッシュが効かなくなります。 https://docs.docker.com/develop/develop-images/dockerfile_best-practices#leverage-build-cache

docker build の問題点をどうやって kaniko は解決したか?

docker daemon の実行にルート権限が必要問題

docker daemon の実行にルート権限が必要になるための起こるセキュリティ問題については、ビルドをユーザー空間で実行するという方法で回避しています。

kankko では↓の流れでビルドが行われます

  1. Dockerfile をパース
  2. ベースイメージのファイルシステムをルートに展開
  3. Dockerfile 内のコマンドを順番に実行
  4. 実行後のファイルシステムのスナップショットを作成
  5. スナップショットを tarball として保存しベースイメージにレイヤーとして追加
  6. 3~5 を Dockerfile のすべてのコマンドで実行し最終的なイメージを作成
  7. 作成されたイメージを指定のコンテナイメージのレジストリにプッシュ

ビルドをすべてユーザー空間で行っているために docker build のようにカーネル空間で行うよりはパフォーマンスは悪くなりますが、よく使用されるような Dockerfile のコマンドでは無視できる程度のパフォーマンス劣化のようです。

Dockerfile のコマンドの途中で変更があるとそれ以降でキャッシュが効かない問題

ビルドの流れでも言及しましたが、 kaniko では Dockerfile の FROM 以降のコマンドごとにキャッシュを作成します。2回目以降のビルドでは、コマンドの実行前にレジストリにキャッシュがあるかを見て、有ればレジストリから取得して展開、無ければコマンドを実行するので、キャッシュが無いためにコマンドが実行された以降のレイヤーでもキャッシュが効くようになりビルド時間が短縮されます。

Cloud Build で kaniko を使う

ここまでは、 kaniko の開発経緯やビルドの仕組みについて書いてきましたが、 Cloud Build での docker のマルチステージビルドを紹介しつつ、それを kaniko で行う方法を説明します。

ちなみに Cloud Build というのは google が提供しているの CI/CD サービスのことです。 https://cloud.google.com/cloud-build/ また、Cloud Build での kaniko の詳しい使い方についてはドキュメントが用意されてるのでそちらをご覧ください。 https://cloud.google.com/cloud-build/docs/kaniko-cache

docker でマルチステージビルド

docker にはマルチステージビルドというより軽量なイメージをビルドできる機能がありますが、 それを Cloud Build で行う場合にキャッシュを効かせるために工夫が必要になります。

↓はキャッシュを効かせる工夫を施した Cloud Build の yaml の例です。 それぞれのステージ毎にビルドをし、 --cache-from でそれを明示的に指定する必要がある分ステップが増え可読性が落ちてしまいます。

steps:
  # それぞれのステージのイメージを pull する。
  - name: 'gcr.io/cloud-builders/docker'
    args: ['pull', 'gcr.io/$PROJECT_ID/app_builder:latest']
    waitFor: ['-']
    id: 'pull-builder'
  - name: 'gcr.io/cloud-builders/docker'
    args: ['pull', 'gcr.io/$PROJECT_ID/app:latest']
    waitFor: ['-']
    id: 'pull-app'

  # pull したイメージを --cache-from で指定してキャッシュを効かせるようにし、 --target でビルドするステージの指定をする。 target は Dockerfile の FROM コマンドで as で指定することが出来る。
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '--cache-from', 'gcr.io/$PROJECT_ID/app_builder:latest', '-t', 'gcr.io/$PROJECT_ID/app_builder:latest', '--target', 'app_builder', '.']
    waitFor: ['pull-builder']
    id: 'build-builder'

  # キャッシュを効かせるために --cache-from で各ステージのイメージを指定する。
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '--cache-from', 'gcr.io/$PROJECT_ID/app_builder:latest', '--cache-from', 'gcr.io/$PROJECT_ID/app:latest', '-t', 'gcr.io/$PROJECT_ID/app:latest',  '.']
    waitFor: ['build-builder']
    id: 'build-app'

  # 各ステージ用にビルドしたイメージをそれぞれ push する。
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', 'gcr.io/$PROJECT_ID/app_builder:latest']
    waitFor: ['build-builder']
    id: 'push-builder'
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', 'gcr.io/$PROJECT_ID/app:latest']
    waitFor: ['tag-app']
    id: 'build-app'

kaniko でマルチステージビルド

kaniko では、内部でマルチステージビルドしてくれる機能があるので特に何かを設定する必要がなく yaml が非常にシンプルになります。

steps:
  - name: 'gcr.io/kaniko-project/executor:v0.13.0'
    args:
      - --destination=gcr.io/$PROJECT_ID/app:latest
      - --cache=true
      - --cache-ttl=6h

まとめ

ここまで書いてきたように、kaniko は docker のセキュリティやキャッシュの問題を解決してたり、イメージで提供されてることから非常に使いやすいビルドツールです。

しかし、活発に開発が進んでることもあり issue を見ると不具合の報告がたくさんあるので、使用する際は動作確認などをしっかり行う必要があります。 特にアップグレードの際は差分をよく確認することをおすすめします。

以前に kaniko のイメージのタグを latest で使用してしまっており、 v0.13.0 から v0.14.0 に上がったタイミングでキャッシュが効きすぎてしまい変更が反映されないという不具合が発生した、バージョンについてはタグで明示的に設定するしたほうが良いでしょう。(具体的な原因については調査中で、分かり次第 issue に上げる予定です。)

時間切れで docker 謹製の buildkit との違いが追えなかったので別途まとめます。

参考