🚀 요약

SUMMARY

파이프라인 체크아웃 중 git-lfs가 Jenkins가 쥔 SCM 토큰을 넘겨받지 못해 LFS API에서 “Bad credentials”로 막혀 체크아웃이 실패했다. Jenkins 이미지에 git-lfs를 설치하고 git lfs install --system으로 필터를 등록한 뒤, 체크아웃 단계에서는 자동 smudge를 끄고 인증이 붙은 명시적 pull(GitLFSPull 확장 또는 GIT_LFS_SKIP_SMUDGE=1 + git lfs pull)로 LFS 객체를 당기도록 바꿨다.

⚙️ 환경

  • Jenkins: 2.555.1 (jdk21 기반 커스텀 이미지)
  • SCM: GitHub (프라이빗 리포, 토큰 인증)
  • 애플리케이션 리포에 LFS로 관리되는 대용량 파일 포함, GitOps 리포는 LFS 미사용

💬 이슈

빌드 잡이 소스를 체크아웃하다 LFS 객체를 받는 단계에서 실패했다. 로그를 파보면 LFS API 응답이 Bad credentials였다.

이상했던 건, git 자체 체크아웃(clone/fetch)은 멀쩡히 됐다는 점이다. 토큰은 분명히 잘 먹고 있는데 LFS만 인증에서 튕겼다. 원인은 git-lfs가 별도 프로세스라는 데 있었다.

git이 LFS로 추적되는 파일을 체크아웃할 때는 smudge 필터가 돈다. 이 필터가 git-lfs를 별도 프로세스로 띄워 LFS API에서 실제 파일 본체를 내려받는데, 이 프로세스는 Jenkins가 git remote helper에 꽂아준 자격증명을 자동으로 물려받지 못한다. 그러니 git 본체는 토큰으로 인증되는데 git-lfs는 무인증(또는 엉뚱한 자격증명)으로 LFS API를 두드리다 “Bad credentials”를 맞고, 체크아웃이 통째로 깨지는 구조였다.

여기에 git-lfs가 필터로 등록돼 있지 않으면 LFS 포인터 파일(실제 바이너리가 아니라 해시만 든 텍스트)이 그대로 워크스페이스에 남아 뒤 단계의 빌드가 엉뚱하게 깨진다. 정리하면 두 가지를 같이 손봐야 했다.

  • Jenkins 이미지에 git-lfs가 설치되고 필터가 등록돼 있을 것
  • 체크아웃 시 git-lfs가 SCM 토큰으로 인증할 것

🧗 해결

1. Jenkins 이미지에 git-lfs 설치와 system 등록

먼저 파이프라인이 도는 Jenkins 이미지에 git-lfs를 넣고, 필터를 system-wide로 등록했다. --system으로 걸어두면 이 이미지로 도는 모든 잡이 별도 설정 없이 LFS 필터를 쓴다.

