docs: edge local dev guide 및 관련 문서 업데이트

This commit is contained in:
toki 2026-05-28 20:10:54 +09:00
parent e02ae80ce6
commit 95693f1cec
6 changed files with 160 additions and 252 deletions

View file

@ -1,4 +1,4 @@
.PHONY: all build build-edge build-node build-node-target pack-node-target pack-edge tidy test test-e2e test-openai-ollama proto proto-dart portal-test portal-build-web clean
.PHONY: all build build-local build-edge build-node build-node-target build-node-targets pack-node-target pack-edge archive-edge tidy test test-e2e test-openai-ollama proto proto-dart portal-test portal-build-web clean
GOFLAGS ?= -trimpath
BUILD_DIR ?= build
@ -10,16 +10,17 @@ EDGE_PACKAGE_NAME ?= iop-edge-$(EDGE_TARGET)
EDGE_PACKAGE ?= $(PACK_DIR)/$(EDGE_PACKAGE_NAME).tar.gz
EDGE_PACKAGE_TMP ?= $(BUILD_DIR)/.edge-package
NODE_TARGET ?= $(shell go env GOOS)-$(shell go env GOARCH)
NODE_TARGETS ?= linux-arm64 linux-amd64 darwin-arm64 darwin-amd64 windows-arm64 windows-amd64
NODE_TARGET_PARTS = $(subst -, ,$(NODE_TARGET))
NODE_GOOS = $(word 1,$(NODE_TARGET_PARTS))
NODE_GOARCH = $(word 2,$(NODE_TARGET_PARTS))
all: build
build:
mkdir -p $(BUILD_BIN_DIR)
go build $(GOFLAGS) -o $(BUILD_BIN_DIR)/iop-edge ./apps/edge/cmd/edge
go build $(GOFLAGS) -o $(BUILD_BIN_DIR)/iop-node ./apps/node/cmd/node
build: build-node-targets
$(MAKE) archive-edge
build-local: build-edge build-node
build-edge:
mkdir -p $(BUILD_BIN_DIR)
@ -34,10 +35,20 @@ build-node-target:
mkdir -p $(BUILD_BIN_DIR)
GOOS=$(NODE_GOOS) GOARCH=$(NODE_GOARCH) go build $(GOFLAGS) -o $(BUILD_BIN_DIR)/iop-node-$(NODE_TARGET) ./apps/node/cmd/node
build-node-targets: build-edge
rm -rf "$(ARTIFACT_DIR)"
@for target in $(NODE_TARGETS); do \
echo "==> node target $$target"; \
$(MAKE) build-node-target NODE_TARGET=$$target; \
$(BUILD_BIN_DIR)/iop-edge bootstrap pack --target $$target --node-binary $(BUILD_BIN_DIR)/iop-node-$$target --output $(ARTIFACT_DIR); \
done
pack-node-target: build-edge build-node-target
$(BUILD_BIN_DIR)/iop-edge bootstrap pack --target $(NODE_TARGET) --node-binary $(BUILD_BIN_DIR)/iop-node-$(NODE_TARGET) --output $(ARTIFACT_DIR)
pack-edge: build-edge
pack-edge: build-edge archive-edge
archive-edge:
@test -d "$(ARTIFACT_DIR)/bootstrap" || (echo "missing node bootstrap artifacts; run make pack-node-target NODE_TARGET=<goos>-<goarch> first" >&2; exit 2)
rm -rf "$(EDGE_PACKAGE_TMP)"
mkdir -p "$(EDGE_PACKAGE_TMP)/$(EDGE_PACKAGE_NAME)/artifacts" "$(EDGE_PACKAGE_TMP)/$(EDGE_PACKAGE_NAME)/logs" "$(PACK_DIR)"

View file

