Docker イメージ構築時の'ModuleNotFoundError'の修正方法

DockerBeginner
オンラインで実践に進む

はじめに

Python アプリケーションの Docker イメージを構築する際、開発者はしばしば「ModuleNotFoundError」というメッセージに遭遇します。このエラーは、Python がアプリケーションが必要とするモジュールまたはパッケージを見つけられない場合に発生します。Docker 初心者にとって、これは特にトラブルシューティングが難しい場合があります。

この実践的な実験(Lab)では、シンプルな Python アプリケーションを作成し、Docker でコンテナ化し、ModuleNotFoundError に遭遇し、それを解決するための実践的な方法を学びます。最終的には、Docker イメージで Python の依存関係を適切に管理し、プロジェクトでこの一般的な問題を回避する方法を理解できるようになります。

シンプルな Python アプリケーションの作成

基本的な Python アプリケーションを作成し、それを実行するための Docker を設定しましょう。これにより、Docker 環境で ModuleNotFoundError がどのように発生するかを理解できます。

Python アプリケーションの構造を理解する

まず、プロジェクトディレクトリを作成し、そこに移動しましょう。

mkdir -p ~/project/docker-python-app
cd ~/project/docker-python-app

次に、サードパーティモジュールをインポートするシンプルな Python アプリケーションを作成しましょう。2 つのファイルを作成します。

  1. メインアプリケーションファイル
  2. 依存関係をリストする requirements ファイル

メインアプリケーションファイルを作成します。

nano app.py

app.pyに以下のコードを追加します。

import requests

def main():
    response = requests.get("https://www.example.com")
    print(f"Status code: {response.status_code}")
    print(f"Content length: {len(response.text)} characters")

if __name__ == "__main__":
    main()

このシンプルなスクリプトは、requestsライブラリを使用して example.com への HTTP リクエストを行い、レスポンスに関する基本的な情報を出力します。

次に、requirements ファイルを作成します。

nano requirements.txt

requirements.txtに以下の行を追加します。

requests==2.28.1

基本的な Dockerfile の作成

次に、ModuleNotFoundError を実演するシンプルな Dockerfile を作成しましょう。

nano Dockerfile

Dockerfile に以下の内容を追加します。

FROM python:3.9-slim

WORKDIR /app

COPY app.py .

## We're intentionally NOT copying or installing requirements
## to demonstrate the ModuleNotFoundError

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

この Dockerfile は以下を行います。

  • Python 3.9 slim イメージをベースとして使用します。
  • 作業ディレクトリを/appに設定します。
  • アプリケーションファイルをコピーします。
  • アプリケーションを実行するコマンドを指定します。

意図的に requirements.txt ファイルをコピーしたり、依存関係をインストールしたりしていないことに注意してください。これにより、コンテナを実行しようとしたときに ModuleNotFoundError が発生します。

Docker イメージのビルドと実行

Docker イメージをビルドしましょう。

docker build -t python-app-error .

次のような出力が表示されるはずです。

Sending build context to Docker daemon  3.072kB
Step 1/4 : FROM python:3.9-slim
 ---> 3a4bac80b3ea
Step 2/4 : WORKDIR /app
 ---> Using cache
 ---> a8a4f574dbf5
Step 3/4 : COPY app.py .
 ---> Using cache
 ---> 7d5ae315f84b
Step 4/4 : CMD ["python", "app.py"]
 ---> Using cache
 ---> f5a9b09d7d8e
Successfully built f5a9b09d7d8e
Successfully tagged python-app-error:latest

次に、Docker コンテナを実行しましょう。

docker run python-app-error

次のようなエラーメッセージが表示されるはずです。

Traceback (most recent call last):
  File "/app/app.py", line 1, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

これは、この実験(Lab)で焦点を当てている ModuleNotFoundError です。このエラーは、必要なrequestsモジュールを Docker イメージに含めなかったために発生します。

ModuleNotFoundError の理解と修正

ModuleNotFoundError に遭遇したので、その原因と修正方法を理解しましょう。

なぜ Docker で ModuleNotFoundError が発生するのか?

ModuleNotFoundError は、Docker でいくつかの一般的な理由で発生します。

  1. 依存関係のインストールの欠如: Docker イメージに必要な Python パッケージをインストールしませんでした。
  2. 誤った PYTHONPATH: Python インタプリタが、期待される場所にモジュールを見つけられません。
  3. ファイル構造の問題: アプリケーションのコード構造が、インポートの実行方法と一致していません。

今回のケースでは、Docker イメージにrequestsパッケージをインストールしなかったためにエラーが発生しました。ローカルの開発環境では、このパッケージがグローバルにインストールされている可能性がありますが、Docker コンテナは分離された環境です。

方法 1:Dockerfile で pip を使用して依存関係をインストールする

必要な依存関係をインストールするように Dockerfile を変更しましょう。

nano Dockerfile

Dockerfile を以下の内容で更新します。

FROM python:3.9-slim

WORKDIR /app

COPY app.py .

## Fix Method 1: Directly install the required package
RUN pip install requests==2.28.1

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

この更新されたイメージをビルドして実行しましょう。

