<!-- canonical: https://docs.axelabs.ai/partner/macmini-existing -->
<!-- source: content/partner/macmini-existing.mdx -->

---
title: 기존 운영 macmini → customer 전환
description: 이미 운영 중인 operator-grade macmini (axe stack live) 를 customer-served host 로 전환 — customers.yaml entry + vault collection + tunnel route + container 식별만 바뀐다.
---

# 기존 운영 macmini → customer 전환

표준 onboarding ([/partner](/partner) → [/partner/macmini-prep](/partner/macmini-prep)) 은 **신품 macmini** 를 전제로 한다 (D-7 절전 OFF + Tailscale 합류 + Docker 설치부터). 본 페이지는 다른 출발점 — **운영자가 이미 띄워 운영 중인 macmini** (예: `axe` stack 이 live 인 operator-grade 박스, 또는 사전 셋업을 끝낸 staging 박스) 에 **새 customer 신원을 얹어 customer-served host 로 전환**하는 경우를 다룬다.

> 🎯 핵심: 하드웨어·OS·Docker·Tailscale 은 **이미 준비됐다**. 따라서 표준 흐름의 인프라 prep (macmini-prep 6단계) 은 건너뛰고, **논리 식별 4가지만 바뀐다** — ① `customers.yaml` entry, ② vault organization/collection, ③ cloudflared tunnel route, ④ container 식별 (실제 rename 은 아직 없음 — 아래 참조). 그 위에 customer-scoped service stack 을 `axe deploy` 로 올린다.

## 언제 이 경로인가

