コンテナイメージは本当にディストリビューションレスですか?

コンテナ化は、エンジニアがアプリケーションのランタイム環境をより詳細に制御できるようにすることで、アプリケーションのセキュリティを大幅に向上させるのに役立ちました。 しかし、これらのアプリケーションのセキュリティ体制を維持するには、新しい脆弱性が日々発見され、言語やフレームワークが定期的にリリースされていることを考えると、かなりの時間の投資が必要です。 

「ディストリビューションレス」イメージの概念は、一般的なコンテナイメージに含まれるソフトウェアのほとんどを排除することで、アプリケーションのセキュリティを維持するために必要な時間を大幅に短縮することを約束します。 また、このアプローチにより、チームは脆弱性の修正に費やす時間が短縮され、使用しているソフトウェアのみに集中できるようになります。 

この記事では、イメージをディストリビューションレスにする理由を説明し、ディストリビューションレスイメージの作成を実用的にするツールについて説明し、ディストリビューションレスイメージがその可能性に応えるかどうかについて説明します。

2400x1260 あなたのイメージは本当にディストリビューションレスですか

ディストリビューションとは何ですか?

Linuxディストリビューションは、Linuxカーネルを中心に構築された完全なオペレーティングシステムであり、パッケージ管理システム、GNUツールとライブラリ、追加のソフトウェア、および多くの場合、グラフィカルユーザーインターフェイスで構成されています。

一般的なLinuxディストリビューションには、Debian、Ubuntu、Arch Linux、Fedora、Red Hat Enterprise Linux、CentOS、およびAlpine Linux(コンテナの世界ではより一般的)が含まれます。 これらのLinuxディストリビューションは、ほとんどのLinuxディストリビューションと同様に、セキュリティを真剣に扱っており、チームは既知の脆弱性に対するパッチとアップデートを頻繁にリリースするために熱心に取り組んでいます。 すべてのLinuxディストリビューションが直面しなければならない重要な課題には、ユーザビリティとセキュリティのジレンマが含まれます。 

Linuxカーネル自体はあまり使い勝手が悪いため、多くのユーティリティコマンドがディストリビューションに含まれており、さまざまなユースケースに対応しています。 追加のパッケージをインストールすることなく、適切なユーティリティをディストリビューションに含めることで、ディストリビューションの使いやすさが大幅に向上します。 ただし、このユーザビリティの向上の欠点は、最新の状態に保つための攻撃対象領域が増えることです。 

Linuxディストリビューションは、これら2つの要素のバランスをとる必要があり、ディストリビューションが異なれば、そのためのアプローチも異なります。 覚えておくべき重要な側面は、ユーザビリティを重視するディストリビューションは、ユーザビリティを重視しないディストリビューションよりも「安全性が低い」わけではないということです。 つまり、より多くのユーティリティパッケージを備えたディストリビューションは、それを安全に保つためにユーザーからより多くの努力を必要とするということです。

マルチステージビルド

マルチステージ ビルドを使用すると、開発者はビルド時の依存関係をランタイムの依存関係から分離できます。 開発者は、必要なすべてのコンポーネントがインストールされたフル機能のビルドイメージから開始し、必要なビルドステップを実行してから、それらのステップの結果のみを「スクラッチ」と呼ばれるより最小限の、または空のイメージにコピーできるようになりました。 このアプローチでは、依存関係をクリーンアップする必要がなく、さらに、ビルド ステージもキャッシュ可能であるため、ビルド時間を大幅に短縮できます。 

次の例は、マルチステージビルドを利用するGoプログラムを示しています。 Golangランタイムはバイナリにコンパイルされるため、バイナリ証明書とルート証明書のみを空白のスレートイメージにコピーする必要があります。

FROM golang:1.21.5-alpine as build
WORKDIR /
COPY go.* .
RUN go mod download
COPY . .
RUN go build -o my-app