docker build -t python-app-fixed-1 .

パッケージのインストールを含む次のような出力が表示されるはずです。

Sending build context to Docker daemon  3.072kB
Step 1/5 : FROM python:3.9-slim
 ---> 3a4bac80b3ea
Step 2/5 : WORKDIR /app
 ---> Using cache
 ---> a8a4f574dbf5
Step 3/5 : COPY app.py .
 ---> Using cache
 ---> 7d5ae315f84b
Step 4/5 : RUN pip install requests==2.28.1
 ---> Running in 5a6d7e8f9b0c
Collecting requests==2.28.1
  Downloading requests-2.28.1-py3-none-any.whl (62 kB)
Collecting charset-normalizer<3,>=2
  Downloading charset_normalizer-2.1.1-py3-none-any.whl (39 kB)
Collecting certifi>=2017.4.17
  Downloading certifi-2022.9.24-py3-none-any.whl (161 kB)
Collecting idna<4,>=2.5
  Downloading idna-3.4-py3-none-any.whl (61 kB)
Collecting urllib3<1.27,>=1.21.1
  Downloading urllib3-1.26.12-py2.py3-none-any.whl (140 kB)
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests
Successfully installed certifi-2022.9.24 charset-normalizer-2.1.1 idna-3.4 requests-2.28.1 urllib3-1.26.12
 ---> 2b3c4d5e6f7g
Removing intermediate container 5a6d7e8f9b0c
Step 5/5 : CMD ["python", "app.py"]
 ---> Running in 8h9i0j1k2l3m
 ---> 3n4o5p6q7r8s
Removing intermediate container 8h9i0j1k2l3m
Successfully built 3n4o5p6q7r8s
Successfully tagged python-app-fixed-1:latest

次に、修正されたコンテナを実行しましょう。

docker run python-app-fixed-1

次のような出力が表示されるはずです。

Status code: 200
Content length: 1256 characters

素晴らしい!必要な依存関係をインストールしたので、アプリケーションが正常に実行されるようになりました。

方法 2:依存関係管理に requirements.txt を使用する

パッケージを直接インストールすることもできますが、より整理された依存関係管理には、requirements.txt ファイルを使用することをお勧めします。Dockerfile を更新しましょう。

nano Dockerfile

Dockerfile を以下の内容で更新します。

FROM python:3.9-slim

WORKDIR /app

## Copy requirements first to leverage Docker cache
COPY requirements.txt .

## Fix Method 2: Use requirements.txt
RUN pip install -r requirements.txt

## Copy the rest of the application
COPY app.py .

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

このアプローチには、いくつかの利点があります。

  • 依存関係管理をコードから分離します。
  • 依存関係の更新が容易になります。
  • Docker イメージのレイヤーキャッシングのベストプラクティスに従います。

この更新されたイメージをビルドして実行しましょう。

docker build -t python-app-fixed-2 .

以前のビルドと同様の出力が表示されますが、今回は requirements.txt を使用しています。

Sending build context to Docker daemon  4.096kB
Step 1/5 : FROM python:3.9-slim
 ---> 3a4bac80b3ea
Step 2/5 : WORKDIR /app
 ---> Using cache
 ---> a8a4f574dbf5
Step 3/5 : COPY requirements.txt .
 ---> Using cache
 ---> b2c3d4e5f6g7
Step 4/5 : RUN pip install -r requirements.txt
 ---> Running in h8i9j0k1l2m3
Collecting requests==2.28.1
  Using cached requests-2.28.1-py3-none-any.whl (62 kB)
Collecting charset-normalizer<3,>=2
  Using cached charset_normalizer-2.1.1-py3-none-any.whl (39 kB)
Collecting idna<4,>=2.5
  Using cached idna-3.4-py3-none-any.whl (61 kB)
Collecting certifi>=2017.4.17
  Using cached certifi-2022.9.24-py3-none-any.whl (161 kB)
Collecting urllib3<1.27,>=1.21.1
  Using cached urllib3-1.26.12-py2.py3-none-any.whl (140 kB)
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests
Successfully installed certifi-2022.9.24 charset-normalizer-2.1.1 idna-3.4 requests-2.28.1 urllib3-1.26.12
 ---> n4o5p6q7r8s9
Removing intermediate container h8i9j0k1l2m3
Step 5/5 : COPY app.py .
 ---> t0u1v2w3x4y5
Step 6/6 : CMD ["python", "app.py"]
 ---> Running in z5a6b7c8d9e0
 ---> f1g2h3i4j5k6
Removing intermediate container z5a6b7c8d9e0
Successfully built f1g2h3i4j5k6
Successfully tagged python-app-fixed-2:latest

次に、コンテナを実行しましょう。

docker run python-app-fixed-2

同じ成功した出力が表示されるはずです。

Status code: 200
Content length: 1256 characters

2 つの異なる方法を使用して、ModuleNotFoundError を正常に修正しました!

ModuleNotFoundError を回避するためのベストプラクティス

すぐに問題は解決しましたが、Docker イメージで ModuleNotFoundError を回避するためのベストプラクティスを見ていきましょう。

