iop/docs/deploy-dev.md

22 KiB

Dev Field Deployment

이 문서는 dev 필드 테스트 기준의 배포 단위를 정리한다. Control Plane과 Web은 compose 서비스로 묶고, Edge와 Node는 호스트에서 직접 실행하는 단일 바이너리로 배포한다.

배포 단위

control host
  docker compose
    - postgres
    - redis
    - control-plane
    - web

edge host
  iop-edge binary
  edge.yaml

node host
  iop-node binary
  node.yaml

oto build host
  oto binary
  agent.yaml

Edge와 Node는 Docker 이미지로 만들지 않는다. Jenkins는 repo의 shell entrypoint를 호출해 Edge/Node 바이너리 산출물을 만든다. OTO는 별도 프로젝트의 단일 바이너리 산출물로 두고, IOP Edge는 장기적으로 OTO agent를 설치/등록시키는 bootstrap provider 역할을 가진다. Agent 설치가 어렵거나 일회성 유지보수 대상인 host/device는 별도 배포 단위로 보지 않고, 대상에 접근 가능한 Node의 terminal profile을 통해 Edge가 remote terminal bridge session을 중계하는 방향으로 둔다.

Edge/Node 바이너리 빌드

기본 빌드는 현재 호스트의 GOOS/GOARCH 대상으로 edge, node를 모두 만든다.

bin/build/field-binaries.sh

Jenkins에서 Linux amd64/arm64를 함께 만들 때는 다음처럼 호출한다.

TARGETS="linux/amd64 linux/arm64" \
VERSION="${BUILD_TAG}" \
COMMIT="${GIT_COMMIT}" \
bin/build/field-binaries.sh

산출물은 dist/field/<version>/<goos>-<goarch>/ 아래에 생성된다.

iop-edge
iop-edge.sha256
iop-node
iop-node.sha256
SHA256SUMS
manifest.env

Control Plane/Web Compose

루트의 .env.example을 참고해 필요한 값만 .env로 복사해 조정한다.

cp .env.example .env
docker compose up --build -d

서비스 포트 기본값은 다음과 같다.

Service Port Purpose
web 3000 Web Portal
control-plane 9080 HTTP health/bootstrap
control-plane wire 19080 IOP wire endpoint
postgres 5432 Control Plane DB
redis 6379 Control Plane cache/queue 후보

현재 Control Plane 구현은 DB/Redis를 아직 사용하지 않지만, dev compose에는 먼저 포함해 이후 schema, queue, audit, event 처리 작업을 같은 배포 단위 안에서 진행할 수 있게 둔다. 로컬에서 Control Plane 바이너리를 직접 실행하는 테스트는 외부 code-server-postgresiop-control-plane-local DB와 외부 code-server-redis의 Redis DB 1, iop:control-plane:local: key prefix를 사용한다. dev compose 환경은 Postgres iop-control-plane-dev DB와 Redis DB 2, iop:control-plane:dev: key prefix를 사용한다. compose의 control-plane 서비스는 IOP_DATABASE_URL, IOP_REDIS_URL, IOP_REDIS_KEY_PREFIX 환경변수로 dev 값을 지정해 configs/control-plane.yaml의 로컬 기본값을 덮어쓴다.

헬스체크는 다음 명령으로 확인한다.

docker compose ps
curl -fsS http://localhost:${IOP_CONTROL_PLANE_HTTP_PORT:-9080}/healthz
curl -fsS http://localhost:${IOP_CONTROL_PLANE_HTTP_PORT:-9080}/readyz

Edge 실행

Jenkins 산출물의 iop-edge를 edge host에 배포한 뒤, 초기에는 dev 환경의 edge.yaml을 지정해 직접 실행한다.

./iop-edge serve --config /etc/iop/edge.yaml

콘솔 기반 현장 smoke가 필요하면 다음처럼 실행한다.

./iop-edge console --config /etc/iop/edge.yaml

dev 초기 단계에서는 repo의 configs/edge.yaml을 기준으로 포트와 node token을 맞춘다. 이후 Control Plane enrollment/config sync가 붙으면 edge 설정 파일은 bootstrap 정보만 남기는 방향으로 줄인다.

