GitLab CI/CD에서 Git 서브모듈 사용하기

GitLab CI/CD에서 Git 서브모듈 사용하기

Git 서브모듈을 사용하면 하나의 Git 저장소를 다른 Git 저장소의 하위 디렉터리로 유지할 수 있어요. 다른 저장소를 프로젝트 안으로 클론하고, 커밋은 따로 유지하는 거죠. 이 페이지에서는 GitLab CI/CD 잡에서 서브모듈을 제대로 동작시키는 방법을 다뤄요.

출처: 문서

본문

  • Tier: Free, Premium, Ultimate
  • Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated

.gitmodules 파일 설정하기

Git 서브모듈을 사용할 때 프로젝트에는 .gitmodules라는 파일이 있어야 해요. GitLab CI/CD 잡에서 동작하도록 이 파일을 설정하는 방법은 여러 가지가 있어요.

절대 URL 사용하기

예를 들어 다음 상황이라면 생성된 .gitmodules 설정이 이렇게 생길 수 있어요.

  • 프로젝트가 https://gitlab.com/secret-group/my-project에 있어요.
  • 프로젝트가 https://gitlab.com/group/project에 의존하는데, 이걸 서브모듈로 포함하고 싶어요.
  • 소스를 [email protected]:secret-group/my-project.git 같은 SSH 주소로 체크아웃해요.
[submodule "project"]
  path = project
  url = [email protected]:group/project.git

이 경우 GIT_SUBMODULE_FORCE_HTTPS 변수로 GitLab Runner가 서브모듈을 클론하기 전에 URL을 HTTPS로 변환하도록 지시할 수 있어요.

또는 로컬에서도 HTTPS를 쓴다면 HTTPS URL을 설정할 수도 있어요.

[submodule "project"]
  path = project
  url = https://gitlab.com/group/project.git

이 경우 추가 변수를 설정할 필요는 없지만, 로컬에서 클론하려면 개인 액세스 토큰을 사용해야 해요.

상대 URL 사용하기

상대 URL을 쓰면 포크(fork) 워크플로에서 서브모듈이 잘못 해석될 수 있어요. 프로젝트에 포크가 생길 것으로 예상한다면 절대 URL을 쓰는 게 좋아요.

서브모듈이 같은 GitLab 서버에 있다면 .gitmodules 파일에서 상대 URL도 사용할 수 있어요.

[submodule "project"]
  path = project
  url = ../../project.git

위 설정은 Git이 소스를 클론할 때 사용할 URL을 자동으로 추론하도록 지시해요. 모든 CI/CD 잡에서 HTTPS로 클론하고, 로컬에서는 계속 SSH로 클론할 수 있어요.

같은 GitLab 서버에 없는 서브모듈은 항상 전체 URL을 사용해요.

[submodule "project-x"]
  path = project-x
  url = https://gitserver.com/group/project-x.git

CI/CD 잡에서 Git 서브모듈 사용하기

사전 요건:

  • 파이프라인 잡에서 서브모듈을 클론할 때 CI_JOB_TOKEN을 사용한다면 서브모듈 저장소의 코드를 가져오려면 해당 저장소에서 Reporter, Developer, Maintainer, Owner 역할이 있어야 해요.
  • 업스트림 서브모듈 프로젝트에서 CI/CD 잡 토큰 접근이 제대로 설정되어 있어야 해요.

CI/CD 잡에서 서브모듈을 제대로 동작시키려면:

  1. GIT_SUBMODULE_STRATEGY 변수를 normal 또는 recursive로 설정해서 러너가 잡 전에 서브모듈을 가져오도록 지시할 수 있어요.
variables:
  GIT_SUBMODULE_STRATEGY: recursive
  1. 같은 GitLab 서버에 있고 Git 또는 SSH URL로 설정된 서브모듈은 GIT_SUBMODULE_FORCE_HTTPS 변수를 반드시 설정해요.
  2. GIT_SUBMODULE_DEPTH로 서브모듈의 클론 깊이를 GIT_DEPTH 변수와 별개로 설정해요.
variables:
  GIT_SUBMODULE_DEPTH: 1
  1. GIT_SUBMODULE_PATHS로 특정 서브모듈을 필터링하거나 제외해서 어떤 서브모듈을 동기화할지 제어할 수 있어요.
variables:
  GIT_SUBMODULE_PATHS: submoduleA submoduleB
  1. GIT_SUBMODULE_UPDATE_FLAGS로 고급 체크아웃 동작을 제어하는 추가 플래그를 제공할 수 있어요.
variables:
  GIT_SUBMODULE_STRATEGY: recursive
  GIT_SUBMODULE_UPDATE_FLAGS: --jobs 4

중첩 서브모듈 체크아웃하기

  • GitLab Runner 18.6에서 도입됐어요.

