DooD(Docker outside of Docker)で、Docker Composeで一部コンテナを逐次アップデートする

Docker Composeで複数コンテナからなるサービスを動かしている最中に、一部コンテナをアップデートしたいことがあります[^1]。

そこで、本記事ではその方法を紹介します(もし、さらに良い方法がありましたらぜひ伺いたいです)。

<!-- more -->

## 前提
例えば下図のサービス構成で、service_1~3を動的に更新することを考えます。

この構成では、apiコンテナがコンテナ外部とのインタフェースになっています。コンテナ更新用のAPIにリクエストがあると、core_controllコンテナを介してservice\_\*用imageコンテナを都度ビルド、新規コンテナをアタッチする想定です。

```
<network>     api             internal
        |--------------|  |---------------|

                                 <=> [service_1]
     [api] <=>  [core_controll]  <=> [service_2]
                                 <=> [service_3]
```

新たにコンテナをビルドをするにはDockerデーモンにアクセスする必要があるのですが、デフォルト設定ではコンテナ内からアクセスできません。

そこで、DooD(Docker outside of Docker)を使用します[^2]。DooDの説明にいては割愛します。

## 環境構築
### ファイル構成を準備する

以下の通りファイルを配置します[^3]。

```
some_service/
- docker-compose.yml
- api/
  - Dockerfile
  - ...(APIサーバ用コード)...
- core_controll/
  - Dockerfile
  - docker-compose.yml
  - ...(core_controll用コード)...

service1/
- Dockerfile
- ...(service1用コード)...

service2/
- Dockerfile
- ...(service1用コード)...

service3/
- Dockerfile
- ...(service1用コード)...
```

この構成において、some_service直下で`docker-compose up`を実行することによりapiコンテナ、core_controllコンテナを作成するようにしています。

### DooDを使えるようにする
some_service/docker-compose.ymlについて、volumesで`/var/run/docker.sock:/var/run/docker.sock`と指定します。

この設定がホスト環境のDockerのsockをcore_controllコンテナに共有する設定になります。これにより、コンテナ側からホスト側のDockerを操作できるようになります。

**some_service/docker-compose.yml**

```yaml
version: '3.4'

services:
  vision:
    container_name: "api"
    build:
      context: ./api
      dockerfile: Dockerfile
    restart: always
    tty: true
    ports:
      - "8000:8000"
    networks:
      - api

  core_controll:
    container_name: "core_controll"
    build:
      context: ./core_controll
      dockerfile: Dockerfile
    depends_on:
      - service1
      - service2
      - service3
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - api
      - internal

networks:
  api:
  internal:
```

### コンテナ内部でdocker-composeコマンドを使えるようにする
core_controllコンテナ内でdocker-composeコマンドを使えるようインストールに加えます。

(Alpine Linux環境のためapkを使っていますが、ベースOSに応じてyumやapt-get等に読み替えてください)

```Dockerfile
FROM node:14-alpine

RUN apk add --update docker-compose
```

### コンテナ内部でservice_\*用コードを取得、ビルドする機構を作る
コード上で、以下2処理を記載します。

- service1~3のコードをsome_service/下にフェッチします(サンプルコードではシェルスクリプトによりコピーしているので、割愛しています)。
- サブプロセスで`docker-compose up -d`を実行します。pythonの場合、`subprocess.run`、Node.jsの場合`child_process.exec`などが該当します。

### service1~3構成用Docker Compose設定を記載する
some_service/core_controll/docker-compose.ymlにservice1~3の構成を記載します。

```yaml
version: '3.4'

services:
  service1:
    container_name: "service1"
    build:
      context: ./service2
      dockerfile: Dockerfile
    networks:
      - internal

  service2:
    container_name: "service2"
    build:
      context: ./service2
      dockerfile: Dockerfile
    networks:
      - internal

  service3:
    container_name: "service3"
    build:
      context: ./service3
      dockerfile: Dockerfile
    networks:
      - internal

networks:
  internal:
    external:
      name: some_service_internal
```

このうち、networksでinternalネットワークと、some_service_internalネットワークを接続しています。このsome_service_internalネットワークは、some_service/docker-compose.ymlで設定したinternalネットワークに対応します。

以上で設定は完了です。

## service1~3コンテナを動的に更新する
※ 具体的な実装例、および動作をテストしてみたい場合はサンプルコードをご覧ください。

(事前処理) some_service/下で`docker-compose up -d`を実行し、apiコンテナとcore_controllコンテナを立ち上げておきます。

コンテナ更新用のAPIを呼び出し、core_controll下でdocker-composeコマンドによるビルドを行います。ビルドが成功していれば、service1~3コンテナが自動的にアタッチされます。

これによりサービス全体を動かしつつ、service1~3を動的に更新することができます。

## サンプルコード
以下URLを参照ください。
https://github.com/pirika-inc/docker_compose_hot_container_update_sample

以上になります。ご覧いただきありがとうございます！

[^1]: 例えば深層学習において、あるメンバーはTensorFlowで解析するサービス、あるメンバーはPyTorchで解析するサービスを作る、という構成が挙げられます
[^2]: 他にもDocker inside of Dockerもありますが、コンテナにprivileged権限を与える必要があります。セキュリティ観点上、--cap-dropや--deviceで権限を細かく制御する必要があり、公式にも推奨されません
[^3]: 実運用では、service_*をgitのリモートリポジトリなどから持ってくることになります。通信インタフェースが一致している限り、別機能を持ったサービスを持ってくることも可能です