Legacy Engineering Note · 기존 기술 블로그에서 선별 이관한 기록입니다. 작성 당시 환경과 버전을 기준으로 합니다.
Microsoft Fabric CI/CD: 로컬 디렉토리 구조 기반 미러링 배포 전략 가이드
1. 레포지토리 전체 구조
txt(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 설정 설명
yamlfabric-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 배포 스크립트 설명
pythonimport 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 변수를 결합하여 브랜치별로 배포 대상 워크스페이스가 자동 결정되도록 설계했습니다.