Chuyển đến nội dung chính

レッスン 16: OHDSI スタックを Docker および Kubernetes にデプロイする

OHDSI スタック (PostgreSQL、WebAPI、ATLAS) 用の Docker Compose、Kubernetes Helm チャート、CDM のバックアップ/復元、Prometheus/Grafana によるモニタリング、PostgreSQL のパフォーマンス チューニング。

🏗️ アーキテクチャ — レッスン 16 レッスン 16: OHDSI スタックを Docker にデプロイする &Kubernetes

OHDSI および OMOP CDM — 包括的な医療データ分析

パート 6: 生産とネットワークの研究

xdev.asia

レッスン 16: OHDSI スタックを Docker および Kubernetes にデプロイする

はじめに

この記事では、単一ノードの Docker Compose やクラスターの Kubernetes Helm を含む、OHDSI スタック全体 (PostgreSQL + WebAPI + ATLAS + ACHILLES) を運用環境にデプロイする方法を説明します。

OHDSI Production Stack:

┌─────────────────────────────────────────────────┐
│                  Reverse Proxy                   │
│              (Nginx / Traefik)                   │
│         :443 (HTTPS, SSL termination)            │
├──────────────┬──────────────────────────────────┤
│              │                                   │
│  ATLAS       │   WebAPI                          │
│  (Static     │   (Tomcat / Spring Boot)          │
│   SPA)       │   :8080                           │
│  :80         │                                   │
│              │   ┌──────────────┐                │
│              │   │ PostgreSQL   │                │
│              │   │ CDM + Results│                │
│              │   │ :5432        │                │
│              │   └──────────────┘                │
│              │                                   │
│              │   ┌──────────────┐                │
│              │   │ R Server     │                │
│              │   │ ACHILLES/DQD │                │
│              │   │ HADES        │                │
│              │   └──────────────┘                │
└──────────────┴──────────────────────────────────┘

1. Docker Compose

1.1 docker-compose.yml

version: "3.8"

services:
  # ─── PostgreSQL CDM Database ───
  ohdsi-db:
    image: postgres:16-alpine
    container_name: ohdsi-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: ohdsi
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: ohdsi
    volumes:
      - ohdsi-pgdata:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ohdsi"]
      interval: 10s
      timeout: 5s
      retries: 5
    secrets:
      - db_password
    deploy:
      resources:
        limits:
          memory: 4G
          cpus: "2.0"

  # ─── WebAPI ───
  ohdsi-webapi:
    image: ohdsi/webapi:latest
    container_name: ohdsi-webapi
    restart: unless-stopped
    environment:
      DATASOURCE_DRIVERCLASSNAME: org.postgresql.Driver
      DATASOURCE_URL: jdbc:postgresql://ohdsi-db:5432/ohdsi
      DATASOURCE_USERNAME: ohdsi_app
      DATASOURCE_PASSWORD_FILE: /run/secrets/app_password
      DATASOURCE_OHDSI_SCHEMA: ohdsi
      SPRING_JPA_PROPERTIES_HIBERNATE_DEFAULT_SCHEMA: ohdsi
      SPRING_BATCH_REPOSITORY_TABLEPREFIX: ohdsi.BATCH_
      FLYWAY_DATASOURCE_DRIVERCLASSNAME: org.postgresql.Driver
      FLYWAY_DATASOURCE_URL: jdbc:postgresql://ohdsi-db:5432/ohdsi
      FLYWAY_DATASOURCE_USERNAME: ohdsi_admin
      FLYWAY_DATASOURCE_PASSWORD_FILE: /run/secrets/admin_password
      FLYWAY_SCHEMAS: ohdsi
    ports:
      - "8080:8080"
    depends_on:
      ohdsi-db:
        condition: service_healthy
    secrets:
      - app_password
      - admin_password
    deploy:
      resources:
        limits:
          memory: 2G

  # ─── ATLAS ───
  ohdsi-atlas:
    image: ohdsi/atlas:latest
    container_name: ohdsi-atlas
    restart: unless-stopped
    volumes:
      - ./atlas-config/config-local.js:/usr/share/nginx/html/atlas/js/config-local.js:ro
    ports:
      - "80:80"
    depends_on:
      - ohdsi-webapi

  # ─── R Server (ACHILLES, DQD, HADES) ───
  ohdsi-r:
    image: ohdsi/broadsea-hades:latest
    container_name: ohdsi-r
    restart: unless-stopped
    environment:
      CDM_DB_SERVER: ohdsi-db/ohdsi
      CDM_DB_USER: ohdsi_app
      CDM_DB_PASS_FILE: /run/secrets/app_password
    volumes:
      - ./r-scripts:/scripts
      - ohdsi-r-output:/output
    depends_on:
      ohdsi-db:
        condition: service_healthy
    secrets:
      - app_password