FROM scratch
COPY --from=build
  /etc/ssl/certs/ca-certificates.crt
  /etc/ssl/certs/ca-certificates.crt
COPY --from=build /my-app /usr/local/bin/my-app
ENTRYPOINT ["/usr/local/bin/my-app"]

BuildKit

ビルドKによって docker build 現在使用されているエンジン である は 、拡張可能でプラグ可能なアーキテクチャのおかげで、開発者が最小限のイメージを作成するのに役立ちます。代替フロントエンド (デフォルトは使い慣れた Dockerfile) を指定して、ディストリビューション イメージの作成の複雑さを抽象化して隠す機能を提供します。 これらのフロントエンドは、ビルドのより合理化された宣言的な入力を受け入れることができ、アプリケーションの実行に必要なソフトウェアのみを含むイメージを生成できます。 

次の例は、 Julian Goede による mopy という Python アプリケーションを作成するためのフロントエンドの入力を示しています。

#syntax=cmdjulian/mopy
apiVersion: v1
python: 3.9.2
build-deps:
  - libopenblas-dev
  - gfortran
  - build-essential
envs:
  MYENV: envVar1
pip:
  - numpy==1.22
  - slycot
  - ./my_local_pip/
  - ./requirements.txt
labels:
  foo: bar
  fizz: ${mopy.sbom}
project: my-python-app/

それで、あなたのイメージは本当にディストリビューションレスですか?

マルチステージビルドや BuildKit などのコンテナイメージを作成するための新しいツールのおかげで、必要なソフトウェアとそのランタイム依存関係のみを含むイメージを作成することがより実用的になりました。 

しかし、ディストリビューションレスを謳う多くのイメージには、シェル(通常はBash)やBusyBoxが含まれており、Linuxディストリビューションが行う wget 多くのコマンドを提供し、コンテナをLiving off the land(LOTL)攻撃に対して脆弱なままにする可能性があります。 このことから、「なぜディストリビューションレスにしようとしているイメージに、Linuxディストリビューションの重要な部分が含まれているのか」という疑問が湧いてきます。 その答えは、通常、コンテナーの初期化に関係します。 

開発者は、多くの場合、ユーザーのニーズに合わせてアプリケーションを構成できるようにする必要があります。 ほとんどの場合、これらの構成はビルド時にわからないため、実行時に構成する必要があります。 多くの場合、これらの構成はシェル初期化スクリプトを使用して適用され、シェル初期化スクリプトは sed、grep、cp などの一般的な Linux ユーティリティに依存します。 この場合、シェルとユーティリティは、コンテナーの有効期間の最初の数秒間だけ必要です。 幸いなことに、ほとんどのコンテナー オーケストレーターから入手できるツール (init コンテナー) を使用して初期化できるようにしながら、真のディストリビューションレス イメージを作成する方法があります。

コンテナーの初期化

Kubernetesでは、 initコンテナ は起動するコンテナであり、プライマリコンテナが起動する前に正常に完了する必要があります。 非ディストリビューションレスコンテナを、プライマリコンテナとボリュームを共有するinitコンテナとして使用することで、アプリケーションを起動する前にランタイム環境とアプリケーションを構成できます。 

その init コンテナーの有効期間は短く (多くの場合、わずか数秒)、通常はインターネットに公開する必要はありません。 マルチステージビルドで開発者がビルド時の依存関係をランタイムの依存関係から分離できるのと同じように、init コンテナを使用すると、開発者は初期化の依存関係を実行の依存関係から分離できます。 

init コンテナの概念は、リレーショナルデータベースを使用している場合に馴染みがあり、新しいバージョンのアプリケーションを起動する前にスキーマの移行を実行するために init コンテナがよく使用されます。

Kubernetes の例

ここでは、init コンテナの使用例を 2 つ紹介します。 まず、Kubernetesを使用します。

apiVersion: v1
kind: Pod
metadata:
  name: kubecon-postgress-pod
  labels:
    app.kubernetes.io/name: KubeConPostgress
