64bit ARM チップを積んだ Edge device 用に Docker image をビルドする

この記事は [ABEJA Platform Advent Calendar](https://qiita.com/advent-calendar/2018/abejaplatform) の 13 日目の記事です。

昨日までの Advent Calendar では、比較的 ABEJA Platform を利用する内容の記事が多かったので、本記事では、ABEJA Platform を開発するのにこんなことしてますよ（の一部）という内容を書きます。
以前、[ABEJA Arts Blog](https://tech-blog.abeja.asia/) にて[Jetson 上で Docker イメージをビルドするのが辛かったので EC2 上にビルド環境を作った](https://tech-blog.abeja.asia/entry/environment-of-building-docker-image-for-jetson)　という話を書きましたが、今回はその記事の焼き直しです。

## AWS EC2 A1 インスタンスファミリー
焼き直し記事を書かざるを得なかった理由がこれです。
そう、AWS EC2 に、ARM ベースのインスタンスファミリーが登場したので、以前書いた記事は要らない子になってしまったのでした。
というわけで今回は、A1 インスタンスを使った ARM 用 Docker image 作成方法について記載することにしました。

## A1インスタンスセットアップ
### choose ami for arm
AMI の検索窓に`arm`と入力すれば、arm用のamiを見つけられます。
![305b3ae3-213e-4c2e-923b-78ec743150a7.png](https://qiita-image-store.s3.amazonaws.com/0/8996/43927a85-50ce-95de-8b55-6c3ff6cd8f73.png)

arm 用の AMI を選択すると、「Choose an Amazon Machine Image」ページでも、A1インスタンスファミリーのみが選択できるようになってますね。当然といえば当然ですが、地味に便利。
![9367beb7-37f2-4a88-a9af-f6823c7ca619.png](https://qiita-image-store.s3.amazonaws.com/0/8996/77a1b482-0068-6a6a-aa0b-9324d304057b.png)

起動したインスタンスのアーキテクチャを見てみると、 `aarch64` となっていて、間違いなく 64bit ARM であることがわかります。

```bash:command-line
$ uname -m
aarch64
```

### Docker CE のインストール（必要な場合）
選択した AMI によっては、 `docker` がインストールされていない場合があります。 `ubuntu 16.04` とか。
せっかくなので最新の docker をインストールしたくなりますが、Docker の Documentation（たとえば [Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/)）を見ると、aarch64(arm64) について記載がなく、まさかの docker のビルドからか、、、と一気にモチベーションが下がりますが、実は [download.docker.com](https://download.docker.com/linux/) に aarch64 用のバイナリパッケージも配置してありました。
Ubuntu 16.04 用なら、 https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/arm64/ 配下に deb パッケージがあります。

あとは、 `sudo usermod -aG docker ubuntu` して logout/login すれば、 `sudo` 不要で `ubuntu` ユーザーから `docker` コマンドを利用できるようになります。

## パフォーマンス比較してみる
せっかくなので、以前まで使っていた QEMU 環境とのパフォーマンス比較もしてみました。
比較環境としては、それぞれ以下のインスタンスを利用しました。

- QEMU 環境側は `c5.4xlarge`
- A1 側は `a1.4xlarge`

コンピュート最適化インスタンスである C5 インスタンスファミリーから、 A1 インスタンスファミリー最多 vCPU を誇る `a1.4xlarge` と同じ vCPU 数の `c5.4xlarge` をチョイス。
Docker image のビルド用に作った環境なので、 `docker build` にかかる時間を比較します。
比較に利用する `Dockerfile`　は、先日業務で必要になった `llvmlite` ビルド用の Dockerfile をほぼそのまま使いました。

```docker:Dockerfile
FROM arm64v8/ubuntu:16.04                # for a1 instance
FROM multiarch/ubuntu-core:arm64-xenial  # for c5 instance
WORKDIR /tmp

ARG CPU_COUNT=16
ARG LLVM_VERSION=6.0.1
ARG LLVMLITE_VERSION=0.25.0

##############################################################################
# base tools
##############################################################################
RUN apt-get update \
    && apt-get install -y --no-install-recommends software-properties-common \
        unzip \
        wget \
        xz-utils \
        g++ \
        make \
        cmake \
        libedit-dev \
        patch \
    && add-apt-repository ppa:deadsnakes/ppa \
    && apt update \
    && apt install -y --no-install-recommends \
        python3.6 \
        python3.6-dev \
    && wget -O ~/get-pip.py https://bootstrap.pypa.io/get-pip.py \
    && python3.6 ~/get-pip.py \
    && ln -s /usr/bin/python3.6 /usr/local/bin/python3 \
    && ln -s /usr/bin/python3.6 /usr/local/bin/python \
    && rm ~/get-pip.py \
    && python -m pip --no-cache-dir install --upgrade setuptools \

##############################################################################
# build llvm
##############################################################################
    && wget https://github.com/numba/llvmlite/archive/v${LLVMLITE_VERSION}.zip \
    && unzip v${LLVMLITE_VERSION}.zip \
    && wget http://releases.llvm.org/${LLVM_VERSION}/llvm-${LLVM_VERSION}.src.tar.xz \
    && tar xf llvm-${LLVM_VERSION}.src.tar.xz \
    && cd llvm-${LLVM_VERSION}.src \
    && patch include/llvm/Analysis/CFGPrinter.h /tmp/llvmlite-${LLVMLITE_VERSION}/conda-recipes/twine_cfg_undefined_behavior.patch \
    && sed -i -e "/^# CPU_COUNT=1$/a _cmake_config\+=\(-DLLVM_TARGETS_TO_BUILD=\"ARM;X86;AArch64\"\)" /tmp/llvmlite-${LLVMLITE_VERSION}/conda-recipes/llvmdev/build.sh \
    && export CPU_COUNT=${CPU_COUNT} \
    && chmod 755 /tmp/llvmlite-${LLVMLITE_VERSION}/conda-recipes/llvmdev/build.sh \
    && /tmp/llvmlite-${LLVMLITE_VERSION}/conda-recipes/llvmdev/build.sh \

##############################################################################
# build llvmlite
##############################################################################
    && cd /tmp/llvmlite-${LLVMLITE_VERSION} \
    && python setup.py bdist_wheel
```

LLVM_TARGETS_TO_BUILD に X86 とか入ってますが気にしないでください。
`make` じゃなくて `ninja` でしょ、とか `clang` 使おうぜ、とかも見なかったことにしていただきたく。
あと、`FROM` 行を 2 行書いているのは、コメントでおわかりの通り、環境によって切り替えています。

上記 Dockerfile を、以下のコマンドで3回実行し、 `real` の時間で比較します。

```bash:command-line
$ time docker build -t llvmlite:latest -f Dockerfile --no-cache .
```

計測結果がこちら。

| instance type | 1回目 | 2回目 | 3回目 | 平均 | 時間単価(Oregon) |
| :---: | :---: | :---: | :---: | :---: | :---: |
| a1.4xlarge | 15m13.276s | 15m57.977s | 15m22.979s | 15m31.411s | $0.408/h |
| c5.4xlarge | 58m 14.567s | 58m54.770s | 59m42.746s | 58m57.361s | $0.68/h |

なんと a1.4xlarge の方が **4倍近く速い**という結果に。
それでいてお値段は 2/3 以下。

`c5.18xlarge` とか使えば、単純計算では `a1.4xlarge` を越えそうですが、 Dockerfile 内の処理が常にマルチコアを有効に使えるわけではないし、単価も跳ね上がるので、 64bit ARM 用の 作業をするなら A1 インスタンスファミリーが最適解かと思います。

## まとめ
- 64bit ARM 向けの Docker image のビルドは A1 インスタンスファミリーを使いましょう
- でもわざわざビルドしなくても ABEJA Platform ならビルド済みのイメージがイロイロ揃ってますよ（宣伝）