Skip to main content

Dockerイメージをより速く再ビルドする3つのヒント(CIの実行時間を短縮!)

著者
feature docker rebuild

2023年12月28日

0 分で読めます

Dockerは、アプリケーションのビルド、配布、実行のプロセスを簡素化する、欠かせないツールとなっています。Dockerを使うと、開発者はアプリケーションとすべての依存関係を、コンテナイメージと呼ばれる標準化された単位にパッケージ化できます。

# Example of a Dockerfile
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

Dockerfileは、コンテナイメージのビルドに必要なすべてのコマンドを記述したシンプルなファイルです。このイメージは、実行時にDockerコンテナのベースとなります。

コンテナは軽量で、独立して実行できるパッケージです。コード、システムツール、ライブラリ、設定など、ソフトウェアの実行に必要なものがすべて含まれています。

コンテナの優れた点は、可搬性にあります。デプロイ先の環境にかかわらず、アプリケーションをスムーズに実行できます。これによりソフトウェア開発プロセスが大幅に効率化され、継続的インテグレーションとデプロイを可能にするDevOpsアプローチの要となっています。

Dockerイメージのビルドを高速化する必要性

コンテナイメージはさまざまな環境でアプリケーションを実行するうえで欠かせませんが、ビルドに時間がかかることがあります。依存関係の多い大規模なアプリケーションでは、特にその傾向があります。

# Building a container image can take time
docker build -t my-application .

上の例では、Dockerが現在のディレクトリにあるDockerfileを読み込み、my-applicationという名前のイメージをビルドします。アプリケーションの複雑さやサイズによっては、かなりの時間がかかることがあります。

Dockerイメージビルドの基本

Dockerイメージのビルドは、コンテナを扱ううえで基本となる作業です。Dockerイメージは、ある時点におけるDockerコンテナの「スナップショット」のようなもので、新しいDockerコンテナを作成するためのベースとなります。

Dockerイメージのビルドには、イメージを作成するために必要なすべてのコマンドを記述したテキストファイル、Dockerfileを作成します。Dockerfileはレシピのようなもので、イメージを作成するときにDockerが何をすべきかを指示します。

シンプルなDockerfileの例を見てみましょう。

# Use an official Python runtime as a base image
FROM python:3.7-slim

# Set the working directory in the container
WORKDIR /app

# Add the current directory contents into the container at /app
ADD . /app

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Make port 80 available to the world outside this container
EXPOSE 80

# Define environment variable
ENV NAME World

# Run app.py when the container launches
CMD ["python", "app.py"]

Dockerfileを用意したら、docker buildコマンドでイメージを作成できます。実行例は次のとおりです。

docker build -t my-python-app .

このコマンドは、現在のディレクトリにあるDockerfile(コマンド末尾の.)を使ってイメージをビルドし、-tオプションでmy-python-appというタグを付けます。

注: Python開発者の方、またはPythonアプリケーションのコンテナを構築するDevOpsチームの方には、DockerでPythonアプリケーションをコンテナ化するためのベストプラクティスを確認し、適用することを強くおすすめします。

Dockerイメージのビルドでよくある課題

Dockerイメージのビルドは、必ずしも簡単とは限りません。開発者やDevOpsチームが直面する、よくある課題をいくつか紹介します。

  • 依存関係の管理: よくある課題の一つが、依存関係の管理です。アプリケーションの依存関係は時間とともに変わることがあり、Dockerイメージを最新の状態に保つのは簡単ではありません。対策として、依存関係を自動的に管理するパッケージマネージャーの利用が役立ちます。

  • イメージサイズ: Dockerイメージのサイズをできるだけ小さく保つことも課題です。サイズの大きいイメージはビルドやDockerリポジトリへのプッシュに時間がかかり、CI/CDパイプラインが遅くなる可能性があります。イメージサイズを抑えるには、小さなベースイメージを使う、不要なファイルを削除する、マルチステージビルドで最終イメージの内容を最小限にするといった方法があります。

  • セキュリティ: Dockerイメージのビルドでは、セキュリティも大きな懸念事項です。イメージに攻撃者に悪用される脆弱性が含まれていないことを確認する必要があります。Docker Bench for SecurityやSnyk Containerなどのツールを使って、イメージの脆弱性をスキャンできます。

ヒント1: リポジトリに.dockerignoreファイルを追加する