중첩 서브모듈(nested submodule)은 자기 자신의 서브모듈을 포함하는 서브모듈이에요. 저장소의 모든 서브모듈 대신 특정 중첩 서브모듈만 체크아웃해야 할 수도 있어요.

GitLab Runner 18.6부터는 빌드 디렉터리를 오염시키지 않도록 Git 설정(자격 증명 포함)을 별도 파일로 외부화해요. 서브모듈 디렉터리로 들어가 Git 명령을 실행하면 GIT_SUBMODULE_STRATEGY에 따라 모든 서브모듈에 대해 메인 저장소의 설정이 자동으로 상속돼요.

  • GIT_SUBMODULE_STRATEGY: normal을 사용하면 최상위 서브모듈이 초기화돼요.
  • GIT_SUBMODULE_STRATEGY: recursive를 사용하면 모든 중첩 서브모듈이 초기화돼요.

중첩 서브모듈의 일부만 체크아웃하려면:

  1. GIT_SUBMODULE_STRATEGYnormal로 설정해요.
variables:
  GIT_SUBMODULE_STRATEGY: normal
  1. 잡에서 외부화된 설정을 명시적으로 전달해요.
my-job:
  script:
    - git submodule sync
    - git submodule update --init
    - cd path/to/submodule-with-nested-submodule
    - git -c "include.path=$(git -C $CI_PROJECT_DIR config include.path)" submodule update --init nested-submodule

git -C $CI_PROJECT_DIR config include.path 명령은 메인 저장소에서 외부화된 설정 파일의 경로를 가져와요. 이렇게 하면 중첩 서브모듈을 체크아웃할 때 자격 증명과 기타 설정을 사용할 수 있어요.

다른 GitLab 인스턴스의 서브모듈 사용하기

서브모듈이 메인 프로젝트와 다른 GitLab 인스턴스에 호스팅되어 있다면, 현재 인스턴스의 CI_JOB_TOKEN으로는 외부 인스턴스에 인증할 수 없어요. 외부 인스턴스에서 만든 토큰으로 인증해야 해요.

외부 GitLab 인스턴스에 인증하는 주요 방법은 두 가지예요.

  • URL 재작성(URL rewriting): Git URL에 인증 자격 증명을 포함하도록 수정해요.
  • Git credential helper: Git이 필요할 때 자동으로 사용하는 자격 증명을 저장해요.

선택하는 인증 방법은 GitLab Runner executor 유형에 따라 달라져요.

  • 컨테이너화된 executor(Docker 또는 Kubernetes): 각 잡이 격리된 컨테이너에서 실행되므로, 전역 Git 설정 변경은 현재 잡에만 영향을 주고 컨테이너가 파괴될 때 자동으로 정리돼요.
  • Shell executor: 잡이 러너 호스트 시스템에서 직접 실행되므로 전역 Git 설정 변경이 잡 사이에 유지돼요. 서로 다른 잡이 다른 자격 증명을 사용하면 인증 충돌이 발생할 수 있어요.

Shell executor를 사용할 때는 인증 자격 증명을 유지시키는 git config --global 명령을 피하세요. 이런 설정은 잡 사이에 계속 남아 있어서, 서로 다른 잡이 다른 자격 증명을 사용하면 인증 실패나 보안 문제를 일으킬 수 있어요.

다음 토큰 유형 중 하나를 사용할 수 있어요.

URL 재작성으로 인증 설정하기

URL 재작성으로 인증을 설정하는 방법은 이래요.

  1. .gitmodules 파일에서 서브모듈에 절대 HTTPS URL을 사용해요.
[submodule "external-project"]
  path = external-project
  url = https://other-gitlab.example.com/group/project.git
  1. 외부 GitLab 인스턴스에서 read_repository 범위의 토큰을 만들어요.
  2. 메인 프로젝트에서 이 토큰을 마스킹된 CI/CD 변수로 추가해요. 예를 들어 EXTERNAL_GITLAB_TOKEN이라고 이름을 지어요.
  3. .gitlab-ci.yml 파일에서 executor 유형에 따라 인증을 설정해요. 컨테이너화된 executor(Docker/Kubernetes)용: 쉘 executor용: 컨테이너화된 executor의 모든 잡에 대해 전역으로 인증을 설정할 때만: <username>은 토큰과 연결된 GitLab 사용자 이름으로 바꿔요.
variables:
  GIT_SUBMODULE_STRATEGY: recursive

my-job:
  before_script:
    - git config --global url."https://<username>:${EXTERNAL_GITLAB_TOKEN}@other-gitlab.example.com/".insteadOf "https://other-gitlab.example.com/"
  script:
    - echo "Submodules are fetched with authentication"
    - ls -la external-project/
variables:
  GIT_SUBMODULE_STRATEGY: none

