セキュアで軽量なdistrolessコンテナを作成する

# はじめに

[NRI OpenStandia Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nri-openstandia)、15日目はコンテナ について考えます。

Linuxのコンテナ技術はプロダクション環境でも当たり前のように使われています。これに伴いコンテナ開発・運用の様々なベストプラクティスが提案されています。`distroless`なコンテナもベストプラクティスとして提唱されているものの1つです。
本記事ではこの`distroless`なコンテナについて考えます。
[#distrolessイメージを作成する](#distrolessイメージを作成する)では実際にdistrolessなコンテナイメージを作成します。

# distrolessとは

[Docker Hub](https://hub.docker.com)をはじめ、ネット上で様々なコンテナイメージ提供されていますが、これらのイメージは基本的にLinuxディストリビューションを含むコンテナイメージをベースとしています。
Linuxディストリビューションはdistro（ディストロ）とも呼ばれます。余談ですが、[DistroWatch](https://distrowatch.com/dwres.php?resource=popularity)のサイトでは注目されているLinuxディストリビューションの一覧を確認できます。

例えば、PythonのコンテナイメージではDocker Hub上で以下のようなタグのイメージが提供されています。これらのタグからPythonのコンテナイメージはalpineやdebian(bullseye、buster)をベースとして作られていることがわかります。

![Pythonコンテナタグ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705474/fd74349e-15a1-2905-8994-96cb04b10f13.png)

ベースとして利用されるLinuxディストリビューションはカーネルを除く基本的な設定ファイルやパッケージが一通り含まれます。そのため実際にほとんど使用しないような不要なファイルが多数含まれています。
distrolessなコンテナイメージは、こうした**不要なファイルを削除し、アプリケーションの実行に必要な最小限のファイルのみを含んだコンテナイメージ**のことを指します。

# distrolessはdistrolessではない

distrolessなコンテナイメージはその名前からLinuxディストリビューションが含まれていないように思われますが、そうではありません。
LinuxはLinuxカーネル(kernel space:カーネル空間)とそれ以外(user space:ユーザ空間)から構成されています。kernel spaceとuser spaceについてはRedHatのこちらの記事（[Architecting Containers Part 1: Why Understanding User Space vs. Kernel Space Matters](https://www.redhat.com/en/blog/architecting-containers-part-1-why-understanding-user-space-vs-kernel-space-matters)）が参考になります。

Linuxカーネルは基本的には[1種類](https://www.kernel.org)しかありません。またカーネルだけでOSを起動することもできないため、ユーザ空間に必要な設定ファイルや実行ファイルを用意しなければなりません。
この時のユーザ空間の作り込みの違いがLinuxディストリビューションの多様性を生みます。
そのためコンテナであってもOSを起動している時点でやはり何かしらのディストリビューションを含んでいると言えます。

distrolessなコンテナイメージは**パッケージマネージャーやshellなど、アプリケーションを実行する際には不要となるファイルを含まない**という意味でdistrolessと言われます。
重要な点なので強調しておきますが、**distrolessなコンテナイメージにはshellも含まれません。**

# なぜdistrolessなコンテナイメージを使うのか

* **理由① distrolessはスリム**

distrolessなコンテナイメージにはアプリケーションの実行に必要な設定や実行ファイルのみが含まれています。
最小限の構成にしているため、コンテナイメージのサイズを小さくすることができます。

* **理由② distrolessはセキュア**

繰り返しになりますが、distrolessなコンテナイメージはアプリケーションの実行に必要な最低限の設定や実行ファイルのみが含まれています。そのため不要なバグや脆弱性を埋め込みにくいという利点があります。これにより、使用していない機能に脆弱性があったというような理由でパッチを当てることも少なくなります。
不要なファイルを含まないことで、distrolessなコンテナイメージは攻撃対象領域(Attack Surface)を最小限に抑えています。

# distrolessはベストプラクティスか

sysdigが公表した[Top 20 Dockerfile best practices](https://sysdig.com/blog/dockerfile-best-practices/)の中ではdistrolessがベストプラクティスの１つだと述べられています。
また、[Technology Radar](https://www.thoughtworks.com/radar/techniques/distroless-docker-images)においても2021年4月時点でdistrolessはTrialのステージにあり、distrolessを検討することは企業にとって重要であるとしています。なおTechnology Raderでは技術を下記４つのステージに分類しています。

> **Adopt**: We feel strongly that the industry should be adopting these items. We use them when appropriate in our projects.
> **Trial**: Worth pursuing. It’s important to understand how to build up this capability. Enterprises can try this technology on a project that can handle the risk.
> **Assess**: Worth exploring with the goal of understanding how it will affect your enterprise.
> **Hold**: Proceed with caution.
> [Technology Radar](https://www.thoughtworks.com/content/dam/thoughtworks/documents/radar/2021/10/tr_technology_radar_vol_25_en.pdf)


個人的な意見としても、distrolessはベストプラクティスとして良いと思います。
下記２点においてdistrolessは確かに利用する価値はあります。

1. コンテナ イメージサイズが小さくなる
1. セキュリティリスクが低減される

しかし、一方で課題もあります。
distrolessイメージにはshellが含まれていません。
したがって下記のような事象が発生します。

1. ユーザビリティが損なわれる（デバッグがし辛いなど...）
1. shellがないためコンテナ内に入れない
1. shellなどでちょっとした処理を追加できない

これらの課題を克服するために、ログ設計を十分に検討し障害に備えることや、デバッグ用にshellなどを含んだイメージをあらかじめ用意しておくことが重要になるのではないかと考えています。
また、distrolessのなコンテナイメージの特徴をふまえた上でステークホルダーからの理解を得ることも大切になるのではないでしょうか。

distrolessコンテナについてはRedHatが興味深い記事を出しています。
[Why distroless containers aren't the security solution you think they are](https://www.redhat.com/en/blog/why-distroless-containers-arent-security-solution-you-think-they-are)
この記事ではdistrolessイメージの恩恵を十分に受けるために理解しておくべき事項が書かれています。記事内ではアプリケーションの標準化とソフウェア品質を高めることは、distrolessイメージを用いることよりも攻撃対象領域(Attack Surface)を低減させるのには有効であるとも述べられています。

# distrolessイメージを作成する

## GoogleContainerTools

distrolessなコンテナイメージの作成に使えるツールとしては[GoogleContainerTools/distroless](https://github.com/GoogleContainerTools/distroless)が有名(デファクトスタンダード)です。
コンテイメージのレポジトリは[gcr.io/distroless](https://console.cloud.google.com/gcr/images/distroless/GLOBAL)です。


## イメージ作成例① Python

ここではPythonを実行するためのdistrolessイメージを作成します。
実行するのは簡単なHelloWorld...ではなくHello Distrolessアプリとします。
`hello.py`に下記のように記述します。

```python:hello.py
import time

while True:
    print("Hello Distroless !!")
    time.sleep(1)
```

distrolessイメージをビルドするためにDockerfileを作成します。
ベースイメージとして[gcr.io/distroless/python3](https://console.cloud.google.com/gcr/images/distroless/GLOBAL/python3)を使用します。

```Dockerfile:Dockerfile
FROM gcr.io/distroless/python3:latest
COPY ./hello.py /app/
WORKDIR /app
CMD ["hello.py"]
```

作成したDockerfileを用いてコンテナイメージをビルドし、コンテナを起動してみます。
※分かりやすくするため一部出力を加工しています

```:distrolessコンテナのビルドと起動
D:\distroless_python>docker build -t distroless_hello_python ./
[+] Building 6.3s (8/8) FINISHED
~略~

D:\distroless_python>docker run -it --rm distroless_hello_python
Hello Distroless !!
Hello Distroless !!
^C
KeyboardInterrupt
```

distrolessコンテナにはshやbashなどのshellは含まれていないためコンテナ内に入ることはできません。

```:distrolessコンテナには入れない
D:\distroless_python>docker run -it --rm distroless_hello_python sh
/usr/bin/python3.9: can't open file '/app/sh': [Errno 2] No such file or directory

D:\distroless_python>docker run -it --rm distroless_hello_python /bin/sh
SyntaxError: Non-UTF-8 code starting with '\xac' in file /bin/sh on line 2, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

D:\distroless_python>docker run -it --rm distroless_hello_python bash
/usr/bin/python3.9: can't open file '/app/bash': [Errno 2] No such file or directory

D:\distroless_python>docker run -it --rm distroless_hello_python /bin/bash
/usr/bin/python3.9: can't open file '/bin/bash': [Errno 2] No such file or directory
```

distrolessコンテナであっても普通にLinuxが起動しているのでrootユーザやnonroot、nobodyユーザでコンテナを実行することができます。
※分かりやすくするため一部出力を加工しています

```:rootユーザ、一般ユーザで実行する
D:\distroless_python>docker run -it --rm -u root distroless_hello_python
Hello Distroless !!
Hello Distroless !!
^C
KeyboardInterrupt

D:\distroless_python>docker run -it --rm -u nonroot distroless_hello_python
Hello Distroless !!
Hello Distroless !!
^C
KeyboardInterrupt

D:\distroless_python>docker run -it --rm -u nobody distroless_hello_python
Hello Distroless !!
Hello Distroless !!
^C
KeyboardInterrupt
```

## イメージ作成例② WebAssembly

今度はWebAssemblyを実行するコンテナを作成してみます。
実行するのは先ほどと同様、HelloDistrolessと表示するアプリにします。
下記のように記述した`hello.c`を用意します。

```c:hello.c
#include <stdio.h>
#include <unistd.h>

int main()
{
    while(1){
        printf("Hello Distroless !!\n");
        sleep(1);
    }
    return 0;
}
```

上記のhello.cからWebAssemblyへコンパイルし、distrolessコンテナへ詰めるためにマルチステージビルドを行います。Dockerfileは以下のように記述します。
不要なものは含めないことがdistrolessなコンテナイメージの原則になります。
ベースイメージとして[gcr.io/distroless/cc](https://console.cloud.google.com/gcr/images/distroless/GLOBAL/cc)、WebAssemblyのランタイムとして[wasmtime](https://github.com/bytecodealliance/wasmtime/releases/tag/v0.31.0)を使用しています。

```dockerfile:Dockerfile
FROM hltj/wasi-sdk:latest AS builder
COPY hello.c /tmp/
WORKDIR /tmp
RUN /opt/wasi-sdk/bin/clang hello.c -o hello.wasm

FROM gcr.io/distroless/cc:latest
COPY wasmtime /app/
COPY --from=builder /tmp/hello.wasm /app/
WORKDIR /app
CMD ["./wasmtime", "hello.wasm"]
```

作成したDockerfileを用いてコンテナイメージをビルドし、コンテナを起動してみます。
※分かりやすくするため一部出力を加工しています

```:distrolessコンテナのビルドと起動
D:\distroless_wasm>docker build -t distroless_hello_wasm ./
[+] Building 1.4s (14/14) FINISHED
~略~                                                                              

D:\distroless_wasm>docker run -it --rm distroless_hello_wasm
Hello Distroless !!
Hello Distroless !!
~略~
```

# 参考資料

* [Why should I use distroless images?](https://github.com/GoogleContainerTools/distroless)
* [Top 20 Dockerfile best practices](https://sysdig.com/blog/dockerfile-best-practices/)
* [Why distroless containers aren't the security solution you think they are](https://www.redhat.com/en/blog/why-distroless-containers-arent-security-solution-you-think-they-are)