Dockerやコンテナセキュリティの世界では、効率性が重要です。開発者やDevOpsのプロフェッショナルなら、継続的インテグレーション(CI)のプロセスでは一秒一秒が大切だとご存じでしょう。

Dockerイメージのビルドを高速化する、シンプルで効果的な方法の一つは、リポジトリに.dockerignoreファイルを追加することです。.dockerignoreファイルの仕組みを見て、Dockerイメージのビルドを速くする方法と、リポジトリに.dockerignoreファイルを追加する手順を確認しましょう。

.dockerignoreファイルとは?

.dockerignoreファイルは.gitignoreファイルとよく似たテキストファイルで、Dockerfileからイメージをビルドする際にDockerが無視するファイルやディレクトリのパターンを指定します。つまり、ビルドコンテキスト内で除外するファイルやディレクトリをDockerに伝えるものです。

# Sample .dockerignore file
node_modules
npm-debug.log
Dockerfile*
*.md

上のコード例は.dockerignoreファイルの例です。Dockerに対して、node_modulesディレクトリ、npm-debug.logファイル、拡張子を問わずすべてのDockerfile、すべてのMarkdownファイルを無視するよう指示しています。

.dockerignoreファイルでDockerイメージのビルドを高速化する方法

Dockerfileからイメージをビルドするとき、Dockerはビルドコンテキスト(現在のディレクトリとその配下のディレクトリ)全体をDockerデーモンに送信します。.dockerignoreファイルがないと、不要なファイルやディレクトリもDockerデーモンに送られるため、ビルド時間が大幅に長くなる可能性があります。

リポジトリに.dockerignoreファイルを追加すると、不要なファイルやディレクトリを除外するようDockerに指示でき、ビルドコンテキストのサイズを小さくできます。その結果、Dockerイメージをより速くビルドでき、貴重なCIの時間を節約できます。

リポジトリに.dockerignoreファイルを追加する手順

リポジトリに.dockerignoreファイルを追加する方法は次のとおりです。

1. リポジトリのルートディレクトリに.dockerignoreという名前の新しいファイルを作成します。

touch .dockerignore


2. 作成した.dockerignoreファイルを、お好みのテキストエディターで開きます。

nano .dockerignore

3. Dockerイメージのビルド時に無視するファイルやディレクトリのパターンを追加します。たとえば、次のようにすべてのログファイルとnode_modulesディレクトリを除外できます。

*.log
node_modules/

4. .dockerignoreファイルを保存して閉じます。次回、このリポジトリからDockerイメージをビルドすると、指定したファイルやディレクトリが無視され、ビルドが速くなります。

.dockerignoreファイルは、Dockerイメージのビルド時間を大幅に短縮できる、シンプルながら強力なツールです。リポジトリに追加することで、DockerとCIのプロセスをより効率的かつ安全にできます。

ヒント2: 依存関係のロックファイルを使う

依存関係のロックファイルは、Dockerのビルドを高速かつ決定論的に行うための重要なツールです。プロジェクトの依存関係のバージョンを固定し、以降のビルドで常にまったく同じバージョンが使われるようにします。これによりビルドが速くなるだけでなく、Dockerイメージの信頼性と安全性も高まります。

依存関係のロックファイルとは?なぜ重要?

依存関係のロックファイルは、特定の時点におけるプロジェクトの依存関係とその正確なバージョンを記録した、パッケージマネージャーが生成するファイルです。プロジェクトの依存関係をスナップショットとして記録するようなものです。Dockerの場合、特定のDockerイメージのビルドに使用したソフトウェアのバージョンを記録します。

ロックファイルが重要な理由はいくつかあります。Dockerのビルドが決定論的で、繰り返し再現できるようになります。つまり、Dockerイメージをビルドするたびに同じ結果が得られます。Dockerイメージに何が含まれているかを常に正確に把握できるため、デバッグやセキュリティコンプライアンスにおいて重要です。

さらに、依存関係のロックファイルを使うと、Dockerのビルドを大幅に高速化できます。依存関係の正確なバージョンが記録されているため、Dockerはレイヤーをキャッシュし、次回以降のビルドではインストール手順を省略できるので、ビルド時間を短縮できます。

依存関係のロックファイルでビルドを高速化し、決定論的にする方法

ロックファイルを使わない、シンプルなDockerfileを見てみましょう。