volumes:
  ohdsi-pgdata:
    driver: local
  ohdsi-r-output:
    driver: local

secrets:
  db_password:
    file: ./secrets/db_password.txt
  app_password:
    file: ./secrets/app_password.txt
  admin_password:
    file: ./secrets/admin_password.txt

1.2 ATLAS 構成

// atlas-config/config-local.js
define([], function () {
  var configLocal = {};

  configLocal.api = {
    name: "OHDSI",
    url: "https://ohdsi.example.com/WebAPI/"
  };

  configLocal.cohortComparisonResultsEnabled = true;
  configLocal.userAuthenticationEnabled = false;
  configLocal.plpResultsEnabled = true;

  return configLocal;
});

1.3 初期化スクリプト

-- init-scripts/01-create-schemas.sql
CREATE SCHEMA IF NOT EXISTS cdm;
CREATE SCHEMA IF NOT EXISTS results;
CREATE SCHEMA IF NOT EXISTS ohdsi;

-- Tạo users riêng biệt (principle of least privilege)
CREATE USER ohdsi_admin WITH PASSWORD 'changeme_admin';
CREATE USER ohdsi_app WITH PASSWORD 'changeme_app';

-- Admin: DDL rights (cho Flyway migrations)
GRANT ALL ON SCHEMA ohdsi TO ohdsi_admin;
GRANT ALL ON SCHEMA cdm TO ohdsi_admin;
GRANT ALL ON SCHEMA results TO ohdsi_admin;

-- App: DML rights only
GRANT USAGE ON SCHEMA cdm TO ohdsi_app;
GRANT SELECT ON ALL TABLES IN SCHEMA cdm TO ohdsi_app;
GRANT USAGE ON SCHEMA results TO ohdsi_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA results TO ohdsi_app;
GRANT USAGE ON SCHEMA ohdsi TO ohdsi_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA ohdsi TO ohdsi_app;

1.4 起動

# Tạo secrets
mkdir -p secrets
openssl rand -base64 32 > secrets/db_password.txt
openssl rand -base64 32 > secrets/app_password.txt
openssl rand -base64 32 > secrets/admin_password.txt

# Start stack
docker compose up -d

# Kiểm tra
docker compose ps
docker compose logs ohdsi-webapi --tail 50

# Test WebAPI
curl -s http://localhost:8080/WebAPI/info | jq .

2. Kubernetes のデプロイメント

2.1 名前空間と秘密

# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: ohdsi
  labels:
    app.kubernetes.io/name: ohdsi

---
# k8s/secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: ohdsi-db-credentials
  namespace: ohdsi
type: Opaque
stringData:
  POSTGRES_PASSWORD: "${DB_PASSWORD}"
  OHDSI_ADMIN_PASSWORD: "${ADMIN_PASSWORD}"
  OHDSI_APP_PASSWORD: "${APP_PASSWORD}"

2.2 PostgreSQL ステートフルセット

# k8s/postgresql.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: ohdsi-postgresql
  namespace: ohdsi
spec:
  serviceName: ohdsi-postgresql
  replicas: 1
  selector:
    matchLabels:
      app: ohdsi-postgresql
  template:
    metadata:
      labels:
        app: ohdsi-postgresql
    spec:
      containers:
        - name: postgresql
          image: postgres:16-alpine
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_USER
              value: "ohdsi"
            - name: POSTGRES_DB
              value: "ohdsi"
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ohdsi-db-credentials
                  key: POSTGRES_PASSWORD
          resources:
            requests:
              memory: "2Gi"
              cpu: "1000m"
            limits:
              memory: "4Gi"
              cpu: "2000m"
          volumeMounts:
            - name: pgdata
              mountPath: /var/lib/postgresql/data
          livenessProbe:
            exec:
              command: ["pg_isready", "-U", "ohdsi"]
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            exec:
              command: ["pg_isready", "-U", "ohdsi"]
            initialDelaySeconds: 5
            periodSeconds: 5
  volumeClaimTemplates:
    - metadata:
        name: pgdata
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi

