このポートフォリオのために最新の公式メインキャラクターをもとに整えたEco²の地球キャラクター:緑の陸地のある空色の地球、頭の上の新芽、茶色の笑顔
展示室へ戻る

Eco²

Backend · Cloud infrastructure

分別の案内を、写真1枚と対話で。

韓国の分別回収の仕組みを理解するオールインワンのAIアシスタントです。

2025.10.31 — 2026.02.10

チームプロジェクト · インフラ・デプロイ基盤 → ドメインバックエンド → 非同期処理・対話エージェント・検証

2025 새싹 해커톤 (Saessak Hackathon)優秀賞 · 2025年12月2日

  • Webアプリ運用終了

Eco² vocabulary

Lens
Service Path
Request Path
Async Work
Operations
Infrastructure
Measurement
  1. edge
  2. auth offload
  3. domain api
  1. task queue
  2. scan pipeline
  3. chat agent
  4. event bus
  1. cluster
  2. gitops
  3. autoscaling
  1. observability
  2. load test
  3. evaluation

Edge: ROUTE 53 · ALB · HTTPS 443 · internet-facing · external-dns · istio-ingressgateway · NodePort · eco2-gateway · VirtualService · x-canary · AuthorizationPolicy · ENVOY SIDECAR · kiali-serverAuth Offload: AuthorizationPolicy · action: CUSTOM · notPaths · envoyExtAuthzGrpc · failOpen: false · CheckRequest · x-user-id · BlacklistCache · BootstrapFromRedis · outbox:blacklist · blacklist.events · UsersServiceDomain API: VirtualService · x-user-id · /api/v1/scan · X-Idempotency-Key · job_id · stream_url · 202 ACCEPTED · Retry-After · ScanTask · UsersService · ImageService · ENVOY SIDECARTask Queue: outbox:blacklist · blacklist.events · RabbitmqCluster · scan.direct · reward.events · dlx · x-dead-letter-exchange · chat.process · AioPikaBroker · task_acks_late · apply_async · gevent · scan.vision · scan.rule · scan.answer · scan.reward · CELERY CHAIN · QueueLength · inspect ping · QUEUE SATURATIONScan Pipeline: ScanTask · scan.direct · reward.events · apply_async · gevent · scan.vision · scan.rule · scan.answer · scan.reward · CELERY CHAIN · RewardStep · item_class_list.yaml · situation_tags.yaml · LITE RAG · JsonRegulationRetriever · PipelineStage · waste_rag · XADD · domain: worker-ai · pipeline_duration · vision_stage_durationChat Agent: ImageService · chat.process · AioPikaBroker · create_chat_graph · route_by_intent · MULTI-INTENT FANOUT · waste_rag · recyclable_price · collection_point · image_generation · aggregator · thread_id · SyncableRedisSaver · AsyncPostgresSaver · LANGSMITH_TRACING · regenerateEvent Bus: job_id · stream_url · PipelineStage · XADD · scan:events:{shard} · XREADGROUP · XACK · XAUTOCLAIM · PendingReclaimer · STATE KV · scan:state:{job_id} · sse:events:{shard} · SSEBroadcastManager · Last-Event-ID · scan_sse_connections_active · ServiceMonitor · traceparentCluster: NodePort · kubeadm init · kubeadm join · vxlanMode · BGPConfiguration · module "vpc" · worker_ai · nodeSelector · domain: worker-ai · NoSchedule · 24-NODE CLUSTERGitOps: external-dns · x-canary · dev-root · ApplicationSet · sync-wave · selfHeal · ignoreDifferences · argocd-image-updater · update-strategy: digest · write-back-method · targetRevision · ci-services.ymlAutoscaling: ignoreDifferences · ScaledObject · QueueLength · cooldownPeriod · pollingInterval · fallback · maxReplicaCount · stabilizationWindowSeconds · HorizontalPodAutoscaler · scan_sse_connections_active · prometheus-adapterObservability: 24-NODE CLUSTER · prometheus-adapter · FLUENT BIT · Elasticsearch · Kibana · kube-prometheus-stack · AlertmanagerConfig · ServiceMonitor · JAEGER · OPENTELEMETRY · traceparent · kiali-server · LANGSMITH_TRACINGLoad Test: k6 · ramping-vus · thresholds · pipeline_duration · vision_stage_duration · poll_timeout · inspect ping · WORKER RESTART · POLLING TIMEOUT · QUEUE SATURATIONEvaluation: EvalConfig · eval_mode · eval_sample_rate · eval_cost_budget_daily_usd · EvalResult · EvalGrade · code_grader · llm_grader · CUSUM · regenerate · OpenAIBARSEvaluator