my-job:
  before_script:
    - parent_include_path=$(git -C $CI_PROJECT_DIR config include.path)
    - git -c "include.path=${parent_include_path}" -c "url.https://<username>:${EXTERNAL_GITLAB_TOKEN}@other-gitlab.example.com/.insteadOf=https://other-gitlab.example.com/" submodule update --init --recursive --force
  script:
    - echo "Submodules are fetched with authentication"
    - ls -la external-project/
hooks:
  pre_get_sources_script:
    - git config --global url."https://<username>:${EXTERNAL_GITLAB_TOKEN}@other-gitlab.example.com/".insteadOf "https://other-gitlab.example.com/"

Git credential helper로 인증 설정하기

Git credential helper로 인증을 설정하는 방법은 이래요.

  1. 외부 GitLab 인스턴스에서 read_repository 범위의 토큰을 만들어요.
  2. 메인 프로젝트에서 이 토큰을 마스킹된 CI/CD 변수로 추가해요. 예를 들어 EXTERNAL_GITLAB_TOKEN이라고 이름을 지어요.
  3. .gitlab-ci.yml 파일에서 executor 유형에 따라 credential helper를 설정해요. 컨테이너화된 executor(Docker/Kubernetes)용: 쉘 executor용: <username>은 토큰과 연결된 GitLab 사용자 이름으로 바꿔요.
my-job:
  before_script:
    - git config --global credential.helper store
    - echo "https://<username>:${EXTERNAL_GITLAB_TOKEN}@other-gitlab.example.com" >> ~/.git-credentials
  script:
    - echo "Submodules are fetched with authentication"
    - ls -la external-project/
my-job:
  before_script:
    - TEMP_CREDS=$(mktemp)
    - echo "https://<username>:${EXTERNAL_GITLAB_TOKEN}@other-gitlab.example.com" > "$TEMP_CREDS"
    - git config credential.helper "store --file=$TEMP_CREDS"
    - trap "rm -f $TEMP_CREDS" EXIT
  script:
    - echo "Submodules are fetched with authentication"
    - ls -la external-project/

문제 해결 (Troubleshooting)

.gitmodules 파일을 찾을 수 없어요

.gitmodules 파일은 보통 숨김 파일이라 찾기 어려울 수 있어요. 사용 중인 OS 문서에서 숨김 파일을 찾고 표시하는 방법을 확인할 수 있어요.

.gitmodules 파일이 없다면 서브모듈 설정이 git config 파일에 있을 가능성이 있어요.

오류: fatal: run_command returned non-zero status

이 오류는 GIT_STRATEGYfetch로 설정되어 있고 서브모듈을 다룰 때 잡에서 발생할 수 있어요.

GIT_STRATEGYclone으로 설정하면 문제를 해결할 수 있어요.

오류: fatal: could not read Username for 'https://gitlab.com': No such device or address

CI/CD 잡이 서브모듈로 clone, fetch 또는 다른 Git 작업을 시도할 때 이 오류가 발생할 수 있어요. 이 문제는 다음 상황에서 발생해요.

  • 서브모듈 디렉터리 안에서(git fetch 같은) Git 명령을 실행할 때. 외부화된 Git 설정이 모든 Git 작업에 자동으로 상속되지 않을 수 있기 때문이에요.
  • 중첩 서브모듈을 다룰 때. GitLab Runner 18.6부터 Git 설정을 외부화하는데, 서브모듈이 이를 자동으로 상속하지 못할 수 있기 때문이에요.
  • https://gitlab.com을 참조하는 서브모듈과 함께 GitLab 호스팅 러너를 사용할 때. CI_SERVER_FQDNgitlab.com과 다르기 때문이에요. GitLab Runner는 초기 체크아웃 시 Git URL 치환을 자동으로 수행하지만, 서브모듈 디렉터리의 이후 Git 작업에는 적용되지 않을 수 있어요.

이 문제를 해결하려면:

  • 중첩 서브모듈의 경우 중첩 서브모듈 체크아웃을 참고해요.
  • 서브모듈 디렉터리의 Git 작업은 외부화된 설정을 명시적으로 전달해요.
my-job:
  script:
    - cd path/to/submodule
    - git -c "include.path=$(git -C $CI_PROJECT_DIR config include.path)" fetch origin
my-job:
  script:
    - cd path/to/submodule
    - git -c "include.path=$(git -C $CI_PROJECT_DIR config include.path)" -c "url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_FQDN}/.insteadOf=https://gitlab.com/" fetch origin

더 알아보기

서브모듈과 함께 쓰는 러너 변수(GIT_SUBMODULE_STRATEGY, GIT_SUBMODULE_FORCE_HTTPS 등)의 전체 동작은 러너 설정 문서에서 더 자세히 볼 수 있어요. 또 외부 인스턴스 인증이 필요하다면 CI_JOB_TOKEN 접근 제어 설정을 함께 익혀두면 좋아요.