#0123（2025/05/05）`requirements.in` を使ったアーキテクチャが異なる環境構築

## はじめに

Pythonプロジェクトでは、従来 `requirements.txt` のみを使って依存関係を管理しているケースが多いです。しかし、`requirements.txt` はトップレベル依存とサブ依存（再帰的に必要なライブラリ）が混在しており、バージョン固定や更新制御が難しいという課題があります。

本記事ではまず、`requirements.in` とは何か、なぜ導入するのかを解説し、その後に **arm64環境とx86環境** 双方で安定したビルドを行うための **Dockerfile** × **pip-tools** 構成をご紹介します。

## `requirements.in` とは

* **トップレベル依存**のみを記述するためのファイル。
* 例: アプリが直接使う `flask` や `requests` などを列挙。
* サブ依存のバージョン固定やハッシュ管理は行わない。

### requirements.in のサンプル

```txt
# requirements.in
flask>=2.0,<3.0        # Web フレームワーク
requests==2.28.1       # HTTP クライアント
pydantic               # バリデーションライブラリ
python-dotenv>=0.21.0  # 環境変数管理
```

* **トップレベル依存**のみを記述するためのファイル。
* 例: アプリが直接使う `flask` や `requests` などを列挙。
* サブ依存のバージョン固定やハッシュ管理は行わない。

### メリット

1. **可読性向上**: 必要なライブラリだけが一覧化され、プロジェクトの依存構造が読みやすくなる。
2. **再現性の強化**: `pip-compile` コマンドで全依存を解決し、正確なバージョンを `requirements.txt` に出力できる。
3. **アップグレード管理**: `pip-compile -U` で依存の一括アップデートが可能。

### 基本の流れ

```bash
# トップレベル依存を requirements.in に記述
echo "flask" > requirements.in
echo "requests" >> requirements.in

# 全依存を解決し、バージョン固定した requirements.txt を生成
pip-compile requirements.in

# ロックされた依存をインストール
pip-sync requirements.txt
```

---

# 1. アーキテクチャ対応の課題

近年、Apple M1/M2（arm64）やラズパイなどのarm64環境が普及しつつあります。一方で、従来のIntel/AMD（x86\_64）環境も依然として広く使われています。これら両者で同じPythonアプリを再現性高く動かすには、依存パッケージのビルド・管理が大きな課題となります。

* **依存ライブラリのバイナリ互換性**: `numpy`や`pillow`などにはプラットフォーム依存のWheelが存在し、バージョンによっては片方でのみビルドに失敗する場合があります。
* **環境差異による不具合**: 開発者のローカル環境（例：M1 Mac）とCI/CDランナー（例：x86\_64）で挙動が異なることがあります。

これらを避けるために、ビルドタイミングでアーキテクチャを判別し、適切な依存セットをインストールできるようにします。

---

# 2. pip-toolsで依存関係を一元管理

[`pip-tools`](https://github.com/jazzband/pip-tools) は、`requirements.in` → `requirements.txt` の2段階管理を可能にするツールです。

## 2.1 基本コマンド

```bash
# トップレベル依存を書く
echo "flask" > requirements.in

# 依存を解決しバージョン固定
pip-compile requirements.in

# 環境を同期（不要なパッケージを削除）
pip-sync requirements.txt
```

* `pip-compile`: `requirements.in` に書かれたライブラリとその再帰的依存を解決し、バージョン付きで `requirements.txt` を生成
* `pip-sync`: `requirements.txt` と完全に一致する環境に同期

## 2.2 アーキ対応ファイルの生成

```bash
# x86_64 用
pip-compile --output-file=requirements-x86.txt requirements.in

# arm64 用
pip-compile --output-file=requirements-arm64.txt requirements.in
```

これで、プラットフォームごとに最適化されたロックファイルを保持できます。

---

# 3. Dockerfileでプラットフォーム切り替え

`TARGETARCH`（`docker buildx`が渡すビルド変数）を利用して、アーキテクチャに応じた `requirements-*.txt` を選択します。

```Dockerfile
FROM python:3.10-slim

ARG TARGETARCH
ENV TARGETARCH=${TARGETARCH}
WORKDIR /app

# 共通の入力ファイル
COPY requirements.in .
# アーキ別のロックファイルを requirements.txt にリネーム
COPY requirements-${TARGETARCH}.txt requirements.txt

RUN pip install --upgrade pip \
    && pip install -r requirements.txt

COPY ./src /app
CMD ["python", "main.py"]
```

## ビルド例

```bash
# x86_64 向け
docker buildx build --platform=linux/amd64 \
  --build-arg TARGETARCH=x86 \
  -t myapp:x86 .

# arm64 向け
docker buildx build --platform=linux/arm64 \
  --build-arg TARGETARCH=arm64 \
  -t myapp:arm64 .
```

---

# 4. CI/CD自動化のヒント

* **ワークフロー分岐**: GitHub Actions などで `matrix.platform: [linux/amd64, linux/arm64]` を使い分け
* **ロックファイル更新**: 両アーキ別に `pip-compile` を実行し、PRで一括レビュー

```yaml
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        platform: [amd64, arm64]
    steps:
      - uses: actions/checkout@v3
      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: 3.10
      - name: Generate lock
        run: |
          pip install pip-tools
          pip-compile --output-file=requirements-${{ matrix.platform }}.txt requirements.in
```

---

# まとめ

* **`requirements.in`** でトップレベル依存を整理し、`pip-compile` で再帰的にバージョンを固定
* **`pip-sync`** でクリーンな環境を維持
* **`docker buildx`** と **`ARG TARGETARCH`** によるプラットフォーム対応
* **CI/CD** で自動化すれば、どのマシンでも同じビルド結果を得られる