24ノードが実際にどうつながるかを、フローごとに。

kubeadmで構築したセルフマネージドのKubernetesです。構成図はREADMEの5つのレイヤーの中に24の物理ノードとその上のワークロードを配置し、実際の接続でつないでいます。フローを選ぶと、その経路だけが番号付きのステップで強調されます。

写真から廃棄物を識別して出し方を案内し、対話では場所・天気・リサイクル価格・Web検索のツールを呼び出し、画像も生成します。このチームプロジェクトでは、クラスターとデプロイの基盤から始め、ドメインのバックエンド、非同期処理、対話エージェント、検証へと担当範囲を広げました。

大会での開発から、バックエンドと運用の強化まで

サービスは運用を終えています。録画したデモと技術レポートで、動作の様子を見られます。

クラスター接続図 · コミットa072127のマニフェストとコードから再作図
Eco²クラスター接続図 · 24ノードREADMEの5つのレイヤーの帯の中に、24の物理ノードを配置した構成図です。ブラウザはRoute 53で名前を引くだけで、443番のALBでTLSが終端された後、NodePort上のIstio ingress EnvoyがリクエストをドメインAPIへ送ります。Scan APIはRabbitMQに作業をキューイングしてすぐにjob_idを返し、AIプールのscan-workerが進捗イベントをRedis StreamsにXADDします。Event Routerがそれを読み、State KVを更新してPub/Subに発行し、SSE Gatewayがブラウザへ送ります。checkpoint-syncerがChatのチェックポイントを、Chatノード上のpersistence consumerがメッセージを、PostgreSQLに保存します。Platform Layerはリクエストの経路ではなく、構成・スケーリング・観測を担います。ブラウザHTTPS · SSEAWS ALB443 · ここでTLS終端Route 53DNSGitHub Actions · Docker Hubイメージ · マニフェストのリビジョンLLM APIOpenAI · GoogleLangSmithLLM実行のトレースFluent BitDaemonSet · 全ノードk8s-masterALB Controller · ExternalDNSArgo CD image updaterIstiodcontrol planek8s-ingress-gatewayIstio ingress Envoyk8s-api-scanScan APIk8s-api-chatChat APImessage consumerpersistence consumerk8s-api-authauth APIext-authzk8s-api-infoInfo APIinfo-workerk8s-api-charactercharacter APIk8s-api-imageimages APIk8s-api-usersusers APIk8s-api-locationlocation APIk8s-sse-gatewaySSE Gatewayk8s-event-routerEvent Routerk8s-rabbitmqRabbitMQk8s-worker-aik8s-worker-ai-2k8s-worker-storagek8s-worker-storage-2k8s-redis-pubsubPub/Subk8s-redis-streamsStreamsState KVk8s-redis-cacheCachek8s-postgresqlPostgreSQLk8s-redis-authblacklistk8s-monitoringKEDAPrometheusGrafanaAlertmanagerk8s-loggingElasticsearchKibanascan-workerchat-workercheckpoint-syncerauth-relayauth-workerusers-workercharacter-workercharacter-match-workerKialiJaeger · dev in-memoryKubernetesクラスター24ノード · kubeadmEdge Layer1ノードService Layer8ノード · EnvoyサイドカーIntegration Layer7ノードPersistence Layer5ノードPlatform Layer3ノード · 構成 · スケーリング · 観測AWSマネージドAIプールdomain: worker-aistorageプールdomain: worker-storageノード指定なし1DNS参照2 HTTPS 443 · ブラウザ → AWS ALB23HTTP · NodePort4VirtualService5Celeryタスク6 consume · RabbitMQ → scan-worker67 HTTPS · AIプール → LLM API78XADD scan:events9XREADGROUP10SETEX state11PUBLISH12SUBSCRIBE13SSEストリーム14再接続時の復旧1 HTTPS 443 · ブラウザ → AWS ALB12HTTP · NodePort3VirtualService4Taskiqタスク5 consume · RabbitMQ → chat-worker56 HTTPS · AIプール → LLM API67XADD chat:events8XREADGROUP9PUBLISH10SUBSCRIBE11SSEストリーム12checkpoint13チェックポイントの読み取り14PostgreSQLへの同期15chat-persistence group16メッセージの保存1DNS参照2 HTTPS 443 · ブラウザ → AWS ALB23HTTP · NodePort4ext-authz · gRPC 500515ブラックリストの照会6VirtualService7gRPC8gRPC1outbox:blacklist2blacklist.events3 blacklist.events · RabbitMQ → auth-worker34ブラックリストの保存5 Celery batch · RabbitMQ → users-worker56UPSERT1イメージ · マニフェストのリビジョン2Ingress → ALB · DNSレコード3xDS設定4Pod数5 Pod数 · KEDA → storageプール56ログ7 LLM実行のトレース · AIプール → LangSmith7