| 상황 | 이 페이지 적용 |
|---|---|
| 운영자가 자기 `axe` stack 돌리던 박스를 customer 에게 인계 (이관) | ✅ |
| 사전에 운영자가 prep 끝낸 staging macmini 를 customer 용으로 승격 | ✅ |
| 운영자 박스에 customer 의 추가 service (stream/magnet 류) 만 co-tenant | ✅ (부분 — § 부분 전환) |
| 완전 신품 macmini, customer IT 가 직접 prep | ❌ → [표준 onboarding](/partner) |
| customer 가 자기 Vaultwarden 을 이미 운영 중 (vault 유지) | ❌ → [/partner/deploy § Vault OIDC SSO 통합](/partner/deploy#vault-oidc-sso-통합--기존-vaultwarden-운영-중인-customer-용) |

> ⚠️ AXE 의 격리 모델은 **customer-per-macmini** 다 ([/architecture/secrets § 컨테이너 이름 컨벤션](/architecture/secrets)). 같은 host 에 서로 다른 customer 둘을 영구 co-tenant 하는 것은 표준이 아니다 — 컨테이너 이름이 아직 customer-prefix 가 아니라 (`frame-postgres` 등 고정명) 두 customer 의 frame stack 이 같은 박스에서 포트·이름 충돌한다. 따라서 "전환" 의 정상 종착지는 **그 박스가 한 customer 의 host 가 되는 것** (operator `axe` 신원에서 customer 신원으로 소유 이전), 단일 host 에 영구 다중 customer 가 아니다.

## 무엇이 바뀌는가 (4가지)

### ① customers.yaml entry

운영자 macmini 는 `axe` customer 로 동작한다 — axe CLI 의 `is_local = (customer_id == "axe")` 분기가 이 박스를 **local exec** (SSH 없이 직접) 로 다룬다. customer 로 전환하면 그 박스는 더 이상 `axe` local-exec 호스트가 아니라 **별도 customer entry** 가 되고, axe CLI 가 Tailscale SSH 로 접근한다.

1. customers.yaml 에 customer 블록 추가 (stub 은 `axe customers add {cid}`):

```yaml
{customer}:
  legal_name: "<법인명>"
  primary_domain: "<회사 도메인>"
  public_domain: "{customer}.axelabs.ai"
  entities: ["<entity_id>"]
  tailscale_host: "{customer}-macmini"   # 이미 가입돼 있으면 현 hostname
  ssh_user: "<현 macOS 사용자명>"          # 기존 박스의 실제 user (axe 가 아니면 명시)
  sso:
    provider: "microsoft_entra_id"
    tenant_id: "<...>"
    apps: { blueprint: {...}, vaultwarden: {...}, frame_mcp: {...} }
  services:
    frame:      { env_file: "/Users/<user>/frame/.env.local", secrets: [...] }
    blueprint:  { env_file: "/Users/<user>/blueprint/.env",   secrets: [...] }
    hive:       { env_file: "/Users/<user>/hive/.env.local",  secrets: [...] }
    vaultwarden:{ env_file: "/Users/<user>/.axe/vault/.env", bootstrap_only: true }
  onboarded: "<date>"
```

전체 entry 스키마 (sso.apps + 각 service secrets[]) 는 [/ops/runbook/customer-onboarding § D-7](/ops/runbook/customer-onboarding) 와 동일하다. customer IT 가 Entra app 3개를 새로 등록 (`axelabs-bootstrap.sh`) 해서 JSON pack 을 회신하면 `axe customers ingest {cid} <pack>.json --apply` 가 sso 블록 + client_secret 3개 vault push 를 한 번에 흡수한다 ([/partner/registration](/partner/registration), [/partner/handoff](/partner/handoff)).

> **핵심 차이**: 기존 운영 박스는 `axe` 신원으로 컨테이너가 이미 돌고 데이터가 들어있을 수 있다. customer 로 전환하면 그 데이터의 소유·접근 경계가 customer org 로 옮겨간다. 운영 중 frame/hive 회계·HR 데이터가 `axe` 법인 것이면 customer 박스로 넘기기 전에 **반드시 분리/정리** (customer 의 host 에 운영자 법인 데이터가 남지 않도록). 이건 코드가 자동으로 못 한다 — 운영자 판단.

### ② vault organization / collection

운영자 vault (`/Users/axe/.axe/vault/`) 의 AXE org (`0c5d8bbd-...`) 는 **운영자 자신의** 비밀을 담는다. customer 박스는 자기 Vaultwarden 인스턴스 + 자기 org 를 가져야 한다 (customer sovereignty). 전환 시:

- 그 박스의 `axe-vaultwarden` 이 **customer 의 org** 를 호스트하도록 한다 — 운영자 org 데이터를 customer 박스에 남기지 않는다.
- customer org 생성 + collection 구조 v1 ([D-ops-32](/ops/decisions)):
  - entity 1개 customer: 4-collection (`Platform — Service Secrets` / `Platform — Infrastructure` (operator only) · `{Customer Entity}` / `Shared Tools` (전직원 RW))
- dual-identity 계정 ([D-ops-29](/ops/decisions)): `ai@{customer-domain}` (bot) + 사람 admin, 둘 다 Owner + access_all.
- vault env 4-key 패치 (JIT 가입 차단 회피) — `axe vault bootstrap {customer} --apply` 가 SSH 로 customer macmini 의 `docker-compose.yml` 에 in-place 적용 + force-recreate ([D-ops-24/26/27](/ops/decisions), [D-ops-33](/ops/decisions)).

서비스 비밀 흐름 자체는 동일 — `customers.yaml services.<svc>.secrets[]` → `axe secret push` → customer vault org → `axe secret pull <svc> --customer {cid}` → env_file. 단 vault item 네이밍의 customer 세그먼트가 `axe` → `{customer}` 로 바뀐다 (`frame/axe/db-password` → `frame/{customer}/db-password`). 상세 = [/services/vaultwarden](/services/vaultwarden) + [/architecture/secrets](/architecture/secrets).

### ③ cloudflared tunnel route

운영자 박스의 tunnel 은 `axelabs-tunnel` (historical 명, customer infix 없음 — `axe` 전용) 이고 docs.axelabs.ai / admin.axelabs.ai 등 **운영자 자기 서비스** 를 서빙한다 ([axe CLI `TUNNEL_NAME_FMT = "axelabs-{customer}"`](/architecture/secrets), `axe` 는 예외적으로 infix 생략).

customer 로 전환하면 그 박스는 **자기 customer tunnel** 을 가진다:

- `axe onboard {customer} --apply` 가 customer macmini 에 **독립 tunnel** (`axelabs-{customer}-tunnel`) 을 생성 + launchd 등록 + boot 한다 (`_render_cloudflared_config`). 중앙 tunnel 편집은 불필요 — [2026-05-23 drift 정정](/ops/known-gaps).
- ingress route 는 `{customer}.axelabs.ai` 의 path 들 (`/`, `/vault`, `/frame/mcp`, `/blueprint`, `/hive/mcp`) 을 host 내부 포트로 매핑.
- Cloudflare DNS 의 `{customer}` CNAME (`<tunnel-uuid>.cfargotunnel.com`) 은 `axe onboard` step 5 가 API 로 자동 생성.

> 즉 운영자 박스가 `axelabs-tunnel` 로 운영자 도메인을 서빙하던 것에서, customer 박스는 `axelabs-{customer}-tunnel` 로 `{customer}.axelabs.ai` 를 서빙한다. 한 박스를 인계하는 전환이면 운영자 tunnel/launchd 를 그 박스에서 **제거**하고 customer tunnel 만 남긴다.

### ④ container 식별 — 실제 rename 은 아직 없음

여기가 가장 오해하기 쉬운 부분이다. **컨테이너 이름은 customer 와 무관하게 고정**이다:

| 패턴 | 예시 | customer 식별 방식 |
|---|---|---|
| customer-agnostic (service-prefixed) | `frame-postgres`, `frame-mcp-blue`, `hive-mcp-blue`, `blueprint-app` | host (macmini hostname) 로 식별. customer-per-macmini 격리라 같은 host 에 다른 customer 없음 → 충돌 0 |
| historical `axe-` prefix 잔존 | `axe-frame-proxy`, `axe-hive-proxy`, `axe-vaultwarden`, `axe-vault-caddy` | compose 의 `container_name` 이 `axe-` 박혀 있음 — **customer 박스에서도 이름은 `axe-...`** (misleading 하지만 동작엔 무해) |
| customer-prefix 적용 | `axelabs-{customer}-tunnel` (cloudflared) | ✅ 유일하게 customer ID 가 들어가는 것 = tunnel |

즉 전환해도 frame/hive/blueprint 컨테이너 이름은 안 바뀐다 (`frame-postgres` 그대로). **변수화는 backlog** ([B-container-name-customer-prefix](/ops/backlog), compose `container_name` 을 `${CUSTOMER_PREFIX}-` 로 — 실 운영 영향 없어 우선순위 中). 그래서 "container prefix 변경" 이라기보다 **container 의 소유 host 가 바뀌는 것** 으로 이해해야 한다.

## 전환 절차

전제: vault session 살아있음 (`bw unlock` 8h cache, [Phase A](/ops/runbook/customer-onboarding#phase-a--세션-prerequisite-한-번-이후-8-시간-자동)) + customer Entra pack 회신 받음 + 그 박스 Tailscale reachable.

```bash
# 0. (인계 전환이면) 박스에서 운영자 axe 신원 정리:
#    - 운영자 법인 데이터 (frame/hive) 분리 — 코드 자동화 없음, 운영자 판단
#    - 운영자 axelabs-tunnel launchd 제거 (customer tunnel 로 대체될 것)
#    - 운영자 org 데이터가 그 박스 vault 에 남지 않도록 정리

# 1. customers.yaml entry 흡수 (Entra pack → sso 블록 + client_secret 3개 vault push)
axe customers ingest {customer} ~/Downloads/axelabs-bootstrap-{customer}.json --apply
#    (먼저 --apply 빼고 dry-run 으로 변경 계획 확인)

# 2. cloudflared customer tunnel + DNS + launchd (그 박스에 독립 tunnel 생성)
axe onboard {customer} --apply --skip-frame

# 3. customer 측 bw vault session bootstrap (customer IT interactive 1회)
scp /Users/axe/.axe/bw-bootstrap.sh {customer}-macmini:~/bw-bootstrap.sh
ssh -t {customer}-macmini "brew install bitwarden-cli && ~/bw-bootstrap.sh https://<vault-url> <login-email>"

# 4. customer-scoped service stack 배포 (각 13-step, idempotent)
axe deploy frame {customer} --apply
axe deploy blueprint {customer} --apply
axe deploy hive {customer} --apply

# 5. vault env 4-key 패치 + force-recreate (JIT 가입 차단 회피)
axe vault bootstrap {customer} --apply
```

각 `axe deploy {svc} {customer}` 는 13-step (preflight → clone → vault_check → secrets_bootstrap → env_local → wrapper push → network → (blueprint) frame_mcp_token → compose up → (frame) proxy push → health → (frame+hive) register_entities → ingress swap) 을 SSH 로 그 박스에서 수행한다. 표준 onboarding 의 D-day 와 동일한 명령 — 차이는 **macmini-prep 6단계를 건너뛴다** (인프라 이미 준비됨) 는 것뿐.

전환 후 검증 + D+1/D+2/D+7 후속 (admin 부여 · entity 등록 · 직원 SSO 안내 · 안정화 점검) 은 [/ops/runbook/customer-onboarding](/ops/runbook/customer-onboarding) 의 해당 시점 절차를 그대로 따른다.

## 부분 전환 — 운영자 박스에 customer service 만 co-tenant

(엄밀 격리 모델 외 예외) customer 의 기존 자산 service (예: realchoice 의 `stream` :8780 / `magnet` :8770) 가 운영자 manifest 에만 정렬돼 도는 형태가 있다 — 이 경우 컨테이너는 customer 의 것이되 customers.yaml manifest 만 운영자 SoT 에 등재된다 ([서비스 카탈로그](/services) 의 stream/magnet `live (realchoice)` 표기). 이건 "박스 전환" 이 아니라 **manifest 정렬** 이므로 위 ①만 (customers.yaml services 슬롯 추가) 적용하고 ②③④ 는 customer 측 기존 인프라를 그대로 둔다. 포트 충돌만 사전 확인 (`portcheck` / `portdup`).

## 함정

| 함정 | 결과 | 회피 |
|---|---|---|
| 운영자 법인 데이터가 customer 박스에 잔존 | customer 가 운영자 회계/HR 데이터 접근 가능 (sovereignty 위반) | step 0 에서 frame/hive 데이터 분리 — 코드 자동화 없음, 운영자 책임 |
| 운영자 `axelabs-tunnel` launchd 가 그 박스에 남음 | customer tunnel 과 ingress 경합 / 운영자 도메인이 customer 박스로 샘 | step 0 에서 운영자 tunnel launchd 제거 |
| `ssh_user` 가 `axe` 가 아닌데 미명시 | axe CLI 가 `is_local` 분기 오판 또는 SSH user 틀림 | customers.yaml entry 에 실제 macOS 사용자명 명시 |
| 같은 host 에 두 customer 영구 co-tenant 시도 | `frame-postgres` 등 컨테이너 이름·포트 충돌 | customer-per-macmini 유지 — co-tenant 는 부분 전환 (manifest 정렬) 한정 |
| 컨테이너 이름이 `axe-...` 라 customer 박스인데 운영자 것으로 오인 | 운영 혼동 (실 동작 무해) | host (macmini hostname) 로 식별. rename 은 [B-container-name-customer-prefix](/ops/backlog) 대기 |
| `docker compose restart` 로 env 변경 반영 시도 | env reload 안 됨 | `docker compose up -d --force-recreate <container>` |

## 참고

- 표준 (신품 macmini) onboarding: [/partner](/partner) → [/partner/macmini-prep](/partner/macmini-prep) → [/partner/registration](/partner/registration) → [/partner/handoff](/partner/handoff) → [/partner/deploy](/partner/deploy)
- 운영자 측 D-day 절차 (동일 명령): [/ops/runbook/customer-onboarding](/ops/runbook/customer-onboarding)
- vault 서비스 정의 + org 모델: [/services/vaultwarden](/services/vaultwarden)
- 비밀 흐름 + 컨테이너 이름 컨벤션: [/architecture/secrets](/architecture/secrets)
- 기존 vault 유지 customer: [/partner/deploy § Vault OIDC SSO 통합](/partner/deploy)
