
Eco²
Backend · Cloud infrastructure
Recycling guidance from one photo and a conversation.
An all-in-one AI assistant that understands how recycling works in Korea.
Eco² vocabulary
- edge
- auth offload
- domain api
- task queue
- scan pipeline
- chat agent
- event bus
- cluster
- gitops
- autoscaling
- observability
- load test
- 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
How the 24 nodes actually connect, one flow at a time.
Self-managed Kubernetes built with kubeadm. The map places the 24 physical nodes and the workloads on them inside README’s five layers and joins them with their actual connections. Pick a flow and only that path is highlighted, with numbered steps.
It identifies waste from a photo and explains how to dispose of it, and in conversation it calls tools for places, weather, recycling prices and web search, and generates images. In this team project I started on the cluster and deployment foundation and widened my scope to the domain backend, asynchronous processing, the conversational agent and verification.
From competition development through backend and operations hardening
The service has finished its run; a recorded demo and the technical report show it at work.
Scan request → live delivery
A photo-analysis request becomes queued work, and its progress events come back to the same browser.
BrowserDNS lookup: DNS lookupRoute 53
Only the name is resolved from Route 53 records. Requests do not pass through DNS.
Browserdata flow: HTTPS 443AWS ALB
TLS ends at the ALB.
AWS ALBdata flow: HTTP · NodePortIstio ingress Envoy
k8s-ingress-gatewayHanded to the Istio ingress Envoy through the EC2 instance target’s NodePort.
Istio ingress Envoy
k8s-ingress-gatewaydata flow: VirtualServiceScan APIk8s-api-scanRouted to the Scan API, which queues the work and hands back a job_id at once; a result poll answers 202 until the job is done.
Scan API
k8s-api-scandata flow: Celery taskRabbitMQk8s-rabbitmqThe Scan task goes onto RabbitMQ.
RabbitMQ
k8s-rabbitmqdata flow: consumescan-workerk8s-worker-ai · k8s-worker-ai-2scan-worker (Celery · gevent) in the AI pool takes it and runs Vision → Rule → Answer → Reward. The four stages are chained Celery tasks, not nodes.
AI pool
k8s-worker-ai · k8s-worker-ai-2data flow: HTTPSLLM APIThe scan and chat workers call the OpenAI and Google APIs.
scan-worker
k8s-worker-ai · k8s-worker-ai-2data flow: XADD scan:eventsStreamsk8s-redis-streamsProgress events are written to one of four Streams shards.
Streams
k8s-redis-streamsdata flow: XREADGROUPEvent Routerk8s-event-routerThe Event Router reads them as a consumer group and XACKs after processing; pending and reclaim keep delivery at-least-once.
Event Router
k8s-event-routerdata flow: SETEX stateState KVk8s-redis-streamsIt writes the latest state and a duplicate-processing marker to State KV on the same Redis node.
Event Router
k8s-event-routerdata flow: PUBLISHPub/Subk8s-redis-pubsubDelivered in real time on the job’s channel in shard Pub/Sub.
Pub/Sub
k8s-redis-pubsubdata flow: SUBSCRIBESSE Gatewayk8s-sse-gatewayThe SSE Gateway subscribes through one shared listener per process and fans out to each connected browser.
SSE Gateway
k8s-sse-gatewaydata flow: SSE streamBrowserA connection opened over the same ALB and ingress path. Delivery is at-least-once; duplicates are handled idempotently.
State KV
k8s-redis-streamsdata flow: Reconnect recoverySSE Gatewayk8s-sse-gatewayOn reconnect with Last-Event-ID, State KV and the retained Streams fill in missed events.
Chat · tokens and storage
Chat tokens take the same event path as Scan; checkpoints and messages are stored by two different workloads.
Browserdata flow: HTTPS 443AWS ALB
TLS ends at the ALB.
AWS ALBdata flow: HTTP · NodePortIstio ingress Envoy
k8s-ingress-gatewayHanded to the Istio ingress Envoy through the EC2 instance target’s NodePort.
Istio ingress Envoy
k8s-ingress-gatewaydata flow: VirtualServiceChat APIk8s-api-chatRouted to the Chat API.
Chat API
k8s-api-chatdata flow: Taskiq taskRabbitMQk8s-rabbitmqThe Chat task goes onto RabbitMQ.
RabbitMQ
k8s-rabbitmqdata flow: consumechat-workerk8s-worker-ai · k8s-worker-ai-2chat-worker (Taskiq · asyncio) classifies intent with LangGraph, then runs vision if needed, the selected tools, the aggregator and a summary if needed, and answers. Each request runs only the branches it needs.
AI pool
k8s-worker-ai · k8s-worker-ai-2data flow: HTTPSLLM APIThe scan and chat workers call the OpenAI and Google APIs.
chat-worker
k8s-worker-ai · k8s-worker-ai-2data flow: XADD chat:eventsStreamsk8s-redis-streamsTokens and stage events are written to four Streams shards; from there the delivery path is the same as Scan’s.
Streams
k8s-redis-streamsdata flow: XREADGROUPEvent Routerk8s-event-routerThe Event Router reads them as a consumer group and XACKs after processing; pending and reclaim keep delivery at-least-once.
Event Router
k8s-event-routerdata flow: PUBLISHPub/Subk8s-redis-pubsubDelivered in real time on the job’s channel in shard Pub/Sub.
Pub/Sub
k8s-redis-pubsubdata flow: SUBSCRIBESSE Gatewayk8s-sse-gatewayThe SSE Gateway subscribes through one shared listener per process and fans out to each connected browser.
SSE Gateway
k8s-sse-gatewaydata flow: SSE streamBrowserA connection opened over the same ALB and ingress path. Delivery is at-least-once; duplicates are handled idempotently.
chat-worker
k8s-worker-ai · k8s-worker-ai-2data flow: checkpointCachek8s-redis-cacheThe LangGraph checkpoint is kept in Redis Cache.
Cache
k8s-redis-cachedata flow: read checkpointcheckpoint-syncerk8s-worker-ai · k8s-worker-ai-2checkpoint-syncer reads the checkpoint from Redis.
checkpoint-syncer
k8s-worker-ai · k8s-worker-ai-2data flow: sync to PostgreSQLPostgreSQLk8s-postgresqlAsyncPostgresSaver stores it in PostgreSQL asynchronously.
Streams
k8s-redis-streamsdata flow: chat-persistence grouppersistence consumerk8s-api-chatThe persistence consumer on the Chat node reads done events as a consumer group.
persistence consumer
k8s-api-chatdata flow: save messagesPostgreSQLk8s-postgresqlChat messages are saved to PostgreSQL. This is a different workload from checkpoint-syncer.
Entry · auth
The synchronous path from the cluster’s entry to a domain API. Only policy-guarded paths check auth first; pod traffic between nodes runs on Calico VXLAN.
BrowserDNS lookup: DNS lookupRoute 53
Only the name is resolved from Route 53 records. Requests do not pass through DNS.
Browserdata flow: HTTPS 443AWS ALB
TLS ends at the ALB.
AWS ALBdata flow: HTTP · NodePortIstio ingress Envoy
k8s-ingress-gatewayHanded to the Istio ingress Envoy through the EC2 instance target’s NodePort.
Istio ingress Envoy
k8s-ingress-gatewaydata flow: ext-authz · gRPC 50051ext-authzk8s-api-authOnly paths under a CUSTOM policy ask the Go ext-authz first (2 s timeout, failOpen false). Paths such as login, health and docs are excluded.
ext-authz
k8s-api-authdata flow: blacklist lookupblacklistk8s-redis-authThe token is checked against the Redis blacklist.
Istio ingress Envoy
k8s-ingress-gatewaydata flow: VirtualServiceChat APIk8s-api-chatRouted to the Chat API.
Chat API
k8s-api-chatdata flow: gRPCimages APIk8s-api-imageThe chat → images synchronous call named in README.
auth API
k8s-api-authdata flow: gRPCusers APIk8s-api-usersThe auth → users synchronous call named in README.
Storage pool · async writes
The auth blacklist and character ownership are stored by storage-pool workers that take their work from RabbitMQ.
blacklist
k8s-redis-authdata flow: outbox:blacklistauth-relayk8s-worker-storage · k8s-worker-storage-2auth-relay (README’s auth-Bus) reads the Redis outbox.
auth-relay
k8s-worker-storage · k8s-worker-storage-2data flow: blacklist.eventsRabbitMQk8s-rabbitmqIt republishes to RabbitMQ, with retries and a DLQ.
RabbitMQ
k8s-rabbitmqdata flow: blacklist.eventsauth-workerk8s-worker-storage · k8s-worker-storage-2auth-worker receives the blacklist events.
auth-worker
k8s-worker-storage · k8s-worker-storage-2data flow: store blacklistblacklistk8s-redis-authThey are stored in the Redis blacklist.
RabbitMQ
k8s-rabbitmqdata flow: Celery batchusers-workerk8s-worker-storage · k8s-worker-storage-2users-worker receives character-ownership work in batches.
users-worker
k8s-worker-storage · k8s-worker-storage-2data flow: UPSERTPostgreSQLk8s-postgresqlIt is applied to PostgreSQL.
Configure · scale · observe
The Platform Layer is not a step requests pass through; it deploys, configures, scales and observes the other layers. The nodes are EC2 instances made with Terraform, with kubeadm and Calico installed by Ansible; CI only validates the Terraform and never applies it automatically.
GitHub Actions · Docker Hubconfigure · scale: image · manifest revisionArgo CD image updater
k8s-masterArgo CD brings the cluster configuration in line through App-of-Apps and Sync Waves. Only the image updater’s control-plane placement was confirmed.
ALB Controller · ExternalDNS
k8s-masterconfigure · scale: Ingress → ALB · DNS recordsAWS managedThe ALB Controller and ExternalDNS configure AWS-side resources. This is not a path requests take.
Istiod
k8s-masterconfigure · scale: xDS configurationIstio ingress Envoyk8s-ingress-gatewayIstiod pushes the Gateway, VirtualService and EnvoyFilter configuration over xDS to the ingress Envoy and each API’s Envoy sidecar.
KEDA
k8s-monitoringconfigure · scale: pod countAI poolk8s-worker-ai · k8s-worker-ai-2KEDA adjusts worker pod counts. The node count does not change.
KEDA
k8s-monitoringconfigure · scale: pod countstorage poolk8s-worker-storage · k8s-worker-storage-2Storage-pool worker pods are scaled the same way.
Fluent Bitobservability: logsElasticsearch
k8s-loggingFluent Bit is a DaemonSet on every node and sends logs to Elasticsearch.
AI pool
k8s-worker-ai · k8s-worker-ai-2observability: LLM run tracingLangSmithLLM call traces go to LangSmith, an external SaaS.
Every connection at the same weight. Pick a flow to highlight only its path, with numbered steps.
- Solid · the direction requests, work, events and writes flow
- Dotted · DNS lookup; requests do not pass through it
- Dashed · configure or scale; not a data path
- Long dash-dot · logs and run tracing
- Heavy black line · the selected flow; numbers give the order
- Box · a physical node, host name in its header
- Inner chip · a workload running on that node
- Dashed frame · a two-node scheduling pool
- Dashed box · outside the cluster, not counted as a node
- Orange outline · where a model decides
- Control plane1 node
- Domain API8 nodes
- AI pool2 nodes
- storage pool2 nodes
- Data5 nodes
- Platform6 nodes
README’s five layers (the bands) and the physical groups (the shade of each node’s header) group the same 24 nodes differently. State KV shares the Streams node and is not counted separately; Route 53, the ALB and services outside the cluster are not nodes. A pool is a scheduling range, not a claim that every pod runs on both nodes.
24nodes
24 = 1 control plane + 8 API + 4 worker + 5 data + 6 platform
1Control plane
role: control-planekube-apiserver · scheduler · controller-manager · etcd, plus the platform controllers.
- k8s-master
8Domain API
domain: <service>One node per domain. Internal calls use gRPC.
- k8s-api-auth
- k8s-api-users
- k8s-api-scan
- k8s-api-character
- k8s-api-location
- k8s-api-image
- k8s-api-chat
- k8s-api-info
4Execution worker
Two scheduling pools of two nodes each. KEDA changes pod counts; the node count stays the same.
storage pool
domain: worker-storage- k8s-worker-storage
- k8s-worker-storage-2
AI pool
domain: worker-ai- k8s-worker-ai
- k8s-worker-ai-2
5Data
Redis is split across four purpose-specific nodes. PostgreSQL storage is EBS CSI · gp3.
- k8s-postgresql
- k8s-redis-auth
- k8s-redis-streams
- k8s-redis-cache
- k8s-redis-pubsub
6Platform
Messaging, observability, ingress and realtime delivery each have their own node.
- k8s-rabbitmq
- k8s-monitoring
- k8s-logging
- k8s-ingress-gateway
- k8s-sse-gateway
- k8s-event-router
- Every node
- Fluent Bit · DaemonSet
- Calico
Configured models · by provider
The service has been shut down, so these are the models configured in the repository, not choices available today. Each role is listed separately; this is not a list of verified runs.
OpenAI
Chat default provider
Chat
openai/gpt-5.2default
The default model is gpt-5.2; a model named in the request is used instead.
Scan
gpt-5.2defaultgpt-5.2-progpt-5.1gpt-5gpt-5-progpt-5-mini
Models outside the 13-model mapping are rejected.
Image mapping
gpt-image-1.5
Separate from gpt-5.2, the Responses orchestration model. Image generation is off by default.
Evaluation
gpt-4o-minidefault
The LLM grader for conversation evaluation, separate from the Chat model.
Google
When Chat selects Google
Chat
google/gemini-3-pro-previewgoogle/gemini-3-flash-preview
With Google selected, gemini-3-flash-preview is the default.
Scan
gemini-3-pro-previewgemini-3-flash-previewgemini-2.5-progemini-2.5-flashgemini-2.5-flash-litegemini-2.0-flashgemini-2.0-flash-lite
Image mapping
gemini-3-pro-image-preview
Sourcebackend main d8840c7Public commit September 29, 2026 · checked October 8, 2026