Dockerfileの代わりの言語modus、使ってみた

https://modus-continens.com

# まとめ

現状だとメリットと学習コストが釣り合ってないから、まだ使わないかも！
今後書きやすくなったり、並列ビルドの機運が出てきた時に要検討といったところでした。


# インストール
linuxであればバイナリがあります。macの場合はビルドしないといけないみたいなのでcargoを使います

```bash
🎐 ❯ cargo install modus
```

# Modusfileを書く

書くだけであればほぼDockerfileなので例を見れば理解できると思います。

```Dockerfile
FROM ubuntu
ENV DEBIAN_FRONTEND noninteractive
WORKDIR /var/www/html
RUN apt update && \
    apt install -y git zip unzip libicu-dev libzip-dev
```

↓

```Modusfile
app :- (
  from("ubuntu"),
  run("apt update && \
      apt install -y git zip unzip libicu-dev libzip-dev")
)
::set_workdir("/var/www/html")
::set_env("DEBIAN_FRONTEND", "nontnteractive").
```

`app :-` ここはappという名前の新しいruleを定義しています。 `modus build . 'app'` というようにビルド対象を指定するのに使います。

他の書き方はドキュメントや、自分など他の人が書いたModusfileを参考にしてください。

https://docs.modus-continens.com/library/predicates/index.html

https://gist.github.com/sun-yryr/16c25e11e7d7ea54da96ce654d66e5f1

# 実行計画を見る、文法チェックする
文法が正しいかどうかや、実際に実行するとどんなイメージが出来上がるかを確認できます。

```bash
# まずghcr.ioにログインしてください（自分の環境では必要でした）
🎐 ❯ docker login ghcr.io -u {username}
# password: {PAT}
🎐 ❯ modus check . # -> OKなら何も出ない
🎐 ❯ modus proof . 'app'
1 proof(s) found for query app

└─ app
   ├─ (
   │  ├─ from("ubuntu")
   │  └─ run("apt update && apt install -y git zip unzip libicu-dev libzip-dev")
   ├─ )::set_workdir("/var/www/html")
   └─  ::set_env("DEBIAN_FRONTEND", "nontnteractive")
```

# ビルドする
modusでビルドしたイメージにはタグがついてないので、つける方法もドキュメントにあります。
実行結果はjsonの形式で出せるので、それをなんやかんやしてつけます。

```bash
🎐 ❯ modus build --json . 'app'
[
  {
    "predicate": "app",
    "args": [],
    "digest": "sha256:08495686536f9e93b6a71bbe44e6633ff772a5777bc3ba328ecd4295bce14185"
  }
]
# argsがない場合
🎐 ❯ modus build --json . 'app' | jq '.[] | [.digest, .predicate] | join(" ")' | xargs -I % sh -c 'docker tag %'
# args がある場合
🎐 ❯ modus build --json . 'app(x)' | jq '.[] | [.digest, .predicate + ":" + (.args | join("-"))] | join(" ")' | xargs -I % sh -c 'docker tag %'
```

# 実行する
ここまできたらただのdocker imageなので普通に使えます


# Modusっぽい書き方1 - ruleの引数

いくつかの環境で同じdockerfileを使う場合、どこか1つのコマンドだけ分けたいって場面ありますよね。

例えば本番環境とローカル環境であれば、本番に必要ないライブラリはインストールしないように書きたいです。modusだとこんな感じに書き分けることができます。

```Modusfile
setup_composer("local") :- run("composer install --no-scripts").
setup_composer("prod") :- run("composer install --no-scripts --prefer-dist --no-dev").

app(profile) :- (
  from("php"),
  copy("composer.json", "."),
  copy("composer.lock", "."),
  setup_composer(profile),
  copy(".", ".")
).
```

実行時の指定でどちらかの環境(profile)を選ぶこともできますし、まとめて指定することもできます。

```bash
# 指定する
🎐 ❯ modus proof . 'app("local")'
# 全て
🎐 ❯ modus proof . 'app(X)' # app(profile)でも可

# 結果
2 proof(s) found for query app(x)

└─ app("prod")
   ├─ from("php")
   ├─ copy("composer.json", ".")
   ├─ copy("composer.lock", ".")
   ├─ setup_composer("prod")
   │  └─ run("composer install --no-scripts --prefer-dist --no-dev")
   └─ copy(".", ".")

└─ app("local")
   ├─ from("php")
   ├─ copy("composer.json", ".")
   ├─ copy("composer.lock", ".")
   ├─ setup_composer("local")
   │  └─ run("composer install --no-scripts")
   └─ copy(".", ".")
```


# Modusっぽい書き方2 - 条件分岐をする

phpであればよくある（？）、「ローカル環境だけxdebugを入れたい！」といった要望もかけます。（が、仕様の制約でちょっと汚くなります）

```Modusfile
setup_composer("local") :- run("composer install --no-scripts").
setup_composer("prod") :- run("composer install --no-scripts --prefer-dist --no-dev").

setup_xdebug :- (
  run("pecl install xdebug && \
      docker-php-ext-enable xdebug"),
  copy("./xdebug.ini", "/usr/local/etc/php/conf.d/xdebug.ini")
).

app(profile) :- (
  from("php"),
  (
    (
      profile != "prod",
      setup_xdebug
    )
    ;
    (
      run("echo")
    )
  ),
  copy("composer.json", "."),
  copy("composer.lock", "."),
  setup_composer(profile),
  copy(".", ".")
).
```

```bash
🎐 ❯ modus proof . 'app(x)'
2 proof(s) found for query app(x)

└─ app("local")
   ├─ from("php")
   ├─ setup_xdebug
   │  ├─ run("pecl install xdebug && docker-php-ext-enable xdebug")
   │  └─ copy("./docker/8.1/php_xdebug.ini", "/usr/local/etc/php/conf.d/php_xdebug.ini")
   ├─ copy("composer.json", ".")
   ├─ copy("composer.lock", ".")
   ├─ setup_composer("local")
   │  └─ run("composer install --no-scripts")
   └─ copy(".", ".")

└─ app("prod")
   ├─ from("php")
   ├─ run("echo")
   ├─ copy("composer.json", ".")
   ├─ copy("composer.lock", ".")
   ├─ setup_composer("prod")
   │  └─ run("composer install --no-scripts --prefer-dist --no-dev")
   └─ copy(".", ".")
```

どうやら比較する対象が同じ属性？じゃないといけないみたいです。
今回の例だと `setup_xdebug` がlayer predicateだから、それと反対にもlayer predicateが必要といったところです。