spec:
  containers:
  - name: postgress
    image: laurentgoderre689/postgres-distroless
    securityContext:
      runAsUser: 70
      runAsGroup: 70
    volumeMounts:
    - name: db
      mountPath: /var/lib/postgresql/data/
  initContainers:
  - name: init-postgress
    image: postgres:alpine3.18
    env:
      - name: POSTGRES_PASSWORD
        valueFrom:
          secretKeyRef:
            name: kubecon-postgress-admin-pwd
            key: password
    command: ['docker-ensure-initdb.sh']
    volumeMounts:
    - name: db
      mountPath: /var/lib/postgresql/data/
  volumes:
  - name: db
    emptyDir: {}

- - - 

> kubectl apply -f pod.yml && kubectl get pods
pod/kubecon-postgress-pod created
NAME                    READY   STATUS     RESTARTS   AGE
kubecon-postgress-pod   0/1     Init:0/1   0          0s
> kubectl get pods
NAME                    READY   STATUS    RESTARTS   AGE
kubecon-postgress-pod   1/1     Running   0          10s

Docker Compose の例

init コンテナーの概念は、サービスの依存関係と条件を使用してローカル開発を行うために Docker Compose でエミュレートすることもできます。

services:
 db:
   image: laurentgoderre689/postgres-distroless
   user: postgres
   volumes:
     - pgdata:/var/lib/postgresql/data/
   depends_on:
     db-init:
       condition: service_completed_successfully

 db-init:
   image: postgres:alpine3.18
   environment:
      POSTGRES_PASSWORD: example
   volumes:
     - pgdata:/var/lib/postgresql/data/
   user: postgres
    command: docker-ensure-initdb.sh

volumes:
 pgdata:

- - - 
> docker-compose up 
[+] Running 4/0
 ✔ Network compose_default      Created                                                                                                                      
 ✔ Volume "compose_pgdata"      Created                                                                                                                     
 ✔ Container compose-db-init-1  Created                                                                                                                      
 ✔ Container compose-db-1       Created                                                                                                                      
Attaching to db-1, db-init-1
db-init-1  | The files belonging to this database system will be owned by user "postgres".
db-init-1  | This user must also own the server process.
db-init-1  | 
db-init-1  | The database cluster will be initialized with locale "en_US.utf8".
db-init-1  | The default database encoding has accordingly been set to "UTF8".
db-init-1  | The default text search configuration will be set to "english".
db-init-1  | [...]
db-init-1 exited with code 0
db-1       | 2024-02-23 14:59:33.191 UTC [1] LOG:  starting PostgreSQL 16.1 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 12.2.1_git20220924-r10) 12.2.1 20220924, 64-bit
db-1       | 2024-02-23 14:59:33.191 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
db-1       | 2024-02-23 14:59:33.191 UTC [1] LOG:  listening on IPv6 address "::", port 5432
db-1       | 2024-02-23 14:59:33.194 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
db-1       | 2024-02-23 14:59:33.196 UTC [9] LOG:  database system was shut down at 2024-02-23 14:59:32 UTC
db-1       | 2024-02-23 14:59:33.198 UTC [1] LOG:  database system is ready to accept connections

前の例で示したように、init コンテナーをコンテナーと一緒に使用すると、汎用ソフトウェアが不要になり、真のディストリビューションレス イメージを作成できます。 

結論

この記事では、Docker ビルド ツールを使用して、ビルド時の依存関係と実行時の依存関係を分離して "ディストリビューションレス" イメージを作成する方法について説明しました。 たとえば、init コンテナーを使用すると、開発者はランタイム環境の構成に必要なロジックを環境自体から分離し、より安全なコンテナーを提供できます。 このアプローチは、チームが使用するソフトウェアに労力を集中させ、セキュリティとユーザビリティのより良いバランスを見つけるのにも役立ちます。

さらに詳しく