FROM node:20
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD [ "node", "app.js" ]

ビルドを実行するたびに、npm installはpackage.jsonで定義された依存関係の最新バージョンを取得します。この処理には時間がかかるうえ、ビルドごとにインストールされる依存関係が異なる可能性があるため、ビルド結果が一定しません。

注: DockerでNode.jsアプリケーションをビルドしている方には、DockerでNode.js Webアプリケーションをコンテナ化するためのベストプラクティス10選を確認することを強くおすすめします。

次に、ロックファイルを使ってみましょう。

FROM node:20
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD [ "node", "app.js" ]

ここではpackage-lock.jsonをDockerイメージにコピーし、npm installの代わりにnpm ciを使っています。npm ciコマンドはロックファイルに基づいて依存関係の正確なバージョンをインストールするため、決定論的なビルドが可能になります。また、依存関係のレイヤーをDockerがキャッシュできるので、ビルドが速くなります。

Dockerで依存関係のロックファイルを使う方法

Dockerで依存関係のロックファイルを使うには、そのファイルがDockerのビルドコンテキストに含まれている必要があります。DockerfileのCOPYコマンドを使って、ロックファイルをDockerイメージにコピーできます。

Pipfile.lockを使うPythonプロジェクトの例を見てみましょう。

FROM python:3.7
WORKDIR /app
COPY Pipfile Pipfile.lock ./
RUN pip install pipenv && pipenv install --system --deploy
COPY . .
CMD [ "python", "app.py" ]

このDockerfileでは、PipfileとPipfile.lockをDockerイメージにコピーしています。次にPipfile.lockで定義された依存関係の正確なバージョンをインストールするため、pipenv install --system --deployを実行します。これにより、決定論的なビルドを実現し、Dockerのキャッシュ機能を活用してビルドのパフォーマンスを最適化できます。

依存関係のロックファイルは、Dockerのビルドを高速かつ決定論的に行うための重要なツールです。Dockerイメージの一貫性と信頼性を保つだけでなく、イメージに含まれるソフトウェアのバージョンを常に把握できるため、セキュリティも強化できます。

ヒント3: 変更される可能性に応じてコマンドをグループ化する

効率的なプラクティスは、コンテナセキュリティとソフトウェア開発の向上につながります。Dockerイメージのビルドも例外ではありません。その一つが、変更される可能性に応じてコマンドをグループ化する方法です。Dockerイメージの再ビルドにかかる時間とリソースを大幅に節約できます。仕組みを詳しく見てみましょう。

Dockerにおけるコマンドグループ化の原則

Dockerfileに記述する各コマンドは、Dockerイメージに新しいレイヤーを作成します。Dockerは、イメージのビルドを高速化するために、これらのレイヤーをキャッシュします。

Dockerはイメージをビルドするとき、各コマンドを1行ずつ処理します。前回のビルドから変更されていないコマンドがあれば、再実行する代わりにキャッシュされたレイヤーを使い、ビルド時間を大幅に短縮します。

ただし、1行でも変更されると、Dockerはその行以降の行でキャッシュを利用できません。そのため、頻繁に変更される行をDockerfileの先頭に置くと、キャッシュを効果的に使えず、毎回イメージの大部分を再ビルドすることになります。そこで役立つのが、コマンドのグループ化です。

コマンドのグループ化でDockerイメージの再ビルドを高速化

コマンドをグループ化し、変更される可能性に応じて整理することで、Dockerのキャッシュを最適に活用し、イメージの再ビルドを大幅に高速化できます。

基本的には、変更頻度の高いコマンドをDockerfileの末尾に配置します。そうすれば、変更のない、更新頻度の低いコマンドにはキャッシュされたレイヤーを使い、変更されたコマンドのレイヤーだけを再ビルドできます。

変更される可能性が最も低いコマンドを先頭に置く

Dockerイメージの再ビルドを高速化するもう一つの方法は、変更される可能性が最も低いコマンドをDockerfileの先頭に置き、Dockerのキャッシュ機能を最大限に活用することです。

Dockerfileでは、記述した各コマンドがDockerイメージの新しいレイヤーを作成します。これらのレイヤーは積み重なり、最終的なDockerイメージを構成します。Dockerはレイヤーのキャッシュ機構を利用して、イメージのビルドを高速化します。レイヤーに変更がない場合(つまり、コマンドが前回のビルドと同じ場合)、Dockerはそのレイヤーを再ビルドせず、キャッシュ済みのバージョンを使用します。

