카테고리 보관물: DEVOPS

Resolving GitLab MR Conflicts from Concurrent Merge Requests

개요

이 글은 GitLab MR 충돌이 발생하는 대표적인 상황과 안전한 해결 절차를 정리한 운영 기록입니다. 같은 main에서 갈라진 두 개의 Merge Request가 동일 파일의 인접한 영역을 각각 수정하고 있을 때, 한쪽 MR이 먼저 병합되면 나머지 MR이 갑자기 병합 불가(blocked) 상태로 바뀝니다. 처음에는 문제없이 병합 가능하던 MR이라 원인이 바로 보이지 않는 경우가 많습니다.

이번 사례에서는 두 MR이 같은 설정 파일의 동일 구획에 각각 항목을 추가한 탓에, 먼저 병합된 MR로 인해 뒤에 남은 MR에서 충돌이 드러났습니다. 진단 과정에서 git merge-tree가 충돌을 과소보고하는 함정도 함께 확인했으므로, 재현·진단·해결·검증 순서로 기록합니다.

환경

  • Self-managed GitLab (merge commit 방식, MR 파이프라인의 validate 단계 사용)
  • Git 2.40+ (git merge-tree --write-tree 지원)
  • 대상 파일: 여러 항목을 하나의 목록/딕셔너리에 누적하는 설정 파일 1개 (본문에서는 config-registry.py로 일반화)
  • 브랜치: feature/a, feature/b — 동일 main 커밋에서 분기

단계별 절차

1. 상황 이해: 형제 MR이 같은 영역을 수정

feature/afeature/b는 각각 별도 MR로 열려 있었고, 두 브랜치 모두 같은 설정 파일의 같은 구획(예: 목록의 특정 지점)에 서로 다른 항목을 추가했습니다. 두 MR이 열린 시점에는 각각 main에 대해 병합 가능한 상태였습니다.

feature/a가 먼저 병합되면 main이 갱신되고, 아직 열려 있던 feature/b는 그 갱신된 main 기준으로 다시 3-way 병합을 시도하게 됩니다. 이때 같은 위치에 대한 변경이 겹치면서 feature/b가 충돌·블록 상태가 됩니다.

2. 원인 진단

먼저 원격을 갱신하고 main이 그사이 얼마나 움직였는지, 남은 브랜치가 얼마나 뒤처졌는지 확인합니다.

git fetch origin --prune

# 남은 브랜치가 main 대비 얼마나 뒤처졌는지(behind) 확인
git rev-list --left-right --count origin/main...origin/feature/b
# 출력 예: 3    4   (main-only 3, branch-only 4)

여기서 중요한 함정이 있습니다. git merge-tree로 미리 충돌을 확인하면 “충돌 없음”으로 나오는데도, 실제 git merge에서는 충돌이 발생하는 경우가 있습니다. 따라서 진단은 실제 병합으로 검증해야 합니다.

# merge-tree는 충돌을 과소보고할 수 있음(참고용)
git merge-tree --write-tree origin/main origin/feature/b | grep -i conflict || echo "clean?"

3. 최신 main 병합 및 충돌 해결

남은 브랜치를 최신 main과 다시 맞추기 위해 main을 브랜치로 병합합니다. force-push나 히스토리 재작성 없이 병합 커밋으로 해결하는 편이 안전합니다.

git switch feature/b
git merge origin/main --no-edit
# CONFLICT (content): Merge conflict in config-registry.py

두 브랜치가 모두 “항목 추가”만 했다면, 해결의 원칙은 양쪽 추가분을 모두 보존(union)하는 것입니다. 다만 Git이 공통 boilerplate 줄(닫는 괄호, 공통 키 등)을 기준으로 정렬하면서 서로 다른 항목이 조각조각 뒤섞이는 경우가 있어, 충돌 마커만 기계적으로 지우면 항목이 오염될 수 있습니다. 그래서 마커 제거 대신, main 버전을 기준으로 두고 남은 브랜치가 추가한 블록만 통째로 삽입하는 방식으로 재구성했습니다.

# main 버전(먼저 병합된 항목 포함)을 기준으로 확보
git show origin/main:config-registry.py > /tmp/base.py

