The Eco² globe character, refined for this portfolio from the latest official main character: a sky-blue globe with green land, a sprout on top and a brown smiling face
Back to the hall

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.

2025.10.31 — 2026.02.10

Team project · infrastructure and deployment foundation → domain backend → async processing, conversational agent, verification

2025 새싹 해커톤 (Saessak Hackathon)Excellence Award · 2025.12.02

  • Web appService closed

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

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.

Cluster connection map · redrawn from the manifests and code at commit a072127
Eco² cluster connection map · 24 nodesA map of the 24 physical nodes inside README’s five layer bands. The browser only looks the name up in Route 53; after TLS ends at the ALB on 443, the Istio ingress Envoy on a NodePort sends the request to a domain API. The Scan API queues the work on RabbitMQ and hands back a job_id at once; scan-worker in the AI pool XADDs progress events to Redis Streams. The Event Router reads them, updates State KV and publishes to Pub/Sub, and the SSE Gateway sends them to the browser. checkpoint-syncer stores Chat checkpoints and the persistence consumer on the Chat node stores messages in PostgreSQL. The Platform Layer is not a request path; it configures, scales and observes.BrowserHTTPS · SSEAWS ALB443 · TLS ends hereRoute 53DNSGitHub Actions · Docker Hubimage · manifest revisionLLM APIOpenAI · GoogleLangSmithLLM run tracingFluent BitDaemonSet · every nodek8s-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 cluster24 nodes · kubeadmEdge Layer1 nodeService Layer8 nodes · Envoy sidecarIntegration Layer7 nodesPersistence Layer5 nodesPlatform Layer3 nodes · configure · scale · observeAWS managedAI pooldomain: worker-aistorage pooldomain: worker-storageNo pinned node1DNS lookup2 HTTPS 443 · Browser → AWS ALB23HTTP · NodePort4VirtualService5Celery task6 consume · RabbitMQ → scan-worker67 HTTPS · AI pool → LLM API78XADD scan:events9XREADGROUP10SETEX state11PUBLISH12SUBSCRIBE13SSE stream14Reconnect recovery1 HTTPS 443 · Browser → AWS ALB12HTTP · NodePort3VirtualService4Taskiq task5 consume · RabbitMQ → chat-worker56 HTTPS · AI pool → LLM API67XADD chat:events8XREADGROUP9PUBLISH10SUBSCRIBE11SSE stream12checkpoint13read checkpoint14sync to PostgreSQL15chat-persistence group16save messages1DNS lookup2 HTTPS 443 · Browser → AWS ALB23HTTP · NodePort4ext-authz · gRPC 500515blacklist lookup6VirtualService7gRPC8gRPC1outbox:blacklist2blacklist.events3 blacklist.events · RabbitMQ → auth-worker34store blacklist5 Celery batch · RabbitMQ → users-worker56UPSERT1image · manifest revision2Ingress → ALB · DNS records3xDS configuration4pod count5 pod count · KEDA → storage pool56logs7 LLM run tracing · AI pool → LangSmith7

Scan request → live delivery

