BuildgでDockerfileを対話的にデバッグする方法

# この記事の内容

Dockerfileのデバッグは非常に面倒です。ビルドの途中でエラーが出たら、Dockerfileを変更して再実行して確認することである程度デバッグできますが、規模の大きいDockerfileになると時間が掛かります。

[Buildg](https://github.com/ktock/buildg)はこうした問題を解決するために、Dockerfileのデバッグを対話的に行うことができるツールです。
## 対象者

- Dockerの経験がある程度あり、Dockerfileを書いた経験がある人
- Dockerfileでイメージをビルドするときにデバッグが上手くできずに苦労した経験がある人

## 検証環境
- Ubuntu 20.04
- Docker 23.0.1


## Buildgを使わない場合のデバッグ方法の例

Dockerfileでイメージビルドするときにエラーが出たときに、以下の手順でコンテナを調査することができます。

1. `docker ps -a`でビルドに失敗したコンテナを見つけ、`docker commit`してそのコンテナのイメージを保存する。
2. 保存したイメージからコンテナを作成して、そのコンテナにログインして中身を調べる。

この方法でもある程度調査できますが、不要なイメージが溜まっていくのが個人的にストレスだったり、地味に手順1が面倒だなあと思っていました。

そこで、見つけたのがBuildgでした。


## Buildgとは？

Githubから引用すると、「[Buildkit](https://github.com/moby/buildkit)をベースにしたDockerfileをインタラクティブにデバッグするツール」とあります。

>buildg is a tool to interactively debug Dockerfile based on BuildKit.


Buildkitについては深入りしませんが、興味がある方は[こちら](https://roy-n-roy.nyan-co.page/Docker/BuildKit/)の記事が分かりやすかったです。

Buildgでできることを紹介すると、以下の通りです。

>- ソースレベルの検査
>- ブレークポイントとステップ実行
>- 独自のデバッグ ツールを使用したステップ上の対話型シェル
>- BuildKit ベース (マージされていないパッチあり)
>- ルートレスをサポート

参考：[ktock/buildg](https://github.com/ktock/buildg#buildg-interactive-debugger-for-dockerfile)

個人的に嬉しい機能が、「ブレークポイントとステップ実行」です。Dockerfileの行番号を指定して、その行までビルドするということができます。ブレイクポイントまでビルドした状態でコンテナを起動して中身を確認できます。


## Buildgのセットアップ

今回は記事投稿時に最新である`v0.4.1`をインストールします。以下のスクリプトを適当な場所に配置して実行すればセットアップできます。今回はバイナリをインストールして利用します。`make`を利用してバイナリをビルドすることもできます。インストール手順は[こちら](https://github.com/ktock/buildg#install)で最新の情報を確認してください。

```bash:setup.sh
#! /bin/bash

# 実行パス取得
readonly CURRENT=$(cd $(dirname $0);pwd)

# Buildgのバージョン
BUILDG_VERSION="v0.4.1"

# バイナリーのインストール
wget -P ${CURRENT} https://github.com/ktock/buildg/releases/download/${BUILDG_VERSION}/buildg-full-${BUILDG_VERSION}-linux-amd64.tar.gz 

# 解凍
tar -zxvf buildg-full-${BUILDG_VERSION}-linux-amd64.tar.gz

# バイナリをコピー
cp ${CURRENT}/bin/* /usr/local/bin
```

## Buildgのデモ

### 利用するソース
ファイルの配置は以下を想定しています。
```bash
.
├── Dockerfile
├── hello.c
└── world.c
```

```Dockerfile:Dockerfile
# hello.cのコンパイル
FROM ubuntu:20.04 AS hello
RUN apt-get update && \
    apt-get install -y gcc

# ソースをホストからコピー
COPY hello.c /

# "hello"という名前の実行ファイルを作成
RUN gcc -o /hello /hello.c

# world.cのコンパイル
FROM ubuntu:20.04 AS world
RUN apt-get update && \
    apt-get install -y gcc

# ソースをホストからコピー
COPY world.c /

# "world"という名前の実行ファイルを作成
RUN gcc -o /world /world.c

FROM ubuntu:20.04
COPY --from=hello /hello /hello
COPY --from=world /world /world
CMD /hello ; /world
```

```c:hello.c
#include <stdio.h>
int main () {
  printf("Hello");
  return 0;
}
```
```c:world.c
#include <stdio.h>
int main () {
  printf("World!");
  return 0;
}
```

### デバッグを開始

コマンドリファレンスは以下を参考にしてください。
- [Command reference](https://github.com/ktock/buildg#command-reference)

Dockerfileを配置したディレクトリで以下を実行します。

```
buildg debug .
```

Proxy環境下で実行する場合、`--build-arg`で構築時の変数を指定できます。（ただし、DockerfileにProxyを利用する設定の追記が必要。）

```
buildg debug --build-arg http_proxy="proxy-url" --build-arg no_proxy="no-proxy-list" .
```

### ブレイクポイントの指定
`break`でブレイクポイントを指定します。`continue`でブレイクポイントまでビルドします。

```
(buildg) break 25
(buildg) continue
```

### ブレイクポイントまでビルドした状態のコンテナに入る
ビルドした部分までの状態でコンテナ内を調べたい場合は、`exec`を実行します。コンテ内でコマンドを実行できることが確認できました。

```
(buildg) exec
# cat hello.c
#include <stdio.h>
int main () {
  printf("Hello");
  return 0;
}
# ./hello
Hello
```

### コンテナ、およびデバッグから抜ける
コンテナから抜ける場合は、`exit`を実行し、デバッグを終了したい場合も`exit`を実行します。
```
# exit
(buildg) exit
#6 DONE 10.8s

#7 [hello 3/4] COPY hello.c /
#7 DONE 0.0s

#8 [world 3/4] COPY world.c /
#8 DONE 0.0s

#9 [world 4/4] RUN gcc -o /world /world.c
#9 ...

#10 [hello 4/4] RUN gcc -o /hello /hello.c
#10 CANCELED

#9 [world 4/4] RUN gcc -o /world /world.c
#9 CANCELED
```

## まとめ

Buildgを活用することで、Dockerfileのデバッグが対話的に行えることが分かりました。
buildで失敗したときに気軽にコンテナ内を調査できそうですね。