ただし、依存関係にあるレイヤーのいずれかが変更されると、Dockerはそのレイヤーのキャッシュを無効にします。つまり、Dockerfileの先頭付近にあるコマンドを変更すると、その下にあるコマンドに変更がなくても、すべてのレイヤーが再ビルドされます。

変更される可能性が最も低いコマンドをDockerfileの先頭に配置すると、ほとんどのビルドでこれらのレイヤーのキャッシュが利用されます。この方法により、Dockerが再ビルドするレイヤーの数を減らし、ビルドプロセスを高速化できます。

コマンドをまとめる実践例

Dockerfileの例を見てみましょう。

FROM debian:stretch

RUN apt-get update && apt-get install -y \
    python3 \
    python3-pip

COPY . /app

WORKDIR /app

RUN pip3 install -r requirements.txt

CMD ["python3", "app.py"]

このDockerfileでは、アプリケーションのコードを更新するたびに、COPY . /appコマンドが頻繁に変更される可能性があります。このコマンドと、requirements.txtファイルが変更されるたびに更新が必要なRUN pip3 install -r requirements.txtコマンドをDockerfileの末尾に移動すると、変更頻度の低いRUN apt-get update && apt-get install -y python3 python3-pipコマンドにキャッシュを利用できます。

最適化したDockerfileは次のようになります。

FROM debian:stretch

RUN apt-get update && apt-get install -y \
    python3 \
    python3-pip

WORKDIR /app

COPY . /app

RUN pip3 install -r requirements.txt

CMD ["python3", "app.py"]

変更される可能性に応じてコマンドをまとめることで、開発者やDevOpsチームはDockerイメージの再ビルドを最適化し、CIの実行時間を短縮しながら、コンテナのセキュリティとソフトウェア開発の手法を改善できます。

まとめ

Dockerイメージの再ビルドを高速化するメリットは、単なる時間の節約にとどまりません。CIパイプラインの効率や、ソフトウェア開発プロセス全体にも大きな影響をもたらします。

Dockerイメージのビルドが速くなると、CIパイプラインでのビルド時間が短縮され、開発者はより迅速なフィードバックを得られます。その結果、ソフトウェア開発のペースが上がり、変更をより速く反復できるようになります。

さらに、CIパイプラインで使う計算リソースが減るため、コスト削減にもつながります。CPU使用率やビルド時間がコストに直結しやすいクラウドコンピューティングの世界では、Dockerイメージのビルドを高速化することで、長期的に大幅なコスト削減が期待できます。

最後に、アプリケーション全体のセキュリティ態勢も強化できます。ビルド時間が短縮されれば、Dockerイメージに潜むセキュリティ上の問題をより早く把握でき、迅速な修正によってリスクにさらされる期間を短縮できます。

結論として、Dockerイメージの再ビルドを高速化するこれらの戦略は、あれば便利というだけでなく、ソフトウェア開発のスピードが求められる今、不可欠なものです。CIパイプラインのスピードと効率を高めるだけでなく、ソフトウェア開発プロセスのセキュリティとコスト効率を改善するうえでも重要です。

DevSecOpsのためのコンテナセキュリティ

Snykでコンテナの脆弱性を無料で検出・修正しましょう。

カテゴリー:

続きを読む

Live Stream

修正エージェントをわかりやすく解説:見つけるより直すことが重要な理由

SnykのRemediation Agentが、セキュリティインテリジェンス、破壊可能性分析、検証を活用して、脆弱性をマージ可能なプルリクエストに変える仕組みをご覧ください。

blog feature snyk secrets aazgdq
Blog

拡大するシークレットの拡散を阻止:Snyk Secretsが一般提供開始

Snyk Secretsが一般提供を開始しました。コンテキストを考慮したML検知、コミット時のセキュリティ対策、統合されたシークレットガバナンスをSnyk AI Security Platformに提供します。

feature insights context
Blog

keyv npm侵害の実態:preinstallマルウェア、信頼された来歴、IDEフック

keyv 6.0.0と関連するnpmリリース10件に、インストール時に実行されるマルウェアが含まれていました。影響を受けるバージョン、ハッシュ、検出手順、安全な修復の順序を解説します。