ConoHa VPSで(筆者が)学ぶDockerによるクロスビルド

# はじめに

これは[ConoHa Advent Calendar 2024](https://qiita.com/advent-calendar/2024/conoha)の12日目の記事です。

私は最近なんか手広くやっていますがたぶん本業は組み込みLinux開発ということになっており、息をするようにArm Linuxバイナリをひねり出す日々を送っております。そのためにいまのところは普通にローカルホストにクロス環境を構築しているわけですが、なんか昨今そういうのは[Docker](https://www.docker.com/ja-jp/)でやるのがナウでヤングじゃんという噂を小耳にはさみ、またCIの設定でわけわからんまま雰囲気でDockerイメージ作ったりしたのもそのままじゃよくないし、いいかげんおべんきょうしないとなあと思ったというわけです。

というわけで、この記事ではDockerを学んで[Rust](https://www.rust-lang.org/ja)のArm Linuxクロス環境をDockerコンテナー内に作り、それを使ってクロスビルドを行えるようになるぞ! という目標を設定し、今回はそのためのホスト環境を[ConoHa](https://www.conoha.jp/conoha/) VPS上に構築します。これは気軽に立てられて用途が終わったら削除できる、またクラウド上にあれば出先からでもアクセスできるというメリットを享受しようというものです。

ヤングもすなるDockerといふものを`検閲により削除`もしてみむとてするなり。

なお、この記事の執筆にあたり作成したファイルは[GitHubに上げてあります](https://github.com/ygohko/TestDocker)。

# サーバーを立てる

ではさっそくサーバーを立てていきましょう。VPS Ubuntu 24.04 RAM 1GBとかで適当に立てます。立ち上がったら、sudoができる一般ユーザーが公開鍵認証でSSHログインできるように設定していきます。あと、私は通勤電車からスマートフォンで操作したりするので[Mosh](https://mosh.org/)も設定します。まあ、後者についてはほとんどの人は必要ないかもしれませんが...

ファイアウォールがデフォルトで有効になっており、SSH(TCP 22)とMosh(UDP 60000 - 61000)が通るように設定してあげる必要があります。[してあげましょう](https://qiita.com/010Ri/items/f37f7f5fb35265823c7c)。

そして今回のハマりポイントですが、ConoHa Ver.3.0になって[セキュリティグループ](https://support.conoha.jp/v/v3-security-securitygroup/)という概念が追加されています。VPSの外側にもファイアウォールのようなものがある感じですかね? こちらも同様に必要なポートを開けてあげましょう。

SSHなりMoshなりで接続できましたか? できたっぽかったら次に進みましょう。

# Docker Engineをインストールする

続いて[Docker Engine](https://docs.docker.com/engine/)をインストールします。とはいってもやることは[公式ドキュメントの内容](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository)そのまんまです。

まずはaptリポジトリを設定します。

```
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
```

設定できたらインストールします。

```shell
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

the Docker Communityが提供しているhello-worldイメージが実行できたら準備としてはOKみたいです。

```shell
$ sudo docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
	(amd64)
 3. The Docker daemon created a new container from that image which runs the
	executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
	to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
```

動いたっぽい!

# Lesson 1: Hello, World!を書く

準備ができたところで、では実際にDockerイメージの作り方を学んでいきましょう。ここではよいこのみんなのおやくそくとしてHello, World!をやってみます。hello-worldイメージの実行はさっきやったところですが、次はそれを自分で作れるようになるということが必要です。

以下のようなDockerfileを書きました。

```dockerfile:Dockerfile
# 元となるイメージ
FROM ubuntu:24.04

# イメージ作成時のコマンド
RUN apt update && apt install -y python3

# 作業ディレクトリ
WORKDIR /work

# ファイルのコピー
COPY hello.py /work

# コンテナ起動時のコマンド
CMD [ "python3", "/work/hello.py" ]
```

また、以下のようなPythonコードを書いて、hello.pyとして保存しました。

```python:hello.py
if __name__ == "__main__":
    print("Hello, World!")
```

では、これらを使用してtestdocker:1というイメージをビルドしてみます。

```shell
$ sudo docker build -t testdocker:1 .
```

イメージのビルドがうまくいったようなら実行してみましょう。

```shell
$ sudo docker run testdocker:1
Hello, World!
```

動きました! まずは第一歩を踏み出しましたね。

# Lesson 2: ファイル入出力を行う

目標を達成するためにはコンテナー内でコンパイラーを動かしたいわけですが、それにはホスト側からソースファイルを与えてバイナリファイルを得るという、ファイルの入出力ができないといけません。ひとまずそのへんの実験からしていきます。

以下のようなDockerfileを書きました。

```dockerfile:Dockerfile
FROM ubuntu:24.04

RUN apt update && apt install -y python3

WORKDIR /work

CMD [ "python3", "/work/hello.py" ]
```

また、以下のようなPythonコードを書いて、hello.pyとして保存しました。今回は標準出力の他にファイル greeting.txtにもHello, World!を出力してみます。

```python:hello.py
if __name__ == "__main__":
    greeting = "Hello, World!"
    print(greeting)
    with open("greeting.txt", "w") as file:
        file.write(greeting + "\n")
```

ではこれらを使用してtestdocker:2というイメージをビルドしてみます。

```shell
$ sudo docker build -t testdocker:2 .
```

イメージのビルドがうまくいったようなら実行してみましょう。この時、-vオプションでカレントディレクトリをコンテナーの/workにマウントするよう指定するのがポイントです。

```shell
$ sudo docker run -v .:/work testdocker:2
Hello, World!
```

ホスト側にあるhello.pyをコンテナー側のpython3で実行することができました。では、ファイルの出力はできているでしょうか。

```shell
$ cat greeting.txt
Hello, World!
```

ちゃんとできているようです。ファイルの入出力がともにできるということはわかったので、あとはPythonのインタプリターがRustのコンパイラーに変われば目標を達成できそうです。

# Lesson 3: Rustのビルドを行う

ではいよいよ、Rustのビルドに挑戦してみましょう。いきなりクロスビルドだと話がややこしくなるので、まずはx86-64 Linuxのセルフビルドからやってみます。

実験台としては拙作の[zatsu](https://github.com/ygohko/zatsu)を使用します。とりあえずGitHubからソースコードを取ってきましょう。私は最近バージョン管理システムとして[Jujutsu](https://github.com/martinvonz/jj)を使用しているので、Jujutsuリポジトリの初期化もおこないます。

```shell
$ git clone https://github.com/ygohko/zatsu.git
$ cd zatsu/
$ jj git init --colocate
```

:::note
余談です。2024年はバージョン管理システム Jujutsuを知り、導入したことが最大の作業効率改善になりました。私はGitのコマンドラインデザインが本当に苦手で、シンプルで合理的、柔軟で強力なJujutsuに大変助けられた1年でした。

まだ未実装の機能がある、バージョンアップで破壊的な仕様変更が入ることがある(最近も従来branchと呼ばれていたものがbookmarkと改称されたばかり)など未完成なところも散見されますが、Gitに不満がある方は一度試してみてはいかがでしょうか。興味がある方は以下に示すサイトをご参照ください。

* [君のレポジトリを領域展開 - 次世代バージョン管理システム Jujutsu の世界](https://zenn.dev/zetamatta/books/c1e309aea68960)
* [【翻訳】 jj init （バージョン管理システム Jujutsu の紹介）](https://zenn.dev/wartemeinnicht/articles/3059bcf2cd186d)
* [jujutsu こそ、僕たちの欲しかった VCS だ!!](https://zenn.dev/tommy109/articles/d63b46f398379c)
:::

さて、余談も終わってイメージの準備。以下のようなDockerfileを書きました。

```dockerfile:Dockerfile
FROM rust:latest

WORKDIR /work

CMD [ "cargo", "build" ]
```

the Rust Project developers⁠が提供しているオフィシャルイメージを元に、起動したら/workでcargo buildが実行され、Rustプロジェクトのビルドが行われるようにします。では、testdocker:3というイメージをビルドしてみましょう。

```shell
$ sudo docker build -t testdocker:3 .
```

イメージのビルドがうまくいったようなら、**zatsuのソースコードディレクトリに移動してから**以下のように実行します。

```shell
$ sudo docker run -v .:/work testdocker:3
```

バイナリのビルドがうまくいったようなら確認してみましょう。

```shell
$ file target/debug/zatsu
target/debug/zatsu: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ab4633430c74eaf4088fed6e02d9719bfb9fcdda, for GNU/Linux 3.2.0, with debug_info, not stripped
$ target/debug/zatsu --help
Usage: zatsu [COMMAND]

Commands:
  init     Initialize a repository into this directory
  commit   Commit current files into this directory's repository
  log      Show logs of this directory's repository
  get      Get a file or directory that is specified
  forget   Remove stored revisions to shrink this directory's repository to specified size
  upgrade  Upgrade this repository
  help     Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help                                  
```

動いているようです。やったね!

# Lesson 4: Armのクロスビルドを行う

ついに最終段階、Armのクロスビルドに挑みます。以下のようなDockerfileを書きました。

```dockerfile:Dockerfile
FROM rust:latest

# クロス環境のインストール
RUN apt update && apt install -y g++-arm-linux-gnueabihf libc6-dev-armhf-cross
RUN rustup target add armv7-unknown-linux-gnueabihf
RUN rustup toolchain install stable-armv7-unknown-linux-gnueabihf

WORKDIR /work

# リンカーの指定
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc

CMD [ "cargo", "build", "--target", "armv7-unknown-linux-gnueabihf" ]
```

Lesson 3との違いはクロス環境のインストール、リンカーの指定、そしてコンテナ起動時のコマンドに--targetオプションが追加されることです。では例によって、testdocker:4というイメージをビルドしてみましょう。

```shell
$ sudo docker build -t testdocker:4 .
```

イメージのビルドがうまくいったようなら、**zatsuのソースコードディレクトリに移動してから**以下のように実行します。

```shell
$ sudo docker run -v .:/work testdocker:4
```

バイナリのビルドがうまくいったようなら確認してみましょう。

```shell
$ file target/armv7-unknown-linux-gnueabihf/debug/zatsu
target/armv7-unknown-linux-gnueabihf/debug/zatsu: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamical
ly linked, interpreter /lib/ld-linux-armhf.so.3, BuildID[sha1]=72c8a89e67d33c6f687c706ed6854e631adfa356, for GNU/Linux
3.2.0, with debug_info, not stripped
$ target/armv7-unknown-linux-gnueabihf/debug/zatsu
bash: target/armv7-unknown-linux-gnueabihf/debug/zatsu: バイナリファイルを実行できません: 実行形式エラー
```

ちゃんとArmのバイナリが出力されているっぽいことがわかりました。動作確認はできていませんが、まあたぶんしかるべき環境に持っていけば動くでしょうからこれで目標達成ということにしてしまいましょう...

# おわりに

この記事ではConoHa上にサーバーを立てるところから始めて順を追ってDockerの動作を学び、実際にRustのクロスビルドが行えるようになりました。この方法を用いればローカルホストから隔離されたクロス環境を作ることができ、どんなにターゲットが増えてもホスト環境をクリーンに保つことができそうです。また、この方法はクロスビルドに限らず、古い処理系じゃないと動かないようなプロジェクトをお守りするなんて時にもたぶん有効でしょう(まあ、そんな機会はないほうがいいに決まっていますが...)。

そして、今回はConoHa VPS上に環境を作ったことで通勤中や外食の提供待ちなどの空き時間にも学習を進めることができ、大変助かりました。

というわけで、今回は自分用の記録としての側面が強い記事でしたが、お読みになったみなさんのお役に立つ機会がありましたら幸いです!