1) 依存の固定(根本対応)
portal-api
requirements.txtかpyproject.tomlを 厳密ピン止め+constraints で固定:# requirements.in(開発用) fastapi==0.110.0 uvicorn==0.29.0 SQLAlchemy==1.4.54 # ← 2.xに上がらないよう固定 psycopg2-binary==2.9.9 # …他# constraints.txt(ロック用途) SQLAlchemy==1.4.54 numpy<2 # Chroma の依存が NumPy 2.0 の breaking に引っかからないようpip install -r requirements.txt -c constraints.txt- CIで pip-compile(pip-tools)や Poetry lockを使い lockfile をリポジトリにコミット。
chroma
- 公式の
ghcr.io/chroma-core/chromaはバージョンアップで挙動が変わります。
こちらも タグ固定(例:0.4.24)+ ヘルスプローブは /api/v1/heartbeat を使用。
2) コード側の恒久化(sitecustomize から卒業)
- いま
sitecustomize.pyで回避している内容を 本体コードに取り込み、ホットフィックスを外す。 - ポイント:
- SQLAlchemy 2.x 非互換パターン禁止
if stmt:/A and B/A or Bを 絶対使わない- 代わりに
sa.and_(...),sa.or_(...),if stmt is not None:を使う
portal_chroma_doc_repo.list_queued()は Core で実装(今の差し替え版を移植)- さらに安全にするなら DBAPI フォールバックを用意(環境差がひどい時のみ使う)
例)環境変数PORTAL_USE_DBAPI_LIST_QUEUED=trueで切替
- SQLAlchemy 2.x 非互換パターン禁止
参考実装(Core 版・恒久化用)
# app/repos/portal_chroma_doc_repo.py
import sqlalchemy as sa
def list_queued(self, *, collections=None, limit=1000):
t = sa.table(
"portal_chroma_doc",
sa.column("id"), sa.column("doc_id"),
sa.column("entity"), sa.column("natural_key"), sa.column("lang"),
sa.column("collection"), sa.column("doc_text"), sa.column("meta"),
sa.column("state"),
# 存在すれば model を読む(無くても動くよう列存在チェックを別で)
)
stmt = (
sa.select(
t.c.id, t.c.doc_id, t.c.entity, t.c.natural_key, t.c.lang,
t.c.collection, t.c.doc_text, t.c.meta.label("metadata")
)
.where(t.c.state == sa.literal("queued"))
.order_by(t.c.id.asc())
.limit(int(limit))
)
if collections:
stmt = stmt.where(t.c.collection.in_(list(collections)))
rows = self.s.execute(stmt).mappings().all()
# …変換は現行のまま
※ 本番では 1.4固定なのでこれで十分。将来2.x対応するなら SQLAlchemy==2 のCIマトリクスでテスト追加。
3) DBスキーマ差異の吸収
model列が無い環境があることが分かったので、マイグレーションで揃えるか、情報スキーマで存在確認→動的SELECTを恒久化(今回のsitecustomizeの実装を本体へ)。- Alembic などで
ADD COLUMN IF NOT EXISTS model TEXT NULL;を流せるよう、アプリ起動前に Jobで migrate しても良い。
4) 起動時プリフライト(失敗を早く止める)
- portal-api 起動時に以下をチェックしてログへ:
- SQLAlchemy バージョンと import 元パス
- DB の
portal_chroma_docの列一覧(model有無等) CHROMA_URLの/api/v1/heartbeatが 200/{“database”:”ok”} か
- 失敗なら /startupz を Fail にし、Kubernetes の startupProbe でローリングを止める。
5) Kubernetes 側の堅牢化
- portal-api の initContainer で簡易プリフライト:
python -c "import sqlalchemy,sys; assert sqlalchemy.__version__.startswith('1.4')"wget -qO- $CHROMA_URL/api/v1/heartbeatが成功するまで待機(タイムアウト/リトライ上限つき)
- readinessProbe は
/healthzだけでなく、内部から CHROMA_URL へ簡易 GETする軽量エンドポイントを追加し、それを readiness にしても良い。 - chroma は StatefulSet + PVC、
PERSIST_DIRECTORY=/chroma、イメージタグ固定、プローブ/api/v1/heartbeat。
6) CI/CD でのミニ・スモーク(壊れたタグを弾く)
- minikube(profile 固定)で以下を自動実行する Job/Workflow を用意:
kubectl -n portal-dev wait --for=condition=Available deploy/portal-apikubectl -n portal-dev run curltest --rm --image=curlimages/curl:8.10.1 -- curl -fsS http://chroma:8000/api/v1/heartbeat- port-forwardして
POST /chroma/upsert(dry_run=true)
- どれか落ちたら デプロイ失敗(オフショアの方も同じスクリプトを流せばOK)。
7) 静的チェック(地雷の芽を摘む)
CI で grep ルールを追加:
# Clause の真偽値評価につながる典型NG
grep -RInE '\.(where|filter)\([^)]*( and | or )[^(]*\)' app/ && exit 1 || true
grep -RInE 'if[[:space:]]+(stmt|sql|where(_clause)?|filters?)\s*:' app/ && exit 1 || true
8) 監視と運用
- portal-api ログに SQLAlchemy/NumPy バージョン、CHROMA_URL を起動時に1行出力(今回の診断が早くなる)。
/admin/diagのような短い診断APIを追加(SA/NumPy/CHROMA heartbeat/DB列一覧をまとめて返す)。
9) チーム向けワンコマンド復旧スクリプト
オフショア含めて、「困ったらこれ」の統一スクリプトをリポジトリに置く(例:hack/recover_chroma.sh):
- chroma の sts/probe 確認と再起動
- portal-api の /healthz, /chroma/heartbeat, /chroma/upsert(dry_run) まで自動テスト
- 失敗時は原因候補を色付きで表示(DNS / Endpoint / Pod 状態 / 依存バージョン等)
10) いまのホットフィックスの整理(クリーンアップ手順)
- 本体に反映できたら:
# env と volume 片付け(使わなくなったら) kubectl -n portal-dev set env deploy/portal-api PYTHONPATH- kubectl -n portal-dev patch deploy portal-api --type=json -p='[ {"op":"remove","path":"/spec/template/spec/volumes/1"}, {"op":"remove","path":"/spec/template/spec/containers/0/volumeMounts/1"} ]' kubectl -n portal-dev rollout restart deploy/portal-api(順番はマニフェストに合わせて調整。残すならそのままでも可)
すぐできる“最小セット”
- 依存固定(SA 1.4.54 / NumPy<2 / Chromaタグ固定)
list_queued()を Core 実装に置換(本体に移植)- CI にミニ・スモーク(heartbeat + upsert dry_run)
- grep ルールで NG パターンを検出
これだけで、**次回のイメージ更新や依存ズレでも「起動前に検知・弾く/自動で安全側に倒す」**状態にできます。
必要なら、上記の CI スクリプト雛形や Helm/K8s パッチもすぐ出します。
あなた:
上記の CI スクリプト雛形や Helm/K8s パッチ
ChatGPT:
思考時間: 6s
了解!“そのまま置けば動く”系の雛形をまとめました。最小構成ですが、今回ハマったポイント(ChromaのHB、/chroma/upsert dry-run、依存ピン止めの検証)をCIで自動チェックします。
1) GitHub Actions: ミニ E2E スモーク(minikube 上)
.github/workflows/smoke.yml
name: smoke-on-minikube
on:
push:
branches: [ main ]
pull_request:
jobs:
smoke:
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Set up kubectl & helm
uses: azure/setup-kubectl@v4
with:
version: "v1.30.5"
- uses: azure/setup-helm@v4
with:
version: "v3.14.4"
- name: Start minikube
uses: medyagh/setup-minikube@v0.0.18
with:
minikube-version: "v1.33.1"
kubernetes-version: "v1.30.0"
driver: docker
addons: "ingress"
- name: Create namespace
run: kubectl create ns portal-dev || true
- name: Deploy Chroma (pinned)
run: |
kubectl -n portal-dev apply -f k8s/chroma.yaml
kubectl -n portal-dev rollout status sts/chroma --timeout=180s
kubectl -n portal-dev run curltest --rm --restart=Never \
--image=curlimages/curl:8.10.1 -- \
sh -lc 'curl -fsS http://chroma:8000/api/v1/heartbeat'
- name: Deploy portal-api
run: |
# 例: 事前に container registry のイメージを使う or build/push 済みを pull
# ここでは単純にデモ用マニフェストを適用
kubectl -n portal-dev apply -f k8s/portal-api.yaml
kubectl -n portal-dev rollout status deploy/portal-api --timeout=240s
- name: Port-forward & smoke
run: |
set -euo pipefail
kubectl -n portal-dev port-forward svc/portal-api 18080:80 >/tmp/pf.log 2>&1 &
PF_PID=$!
sleep 5
trap "kill $PF_PID || true" EXIT
# /healthz
curl -fsS http://127.0.0.1:18080/healthz
# chroma heartbeat via cluster svc
kubectl -n portal-dev run c2 --rm --restart=Never \
--image=curlimages/curl:8.10.1 -- \
curl -fsS http://chroma:8000/api/v1/heartbeat
# /chroma/upsert dry-run
curl -fsS -X POST http://127.0.0.1:18080/chroma/upsert \
-H 'Content-Type: application/json' \
-d '{"limit":10,"dry_run":true}'
- name: Dump diagnostics on failure
if: failure()
run: |
kubectl -n portal-dev get all -o wide
kubectl -n portal-dev describe sts/chroma || true
kubectl -n portal-dev describe deploy/portal-api || true
kubectl -n portal-dev logs -l app=portal-api --tail=200 || true
2) ミニスモーク実行スクリプト(ローカル/CI共通)
hack/smoke.sh
#!/usr/bin/env bash
set -euo pipefail
NS=${NS:-portal-dev}
echo "== 1) Chroma heartbeat"
kubectl -n "$NS" run curltest --rm --restart=Never \
--image=curlimages/curl:8.10.1 -- \
curl -fsS http://chroma:8000/api/v1/heartbeat
echo "== 2) portal-api /healthz"
kubectl -n "$NS" wait --for=condition=Available deploy/portal-api --timeout=180s
PF_PORT=${PF_PORT:-18080}
kubectl -n "$NS" port-forward svc/portal-api "$PF_PORT":80 >/tmp/pf.log 2>&1 &
PF_PID=$!
sleep 5
trap "kill $PF_PID || true" EXIT
curl -fsS "http://127.0.0.1:${PF_PORT}/healthz"
echo "== 3) /chroma/upsert dry-run"
curl -fsS -X POST "http://127.0.0.1:${PF_PORT}/chroma/upsert" \
-H 'Content-Type: application/json' \
-d '{"limit":10,"dry_run":true}'
echo "OK"
3) Helm/K8s: 最小値のマニフェスト・パッチ
3.1 Chroma(StatefulSet + Service)
k8s/chroma.yaml
apiVersion: v1
kind: Service
metadata:
name: chroma
namespace: portal-dev
labels:
app: chroma
spec:
type: ClusterIP
selector:
app: chroma
ports:
- name: http
port: 8000
targetPort: 8000
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: chroma
namespace: portal-dev
spec:
serviceName: chroma
replicas: 1
selector:
matchLabels: { app: chroma }
template:
metadata:
labels: { app: chroma }
spec:
containers:
- name: chroma
image: ghcr.io/chroma-core/chroma:0.4.24 # ← タグ固定
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8000
env:
- name: PERSIST_DIRECTORY
value: /chroma
- name: CHROMA_SERVER_HOST
value: "0.0.0.0"
- name: CHROMA_SERVER_HTTP_PORT
value: "8000"
readinessProbe:
httpGet:
path: /api/v1/heartbeat
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 12
livenessProbe:
httpGet:
path: /api/v1/heartbeat
port: http
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
volumeMounts:
- name: data
mountPath: /chroma
volumes: [] # pvc は volumeClaimTemplates から
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi
storageClassName: standard
3.2 portal-api(Deployment + Service)
- 起動前プリフライト:ChromaのHB待ち
- readiness:
/healthz - 依存のピン止めはイメージ側で実施(CIで検証)。必要なら initContainer で簡易検証だけ追加可能。
k8s/portal-api.yaml
apiVersion: v1
kind: Service
metadata:
name: portal-api
namespace: portal-dev
labels:
app: portal-api
spec:
type: ClusterIP
selector:
app: portal-api
ports:
- name: http
port: 80
targetPort: 8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: portal-api
namespace: portal-dev
spec:
replicas: 2
selector:
matchLabels: { app: portal-api }
template:
metadata:
labels: { app: portal-api }
spec:
serviceAccountName: portal-api
initContainers:
- name: wait-chroma
image: curlimages/curl:8.10.1
command: ["/bin/sh","-lc"]
args:
- |
for i in $(seq 1 60); do
if curl -fsS http://chroma:8000/api/v1/heartbeat >/dev/null; then
echo "chroma ok"; exit 0;
fi
echo "waiting chroma.."; sleep 2;
done
echo "chroma timeout"; exit 1
containers:
- name: api
image: portal-api:dev # ← あなたのレジストリの固定タグに
imagePullPolicy: IfNotPresent
env:
- name: CHROMA_URL
value: http://chroma:8000
ports:
- name: http
containerPort: 8000
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
4) Helm の values での差し込み例
あなたのチャートに「追加の initContainers / env / probes / volumes」を入れるための values 例です。
values.dev.yaml(抜粋)
portalApi:
replicaCount: 2
image:
repository: your-registry/portal-api
tag: "v2025-10-12" # ← 固定
pullPolicy: IfNotPresent
env:
- name: CHROMA_URL
value: http://chroma:8000
extraInitContainers:
- name: wait-chroma
image: curlimages/curl:8.10.1
command: ["/bin/sh","-lc"]
args:
- |
for i in $(seq 1 60); do
if curl -fsS http://chroma:8000/api/v1/heartbeat >/dev/null; then
echo "chroma ok"; exit 0;
fi
echo "waiting chroma.."; sleep 2;
done
echo "chroma timeout"; exit 1
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
chroma:
image:
repository: ghcr.io/chroma-core/chroma
tag: "0.4.24" # ← 固定
pullPolicy: IfNotPresent
persistence:
enabled: true
size: 20Gi
service:
port: 8000
probes:
readiness:
path: /api/v1/heartbeat
liveness:
path: /api/v1/heartbeat
チャート側テンプレートで
extraInitContainers,env,readinessProbe,livenessProbeをwith/toYamlでマージする構造にしておくと将来楽です。
5) Kustomize パッチ(既存マニフェストに後付け)
kustomization.yaml
resources:
- chroma.yaml
- portal-api.yaml
patches:
- target:
kind: Deployment
name: portal-api
namespace: portal-dev
path: patches/portal-api-probes.yaml
- target:
kind: StatefulSet
name: chroma
namespace: portal-dev
path: patches/chroma-probes.yaml
patches/portal-api-probes.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: portal-api
spec:
template:
spec:
initContainers:
- name: wait-chroma
image: curlimages/curl:8.10.1
command: ["/bin/sh","-lc"]
args:
- |
for i in $(seq 1 60); do
if curl -fsS http://chroma:8000/api/v1/heartbeat >/dev/null; then
exit 0
fi
sleep 2
done
exit 1
containers:
- name: api
readinessProbe:
httpGet: { path: /healthz, port: http }
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet: { path: /healthz, port: http }
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
patches/chroma-probes.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: chroma
spec:
template:
spec:
containers:
- name: chroma
readinessProbe:
httpGet: { path: /api/v1/heartbeat, port: http }
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 12
livenessProbe:
httpGet: { path: /api/v1/heartbeat, port: http }
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
6) 依存のピン止め(CIで保証)
例:requirements.txt / constraints.txt
requirements.txt
fastapi==0.110.0
uvicorn==0.29.0
SQLAlchemy==1.4.54
psycopg2-binary==2.9.9
# ほか依存…
constraints.txt
SQLAlchemy==1.4.54
numpy<2
Dockerfile(抜粋)
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt constraints.txt ./
RUN pip install --no-cache-dir -r requirements.txt -c constraints.txt
COPY app/ /app/app/
CMD ["uvicorn", "app.main:app", "--host","0.0.0.0","--port","8000"]
必要なら、既存のHelmチャートに合わせた values.yaml キー名(extraVolumes, extraVolumeMounts, extraEnv, extraInitContainers, podAnnotations など)に合わせて書き換えます。
また、オフショア向けには hack/smoke.sh と k8s/*.yaml をリポジトリ直下に置いて「READMEに3コマンド」だけ載せておくと迷子になりません。
コメントを残す