依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類 共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git), 讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等 per-branch 筆記。 - 02-prd/ 21 個檔(PRD、features、market-analysis 等) - 03-design/ 18 個檔(design-spec、wireframes、flows 等) - 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等) - 07-delivery/ 3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup) 合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv, 但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
344 lines
11 KiB
Markdown
344 lines
11 KiB
Markdown
# Build & Deploy
|
||
|
||
> 建置、本機開發、Docker 打包、部署的實務細節。
|
||
|
||
---
|
||
|
||
## 1. Makefile(visionA-backend)
|
||
|
||
```makefile
|
||
.PHONY: build build-api build-proxy build-dev test clean docker-build docker-up docker-down run-dev
|
||
|
||
GO ?= go
|
||
GO_FLAGS ?= -ldflags="-s -w"
|
||
OUT_DIR ?= dist
|
||
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||
|
||
build: build-api build-proxy
|
||
|
||
build-api:
|
||
$(GO) build $(GO_FLAGS) -o $(OUT_DIR)/api-server ./cmd/api-server
|
||
|
||
build-proxy:
|
||
$(GO) build $(GO_FLAGS) -o $(OUT_DIR)/remote-proxy ./cmd/remote-proxy
|
||
|
||
test:
|
||
$(GO) test -race -coverprofile=coverage.out ./...
|
||
|
||
lint:
|
||
$(GO) vet ./...
|
||
gofmt -l . | grep -v '^$$' && exit 1 || true
|
||
|
||
clean:
|
||
rm -rf $(OUT_DIR) coverage.out
|
||
|
||
# --- Docker ---
|
||
docker-build:
|
||
docker build -f docker/Dockerfile.api-server -t visiona/api-server:$(VERSION) .
|
||
docker build -f docker/Dockerfile.remote-proxy -t visiona/remote-proxy:$(VERSION) .
|
||
|
||
docker-up:
|
||
docker compose -f docker/docker-compose.yml up --build
|
||
|
||
docker-down:
|
||
docker compose -f docker/docker-compose.yml down
|
||
|
||
# --- Dev ---
|
||
# 本機同時啟動兩個 binary(非雛形交付物,僅開發便利)。
|
||
# 交付物定義見 design-doc.md §2.4 Non-Goal。
|
||
run-dev:
|
||
@trap 'kill 0' EXIT; \
|
||
$(GO) run ./cmd/remote-proxy & \
|
||
$(GO) run ./cmd/api-server & \
|
||
wait
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Dockerfile.api-server
|
||
|
||
```dockerfile
|
||
# --- build stage ---
|
||
FROM golang:1.26-alpine AS build
|
||
WORKDIR /src
|
||
COPY go.mod go.sum ./
|
||
RUN go mod download
|
||
COPY . .
|
||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/api-server ./cmd/api-server
|
||
|
||
# --- runtime stage ---
|
||
FROM gcr.io/distroless/static:nonroot
|
||
WORKDIR /app
|
||
COPY --from=build /out/api-server /app/api-server
|
||
USER nonroot:nonroot
|
||
EXPOSE 3001
|
||
ENTRYPOINT ["/app/api-server"]
|
||
```
|
||
|
||
## 3. Dockerfile.remote-proxy
|
||
|
||
```dockerfile
|
||
FROM golang:1.26-alpine AS build
|
||
WORKDIR /src
|
||
COPY go.mod go.sum ./
|
||
RUN go mod download
|
||
COPY . .
|
||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/remote-proxy ./cmd/remote-proxy
|
||
|
||
FROM gcr.io/distroless/static:nonroot
|
||
WORKDIR /app
|
||
COPY --from=build /out/remote-proxy /app/remote-proxy
|
||
USER nonroot:nonroot
|
||
EXPOSE 3800 3801
|
||
ENTRYPOINT ["/app/remote-proxy"]
|
||
```
|
||
|
||
---
|
||
|
||
## 4. docker/docker-compose.yml
|
||
|
||
```yaml
|
||
services:
|
||
api-server:
|
||
build:
|
||
context: ../
|
||
dockerfile: docker/Dockerfile.api-server
|
||
image: visiona/api-server:dev
|
||
ports: ["3001:3001"]
|
||
environment:
|
||
VISIONA_API_PORT: "3001"
|
||
VISIONA_SESSION_BACKEND: inmemory
|
||
VISIONA_STORAGE_BACKEND: localfs
|
||
VISIONA_STORAGE_LOCALFS_ROOT: /data/storage
|
||
VISIONA_STORAGE_LOCALFS_BASE_URL: http://localhost:3001/storage
|
||
VISIONA_STORAGE_SIGNING_SECRET: dev-secret-do-not-use-in-prod
|
||
VISIONA_AUTH_MODE: static
|
||
VISIONA_STATIC_USER_ID: demo-user
|
||
VISIONA_PAIRING_MODE: static
|
||
VISIONA_PAIRING_TOKEN: "${VISIONA_PAIRING_TOKEN}"
|
||
VISIONA_CONVERTER_MODE: stub
|
||
volumes:
|
||
- storage-data:/data
|
||
|
||
remote-proxy:
|
||
build:
|
||
context: ../
|
||
dockerfile: docker/Dockerfile.remote-proxy
|
||
image: visiona/remote-proxy:dev
|
||
ports:
|
||
- "3800:3800" # tunnel
|
||
- "3801:3801" # internal
|
||
environment:
|
||
VISIONA_TUNNEL_PORT: "3800"
|
||
VISIONA_PROXY_INTERNAL_PORT: "3801"
|
||
VISIONA_SESSION_BACKEND: inmemory
|
||
VISIONA_PAIRING_MODE: static
|
||
VISIONA_PAIRING_TOKEN: "${VISIONA_PAIRING_TOKEN}"
|
||
|
||
# 雛形設計(ADR-006 / Q1):
|
||
# - remote-proxy 是唯一持有 yamux.Session 的 process(in-memory)
|
||
# - api-server 無狀態,透過 internal HTTP 向 remote-proxy 查詢 session
|
||
# - 兩 binary 之間用 VISIONA_PROXY_INTERNAL_URL 連結(下方 api-server 已設 env)
|
||
|
||
volumes:
|
||
storage-data:
|
||
```
|
||
|
||
另外 api-server service 需要新增 env(指向 remote-proxy 的 internal URL):
|
||
|
||
```yaml
|
||
api-server:
|
||
environment:
|
||
# 上方所有 env 保留,新增:
|
||
VISIONA_SESSION_BACKEND: proxy-client
|
||
VISIONA_PROXY_INTERNAL_URL: http://remote-proxy:3801
|
||
```
|
||
|
||
### 4.1 雛形推薦的開發方式
|
||
|
||
```bash
|
||
# .env
|
||
VISIONA_PAIRING_TOKEN="vAc_$(openssl rand -hex 16)" # 格式見 security.md §1.3
|
||
|
||
# 方式 1:本機 Makefile 平行跑兩個 binary(開發便利工具)
|
||
make run-dev
|
||
|
||
# 方式 2:Docker Compose(更接近 Production 拓撲)
|
||
make docker-up
|
||
```
|
||
|
||
**結論**:雛形交付物是雙 binary + docker-compose(兩者皆可用於 demo);`make run-dev` 僅為本機開發便利工具(**非交付物**,見 design-doc.md §2.4 Non-Goal)。
|
||
|
||
---
|
||
|
||
## 5. 前端建置
|
||
|
||
```bash
|
||
cd visionA-frontend
|
||
pnpm install
|
||
pnpm dev # 本機開發 http://localhost:3000
|
||
pnpm build # 產出 .next/
|
||
pnpm start # 生產模式跑 Next.js server
|
||
```
|
||
|
||
### 5.1 Next.js Build 模式
|
||
|
||
| 模式 | 用途 | 如何設定 |
|
||
|------|------|---------|
|
||
| `next build` + `next start` | SSR / ISR 支援 | 適合 Phase 1,需要 Next.js runtime |
|
||
| `output: 'export'` | 靜態 export,放 CDN | 若所有頁面都可靜態化,最便宜 |
|
||
|
||
**雛形建議**:`next start` on Node,方便 API rewrites / middleware;Phase 1 視需求切換。
|
||
|
||
### 5.2 環境變數
|
||
|
||
```
|
||
# visionA-frontend/.env.example
|
||
NEXT_PUBLIC_API_BASE=http://localhost:3001
|
||
NEXT_PUBLIC_WS_BASE=ws://localhost:3001
|
||
# 雛形開發用:
|
||
NEXT_PUBLIC_DEV_PAIRING_TOKEN=<same as VISIONA_PAIRING_TOKEN>
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 本機完整開發流程
|
||
|
||
```bash
|
||
# Terminal 1 — Backend
|
||
cd visionA-backend
|
||
export VISIONA_PAIRING_TOKEN=$(openssl rand -hex 32)
|
||
export VISIONA_STATIC_USER_ID=demo-user
|
||
make run-dev
|
||
# api at :3001, tunnel at :3800
|
||
|
||
# Terminal 2 — local agent (local-tool 現有 + 開雲端模式,或 POC 的 edge-ai-server)
|
||
cd /Users/jimchen/Innovedus/edge-ai-platform/edge-ai-platform
|
||
./dist/edge-ai-server --relay-url=ws://localhost:3800/tunnel/connect --relay-token=$VISIONA_PAIRING_TOKEN
|
||
|
||
# Terminal 3 — Frontend
|
||
cd visionA-frontend
|
||
cp .env.example .env.local
|
||
# 編輯 .env.local 填入 PAIRING_TOKEN
|
||
pnpm dev
|
||
# http://localhost:3000
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Phase 1 部署草圖
|
||
|
||
### 7.1 AWS ECS Fargate + Application Load Balancer
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ Route 53 — api.visiona.cloud / proxy.visiona.cloud │
|
||
└─────────────────────┬────────────────────┬───────────────────┘
|
||
│ │
|
||
▼ ▼
|
||
┌─────────────────┐ ┌───────────────────┐
|
||
│ ALB │ │ NLB │
|
||
│ (HTTPS/WSS) │ │ (TCP passthrough)│
|
||
│ api.* │ │ proxy.* │
|
||
└────────┬────────┘ └─────────┬─────────┘
|
||
│ │
|
||
▼ ▼
|
||
┌─────────────────────┐ ┌─────────────────────┐
|
||
│ ECS Service: │ │ ECS Service: │
|
||
│ api-server │ │ remote-proxy │
|
||
│ (Fargate, 2+ tasks)│ │ (Fargate, 2+ tasks) │
|
||
└──────┬──────────────┘ └──────────┬──────────┘
|
||
│ │
|
||
└──────┬───────────────┬──────┘
|
||
│ │
|
||
▼ ▼
|
||
┌──────────────┐ ┌─────────────────┐
|
||
│ ElastiCache │ │ RDS │
|
||
│ Redis │ │ PostgreSQL │
|
||
└──────────────┘ └─────────────────┘
|
||
▲ ▲
|
||
│ │
|
||
└────────────────┘
|
||
│
|
||
┌──────────────┐
|
||
│ S3 bucket │
|
||
└──────────────┘
|
||
```
|
||
|
||
### 7.2 Kubernetes 方案
|
||
|
||
- api-server:`Deployment` + `HorizontalPodAutoscaler`
|
||
- remote-proxy:`Deployment` + HPA(按 tunnel 數 metric)
|
||
- Redis / Postgres:managed service 或 StatefulSet
|
||
- Ingress:nginx-ingress 或 cloud LB controller
|
||
|
||
**Cloud-agnostic 原則**:Helm chart 不綁特定雲,storage / DB 依 env 注入連線資訊。
|
||
|
||
---
|
||
|
||
## 8. CI/CD(Phase 1 規劃)
|
||
|
||
### 8.1 GitHub Actions
|
||
|
||
```yaml
|
||
# .github/workflows/ci.yml
|
||
name: CI
|
||
on: [push, pull_request]
|
||
jobs:
|
||
backend-test:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: actions/setup-go@v5
|
||
with: { go-version: '1.26' }
|
||
- run: cd visionA-backend && make test
|
||
- run: cd visionA-backend && make lint
|
||
|
||
frontend-test:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: pnpm/action-setup@v3
|
||
- uses: actions/setup-node@v4
|
||
with: { node-version: '20', cache: 'pnpm' }
|
||
- run: cd visionA-frontend && pnpm install --frozen-lockfile
|
||
- run: cd visionA-frontend && pnpm test
|
||
- run: cd visionA-frontend && pnpm build
|
||
```
|
||
|
||
### 8.2 Release Pipeline(Phase 1)
|
||
|
||
1. PR merged to `main`
|
||
2. CI 跑 test + lint + build
|
||
3. 產出 Docker image → push to registry(ECR / GCR / GitHub Packages)
|
||
4. Tag image 為 `main-<sha>`
|
||
5. 手動觸發 deploy(或 GitOps 自動)→ ECS task definition 更新 / K8s rollout
|
||
|
||
---
|
||
|
||
## 9. 環境變數對照表(摘要)
|
||
|
||
| Env | 雛形 | Phase 1 |
|
||
|-----|------|--------|
|
||
| `VISIONA_AUTH_MODE` | `static` | `clerk` / `oidc` |
|
||
| `VISIONA_PAIRING_MODE` | `static` | `db` |
|
||
| `VISIONA_SESSION_BACKEND` | `inmemory` | `redis` |
|
||
| `VISIONA_STORAGE_BACKEND` | `localfs` | `s3` |
|
||
| `VISIONA_CONVERTER_MODE` | `stub` | `http` |
|
||
| `VISIONA_REDIS_URL` | — | `redis://...` |
|
||
| `VISIONA_DB_URL` | — | `postgres://...` |
|
||
| `VISIONA_S3_*` | — | 實際 credentials |
|
||
|
||
---
|
||
|
||
**雛形實作重點**:
|
||
- `make run-dev`(單 binary 兩 listener)
|
||
- 不需要 Docker;Docker 檔案寫好以備 Phase 1
|
||
- 不需要 CI;但 Makefile 有 `test` + `lint` 方便本機檢查
|
||
|
||
**Phase 1 必做**:
|
||
- Docker image CI 產出
|
||
- K8s / ECS manifest
|
||
- Blue-green 或 rolling deploy
|
||
- Staging 環境
|