# 남은 브랜치가 추가한 블록만 추출해 base의 안정적 anchor 앞에 삽입
#  → 스크립트로 블록을 잘라 붙이면 손으로 마커를 지우는 것보다 안전합니다

트러블슈팅

정상 병합 가능하던 MR이 갑자기 블록됨

현상: 생성 시점에는 병합 가능하던 MR이, 별다른 변경 없이 갑자기 “merge blocked / conflict” 상태가 됨.

원인: 같은 파일의 같은 구획을 수정한 형제 MR이 먼저 병합되어 main이 갱신됨. 남은 MR의 병합 기준(base)이 바뀌면서 충돌이 새로 발생.

해결: 남은 브랜치에 최신 main을 병합(또는 rebase)해 다시 최신 상태로 맞춘 뒤 충돌을 해결하고 push. 병합 커밋 방식이면 rebase·force-push 없이 처리할 수 있습니다.

merge-tree는 clean인데 실제 merge에서 충돌

현상: git merge-tree --write-tree로는 충돌이 안 보였는데 실제 git merge에서 충돌 발생.

원인: merge-tree의 사전 판정이 실제 3-way 병합과 항상 일치하지는 않아, 특정 정렬 상황에서 충돌을 과소보고할 수 있음.

해결: 사전 점검은 참고로만 쓰고, 실제 git merge 결과로 최종 판정합니다. 실제 병합이 충돌을 정확히 드러냅니다.

충돌 마커가 서로 다른 항목을 뒤섞음

현상: 충돌 구간에서 <<<<<<</=======/>>>>>>> 마커가 한 항목의 일부와 다른 항목의 일부를 뒤섞어 놓음.

원인: 두 브랜치가 같은 위치에 각각 완결된 블록을 추가했지만, Git이 공통 boilerplate 줄을 기준으로 hunk를 정렬하면서 블록 경계가 깨짐.

해결: 마커를 손으로 지우지 말고, 한쪽(예: main) 전체를 기준으로 두고 다른 쪽이 추가한 블록만 통째로 삽입해 재구성. 이후 반드시 자동 검증(구문 컴파일, 중복 키 검사, 기대 항목 존재 여부)으로 union이 정확한지 확인합니다.

결과 확인

재구성한 파일이 구조적으로 올바른지 먼저 검증하고, 그다음 브랜치가 실제로 충돌 없이 병합 가능한지 확인했습니다.

# 1) 구문/구조 검증 (예: Python 파일)
python3 -m py_compile config-registry.py && echo "compile OK"

# 2) 병합 커밋 후, 충돌 없음 + 최신 main 포함(up-to-date) 확인
git merge-tree --write-tree origin/main origin/feature/b | grep -i conflict \
  && echo "conflict" || echo "clean"
git merge-base --is-ancestor origin/main origin/feature/b \
  && echo "up-to-date (not behind)"

충돌 마커가 0개이고, 재구성한 파일이 컴파일되며 양쪽 항목이 모두 남아 있고, merge-tree가 clean이면 MR 블록이 해소됩니다. 결국 GitLab MR 충돌의 핵심 예방책은, 같은 파일의 같은 구획을 여러 MR이 동시에 건드리지 않도록 작업을 분리하고, 형제 MR이 먼저 병합되면 남은 MR에 main을 다시 병합해 최신 상태로 유지하는 것입니다.

참고

관련 포스트:

참고 문서: git-merge · git-merge-tree · GitLab: Merge conflicts

Migrating WordPress from a Standalone LAMP Server to Kubernetes with Traefik

개요

WordPress Kubernetes 마이그레이션을 진행하여 레거시 CentOS 7 + Apache + PHP 7.4 단일 서버에서 운영하던 WordPress를 자체 관리형 Kubernetes 클러스터로 이전했습니다. 대상 서버는 두 개 도메인을 서빙하고 있었고, DB는 이미 클러스터 내 MariaDB를 외부 LoadBalancer IP로 접속하고 있어 데이터베이스 자체는 새로 구축할 필요 없이 접속 경로만 전환하면 되는 상태였습니다.