Scanリクエスト → リアルタイム配信

写真解析のリクエストがキューの作業になり、その進捗イベントが同じブラウザに戻ります。

  1. ブラウザDNS参照: DNS参照Route 53

    Route 53のレコードで名前を解決するだけです。リクエストはDNSを通りません。

  2. ブラウザデータフロー: HTTPS 443AWS ALB

    ALBでTLSが終端されます。

  3. AWS ALBデータフロー: HTTP · NodePortIstio ingress Envoyk8s-ingress-gateway

    EC2インスタンスターゲットのNodePortを通じて、Istio ingress Envoyへ渡されます。

  4. Istio ingress Envoyk8s-ingress-gatewayデータフロー: VirtualServiceScan APIk8s-api-scan

    Scan APIへルーティングされます。Scan APIは作業をキューに入れてすぐにjob_idを返し、結果の照会にはジョブが終わるまで202で応答します。

  5. Scan APIk8s-api-scanデータフロー: CeleryタスクRabbitMQk8s-rabbitmq

    ScanのタスクがRabbitMQに入ります。

  6. RabbitMQk8s-rabbitmqデータフロー: consumescan-workerk8s-worker-ai · k8s-worker-ai-2

    AIプールのscan-worker(Celery · gevent)がそれを受け取り、Vision → Rule → Answer → Rewardを実行します。4つの段階はchainでつないだCeleryタスクであり、ノードではありません。

  7. AIプールk8s-worker-ai · k8s-worker-ai-2データフロー: HTTPSLLM API

    scanとchatのワーカーが、OpenAIとGoogleのAPIを呼び出します。

  8. scan-workerk8s-worker-ai · k8s-worker-ai-2データフロー: XADD scan:eventsStreamsk8s-redis-streams

    進捗イベントは、4つのStreamsシャードのいずれかに書き込まれます。

  9. Streamsk8s-redis-streamsデータフロー: XREADGROUPEvent Routerk8s-event-router

    Event Routerがコンシューマーグループとしてそれを読み、処理後にXACKします。pendingとreclaimにより、配信をat-least-onceに保ちます。

  10. Event Routerk8s-event-routerデータフロー: SETEX stateState KVk8s-redis-streams

    同じRedisノード上のState KVに、最新の状態と重複処理の印を書き込みます。

  11. Event Routerk8s-event-routerデータフロー: PUBLISHPub/Subk8s-redis-pubsub

    シャードPub/Subのジョブのチャンネルで、リアルタイムに配信されます。

  12. Pub/Subk8s-redis-pubsubデータフロー: SUBSCRIBESSE Gatewayk8s-sse-gateway

    SSE Gatewayはプロセスごとに共有のリスナー1つで購読し、接続中の各ブラウザへ振り分けて送ります。

  13. SSE Gatewayk8s-sse-gatewayデータフロー: SSEストリームブラウザ

    同じALBとingressの経路で開かれた接続です。配信はat-least-onceで、重複は冪等に処理します。

  14. State KVk8s-redis-streamsデータフロー: 再接続時の復旧SSE Gatewayk8s-sse-gateway

    Last-Event-IDで再接続すると、State KVと保持されたStreamsが取りこぼしたイベントを補います。

Chat · トークンと保存

