docker applicationを動かしてみた

株式会社日立製作所　研究開発グループ　サービスコンピューティング研究部の長沼です。
今回はCloud Native Application Bundles（CNAB）という観点で、実装の1つであるdocker applicationを動かしてみましたので、その内容について紹介します。
CNABの概要については、[こちらの記事](https://qiita.com/yukinaganuma/items/daf17165d4c0b5eee5ed#cnab%E3%81%A8%E3%81%AF)を参照ください。

# docker applicationとは
[公式のREADME.md](https://github.com/docker/app)によれば、
docker CLI Pluginとして動作する、アプリケーションをインストール・共有する仕組みです。  
CNAB仕様と互換があり、Composeファイル（docker-compose.yml）から
CNABのinvocation image（アプリケーションをインストールするためのコンテナイメージ、すなわちインストーラ）を作成し、このinvocation imageをコンテナレジストリを用いて共有（配布・再利用）するための機能群を提供するものです。

以降では、この一連の流れを具体的な手順と合わせて説明していきます。

# docker applicationの準備

## 動作環境
今回使用するマシンのOSとdocker、およびdocker-app（docker applicationを実現するコマンド）のversionは以下の通りとなります。
- ubuntu 18.04 LTS
- docker
  - client: Docker Engine - Community 19.03.2
  - Server: Docker Engine - Community 19.03.2
- docker-app: v0.8.0

またアプリケーションを展開するためのプライベートレジストリ（[Harbor](https://goharbor.io/)）と、インストール先として上記マシン上で動作するdocker swarmクラスタ、も準備しておきます。

## インストール
まずdocker application(docker-appコマンド)のインストールを以下手順に従い実施していきます。
https://github.com/docker/app#installation

上記で紹介されているようにdocker-appは、スタンドアローンとして利用する方法と、
docker CLI Pluginによりdockerのサブコマンドとして利用する方法があります。  
今回は、dockerのサブコマンドとして利用する方法を使う方法で紹介していきます。
具体的には、以下コマンドを実行します。  

```console
# docker-appのdownload
$ export OSTYPE="$(uname | tr A-Z a-z)"
$ curl -fsSL --output "/tmp/docker-app-${OSTYPE}.tar.gz" "https://github.com/docker/app/releases/download/v0.8.0/docker-app-${OSTYPE}.tar.gz"
$ tar xf "/tmp/docker-app-${OSTYPE}.tar.gz" -C /tmp/

# docker CLI plugin利用の設定
$ mkdir -p ~/.docker/cli-plugins && cp "/tmp/docker-app-plugin-${OSTYPE}" ~/.docker/cli-plugins/docker-app
```

appがサブコマンドとして認識されない(`'app' is not a docker command.`などとなる）場合、[dockerのexperimental](https://github.com/docker/cli/tree/master/experimental)を有効にすることで解決する場合があります。
例えば、以下を実行します。

```console
$ export DOCKER_CLI_EXPERIMENTAL=enabled
```

# docker applicationの利用

docker applicationを利用する上で必要となる一連の流れとして、
- invocation imageの元となるコンテンツ開発
- CNAB invocation imageへのビルド、
- レジストリへのpush/pull、
- アプリケーションのインストール
を以下で紹介していきます。

## コンテンツの開発
docker applicationは主に以下の３つのファイルからなります。
- metadata file (metadata.yml): アプリケーションのバージョンや説明などのメタ情報を記述
- compose file (docker-compose.yml)： アプリケーションで使うイメージやポートなどの構成を記述
- parameter file (parameters.yml)： compose fileに与える変数の定義を記述

以降では、[docker applicationの例](https://github.com/docker/app#looking-at-an-example)でも用いられている、指定メッセージを表示する簡単なWebサーバを動作させるアプリケーションの作成を例に挙げ、説明を行っていきます。

まず上記ファイルの雛形をinitコマンドを使って生成します。

```console
$ docker app init hello-message
```

するとhello-message.dockerappというディレクトリが作成され、３つのファイルの雛形が作成されているのが確認できます。  
なおinitコマンドに`--single-file`オプションを付けると、この３つのファイルを一つにまとめた１ファイル（今回の場合はhello-message.dockerappというYAMLファイル）
として雛形を出力することもできます。

```console
$ tree hello-message.dockerap
hello-message.dockerapp/
├── metadata.yml
├── parameters.yml
└── docker-compose.yml
```

この雛形を編集して、実際に動かすアプリケーションに必要な情報へ書き換えを行います。  
ここでアプリケーション動作時に変更したい（変数化しておきたい）項目は、parameters.ymlにkey:valueの形式で記述し、docker-compose.yml側で変数`${key}`として埋め込むことができます。  
今回の例では、表示するメッセージ（docker-compose.ymlの`${message}`）と公開ポート(同じく`${ports.port1}`)を変数としています。

```yaml:metadata.yml
# アプリケーションのバージョン
version: 0.1.2
# アプリケーションの名前
name: hello-message
# アプリケーションの簡単な説明
description: This application returns a hello message for HTTP request.
# アプリケーションやパッケージのメンテナの名前・E-Mail
maintainers:
  - name: maintainer.name
    email: maintainer@email.address
```

```yaml:parameters.yml
# docker-compose.ymlの${message}に対応するkeyとvalue(デフォルト値)
message: world 
# keyを階層構造にもできる
ports:
  port1: 5678 
```

```yaml:docker-compose.yml
version: "3.3"
services: 
  hello-server:
    image: hashicorp/http-echo
    # ${message}は変数として扱われる
    command: ["-text", "hello ${message}"]
    ports:
      - ${ports.port1}:5678
```

これら用いて、`docker app render`により変数化部分に値が入ったdocker-composeファイルを生成することもできます。
以下ではメッセージ部分はデフォルト値（parameters.yamlで指定した値）、公開ポート部分は`--set`オプションで変更した値、で生成したdocker-composeファイルを生成する例を示しています。

```console
$ docker app render --set ports.port1=8080
version: "3.3"
services:
  hello-server:
    command:
    - -text
    - hello world
    image: hashicorp/http-echo
    ports:
    - mode: ingress
      target: 5678
      published: 8080
      protocol: tcp
```

## invocation imageのビルド・レジストリへのpush/pull

### invocation imageのビルド
`docker app bundle`コマンドにより、hello-message.dockerappに対する
- [CNABのinvocation image（アプリのインストーラ）](https://github.com/deislabs/cnab-spec/blob/master/102-invocation-image.md)の作成
- [CNABのbundle.json（CNABが規定するパッケージのメタ情報）](https://github.com/deislabs/cnab-spec/blob/master/101-bundle-json.md)
を行うことができます。

```console
# invocation imageのbuild
$ docker app bundle
Invocation image "hello-message:0.1.2-invoc" successfully built

# invocation imageの確認
$ docker images | grep hello-message
hello-message          0.1.2-invoc         fbce87b60497        23 seconds ago      49.1MB

# bundle.jsonの確認
$ls .
bundle.json  docker-compose.yml  metadata.yml  parameters.yml
```

ちなみに現在のdocker-appの実装では、実は`docker app render`でもCNAB invocation imageのビルドが行われるようです。
変数に値を入れたdocker-composeファイルを生成するにしては処理時間がかかると感じたら、実はイメージのビルドも行っていたんですね。。

### 作成したinvocation imageの中身
作成したinvocation image（hello-message:0.1.2-invoc）とは、いったいどんなものか気になり、中身をのぞいてみました。

```console
$ docker inspect hello-message:0.1.2-invoc
[
  {
    ...
    "Config": {
　    "Hostname": "",
      "Domainname": "",
      "User": "cnab",
      "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],
      "Cmd": [
        "/bin/sh",
        "-c",
        "/cnab/app/run"
      ], ...,
    }, ...
  }
]
```

から分かるように、invocation image実行時は、イメージ内の/cnab/app/runが起動されるようです。  
invocation image内の/cnab内にどのようなものが格納されているかを見てみると、以下のように今回作成したhello-message.dockerappとrunコマンド（バイナリ）が格納されたものであることが確認できました。

```
# tree /cnab の結果
cnab
└── app
    ├── hello-message.dockerapp
    │   ├── bundle.json
    │   ├── docker-compose.yml
    │   ├── metadata.yml
    │   └── parameters.yml
    └── run
```


### レジストリへのpush・pull
さらに作成したinvocation imageは、レジストリに格納することができます。
これにより、dockerイメージと同じようにレジストリを介し、invocation image（すなわちアプリのインストーラ）を配布・再利用することができるようになります。

以降ではローカルに立てたレジストリ（registry:5000）を介してinvocation imageをpush/pullする例を示しています。
今回はinsecure registryを使ったため、`--insecure-registries`オプションを付ける必要がありました。

```console
# レジストリへpush
$ docker app push --insecure-registries registry:5000 --tag registry:5000/hello-message:0.1.2

# レジストリからのpull
$ docker app pull --insecure-registries registry:5000 registry:5000/hello-message:0.1.2
```

さらに`docker app inspect`をpush前後で比較してみると単純なpushではなく、以下のようにアプリケーションのイメージ（hello-serverに対応するimage）
も参照場所が書き換えられていました。
すなわちinvocation imageだけでなく、invocation imageに含まれるアプリのイメージも、今回invocation imageを格納したレジストリへ集約しているようです。

```console
# hello-message.dockerappディレクトリ直下で実行した結果
$ docker app inspect .
hello-message 0.1.2

Maintained by: maintainer.name <maintainer@email.address>

This application returns a hello message for HTTP request.

Service (1)  Replicas Ports Image
-----------  -------- ----- -----
hello-server 1        5678  hashicorp/http-echo

Parameters (2) Value
-------------- -----
message        world
ports.port1    5678

Attachments (7)                                     Size
---------------                                     ----
bundle.json                                         2.345kB
...(略)


# pushしたregistry:5000/hello-message:0.1.2に対して実行するとhello-serverのImage部分が変更されている
$ docker app inspect --insecure-registries registry:5000 registry:5000/hello-message:0.1.2
hello-message 0.1.2

Maintained by: maintainer.name <maintainer@email.address>

This application returns a hello message for HTTP request.

Service (1)  Replicas Ports Image
-----------  -------- ----- -----
hello-server 1        5678  registry:5000/hello-message@sha256:ba27d460cd1f22a1a4331bdf74f4fccbc025552357e8a3249c40ae216275de96

Parameters (2) Value
-------------- -----
message        world
ports.port1    5678

Attachments (7)                                     Size
---------------                                     ----
bundle.json                                         2.345kB
...
```

## アプリの起動

このようにビルドしたinvocation imageは、現状ではdocker swarmとkubernetesへインストール（デプロイ）することができます。
swarmの場合は、以下のようにしてアプリをインストール・確認できます。

```console
# hello-messageのswarmへのインストール
$ docker app install --insecure-registries registry:5000 registry:5000/hello-message:0.1.2

# インストール結果の確認
$ docker app list
INSTALLATION  APPLICATION           LAST ACTION RESULT  CREATED    MODIFIED   REFERENCE
hello-message hello-message (0.1.2) install     success 12 seconds 11 seconds registry:5000/hello-message:0.1.2

# アンインストール
$ docker app uninstall hello-message
```

kubernetesも、[compose-on-kubernetes](https://github.com/docker/compose-on-kubernetes/tree/master/docs)でAPIを拡張したkubernetesクラスタに対して、`--orchestrator kubernetes`オプションを付けることでアプリケーションをインストールできるとのことです。
ただ今回準備した環境ではうまく動かすことができず、こちらは引き続き調査などを行っていきたいと思います。


# まとめ
今回はdocker-appを使って、コンテンツの準備、CNABのinvocation imageのビルド/push/pull、およびアプリのインストールまでの流れを紹介しました。  
これまでの資産として既にdocker-composeファイルで動作するアプリケーションがあり、これをCNAB仕様に従って共有したいようなユースケースには、うまくフィットする気がしました。
特にdocker app pull/pushという、慣れ親しんだdokcerコマンドの使い勝手でinvocation imageを配布/再利用（push/pull）できる仕組みはステキに感じました。  
またCNAB向けのレジストリという点では[cnab-to-oci](https://github.com/docker/cnab-to-oci)というプロジェクトでも議論されており、こういった周辺動向含め引き続き見ていきたいと思います。

# 参考資料
- CNAB: https://github.com/deislabs/cnab-spec
- docker-app: https://github.com/docker/app
- cnab-to-oci: https://github.com/docker/cnab-to-oci