이번 작업에서는 ① 진입점을 Traefik Ingress로 전환 ② MariaDB 접속을 ClusterIP로 전환 ③ 다중 파드 확장을 고려해 Rook CephFS(ReadWriteMany) 볼륨 채택 ④ PHP 7.4(EOL)에서 PHP 8.4로 업그레이드, 이 네 가지를 목표로 설계했습니다. 로컬 환경에 docker/podman이 없어 커스텀 이미지를 클러스터 안에서 직접 빌드해야 했고, 그 과정에서 GitLab Container Registry를 신규로 활성화하는 작업도 함께 진행했습니다.

환경

항목 기존 (192.168.x.x 레거시 서버) 신규 (Kubernetes)
OS / 웹서버 / PHP CentOS 7, Apache 2.4.6 (mod_php, mpm_prefork), PHP 7.4.33 Debian(컨테이너), Apache 2.4.68, PHP 8.4.23
진입점 HAProxy가 TLS 종료 후 백엔드로 프록시 Traefik Ingress (websecure 443)
DB 연결 MariaDB LoadBalancer 외부 IP MariaDB ClusterIP (Service DNS)
스토리지 로컬 디스크 단일 서버 Rook CephFS RWX PVC 5Gi, 파드 2개 공유
메일 발송 로컬 Postfix (direct-send) 이미지 내 Postfix 동일 구성
이미지 GitLab Container Registry + Kaniko 빌드

단계별 절차

1. 소스 서버 조사 및 이관 설계

SSH로 레거시 서버에 직접 접속해 vhost 설정, wp-config.php, 활성 플러그인 목록, PHP 설정을 조사했습니다. DB 접속 정보(DB_HOST)가 이미 클러스터 내 MariaDB LoadBalancer 외부 IP로 되어 있다는 점을 확인했고, HAProxy IP 하나만 신뢰하도록 되어 있던 mod_rpaf 설정은 새 토폴로지에서는 의미가 없어 mod_remoteip + Pod 네트워크 대역으로 대체가 필요하다는 점도 함께 확인했습니다.

2. 스토리지 설계: webroot 전체를 RWX PVC로

WordPress는 FS_METHOD=direct로 동작해 .htaccess나 플러그인/코어 업데이트를 파일시스템에 직접 씁니다. 다중 파드 환경에서 모든 레플리카가 일관된 상태를 보게 하려면 wp-content만이 아니라 /var/www/html 전체를 공유 볼륨에 올려야 한다고 판단했습니다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wordpress-webroot
spec:
  accessModes: [ReadWriteMany]
  storageClassName: rook-cephfs
  resources:
    requests:
      storage: 5Gi

wp-config.php는 이 PVC 밖에 두었습니다. DB 접속 정보 등 비밀이 아닌 값은 ConfigMap으로, DB 비밀번호와 AUTH/SALT 키는 Secret으로 분리해 두 파일을 각각 subPath로 마운트하고, 본체 wp-config.phprequire_once로 Secret 쪽 파일을 불러오는 구조로 구성했습니다.

3. 커스텀 이미지 빌드: GitLab Container Registry + Kaniko

로컬 작업 환경에 docker/podman이 없었고, 클러스터도 이전까지 공개 이미지만 사용해 온 상태라 커스텀 이미지를 빌드할 경로가 필요했습니다. GitLab Container Registry가 비활성 상태였어서 이번에 신규로 활성화했습니다.

FROM wordpress:php8.4-apache

# 원본에 없던 zip 확장 추가
RUN apt-get update && apt-get install -y libzip-dev && docker-php-ext-install zip

# 원본과 동일한 Postfix(direct-send) 구성
RUN echo "postfix postfix/main_mailer_type select Internet Site" | debconf-set-selections && \
    DEBIAN_FRONTEND=noninteractive apt-get install -y postfix

# mod_rpaf 대체
RUN a2enmod remoteip headers rewrite expires
COPY security-headers.conf remoteip.conf /etc/apache2/conf-enabled/
CMD ["/usr/local/bin/start.sh"]

이미지는 로컬 빌드 없이 Kaniko Job으로 클러스터 안에서 빌드하고 바로 Registry에 푸시했습니다.

containers:
  - name: kaniko
    image: gcr.io/kaniko-project/executor:v1.23.2
    args:
      - --dockerfile=/workspace/Dockerfile
      - --context=dir:///workspace/
      - --destination=gitlab.sierracloud.dev:5050/infra/k8s-ops/wordpress:php8.4-2

4. 데이터 이관