ChatのトークンはScanと同じイベント経路をたどり、チェックポイントとメッセージは別々のワークロードが保存します。

  1. ブラウザデータフロー: HTTPS 443AWS ALB

    ALBでTLSが終端されます。

  2. AWS ALBデータフロー: HTTP · NodePortIstio ingress Envoyk8s-ingress-gateway

    EC2インスタンスターゲットのNodePortを通じて、Istio ingress Envoyへ渡されます。

  3. Istio ingress Envoyk8s-ingress-gatewayデータフロー: VirtualServiceChat APIk8s-api-chat

    Chat APIへルーティングされます。

  4. Chat APIk8s-api-chatデータフロー: TaskiqタスクRabbitMQk8s-rabbitmq

    ChatのタスクがRabbitMQに入ります。

  5. RabbitMQk8s-rabbitmqデータフロー: consumechat-workerk8s-worker-ai · k8s-worker-ai-2

    chat-worker(Taskiq · asyncio)がLangGraphで意図を分類し、必要に応じてvision、選ばれたツール、aggregator、必要に応じて要約を経て回答します。リクエストごとに必要な分岐だけを実行します。

  6. AIプールk8s-worker-ai · k8s-worker-ai-2データフロー: HTTPSLLM API

    scanとchatのワーカーが、OpenAIとGoogleのAPIを呼び出します。

  7. chat-workerk8s-worker-ai · k8s-worker-ai-2データフロー: XADD chat:eventsStreamsk8s-redis-streams

    トークンと段階イベントは4つのStreamsシャードに書き込まれ、そこからの配信経路はScanと同じです。

  8. Streamsk8s-redis-streamsデータフロー: XREADGROUPEvent Routerk8s-event-router

    Event Routerがコンシューマーグループとしてそれを読み、処理後にXACKします。pendingとreclaimにより、配信をat-least-onceに保ちます。

  9. Event Routerk8s-event-routerデータフロー: PUBLISHPub/Subk8s-redis-pubsub

    シャードPub/Subのジョブのチャンネルで、リアルタイムに配信されます。

  10. Pub/Subk8s-redis-pubsubデータフロー: SUBSCRIBESSE Gatewayk8s-sse-gateway

    SSE Gatewayはプロセスごとに共有のリスナー1つで購読し、接続中の各ブラウザへ振り分けて送ります。

  11. SSE Gatewayk8s-sse-gatewayデータフロー: SSEストリームブラウザ

    同じALBとingressの経路で開かれた接続です。配信はat-least-onceで、重複は冪等に処理します。

  12. chat-workerk8s-worker-ai · k8s-worker-ai-2データフロー: checkpointCachek8s-redis-cache

    LangGraphのチェックポイントは、Redis Cacheに保持されます。

  13. Cachek8s-redis-cacheデータフロー: チェックポイントの読み取りcheckpoint-syncerk8s-worker-ai · k8s-worker-ai-2

    checkpoint-syncerがRedisからチェックポイントを読みます。

  14. checkpoint-syncerk8s-worker-ai · k8s-worker-ai-2データフロー: PostgreSQLへの同期PostgreSQLk8s-postgresql

    AsyncPostgresSaverがPostgreSQLへ非同期に保存します。

  15. Streamsk8s-redis-streamsデータフロー: chat-persistence grouppersistence consumerk8s-api-chat

    Chatノード上のpersistence consumerが、doneイベントをコンシューマーグループとして読みます。

  16. persistence consumerk8s-api-chatデータフロー: メッセージの保存PostgreSQLk8s-postgresql

    チャットのメッセージをPostgreSQLに保存します。checkpoint-syncerとは別のワークロードです。

入口 · 認証