効率的なビルドのための Docker キャッシングの理解

Docker は、イメージを構築するためにレイヤー化されたアプローチを使用します。Dockerfile の各命令は新しいレイヤーを作成します。イメージを再構築すると、Docker は可能な限りキャッシュされたレイヤーを再利用し、ビルドプロセスを大幅に高速化できます。

Python アプリケーションの場合、次のようにキャッシングを最適化できます。

  1. アプリケーションコードをコピーする前に、requirements をコピーしてインストールする
  2. 頻繁に変更されるファイル(アプリケーションコードなど)を後のレイヤーに保持する

これらのベストプラクティスに従うように Dockerfile を更新しましょう。

nano Dockerfile

Dockerfile を以下の最適化された内容で更新します。

FROM python:3.9-slim

WORKDIR /app

## Copy requirements first for better caching
COPY requirements.txt .

## Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

## Copy application code (changes more frequently)
COPY . .

## Make sure we run the application with Python's unbuffered mode for better logging
CMD ["python", "-u", "app.py"]

この最適化されたイメージをビルドしましょう。

docker build -t python-app-optimized .

そして、それが機能することを確認するために実行します。

docker run python-app-optimized

同じ成功した出力が表示されるはずです。

Status code: 200
Content length: 1256 characters

.dockerignore ファイルの使用

Docker ビルドをより効率的にするために、Docker イメージに不要なファイルやディレクトリを除外するために.dockerignoreファイルを使用することをお勧めします。これにより、ビルドコンテキストのサイズが削減され、ビルドのパフォーマンスが向上します。

.dockerignoreファイルを作成しましょう。

nano .dockerignore

以下の内容を追加します。

__pycache__
*.pyc
*.pyo
*.pyd
.Python
.git
.gitignore
*.log
*.pot
*.env

より複雑なアプリケーション構造の作成

複数のモジュールを持つ大規模なアプリケーションの場合、プロジェクトを正しく構造化することが重要です。少し複雑な例を作成しましょう。

mkdir -p myapp

モジュールファイルを作成します。

nano myapp/__init__.py

このファイルは空のままにしておきます(ディレクトリを Python パッケージとしてマークするだけです)。

次に、いくつかの機能を持つモジュールファイルを作成します。

nano myapp/utils.py

以下のコードを追加します。

def get_message():
    return "Hello from myapp.utils module!"

次に、このモジュールを使用するようにメインアプリケーションを更新します。

nano app.py

内容を以下に置き換えます。

import requests
from myapp.utils import get_message

def main():
    response = requests.get("https://www.example.com")
    print(f"Status code: {response.status_code}")
    print(f"Content length: {len(response.text)} characters")
    print(get_message())

if __name__ == "__main__":
    main()

更新されたアプリケーションをビルドして実行します。

docker build -t python-app-modules .
docker run python-app-modules

カスタムメッセージを含む出力が表示されるはずです。

Status code: 200
Content length: 1256 characters
Hello from myapp.utils module!

その他のベストプラクティス

Docker で ModuleNotFoundError を回避するためのその他のベストプラクティスを以下に示します。

  1. 仮想環境: Docker では厳密には必要ありません(コンテナは分離されているため)が、仮想環境を使用すると、開発と本番環境の間で一貫性を確保できます。

  2. 固定された依存関係: さまざまな環境間で一貫性を確保するために、常に依存関係の正確なバージョンを指定します。

  3. マルチステージビルド: 本番イメージの場合、必要な依存関係のみを含む、より小さなイメージを作成するために、マルチステージビルドを検討してください。

  4. 定期的な依存関係の更新: セキュリティ修正と改善を得るために、依存関係を定期的に更新します。

これらのベストプラクティスに従うことで、Docker コンテナで ModuleNotFoundError が発生する可能性を最小限に抑え、より効率的で保守性の高い Docker イメージを作成できます。

まとめ

この実験(Lab)では、Python アプリケーションの Docker イメージを扱う際に、ModuleNotFoundError を特定し、トラブルシューティングし、修正する方法を学びました。以下の実践的な経験を積みました。

  • 基本的な Python アプリケーションを作成し、Docker でコンテナ化する
  • Docker 環境で ModuleNotFoundError が発生する理由を理解する
  • 直接的なパッケージインストールと requirements.txt を使用して依存関係の問題を修正する
  • 適切なレイヤーキャッシングやファイル構造など、Docker のベストプラクティスを実装する
  • 複数のモジュールを持つ、より複雑なアプリケーション構造を作成する
  • .dockerignore を使用して Docker ビルドを最適化する

これらのスキルは、Python アプリケーション向けに、より信頼性が高く、保守性の高い Docker イメージを作成するのに役立ちます。この実験で取り上げたベストプラクティスに従うことで、ModuleNotFoundError のような一般的な落とし穴を回避し、Docker 開発ワークフローを最適化できます。

コンテナ化されたアプリケーションを扱う際には、適切な依存関係管理が不可欠であることを忘れないでください。常に、Docker イメージにすべての必要な依存関係が含まれ、コードが適切に構造化され、効率性と保守性のためのベストプラクティスに従っていることを確認してください。