CircleCI上でDockerとファイルをうまくやりとりする

この記事は[ミクシィグループ advent calendar 2019](https://qiita.com/advent-calendar/2019/mixi)の12月25日分にあたります。

# TL;DR
 - CircleCI remote-docker上でvolumeが使えないので、コンテナの内と外でファイルを共用できず、つらい
 - シェルコマンドで解決する
 - machine executor ubuntuを用いて解決する



# CircleCI上でDockerとファイルをうまくやりとりする
先日登壇発表した[Nuxt.js SSR E2Eテストの実装](https://speakerdeck.com/samayotta/nuxt-dot-js-ssrniokerue2etesutofalseshi-zhuang)のこぼれ話です。
スライドの内容をかいつまむと、CI上でうまくDockerを走らせてスクリーンショットテストを実行するというものですが、実装中に頭を悩ませたのが**CircleCIでDocker Volumeが使えない**という問題でした。そのため、ヘッドレスブラウザで生成した画像をうまく外部にアップロードできないトラブルが発生していました。この記事ではその現象をうまく切り抜けるためのテクニックを紹介します。

## CircleCI 上でvolumeが使えない
さっそく具体例を見ていきます。

```
/blog
├── Dockerfile
├── asset
│   ├── a.txt
│   └── b.txt
└── docker-compose.yml
```
というフォルダがあったとしましょう。
このとき、docker-compose.ymlとDockerfileは次のように設定されています:

```yml:docker-compose.yml

version: '3.7'

services:
  blog:
    build: .
    tty: true
    volumes:
      - ./asset:/asset-in-docker
    command: ls /asset-in-docker
```

```docker:Dockerfile

FROM node:10-slim
```
Docker volumeとして`asset`ディレクトリをマウントし、その内部を`ls`するだけのシンプルな構造と仮定します。

### ローカルで実行すると:
```
$ docker-compose up

Starting blog_blog_1 ... done
Attaching to blog_blog_1
blog_1  | a.txt  b.txt
blog_blog_1 exited with code 0
```
確かに`a.txt` および `b.txt`があるのが見えます。
このまますんなりいってくれればよいのですが……。
### CIで実行すると:

```yaml:.circleci/config.yml

jobs:
  ls_asset:
    parallelism: 1
    docker:
      - image: circleci/node:12.13
    working_directory: ~/repo
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: docker-compose up
          command: docker-compose up
          working_directory: ~/repo/blog
```

**docker-compose up** を走らせると、

```
[circleci]$ docker-compose up

Starting blog_blog_1 ... done
Attaching to blog_blog_1
blog_blog_1 exited with code 0
```
`a.txt`と`b.txt`を見つけられませんでした。

## 原因特定
CircleCIに直接ssh接続してみると、Dockerコンテナ自体が`asset`ディレクトリをマウントできていないことが分かります。ここでこの情報をもとに調べてみると、
[Running Docker Commands - CircleCI](https://circleci.com/docs/2.0/building-docker-images/)
という公式ドキュメントが引っかかります。


>It is **not** possible to mount a volume from your job space into a container in Remote Docker (and vice versa). You may use the docker cp command to transfer files between these two environments.

曰く、remote docker環境ではボリュームをマウントできません。代わりに`docker cp`コマンドを使って二つの環境間でファイルを交換してくださいとあります。そもそもremote dockerとはdockerを安全にビルドするための隔絶された空間なので、二環境間でディレクトリのマウントができないのには納得が行きます。

## シェルコマンドで解決する

```docker:dockerfileに追記

COPY asset asset-in-docker
```
```
$ mkdir dir-in-ci
$ docker-compose restart blog && docker cp $(docker-compose ps -q blog):/repo/blog/asset ./dir-in-ci/
$ ls ./dir-in-ci
```
コンテナのビルド時にassetを含めるようにします。
その後ドキュメントの指示通りに`docker cp`コマンドを使い、ファイルをコンテナから取り出します。ここでもう一つちょっとした落とし穴があり、少なくとも2019年12月20日現在、ドキュメントに書いてあるサンプルコマンドでは失敗するはずです。`docker cp`を使うためにはそのコンテナが起動状態になければならないからです。ここでは`restart`を用いてコンテナを起動し、docker-compose経由でコンテナidを取得して`docker cp`します。各自の環境に合わせてうまく書き換えてください。

## machine executor ubuntuを用いて解決する
しかしこれでは完全な解決になったとはいえません。本来この仕事ではdocker volumeを使いたかったはず。CIのために`COPY`を使い、ローカル実行時にvolumeが使えない。帳尻を合わせるようにローカル専用コマンドを作るのでは何のためにdockerコンテナを書いたのか分からなくなってしまいます。どこでも同じように・誰もが見通し良くコンテナを使えるようになっていてほしいですね。
ここは是が非でもdocker volumeを貫き通したいと思い調べ続けていたところ、次の短いページを発見しました！

[How can I mount volumes to docker containers? - CircleCI](https://support.circleci.com/hc/en-us/articles/360007324514-How-can-I-mount-volumes-to-docker-containers-)

>It's not possible to use volume mounting with the docker executor, but using the machine executor it's possible to mount local directories to your running Docker containers.

:cool: 
machine executorすなわち実行マシンの設定を変えればいい！
ということで`ubuntu-1604:201903-01`を使ってみます。

```yaml.:circleci/config.yml

jobs:
  ls_asset:
    parallelism: 1
    machine:
      image: ubuntu-1604:201903-01
```
volumeを使用するため、Dockerfileとdocker-compose.ymlも最初の状態に戻しておきます。

すると...

```

[circleci]$ docker-compose up

Starting blog_blog_1 ... done
Attaching to blog_blog_1
blog_1  | a.txt  b.txt
blog_blog_1 exited with code 0
```

確かにローカルと同じように動作させることができました！

---
ただし[公式ドキュメント](https://circleci.com/docs/2.0/executor-types/#using-machine)曰く、machine-executorは新しいコンテナビルドの方式しかサポートしないないこと、今後のアップデートによっては課金要素になり追加料金を請求されるかもしれないという二点に注意が必要です。