@ -104,8 +104,10 @@ sudo iop-edge setup --dry-run --binary /usr/local/bin/iop-edge
sudo iop-edge setup --enable --start --binary /usr/local/bin/iop-edge
```
Edge 배포 archive는 repo root에서 `make pack-node-target NODE_TARGET=<goos>-<goarch>``make pack-edge`로 만든다.
archive에는 `iop-edge`와 Node bootstrap용 `artifacts/`가 들어가며, Edge host에서 `config init``node register`를 수행해 현재 host 기준 bootstrap URL을 확정한다.
Edge 배포 archive는 repo root에서 `make build`로 만든다.
archive에는 `iop-edge`와 Linux/macOS/Windows arm64/amd64 Node bootstrap용 `artifacts/`가 들어가며, Edge host에서 `config init``node register`를 수행해 현재 host 기준 bootstrap URL을 확정한다.
Node host는 `<artifact-base-url>/bootstrap/node.sh` 하나를 실행하고, bootstrap script가 OS/architecture를 감지해 맞는 Node artifact를 받는다.
특정 Node target만 담긴 개발용 package는 `make build NODE_TARGETS="<goos>-<goarch>"`로 만들 수 있다. Edge만 갱신할 때는 `make build-edge && make pack-edge`, Node artifact만 갱신할 때는 `make pack-node-target NODE_TARGET=<goos>-<goarch> && make pack-edge`를 사용한다.
`setup``--config` 기본값은 `/etc/iop/edge.yaml`이다. dev 단계 명령(`serve`, `console`, `config init/print/check`, `env`, `node register`)의 root persistent `--config` 기본값은 bundle-local `edge.yaml` (없을 시 executable-adjacent `edge.yaml`, 그 다음 repo dev fallback `configs/edge.yaml`)로 확인된다. 기존 설정 파일은 그대로 두며 `--overwrite-config`를 지정해야 덮어쓴다.

View file

@ -400,6 +400,10 @@ func bootstrapScriptURL(baseURL, target string) string {
return strings.TrimSuffix(baseURL, "/") + "/bootstrap/node-" + target + ".sh"
}
func universalBootstrapScriptURL(baseURL string) string {
return strings.TrimSuffix(baseURL, "/") + "/bootstrap/node.sh"
}
type envReport struct {
ConfigPath string
BinaryDir string
@ -428,7 +432,7 @@ func renderEnv(w io.Writer, r envReport) {
fmt.Fprintf(w, "artifact_dir: %s\n", r.Bootstrap.ArtifactDir)
fmt.Fprintf(w, "artifact_listen: %s\n", r.Bootstrap.Listen)
fmt.Fprintf(w, "artifact_base_url: %s (%s)\n", artifactURL, artifactSource)
fmt.Fprintf(w, "node_bootstrap: %s\n", bootstrapScriptURL(artifactURL, runtime.GOOS+"-"+runtime.GOARCH))
fmt.Fprintf(w, "node_bootstrap: %s\n", universalBootstrapScriptURL(artifactURL))
if r.OpenAI.Enabled {
fmt.Fprintf(w, "openai_url: http://%s/v1\n", addressFor(r.Advertise.Host, r.OpenAI.Listen))
} else {
@ -551,7 +555,7 @@ func bootstrapPackCmd() *cobra.Command {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "wrote bootstrap artifacts to %s\n", outputDir)
fmt.Fprintf(cmd.OutOrStdout(), "bootstrap: %s\n", bootstrapScriptURL(artifactBaseURL, target))
fmt.Fprintf(cmd.OutOrStdout(), "bootstrap: %s\n", universalBootstrapScriptURL(artifactBaseURL))
return nil
},
}
@ -642,6 +646,22 @@ func packNodeBootstrap(opts nodeBootstrapPackOptions) error {
if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
return fmt.Errorf("write bootstrap script: %w", err)
}
if err := writeUniversalBootstrapScript(bootstrapDir, opts); err != nil {
return err
}
return nil
}
func writeUniversalBootstrapScript(bootstrapDir string, opts nodeBootstrapPackOptions) error {
universalOpts := opts
universalOpts.Target = ""
scriptPath := filepath.Join(bootstrapDir, "node.sh")
if err := os.MkdirAll(bootstrapDir, 0o755); err != nil {
return fmt.Errorf("mkdir bootstrap dir: %w", err)
}
if err := os.WriteFile(scriptPath, []byte(nodeBootstrapScript(universalOpts)), 0o755); err != nil {
return fmt.Errorf("write universal bootstrap script: %w", err)
}
return nil
}
@ -702,18 +722,42 @@ func nodeBootstrapScript(opts nodeBootstrapPackOptions) string {
set -euo pipefail
VERSION=` + shellQuote(opts.Version) + `
TARGET=` + shellQuote(opts.Target) + `
DEFAULT_TARGET=` + shellQuote(opts.Target) + `
DEFAULT_ARTIFACT_BASE_URL=` + shellQuote(strings.TrimSuffix(opts.ArtifactBaseURL, "/")) + `
DEFAULT_EDGE_ADDR=` + shellQuote(opts.EdgeAddr) + `
POSITIONAL_NODE_TOKEN="${1:-}"
TARGET="$DEFAULT_TARGET"
ARTIFACT_BASE_URL="$DEFAULT_ARTIFACT_BASE_URL"
EDGE_ADDR="$DEFAULT_EDGE_ADDR"
NODE_TOKEN="$POSITIONAL_NODE_TOKEN"
INSTALL_DIR="${IOP_HOME:-$HOME/iop-field}"
CONFIG_FILE="${IOP_NODE_CONFIG:-$INSTALL_DIR/node.yaml}"
BIN_FILE="$INSTALL_DIR/iop-node"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/iop-node-bootstrap.XXXXXX")"
detect_target() {
local os arch
case "$(uname -s)" in
Linux*) os="linux" ;;
Darwin*) os="darwin" ;;
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
*)
echo "[iop-bootstrap] unsupported OS: $(uname -s)" >&2
return 2
;;
esac
case "$(uname -m)" in
x86_64|amd64) arch="amd64" ;;
arm64|aarch64) arch="arm64" ;;
*)
echo "[iop-bootstrap] unsupported architecture: $(uname -m)" >&2
return 2
;;
esac
printf '%s-%s\n' "$os" "$arch"
}
cleanup() {
rm -rf "$TMP_DIR"
}
@ -723,10 +767,19 @@ ARTIFACT_BASE_URL="${ARTIFACT_BASE_URL%/}"
if [[ -z "$NODE_TOKEN" ]]; then
echo "[iop-bootstrap] missing node token" >&2
echo "[iop-bootstrap] usage: curl -fsSL <artifact-base-url>/bootstrap/node-$TARGET.sh | bash -s TOKEN" >&2
echo "[iop-bootstrap] usage: curl -fsSL <artifact-base-url>/bootstrap/node.sh | bash -s TOKEN" >&2
exit 2
fi
if [[ -z "$TARGET" ]]; then
TARGET="$(detect_target)"
fi
BIN_FILE="$INSTALL_DIR/iop-node"
if [[ "$TARGET" == windows-* ]]; then
BIN_FILE="$INSTALL_DIR/iop-node.exe"
fi
echo "[iop-bootstrap] version=$VERSION target=$TARGET"
echo "[iop-bootstrap] install_dir=$INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
@ -785,6 +838,18 @@ var tokenGenerator = func() string {
func refreshBootstrapForExistingArtifact(cfg *config.EdgeConfig, target, artifactURL string) error {
outputDir := resolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir)
adv := resolveAdvertiseHost(cfg.Server.AdvertiseHost)
bootstrapDir := filepath.Join(outputDir, "bootstrap")
baseOpts := nodeBootstrapPackOptions{
OutputDir: outputDir,
Version: version.Version,
ArtifactBaseURL: artifactURL,
EdgeAddr: addressFor(adv.Host, cfg.Server.Listen),
}
if err := writeUniversalBootstrapScript(bootstrapDir, baseOpts); err != nil {
return err
}
nodeBinary := filepath.Join(outputDir, target, "iop-node")
if _, err := os.Stat(nodeBinary); err != nil {
if errors.Is(err, os.ErrNotExist) {
@ -792,15 +857,10 @@ func refreshBootstrapForExistingArtifact(cfg *config.EdgeConfig, target, artifac
}
return fmt.Errorf("stat node artifact: %w", err)
}
adv := resolveAdvertiseHost(cfg.Server.AdvertiseHost)
return packNodeBootstrap(nodeBootstrapPackOptions{
NodeBinary: nodeBinary,
OutputDir: outputDir,
Target: target,
Version: version.Version,
ArtifactBaseURL: artifactURL,
EdgeAddr: addressFor(adv.Host, cfg.Server.Listen),
})
targetOpts := baseOpts
targetOpts.NodeBinary = nodeBinary
targetOpts.Target = target
return packNodeBootstrap(targetOpts)
}
func validateEdgeConfig(cfg *config.EdgeConfig) error {
@ -861,7 +921,7 @@ edge.yaml. The printed bootstrap command is the user-facing command to run on
the Node host; the user should not create node.yaml or run iop-node serve
directly in the default flow.`,
Example: ` iop-edge node register node-silicon-ollama --adapter ollama --ollama-base-url http://127.0.0.1:11434
iop-edge node register node-linux-cli --adapter cli --target linux-amd64
iop-edge node register node-linux-cli --adapter cli
iop-edge config check`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
@ -1038,7 +1098,7 @@ directly in the default flow.`,
}
// Render final user-facing bootstrap command
bootstrapURL := bootstrapScriptURL(artifactURL, target)
bootstrapURL := universalBootstrapScriptURL(artifactURL)
fmt.Fprintf(cmd.OutOrStdout(), "curl -fsSL %s | bash -s %s\n", bootstrapURL, nodeDef.Token)
return nil

View file

@ -6,7 +6,6 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
@ -362,7 +361,7 @@ func TestEnvCmdMarksAutoAdvertiseHostAndOfflineStateForEmptyConfig(t *testing.T)
if !strings.Contains(got, expectedArtifact) {
t.Errorf("expected derived artifact_base_url %q, got:\n%s", expectedArtifact, got)
}
expectedBootstrap := "node_bootstrap: " + bootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host), runtime.GOOS+"-"+runtime.GOARCH)
expectedBootstrap := "node_bootstrap: " + universalBootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host))
if !strings.Contains(got, expectedBootstrap) {
t.Errorf("expected node_bootstrap %q, got:\n%s", expectedBootstrap, got)
}
@ -436,6 +435,7 @@ bootstrap:
for _, path := range []string{
filepath.Join(outDir, "linux-arm64", "iop-node"),
filepath.Join(outDir, "linux-arm64", "SHA256SUMS"),
filepath.Join(outDir, "bootstrap", "node.sh"),
filepath.Join(outDir, "bootstrap", "node-linux-arm64.sh"),
} {
if _, err := os.Stat(path); err != nil {
@ -452,6 +452,16 @@ bootstrap:
if !strings.Contains(string(script), `DEFAULT_EDGE_ADDR="edge.example.test:39090"`) {
t.Fatalf("script missing derived edge addr:\n%s", script)
}
universalScript, err := os.ReadFile(filepath.Join(outDir, "bootstrap", "node.sh"))
if err != nil {
t.Fatalf("read universal script: %v", err)
}
if !strings.Contains(string(universalScript), `DEFAULT_TARGET=""`) {
t.Fatalf("universal script should detect target dynamically:\n%s", universalScript)
}
if !strings.Contains(string(universalScript), `detect_target()`) {
t.Fatalf("universal script missing target detection:\n%s", universalScript)
}
}
func TestNodeRegisterRefreshesExistingBootstrapArtifact(t *testing.T) {
@ -497,13 +507,23 @@ nodes: []
if err != nil {
t.Fatalf("read refreshed script: %v", err)
}
universalScript, err := os.ReadFile(filepath.Join(artifactDir, "bootstrap", "node.sh"))
if err != nil {
t.Fatalf("read refreshed universal script: %v", err)
}
if !strings.Contains(string(script), `DEFAULT_ARTIFACT_BASE_URL="http://edge.deploy.test:38080"`) {
t.Fatalf("script missing refreshed artifact URL:\n%s", script)
}
if !strings.Contains(string(universalScript), `DEFAULT_ARTIFACT_BASE_URL="http://edge.deploy.test:38080"`) {
t.Fatalf("universal script missing refreshed artifact URL:\n%s", universalScript)
}
if !strings.Contains(string(script), `DEFAULT_EDGE_ADDR="edge.deploy.test:39090"`) {
t.Fatalf("script missing refreshed edge addr:\n%s", script)
}
if !strings.Contains(out.String(), `curl -fsSL http://edge.deploy.test:38080/bootstrap/node-darwin-arm64.sh | bash -s test-token`) {
if !strings.Contains(string(universalScript), `DEFAULT_EDGE_ADDR="edge.deploy.test:39090"`) {
t.Fatalf("universal script missing refreshed edge addr:\n%s", universalScript)
}
if !strings.Contains(out.String(), `curl -fsSL http://edge.deploy.test:38080/bootstrap/node.sh | bash -s test-token`) {
t.Fatalf("unexpected register output:\n%s", out.String())
}
}
@ -645,7 +665,7 @@ func TestNodeRegisterUpsertsYAMLAndOutputsBootstrap(t *testing.T) {
output := out.String()
// Check one-line bootstrap output
expectedCmd := "curl -fsSL " + bootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host), runtime.GOOS+"-"+runtime.GOARCH) + " | bash -s mocked-token-1234"
expectedCmd := "curl -fsSL " + universalBootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host)) + " | bash -s mocked-token-1234"
if strings.TrimSpace(output) != expectedCmd {
t.Errorf("expected only bootstrap command %q, got %q", expectedCmd, output)
}
@ -679,7 +699,7 @@ func TestNodeRegisterUpsertsYAMLAndOutputsBootstrap(t *testing.T) {
}
output = out.String()
expectedCmd2 := "curl -fsSL " + bootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host), runtime.GOOS+"-"+runtime.GOARCH) + " | bash -s explicit-token-abc"
expectedCmd2 := "curl -fsSL " + universalBootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host)) + " | bash -s explicit-token-abc"
if strings.TrimSpace(output) != expectedCmd2 {
t.Errorf("expected only bootstrap command %q, got %q", expectedCmd2, output)
}
@ -850,7 +870,7 @@ bootstrap:
}
output := out.String()
expectedCmd := "curl -fsSL http://configured-artifact-server:8080/bootstrap/node-darwin-arm64.sh | bash -s test-token"
expectedCmd := "curl -fsSL http://configured-artifact-server:8080/bootstrap/node.sh | bash -s test-token"
if !strings.Contains(output, expectedCmd) {
t.Errorf("expected bootstrap command %q, got %q", expectedCmd, output)
}
@ -866,7 +886,7 @@ bootstrap:
}
output = out.String()
expectedCmdOverride := "curl -fsSL http://flag-artifact-server:9000/bootstrap/node-linux-amd64.sh | bash -s test-token-2"
expectedCmdOverride := "curl -fsSL http://flag-artifact-server:9000/bootstrap/node.sh | bash -s test-token-2"
if !strings.Contains(output, expectedCmdOverride) {
t.Errorf("expected bootstrap command %q, got %q", expectedCmdOverride, output)
}
@ -895,7 +915,7 @@ bootstrap:
t.Fatalf("execute register: %v\n%s", err, out.String())
}
expectedCmd := "curl -fsSL http://edge.example.test:18080/bootstrap/node-linux-amd64.sh | bash -s test-token"
expectedCmd := "curl -fsSL http://edge.example.test:18080/bootstrap/node.sh | bash -s test-token"
if !strings.Contains(out.String(), expectedCmd) {
t.Errorf("expected bootstrap command %q, got %q", expectedCmd, out.String())
}
@ -927,7 +947,7 @@ func TestTempFileCLIWorkflowConfigInitNodeRegisterConfigCheck(t *testing.T) {
}
output := out.String()
expectedCmd := "curl -fsSL http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh | bash -s test-token"
expectedCmd := "curl -fsSL http://toki-labs.com:18080/bootstrap/node.sh | bash -s test-token"
if !strings.Contains(output, expectedCmd) {
t.Errorf("expected bootstrap command %q, got %q", expectedCmd, output)
}