クラスターの入口からドメインAPIに届くまでの同期経路です。ポリシー対象のパスだけが先に認証を確認し、ノード間のPod通信はCalico VXLANです。

  1. ブラウザDNS参照: DNS参照Route 53

    Route 53のレコードで名前を解決するだけです。リクエストはDNSを通りません。

  2. ブラウザデータフロー: HTTPS 443AWS ALB

    ALBでTLSが終端されます。

  3. AWS ALBデータフロー: HTTP · NodePortIstio ingress Envoyk8s-ingress-gateway

    EC2インスタンスターゲットのNodePortを通じて、Istio ingress Envoyへ渡されます。

  4. Istio ingress Envoyk8s-ingress-gatewayデータフロー: ext-authz · gRPC 50051ext-authzk8s-api-auth

    CUSTOMポリシーの対象パスだけが、先にGoのext-authzに問い合わせます(タイムアウト2秒、failOpen false)。ログイン、health、docsなどのパスは除外されます。

  5. ext-authzk8s-api-authデータフロー: ブラックリストの照会blacklistk8s-redis-auth

    トークンをRedisのブラックリストと照合します。

  6. Istio ingress Envoyk8s-ingress-gatewayデータフロー: VirtualServiceChat APIk8s-api-chat

    Chat APIへルーティングされます。

  7. Chat APIk8s-api-chatデータフロー: gRPCimages APIk8s-api-image

    READMEに記されたchat → imagesの同期呼び出しです。

  8. auth APIk8s-api-authデータフロー: gRPCusers APIk8s-api-users

    READMEに記されたauth → usersの同期呼び出しです。

storageプール · 非同期保存

認証のブラックリストとキャラクター所有権は、RabbitMQから作業を受け取るstorageプールのワーカーが保存します。

  1. blacklistk8s-redis-authデータフロー: outbox:blacklistauth-relayk8s-worker-storage · k8s-worker-storage-2

    auth-relay(READMEのauth-Bus)がRedisのoutboxを読みます。

  2. auth-relayk8s-worker-storage · k8s-worker-storage-2データフロー: blacklist.eventsRabbitMQk8s-rabbitmq

    RabbitMQへ再発行します。再試行とDLQがあります。

  3. RabbitMQk8s-rabbitmqデータフロー: blacklist.eventsauth-workerk8s-worker-storage · k8s-worker-storage-2

    auth-workerがブラックリストのイベントを受け取ります。

  4. auth-workerk8s-worker-storage · k8s-worker-storage-2データフロー: ブラックリストの保存blacklistk8s-redis-auth

    Redisのブラックリストに保存されます。

  5. RabbitMQk8s-rabbitmqデータフロー: Celery batchusers-workerk8s-worker-storage · k8s-worker-storage-2

    users-workerが、キャラクター所有権の処理をまとめて受け取ります。

  6. users-workerk8s-worker-storage · k8s-worker-storage-2データフロー: UPSERTPostgreSQLk8s-postgresql

    PostgreSQLに反映されます。

構成 · スケーリング · 観測

Platform Layerはリクエストが通過するステップではなく、他のレイヤーをデプロイ・構成・スケール・観測します。ノードはTerraformで作成したEC2で、AnsibleでkubeadmとCalicoを導入しています。CIはTerraformを検証するだけで、自動でapplyすることはありません。

  1. GitHub Actions · Docker Hub構成 · スケール: イメージ · マニフェストのリビジョンArgo CD image updaterk8s-master

    Argo CDがApp-of-AppsとSync Wavesでクラスター構成をそろえます。control-planeへの配置を確認できたのは、image updaterだけです。

  2. ALB Controller · ExternalDNSk8s-master構成 · スケール: Ingress → ALB · DNSレコードAWSマネージド

    ALB ControllerとExternalDNSがAWS側のリソースを構成します。リクエストが通る経路ではありません。

  3. Istiodk8s-master構成 · スケール: xDS設定Istio ingress Envoyk8s-ingress-gateway

    IstiodがGateway、VirtualService、EnvoyFilterの設定を、xDSでingress Envoyと各APIのEnvoyサイドカーに配布します。

  4. KEDAk8s-monitoring構成 · スケール: Pod数AIプールk8s-worker-ai · k8s-worker-ai-2

    KEDAはワーカーのPod数を調整します。ノード数は変わりません。

  5. KEDAk8s-monitoring構成 · スケール: Pod数storageプールk8s-worker-storage · k8s-worker-storage-2

    storageプールのワーカーPodも、同じ方法でスケールします。

  6. Fluent Bit観測: ログElasticsearchk8s-logging

    Fluent Bitは全ノードで動くDaemonSetで、ログをElasticsearchへ送ります。

  7. AIプールk8s-worker-ai · k8s-worker-ai-2観測: LLM実行のトレースLangSmith

    LLM呼び出しのトレースは、外部SaaSのLangSmithへ送られます。

