Docker を使わずに Dockerfile をビルドしてみた

システム本部データ統括部AI基盤部MLエンジニアリング第一グループで MLOps エンジニアをしている @coil_msp123 です。
[DeNA Advent Calendar 2021](https://qiita.com/advent-calendar/2021/dena)の 15 日目になります。
今回は直近業務に必要になっているわけではありませんが、将来的な選択肢として k8s 上に CI/CD パイプラインを構築する可能性があるため k8s 上で Dockerfile のビルドを行うために色々検証した結果を共有してみようと思います。

# BuildKit

今回主に使用するツールは BuildKit　になります。

https://github.com/moby/buildkit
BuildKit は Moby プロジェクトの成果物の一つです。

###note:
Moby プロジェクトは従来 Docker というツールが担っていた各種機能を役割ごとに分割して管理する的なものです。Docker ∋ containerd, BuildKit, etc... containerd はコンテナを動かす（CRIランタイム）。BuildKit は Dockerfile をビルドする。 Docker を使うときに実はこれらのコンポーネントが内部的に動いています。(設定によるかも)

```shell
DOCKER_BUILDKIT=1 docker build
COMPOSE_DOCKER_CLI_BUILD=1 docker-compose build
```

上記のような環境変数を渡してあげることで Docker 及び Docker Compose でビルドに buildkit が使われます。
Macだと現在はデフォルトで BuildKit が使われているかもしれません。

```json
{
  "experimental": false,
  "builder": {
    "gc": {
      "enabled": true,
      "defaultKeepStorage": "20GB"
    }
  },
  "features": {
    "buildkit": true
  },
  "ip": "127.0.0.1"
}
```

Docker Desktop の　preferences -> Docker Engine の設定で、上記のように features: { "buildkit": true } となっていれば buildkit が使われるようになっているようです。
https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/build_enhancements/

従来の docker build は Dockerfile に記述されている内容を上から順にビルドしていましたが、依存関係をうまく処理する仕組みなどにより並列ビルドできるようにしたり、キャッシュを改良したりしたものが　BuildKit です。とりあえず有効化しておくとビルドが早くなるのでお得です。
参考: https://www.slideshare.net/zembutsu/dockerfile-bestpractices-19-and-advice

###note:

BuildKit は Dockerfile を一旦 Low-Level builder (LLB) と呼ばれる中間言語に変換してからビルドを行います。逆に LLB さえ用意すれば BuildKit を使ってビルドが可能なので、Dockerfile じゃない形式からビルドすることも可能なようです。llvm に似ていますね。
参考: https://qiita.com/po3rin/items/4fa7686c271a20f89c66 https://jongz.hatenablog.com/entry/2019/12/15/121805

##Docker Buildx

今回の主旨からは外れますが　Buildx というプラグインがあります。これは Docker コマンドを拡張するプラグインで BuildKit の機能に対応しているものになります。
インストールされていれば以下のようなコマンドで Buildx が使用可能です。build コマンドなどが docker buildx のサブコマンドとして定義されるような構成になっています。Docker Desktop にはデフォルトで含まれており、知らず識らずのうちに使えるようになっていたようです。

```
docker buildx
```

```
docker buildx install
```

を実行すると、以後 docker build コマンドが buildx へのエイリアスとなります。

https://matsuand.github.io/docs.docker.jp.onthefly/buildx/working-with-buildx/

#モチベーション
GKE ではコンテナランタイムとして Docker が非推奨化され containerd が使われるようになって結構経ちました。当時はそのタイトルのインパクトだけで色々と物議を醸したのが記憶に残っています...。いずれにせよ現状の GKE には Docker が存在しないため、そのままでは Docker ランタイムを使用した Dockerfile のビルドは不可能です。そこで、別途 BuildKit を使うことで Dockerfile を GKE でビルドすることを試みたのが本記事のモチベーションになります。

#BuildKit 概要
BuildKit は buildkitd デーモンと buildctl クライアントから構成されています。
buildkitd に対して buildctl から通信して各種処理をする仕組みです。もちろんネットワーク越しに通信可能なので、buildkitd を起動したサーバに対して buildctl をローカルから使ってリモートでビルドが可能です。
ただし buildkitd は linux でしか動かないことに注意が必要。
buildctl は mac でも使えて、おなじみ homebrew でインストールできます。

```
brew install buildctl
```

#docker で使ってみる

buildkitd が linux でしか動かないので Docker を使って起動してみます。buildkitd 用コンテナが docker hub に上がっているので、試しにこいつを使ってみるのが手っ取り早いでしょう。
https://hub.docker.com/r/moby/buildkit

個人的な趣味で Docker Compose を使って起動します。docker-compose.yamlの内容は以下の通り。

```yaml
version: '3.8'

services:
  ubuntu:
    image: ubuntu
    tty: true
  buildkit:
    image: moby/buildkit
    privileged: true
    ports: 
      - '1234:1234'
    command: '--addr tcp://0.0.0.0:1234'
```

ENTRYPOINT が buildkitd なので引数として addr を渡しています。
サーバを立ち上げたらあとはローカルで適当な Dockerfile を用意してこのサーバを対象にビルドコマンドを打てばよいことになります。

今回は適当に ubuntu を用意するだけの Dockerfile を使用しました。

```Dockerfile
FROM ubuntu
```

次のようなコマンドを打つと、上記の Dockerfile が Docker で立てたサーバ上の buildkitd によってビルドされます。

```shell
buildctl --addr tcp://127.0.0.1:1234 build \\
--frontend=dockerfile.v0 \\
--local context=. \\
--local dockerfile=.
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53020/df57c9b9-a9fe-468f-cf27-c260a1e260b4.png)


BuildKit を有効化しているときの Docker build 時のようなログが流れているのが分かると思います。
ここで注意点として buildkit はあくまでビルドを行うのみなので、ビルドしたイメージを確認することはできないみたいです。

# Kubernetes 上で使ってみる

BuildKit レポジトリにサンプルがあるので、そちらをベースに作業を行います。
https://github.com/moby/buildkit/tree/master/examples/kubernetes
Kubernetes 上で動く buildkitd には rootless モード及び privileged モードがありますが、今回使用した GKE の制約なのかは不明ですが privileged モードしか動かなかったため、そちらで検証をしました。加えて unix ドメインソケットも利用できないようで細かくマニフェストを修正する必要があります。

書き換えたマニフェストは以下の通りです。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: buildkitd
  name: buildkitd
spec:
  replicas: 1
  selector:
    matchLabels:
      app: buildkitd
  template:
    metadata:
      labels:
        app: buildkitd
    spec:
      containers:
        - name: buildkitd
          image: moby/buildkit:master
          args:
          # unix ドメインソケット。おそらくヘルスチェック用？no such fileと出て動かない
          #  - --addr
          #  - unix:///run/buildkit/buildkitd.sock
            - --addr
            - tcp://0.0.0.0:1234
            - --tlscacert
            - /certs/ca.pem
            - --tlscert
            - /certs/cert.pem
            - --tlskey
            - /certs/key.pem
          # the probe below will only work after Release v0.6.3
          #readinessProbe:
          #  exec:
          #    command:
          #      - buildctl
          #      - debug
          #      - workers
          #  initialDelaySeconds: 5
          #  periodSeconds: 30
          # the probe below will only work after Release v0.6.3
          #livenessProbe:
          #  exec:
          #    command:
          #      - buildctl
          #      - debug
          #      - workers
          #  initialDelaySeconds: 5
          #  periodSeconds: 30
          securityContext:
            privileged: true
          ports:
            - containerPort: 1234
          volumeMounts:
            - name: certs
              readOnly: true
              mountPath: /certs
      volumes:
        # buildkit-daemon-certs must contain ca.pem, cert.pem, and key.pem
        - name: certs
          secret:
            secretName: buildkit-daemon-certs
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: buildkitd
  name: buildkitd
spec:
  ports:
    - port: 1234
      protocol: TCP
  selector:
    app: buildkitd
```

セキュリティを考えると rootless モードのほうが望ましいので、そちらの方法は後々調べたいと思います。
上記のマニフェストを apply すると buildkitd のサーバと、そこにアクセスするための service リソースが生成されます。

###note:
rootless モードにすると権限がなくて /tmpフォルダへマウントできないと言った意味合いのメッセージが出ました。 unix ドメインソケットはおそらくヘルスチェック用のようです。readinessProbe 及び livenessProbe で使われている buildctl debug workers コマンドでこれが使われるみたいです。ただし GKE の制約なのかはわかりませんが unix ドメインソケットがないと怒られて失敗します。もしかしたら containerd OS のせいでしょうか。

## ローカルからビルド

まずはローカルの mac からリモートでの k8s 上でのビルドを試してみます。
これは非常に簡単で、ポートフォワードしてチェックができます。

```shell
kubectl port-forward service/buildkitd 1234
```

先程の方法と同じコマンドで同じくビルドができます。これで k8s 上での Dockerfile のビルドが成功したことが確認できました。（イメージは見えませんが...）

```
buildctl --addr tcp://127.0.0.1:1234 build \\
--frontend=dockerfile.v0 \\
--local context=. \\
--local dockerfile=.
```

## k8s 上の pod からビルド

さて、せっかくなので完全に k8s 上で Dockerfile のビルドをしてみます。
適当に以下のようなマニフェストで ubuntu を pod として用意しました。

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: ubuntu
spec:
  containers:
    - name: ubuntu
      image: ubuntu:latest
      tty: true
```
Buildkit のレポジトリで buildkit が配布されているので、ごにょごにょして buildkit を入れて適当な Dockerfile を用意するのが簡単です。
https://github.com/moby/buildkit/releases

```shell
buildctl --addr tcp://buildkitd:1234 build \\
--frontend=dockerfile.v0 \\
--local context=. \\
--local dockerfile=.
```

今回は先程までと違い addr に buildkitd のサービスを指定すれば k8s の機能でよしなに名前解決してくれます。
https://kubernetes.io/ja/docs/concepts/services-networking/dns-pod-service/

#Tekton

上記の手順で一応 Dockerfile のビルドは Docker がなくてもできました。ただし、素の buildkit を使うのはいろいろハードルが高そうです。Buildkit でビルドしてもあくまでレイヤーができるだけなので、イメージとして書き出してプッシュなどはまた別の手段が必要になります。
ちなみにレイヤーは以下のコマンドで確認できます。

```
buildctl --addr tcp://buildkitd:1234 du
```

Docker を使えば生成したイメージの確認ができますが、今回の主旨とはズレるので割愛します。実際に k8s 上でイメージのビルドをするような場合は専用の CI/CD ツールを使うのが無難そうです。例えば以下のようなツールがあります。
https://tekton.dev/
Tekton は k8s 上で動く CI/CD ツールです。もちろんイメージのビルドも可能です。その際ビルダーとして buildkit が指定できるので、おそらく上記のように docker なしのビルドをよしなにやってくれると思われます。イメージのプッシュももちろんやってくれるので、実際はこれを使うのが良さそうです。

他のイメージビルダー

Buildah, Kaniko, Makisu とかがあるみたいです。名前くらいはちょいちょい聞いたことがあるかもしれません。

#結論
Docker がなくても moby プロジェクトの BuildKit を使うことで Dockerfile がビルドできることが確認できました。また BuildKit を使う場合は、ビルド対象が Dockerfile に限らないので色々面白いことができそうです。CI/CD まで k8s に寄せることができるので、特定のクラウドなどにほぼ依存しないシステムを k8s 上に構築できそうですね。

#参考

https://medium.com/nttlabs/buildkit-on-kubernetes-d37a03150192