Node 실행

Jenkins 산출물의 iop-node를 node host에 배포한 뒤, 초기에는 edge 주소와 token이 들어 있는 node.yaml로 직접 실행한다.

./iop-node serve --config /etc/iop/node.yaml

repo root에서 수동으로 검증할 때는 기존 helper를 사용할 수 있다.

IOP_EDGE_ADDR=<edge-host>:9090 ./bin/node.sh

Field 테스트 환경 라우팅

field 테스트의 1차 기준 환경은 ssh toki@toki-labs.com의 code-server 컨테이너다. 컨테이너 compose 원본은 원격 ~/docker/services/code-server/compose/docker-compose.yml이며, 재기동 타이밍은 운영자가 정한다. 이 저장소의 작업자는 compose를 수정할 수 있지만, 사용자가 명시하지 않으면 code-server 컨테이너를 재시작하지 않는다.

Host port Container port Purpose
13000-13099 3000-3099 web/dev app preview range
18080 18080 IOP field artifact/bootstrap HTTP
18081 8080 Edge OpenAI-compatible HTTP
19080 19080 Portal/Control Plane wire test endpoint
19090 9090 Edge-Node TCP transport
19092 9092 Edge metrics
19190 9190 OTO/specialized agent field transport reserve

field bootstrap UX는 Jenkins agent 연결처럼 Edge가 command를 제시하고 대상 host가 실행하는 형태를 목표로 한다. 현재 field 기준 artifact/bootstrap 외부 주소는 http://toki-labs.com:18080이며, Edge-Node 런타임 주소는 toki-labs.com:19090이다. 초기 field 단계의 Node bootstrap은 다음 요구를 만족해야 한다.

  • Edge 또는 field artifact server가 dist/field/<version>/<goos>-<goarch>/iop-node를 정해진 URL로 노출한다.
  • Apple Silicon Mac은 darwin/arm64 산출물을 사용한다. linux/arm64는 Mac 실행 대상이 아니다.
  • 사용자가 실행하는 명령은 한 줄 수준으로 유지하고, 내부에서 다운로드, 실행 권한 부여, config 생성, iop-node serve까지 이어진다.
  • 직접 포트가 아직 적용되지 않았거나 외부망에서 막히면 SSH tunnel 또는 reverse tunnel은 fallback으로만 사용한다.

Apple Silicon Node용 field artifact는 다음처럼 만든다. Node token은 artifact에 굽지 않고 사용자 명령의 첫 번째 인자로 전달한다.

VERSION=<field-version> \
APPS=node \
TARGETS="darwin/arm64" \
ARTIFACT_BASE_URL="http://toki-labs.com:18080" \
BOOTSTRAP_EDGE_ADDR="toki-labs.com:19090" \
DIST_DIR=dist/field \
./bin/build/field-binaries.sh

산출물 layout과 artifact URL 규칙은 다음을 기준으로 한다.

dist/field/<field-version>/darwin-arm64/iop-node
dist/field/<field-version>/darwin-arm64/SHA256SUMS
dist/field/<field-version>/bootstrap/node-darwin-arm64.sh

http://toki-labs.com:18080/darwin-arm64/iop-node
http://toki-labs.com:18080/darwin-arm64/SHA256SUMS
http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh

정적 artifact/bootstrap HTTP는 version 디렉터리를 document root로 열어 둔다.

python3 -m http.server 18080 --bind 0.0.0.0 -d dist/field/<field-version>

작업자는 artifact URL을 완성한 뒤 사용자가 token 값 하나만 바꾸는 한 줄 명령을 전달한다. 사용자에게 전달하는 명령에는 <artifact-base-url> placeholder를 남기지 않는다.

curl -fsSL http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh | bash -s CHANGE_ME_FIELD_TOKEN

bootstrap 스크립트는 ~/iop-field를 만들고, darwin-arm64/iop-nodeSHA256SUMS를 받아 checksum을 확인한 뒤 node.yaml을 생성하고 iop-node serve --config ./node.yaml을 실행한다. 첫 번째 인자 CHANGE_ME_FIELD_TOKEN은 Edge config의 nodes[].token과 같은 값이어야 한다.

