Git 연계 파일 전송

IT 엔지니어개발자

시작하기

기본 개념

Git으로 관리하지 않는 파일과 결과물을 별도로 전송하기

Git에서는 소스 코드와 변경 이력을 관리하고, 빌드 결과물이나 배포 패키지, 대용량 데이터와 같은 파일은 별도의 전송 흐름으로 관리할 수 있습니다.

Git 작업 이후 생성되거나 준비된 파일을 필요한 장비로 전송하도록 연결하면, 파일의 특성과 활용 환경에 맞춰 관리 흐름을 구성할 수 있습니다.

text
Git Repository
      │
      │ Code Change
      ▼
Build / Processing
      │
      │ Output Files
      ▼
File Transfer
      │
      ▼
Target Device

예를 들어 소스 코드를 변경한 후 빌드 작업이 완료되면 생성된 패키지나 결과 파일을 테스트 서버, 배포 서버, 데이터 처리 장비로 자동 전송할 수 있습니다.

연계 흐름

Git 작업부터 파일 전송까지 자동으로 이어가기

Git 작업을 기준으로 빌드나 파일 처리 작업을 진행하고, 필요한 파일이 준비되면 지정된 조건에 따라 파일 전송을 시작합니다.

전송이 완료되면 대상 장비에서 테스트, 배포, 데이터 처리와 같은 다음 업무를 이어갈 수 있습니다.

text
Git 작업
   │
   ▼
빌드 · 파일 처리
   │
   ▼
결과 파일 생성
   │
   ▼
전송 작업 실행
   │
   ▼
대상 장비 반영
   │
   ▼
다음 작업 진행

분리 효과

소스 코드와 업무 파일을 각각의 방식으로 관리하기

Git과 파일 전송을 함께 활용하면 소스 코드의 변경 관리와 별도 파일의 전송 및 활용을 하나의 업무 흐름 안에서 연결할 수 있습니다.

구분Git파일 전송
관리 대상소스 코드와 변경 이력결과 파일과 업무 파일
주요 역할코드 변경과 버전 관리파일 처리와 대상 장비 반영
실행 시점코드 작업과 이벤트 발생설정한 작업 조건 충족
활용 환경개발과 형상 관리테스트, 배포, 업무 장비

이를 통해 각 파일을 목적에 맞게 관리하면서 필요한 시점에는 Git 작업 이후의 파일 전송을 자동으로 실행할 수 있습니다.

IT 엔지니어

환경 연결

Git 작업과 파일 전송 장비 연결하기

먼저 Git 작업과 파일 전송에 사용할 장비를 하나의 흐름으로 연결합니다.

Git에서 발생하는 작업과 빌드 또는 파일 처리 환경, 결과 파일을 사용할 대상 장비를 연결하면 전체 파일 흐름을 구성할 수 있습니다.

text
┌──────────────┐
│     Git      │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ Build Server │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ File Transfer│
└──────┬───────┘
       │
       ▼
┌──────────────┐
│Target Device │
└──────────────┘

각 장비와 작업을 연결하면 Git 작업 이후 생성되는 파일이 다음 전송 단계까지 이어지는 기본 환경이 준비됩니다.

전송 규칙

파일과 대상, 실행 조건을 하나의 기준으로 설정하기

별도로 관리할 파일과 전송 대상 장비를 지정하고, Git 또는 후속 작업의 어떤 시점에 파일 전송을 시작할지 함께 설정합니다.

전송할 파일의 경로와 종류를 지정하고, 대상 서버 또는 장비를 연결합니다. 이후 코드 변경, 빌드 완료, 결과 파일 생성과 같은 작업 조건을 전송 시작 기준으로 구성할 수 있습니다.

text
Git / Build Event
        │
        ▼
 Start Condition
        │
        ├── Source
        │      └── File / Path
        │
        └── Target
               └── Device
                      │
                      ▼
                 Transfer Run

구성 항목설정 내용
시작 조건Git 작업 또는 빌드 완료 등 전송을 시작할 기준
Source결과 파일과 파일 경로
Filter전송할 파일 이름과 확장자 조건
Target파일을 사용할 서버 또는 장비
Transfer설정된 조건에 따라 실행할 파일 전송