View file

@ -30,7 +30,7 @@ Edge가 제시한 bootstrap 명령 한 줄을 실행하면 Node binary 다운로
```bash
# 빌드
make build-node
make build-local
# 버전 확인
./build/bin/iop-node version

View file

@ -1,264 +1,79 @@
# Edge-local Dev Guide
# Edge Local Quickstart
이 문서는 Control Plane 없이 로컬 또는 개발 서버에서 Edge/Node bundle을 빠르게 세팅하고 확인하는 사용자용 흐름이다.
사용자는 `iop-edge`를 중심으로 진행한다.
Edge는 package 하나, Node는 bootstrap 한 줄로 설치한다.
하지 않는다:
- `node.yaml` 생성, 편집, 확인
- `edge.yaml``nodes[]` 수동 작성
- `iop-node serve --config ...` 직접 실행
- `scripts/dev/*.sh` helper나 별도 dev deploy 바이너리를 공식 경로로 사용
## 1. 빌드와 Edge 배포 패키지
공식 빌드 진입점은 `make build` 하나로 둔다.
이 명령은 Edge와 Node를 함께 빌드하고, 현재 host에서 바로 쓸 수 있는 바이너리만 `build/bin/`에 맞춰 둔다.
## 1. Package
```bash
make build
```
Edge 또는 Node만 갱신할 때도 같은 빌드 통로를 사용한다.
결과: `build/packages/iop-edge-<edge-os>-<edge-arch>.tar.gz`
## 2. Edge
```bash
make build-edge
make build-node
```
tar -xzf iop-edge-<edge-os>-<edge-arch>.tar.gz -C /opt
cd /opt/iop-edge-<edge-os>-<edge-arch>
산출물:
```text
build/bin/iop-edge
build/bin/iop-node
```
Edge host에 배포할 때는 Node target artifact를 먼저 준비한 뒤 Edge archive를 만든다.
```bash
make pack-node-target NODE_TARGET=darwin-arm64
make pack-edge
```
Intel Mac Node면 `NODE_TARGET=darwin-amd64`를 사용한다.
Edge만 새로 빌드한 경우에는 기존 `build/artifacts/`를 유지한 상태에서 `make pack-edge`만 다시 실행한다.
Node binary를 새로 빌드한 경우에는 `make pack-node-target NODE_TARGET=<goos>-<goarch>``make pack-edge`를 다시 실행한다.
배포 산출물:
```text
build/packages/iop-edge-<edge-os>-<edge-arch>.tar.gz
```
## 2. Edge 패키지 압축 해제
Edge host에서는 archive 하나만 옮긴 뒤 압축을 푼다.
```bash
tar -xzf build/packages/iop-edge-$(go env GOOS)-$(go env GOARCH).tar.gz -C /opt
cd /opt/iop-edge-$(go env GOOS)-$(go env GOARCH)
```
압축 해제 후 기본 구조:
```text
iop-edge
artifacts/
logs/
```
repo-local에서 archive 없이 확인할 때는 새 bundle 디렉터리에 Edge binary와 artifacts를 함께 둔다.
```bash
mkdir -p /tmp/iop-edge-dev
cp build/bin/iop-edge /tmp/iop-edge-dev/
cp -R build/artifacts /tmp/iop-edge-dev/
cd /tmp/iop-edge-dev
```
## 3. Edge config 생성
```bash
./iop-edge config init
```
기대 결과:
```text
<bundle-dir>/edge.yaml
```
`edge.yaml`은 Control Plane이 없는 동안 테스트용 source of truth다.
처음부터 직접 작성하지 않고 `iop-edge`가 생성한 템플릿에서 시작한다.
## 4. 현재 환경 확인
```bash
./iop-edge env
```
출력에서 최소한 아래 항목을 확인한다.
필요하면 `edge.yaml`의 host/port/model만 수정한다.
```text
config path
binary dir
log dir
artifact dir
advertise host
Node transport address
artifact/bootstrap URL
OpenAI-compatible URL
configured nodes
connected nodes
```
`advertise host`가 비어 있으면 `iop-edge`가 host 기준으로 자동 판별한다.
명시 설정이 있으면 자동 판별보다 설정값을 따른다.
로그 기본 위치는 바이너리 디렉터리 아래다.
```text
<bundle-dir>/logs/
```
## 5. Bootstrap artifact 확인
Edge archive에는 `make pack-node-target`으로 준비한 Node artifact가 들어 있어야 한다.
```text
artifacts/
bootstrap/node-<os>-<arch>.sh
<os>-<arch>/iop-node
<os>-<arch>/SHA256SUMS
```
`node register`는 해당 target의 `artifacts/<target>/iop-node`가 있으면 현재 `edge.yaml` 기준으로 bootstrap script와 checksum을 다시 생성한다.
즉 build machine에서 만든 archive를 다른 Edge host에 풀어도, Edge host에서 config를 확정한 뒤 등록하면 Node가 받을 URL과 Edge 주소가 현재 host 기준으로 맞춰진다.
새 Node binary를 Edge host에서 직접 교체한 경우에만 `./iop-edge bootstrap pack --target <goos>-<goarch> --node-binary <path>`로 다시 pack한다.
## 6. Node 등록과 bootstrap 출력
Node 정의는 `edge.yaml`을 직접 편집하지 않고 `iop-edge node register`로 만든다.
기본 등록:
## 3. Node 등록
```bash
./iop-edge node register node-silicon-ollama
./iop-edge node register node-1
```
Ollama Node로 등록할 때:
Ollama Node:
```bash
./iop-edge node register node-silicon-ollama \
--target darwin-arm64 \
./iop-edge node register node-1 \
--adapter ollama \
--ollama-base-url http://127.0.0.1:11434
```
이 명령은 `edge.yaml``nodes[]`를 생성 또는 갱신하고, 마지막에 Node host에서 실행할 bootstrap 명령을 출력한다.
`--target` 값은 `artifacts/<target>/`에 들어 있는 Node artifact와 같아야 한다.
출력된 `curl ... | bash -s <token>` 한 줄을 복사한다.
출력 예:
```bash
curl -fsSL <bootstrap-url> | bash -s <generated-token>
```
사용자는 이 한 줄만 Node host에서 실행한다.
전달받은 명령에 `IOP_ARTIFACT_BASE_URL=`, `IOP_EDGE_ADDR=`, `IOP_NODE_TOKEN=` 같은 named parameter가 있으면 기본 경로가 아니다.
## 7. Config 검증
## 4. Edge 실행
```bash
./iop-edge config check
```
이 단계에서 `nodes[]` token/id/alias 충돌, adapter/runtime 설정 오류가 잡혀야 한다.
## 8. Edge 실행
개발 중 foreground 실행:
```bash
./iop-edge serve
```
`serve`는 Edge transport와 함께 bootstrap artifact 서버도 시작한다.
기본 artifact 경로는 `<bundle-dir>/artifacts/`이고 기본 bootstrap URL 포트는 `18080`이다.
서비스로 올릴 때:
서비스 실행:
```bash
sudo ./iop-edge setup --enable --start
```
로그 확인:
## 5. Node 설치
Node host에서 복사한 명령을 실행한다.
```bash
curl -fsSL http://<edge-host>:18080/bootstrap/node.sh | bash -s <token>
```
`node.sh`가 OS/architecture를 자동 감지한다.
## 6. 확인
```bash
tail -f logs/edge.log
```
## 9. Node host에서 bootstrap 실행
`iop-edge node register ...`가 출력한 한 줄을 Node host에서 그대로 실행한다.
```bash
curl -fsSL <bootstrap-url> | bash -s <generated-token>
```
Node host 사용자는 `node.yaml`을 만들거나 편집하지 않는다.
Node 프로세스 실행 방식은 bootstrap 내부 구현 세부다.
## 10. 연결 확인
Edge host 로그에서 Node 등록 이벤트를 확인한다.
```bash
tail -f logs/edge.log
```
기대 로그:
```text
node registered
```
설정에 등록된 Node 목록은 아래 명령으로 확인한다.
```bash
./iop-edge nodes list
```
OpenAI-compatible smoke는 OpenAI 입력 표면과 Ollama model 설정이 완료된 뒤 실행한다.
정상: `node registered`
## Build Options
```bash
./iop-edge smoke openai --model gemma4:26b
make build NODE_TARGETS="darwin-arm64"
make build-edge && make pack-edge
make pack-node-target NODE_TARGET=darwin-arm64 && make pack-edge
```
## 11. Cline 접속 정보 확인
```bash
./iop-edge env
```
출력의 OpenAI-compatible URL을 Cline에 넣는다.
```text
Base URL: <OpenAI-compatible URL>
Model: gemma4:26b
API Key: ASCII 값
```
API Key에는 한글이나 non-ASCII 문자를 넣지 않는다.
## 12. 문제 발생 시 공유할 것
```bash
./iop-edge env
./iop-edge nodes list
./iop-edge config check
tail -n 120 logs/edge.log
```
공유할 때 token 값은 지운다.