A photo-analysis request becomes queued work, and its progress events come back to the same browser.

  1. BrowserDNS lookup: DNS lookupRoute 53

    Only the name is resolved from Route 53 records. Requests do not pass through DNS.

  2. Browserdata flow: HTTPS 443AWS ALB

    TLS ends at the ALB.

  3. AWS ALBdata flow: HTTP · NodePortIstio ingress Envoyk8s-ingress-gateway

    Handed to the Istio ingress Envoy through the EC2 instance target’s NodePort.

  4. Istio ingress Envoyk8s-ingress-gatewaydata flow: VirtualServiceScan APIk8s-api-scan

    Routed 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.

  5. Scan APIk8s-api-scandata flow: Celery taskRabbitMQk8s-rabbitmq

    The Scan task goes onto RabbitMQ.

  6. RabbitMQk8s-rabbitmqdata flow: consumescan-workerk8s-worker-ai · k8s-worker-ai-2

    scan-worker (Celery · gevent) in the AI pool takes it and runs Vision → Rule → Answer → Reward. The four stages are chained Celery tasks, not nodes.

  7. AI poolk8s-worker-ai · k8s-worker-ai-2data flow: HTTPSLLM API

    The scan and chat workers call the OpenAI and Google APIs.

  8. scan-workerk8s-worker-ai · k8s-worker-ai-2data flow: XADD scan:eventsStreamsk8s-redis-streams

    Progress events are written to one of four Streams shards.

  9. Streamsk8s-redis-streamsdata flow: XREADGROUPEvent Routerk8s-event-router

    The Event Router reads them as a consumer group and XACKs after processing; pending and reclaim keep delivery at-least-once.

  10. Event Routerk8s-event-routerdata flow: SETEX stateState KVk8s-redis-streams

    It writes the latest state and a duplicate-processing marker to State KV on the same Redis node.

  11. Event Routerk8s-event-routerdata flow: PUBLISHPub/Subk8s-redis-pubsub

    Delivered in real time on the job’s channel in shard Pub/Sub.

  12. Pub/Subk8s-redis-pubsubdata flow: SUBSCRIBESSE Gatewayk8s-sse-gateway

    The SSE Gateway subscribes through one shared listener per process and fans out to each connected browser.

  13. SSE Gatewayk8s-sse-gatewaydata flow: SSE streamBrowser

    A connection opened over the same ALB and ingress path. Delivery is at-least-once; duplicates are handled idempotently.

  14. State KVk8s-redis-streamsdata flow: Reconnect recoverySSE Gatewayk8s-sse-gateway

    On 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.

  1. Browserdata flow: HTTPS 443AWS ALB

    TLS ends at the ALB.

  2. AWS ALBdata flow: HTTP · NodePortIstio ingress Envoyk8s-ingress-gateway

    Handed to the Istio ingress Envoy through the EC2 instance target’s NodePort.

  3. Istio ingress Envoyk8s-ingress-gatewaydata flow: VirtualServiceChat APIk8s-api-chat

    Routed to the Chat API.

  4. Chat APIk8s-api-chatdata flow: Taskiq taskRabbitMQk8s-rabbitmq

    The Chat task goes onto RabbitMQ.

  5. RabbitMQk8s-rabbitmqdata flow: consumechat-workerk8s-worker-ai · k8s-worker-ai-2

    chat-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.

  6. AI poolk8s-worker-ai · k8s-worker-ai-2data flow: HTTPSLLM API

    The scan and chat workers call the OpenAI and Google APIs.

  7. chat-workerk8s-worker-ai · k8s-worker-ai-2data flow: XADD chat:eventsStreamsk8s-redis-streams

    Tokens and stage events are written to four Streams shards; from there the delivery path is the same as Scan’s.

  8. Streamsk8s-redis-streamsdata flow: XREADGROUPEvent Routerk8s-event-router

    The Event Router reads them as a consumer group and XACKs after processing; pending and reclaim keep delivery at-least-once.

  9. Event Routerk8s-event-routerdata flow: PUBLISHPub/Subk8s-redis-pubsub

    Delivered in real time on the job’s channel in shard Pub/Sub.

  10. Pub/Subk8s-redis-pubsubdata flow: SUBSCRIBESSE Gatewayk8s-sse-gateway

    The SSE Gateway subscribes through one shared listener per process and fans out to each connected browser.

  11. SSE Gatewayk8s-sse-gatewaydata flow: SSE streamBrowser

    A connection opened over the same ALB and ingress path. Delivery is at-least-once; duplicates are handled idempotently.

  12. chat-workerk8s-worker-ai · k8s-worker-ai-2data flow: checkpointCachek8s-redis-cache

    The LangGraph checkpoint is kept in Redis Cache.

  13. Cachek8s-redis-cachedata flow: read checkpointcheckpoint-syncerk8s-worker-ai · k8s-worker-ai-2

    checkpoint-syncer reads the checkpoint from Redis.

  14. checkpoint-syncerk8s-worker-ai · k8s-worker-ai-2data flow: sync to PostgreSQLPostgreSQLk8s-postgresql

    AsyncPostgresSaver stores it in PostgreSQL asynchronously.

  15. Streamsk8s-redis-streamsdata flow: chat-persistence grouppersistence consumerk8s-api-chat

    The persistence consumer on the Chat node reads done events as a consumer group.

  16. persistence consumerk8s-api-chatdata flow: save messagesPostgreSQLk8s-postgresql

    Chat 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.

  1. BrowserDNS lookup: DNS lookupRoute 53

    Only the name is resolved from Route 53 records. Requests do not pass through DNS.

  2. Browserdata flow: HTTPS 443AWS ALB

    TLS ends at the ALB.

  3. AWS ALBdata flow: HTTP · NodePortIstio ingress Envoyk8s-ingress-gateway

    Handed to the Istio ingress Envoy through the EC2 instance target’s NodePort.

  4. Istio ingress Envoyk8s-ingress-gatewaydata flow: ext-authz · gRPC 50051ext-authzk8s-api-auth

    Only 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.

  5. ext-authzk8s-api-authdata flow: blacklist lookupblacklistk8s-redis-auth

    The token is checked against the Redis blacklist.

  6. Istio ingress Envoyk8s-ingress-gatewaydata flow: VirtualServiceChat APIk8s-api-chat

    Routed to the Chat API.

  7. Chat APIk8s-api-chatdata flow: gRPCimages APIk8s-api-image

    The chat → images synchronous call named in README.

  8. auth APIk8s-api-authdata flow: gRPCusers APIk8s-api-users

    The 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.

  1. blacklistk8s-redis-authdata flow: outbox:blacklistauth-relayk8s-worker-storage · k8s-worker-storage-2

    auth-relay (README’s auth-Bus) reads the Redis outbox.

  2. auth-relayk8s-worker-storage · k8s-worker-storage-2data flow: blacklist.eventsRabbitMQk8s-rabbitmq

    It republishes to RabbitMQ, with retries and a DLQ.

  3. RabbitMQk8s-rabbitmqdata flow: blacklist.eventsauth-workerk8s-worker-storage · k8s-worker-storage-2

    auth-worker receives the blacklist events.

  4. auth-workerk8s-worker-storage · k8s-worker-storage-2data flow: store blacklistblacklistk8s-redis-auth

    They are stored in the Redis blacklist.

  5. RabbitMQk8s-rabbitmqdata flow: Celery batchusers-workerk8s-worker-storage · k8s-worker-storage-2

    users-worker receives character-ownership work in batches.

  6. users-workerk8s-worker-storage · k8s-worker-storage-2data flow: UPSERTPostgreSQLk8s-postgresql

    It 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.

  1. GitHub Actions · Docker Hubconfigure · scale: image · manifest revisionArgo CD image updaterk8s-master

    Argo CD brings the cluster configuration in line through App-of-Apps and Sync Waves. Only the image updater’s control-plane placement was confirmed.

  2. ALB Controller · ExternalDNSk8s-masterconfigure · scale: Ingress → ALB · DNS recordsAWS managed

    The ALB Controller and ExternalDNS configure AWS-side resources. This is not a path requests take.

  3. Istiodk8s-masterconfigure · scale: xDS configurationIstio ingress Envoyk8s-ingress-gateway

    Istiod pushes the Gateway, VirtualService and EnvoyFilter configuration over xDS to the ingress Envoy and each API’s Envoy sidecar.

  4. KEDAk8s-monitoringconfigure · scale: pod countAI poolk8s-worker-ai · k8s-worker-ai-2

    KEDA adjusts worker pod counts. The node count does not change.

  5. KEDAk8s-monitoringconfigure · scale: pod countstorage poolk8s-worker-storage · k8s-worker-storage-2

    Storage-pool worker pods are scaled the same way.

  6. Fluent Bitobservability: logsElasticsearchk8s-logging

    Fluent Bit is a DaemonSet on every node and sends logs to Elasticsearch.

  7. AI poolk8s-worker-ai · k8s-worker-ai-2observability: LLM run tracingLangSmith

    LLM 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.

