CodeCrafters の Docker を作るコースを Nim でやってみた

[CodeCrafters](https://codecrafters.io/)というプログラミング学習サービスの「Build your own Docker」というコースを Nim でやってみました。
単にNimの書き方を学習するだけではなく、Dockerイメージや、chroot、PID名前空間について学ぶことができ、とても勉強になりました。

https://codecrafters.io/

## きっかけ

こちらのツイートを見て CodeCrafters を知りました。ありがとうございます :bow:

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">教育オタクなのでプログラミング学習サービスは大体知ってるんだけど、最近で一番いいなと思った。<a href="https://t.co/ioFOAjoUIM">https://t.co/ioFOAjoUIM</a><br>- Redis, Git, Dockerの再実装など少し複雑なソフトウェアを自作する課題<br>- 名だたる企業のエンジニアが作ったカリキュラムでベストプラクティスも学べる<br>- C/Rust/Goも使える <a href="https://t.co/cIXem7KJkp">pic.twitter.com/cIXem7KJkp</a></p>&mdash; Yu Fukuyama (@yuskefukuyama) <a href="https://twitter.com/yuskefukuyama/status/1610591440404643845?ref_src=twsrc%5Etfw">January 4, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## Build your own Docker コース

:::note warn
2023年4月時点でNimで取り組めるのはこのコースのみ（かつβ版）です。  
Nim v1.0.6 を想定したコースとなっています。
:::


「Build your own Docker」コースの概要は以下の通り（CodeCraftersサイトから引用）。
![「Build your own Docker」コース概要](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110572/043a8c0b-1425-e2a6-3c4c-535f95a0cb7f.png)


> （拙訳）
> 　Dockerイメージとは何なのか、Dockerレジストリにどのように保存されているかを学習します。
> 　Nimでのシステムプログラミングに触れ、chrootやカーネル名前空間について学習します。


このコースは6のステージで構成されていて、各ステージで与えられた課題に沿ってプログラムを作成するというものです。作ったプログラムを git push すると自動でテストが実行され、テストを通過すると次のステージに進むことができます。

今回作成したプログラムはこちらからご覧ください:grinning:

https://github.com/TakumiHANAUE/codecrafters-docker-nim/tree/master


### ステージ1 : Execute a program

`docker run` コマンドみたいに、引数で受け取ったコマンドを実行するプログラムを作るという課題です。作成するプログラム名を `myprogram` としたとき、`myprogram echo Hello` と実行すると、`echo Hello` を実行するプログラムを作成します。

引数の取得には [commandLineParams()](https://nim-lang.org/1.0.6/os.html#commandLineParams) を使いました。引数で渡されたコマンドの実行を行う関数は [osproc](https://nim-lang.org/1.0.6/osproc.html) ライブラリの [execProcess()](https://nim-lang.org/1.0.6/osproc.html#execProcess%2Cstring%2Cstring%2CopenArray%5Bstring%5D%2CStringTableRef%2Cset%5BProcessOption%5D) を使いました。

### ステージ2 : Wireup stdout & stderr

実行したコマンドの標準出力／標準エラー出力を、コマンドを実行した元プロセスで表示するように変更する課題です。

標準出力／標準エラー出力ってそういえば何だっけ？状態だったのでそのあたりを調べるところから取り組みました。コマンド実行には [execCmd()](https://nim-lang.org/1.0.6/osproc.html#execCmd%2Cstring) を使いましたが、この関数は標準入力／標準出力／標準エラー出力を呼び出し元プロセスから継承する関数となっていて、この課題にぴったりなものでした。

### ステージ3 : Handle exit codes

実行したコマンドの終了コードをコマンドを実行した元プロセスに渡す課題です。`myprogram echo Hello` を実行したとき、`echo Hello` の終了コードがそのまま、`myprogram echo Hello` の終了コードとなるようにします。

ステージ2で使った [execCmd()](https://nim-lang.org/1.0.6/osproc.html#execCmd%2Cstring) は実行したコマンドの終了コードを返却するので、その返却値を利用することができました。

### ステージ4 : Filesystem isolation

実行するコマンドが他のファイルにアクセスできないように、コマンドを実行するディレクトリをrootディレクトリに変更する課題です。`chroot`コマンドを使用することで、任意のディレクトリを新たなrootディレクトリとして隔離することができます。コマンドを実行する環境を他のファイル／ディレクトリから隔離することで、周囲のファイルに影響を及ぼすことなく安全にコマンドを実行できるようになります。

rootって一つじゃないの？変えるってどういうこと？という状態だったので、「chrootとは何か」から調べる必要があり大変でした。

やるべき処理の概要は以下の通り。
1. コマンド実行用ディレクトリを作る
1. コマンド実行に必要なファイルをコマンド実行用ディレクトリにコピーする
1. コマンド実行用ディレクトリを root に変更する
1. コマンドを実行する

「コマンド実行に必要なファイルをコマンド実行用ディレクトリにコピーする」が初めはわかっておらず、chrootしたあとにコマンドを実行しようとしたら、そのコマンド自体が参照できずに失敗するということがありました。
chrootしたのだから、新たなrootから外のディレクトリを参照できないのは当たり前（そのための chroot）で、「なるほど、隔離するってこういうことか」と身をもって理解することができました。

ちなみに、chrootに対応する関数はNimには標準で用意されていないので以下のように `importc`プラグマを使用してchroot関数を定義する必要があります。[posix](https://nim-lang.org/1.0.6/posix.html)ライブラリの [chdir](https://nim-lang.org/1.0.6/posix.html#chdir%2Ccstring) 関数も同様に定義されています。

```Nim
proc chroot*(path: cstring): cint {.importc, header: "<unistd.h>".}
```

### ステージ5 : Process isolation

実行するコマンドを新たなプロセスツリーで実行する課題です。呼出し元とは異なるPID名前空間でコマンドを実行するようにします。プロセスツリーを隔離しないと、呼び出し元のプロセスにシグナルを送って悪さをすることができてしまうので、それを防ぐためです。

PID名前空間？何それ？状態だったので、まずはそのあたりの勉強から始めたので時間がかかりました。
以下は学習の際に参照したページです。

- [Linux の PID Namespace について](https://blog.amedama.jp/entry/linux-pid-namespace_1)
- [NAMESPACES : Linux Programmer's Manual (7)](https://linuxjm.osdn.jp/html/LDP_man-pages/man7/namespaces.7.html)

### ステージ6 : Fetch an image from the Docker Registry

実際にDockerイメージをpullしてきて、コマンドを実行するという課題です。Docker registry API を使って公式イメージをpullし、そのイメージを使ってコマンドを実行するという内容になっています。

やることは以下の3ステップ。

1. 認証のためのTokenを取得する
1. イメージマニフェストを取得する
1. イメージを取得して展開する

この課題では HTTPリクエスト として Docker registry API を使いますが、例によってHTTPリクエスト何それ？状態だったので、まずはその勉強から始めました。さらに [Tokenの取得の仕方](https://docs.docker.com/registry/spec/auth/token/)、[Docker Registry HTTP API](https://docs.docker.com/registry/spec/api/) の使い方など、Docker の公式ドキュメントを読む必要がありました。HTTPリクエスト何それ？状態 ＋ 英語ドキュメントだったので、概要をつかむまでにかなり時間がかかりました。

[練習用に作った別リポジトリ](https://github.com/TakumiHANAUE/practiceNim/tree/main/getDockerImage)で、Docker registry API を使い方を確認してからこのステージに取り組みました。

https://github.com/TakumiHANAUE/practiceNim/tree/main/getDockerImage

## やってみた感想 :tada:

自分が普段使っている Docker を実際に作ってみるというのは楽しかったです。  
作ったプログラムは「イメージをpullしてコマンドを実行する」というだけのものでしたが、それでも実際に動いたときは嬉しかったです。
また、「Dockerってすごいな、こんな便利なものを作ってくれてありがとう！」という気持ちにもなりました。

関連技術・概念についても学ぶことができた点が良かったです。  
ステージ4以降を解くには、chroot や PID名前空間、 Docker Registry HTTP API など、言語の書き方以外の知識も同時に必要になりました。これらを学んでいる間は課題が進まずもどかしい部分もありましたが、学んだことをすぐに生かすことができ、理解も深まるのでとても良かったと思いました。

他言語での解法を参考にできる点も良かったです。  
CodeCraftersでは他の言語で解いた人のプログラムを見ることができます。どうやってプログラムを書いたら良いか分からず手が止まった時は、他の言語での解法を参考にさせてもらいました。あえてあまり触れたことのない言語の解法を見て、やるべき処理をなんとなく理解したあと、じゃあNimでどう書こうかということを考えながら取り組んでいました。同じ言語の解法を見てしまうと、どうしても自分で解いた感が薄れてしまいますが、そのあたりを防ぎつつ課題を進めることができる点が自分に合っていたと感じました。