kaniko が解決したいこと + 実際使ってみたハマったところ

この記事は [Kubernetes2 Advent Calendar](https://qiita.com/advent-calendar/2019/kubernetes2) の20日目の記事です。

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

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


# kaniko とは？
![Kaniko-Logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56981/901d9475-e7d2-69f9-d978-c4f635a986ec.png)
https://github.com/GoogleContainerTools/kaniko

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

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


# なぜ kaniko が作られたのか？
kubernetes が本番環境などで広く利用されるようになりイメージのビルドも kubernets 上でコンテナイメージをビルドしたいという要望が増えてきたからのようです。[^1] [^2]

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

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

上記で述べた要望と受けて議論された結果、これらの課題を解決するツールとして kaniko が開発されました。[^4]

ちなみに Openshift ではすでにイメージのビルドも可能でした。 [^5]
v3 までは docker daemon に依存することによる危険性はありましたが、v4 で [Buildah](https://buildah.io/) という 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` など)があるとそれ以降のコマンドではキャッシュが効かなくなります。[^6]


# 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 サービスのことです。[^7]
また、Cloud Build での kaniko の詳しい使い方についてはドキュメントが用意されてるのでそちらをご覧ください。
https://cloud.google.com/cloud-build/docs/kaniko-cache

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

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

```yaml
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 が非常にシンプルになります。

```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 の本番運用で発生した問題

ここまで kaniko のいいところばかりを紹介してきましたが、ここで実際に使用した際に発生した問題を紹介します。

### `--build_arg` にビルド毎に変わる変数を指定するとキャッシュが効かない

##### 発生した問題
運用上の関係でソースコード内で Cloud Build の `REVISION_ID` (毎回のビルドで生成されるハッシュ値) を参照している箇所があるため、それを外から渡してやる必要がありました。 kaniko では `--build-arg` で変数を指定することで Dockerfile 内で参照することが出来るようになります。

```yaml
steps:
  - name: 'gcr.io/kaniko-project/executor:v0.13.0'
    args:
      - --destination=gcr.io/$PROJECT_ID/app:latest
      - --cache=true
      - --cache-ttl=6h
      - --build-arg=BUILD_ID=${REVISION_ID}
```

↓ は Dockerfile 内で設定している変数です。

```docker

ARG BUILD_ID
```

その後、コンテナ内で正常に変数を取得できることを確認できたのですが、それ以降のビルドで変更が無い Dockerfile コマンド (`COPY` など)にもかかわらず全くキャッシュが効かないという事象が発生しました。

##### 原因と解決方法
issue を漁ってみたところ同じような問題が報告されているのを見つけました。
https://github.com/GoogleContainerTools/kaniko/issues/637

kaniko ではキャッシュ用の key を作成する際の要素として `--build-arg` も含まれており、ビルドのたびに変更されるのでにその度に作られる key が毎回違うものになってたためにキャッシュが効かない状態となっていました。

ソースを確認したところ、キャッシュ用のキーを生成する際に `--build-arg` の値も参照されており、それが毎回変更される結果として生成されるキーも毎回違うものになってたことが原因でした。

```go
compositeKey.AddKey(s.opts.BuildArgs...)
```
https://github.com/GoogleContainerTools/kaniko/blob/c8fabdf6e43b19f6a223f1d0b06e127d0774bd7e/pkg/executor/build.go#L198

これは問題というか仕様なので、 `--build-arg` の使用をやめ別の方法で `REVISION_ID` を渡すように変更して対応しました。
kaniko を使用する際は `--build-arg` で常に変更される変数を渡さないように注意してください。


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

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

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

時間切れで docker 謹製の [buildkit](https://github.com/moby/buildkit) との違いが追えなかったので別途まとめます。


# 参考
- [Kaniko Design Doc](https://github.com/GoogleContainerTools/kaniko/blob/master/docs/designdoc.md)
- [Docker security](https://docs.docker.com/engine/security/security/)
- [BuildKitによる高速でセキュアなイメージビルド](https://www.slideshare.net/AkihiroSuda/buildkit)
- [BuildKit: A Modern Builder Toolkit on Top of containerd - Tonis Tiigi & Akihiro Suda](https://www.youtube.com/watch?v=yd0lvUXitxY)
- [Multi-stageのDockerfileをCloud Buildでビルドするコツ](https://qiita.com/bigplants/items/73f4e16b840207f09e81)
- [Using Kaniko cache](https://cloud.google.com/cloud-build/docs/kaniko-cache)
- [Dockerセキュリティ: 今すぐ役に立つテクニックから，次世代技術まで](https://www.slideshare.net/AkihiroSuda/docker-125002128)
- [Docker daemon attack surface](https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface)



[^1]: https://github.com/kubernetes/kubernetes/issues/1294
[^2]: https://github.com/kubernetes/kubernetes/issues/4018
[^3]: https://github.com/kubernetes/kubernetes/issues/503
[^4]: https://github.com/kubernetes/kubernetes/issues/1806
[^5]: https://docs.openshift.com/container-platform/4.2/builds/understanding-image-builds.html#builds-strategy-docker-build_understanding-image-builds
[^6]: https://docs.docker.com/develop/develop-images/dockerfile_best-practices#leverage-build-cache
[^7]: https://cloud.google.com/cloud-build/