DockerfileにてなぜADDよりCOPYが望ましいのか


# はじめに

[Docker公式のBest practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#add-or-copy)に
ADDよりもCOPYが望ましい(ADDは使わない方が良い)との記述があるのを見つけたのでその理由を確認してみました。

# TL;DR

理由は以下の2つに分けることができます。

① Imageサイズの観点
② セキュリティの観点

# そもそもCOPYとADDはどう違うのか

[こちらの記事](https://snyk.io/blog/10-docker-image-security-best-practices/)にて端的にまとめられていたので日本語訳してしまいます。

> COPY  - 明示的なコピー元とコピー先のファイルまたはディレクトリを指定して、ローカルファイルを再帰的にコピーします。 COPYでは、場所を宣言する必要があります。

> ADD - ローカルファイルを再帰的にコピーし、存在しない場合は暗黙的にコピー先ディレクトリを作成し、アーカイブをコピー元としてローカルURLまたはリモートURLとして受け入れます。アーカイブはそれぞれコピー先ディレクトリに展開またはダウンロードされます。

`COPY`はソース側のマシンにあるものをImageへコピーするだけというシンプルな機能であるのに対し、`ADD`は対象が圧縮ファイルであればそれを展開してImageへ移し、リモート上のリソースも引っ張ってくる機能がついています。このCOPYに付与されたADDの機能自体が望ましくない原因になっています。

# Imageサイズの観点

上記の公式ドキュメントに記載の内容です。

`ADD`を使ってしまっているアンチパターンが以下になります。

```dockerfile:ダメな例
FROM alpine:3.4
RUN apk --update add curl

ADD https://www.python.org/ftp/python/3.7.3/Python-3.7.3.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/Python-3.7.3.tar.xz -C /usr/src/things

```

これの何がいけないかというと、`ADD https://...tar.xz /usr/src/things`の記述により結果的に不要なダウンロードされたディレクトリを持つImageレイヤーができてしまっている点です。

そうではなく、このようにしましょうということです。

```dockerfile:改良
FROM alpine:3.4
RUN apk --update add curl

RUN mkdir -p /usr/src/things \
    && curl -SL https://www.python.org/ftp/python/3.7.3/Python-3.7.3.tar.xz \
    | tar -xJC /usr/src/things
```

それぞれをビルドして`docker images`と`docker history`で比較してみましょう。

```terminal:ダメな例
$ docker images ng-sample
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ng-sample           latest              6f486d575040        15 minutes ago      103MB

$ docker history ng-sample
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
6f486d575040        15 minutes ago      /bin/sh -c tar -xJf /usr/src/things/Python-3…   79.3MB
7640da035c96        15 minutes ago      /bin/sh -c #(nop) ADD da2574b8e3fb62fc8442c4…   17.1MB
15a01069edfd        26 minutes ago      /bin/sh -c apk --update add curl                2.19MB
b7c5ffe56db7        3 months ago        /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>           3 months ago        /bin/sh -c #(nop) ADD file:bcde2f5c6cea41907…   4.82MB
```

```terminal:改良
$ docker images ok-sample
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ok-sample           latest              c88218b0c0cf        28 minutes ago      86.3MB

$ docker history ok-sample
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
c88218b0c0cf        28 minutes ago      /bin/sh -c mkdir -p /usr/src/things     && c…   79.3MB
15a01069edfd        29 minutes ago      /bin/sh -c apk --update add curl                2.19MB
b7c5ffe56db7        3 months ago        /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>           3 months ago        /bin/sh -c #(nop) ADD file:bcde2f5c6cea41907…   4.82MB
```

ダメな方では`ADD`で追加された圧縮ファイルを持ったレイヤー分サイズが大きくなってしまっています。改良版ではcurlした圧縮ファイルをパイプで`tar`へつなげているのでImageには残りません。

# セキュリティの観点

[こちらの記事](https://snyk.io/blog/10-docker-image-security-best-practices/)の内容の紹介になります。

上記の`ADD`の機能を**利用者が知らずに**使っている場合、`ADD`はセキュリティのリスクが生じてしまいます。

* リモート上のリソースを自動で取りに行くのでダウンロード中、中間者攻撃の対象となりえる
* 圧縮ファイルを自動で展開するので、[ZIP爆弾](https://en.wikipedia.org/wiki/Zip_bomb)や[ZIP SLIP脆弱性](https://snyk.io/research/zip-slip-vulnerability)攻撃の対象となりえる

ここで注意するべきは、上記のセキュリティリスクは`COPY`を使っている場合でも起こりえます。ただ、`COPY`を使っていれば、

* 明示的にリモートリソースからダウンロード(wget, curlなど)をする
* 圧縮ファイルを展開(tarなど)する

ので対象リソースが安全かを検証する仕組みを組み込みやすいということです。

# おまけ `COPY`のコツ

これも上記公式ドキュメントに記載の話です。

Imageキャッシュをできるだけ利用できるようにすることを意識してDockerfileを記述しましょう。

```dockerfile:ダメな例
COPY . /tmp/
RUN pip install --requirement /tmp/requirements.txt
```

```dockerfile:改良
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/
```

2つの違いがImageキャッシュ利用にどう関わるか理解しておく必要があります。
`RUN pip install --requirement /tmp/requirements.txt`は`requirements.txt`の差分のみに影響されて実行されるべきです。
ダメな方はそれ以外の`.`以下の何かが変更されるたび、`COPY . /tmp/`のレイヤーのImageコミットが変わるので、`RUN pip install`は以前のキャッシュを利用することができず毎回ダウンロードしてインストールすることになってしまいます。

# 参考

* https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
* https://snyk.io/blog/10-docker-image-security-best-practices/