24-node layout · from the January 21, 2026 dashboard node roster

24nodes

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

  1. 1Control plane

    role: control-plane

    kube-apiserver · scheduler · controller-manager · etcd, plus the platform controllers.

    1. k8s-master
  2. 8Domain API

    domain: <service>

    One node per domain. Internal calls use 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

    Two scheduling pools of two nodes each. KEDA changes pod counts; the node count stays the same.

    storage pooldomain: worker-storage
    1. k8s-worker-storage
    2. k8s-worker-storage-2
    AI pooldomain: worker-ai
    1. k8s-worker-ai
    2. k8s-worker-ai-2
  4. 5Data

    Redis is split across four purpose-specific nodes. PostgreSQL storage is EBS CSI · gp3.

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

    Messaging, observability, ingress and realtime delivery each have their own node.

    1. k8s-rabbitmq
    2. k8s-monitoring
    3. k8s-logging
    4. k8s-ingress-gateway
    5. k8s-sse-gateway
    6. 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.2default
    • gpt-5.2-pro
    • gpt-5.1
    • gpt-5
    • gpt-5-pro
    • gpt-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-preview
    • google/gemini-3-flash-preview

    With Google selected, gemini-3-flash-preview is the default.

    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

    Image mapping

    • gemini-3-pro-image-preview