Microsoft Fabric CI/CD: 로컬 디렉토리 구조 기반 미러링 배포 전략 가이드
1. 레포지토리 전체 구조
(Root)
├── .gitlab/ # GitLab 전용 설정 디렉토리
│ └── gitlab-ci.yml # [핵심] CI/CD 파이프라인 정의 파일
├── Script/ # 배포 로직 스크립트 저장소
│ └── kms_deploy_2026-02-20.py # Fabric API 호출용 Python 스크립트
├── WORKSPACE/ # [중요] 실제 Fabric 배포 대상 루트 디렉토리
│ └── TEST-CICD/ # 대상 워크스페이스 내 최상위 경로
│ ├── 00_pipeline/
│ ├── 01_notebook/
│ └── 02_query/
└── README.md # 프로젝트 가이드 및 설명서
2.주요 구성 요소 상세 가이드
⚙️ CI/CD 설정 (.gitlab/gitlab-ci.yml)
- 위치: 프로젝트 루트가 아닌 .gitlab/ 폴더 내부에 위치합니다.
- 역할: 코드 푸시 시 Azure 로그인, 패브릭 토큰 획득, 배포 스크립트 실행 등 전체 프로세스를 제어합니다.
📁 WORKSPACE/ (실제 배포 영역)
- 배포 원칙: 이 디렉토리 하위에 위치한 파일만 실제 Fabric 워크스페이스로 전송됩니다.
- 구조 동기화: WORKSPACE 내부의 폴더 계층은 Fabric 워크스페이스 상에 동일하게 생성됩니다.
⚠️ [필독] 폴더 명명 규칙 (Folder Naming Rules)
- Fabric API는 폴더 이름에 특수문자 제한이 엄격합니다.
- 에러 원인: 폴더명에 마침표(.)가 포함되면 InvalidFolderDisplayName 에러가 발생하여 배포가 중단됩니다.
- ❌ 배포 불가: 00.pipeline, 01.notebook
- 해결 방법: 마침표를 반드시 언더바(_)로 치환하여 WORKSPACE 하위에 배치하십시오.
- ✅ 배포 가능: 00_pipeline, 01_notebook
- 에러 원인: 폴더명에 마침표(.)가 포함되면 InvalidFolderDisplayName 에러가 발생하여 배포가 중단됩니다.
3. 환경 변수 및 보안 (Variables)
배포 파이프라인은 GitLab Settings > CI/CD > Variables에 등록된 보안 변수를 참조합니다.
- 워크스페이스 변수: DATA_HIGHT_DEV, DATA_HIGHT_STG, DATA_HIGHT_PRD
- Azure 인증 변수: AZURE_TENANT_ID, AZURE_SERVICE_PRINCIPAL_ID 등
주의: 모든 환경 변수는 Protected 속성이 부여되어 있습니다. 따라서 반드시 Protected Branch(develop, stg, prd)에서 파이프라인을 실행해야만 정상적으로 배포가 진행됩니다.
🏗️ 배포 전략: 로컬 미러링 기반 계층형 자동화 (Structural Mirroring Strategy)
이 전략의 핵심은 로컬 Git 레포지토리의 특정 디렉토리(WORKSPACE) 구조를 Microsoft Fabric 워크스페이스 환경에 1:1로 복제(Mirroring)하여 배포하는 것입니다.
1. 구조적 동기화 (Structural Mirroring)
- 논리적 일치: 로컬의 WORKSPACE 하위 폴더 계층이 Fabric 워크스페이스의 폴더 구조가 됩니다.
- 예측 가능성: 개발자는 로컬에서 파일을 배치하는 것만으로 운영 환경의 경로를 직관적으로 결정할 수 있습니다.
- 자동화 로직: 배포 스크립트(kms_deploy_2026-02-20.py)가 로컬 경로를 분석하여 Fabric 내에 없는 폴더를 자동으로 생성하고 파일을 해당 위치로 이동(Move)시킵니다.
2. 브랜치 기반 환경 분리 (Multi-Stage Environment)
- 전략적 분리: develop, stg, prd 브랜치를 각각 개발, 스테이징, 운영 워크스페이스와 매핑했습니다.
- 변수 격리: GitLab의 Protected Variables 기능을 사용하여, 각 브랜치에 푸시될 때 해당 환경의 워크스페이스 ID(DATA_HIGHT_DEV/STG/PRD)가 동적으로 할당됩니다.
3. 유효성 검증 및 명명 규칙 강제 (Naming Convention Enforcement)
- 에러 방지: Fabric API의 기술적 제약(폴더명에 마침표 . 사용 불가)을 인지하고, 이를 WORKSPACE 구조에서 언더바(_)로 강제 전환하는 규칙을 수립했습니다.
- 기존 자산 보호: 기존에 사용하던 마침표 포함 폴더(00.pipeline 등)는 로컬 참조용으로 남겨두고, 배포용 파일만 WORKSPACE로 관리하여 운영 안정성을 확보했습니다.
🛠️ 배포 프로세스 상세 흐름
- 코드 푸시: 개발자가 WORKSPACE/ 내부에 파일을 배치하고 브랜치에 푸시합니다.
- 파이프라인 트리거: .gitlab/gitlab-ci.yml이 작동하며 Azure 서비스 주체(SP)를 통해 인증을 수행합니다.
- 경로 분석: 배포 스크립트가 파일의 로컬 상대 경로(예: TEST-CICD/01_notebook)를 추출합니다.
- 아이템 생성/업데이트: Fabric API를 호출하여 노트북 내용을 생성하거나 기존 파일을 업데이트합니다.
- 폴더 미러링 및 이동: 원격지에 동일한 폴더 구조가 없으면 자동으로 생성하고, 생성된 아이템을 해당 폴더로 최종 이동시킵니다.
🌟 이 전략의 장점
- 관리 편의성: 워크스페이스 UI에 접속하지 않고도 수백 개의 노트북 위치를 한 번에 관리할 수 있습니다.
- 휴먼 에러 감소: 폴더 생성과 파일 이동이 자동화되어 수동 조작으로 인한 누락이 방지됩니다.
- 확장성: 동일한 로직으로 노트북뿐만 아니라 데이터 파이프라인 등 다른 Fabric 아이템으로 확장이 가능합니다.
🛠️ Microsoft Fabric CI/CD 구축 주요 이슈 및 해결 사례
1. 폴더 명명 규칙 위반 이슈 (InvalidFolderDisplayName)
- 이슈 내용: 기존 로컬 폴더 명칭(예: 00.pipeline, 01.notebook)을 그대로 사용하여 배포를 시도했으나, Fabric API에서 400 Bad Request 에러를 반환하며 폴더 생성이 거부됨.
- 원인: Microsoft Fabric API는 폴더 이름에 마침표(.)를 허용하지 않는 규칙이 있음.
- 해결 방법: 로컬 레포지토리의 WORKSPACE 하위 구조를 재편하여 마침표를 언더바(_)로 치환함 (예: 00_pipeline). 이를 통해 API 호환성을 확보하고 자동 배포를 성공시킴.
2. CI/CD 환경 변수 전달 및 브랜치 보호 이슈
- 이슈 내용: stg, prd 등 신규 브랜치에서 파이프라인 실행 시 az login 단계에서 테넌트 ID를 찾지 못하거나(Tenant ‘v2.0’ not found), 워크스페이스 ID가 비어 있어 배포가 중단됨.
- 원인: GitLab CI/CD 변수가 Protected로 설정되어 있어, 해당 브랜치가 Protected Branch로 등록되지 않으면 변수 값을 가져올 수 없었음.
- 해결 방법: GitLab 설정에서 develop, stg, prd 브랜치를 모두 Protected branches로 등록하여 배포에 필요한 보안 자격 증명($AZURE_TENANT_ID, $DATA_HIGHT_PRD 등)이 파이프라인에 정상 주입되도록 조치함.
3. 아이템 가시성 및 배포 속도 지연 (Polling 도입)
- 이슈 내용: 신규 노트북 생성(POST) 직후 이동(Move) API를 호출하면 해당 아이템을 찾을 수 없다는 에러가 빈번하게 발생함.
- 원인: Fabric API 요청은 비동기(202 Accepted)로 처리되는 경우가 많아, 아이템이 실제 워크스페이스 리스트에 노출되기까지 약간의 시간이 소요됨.
- 해결 방법: wait_for_item 함수를 도입하여 아이템 생성 후 리스트에 나타날 때까지 최대 120초 동안 3초 간격으로 확인하는 Polling 로직을 스크립트에 구현하여 안정성을 높임.
4. 중첩 폴더 자동 생성 및 경로 확인 로직
- 이슈 내용: 파일이 깊은 계층 구조(예: TEST-CICD/test/sub)에 있을 때, 상위 폴더가 없으면 배포가 실패함.
- 원인: 단순한 생성 API는 부모 폴더가 존재하지 않으면 하위 폴더를 만들 수 없음.
- 해결 방법: ensure_folder_path 함수를 구현하여 경로를 / 단위로 쪼갠 뒤, 최상위부터 최하위까지 폴더 존재 여부를 전수 조사하고 없는 경우 단계별로 자동 생성하는 재귀적 생성 로직을 적용함.
1. GitLab CI/CD 설정 설명
fabric-deployment:
stage: deploy
# 2025년 1월자 Azure CLI 이미지 사용
image: <>
tags: [docker]
variables:
SOURCE_DIR: "WORKSPACE"
TARGET_ROOT: ""
DEPLOY_SCRIPT: "Script/deploy.py"
script:
- echo "1. Python Dependency Install"
- pip3 install requests --break-system-packages || true
- echo "2. Check Deploy Files"
- |
COUNT=$(find "$SOURCE_DIR" -name "*.ipynb" | wc -l)
echo "Notebook Count = $COUNT"
if [ "$COUNT" -eq 0 ]; then
echo "❌ No notebooks found in $SOURCE_DIR"
exit 1
fi
- echo "3. Azure Login"
- |
az login --service-principal \
--username "$AZURE_SERVICE_PRINCIPAL_ID" \
--password "$AZURE_SERVICE_PRINCIPAL_PWD" \
--tenant "$AZURE_TENANT_ID" \
--allow-no-subscriptions
- echo "4. Get Fabric Token"
- |
export FABRIC_TOKEN=$(az account get-access-token \
--resource https://api.fabric.microsoft.com \
--query accessToken -o tsv)
echo "FABRIC_TOKEN length: $(echo $FABRIC_TOKEN | wc -c)"
echo "TARGET_WORKSPACE_ID=$DATA_HIGHT_PRD"
- echo "5. Deploy to Fabric (Workspace Root)"
- |
export IFS=$'\n'
# SOURCE_DIR 내부의 모든 .ipynb 파일을 찾아서 루프를 돕니다.
for file_path in $(find "$SOURCE_DIR" -name "*.ipynb"); do
# SOURCE_DIR 이후의 상대 경로만 추출합니다.
# 예: DeployFile/SubFolder/test.ipynb -> SubFolder
REL_DIR=$(dirname "$file_path" | sed "s|^$SOURCE_DIR||" | sed 's|^/||')
# FINAL_FOLDER 결정 로직
# TARGET_ROOT를 비웠으므로, 원본의 하위 폴더 구조만 유지합니다.
# 파일이 SOURCE_DIR 바로 아래 있다면 FINAL_FOLDER는 빈 값이 됩니다.
if [ -z "$REL_DIR" ]; then
FINAL_FOLDER=""
else
FINAL_FOLDER="$REL_DIR"
fi
echo "🚀 Deploying: $file_path"
echo "📍 Destination: Workspace Root/${FINAL_FOLDER:- (Root)}"
python3 "$DEPLOY_SCRIPT" \
--token "$FABRIC_TOKEN" \
--workspace "$DATA_HIGHT_PRD" \
--file "$file_path" \
--folder "$FINAL_FOLDER"
done
allow_failure: false
🚀 Microsoft Fabric 자동 배포 시스템 기술 매뉴얼
본 프로젝트는 로컬 구조 미러링(Structural Mirroring) 전략을 사용하여, 개발자의 로컬 디렉토리 구조를 Microsoft Fabric 워크스페이스에 자동으로 동기화하는 배포 자동화 시스템을 구축합니다.
- GitLab CI/CD 파이프라인 설명 (.gitlab/gitlab-ci.yml)
이 설정 파일은 GitLab Runner(EKS) 환경에서 배포 프로세스가 단계별로 수행되도록 제어하는 “배포 설계도”입니다.
🏗️ 주요 구성 요소
- Base Image: apne2-devops-base-azure-cli:20250124 이미지를 사용하여 Azure CLI 및 Python 환경을 즉시 사용합니다.
- Variables (환경 변수):
- SOURCE_DIR: 배포 대상 파일이 위치한 루트인 WORKSPACE를 지정합니다.
- DEPLOY_SCRIPT: 배포 로직이 담긴 Python 스크립트 경로를 설정합니다.
- DATA_HIGHT_PRD: Protected 변수로 등록된 워크스페이스 ID를 참조합니다.
🛠️ 실행 단계별 상세 설명
- 의존성 설치: requests 라이브러리를 설치하여 Fabric REST API 통신을 준비합니다.
- 파일 무결성 검사: find 명령어로 WORKSPACE 내 노트북(.ipynb) 파일 존재 여부를 확인하여 빈 배포를 방지합니다.
- Azure 인증: 서비스 주체(SP) 정보를 활용해 az login을 수행합니다.
- 권한 획득: Fabric API 전용 액세스 토큰(FABRIC_TOKEN)을 발급받아 환경 변수로 등록합니다.
- 미러링 루프 실행:
- 로컬 파일 경로에서 WORKSPACE를 제외한 상대 경로(REL_DIR)를 추출합니다.
- 추출된 경로는 Fabric 워크스페이스 내의 실제 폴더 경로로 매핑됩니다.
- 파일별로 Python 스크립트를 호출하여 배포를 완료합니다.
2. Python 배포 스크립트 설명
import argparse, requests, base64, os, time
# ================= DEBUG =================
def debug(msg):
print(f"[DEBUG] {msg}")
# ================= HTTP =================
def api_get(url, headers):
r = requests.get(url, headers=headers)
debug(f"GET {url} -> {r.status_code}")
if r.status_code != 200:
print(r.text)
return r
def api_post(url, headers, payload):
r = requests.post(url, headers=headers, json=payload)
debug(f"POST {url} -> {r.status_code}")
if r.status_code not in [200,201,202]:
print(r.text)
return r
# ================= ITEM POLLING =================
def wait_for_item(headers, workspace_id, name, timeout=120):
url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/items"
start = time.time()
while time.time() - start < timeout:
items = api_get(url, headers).json().get("value", [])
for i in items:
if i["displayName"] == name:
print(f"✅ Item visible: {name} -> {i['id']}")
return i["id"]
print(f"⏳ Waiting item visibility: {name}")
time.sleep(3)
print(f"❌ Timeout waiting item: {name}")
return None
# ================= FOLDER APIs =================
def get_all_folders(headers, workspace_id):
url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/folders?recursive=true"
r = api_get(url, headers)
return r.json().get("value", [])
def create_folder(headers, workspace_id, name, parent_id=None):
url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/folders"
payload = {"displayName": name}
if parent_id:
payload["parentFolderId"] = parent_id
r = api_post(url, headers, payload)
if r.status_code in [200,201,202]:
fid = r.json()["id"]
print(f"📁 Folder Created: {name} -> {fid}")
return fid
return None
def ensure_folder_path(headers, workspace_id, path):
"""
Ensure nested folder path exists (TEST_TARGET/test/sub)
"""
folders = get_all_folders(headers, workspace_id)
parts = path.split("/")
parent = None
for p in parts:
found = None
for f in folders:
if f["displayName"] == p and f.get("parentFolderId") == parent:
found = f
break
if not found:
print(f"⚠️ Folder missing, creating: {p}")
fid = create_folder(headers, workspace_id, p, parent)
found = {"id": fid, "displayName": p, "parentFolderId": parent}
folders.append(found)
parent = found["id"]
print(f"✅ Folder Path Resolved: {path} -> {parent}")
return parent
# ================= DEPLOY =================
def deploy(token, workspace_id, notebook_path, folder_path):
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
name = os.path.basename(notebook_path).replace(".ipynb","")
print(f"\n🚀 Deploy Notebook: {name}")
with open(notebook_path,"rb") as f:
content = base64.b64encode(f.read()).decode()
list_url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/items"
items = api_get(list_url, headers).json().get("value", [])
existing = next((i for i in items if i["displayName"] == name), None)
# CREATE OR UPDATE
if existing:
item_id = existing["id"]
print(f"🔄 Update notebook: {name}")
url = f"{list_url}/{item_id}/updateDefinition"
payload = {"definition":{"parts":[{"path":"notebook-content.ipynb","payload":content,"payloadType":"InlineBase64"}]}}
api_post(url, headers, payload)
else:
print(f"✨ Create notebook: {name}")
payload = {
"displayName": name,
"type": "Notebook",
"definition": {"format":"ipynb","parts":[{"path":"notebook-content.ipynb","payload":content,"payloadType":"InlineBase64"}]}
}
api_post(list_url, headers, payload)
item_id = wait_for_item(headers, workspace_id, name)
if not item_id:
print("❌ item_id missing")
return
print(f"✅ Deploy OK: {item_id}")
# ENSURE FOLDER TREE
folder_id = ensure_folder_path(headers, workspace_id, folder_path)
# MOVE
move_url = f"{list_url}/{item_id}/move"
payload = {"targetFolderId": folder_id}
api_post(move_url, headers, payload)
print(f"✅ Move Success -> {folder_path}")
# ================= MAIN =================
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--token", required=True)
parser.add_argument("--workspace", required=True)
parser.add_argument("--file", required=True)
parser.add_argument("--folder", required=True)
args = parser.parse_args()
debug(f"Token length={len(args.token)}")
debug(f"Workspace={args.workspace}")
debug(f"File={args.file}")
debug(f"Folder={args.folder}")
deploy(args.token, args.workspace, args.file, args.folder)
이 스크립트는 Microsoft Fabric REST API와 직접 통신하여 아이템 생성 및 위치 관리를 수행하는 “배포 엔진”입니다.
核心 함수 기능 설명
① deploy(token, workspace_id, notebook_path, folder_path)
- 파일 처리: 노트북 파일을 읽어 Base64로 인코딩하여 API 페이로드를 생성합니다.
- 스마트 업데이트: 워크스페이스 내 아이템 존재 여부를 확인하여 updateDefinition(수정) 또는 Create Notebook(신규)을 분기 처리합니다.
- 후처리: 배포 성공 후 해당 아이템을 지정된 폴더 경로로 이동시킵니다.
② ensure_folder_path(headers, workspace_id, path)
- 미러링 핵심: 로컬의 / 구분자로 된 경로를 분석합니다.
- 재귀적 확인: 상위 폴더부터 하위 폴더까지 순차적으로 존재 여부를 확인하고, 없는 폴더는 자동으로 생성(create_folder)합니다.
- 결과: 최종 목적지 폴더의 ID를 반환하여 아이템 이동을 지원합니다.
③ wait_for_item(headers, workspace_id, name)
비동기 대응: Fabric 아이템 생성은 비동기로 처리되므로, 리스트에 나타날 때까지 최대 120초 동안 Polling을 수행하여 배포 안정성을 확보합니다.
3. 배포 아키텍처 및 전략
💡 주요 배포 전략
- Structural Mirroring (구조적 동기화):
- 로컬 WORKSPACE 하위의 폴더 구조를 Fabric 환경에 1:1로 복제합니다.
- 개발자는 UI 조작 없이 Git 폴더 관리만으로 운영 환경을 제어할 수 있습니다.
- Naming Convention (명명 규칙):
- Fabric API 제약(마침표 . 사용 불가)을 해결하기 위해 로컬 폴더명을 언더바(_)로 강제하여 에러를 원천 차단했습니다.
- Branch-based Isolation (환경 격리):
- stg, prd 등 브랜치와 Protected 변수를 결합하여 브랜치별로 배포 대상 워크스페이스가 자동 결정되도록 설계했습니다.