MGMT 클러스터에서 OpenstackConfig CR을 감시하고 OpenStack 네트워크 정보를 수집한 뒤 MGMT 클러스터에 배포된 Viola API로 노드별 인터페이스 정보를 전송하는 오퍼레이터입니다.
- 입력: OpenstackConfig CR (openstackProviderID, k8sProviderID, projectID, VM ID 목록 + settings/secrets)
- 처리: Contrabass → Keystone → Neutron 포트 조회
- 출력: Viola API로 JSON POST (MultiNicNodeConfig 생성용, subnetIDs 우선/없으면 subnetID/subnetName)
- 저장: 오퍼레이터 내부 Inventory API + 파일 기반 DB(JSON)에 최신 상태 upsert (UI 조회용)
- 서브넷 지정:
subnetIDs>subnetID>subnetName우선순위 적용- 여러 서브넷 지정 가능 (예:
subnetIDs: [subnet-a, subnet-b])
- 여러 서브넷 지정 가능 (예:
- 인터페이스 상한: 노드당 최대 10개 (
multinic0~multinic9) - 기준 시점: OpenstackConfig 생성 시각 이후에 생성된 포트만 처리
- 포트 필터:
settings.openstackPortAllowedStatuses에 포함된 포트만 처리 - Viola POST 필수값: k8sProviderID가 있어야
x-provider-id헤더로 전송 가능 - Agent 지원 OS: Ubuntu(netplan), RHEL(NetworkManager) 기반 영속 설정
- 상세 내용은
../multinic-agent/README.md참고
- 상세 내용은
주의:
subnetIDs>subnetID>subnetName순서로 적용합니다.subnetName은 네트워크명이 아니라 서브넷 이름입니다. (동일 이름이 있으면 오류)vmNames에는 VM ID(UUID) 를 넣어야 합니다.- nodeName은 Nova 서버 이름을 사용하며, 필요 시
settings.openstackNodeNameMetadataKey로 metadata 값을 우선 사용하도록 설정할 수 있습니다. - 포트 상태가
settings.openstackPortAllowedStatuses에 포함되지 않거나, 대상 노드의 인터페이스가 비어 있으면 해당 노드는 전송에서 제외됩니다. - OpenstackConfig 생성 시각 이후에 생성된 포트만 처리합니다.
- DOWN 포트가 남아 있으면 빠른 재시도 후(기본 5회) 느린 주기로 재전송합니다.
- 인터페이스는
subnetIDs순서대로 정렬한 뒤 normalize 단계에서 MAC/PortID 기준으로 재정렬됩니다. (현재 동작: 최종multinic순서는 MAC 기준)
- Go 1.25+
- Kubernetes 클러스터 접근 권한
- Contrabass/OpenStack API 접근 가능
운영 환경에서는 Helm 배포 시 이미지 정보만 설정하고, 실제 접속 정보는 OpenstackConfig CR로 전달합니다.
필수 필드:
subnetIDs또는subnetID또는subnetName(subnetIDs/subnetID 권장)vmNames: VM ID(UUID) 목록credentials.openstackProviderIDcredentials.k8sProviderIDcredentials.projectID- Contrabass 암호화 키
secrets.contrabassEncryptKeySecretRef또는settings.contrabassEncryptKey
동작 규칙:
subnetIDs가 있으면subnetID/subnetName은 무시됩니다.
선택 필드:
settings: Contrabass/Viola/OpenStack/폴링 옵션secrets.contrabassEncryptKeySecretRef(권장)
기본 암호화 키:
<namespace>/contrabass-encrypt-keySecret의CONTRABASS_ENCRYPT_KEY를 자동 사용- Secret이 없으면
settings.contrabassEncryptKey를 사용
Secret 예시:
apiVersion: v1
kind: Secret
metadata:
name: contrabass-encrypt-key
namespace: multinic-system
type: Opaque
stringData:
CONTRABASS_ENCRYPT_KEY: "conbaEncrypt2025"Viola API 주소:
spec.settings.violaEndpoint가 있으면 CR별로 사용- 없으면 Helm values의
operatorConfig.violaEndpoint(=VIOLA_ENDPOINT)를 사용
OpenstackConfig 예시:
apiVersion: multinic.example.com/v1alpha1
kind: OpenstackConfig
metadata:
name: openstackconfig-sample
namespace: multinic-system
spec:
subnetIDs:
- "8f0d5f5b-8f3f-4b2b-9c4c-8c9f7c36d1f2" # OpenStack subnet ID
- "dae4f6ea-76ae-4e56-b3a5-87e6df94a574" # OpenStack subnet ID
vmNames:
- "08186d75-754e-449c-b210-c0ea822727a7" # OpenStack VM ID(UUID)
- "c863944f-5cfe-4e05-805f-7522f3e9b080" # OpenStack VM ID(UUID)
- "fbfd0e4d-a4bb-4769-bceb-46cb4b0dc3c5" # OpenStack VM ID(UUID)
credentials:
openstackProviderID: "66da2e07-a09d-4797-b9c6-75a2ff91381e" # Contrabass Provider ID
projectID: "0d5f63c52fc94aeeb767e69790fa73c8" # OpenStack Project(Tenant) ID
k8sProviderID: "f5861c22-b252-42b5-a0c5-cfb1d245c819" # K8s Provider ID (Viola 라우팅 키)
settings:
contrabassEndpoint: "https://expert.bf.okestro.cloud" # Contrabass API base URL
violaEndpoint: "http://viola-api.multinic-system.svc.cluster.local:8080" # Viola API base URL
openstackPortAllowedStatuses:
- "ACTIVE" # 대상 포트 상태
- "DOWN" # 대상 포트 상태
pollFastInterval: "10s" # 변경 직후 빠른 폴링 주기
pollSlowInterval: "2m" # 안정 구간 폴링 주기
secrets:
contrabassEncryptKeySecretRef:
name: contrabass-encrypt-key # adminPw 복호화 키 Secret 이름
key: CONTRABASS_ENCRYPT_KEY # Secret data key자세한 흐름도/아키텍처/단계 설명은 docs/FLOW.md 참고.
flowchart LR
subgraph MGMT["MGMT Cluster"]
OP[Multinic Operator]
VA[Viola API]
INV[Inventory API]
CAPI[Contrabass API]
end
subgraph OS["OpenStack"]
KS[Keystone]
NE[Neutron]
NO[Nova]
end
subgraph BIZ["Biz Cluster"]
KAPI[K8s API Server]
CR[MultiNicNodeConfig CR]
end
OP -->|Provider 조회| CAPI
OP -->|Token 요청| KS
OP -->|Port 조회| NE
OP -->|NodeName 조회| NO
OP -->|노드별 인터페이스 POST| VA
VA -->|CR 적용 요청| KAPI
KAPI -->|CR 생성/갱신| CR
OP -->|상태 저장| INV
sequenceDiagram
autonumber
participant CR as OpenstackConfig
participant OP as Operator
participant CB as Contrabass (MGMT)
participant KS as Keystone
participant NE as Neutron
participant NO as Nova
participant VA as Viola API (MGMT)
participant K8S as Biz K8s API
CR->>OP: CR 생성/수정
OP->>CB: Provider 조회
CB-->>OP: Keystone URL + Admin 계정
OP->>KS: 토큰 요청
KS-->>OP: 토큰 + 카탈로그
OP->>NE: Port 조회 (device_id=VM ID)
OP->>NO: 서버 정보 조회 (nodeName)
OP->>VA: 노드별 인터페이스 POST
VA->>K8S: CR 적용 요청
K8S-->>VA: 적용 결과
- OpenstackConfig CR 이벤트 발생
- Contrabass provider 조회 및 adminPw 복호화
- Keystone 토큰 발급 (서비스 카탈로그 포함)
- Neutron 엔드포인트 결정 (카탈로그 또는 settings)
- subnetIDs/subnetID/subnetName → subnet/network 조회 (CIDR/MTU 확보)
- Neutron 포트 조회 (device_id == VM ID)
- Nova 서버 조회로 nodeName 결정 (metadata key > server name > vmID)
- 대상 subnet에 포함된 포트만 선별
- 노드별 인터페이스 구성
- Viola API POST
- 파일 기반 DB(JSON) 최신 상태 upsert (
k8sProviderID+ nodeName 기준) - 변경 직후 빠른 폴링 → 안정 구간은 느린 폴링
- 시작 조건: OpenstackConfig CR 생성/수정 또는 VM 포트 부착
- CR 생성 시각 이후에 생성된 포트만 처리 (기존 포트 제외)
- OpenstackConfig 입력 의미:
subnetIDs/subnetID/subnetName: 멀티 NIC 대상 서브넷 지정vmNames: 포트 조회 대상 VM ID(device_id 매칭)credentials.openstackProviderID: Contrabass 조회용 Provider IDcredentials.projectID: Keystone 토큰 발급 대상 Project IDcredentials.k8sProviderID: Viola 라우팅 키(x-provider-id)settings.violaEndpoint: Viola API POST 주소contrabassEncryptKey: adminPw 복호화 키(Secret 또는 settings)
- Token/Service Catalog:
- Keystone 토큰은 Neutron/Nova 호출 인증에 필요
- Service Catalog는 서비스별 엔드포인트 URL 결정에 사용
- Port/NodeName 조회:
- Neutron에서 VM ID 기반 포트를 조회 후 서브넷/상태 필터 적용
- Nova 조회로 K8s nodeName 결정 (metadata key 우선, 없으면 서버 이름)
Operator가 OpenStack 포트 정보를 수집한 뒤 MGMT 클러스터에 배포된 Viola API로 POST 요청을 보냅니다.
Viola API 주소는 spec.settings.violaEndpoint가 우선이며, 없으면 Helm values의
operatorConfig.violaEndpoint로 설정합니다.
- Endpoint:
POST /v1/k8s/multinic/node-configs - Headers:
x-provider-id(string, required):OpenstackConfig.spec.credentials.k8sProviderID
- Request Body: 노드별 MultiNicNodeConfig 목록(JSON 배열)
요청 필드:
| 구분 | key | type | required | description |
|---|---|---|---|---|
| Body | nodeName |
string | O | K8s 노드명 |
| Body | instanceId |
string | O | OpenStack VM ID |
| Body | interfaces |
array | O | 노드에 부착된 인터페이스 목록 |
| Body | interfaces[].id |
int | O | 0~9 |
| Body | interfaces[].name |
string | O | multinic0~multinic9 |
| Body | interfaces[].macAddress |
string | O | MAC 주소 |
| Body | interfaces[].address |
string | O | IPv4 주소 |
| Body | interfaces[].cidr |
string | O | 서브넷 CIDR |
| Body | interfaces[].mtu |
int | O | MTU |
x-provider-id 값은 OpenstackConfig.spec.credentials.k8sProviderID를 사용합니다.
예시 (요청 헤더 포함, 2개 노드/각 3개 인터페이스):
POST /v1/k8s/multinic/node-configs HTTP/1.1
Host: viola-api.example.com
Content-Type: application/json
x-provider-id: f5861c22-b252-42b5-a0c5-cfb1d245c819
[
{
"nodeName": "worker-1",
"instanceId": "i-0123456789abcdef0",
"interfaces": [
{
"id": 0,
"name": "multinic0",
"macAddress": "00:1A:2B:3C:4D:5E",
"address": "192.168.1.100",
"cidr": "192.168.1.0/24",
"mtu": 1500
},
{
"id": 1,
"name": "multinic1",
"macAddress": "00:1A:2B:3C:4D:5F",
"address": "192.168.1.101",
"cidr": "192.168.1.0/24",
"mtu": 1500
},
{
"id": 2,
"name": "multinic2",
"macAddress": "00:1A:2B:3C:4D:60",
"address": "192.168.1.102",
"cidr": "192.168.1.0/24",
"mtu": 1500
}
]
},
{
"nodeName": "worker-2",
"instanceId": "i-0fedcba9876543210",
"interfaces": [
{
"id": 0,
"name": "multinic0",
"macAddress": "00:1A:2B:3C:4D:61",
"address": "192.168.2.10",
"cidr": "192.168.2.0/24",
"mtu": 1500
},
{
"id": 1,
"name": "multinic1",
"macAddress": "00:1A:2B:3C:4D:62",
"address": "192.168.2.11",
"cidr": "192.168.2.0/24",
"mtu": 1500
},
{
"id": 2,
"name": "multinic2",
"macAddress": "00:1A:2B:3C:4D:63",
"address": "192.168.2.12",
"cidr": "192.168.2.0/24",
"mtu": 1500
}
]
}
]차트 경로: deployments/helm
Helm values에는 이미지 정보가 필수입니다. Viola API 주소는 CR에서 지정하지 않는 경우에만 Helm values로 설정합니다.
배포 예시:
helm upgrade --install multinic-operator deployments/helm \
-n multinic-operator-system --create-namespace \
--set image.repository=nexus.okestro-k8s.com:50000/multinic-operator \
--set image.tag=dev-20260113062309 \
--set image.pullSecrets[0].name=nexus-regcredvalues.yaml 작성 예시(필수):
image:
repository: nexus.okestro-k8s.com:50000/multinic-operator
tag: "dev-20260113062309"
pullSecrets:
- name: nexus-regcred
operatorConfig:
# CR에서 violaEndpoint를 지정하지 않을 때만 사용
violaEndpoint: "https://viola-api.example.com"values.yaml 작성 예시(선택):
inventory:
enabled: true
service:
port: 18081
persistence:
enabled: falsespec.settings.violaEndpoint를 사용하면 CR별로 Viola API 주소를 지정할 수 있습니다.
CR에 값이 없으면 operatorConfig.violaEndpoint를 기본값으로 사용합니다.
배포 순서 예시:
deployments/helm/values.yaml편집helm upgrade --install ... -f values.yaml로 배포contrabass-encrypt-keySecret 생성- OpenstackConfig CR 적용
사내망에서 인터넷 접근이 불가능할 때는 이미지 tar를 옮겨서 로드한 뒤 사내 Nexus로 push하고 Helm values에 반영합니다.
이미지 tar 경로:
images/multinic-operator_dev-20260113062309.tar
예시:
# 이미지 로드
nerdctl load -i images/multinic-operator_dev-20260113062309.tar
# Nexus에 태그/푸시
nerdctl tag multinic-operator:dev-20260113062309 nexus.okestro-k8s.com:50000/multinic-operator:dev-20260113062309
nerdctl push nexus.okestro-k8s.com:50000/multinic-operator:dev-20260113062309오퍼레이터가 계산한 최신 노드별 인터페이스 스냅샷을 조회하는 내부 API입니다.
UI 조회/디버깅 용도로 사용하며, 실제 적용 상태는 Biz 클러스터의 MultiNicNodeConfig가 기준입니다.
노출 API (3개):
- 클러스터(Provider) 요약 조회:
GET /v1/interfaces/providers - 특정 클러스터 전체 노드 조회:
GET /v1/interfaces/node-configs?providerId=...providerId는 k8sProviderID이며 필수
- instanceId 단건 조회:
GET /v1/interfaces/node-configs/by-instance/{instanceId}?providerId=...instanceId필수,providerId는 중복 방지를 위해 권장
Kubernetes Service:
- Kustomize:
inventory-service(port 18081, namespacesystem) - Helm:
<release>-multinic-operator-inventory(port 18081)
주의: 파일 기반 저장소이므로 오퍼레이터는 1개 replica로 운영하는 것을 권장합니다.
지속 저장이 필요하면 config/manager/manager.yaml의 emptyDir를 PVC로 교체하십시오.
Swagger 문서(Operator -> Viola POST 페이로드):
GET /openapi.yamlGET /docs(Swagger UI, CDN 사용)- POST(viola) + Interfaces API(조회용)가 포함됩니다.
- Swagger는 현재 접속한 주소(호스트/포트)를 기준으로 호출합니다.
kubectl -n multinic-operator-system port-forward svc/<inventory-service-name> 18081:18081
curl -s "http://127.0.0.1:18081/v1/interfaces/providers"
curl -s "http://127.0.0.1:18081/v1/interfaces/node-configs?providerId=<k8s-provider-id>"
curl -s "http://127.0.0.1:18081/v1/interfaces/node-configs/by-instance/<instanceId>?providerId=<k8s-provider-id>"추천 조회 흐름:
/v1/interfaces/providers로 providerId 목록 확인- 해당 providerId로
/v1/interfaces/node-configs조회 - 필요한 instanceId로
/v1/interfaces/node-configs/by-instance/{instanceId}조회- 동일 instanceId가 겹치면 providerId를 추가
/v1/interfaces/providers 응답 예시:
{
"providers": [
{
"providerId": "f5861c22-b252-42b5-a0c5-cfb1d245c819",
"nodeCount": 3,
"updatedAt": "2026-01-12T02:51:32Z",
"nodes": [
{
"providerId": "f5861c22-b252-42b5-a0c5-cfb1d245c819",
"nodeName": "infra01",
"instanceId": "ec4bdcc1-dbcc-4c5d-88a4-581a14beca2d",
"interfaceCount": 3,
"updatedAt": "2026-01-12T02:51:32Z"
}
]
}
]
}노드 상세 응답 예시 (/v1/interfaces/node-configs, /by-instance):
[
{
"providerId": "f5861c22-b252-42b5-a0c5-cfb1d245c819",
"nodeName": "manager01",
"instanceId": "08186d75-754e-449c-b210-c0ea822727a7",
"config": {
"nodeName": "manager01",
"instanceId": "08186d75-754e-449c-b210-c0ea822727a7",
"interfaces": [
{
"id": 0,
"name": "multinic0",
"macAddress": "fa:16:3e:aa:bb:cc",
"address": "10.0.0.10",
"cidr": "10.0.0.0/24",
"mtu": 1450
}
]
},
"lastConfigHash": "a69f59021cf9a8f7",
"updatedAt": "2026-01-11T01:23:45Z"
}
]응답 코드:
200 OK: 조회 성공400 Bad Request: nodeName 누락 등 요청 오류404 Not Found: 조건에 맞는 데이터 없음503 Service Unavailable: inventory 저장소 비활성
OpenstackConfig에는 동기화 상태를 나타내는 Conditions가 갱신됩니다.
Ready: 동기화 성공 여부Degraded: 오류 발생 여부
추가 상태 필드:
lastSyncedAt: 마지막 성공 동기화 시각(Reason=Synced/NoChange일 때 갱신)lastError: 마지막 오류 메시지
make install
make deploy IMG=<registry>/multinic-operator:tag샘플 CR 적용:
kubectl apply -k config/samples/Viola 개발 API가 준비되기 전까지 아래 테스트용 API를 배포해 POST 수신 및 CR 생성까지 확인할 수 있습니다.
# 라우팅 Secret(viola-api-routing)을 먼저 준비해야 합니다.
kubectl apply -f config/test/viola-test-api.yaml동작 방식:
- POST payload를
MultiNicNodeConfig로 변환 kubectl apply -f로 CR 생성/갱신
API 문서(테스트용):
-
GET /healthz- Response:
200 OK+ok(plain text)
- Response:
-
GET /openapi.yaml- OpenAPI 스펙 (YAML)
-
GET /docs- Swagger UI (CDN 사용, 오프라인에서는 동작하지 않을 수 있음)
-
POST /v1/k8s/multinic/node-configs- Headers:
Content-Type: application/jsonx-provider-id(required, 라우팅 키)
- Request Body:
[]NodeConfig(JSON 배열) - Response
200 OK:{"applied":3,"output":"multinicnodeconfig.multinic.io/node-1 configured"} - Response
400 Bad Request:- 잘못된 JSON/필수 필드 누락/라우팅 설정 오류
- Response
500 Internal Server Error:kubectl apply실패(검증 실패/접속 실패 등)
- Headers:
라우팅(테스트용):
- 요청 헤더
x-provider-id를 기준으로 대상 클러스터를 선택합니다.- 값은
OpenstackConfig.spec.credentials.k8sProviderID를 사용합니다.
- 값은
ROUTING_CONFIG에 라우팅 파일을 지정하면 providerId별로 대상 클러스터를 선택합니다.- 모드:
ssh(테스트 권장): 원격 Bastion에 SSH로 접속해kubectl apply.local:kubeApiServer/kubeToken/kubeCaPath로 원격 클러스터에kubectl apply.
- 샘플 파일:
config/test/viola-routing.sample.yaml strict: true로 설정하면 providerId가 없거나 매칭 실패 시 400으로 실패합니다.- SSH 모드는
sshpass가 필요하며 테스트용 Viola API 이미지에는 포함되어 있습니다.
라우팅 Secret 생성 예시(테스트용):
# routing.yaml과 ca.crt를 준비한 뒤 Secret 생성
kubectl -n multinic-system create secret generic viola-api-routing \
--from-file=routing.yaml=./routing.yaml \
--from-file=ca.crt=./ca.crt라우팅 예시:
strict: true
targets:
- providerId: "f5861c22-b252-42b5-a0c5-cfb1d245c819"
mode: ssh
namespace: "multinic-system"
sshHost: "192.168.3.170"
sshUser: "root"
sshPort: 22
sshPass: "cloud1234"
kubectlPath: "kubectl"이미지 빌드 예시:
nerdctl build -f Dockerfile.viola-test-api -t <registry>/multinic-viola-test-api:dev .- 3개 노드에 3개 포트씩 일괄 추가 후 CR 반영 확인
- 포트 제거 시 CR 반영 확인
- DOWN 포트 필터링(
openstackPortAllowedStatuses=ACTIVE) 동작 확인 - 설정 누락 시 ConfigError 발생/복구 확인 (
contrabassEndpoint) - 중복 변경 스킵(
no changes detected) 확인 - 잘못된 VM ID/Project ID 입력 시 오류 처리 확인
- Viola API 장애/timeout 시 재시도 동작 확인
- Inventory API 조회 응답 확인
- Interfaces API: providerId 미입력 시 400 확인
- Interfaces API: 잘못된 instanceId 조회 시 404 확인
- Viola API: x-provider-id 누락 시 400 확인
- Viola API: 정상 payload POST 200 확인
- 멀티 서브넷(test/test2) 동시 부착 동작 확인
- worker/master 다중 노드 동시 적용 확인
- Viola API 라우팅(ssh) 적용 확인
- OpenstackConfig 생성 시각 이후 포트만 처리되는지 확인
- 노드당 인터페이스 10개 초과 시 10개만 전송(
multinic0~9) 확인 -
subnetIDs순서대로 인터페이스 매핑되는지 확인 -
subnetIDs순서 변경 시 최종 인터페이스 순서가 MAC 기준으로 유지됨(현 동작) - OpenstackConfig에서
vmNames제거 시 기존 Inventory/CR 삭제는 하지 않음(현 동작) - Viola API 엔드포인트 오류 시 Ready/Degraded 갱신 확인
- Viola API 장애 복구 후 정상 동기화 확인
- 중복 OpenstackConfig 생성 시 baseline 이전 포트는 전송되지 않음(현 동작)
- DOWN 포트가 ACTIVE로 전환되면 정상 전송됨을 확인
- 오퍼레이터 재시작 후 Inventory가 재동기화됨을 확인