이렇게 구성하면 어떤 작업 이후 어떤 파일을 어느 장비로 전송할지를 하나의 실행 기준으로 관리할 수 있습니다.

자동 흐름

Git 작업 이후 파일을 지정된 장비로 자동 전송하기

앞에서 구성한 장비와 전송 규칙을 기준으로 Git 작업부터 파일 전송까지 자동 흐름을 완성합니다.

Git 작업이 발생하면 연결된 빌드 또는 처리 작업이 진행되고, 설정한 조건에 따라 준비된 파일을 지정된 장비로 전송합니다.

text
Git Push
   │
   ▼
Build
   │
   ▼
Output Ready
   │
   ├──────────────┐
   │              │
   ▼              ▼
Test Server   Deploy Server
   │              │
   └──────┬───────┘
          ▼
       Complete

하나의 결과 파일을 여러 테스트 또는 배포 환경으로 전송하도록 구성하면, Git 작업 이후 필요한 업무 환경까지 파일을 자동으로 연결할 수 있습니다.

결과 관리

전송 상태와 재처리 흐름을 함께 관리하기

실행된 파일 전송 작업은 Runs와 실행 상세 정보를 통해 확인합니다.

Git 작업을 기준으로 어떤 전송 작업이 실행되었는지 확인하고, 대상 장비별 처리 상태와 전송된 파일 정보를 함께 관리할 수 있습니다.

text
Git Workflow
      │
      ▼
 Transfer Run
      │
 ┌────┼───────┐
 ▼    ▼       ▼
Files Status Progress
      │
      ▼
 Result Review
      │
 ┌────┴─────────────┐
 ▼                  ▼
Completed      Check Required
                    │
                    ▼
              Run Details
                    │
                    ▼
               Condition Check
                    │
                    ▼
                  Retry
                    │
                    ▼
                 Complete

주요 확인 항목은 다음과 같습니다.

확인 항목확인 내용
Trigger파일 전송을 시작한 Git 또는 후속 작업
Source전송한 파일과 파일 경로
Target파일을 사용할 대상 장비
Progress전송 진행 상태
Status실행 상태와 처리 결과
Files처리된 파일 수와 용량
Audit Log작업별 실행 기록과 상세 정보

실행 결과에서 추가 확인이 필요한 경우에는 해당 Run의 상세 정보와 Audit Log를 통해 파일 준비 상태, 전송 경로, 대상 장비의 연결 상태를 확인한 후 필요한 작업을 다시 실행할 수 있습니다.

개발자

빌드 산출물을 여러 배포 대상에 보내고 전송 결과를 파이프라인 종료 코드로 반영하기

연동 준비

공통 호출 코드와 CI 자격 준비하기

import os
import requests

BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com").rstrip("/")
TOKEN = os.environ["INNORIX_ACCESS_TOKEN"]
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")   # optional; falls back to the current workspace

STATUS_COMPLETE = 2
TERMINAL = {2, 4, 5, 9, 99}          # complete / error / cancelled / partial / failed
NOT_SUCCEEDED = {4, 5, 9, 99}


def api(method, path, body=None, params=None):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {TOKEN}",
    }

    if WORKSPACE_ID:
        headers["x-workspace-id"] = WORKSPACE_ID

    response = requests.request(
        method, BASE_URL + path,
        headers=headers, json=body, params=params, timeout=30,
    )

    payload = response.json() if response.content else {}

    if not response.ok:
        raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")

    return payload.get("data")


def is_terminal(detail):
    return detail.get("isTerminal", detail.get("status") in TERMINAL)

전송 상태는 아래 값으로 판단합니다. 종료 상태는 다섯 개이고 성공에 해당하는 값은 완료(2)입니다.

상태 값의미종료
2완료
4오류
5취소
9부분 완료
99실패
1 · 6 · 12 · 13시작·전송 중·동기화 중·수신 중아니오