# Jenkins 이미지에 git-lfs 설치
RUN apt-get update && apt-get install -y \
    curl gnupg ca-certificates git-lfs \
    && rm -rf /var/lib/apt/lists/*
 
# LFS 필터를 system-wide로 등록해, 이 이미지로 도는 모든 잡이 LFS 객체를 정상 처리하게 한다.
# 다만 인증은 별개 문제다. git-lfs는 별도 프로세스라 SCM 토큰이 자동 전달되지 않아,
# 파이프라인 쪽에서 자격증명을 연결해주지 않으면 LFS API에서 "Bad credentials"가 난다.
RUN git lfs install --system

git lfs install은 “LFS 파일을 어떻게 다룰지”만 등록할 뿐, “누구 자격증명으로 LFS API에 접속할지”는 해결해주지 않는다. 인증은 파이프라인 단계에서 따로 이어붙여야 했다.

2. 선언형 체크아웃: GitLFSPull 확장과 https

Jenkins Git 플러그인에는 GitLFSPull 확장이 있다. 체크아웃에 이 확장을 넣으면 git-lfs가 SCM 자격증명으로 인증해 LFS 객체를 당긴다. 별도 프로세스인 git-lfs에 토큰을 연결해주는 게 이 확장이 하는 일이다.

checkout([
    $class: 'GitSCM',
    branches: [[name: scmBranch]],
    extensions: [
        [$class: 'CleanBeforeCheckout'],
        [$class: 'LocalBranch', localBranch: scmBranch],
        [$class: 'CloneOption', noTags: true, reference: env.MIRROR_DIR, shallow: false, depth: 0, timeout: 30],
        // LFS 객체를 SCM 자격증명으로 인증해 pull. 이게 없으면
        // smudge가 LFS API에 무인증으로 접근해 "Bad credentials"로 막힌다.
        [$class: 'GitLFSPull']
    ],
    userRemoteConfigs: [[
        credentialsId: env.GIT_CREDENTIAL_ID,
        // https 필수: http로 두면 GitHub가 https로 리다이렉트하며 git-lfs 자격증명 흐름이 깨진다.
        url: "https://github.com/${env.APP_REPO}.git"
    ]]
])

여기서 한 번 더 밟은 지뢰가 리모트 URL 프로토콜이었다. http://로 두면 GitHub가 https://로 리다이렉트하는데, 그 리다이렉트를 타는 과정에서 git-lfs 자격증명 흐름이 깨졌다. 그래서 URL은 처음부터 https://로 박아야 했다. (LFS를 안 쓰는 GitOps 리포에서는 GitLFSPull이 그냥 no-op라 넣어둬도 무해하다.)

3. 셸 기반 체크아웃: skip-smudge 후 명시적 pull

선언형 checkout을 안 쓰고 셸에서 직접 git clone/checkout을 하는 잡도 있었다. 여기서는 자동 smudge를 아예 끄는 쪽으로 갔다. GIT_LFS_SKIP_SMUDGE=1을 걸면 clone·checkout 단계에서 git-lfs가 자동으로 객체를 받으려 들지 않는다(포인터만 받아둔다). 그렇게 실패 지점을 먼저 제거한 뒤, 인증이 확실히 붙은 상태에서 git lfs pull로 한 번에 당겼다.

# GIT_LFS_SKIP_SMUDGE=1: checkout 중 auto-smudge가 LFS를 받다 실패해 checkout이 깨지는 것을 막는다.
# 자격증명은 origin URL(https + 토큰)에 실려 있으므로, 이후 'git lfs pull'이 인증된 채로 객체를 받는다.
rm -rf "${targetDir}"
GIT_LFS_SKIP_SMUDGE=1 git clone ${cloneOptions} \
    https://${GIT_USER}:${GIT_PASS}@github.com/${repo}.git "${targetDir}"
cd "${targetDir}"
git fetch --all --prune --no-tags
GIT_LFS_SKIP_SMUDGE=1 git checkout "${ref}"
git lfs pull

핵심은 두 단계로 쪼갠 것이다. 자동 smudge가 인증 없이 돌다 실패하는 대신, smudge를 꺼서 checkout을 먼저 통과시키고, 자격증명이 실린 origin으로 git lfs pull을 명시적으로 돌린다. ${GIT_USER}/${GIT_PASS}는 자격증명 스토어에서 주입받는 값이라 로그·스크립트에 평문으로 남기지 않는다.

✅ 확인

같은 잡을 다시 돌려 체크아웃이 실패하지 않고 통과하는지 봤다. LFS 단계에서 “Bad credentials”가 사라지고 pull이 정상 종료했다.

워크스페이스의 LFS 파일이 포인터가 아니라 실제 바이너리로 내려왔는지도 확인했다.

# LFS로 추적되는 파일이 실제로 받아졌는지(포인터가 아닌 본체인지) 확인한다.
git lfs ls-files   # 받아진 객체는 앞 상태 표시가 '*' (checkout됨)로 뜬다

git lfs ls-files에서 대상 파일들이 checkout된 상태로 잡히고, 이어지는 빌드가 실제 바이너리를 정상적으로 참조하면 끝이다. 인증이 붙은 뒤로는 실패가 재발하지 않았다.

🔗 참고