PVC를 마운트하는 임시 헬퍼 파드를 띄우고 rsync로 레거시 서버의 /var/www/html을 통째로 옮겼습니다. 총 297MB, 13,822개 파일이었고 컷오버 직전에 증분 동기화를 한 번 더 실행해 다운타임을 최소화했습니다. 미사용 상태로 남아있던 두 번째 WordPress 설치본(운영 DB를 그대로 바라보지만 어떤 vhost에서도 서빙되지 않던 orphan 설치)은 이관 대상에서 제외했습니다.

rsync -az --exclude='wp-config.php' \
  -e 'ssh -i <migration-key>' \
  root@192.168.x.x:/var/www/html/ /mnt/webroot/

트러블슈팅

GitLab Container Registry 활성화 중 nginx 전체 다운

현상: gitlab.rbregistry_external_url을 추가하고 gitlab-ctl reconfigure를 실행하자 nginx가 기동에 실패했고, Registry뿐 아니라 GitLab 웹 UI 전체가 접속 불가 상태가 되었습니다.

원인: registry_nginx가 메인 사이트에 적용해 둔 커스텀 인증서 경로를 상속받지 않고, 존재하지 않는 기본 경로(/etc/gitlab/ssl/<fqdn>.crt)를 찾다가 nginx 설정 검증 자체가 실패했습니다.

해결: registry_nginx['ssl_certificate']/['ssl_certificate_key']를 메인 사이트와 동일한 인증서 경로로 명시적으로 지정한 뒤 재실행해 즉시 복구했습니다.

registry_external_url 'https://gitlab.sierracloud.dev:5050'
gitlab_rails['registry_enabled'] = true
registry_nginx['ssl_certificate'] = "/data/cert/sierracloud.dev/fullchain.pem"
registry_nginx['ssl_certificate_key'] = "/data/cert/sierracloud.dev/privkey.pem"

Kaniko 빌드 컨텍스트의 dangling symlink

현상: Dockerfile과 부속 설정 파일을 ConfigMap으로 만들어 Kaniko 빌드 컨텍스트로 바로 마운트했더니, COPY 단계에서 cannot operate on dangling symlink 에러로 빌드가 실패했습니다.

원인: Kubernetes ConfigMap 볼륨은 각 파일을 실제 파일이 아니라 ..data/<file>을 가리키는 심볼릭 링크로 마운트합니다. Kaniko의 COPY가 이 링크를 그대로 이미지에 복사하면서, 이미지 안에서는 가리키는 대상이 없는 깨진 링크가 되어버렸습니다.

해결: initContainer에서 cp -rL로 심볼릭 링크를 실제 파일 내용으로 역참조 복사한 emptyDir을 만들고, 이 디렉터리를 Kaniko의 빌드 컨텍스트로 사용하도록 변경했습니다.

initContainers:
  - name: prepare-context
    image: busybox:1.36
    command: ["sh", "-c", "cp -rL /configmap-src/. /workspace/"]

Really Simple Security가 리버스 프록시 뒤에서 무한 리다이렉트를 일으킴

현상: .htaccess의 HTTPS 강제 리다이렉트 규칙이 RewriteCond %{HTTPS} !=on 조건을 쓰고 있었는데, 이관 후에는 모든 요청이 계속 리다이렉트되어 파드가 readiness probe를 통과하지 못하고 CrashLoopBackOff 상태에 빠졌습니다.

원인: 원본 서버는 Apache가 직접 TLS를 종료했기 때문에 %{HTTPS}가 정확했지만, 이관 후에는 Traefik이 TLS를 종료하고 WordPress 컨테이너에는 평문 HTTP로 전달되어 %{HTTPS}가 항상 off로 평가되었습니다.

해결: 플러그인 소스를 직접 확인해, 리버스 프록시/로드밸런서 뒤에서 지원하는 방식인 RewriteCond %{HTTP:X-Forwarded-Proto} !https로 교체했습니다. wp-config.php에도 HTTP_X_FORWARDED_PROTO를 확인해 $_SERVER['HTTPS']를 보정하는 코드를 추가해 PHP 레벨의 is_ssl() 판단도 함께 맞췄습니다. 프로브 자체는 이 로직과 무관한 정적 파일(/healthz.html)로 분리해 안정성을 확보했습니다.

