目次
- やりたいこと
- 実装前に考えたこと
- 前提
- 方針
- modelsの切り出し
- Celery Worker側の設定
- DRF側の設定
- 懸念点
- まとめ
- 補足
やりたいこと
- Webの機能(DRF)とLLM系の機能(Celery Worker)を環境ごと分ける
- 完全な環境分離をしながら、models等共有リソースは流用し、ORMを使用する
実装前に考えたこと
昨今、AI・LLMの機能開発を求められる企業は多いのではないでしょうか。
LLM系のライブラリは外部依存が強く、バージョンの互換性が非常にシビアです。
ちょっとしたアップデートでアプリ全体が壊れてしまうことも珍しくありません。
特に、SaaS 企業など長期運用しているプロダクトでは、
ライブラリのアップデートによる新たなバグのリスクや、
その検証コストから 簡単にアップデートできないのが現実です。
そこで出てくる自然な発想が、こうです。
「環境は分離したい!!でも、同じDBにアクセスしてWeb側と同じようにデータ操作を行いたい!!」
じゃあ、モデル定義(models.py)をLLM側でももう一度行う?
→ 管理が二重になりバグが起きやすい ![]()
じゃあ、生SQLを書いて運用しようか
→ SQLインジェクションのリスクがある ![]()
→ 運用が大変。SQLをパフォーマンスなど気をつけて書ける人は多くない![]()
理想形は?![]()
「Web側と同じ Django ORM を LLM 側でも使えるようにする!」
→ 考えてみよう!!
前提
Docker Desktop 使用します
poetry でパッケージを管理する
方針
-
コンテナは以下5つ
- webのバックエンド用のDRF
- webのフロントエンド用のNext.js(今回はあんまり関係ないので無視だが一応)
- db用のPostgres
- LLM用のCelery Worker
- タスクキュー用のRedis(デプロイ時はAWS SQSを使用する)
-
modelsを インストール可能なpythonライブラリとする
- 切り出して、GitHubのプライベートリポジトリにアップし、コンテナビルド時にsshでインストール
modelsの切り出し
切り出したモデルのディレクトリ構造
.
├── README.md
├── poetry.lock
├── pyproject.toml
└── shared_resource
├── __init__.py
└── models
└── __init__.py
[tool.poetry]
name = "shared-resource"
version = "0.1.0"
description = "modelsの分離"
authors = ["nao_ikeda <test@example.com>"]
readme = "README.md"
packages = [{ include = "shared_resource" }]
...省略 tool.poetry.dependenciesが続く
from django.db import models
class SharedBaseModel(models.Model):
class Meta:
abstract = True
app_label = "common"
app_labelをcommonに固定して、全てのモデルはこれを継承させて作ります。
Celery Worker側の設定
- GitHubのプライベートリポジトリ上のパッケージを環境に入れるために、GitHubに公開鍵の登録をすませる
- ローカルで秘密鍵を ~/.ssh/に配置して
poetry add "git+ssh://git@github.com/your_organization/your_repository.git"を実行して、pyproject.tomlにmodelsのパッケージを登録しておく - 秘密鍵の値をbase64エンコードして、以下のように.envに環境変数として置く(環境によっては改行が悪さをするのでbase64エンコードで回避する)
...
SSH_PRIVATE_KEY=***********base64エンコードされた値**********************
...
ディレクトリ構造
.
├── Dockerfile
├── Dockerfile.dev
├── README.md
├── celery_worker
│ ├── __init__.py
│ ├── celery.py
│ ├── celeryconfig.py
│ └── tasks
│ └── sample.py
├── common
│ ├── __init__.py
│ └── apps.py
├── poetry.lock
├── pyproject.toml
└── settings.py
Dockerfile.dev
FROM python:3.12-slim
WORKDIR /app
# システムの依存関係をインストール
RUN apt-get update && apt-get install -y \
gcc \
libpq-dev \
git \
openssh-client \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 環境変数からbase64エンコードしたsshのキーをデコードして.ssh/に配置する
ARG SSH_PRIVATE_KEY
RUN mkdir -p /root/.ssh \
&& echo "$SSH_PRIVATE_KEY" | base64 -d > /root/.ssh/id_ed25519 \
&& chmod 600 /root/.ssh/id_ed25519 \
&& ssh-keyscan github.com >> /root/.ssh/known_hosts && \
ssh -i /root/.ssh/id_ed25519 -T git@github.com || true
RUN pip install poetry
# 仮想環境の設定
RUN poetry config virtualenvs.create true
RUN poetry config virtualenvs.in-project false
RUN poetry install --no-root
COPY . .
CMD bash -c "poetry run celery -A celery_worker.celery:app worker --loglevel=info"
modelsのapp_labelは全てcommon にしてある。
DjangoORMは INSTALLED_APPSに登録されているapp内のmodelsでしか使うことができない
→ commonを無理やりINSTALLED_APPSに登録する必要がある
import os
from dotenv import load_dotenv
load_dotenv()
INSTALLED_APPS = [
"django.contrib.auth", # Djangoを使うならとりあえず入れておかないとダメ
"django.contrib.contenttypes", # Djangoを使うならとりあえず入れておかないとダメ
"common", # common/apps.py に AppConfigを定義する前提
]
SECRET_KEY = os.getenv("SECRET_KEY")
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("DB_NAME"),
"USER": os.getenv("DB_USER"),
"PASSWORD": os.getenv("DB_PASSWORD"),
"HOST": os.getenv("DB_HOST"),
"PORT": 5432,
}
}
from django.apps import AppConfig
class CommonConfig(AppConfig):
name = "common"
import os
from dotenv import load_dotenv
from celery import Celery
import django
load_dotenv()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
django.setup() # djangoの設定のモジュールを読み込む
app = Celery("ai_worker")
app.config_from_object("celery_worker.celeryconfig")
app.autodiscover_tasks(["celery_worker.tasks"])
DRF側の設定
SSHでPythonパッケージをインストールする部分については、Celery側と同様の手順を踏みます。
ここでは、Webまわりの細かい設定は割愛し、共通モデルの利用に関連する箇所のみ紹介します。
from shared_resource.models import SomeModel
from shared_resource.models import AnyModel
...
このように、common/models/__init__.py 内で shared_resource.models からモデルをインポートします。
これにより、共通モデルを common アプリの一部として扱うことができます。
INSTALLED_APPS = [
"common",
...
]
この設定により、Djangoはインポートされたモデルを common アプリのモデルとして認識します。
マイグレーションも common 側で管理されるため、shared_resource 側でマイグレーションを行う必要はありません。
Dockerfileは、poetryによるパッケージのインストール部分はCelery側と同じ構成で問題ありません。
web固有の設定等は必要ですが、割愛します。
懸念点
- 同じモデルを流用した場合、マイグレーションが複数のコンテナから実行され、整合性が崩れる可能性がある
- Celery 側には manage.py を配置しないことで、python manage.py migrate を実行できないようにし、マイグレーションの責任は DRF 側に一元化する
- Celery ワーカー側では、あくまで ORM を使ったデータ操作(DML)のみに限定し、DDL(テーブル定義など)は扱わない
- 共有リソースとして切り出したモデルが Django や load_dotenv に依存しているため、結局は各コンテナ間でバージョンを揃えないと動作しないのではないか。つまり、Django のバージョンに関する依存関係は解消されていないとも言える
- ただし、shared_resource で使うのは models の機能だけに限定する想定
- モデル定義で高度な Django の機能を使用しないのであれば、shared_resource 側の互換性を維持しやすい
- あるいは、shared_resource の tool.poetry.dependencies に Django を含めず、実行時の Django に依存させる設計にする
まとめ
AI/LLMのように外部依存が激しい機能を、安全にプロダクトへ組み込むには、
「環境の完全分離」×「モデルの共有」 という構成が有効です。
まとめると...
-
Web側とLLM側をコンテナ単位で分離す
-
共通の models はライブラリとして切り出し、
-
ORM(Djangoのモデル操作)は両方の環境で再利用しつつ、
-
マイグレーションの責任はDRF側に一元化
-
Celery Worker側ではデータ操作(DML)のみを行う
補足
デプロイ時ちょっとややこしそうかな、とか思ったり...
以下悩み事書いてみます。
- RDSのインバウンドルールを設定して二つの分離されたアプリからアクセスを受け付ける必要ある
- CI/CDに組み込むときにSSHのキーを渡せないので、キーか何かを直接埋め込む必要がある
- AWS周りの制御はCDKを導入した方がいいと思ったので、CDKでのデプロイもそのうち実装しないといけないですね