한 줄로
회사를 만들 때 Fabric 은 안 생긴다. 빈 클러스터에 오퍼레이터만 깔릴 뿐이고, 네트워크는
따로 요청해야 만들어진다. 그 과정이 스크립트 1,178줄, 20단계다.
두 단계로 나뉜다
회사 생성 글에서 클러스터까지는 봤다.
그때 깔리는 건 hlf-operator 뿐이다.
오퍼레이터는 CRD(사용자 정의 리소스)를 감시하다가 실제 파드를 만들어주는 컨트롤러다.
FabricPeer 라는 리소스를 만들면 오퍼레이터가 그걸 보고 피어 파드를 띄운다.
즉 회사 생성 시점에는 일꾼만 배치하고, 실제 주문은 나중에 넣는 구조다.
회사 생성 → 빈 클러스터 + hlf-operator (일꾼 배치)
↓ 나중에, 사용자가 요청
Fabric 생성 → CA · 피어 · 오더러 · 채널 (주문)
무엇이 몇 개 생기나
| 구성요소 | 개수 | 이미지 |
|---|---|---|
| 조직 CA | 조직 수만큼 | fabric-ca:1.5.7 |
| 오더러용 CA | 1 | fabric-ca:1.5.7 |
| 피어 | 조직 × 피어수 | fabric-peer:2.5.5 |
| 상태 DB | 피어마다 1 | couchdb |
| 오더러 | 1 | fabric-orderer:2.5.5 |
| 채널 | 최대 2 | — |
오더러가 1개인 게 눈에 띈다. 스크립트 전체에서 ordnode create 호출이 딱 한 번이다.
Fabric 의 오더러는 트랜잭션 순서를 정하는 역할이라 보통 Raft 로 3~5개를 묶는다. 1개면
정족수 개념이 없다. 이게 죽으면 네트워크 전체가 멈춘다.
개발·시연 환경이라면 합리적인 절충이지만, 운영으로 가면 가장 먼저 손봐야 할 지점이다.
CA 부터 시작하는 이유
순서가 CA → 피어 → 오더러 → 채널 인 데는 이유가 있다.
Fabric 은 모든 참여자가 인증서로 신원을 증명한다. 피어를 만들려면 그 피어의 인증서를 발급할
CA 가 먼저 있어야 한다. 그래서 CA 를 세우고 → 신원을 등록(register)하고 → 인증서를
받아오고(enroll) → 그 인증서로 피어를 만든다.
kubectl hlf ca create --name=org1-ca ...
kubectl hlf ca register --name=org1-ca --user=peer ...
kubectl hlf peer create --ca-name=org1-ca.${ns} ...
kubectl hlf 는 오퍼레이터가 제공하는 플러그인이다. 이 명령들이 사실은 CRD 를 만드는 것이고,
파드를 실제로 띄우는 건 오퍼레이터다.
대기가 곳곳에 박혀 있다
CRD 를 만든다고 파드가 즉시 뜨지는 않는다. 그래서 단계마다 기다린다.
wait_for_pod_ready ...
sleep 15
wait_for_pod_ready 같은 조건 대기는 정상이다. 문제는 그 옆에 붙은 고정 sleep 이다.
CA 준비 후 15초, 오더러 CA 후 10초, 포트포워딩마다 3초…
고정 대기는 서버가 한가할 땐 낭비고, 바쁠 땐 모자란다. 부하가 걸리면 깨지는 종류의 코드다.
자바는 왜 클래스가 6개인가
Fabric 쪽 서비스 클래스가 6개다. 처음엔 기능별 분해인 줄 알았는데 아니었다.
| 클래스 | 실제 역할 |
|---|---|
FabricService (529줄) |
유일한 쓰기 담당 — 생성·설치·삭제·동기화 전부 |
OrgService · PeerService · ChannelService · OrdererService · CertificateService
|
조회 전용 |
뒤의 5개는 화면의 네트워크 구성도에서 노드를 클릭했을 때 보여줄 상세 정보를 만든다.
조직 노드를 누르면 OrgService, 피어를 누르면 PeerService 다.
즉 도메인 분해가 아니라 UI 분해다. 그래서 5개 모두 같은 소유권 검사 코드를 반복한다.
if (!fabricRepository.existsById(...)) throw NOT_FOUND;
같은 검사가 다섯 번 복사돼 있는 게 이 구조의 비용이다.
체인코드는 컨테이너로 돈다
체인코드(스마트 컨트랙트에 해당)를 배포하는 방식이 CCaaS(Chaincode as a Service)다.
피어 안에서 코드를 돌리는 게 아니라 별도 컨테이너로 띄우고 피어가 접속한다.
이미지 이름 규칙이 이렇다.
localhost:5001/{블록체인명}-{체인코드명}:{버전}
폐쇄망이라 여기서도 제약이 나온다. Node 체인코드 Dockerfile 에 npm install 이 없다.
FROM localhost:5001/node:18-alpine
# node_modules 는 tar.gz 안에 이미 들어 있다 (폐쇄망)
의존성을 미리 담아 보내야 한다. 인터넷이 없으니 빌드 시점에 받을 수 없다.
여기서 걸린 것 셋
① 성공 알림이 시작 전에 나간다
컨트롤러가 이렇게 돼 있다.
fabricService.createFabricNetwork(createReq);
executeFabricNetwork(fabricNetwork.getId()); // ← 큐에 넣기만 한다
...
notificationService.createByAgency(NotificationType.NETWORK_INSTALL_SUCCESS);
executeFabricNetwork 는 비동기 큐에 넣고 즉시 반환한다. 실제 설치는 그 뒤로 몇 분간
진행된다. 그런데 알림은 바로 나간다.
더 문제는 비동기 본체가 실패해도 실패 알림을 안 보낸다는 점이다. 상태만 ERROR 로 바꾼다.
사용자 입장에서는 “설치 완료” 를 받았는데 화면에는 이상 발생이 떠 있다.
② Explorer 가 죽으면 네트워크가 ERROR 로 보인다
상태 판정이 이렇게 돈다.
kubectl get pods -n {네임스페이스} ← 라벨 셀렉터 없음
→ 하나라도 Running/Ready 가 아니면 ERROR
네임스페이스 전체를 본다. 그런데 그 안에는 CA·피어·오더러만 있는 게 아니다.
블록 탐색기(Explorer)와 그 DB, 체인코드 컨테이너가 같이 산다.
Explorer 는 스크립트가 “비필수” 로 선언한 컴포넌트다. 실패해도 넘어간다. 그런데 일단
파드가 뜬 뒤 죽으면, 이 판정은 Fabric 네트워크 전체를 ERROR 로 만든다.
비필수라고 선언해놓고 상태 판정에서는 필수로 취급하는 셈이다.
③ 삭제는 실패를 보고할 수 없다
삭제 스크립트의 명령이 전부 이렇다.
kubectl delete ... || true
모든 명령에 || true 가 붙어 있다. 스크립트가 0이 아닌 값으로 끝나는 경우는 클러스터나
kubeconfig 자체가 없을 때뿐이다.
자바는 종료 코드만 본다. 그래서 아무것도 못 지웠어도 성공으로 판정하고, DB 행을 삭제 처리하고
포트를 반환한다. 클러스터에는 리소스가 남는다.
fix-join-org2-peers.sh 의 정체
스크립트 목록에 이질적인 게 하나 있다. 이름부터 다르다.
열어보니 특정 클러스터 UUID·네임스페이스·채널명이 하드코딩돼 있었다. 재사용 가능한 도구가
아니라 한 번의 사고를 수습한 흔적이다.
무슨 사고였나 — org2 의 피어들이 채널 조인에 조용히 실패했다. 에러 없이, 안 붙은 채로.
원인은 본 스크립트 주석에 남아 있다. 오퍼레이터가 상태를 조정(reconcile)하면서 각 피어의
TLS 루트 인증서를 자기 조직 것으로 덮어쓴다. 그러면 다른 조직과의 통신이 깨진다.
수습이 아니라 재발 방지로 이어졌다
이 사고가 일회성으로 끝나지 않았다는 게 git 로그에 남아 있다. 이틀에 걸쳐 두 커밋이
같은 문제를 다룬다.
첫째, 조인됐는지 실제로 확인하게 했다.
feat: peer 채널 조인 검증 추가 (2026-05-11)
- verify_channel_join(): peer channel list로 조인 여부 확인하는 함수 추가
- verify_all_peers_joined(): 채널 내 모든 peer를 일괄 검증 — 조인 누락 시 exit 1
- verify_peer_on_channel(): approve 전 peer가 채널에 조인됐는지 사전 검증
핵심은 exit 1 이다. 그전에는 조인 명령이 실패해도 다음 단계로 갔다. 이제는 붙었는지
직접 확인하고, 아니면 거기서 멈춘다.
03-deploy-chaincode 에도 사전 검증이 들어간 게 눈에 띈다. 체인코드를 승인하려면 피어가
채널에 있어야 하는데, 없으면 엉뚱한 에러가 난다. 원인과 먼 곳에서 증상이 터지는 걸
막으려고 앞단에서 확인하는 것이다.
둘째, 명령이 매달리는 문제를 잡았다.
feat: kubectl exec 타임아웃 및 재시도 로직 추가 (2026-05-11)
- kubectl_with_retry(): kubectl exec/cp 실패 시 최대 3회 재시도
- 모든 kubectl exec/cp에 --request-timeout=60s 적용
heredoc 대신 printf로 config.yaml 생성
(heredoc이 kubectl exec 내부에서 hang 유발 가능)
- join_peer_to_channel(): --request-timeout=90s + 최대 3회 재시도
여기 함정이 하나 숨어 있다. kubectl exec 안에서 heredoc 을 쓰면 멈출 수 있다.
heredoc(<< EOF)은 표준 입력으로 내용을 밀어넣는 방식인데, kubectl exec 는 그 표준 입력을
네트워크 너머로 중계한다. 입력이 끝났다는 신호가 제대로 전달되지 않으면 양쪽이 서로를
기다린다. 에러가 아니라 그냥 안 끝난다.
printf 로 바꾼 건 표준 입력을 아예 안 쓰겠다는 뜻이다. 문제를 우회하는 대신
원인을 제거한 선택이다.
타임아웃 값을 작업마다 다르게 준 것도 눈에 띈다 — 파일 복사는 60초, 채널 조인은 90초.
채널 조인은 오더러와 통신해야 해서 더 오래 걸린다.
남는 교훈
오퍼레이터에 위임한 대가다. 편하게 만들어주는 대신, 오퍼레이터가 내 설정을 되돌릴 수 있다.
그리고 방어가 붙은 순서가 의미심장하다.
사고 발생 → 일회성 스크립트로 수습 (fix-join-org2-peers.sh)
→ 검증 추가 (다시 나면 멈추게)
→ 재시도·타임아웃 추가 (덜 나게)
수습 → 감지 → 예방 순이다. 처음부터 예방이 있었으면 좋았겠지만, 실제로는 겪어야
어디가 약한지 알게 되는 경우가 많다. 일회성 스크립트를 지우지 않고 남겨둔 것도
그 흔적으로서 값이 있다.
정리
- Fabric 은 회사 생성 때 안 생긴다. 오퍼레이터만 깔리고 네트워크는 별도 요청
- 순서가 CA → 피어 → 오더러 → 채널 인 이유는 인증서 의존성
- 오더러가 1개 — 정족수가 없다. 운영 전환 시 1순위 개선 대상
- 자바 클래스 6개는 도메인이 아니라 UI 노드 종류별 조회 서비스다
- 체인코드는 CCaaS, 폐쇄망이라 의존성을 미리 담아 보낸다
- ⚠️ 성공 알림이 설치 시작 전에 나가고, 실패 알림은 없다
- ⚠️ 상태 판정이 네임스페이스 전체를 봐서, 비필수 컴포넌트가 전체를
ERROR로 만든다 - ⚠️ 삭제 스크립트는
|| true때문에 실패를 보고할 수 없다
같이 읽을 글:
Besu 네트워크는 어떻게 생기나 ·
회사마다 클러스터 하나씩 띄운다 ·
클러스터를 지웠는데 디스크가 안 줄어든다