RewriteCond %{HTTP_USER_AGENT} !lscache_runner [NC]
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteCond %{REQUEST_URI} !^/healthz\.html$
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]

Traefik이 평문 HTTP 구간에서는 X-Forwarded-Proto를 신뢰하지 않음

현상: 위 수정 이후에도, HAProxy가 Traefik의 80(web) 엔트리포인트로 붙는 기존 패턴(다른 내부 서비스들과 동일한 구성)으로 연결하면 여전히 무한 리다이렉트가 재현되었습니다.

원인: Traefik은 보안상 클라이언트가 보낸 X-Forwarded-Proto 값을 그대로 신뢰하지 않고, 자신이 실제로 수신한 연결이 TLS인지 여부로 직접 값을 판단해 덮어씁니다. HAProxy와 Traefik 사이 구간이 평문 HTTP(80)이면 Traefik은 이 값을 항상 http로 기록합니다.

해결: HAProxy의 backend를 Traefik의 443(websecure) 엔트리포인트로 변경했습니다(ssl verify none). Traefik이 이 연결에서 직접 TLS를 종료하므로 X-Forwarded-Proto가 정확히 https로 설정됩니다. 마침 traefik 네임스페이스에 이미 로드되어 있던 *.sierracloud.dev 와일드카드 인증서가 Traefik의 기본 인증서로 쓰이고 있어서, 별도 인증서 작업 없이 그대로 유효한 인증서를 응답받을 수 있었습니다.

결과 확인

이번 WordPress Kubernetes 마이그레이션의 최종 상태를 다음과 같이 확인했습니다.

$ kubectl get pods -n wordpress-system -l app=wordpress
NAME                         READY   STATUS    RESTARTS   AGE
wordpress-xxxxxxxxxx-xxxxx   1/1     Running   0          91m
wordpress-xxxxxxxxxx-yyyyy   1/1     Running   0          91m

두 파드가 동일 PVC(RWX)를 실시간으로 공유하는지도 직접 검증했습니다. 한 파드에서 파일을 쓰고 다른 파드에서 즉시 동일한 내용을 읽어, 별도 볼륨이 아니라 완전히 같은 볼륨임을 확인했습니다.

  • 내부망 DNS(도메인 → Traefik IP 직접) 경로로 로그인 페이지, REST API, 실제 게시글 permalink까지 정상 응답 확인
  • 외부에서는 CDN을 경유해 HAProxy → Traefik → 파드로 이어지는 경로로 동일하게 정상 렌더링 확인
  • HAProxy IP로 CDN을 거치지 않고 직접 접근하면 403이 반환되는 것도 확인 — origin을 직접 노출하지 않도록 걸어둔 기존 접근 제어가 그대로 유지되고 있다는 뜻이라 정상 동작으로 판단했습니다

참고

관련 포스트:

참고 문서: Traefik — EntryPoints Forwarded Headers · Kaniko (GoogleContainerTools) · GitLab Container Registry Administration

gitlab-runner container 생성 및 GitLab 등록

개요

Container 환경에서 gitlab-runner를 생성하고 GitLab에 등록하는 절차를 정리합니다. Docker로 gitlab-runner 컨테이너를 실행한 후, 컨테이너 내부에서 gitlab-runner register 명령으로 GitLab 서버와 연결합니다. 등록 완료 후 CI/CD 파이프라인이 해당 runner에서 실행됩니다.

Container 환경에서 gitlab-runner를 생성하고 GitLab에 등록 하는 절차에 관한 내용 입니다.

gitlab-runner 컨테이너 생성

gitlab-runner 컨테이너는 Docker 소켓(/var/run/docker.sock)을 마운트하여 실행합니다. 이를 통해 gitlab-runner 컨테이너가 호스트의 Docker 데몬에 직접 명령을 전달할 수 있습니다. CI/CD 파이프라인에서 Docker 이미지 빌드나 컨테이너 실행이 필요할 때 이 소켓 마운트가 핵심 역할을 합니다. --restart always 옵션으로 서버 재부팅 시에도 gitlab-runner가 자동으로 기동됩니다.