2.3 WebAPI のデプロイメント

# k8s/webapi.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ohdsi-webapi
  namespace: ohdsi
spec:
  replicas: 2
  selector:
    matchLabels:
      app: ohdsi-webapi
  template:
    metadata:
      labels:
        app: ohdsi-webapi
    spec:
      containers:
        - name: webapi
          image: ohdsi/webapi:latest
          ports:
            - containerPort: 8080
          env:
            - name: DATASOURCE_URL
              value: "jdbc:postgresql://ohdsi-postgresql:5432/ohdsi"
            - name: DATASOURCE_USERNAME
              value: "ohdsi_app"
            - name: DATASOURCE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ohdsi-db-credentials
                  key: OHDSI_APP_PASSWORD
          resources:
            requests:
              memory: "1Gi"
              cpu: "500m"
            limits:
              memory: "2Gi"
              cpu: "1000m"
          readinessProbe:
            httpGet:
              path: /WebAPI/info
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 10

2.4 イングレス

# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ohdsi-ingress
  namespace: ohdsi
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - ohdsi.example.com
      secretName: ohdsi-tls
  rules:
    - host: ohdsi.example.com
      http:
        paths:
          - path: /WebAPI
            pathType: Prefix
            backend:
              service:
                name: ohdsi-webapi
                port:
                  number: 8080
          - path: /atlas
            pathType: Prefix
            backend:
              service:
                name: ohdsi-atlas
                port:
                  number: 80

3. バックアップと復元

3.1 バックアップ CDM

#!/bin/bash
# backup-cdm.sh

BACKUP_DIR="/backups/ohdsi"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/ohdsi_cdm_${DATE}.sql.gz"

mkdir -p "$BACKUP_DIR"

# pg_dump CDM + results schemas
docker exec ohdsi-db pg_dump \
  -U ohdsi \
  -d ohdsi \
  --schema=cdm \
  --schema=results \
  --schema=ohdsi \
  --format=custom \
  --compress=9 \
  --file="/tmp/backup.dump"

docker cp ohdsi-db:/tmp/backup.dump "$BACKUP_FILE"
docker exec ohdsi-db rm /tmp/backup.dump

# Xóa backup cũ hơn 30 ngày
find "$BACKUP_DIR" -name "*.dump" -mtime +30 -delete

echo "Backup completed: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"

3.2 復元

#!/bin/bash
# restore-cdm.sh

BACKUP_FILE=$1

if [ -z "$BACKUP_FILE" ]; then
  echo "Usage: $0 <backup_file>"
  exit 1
fi

docker cp "$BACKUP_FILE" ohdsi-db:/tmp/restore.dump

docker exec ohdsi-db pg_restore \
  -U ohdsi \
  -d ohdsi \
  --clean \
  --if-exists \
  --no-owner \
  /tmp/restore.dump

docker exec ohdsi-db rm /tmp/restore.dump

echo "Restore completed from: $BACKUP_FILE"

4. モニタリング

4.1 プロメテウスのメトリクス

# docker-compose.monitoring.yml
services:
  postgres-exporter:
    image: prometheuscommunity/postgres-exporter:latest
    container_name: ohdsi-pg-exporter
    environment:
      DATA_SOURCE_URI: "ohdsi-db:5432/ohdsi?sslmode=disable"
      DATA_SOURCE_USER: ohdsi_monitor
      DATA_SOURCE_PASS_FILE: /run/secrets/monitor_password
    ports:
      - "9187:9187"
    depends_on:
      - ohdsi-db
    secrets:
      - monitor_password

  prometheus:
    image: prom/prometheus:latest
    container_name: ohdsi-prometheus
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    container_name: ohdsi-grafana
    volumes:
      - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_password
    secrets:
      - grafana_password

4.2 主要な指標

PostgreSQL Metrics:
┌────────────────────────────────┬──────────────┬──────────────┐
│ Metric                         │ Warning      │ Critical     │
├────────────────────────────────┼──────────────┼──────────────┤
│ Connections used               │ > 80%        │ > 95%        │
│ Cache hit ratio                │ < 95%        │ < 90%        │
│ Dead tuples ratio              │ > 10%        │ > 20%        │
│ Replication lag                │ > 30s        │ > 120s       │
│ Disk usage                     │ > 80%        │ > 90%        │
│ Long running queries           │ > 300s       │ > 600s       │
└────────────────────────────────┴──────────────┴──────────────┘