すべての接続を同じ太さで表示します。フローを選ぶと、その経路だけを番号付きで強調します。

  • 実線 · リクエスト、作業、イベント、書き込みが流れる方向
  • 点線 · DNS参照。リクエストは通過しない
  • 破線 · 構成またはスケール。データ経路ではない
  • 長い一点鎖線 · ログと実行トレース
  • 太い黒線 · 選んだフロー。番号は順序
  • ボックス · 物理ノード。見出しにホスト名
  • 内側のチップ · そのノードで動くワークロード
  • 破線の枠 · 2ノードのスケジューリングプール
  • 破線ボックス · クラスターの外。ノード数に含めない
  • オレンジの枠 · モデルが判断する場所
  • Control plane1ノード
  • Domain API8ノード
  • AIプール2ノード
  • storageプール2ノード
  • Data5ノード
  • Platform6ノード

READMEの5つのレイヤー(帯)と物理グループ(各ノードの見出しの色)は、同じ24ノードを異なる方法でまとめています。State KVはStreamsノードを共有しているため別に数えず、Route 53、ALB、クラスター外のサービスはノードではありません。プールはスケジューリングの範囲であり、すべてのPodが両方のノードで動くという意味ではありません。

24ノード構成 · 2026年1月21日のダッシュボードのノード一覧から

24ノード

24 = 1 control plane + 8 API + 4 worker + 5 data + 6 platform

  1. 1Control plane

    role: control-plane

    kube-apiserver · scheduler · controller-manager · etcdと、プラットフォームのコントローラー。

    1. k8s-master
  2. 8Domain API

    domain: <サービス>

    ドメインごとに1ノード。内部呼び出しはgRPCです。

    1. k8s-api-auth
    2. k8s-api-users
    3. k8s-api-scan
    4. k8s-api-character
    5. k8s-api-location
    6. k8s-api-image
    7. k8s-api-chat
    8. k8s-api-info
  3. 4Execution worker

    2ノードずつのスケジューリングプールが2つです。KEDAはPod数を変え、ノード数は変わりません。

    storageプールdomain: worker-storage
    1. k8s-worker-storage
    2. k8s-worker-storage-2
    AIプールdomain: worker-ai
    1. k8s-worker-ai
    2. k8s-worker-ai-2
  4. 5Data

    Redisは用途別に4つのノードに分けています。PostgreSQLのストレージはEBS CSI · gp3です。

    1. k8s-postgresql
    2. k8s-redis-auth
    3. k8s-redis-streams
    4. k8s-redis-cache
    5. k8s-redis-pubsub
  5. 6Platform

    メッセージング、可観測性、ingress、リアルタイム配信は、それぞれ専用のノードに置いています。

    1. k8s-rabbitmq
    2. k8s-monitoring
    3. k8s-logging
    4. k8s-ingress-gateway
    5. k8s-sse-gateway
    6. k8s-event-router
全ノード
  • Fluent Bit · DaemonSet
  • Calico

設定されていたモデル · プロバイダー別

サービスは終了しているため、これらは現在選べるモデルではなく、リポジトリで設定されていたモデルです。役割ごとに別々に記載しており、実行を確認した一覧ではありません。

  • OpenAI

    Chatの既定プロバイダー

    Chat

    • openai/gpt-5.2既定

    既定のモデルはgpt-5.2で、リクエストでモデルが指定された場合はそちらを使います。

    Scan

    • gpt-5.2既定
    • gpt-5.2-pro
    • gpt-5.1
    • gpt-5
    • gpt-5-pro
    • gpt-5-mini

    13モデルの対応表にないモデルは拒否されます。

    画像の対応表

    • gpt-image-1.5

    Responsesのオーケストレーションモデルであるgpt-5.2とは別です。画像生成は既定でオフです。

    評価

    • gpt-4o-mini既定

    会話評価に使うLLM graderで、Chatモデルとは別です。

  • Google

    ChatでGoogleを選んだ場合

    Chat

    • google/gemini-3-pro-preview
    • google/gemini-3-flash-preview

    Googleを選ぶと、gemini-3-flash-previewが既定です。

    Scan

    • gemini-3-pro-preview
    • gemini-3-flash-preview
    • gemini-2.5-pro
    • gemini-2.5-flash
    • gemini-2.5-flash-lite
    • gemini-2.0-flash
    • gemini-2.0-flash-lite

    画像の対応表

    • gemini-3-pro-image-preview