<admin-user>@<runner-server>:~$ docker run --detach \
> --name gitlab-runner \
> --restart always \
> --volume /srv/gitlab-runner/config:/etc/gitlab-runner \
> --volume /var/run/docker.sock:/var/run/docker.sock \
> gitlab/gitlab-runner:latest
Unable to find image 'gitlab/gitlab-runner:latest' locally
latest: Pulling from gitlab/gitlab-runner
d9802f032d67: Pull complete
d71acd29818d: Pull complete
2df872e9a082: Pull complete
Digest: sha256:c7e23480375fca186743d8fbf6eff3b682da48b70a9d2980ce89863571fb6fa8
Status: Downloaded newer image for gitlab/gitlab-runner:latest
4f0eb91d3bd9cdc008545ab664e5746de3eefff6f92fce380dd4b29d290c8154
<admin-user>@<runner-server>:~$ docker ps -a
CONTAINER ID   IMAGE                         COMMAND                  CREATED              STATUS              PORTS     NAMES
4f0eb91d3bd9   gitlab/gitlab-runner:latest   "/usr/bin/dumb-init …"   About a minute ago   Up About a minute             gitlab-runner

Docker Executor 동작 방식

gitlab-runner의 executor 종류는 Shell, Docker, Kubernetes 등 다양합니다. 이 포스트에서는 Docker executor를 사용합니다. Docker executor는 각 CI/CD Job을 독립된 Docker 컨테이너 안에서 실행하므로, Job 간 의존성 오염 없이 클린한 실행 환경을 보장합니다. --docker-image docker:latest는 Job 실행에 사용할 기본 이미지이며, 개별 Job의 .gitlab-ci.yml에서 image:를 지정하면 이를 덮어쓸 수 있습니다.

Docker-in-Docker(DinD) 방식으로 파이프라인 내부에서 Docker build를 실행하려면 --docker-volumes /var/run/docker.sock:/var/run/docker.sock를 함께 전달하여 호스트 소켓을 컨테이너에 공유합니다. 이 방식은 격리성보다 편의성을 우선할 때 적합합니다.

등록 token 확인

GitLab 프로젝트(또는 그룹)의 Settings > CI/CD > Runners에서 등록에 필요한 token을 확인합니다.

GitLab 프로젝트 Settings에서 gitlab-runner 등록용 token 확인

gitlab-runner GitLab 등록

컨테이너 내부에서 gitlab-runner register 명령으로 GitLab 서버 URL, token, executor, Docker image를 지정하여 runner를 등록합니다.

<admin-user>@<runner-server>:~$ docker container exec -it gitlab-runner bash
root@4f0eb91d3bd9:/# gitlab-runner register -n \
> --url http://x.x.x.x:8081/ \
> --registration-token <gitlab token> \
> --description gitlab-runner \
> --executor docker \
> --docker-image docker:latest \
> --docker-volumes /var/run/docker.sock:/var/run/docker.sock
Runtime platform                                    arch=amd64 os=linux pid=24 revision=374d34fd version=17.6.0
Running in system-mode.

WARNING: Support for registration tokens and runner parameters in the 'register' command has been deprecated in GitLab Runner 15.6 and will be replaced with support for authentication tokens. For more information, see https://docs.gitlab.com/ee/ci/runners/new_creation_workflow
Registering runner... succeeded                     runner=JdXqqyrV
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!

Configuration (with the authentication token) was saved in "/etc/gitlab-runner/config.toml"

등록 확인

GitLab 프로젝트의 Settings > CI/CD > Runners에서 runner가 정상 등록되었는지 확인합니다. 등록된 runner는 초록색 원(●)으로 표시되며, 이름 아래에 runner ID와 마지막 통신 시간이 표시됩니다. runner 상태가 회색 원으로 표시된다면 gitlab-runner 컨테이너가 실행 중이지 않거나 GitLab 서버와의 네트워크 연결에 문제가 있는 것입니다.

등록 완료 후 프로젝트에 .gitlab-ci.yml 파일을 추가하면 GitLab이 자동으로 파이프라인을 트리거하고 등록된 runner에 Job을 할당합니다. runner가 Specific 타입으로 등록된 경우 해당 프로젝트에서만 동작하며, Group 또는 Shared runner로 등록하면 더 넓은 범위에서 재사용할 수 있습니다.

GitLab CI/CD 설정에서 gitlab-runner 등록 완료 확인

참고

참고 문서: GitLab Runner Docker 설치 공식 문서 · GitLab Runner 등록 공식 문서