From 95693f1cec1fd9bdbd065614815b480d69a75ed8 Mon Sep 17 00:00:00 2001 From: toki Date: Thu, 28 May 2026 20:10:54 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20edge=20local=20dev=20guide=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EB=AC=B8=EC=84=9C=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 23 ++- apps/edge/README.md | 6 +- apps/edge/cmd/edge/main.go | 92 ++++++++++-- apps/edge/cmd/edge/main_test.go | 38 +++-- apps/node/README.md | 2 +- docs/edge-local-dev-guide.md | 251 +++++--------------------------- 6 files changed, 160 insertions(+), 252 deletions(-) diff --git a/Makefile b/Makefile index 08f8c1a..c7ce2d0 100644 --- a/Makefile +++ b/Makefile @@ -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=- 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)" diff --git a/apps/edge/README.md b/apps/edge/README.md index 8ac8869..20f234c 100644 --- a/apps/edge/README.md +++ b/apps/edge/README.md @@ -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=-` 후 `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는 `/bootstrap/node.sh` 하나를 실행하고, bootstrap script가 OS/architecture를 감지해 맞는 Node artifact를 받는다. +특정 Node target만 담긴 개발용 package는 `make build NODE_TARGETS="-"`로 만들 수 있다. Edge만 갱신할 때는 `make build-edge && make pack-edge`, Node artifact만 갱신할 때는 `make pack-node-target NODE_TARGET=- && 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`를 지정해야 덮어쓴다. diff --git a/apps/edge/cmd/edge/main.go b/apps/edge/cmd/edge/main.go index 6c856e2..3ea3f9a 100644 --- a/apps/edge/cmd/edge/main.go +++ b/apps/edge/cmd/edge/main.go @@ -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 /bootstrap/node-$TARGET.sh | bash -s TOKEN" >&2 + echo "[iop-bootstrap] usage: curl -fsSL /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 diff --git a/apps/edge/cmd/edge/main_test.go b/apps/edge/cmd/edge/main_test.go index 772b93f..b7896c0 100644 --- a/apps/edge/cmd/edge/main_test.go +++ b/apps/edge/cmd/edge/main_test.go @@ -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) } diff --git a/apps/node/README.md b/apps/node/README.md index c40e91e..88f4c57 100644 --- a/apps/node/README.md +++ b/apps/node/README.md @@ -30,7 +30,7 @@ Edge가 제시한 bootstrap 명령 한 줄을 실행하면 Node binary 다운로 ```bash # 빌드 -make build-node +make build-local # 버전 확인 ./build/bin/iop-node version diff --git a/docs/edge-local-dev-guide.md b/docs/edge-local-dev-guide.md index 79909e3..1855ec4 100644 --- a/docs/edge-local-dev-guide.md +++ b/docs/edge-local-dev-guide.md @@ -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--.tar.gz` + +## 2. Edge ```bash -make build-edge -make build-node -``` +tar -xzf iop-edge--.tar.gz -C /opt +cd /opt/iop-edge-- -산출물: - -```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=-` 후 `make pack-edge`를 다시 실행한다. - -배포 산출물: - -```text -build/packages/iop-edge--.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 -/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 -/logs/ -``` - -## 5. Bootstrap artifact 확인 - -Edge archive에는 `make pack-node-target`으로 준비한 Node artifact가 들어 있어야 한다. - -```text -artifacts/ - bootstrap/node--.sh - -/iop-node - -/SHA256SUMS -``` - -`node register`는 해당 target의 `artifacts//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 - --node-binary `로 다시 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//`에 들어 있는 Node artifact와 같아야 한다. +출력된 `curl ... | bash -s ` 한 줄을 복사한다. -출력 예: - -```bash -curl -fsSL | bash -s -``` - -사용자는 이 한 줄만 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 경로는 `/artifacts/`이고 기본 bootstrap URL 포트는 `18080`이다. - -서비스로 올릴 때: +서비스 실행: ```bash sudo ./iop-edge setup --enable --start ``` -로그 확인: +## 5. Node 설치 + +Node host에서 복사한 명령을 실행한다. + +```bash +curl -fsSL http://:18080/bootstrap/node.sh | bash -s +``` + +`node.sh`가 OS/architecture를 자동 감지한다. + +## 6. 확인 ```bash tail -f logs/edge.log -``` - -## 9. Node host에서 bootstrap 실행 - -`iop-edge node register ...`가 출력한 한 줄을 Node host에서 그대로 실행한다. - -```bash -curl -fsSL | bash -s -``` - -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: -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 값은 지운다.