From 042391ae7f4dcef9b637b0c8895302a1847dadd2 Mon Sep 17 00:00:00 2001 From: yakenator Date: Mon, 23 Feb 2026 13:50:26 +0900 Subject: [PATCH] feat: APISIX gateway with Docker Compose orchestration for stock microservices - APISIX declarative config with versioned API routes (/api/v1/{service}/...) - proxy-rewrite for path transformation, CORS global rule, rate limiting (100 req/s) - Active health checks on all 6 upstream services - Docker Compose with 11 containers: Redis, PostgreSQL, MongoDB, 6 Python services, APISIX, Next.js frontend - Environment config via .env.example Co-Authored-By: Claude Opus 4.6 --- .env.example | 14 ++ apisix/conf/apisix.yaml | 298 ++++++++++++++++++++++++++++++++++++++++ apisix/conf/config.yaml | 15 ++ docker-compose.yml | 169 +++++++++++++++++++++++ 4 files changed, 496 insertions(+) create mode 100644 .env.example create mode 100644 apisix/conf/apisix.yaml create mode 100644 apisix/conf/config.yaml create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..317564e --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# === API Keys (from stock-analysis reference project) === +DART_API_KEY=your_dart_api_key_here +KIS_APP_KEY=your_kis_app_key_here +KIS_APP_SECRET=your_kis_app_secret_here +KIS_BASE_URL=https://openapivts.koreainvestment.com:9443 +ANTHROPIC_API_KEY=your_anthropic_api_key_here + +# === Infrastructure (defaults work with docker-compose) === +REDIS_URL=redis://redis:6379/0 +POSTGRES_DSN=postgresql://stock:stock@postgres:5432/stockdb +MONGODB_URL=mongodb://mongo:27017 +MONGODB_DB=stock_analysis +LOG_LEVEL=INFO +LOG_FORMAT=json diff --git a/apisix/conf/apisix.yaml b/apisix/conf/apisix.yaml new file mode 100644 index 0000000..bc61e86 --- /dev/null +++ b/apisix/conf/apisix.yaml @@ -0,0 +1,298 @@ +# APISIX Standalone mode - declarative configuration +# All routes, upstreams, and plugins defined here + +routes: + # === DART Collector (port 8001) === + - id: dart-health + uri: /api/v1/dart/health + upstream_id: dart + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/dart/(.*)", "/$1"] + + - id: dart-streams + uri: /api/v1/dart/streams + upstream_id: dart + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/dart/(.*)", "/$1"] + + - id: dart-collect + uri: /api/v1/dart/collect/* + methods: [POST] + upstream_id: dart + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/dart/(.*)", "/$1"] + + # === KIS Collector (port 8002) === + - id: kis-health + uri: /api/v1/kis/health + upstream_id: kis + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/kis/(.*)", "/$1"] + + - id: kis-streams + uri: /api/v1/kis/streams + upstream_id: kis + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/kis/(.*)", "/$1"] + + - id: kis-stocks-list + uri: /api/v1/kis/stocks + methods: [GET] + upstream_id: kis + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/kis/(.*)", "/$1"] + + - id: kis-stocks-detail + uri: /api/v1/kis/stocks/* + methods: [GET] + upstream_id: kis + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/kis/(.*)", "/$1"] + + - id: kis-collect + uri: /api/v1/kis/collect/* + methods: [POST] + upstream_id: kis + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/kis/(.*)", "/$1"] + + # === News Crawler (port 8003) === + - id: news-health + uri: /api/v1/news/health + upstream_id: news + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/news/(.*)", "/$1"] + + - id: news-streams + uri: /api/v1/news/streams + upstream_id: news + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/news/(.*)", "/$1"] + + - id: news-collect + uri: /api/v1/news/collect/* + methods: [POST] + upstream_id: news + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/news/(.*)", "/$1"] + + # === Screener (port 8004) === + - id: screener-health + uri: /api/v1/screening/health + upstream_id: screener + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/screening/(.*)", "/$1"] + + - id: screener-streams + uri: /api/v1/screening/streams + upstream_id: screener + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/screening/(.*)", "/$1"] + + - id: screener-screen + uri: /api/v1/screening/screen + methods: [POST] + upstream_id: screener + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/screening/(.*)", "/$1"] + + - id: screener-results + uri: /api/v1/screening/results/* + upstream_id: screener + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/screening/(.*)", "/$1"] + + # === Catalyst (port 8005) === + - id: catalyst-health + uri: /api/v1/catalyst/health + upstream_id: catalyst + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/catalyst/(.*)", "/$1"] + + - id: catalyst-streams + uri: /api/v1/catalyst/streams + upstream_id: catalyst + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/catalyst/(.*)", "/$1"] + + - id: catalyst-detect + uri: /api/v1/catalyst/detect + methods: [POST] + upstream_id: catalyst + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/catalyst/(.*)", "/$1"] + + - id: catalyst-results + uri: /api/v1/catalyst/results/* + upstream_id: catalyst + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/catalyst/(.*)", "/$1"] + + # === LLM Analyzer (port 8006) === + - id: llm-health + uri: /api/v1/analysis/health + upstream_id: llm + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/analysis/(.*)", "/$1"] + + - id: llm-streams + uri: /api/v1/analysis/streams + upstream_id: llm + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/analysis/(.*)", "/$1"] + + - id: llm-analyze + uri: /api/v1/analysis/analyze + methods: [POST] + upstream_id: llm + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/analysis/(.*)", "/$1"] + + - id: llm-results + uri: /api/v1/analysis/results/* + upstream_id: llm + plugins: + proxy-rewrite: + regex_uri: ["^/api/v1/analysis/(.*)", "/$1"] + + # === Aggregated health check (version-independent) === + - id: gateway-health + uri: /api/health + plugins: + serverless-pre-function: + phase: rewrite + functions: + - "return function(conf, ctx) local core = require('apisix.core'); core.response.exit(200, '{\"gateway\":\"ok\",\"version\":\"v1\"}'); end" + +upstreams: + - id: dart + type: roundrobin + nodes: + "stock-dart-collector:8001": 1 + checks: + active: + type: http + http_path: /health + healthy: + interval: 10 + successes: 2 + unhealthy: + interval: 5 + http_failures: 3 + + - id: kis + type: roundrobin + nodes: + "stock-kis-collector:8002": 1 + checks: + active: + type: http + http_path: /health + healthy: + interval: 10 + successes: 2 + unhealthy: + interval: 5 + http_failures: 3 + + - id: news + type: roundrobin + nodes: + "stock-news-crawler:8003": 1 + checks: + active: + type: http + http_path: /health + healthy: + interval: 10 + successes: 2 + unhealthy: + interval: 5 + http_failures: 3 + + - id: screener + type: roundrobin + nodes: + "stock-screener:8004": 1 + checks: + active: + type: http + http_path: /health + healthy: + interval: 10 + successes: 2 + unhealthy: + interval: 5 + http_failures: 3 + + - id: catalyst + type: roundrobin + nodes: + "stock-catalyst:8005": 1 + checks: + active: + type: http + http_path: /health + healthy: + interval: 10 + successes: 2 + unhealthy: + interval: 5 + http_failures: 3 + + - id: llm + type: roundrobin + nodes: + "stock-llm-analyzer:8006": 1 + checks: + active: + type: http + http_path: /health + healthy: + interval: 10 + successes: 2 + unhealthy: + interval: 5 + http_failures: 3 + +global_rules: + - id: cors + plugins: + cors: + allow_origins: "*" + allow_methods: "GET,POST,PUT,DELETE,OPTIONS" + allow_headers: "*" + max_age: 3600 + + - id: rate-limit + plugins: + limit-req: + rate: 100 + burst: 50 + key_type: var + key: remote_addr + rejected_code: 429 + +#END diff --git a/apisix/conf/config.yaml b/apisix/conf/config.yaml new file mode 100644 index 0000000..e207086 --- /dev/null +++ b/apisix/conf/config.yaml @@ -0,0 +1,15 @@ +# APISIX configuration +apisix: + node_listen: 9080 + enable_admin: false + +deployment: + role: data_plane + role_data_plane: + config_provider: yaml + +plugin_attr: + prometheus: + export_addr: + ip: 0.0.0.0 + port: 9091 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..289b082 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,169 @@ +version: "3.8" + +x-common-env: &common-env + REDIS_URL: redis://redis:6379/0 + POSTGRES_HOST: postgres + POSTGRES_PORT: "5432" + POSTGRES_DB: stockdb + POSTGRES_USER: stock + POSTGRES_PASSWORD: stock + MONGODB_URI: mongodb://mongo:27017 + MONGODB_DB: stock_analysis + LOG_LEVEL: INFO + LOG_FORMAT: json + +services: + # ─── Infrastructure ─────────────────────────────────── + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis-data:/data + command: redis-server --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + retries: 3 + + postgres: + image: timescale/timescaledb:latest-pg16 + environment: + POSTGRES_USER: stock + POSTGRES_PASSWORD: stock + POSTGRES_DB: stockdb + ports: + - "5432:5432" + volumes: + - pg-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U stock"] + interval: 5s + retries: 3 + + mongo: + image: mongo:7 + ports: + - "27017:27017" + volumes: + - mongo-data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.runCommand('ping').ok"] + interval: 5s + retries: 3 + + # ─── API Gateway ────────────────────────────────────── + apisix: + image: apache/apisix:3.8.0-debian + ports: + - "9080:9080" # API Gateway + - "9091:9091" # Prometheus metrics + volumes: + - ./apisix/conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro + - ./apisix/conf/apisix.yaml:/usr/local/apisix/conf/apisix.yaml:ro + depends_on: + - stock-dart-collector + - stock-kis-collector + - stock-news-crawler + - stock-screener + - stock-catalyst + - stock-llm-analyzer + + # ─── Microservices ──────────────────────────────────── + stock-dart-collector: + build: + context: .. + dockerfile: stock-dart-collector/Dockerfile + environment: + <<: *common-env + DART_API_KEY: ${DART_API_KEY} + ports: + - "8001:8001" + depends_on: + redis: + condition: service_healthy + + stock-kis-collector: + build: + context: .. + dockerfile: stock-kis-collector/Dockerfile + environment: + <<: *common-env + KIS_APP_KEY: ${KIS_APP_KEY} + KIS_APP_SECRET: ${KIS_APP_SECRET} + KIS_BASE_URL: ${KIS_BASE_URL:-https://openapivts.koreainvestment.com:9443} + ports: + - "8002:8002" + depends_on: + redis: + condition: service_healthy + + stock-news-crawler: + build: + context: .. + dockerfile: stock-news-crawler/Dockerfile + environment: + <<: *common-env + ports: + - "8003:8003" + depends_on: + redis: + condition: service_healthy + + stock-screener: + build: + context: .. + dockerfile: stock-screener/Dockerfile + environment: + <<: *common-env + ports: + - "8004:8004" + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + + stock-catalyst: + build: + context: .. + dockerfile: stock-catalyst/Dockerfile + environment: + <<: *common-env + ports: + - "8005:8005" + depends_on: + redis: + condition: service_healthy + mongo: + condition: service_healthy + + stock-llm-analyzer: + build: + context: .. + dockerfile: stock-llm-analyzer/Dockerfile + environment: + <<: *common-env + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} + ports: + - "8006:8006" + depends_on: + redis: + condition: service_healthy + + # ─── Frontend ───────────────────────────────────────── + stock-frontend: + build: + context: ../stock-frontend + dockerfile: Dockerfile + args: + NEXT_PUBLIC_API_URL: http://localhost:9080/api/v1 + ports: + - "3000:3000" + depends_on: + - apisix + +volumes: + redis-data: + pg-data: + mongo-data: