Dockerを完全理解するためにコードを実行してくれるDiscord BotをKotlinで作った話

Kotlin(とか)をDiscord上で実行したいことってありますよね？
ありますよね？？

そんな夢を叶えるDiscord Bot 「こーどらんなー」を作ったので少し早いですがプレゼントします！！！

速く試させろって方は記事の一番下へ！

# 機能
まずは機能から紹介します！

## コードをDiscord上で実行できる！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/97f40779-8854-3e08-07d2-916865130f03.png)


おおおお！KotlinがDiscord上で！！！ ~~かわいいね~~

これがこーどらんなーを走らせるコマンドです
````conf:コマンド
!run
```<言語名|拡張子>
<実行ファイル内容>
```
# 下は任意
```input
<標準入力(stdin)に入れる内容>
```
````
````conf:コマンド
!run
```kt
fun main() = println("Hello World!")
```
````

もちろん標準入力にも対応しています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/6fbcd0da-d0b7-1c90-0bda-d6e83479f2a2.png)

## 対応言語 (2023/12/24時点)
|言語|ツール|ツールバージョン|
|--|--|--|
|Brainf*ck|`bf`|20041219|
|C|`gcc`|11.4.0|
|C++|`g++`|11.4.0|
|C#|`dotnet`|7.0.404|
|Go|`go`|1.10.6|
|Java|`java`|Temurin-17.0.9+9|
|JavaScript|`node`|18.18.2|
|Kotlin|`kotlinc`|1.9.10 (JRE 17.0.9+9)|
|なでしこ|`cnako3`|3.4.23|
|Python|`python`|3.12.0|
|Rust|`rustc`|1.73.0|
|WebAssembry (WAT)|`wasmtime`|15.0.1|

たまに変な言語がありますね・・・

言語追加要望は大歓迎です！
Debian・Ubuntu上で動くなら基本大丈夫です
それ以外でもなんとかします

# 実装とかの話

どうでもいいかもしれませんがここからは実装の話を一応書いておきます。


個人的な理由からKotlin以外の言語を使うことは許されなかったためこーどらんなーはDiscord Bot含むすべてがKotlinで書かれています。

![こーどらんなー.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/04754564-0587-d527-a4dc-66ad6ef14a1c.png)

BotとバックエンドはHTTPで通信してます。
本当はgRPCとか使ってみたいけど・・・

