Microsoft Fabric CI/CD: 로컬 디렉토리 구조 기반 미러링 배포 전략 가이드


Fabric CI/CD

Fabric CI/CD
  • Category : Microsoft
  • Tag : Fabric


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

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로 관리하여 운영 안정성을 확보했습니다.

🛠️ 배포 프로세스 상세 흐름

  1. 코드 푸시: 개발자가 WORKSPACE/ 내부에 파일을 배치하고 브랜치에 푸시합니다.
  2. 파이프라인 트리거: .gitlab/gitlab-ci.yml이 작동하며 Azure 서비스 주체(SP)를 통해 인증을 수행합니다.
  3. 경로 분석: 배포 스크립트가 파일의 로컬 상대 경로(예: TEST-CICD/01_notebook)를 추출합니다.
  4. 아이템 생성/업데이트: Fabric API를 호출하여 노트북 내용을 생성하거나 기존 파일을 업데이트합니다.
  5. 폴더 미러링 및 이동: 원격지에 동일한 폴더 구조가 없으면 자동으로 생성하고, 생성된 아이템을 해당 폴더로 최종 이동시킵니다.

🌟 이 전략의 장점

  • 관리 편의성: 워크스페이스 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 워크스페이스에 자동으로 동기화하는 배포 자동화 시스템을 구축합니다.

  1. 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를 참조합니다.

🛠️ 실행 단계별 상세 설명

  1. 의존성 설치: requests 라이브러리를 설치하여 Fabric REST API 통신을 준비합니다.
  2. 파일 무결성 검사: find 명령어로 WORKSPACE 내 노트북(.ipynb) 파일 존재 여부를 확인하여 빈 배포를 방지합니다.
  3. Azure 인증: 서비스 주체(SP) 정보를 활용해 az login을 수행합니다.
  4. 권한 획득: Fabric API 전용 액세스 토큰(FABRIC_TOKEN)을 발급받아 환경 변수로 등록합니다.
  5. 미러링 루프 실행:
    • 로컬 파일 경로에서 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. 배포 아키텍처 및 전략

💡 주요 배포 전략

  1. Structural Mirroring (구조적 동기화):
    • 로컬 WORKSPACE 하위의 폴더 구조를 Fabric 환경에 1:1로 복제합니다.
    • 개발자는 UI 조작 없이 Git 폴더 관리만으로 운영 환경을 제어할 수 있습니다.
  2. Naming Convention (명명 규칙):
    • Fabric API 제약(마침표 . 사용 불가)을 해결하기 위해 로컬 폴더명을 언더바(_)로 강제하여 에러를 원천 차단했습니다.
  3. Branch-based Isolation (환경 격리):
    • stg, prd 등 브랜치와 Protected 변수를 결합하여 브랜치별로 배포 대상 워크스페이스가 자동 결정되도록 설계했습니다.

Share this post