CI에서 호출한다면 토큰을 파이프라인 시크릿으로 주입합니다. 저장소에 커밋하지 않습니다.

yaml
# GitHub Actions example
env:
  INNORIX_BASE_URL: https://app.innorix.com
  INNORIX_ACCESS_TOKEN: ${{ secrets.INNORIX_ACCESS_TOKEN }}

산출물 경로 설계

커밋이나 태그를 경로에 담아 배포 이력 남기기

어느 코드에서 나온 산출물인지 경로에 기록합니다. 버전 폴더를 따로 두면 이전 산출물이 남아 있어 문제가 생겼을 때 되돌리기 쉽습니다.

import os
import subprocess


def git_ref():
    try:
        sha = subprocess.check_output(
            ["git", "rev-parse", "--short", "HEAD"], text=True).strip()
    except (subprocess.CalledProcessError, FileNotFoundError):
        sha = "unknown"

    return os.getenv("GIT_TAG") or sha


def target_path(base, ref):
    return f"{base}/{ref}"

한 경로에 덮어쓰면 롤백할 대상이 없습니다.

산출물 전송

빌드 결과 파일을 대상 장비로 보내기

파일을 보낼 때는 sourceItemisDir: false로 명시합니다. sourcePaths는 모든 경로를 폴더로 취급하므로, 산출물 파일을 넣으면 서버가 폴더로 스캔하려 해 느려지거나 시간이 초과됩니다.

def deploy_artifact(source, target, artifact_path, base):
    ref = git_ref()

    transfer = api("POST", "/api/transfers/manual", {
        "sourceDevice": source,
        "targetDevice": target,
        "targetPath": target_path(base, ref),
        "sourceItem": [{
            "path": artifact_path,
            "isDir": False,
            "isFolder": False,
            "fileSize": os.path.getsize(artifact_path),
        }],
        "sendAllFolder": False,
        "transferOptions": {"target-action": "overwrite"},
    })

    return transfer["monitorId"], target_path(base, ref)

빌드 디렉터리를 폴더 단위로 보낼 때는 sourcePathssendAllFolder: True를 씁니다.

api("POST", "/api/transfers/manual", {
    "sourceDevice": source,
    "targetDevice": target,
    "targetPath": target_path(base, ref),
    "sourcePaths": ["/build/output"],
    "sendAllFolder": True,
    "transferOptions": {"target-action": "overwrite"},
})

같은 참조로 다시 빌드해 올리는 경우가 대부분이므로 overwrite를 씁니다. numbering을 쓰면 재배포할 때마다 사본이 쌓입니다.

여러 대상 배포

하나의 산출물을 여러 환경으로 보내기

하나의 전송은 대상 장비 하나를 다룹니다. 테스트와 스테이징으로 함께 보내려면 전송을 나눠 만듭니다.

TARGETS = [
    ("device-test-01", "/deploy/app"),
    ("device-stage-01", "/deploy/app"),
]

transfers = {
    target: deploy_artifact("device-build-01", target,
                            "/build/output/app.tar.gz", base)[0]
    for target, base in TARGETS
}
def wait(monitor_id, timeout=3600, interval=3):
    deadline = time.time() + timeout

    while time.time() < deadline:
        detail = api("GET", f"/api/transfers/{monitor_id}")

        if is_terminal(detail):
            return detail

        time.sleep(interval)

    raise TimeoutError(monitor_id)


def failed_files(monitor_id):
    result = api("GET", f"/api/transfers/{monitor_id}/files", params={
        "state": "any", "size": 500,
    }) or {}

    return [r for r in (result.get("children") or [])
            if r.get("status") in NOT_SUCCEEDED]


def retry_failed(monitor_id):
    rows = failed_files(monitor_id)

    if not rows:
        return 0

    api("POST", f"/api/transfers/{monitor_id}/retry", {
        "filesRetry": [
            {"filePath": r["sourceFilePath"], "isDir": bool(r.get("isFolder"))}
            for r in rows
        ]
    })

    return len(rows)
results = {}

for target, monitor_id in transfers.items():
    results[target] = wait(monitor_id, timeout=3600)