#### 主な使ったライブラリ
|用途|名前|
|--|--|
|サーバーフレームワーク|[Ktor](https://ktor.io)|
|Discord Bot|[kord](https://github.com/kordlib/kord)|
|Docker API|[docker-java](https://github.com/docker-java/docker-java)|


## 言語実行環境の定義
Dockerfileとmanifest.ymlを追加するだけで新しい言語を増やすことができます。
(あと `curl -X POST http://localhost:8080/runtime/rebuild`)
### 例 [Kotlin](https://github.com/naotiki/code-runner-runtimes/tree/main/kotlin) の場合

```dockerfile:Dockerfile
FROM eclipse-temurin:17-jdk
# 変数
ARG KOTLIN_COMPILER_URL="https://github.com/JetBrains/kotlin/releases/download/v1.9.10/kotlin-compiler-1.9.10.zip"
USER root
# 依存パッケージインストール
RUN apt update && apt install wget zip unzip -y
# ユーザー設定
ARG USERNAME=runner
ARG GROUPNAME=runner
ARG UID=1000
ARG GID=1000
RUN groupadd -g $GID $GROUPNAME && \
    useradd -m -s /bin/bash -u $UID -g $GID $USERNAME
# その他実行環境インストールなど (Option)
WORKDIR /opt
RUN wget $KOTLIN_COMPILER_URL -O kotlin.zip && \
    unzip kotlin.zip
ENV PATH $PATH:/opt/kotlinc/bin
# 初期ユーザー設定
USER $USERNAME
RUN mkdir /home/$USERNAME/work
WORKDIR /home/$USERNAME/work
CMD ["/bin/bash"]
```

```yml:manifest.yml
id: kotlin
name: Kotlin
sourcefileName: Main.kt
extension: kt
alias:
  - kotlin
  - kt
metaData:
  version: 1.9.10
  processor: JVM
commands:
  prepare: null
  compile: 'kotlinc -include-runtime Main.kt -d out.jar'
  execute: 'java -jar out.jar'
```

中のソースコードいじらなくても言語追加ができるのでとてもいいです。

# Q. 変なコード実行しても大丈夫なの？
A. 大丈夫です！

実行環境には様々な制限が掛かっており悪意あるコードもこーどらんなーの前では無力です

たぶん

制限の情報はBotの`/info host limit`から確認できます。
![limits.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/456419f8-91f9-071f-9a8b-8a2681fcc1f0.png)

ここからは対策の一例を挙げていきます
## 実行時間
実行時間が決まっておりこれを過ぎるとコンテナごと終了します。

コンパイル2分、実行10秒です。

無限ループしても10秒で強制終了です。
## メモリ
コンテナあたりのメモリが決まっており、それ以上のメモリ確保はできません。

いっぱい`malloc`したのに`free`しないで放置してやるぜ〜〜〜〜〜
ってコードを実行されても10秒後にはきれいに消えます
## フォーク爆弾
大量に子プロセスを増やすアレです
コンテナのプロセス数を制限することで対策しています

開発中知ったんですが対策なしでフォーク爆弾やられるとDocker Engine自体がすべての操作にエラー返すようになって正常に動かなくなるんですね・・・

↓ 実行中に力尽きたこーどらんなー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/5bbb4dcc-073f-0854-0378-a5685e9f0bea.png)
(Docker Desktopから見たCPU使用率の表示が `NaN%` になっていてびっくりしました)


`withPidsLimit`で対策すると一定のプロセス数を超えてからはしっかりエラーが起きるようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/92d24108-2283-27b4-9f17-9b20330fbe73.png)

## ディスク制限

Dockerでコンテナ毎のディスク使用量制限をする`--storage-opt size=XX`を使用するにはいくつか条件があります。

参考
https://docs.docker.com/engine/reference/commandline/run/#storage-opt

|ドライバー|ファイルシステム|備考|
|----|-------|-----|
|overlay2|xfs|マウントオプションにpquotaを設定する必要あり|
|devicemapper|-|デフォルトのBaseFSより小さいサイズは不可|
|btrfs|-|デフォルトのBaseFSより小さいサイズは不可|
|zfs|-|デフォルトのBaseFSより小さいサイズは不可|


今回は `overlay2 + xfs`を使用します。

`/etc/fstab`を https://reece.tech/posts/docker-container-size-quota/ を参考に編集しました。

```text:/etc/fstab
UUID=700696bd-cd1a-49b0-b17d-08b80f516965 /                       ext4    defaults,noatime                      1 0
UUID=618e96a4-aa0f-49a5-8d74-c0cecb9bd276 /boot                   ext4    defaults,noatime                      1 0
UUID=5232294a-cad8-4040-92c2-6cb5d3b62c4e /var/lib/docker         xfs     defaults,pquota                       0 0
```

これで /var/lib/docker に xfs & pquotaな領域が割り当てられるので`--storage-opt size=XX`が使えるようになりました！

しっかりと256MBまでになっています！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/f34fac18-9579-4187-fc1c-093f8ee97185.png)

# おわり
作ってみて、俺Dockerなにもわかってなかったなってなりました
今もよくわかりません。



# こーどらんなーを試す
試す用のDiscordサーバー作ったのでぜひ！！！
（まだ公開Botにはしません・・・）

https://discord.gg/5vrfpeTekw

# こーどらんなーと同じ環境でコードを実行する
各言語の実行環境をDockerイメージとして公開してます
```sh
docker run -it ghcr.io/naotiki/code-runner-runtimes/kotlin:main
```
[他のイメージ](https://github.com/naotiki?tab=packages&repo_name=code-runner-runtimes)

# ソースコード

https://github.com/naotiki/discord-code-runner

https://github.com/naotiki/code-runner-backend

https://github.com/naotiki/code-runner-runtimes