Field Bootstrap 작업자 테스트 Runbook

이 절차는 LLM/작업자가 사용자 직접 테스트를 준비하고 실패 지점을 분리할 때 기준으로 삼는다. 사용자에게 전달할 짧은 가이드는 docs/field-bootstrap-user-test.md를 사용한다. 다음 작업 재개와 포트 의미 정리는 docs/field-bootstrap-work-guide.md를 먼저 확인한다. 토큰과 version 값은 테스트 시점에 맞춰 바꾸고, 기본 configs/*.yaml은 수정하지 않는다.

1. code-server 포트 확인

원격 host에서 code-server 컨테이너의 외부 포트가 실제로 열렸는지 먼저 확인한다.

ssh toki@toki-labs.com '/usr/local/bin/docker ps --format "{{.Names}} {{.Ports}}" | grep "^code-server "'

기대 포트:

18080 artifact/bootstrap HTTP
18081 Edge OpenAI-compatible HTTP
19090 Edge-Node TCP transport
19092 Edge metrics
19190 OTO/specialized agent reserve

docker ps에 포트가 보이지 않으면 compose 파일은 갱신됐지만 컨테이너 recreate가 아직 안 된 상태일 수 있다. 컨테이너 recreate 타이밍은 운영자가 정한다.

2. Edge field config 작성

edge host에서 임시 설정 파일을 만든다. Apple Silicon Mac에서 Ollama를 실행하고 Node도 같은 Mac에서 실행할 경우, Edge가 Node에 내려줄 Ollama base URL은 Node 관점의 http://127.0.0.1:11434다.

cat > /tmp/iop-edge-field-bootstrap.yaml <<'YAML'
edge:
  id: "edge-field-bootstrap"
  name: "Field Bootstrap Edge"

server:
  listen: "0.0.0.0:9090"

openai:
  enabled: true
  listen: "0.0.0.0:8080"
  node: "node-silicon-ollama"
  adapter: "ollama"
  target: ""
  models:
    - "gemma4:26b"
  session_id: "openai-field"
  timeout_sec: 300
  strict_output: true
  strict_stream_buffer: false

metrics:
  port: 9092

nodes:
  - id: "node-silicon-ollama"
    alias: "silicon-ollama"
    token: "CHANGE_ME_FIELD_TOKEN"
    adapters:
      ollama:
        enabled: true
        base_url: "http://127.0.0.1:11434"
        context_size: 262144
YAML

config가 로드되는지 확인한다.

go run ./apps/edge/cmd/edge config check --config /tmp/iop-edge-field-bootstrap.yaml

3. Apple Silicon artifact 생성

repo root에서 Apple Silicon Node artifact와 bootstrap script를 만든다.

VERSION=field-bootstrap-manual \
APPS=node \
TARGETS="darwin/arm64" \
ARTIFACT_BASE_URL="http://toki-labs.com:18080" \
BOOTSTRAP_EDGE_ADDR="toki-labs.com:19090" \
DIST_DIR=dist/field \
./bin/build/field-binaries.sh

기대 파일:

test -x dist/field/field-bootstrap-manual/darwin-arm64/iop-node
test -f dist/field/field-bootstrap-manual/darwin-arm64/SHA256SUMS
test -x dist/field/field-bootstrap-manual/bootstrap/node-darwin-arm64.sh

4. artifact/bootstrap HTTP 시작

version 디렉터리를 document root로 열어 둔다.

python3 -m http.server 18080 --bind 0.0.0.0 -d dist/field/field-bootstrap-manual

다른 터미널에서 URL이 보이는지 확인한다.

curl -fsS http://127.0.0.1:18080/darwin-arm64/SHA256SUMS
curl -fsS http://127.0.0.1:18080/bootstrap/node-darwin-arm64.sh | sed -n '1,40p'

기준 field 환경에서는 Apple Silicon Mac에서도 같은 URL이 보여야 한다. 18080을 사용할 수 없는 별도 환경에서만 내부망, front/control-plane, CI artifact, 임시 로컬 HTTP 같은 artifact 채널을 사용한다.

curl -fsS http://toki-labs.com:18080/darwin-arm64/SHA256SUMS

5. Edge serve 시작

Edge process를 field config로 실행한다.

go run ./apps/edge/cmd/edge serve --config /tmp/iop-edge-field-bootstrap.yaml

다른 터미널에서 listener를 확인한다.

curl -fsS http://127.0.0.1:8080/healthz
curl -fsS http://127.0.0.1:8080/v1/models

/v1/models는 Node가 아직 연결되지 않아도 configured model 목록을 반환해야 한다.

6. Apple Silicon Mac에서 one-line bootstrap 실행

Mac에서 Ollama가 떠 있고 gemma4:26b를 사용할 수 있는지 먼저 확인한다.

ollama list | grep 'gemma4:26b'

그 다음 one-line bootstrap을 실행한다. 사용자에게 전달할 때는 완성된 URL을 포함하고, token 값만 마지막 positional value로 바꾸게 한다.

curl -fsSL http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh | bash -s CHANGE_ME_FIELD_TOKEN

성공하면 Mac에 다음 파일이 생기고, Node process가 foreground로 실행된다.

~/iop-field/iop-node
~/iop-field/node.yaml

Edge 로그에는 node registerednode-silicon-ollama가 보여야 한다.

7. Edge에서 모델 호출 확인

Edge host에서 Node 등록 후 OpenAI-compatible API를 호출한다.

curl -fsS http://127.0.0.1:8080/v1/models

non-streaming:

curl -fsS http://127.0.0.1:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"gemma4:26b","messages":[{"role":"user","content":"Reply with IOP_FIELD_BOOTSTRAP_OK only."}]}'

streaming:

curl -fsS -N http://127.0.0.1:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"gemma4:26b","stream":true,"messages":[{"role":"user","content":"Reply with IOP_FIELD_STREAM_OK only."}]}'

성공 기준:

  • Mac의 bootstrap 명령이 iop-node serve --config <node.yaml>까지 도달한다.
  • Edge 로그에 node registered가 나온다.
  • /v1/modelsgemma4:26b를 포함한다.
  • non-streaming 응답에서 content가 비어 있지 않다.
  • streaming 응답에서 SSE chunk와 data: [DONE]이 보인다.

8. 실패 시 확인 순서

다운로드 실패:

curl -v http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh
curl -v http://toki-labs.com:18080/darwin-arm64/iop-node

Node가 Edge에 연결하지 못함:

nc -vz toki-labs.com 19090
cat ~/iop-field/node.yaml

token 불일치:

  • Edge config의 nodes[].token 값과 bootstrap 명령 마지막의 token 인자 값을 맞춘다.

Ollama 호출 실패:

curl -fsS http://127.0.0.1:11434/api/tags

Apple Silicon Mac에서 Node와 Ollama를 함께 실행하는 경우 Edge config의 nodes[].adapters.ollama.base_urlhttp://127.0.0.1:11434여야 한다.

9. 정리

Mac의 foreground Node process는 Ctrl-C로 종료한다. 재실행은 같은 one-line command를 다시 실행하거나, 이미 받은 binary를 직접 실행한다.

~/iop-field/iop-node serve --config ~/iop-field/node.yaml

관련 완료 Milestone 기록은 agent-roadmap/archive/phase/serving-routing-optimization/milestones/field-bootstrap-test-port-readiness.md에 보존되어 있다. 이후 field bootstrap 관련 구현, 문서, 테스트 포트 변경은 새 활성 Milestone 또는 후속 Phase 범위로 판단한다.

공통 Ollama 필드 테스트 환경

OpenAI-compatible Ollama E2E 서빙 검증은 아래 필드 환경을 공통 기준으로 사용한다. 이 환경은 특정 마일스톤에만 두지 않고, 이후 모델 서빙, routing, fallback, client 통합 검증에서도 재사용한다. scripts/e2e-openai-ollama.sh 같은 fake Ollama smoke는 보조 확인이다. 완료 판정은 사용자가 직접 테스트하는 방식과 동일하게 bin/edge.sh, bin/node.sh, 실제 Ollama endpoint를 연결한 full-cycle 흐름으로 한다. gemma4:26b는 thinking을 먼저 stream할 수 있으므로, 기본 검증은 think:false로 우회하지 않는다. Edge 응답에서 thinking/reasoning stream과 최종 content stream이 분리되어 보이는지 확인한다.

항목
Node host ssh toki@toki-labs.com
Ollama base URL http://192.168.0.97:11434
기준 model gemma4:26b
권장 node id node-toki-labs-ollama
권장 node alias toki-labs-ollama

기본 configs/*.yaml은 필드 테스트 값으로 덮어쓰지 않는다. edge와 node는 임시 config 또는 /etc/iop/*.yaml 필드 설정을 사용한다. Node가 사용할 Ollama endpoint는 node config가 아니라 edge config의 nodes[].adapters.ollama.base_url에 둔다. Node는 registration 이후 Edge가 내려주는 adapter config로 Ollama를 호출한다.

edge host에서 임시 config를 만들 때의 기준은 다음과 같다. CHANGE_ME_FIELD_TOKEN<edge-host>는 테스트 시점의 값으로 바꾼다. openai.target은 비워 두면 요청의 model을 내부 target으로 사용하고, gemma4:26b로 고정하면 외부 요청 model과 무관하게 해당 model로만 호출한다. Node는 Edge로 outbound 연결하므로 ssh toki@toki-labs.com 환경에서 <edge-host>:9090에 접근할 수 있어야 한다. 로컬 개발 머신이 원격 host에서 직접 보이지 않으면 Edge를 접근 가능한 host에서 실행하거나 터널을 구성한다.

edge:
  id: "edge-toki-labs-field"
  name: "Toki Labs Field Edge"

server:
  listen: "0.0.0.0:9090"

openai:
  enabled: true
  listen: "0.0.0.0:8080"
  node: "node-toki-labs-ollama"
  adapter: "ollama"
  target: ""
  models:
    - "gemma4:26b"
  session_id: "openai-field"
  timeout_sec: 300
  strict_output: true
  strict_stream_buffer: false

nodes:
  - id: "node-toki-labs-ollama"
    alias: "toki-labs-ollama"
    token: "CHANGE_ME_FIELD_TOKEN"
    adapters:
      ollama:
        enabled: true
        base_url: "http://192.168.0.97:11434"
        context_size: 262144

edge 실행:

IOP_EDGE_CONFIG=/tmp/iop-edge-toki-labs.yaml ./bin/edge.sh

node host에 접속한 뒤 node 임시 config를 만든다.

ssh toki@toki-labs.com
transport:
  edge_addr: "<edge-host>:9090"
  token: "CHANGE_ME_FIELD_TOKEN"

logging:
  level: "info"
  pretty: true

metrics:
  port: 9091

node 실행:

IOP_NODE_CONFIG=/tmp/iop-node-toki-labs.yaml ./bin/node.sh

edge host에서 OpenAI-compatible API를 호출해 검증한다.

curl -fsS http://127.0.0.1:8080/v1/models
curl -fsS http://127.0.0.1:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"gemma4:26b","messages":[{"role":"user","content":"Reply with IOP_OLLAMA_E2E_OK only."}]}'

streaming 검증:

curl -fsS -N http://127.0.0.1:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"gemma4:26b","stream":true,"messages":[{"role":"user","content":"Reply with IOP_OLLAMA_STREAM_OK only."}]}'

완료 기준은 사용자가 재현하는 실제 절차에서 node registration, /v1/models 응답, non-streaming chat completion, streaming SSE chunk와 data: [DONE] 확인이다. gemma4:26b가 thinking을 내보내는 경우 thinking/reasoning chunk가 content와 섞이지 않고, content가 시작된 뒤 답변 stream이 이어지는지도 확인한다. Edge/Node 뒤의 소켓 relay가 사용자 UX에 드러나지 않아야 하며, 실패 시 edge와 node 양쪽 로그를 함께 확인한다.

Agent Bootstrap / Bridge References

OTO 같은 specialized domain agent의 bootstrap/enrollment 상세 계획은 agent-roadmap/milestones/agent-bootstrap-oto-enrollment.md에서 관리한다. Agent 설치가 어렵거나 임시 제어만 필요한 대상의 remote terminal bridge 계획은 agent-roadmap/milestones/remote-terminal-bridge-poc.md에서 관리한다.

이 배포 문서에서는 현재 dev field 기준의 배포 단위와 smoke 절차만 유지한다.

Field Smoke

최소 확인 순서는 다음을 기준으로 한다.

  1. docker compose ps에서 postgres, redis, control-plane, web이 healthy/running인지 확인한다.
  2. edge host에서 iop-edge console --config /etc/iop/edge.yaml을 실행한다.
  3. node host에서 iop-node serve --config /etc/iop/node.yaml을 실행한다.
  4. edge console에서 /nodes로 node 등록을 확인한다.
  5. 메시지 2회를 보내고 각 요청의 start, message, complete event가 edge console에 표시되는지 확인한다.
  6. /capabilities, /transport, /sessions를 실행해 command 응답을 확인한다.

보조 자동 smoke는 repo root에서 다음 명령으로 실행한다.

make test-e2e

이 명령은 보조 확인이며, 필드 배포 완료 기준은 실제 edge/node 바이너리 실행 흐름 확인이다.

Edge setup CLI

Edge 바이너리는 호스트 환경 준비를 setup 명령으로 일원화한다. 공식 운영 경로는 하나다.

sudo iop-edge setup --enable --start

배포 전에 결과를 검토하려면 --dry-run을 쓴다.

sudo iop-edge setup --dry-run --binary /usr/local/bin/iop-edge

setup은 다음을 담당한다.

  • 실행 user/group(iop) 준비
  • /etc/iop/var/lib/iop/edge 디렉터리 준비
  • 설정 파일이 없을 때만 기본 템플릿 생성 (--overwrite-config로 강제 갱신)
  • systemd unit 생성 또는 갱신 (/etc/systemd/system/iop-edge.service)
  • systemctl daemon-reload
  • 옵션에 따른 --enable, --start, --restart

setup--config 기본값은 /etc/iop/edge.yaml이다. dev 단계 명령(serve, console, config print/check)의 root persistent --config 기본값(configs/edge.yaml)과는 다르다.

별도 render, service install, service status 명령은 초기 범위에 넣지 않는다. 검토와 CI 확인은 setup --dry-run으로 흡수하고, 상태/로그/재시작은 systemctljournalctl을 기준 운영 도구로 둔다.

현재 구현된 edge CLI 표면은 다음과 같다.

iop-edge serve
iop-edge console
iop-edge setup
iop-edge config print
iop-edge config check
iop-edge version

setup은 여러 번 실행해도 같은 결과로 수렴한다. 기존 설정 파일은 기본적으로 덮어쓰지 않고, 강제 갱신이 필요하면 --overwrite-config를 지정한다.

Node setup CLI

Node 바이너리는 호스트 환경 준비를 setup 명령으로 일원화한다. 공식 운영 경로는 하나다.

sudo iop-node setup --enable --start

배포 전에 결과를 검토하려면 --dry-run을 쓴다.

sudo iop-node setup --dry-run --binary /usr/local/bin/iop-node

setup은 다음을 담당한다.

  • 실행 user/group(iop) 준비
  • /etc/iop/var/lib/iop/node 디렉터리 준비
  • 설정 파일이 없을 때만 기본 템플릿 생성 (--overwrite-config로 강제 갱신)
  • systemd unit 생성 또는 갱신 (/etc/systemd/system/iop-node.service)
  • systemctl daemon-reload
  • 옵션에 따른 --enable, --start, --restart

setup--config 기본값은 /etc/iop/node.yaml이다. dev 단계 명령(serve, config print/check)의 root persistent --config 기본값(configs/node.yaml)과는 다르다.

별도 render, service install, service status 명령은 초기 범위에 넣지 않는다. 검토와 CI 확인은 setup --dry-run으로 흡수하고, 상태/로그/재시작은 systemctljournalctl을 기준 운영 도구로 둔다.

현재 구현된 node CLI 표면은 다음과 같다.

iop-node serve
iop-node setup
iop-node config print
iop-node config check
iop-node version

setup은 여러 번 실행해도 같은 결과로 수렴한다. 기존 설정 파일은 기본적으로 덮어쓰지 않고, 강제 갱신이 필요하면 --overwrite-config를 지정한다.