failed = [t for t, d in results.items() if d["status"] != STATUS_COMPLETE]

한 대상이 실패해도 나머지를 계속 확인해야 어디까지 반영됐는지 알 수 있습니다. 첫 실패에서 멈추면 재배포 범위를 정할 수 없습니다.

대용량 산출물 전송

대용량 산출물의 처리량을 속도·동시성 옵션으로 조절하기

컨테이너 이미지나 데이터셋처럼 크기가 큰 산출물은 처리량 옵션으로 전송 속도를 조절합니다.

api("POST", "/api/transfers/manual", {
    "sourceDevice": source,
    "targetDevice": target,
    "targetPath": target_path(base, ref),
    "sourcePaths": ["/build/output"],
    "sendAllFolder": True,
    "transferOptions": {
        "target-action": "overwrite",
        "networkLevel": 3,           # throughput priority level
        "concurrentTransfers": 8,    # concurrent transfers
    },
})

CI 러너와 배포 대상이 같은 회선을 쓰는 환경이라면 속도 제한을 걸어 다른 작업에 영향이 가지 않게 합니다.

"transferOptions": {
  "target-action": "overwrite",
  "limitRate": 51200
}

limitRate의 단위는 KB/s입니다.

전송 결과로 파이프라인 종료 코드 내기

CI는 종료 코드로 성공 여부를 판단합니다. 전송이 실패하면 파이프라인도 실패해야 합니다.

import sys

if __name__ == "__main__":
    monitor_id, path = deploy_artifact(
        os.environ["BUILD_DEVICE"],
        os.environ["TARGET_DEVICE"],
        os.environ["ARTIFACT_PATH"],
        os.environ["DEPLOY_BASE"],
    )

    detail = wait(monitor_id)

    if detail["status"] != STATUS_COMPLETE:
        for row in failed_files(monitor_id)[:10]:
            print(row["sourceFilePath"], row.get("errorCode"), file=sys.stderr)

        print(f"retried {retry_failed(monitor_id)} files", file=sys.stderr)
        sys.exit(1)

    print(f"deployed: {path}")

monitorId와 커밋 정보를 짝지어 기록하기

monitorId와 커밋 정보를 함께 남기면 배포 이력과 코드 이력이 연결됩니다.

record = {
    "monitorId": monitor_id,
    "commit": subprocess.check_output(
        ["git", "rev-parse", "HEAD"], text=True).strip(),
    "branch": os.getenv("GIT_BRANCH"),
    "buildNumber": os.getenv("BUILD_NUMBER"),
    "targetPath": path,
}

db.insert("deployments", record)
기록 항목내용
monitorId전송 식별자
commit · branch산출물이 나온 코드 지점
buildNumberCI 실행 번호
targetPath대상 장비의 반영 경로

배포에 문제가 생겼을 때 monitorId로 이력을 찾고, 거기서 커밋까지 거슬러 올라갈 수 있습니다.

최근 배포 이력은 기간으로 조회합니다.

from datetime import datetime, timedelta, timezone


def paginate(path, params=None, limit=200, max_pages=50):
    query = dict(params or {})
    query["limit"] = limit
    cursor = None

    for _ in range(max_pages):
        if cursor:
            query["cursor"] = cursor

        result = api("GET", path, params=query) or {}

        for record in result.get("data") or []:
            yield record

        pagination = result.get("pagination") or {}

        if not pagination.get("hasMore"):
            return

        cursor = pagination.get("nextCursor")

        if not cursor:
            return


end = datetime.now(timezone.utc)
fmt = "%Y-%m-%dT%H:%M:%SZ"

for row in paginate("/api/transfer-history", params={
    "startDate": (end - timedelta(days=7)).strftime(fmt),
    "endDate": end.strftime(fmt),
}):
    print(row["monitorId"], row.get("statusName"),
          row.get("targetDeviceName"), row.get("startDate"))
확인 항목확인 내용
산출물전송한 파일과 경로
참조커밋 또는 태그
대상반영된 장비와 경로
상태대상별 성공과 실패
실패 파일파일별 오류와 재전송 결과