WebAPI Metrics:
  - Response time P95 < 2s
  - Error rate < 1%
  - Active sessions
  - Cohort generation queue size

5. パフォーマンスのチューニング

5.1 CDM 用の PostgreSQL 構成

# postgresql.conf — optimized cho OHDSI workload

# Memory (server có 16GB RAM)
shared_buffers = 4GB
effective_cache_size = 12GB
work_mem = 256MB
maintenance_work_mem = 1GB

# Connections
max_connections = 100

# WAL
wal_buffers = 256MB
min_wal_size = 2GB
max_wal_size = 8GB

# Query planner
random_page_cost = 1.1          # SSD storage
effective_io_concurrency = 200  # SSD
default_statistics_target = 500 # better plans for CDM

# Parallel queries (quan trọng cho ACHILLES/DQD)
max_worker_processes = 8
max_parallel_workers = 8
max_parallel_workers_per_gather = 4
max_parallel_maintenance_workers = 4

# Autovacuum (CDM tables lớn)
autovacuum_max_workers = 4
autovacuum_vacuum_cost_limit = 800
autovacuum_vacuum_scale_factor = 0.05
autovacuum_analyze_scale_factor = 0.02

# Logging
log_min_duration_statement = 5000  # log queries > 5s
log_checkpoints = on
log_autovacuum_min_duration = 0

5.2 CDM インデックス

-- Indexes quan trọng cho ATLAS/WebAPI queries

-- Person
CREATE INDEX idx_person_gender ON cdm.person (gender_concept_id);
CREATE INDEX idx_person_year ON cdm.person (year_of_birth);

-- Condition
CREATE INDEX idx_cond_person ON cdm.condition_occurrence (person_id);
CREATE INDEX idx_cond_concept ON cdm.condition_occurrence (condition_concept_id);
CREATE INDEX idx_cond_date ON cdm.condition_occurrence (condition_start_date);

-- Drug exposure
CREATE INDEX idx_drug_person ON cdm.drug_exposure (person_id);
CREATE INDEX idx_drug_concept ON cdm.drug_exposure (drug_concept_id);
CREATE INDEX idx_drug_date ON cdm.drug_exposure (drug_exposure_start_date);

-- Measurement
CREATE INDEX idx_meas_person ON cdm.measurement (person_id);
CREATE INDEX idx_meas_concept ON cdm.measurement (measurement_concept_id);
CREATE INDEX idx_meas_date ON cdm.measurement (measurement_date);

-- Observation period
CREATE INDEX idx_obs_person ON cdm.observation_period (person_id);

-- Visit
CREATE INDEX idx_visit_person ON cdm.visit_occurrence (person_id);
CREATE INDEX idx_visit_concept ON cdm.visit_occurrence (visit_concept_id);

-- Concept (vocabulary queries)
CREATE INDEX idx_concept_code ON cdm.concept (concept_code);
CREATE INDEX idx_concept_vocab ON cdm.concept (vocabulary_id);
CREATE INDEX idx_concept_name ON cdm.concept USING gin (concept_name gin_trgm_ops);

-- ANALYZE sau khi tạo index
ANALYZE cdm.person;
ANALYZE cdm.condition_occurrence;
ANALYZE cdm.drug_exposure;
ANALYZE cdm.measurement;
ANALYZE cdm.visit_occurrence;
ANALYZE cdm.concept;

概要

コンポーネントドッカーKubernetes
ポストグレSQLコンテナ + ボリュームステートフルセット + PVC
ウェブAPIコンテナ + 環境導入 (レプリカ)
アトラスコンテナ + config-local.js導入 + ConfigMap
Rサーバーコンテナ + マウントされたスクリプトジョブ/CronJob
秘密Docker の秘密K8 の秘密
ネットワーキングdocker-compose ネットワークサービス + イングレス
バックアップcron + pg_dumpクロンジョブ
モニタリングpostgres-exporter + Grafanaサービスモニター

次の記事: ネットワーク研究とベスト プラクティス — 多施設研究