본문으로 건너뛰기
← Systems Notebook / Ops
MLOps Notes08 / 15

CI/CD에서 GitOps까지: GitLab은 어디까지 책임져야 하는가

GitLab Pipeline의 실행 조건과 실패 차단을 확인하고, 빌드 결과를 Git 변경으로 Argo CD에 넘기는 역할 구분을 설명합니다.

이 글의 배경

HPE 엔지니어의 작업 자료를 바탕으로 정리한 글입니다. 개인 실험 기록이나 HPE 공식 문서와는 구분합니다. 본문의 환경과 버전은 글 작성 당시를 기준으로 합니다.

GitLab에서 파이프라인을 실행한 뒤에는 어떤 이미지가 배포되었고, 누가 배포 상태를 바꿀 수 있는지도 확인해야 합니다. 테스트를 통과해도 배포된 이미지를 알 수 없거나 CI 잡이 관리자용 kubeconfig를 가지고 있다면, 자동화가 맡을 범위와 권한을 더 정리해야 합니다. 운영자가 kubectl edit로 수정한 값이 Git과 다르게 남아 있는 경우에도 실제 상태를 어디에서 관리할지 확인해야 합니다.

CI와 CD를 앞뒤 실행 순서로만 보면 이러한 역할 차이를 놓치기 쉽습니다. CI는 검증된 소프트웨어를 만들고, GitOps CD는 Git에 기록된 원하는 상태를 읽어 배포합니다. 각 과정이 맡는 일을 구분해야 빌드 권한과 배포 권한을 분리하고, 누가 무엇을 배포했는지 다시 설명할 수 있습니다.

이 글에서 다루는 것

  • CI, Continuous Delivery, Continuous Deployment와 GitOps의 차이
  • GitLab Repository, Merge Request, Pipeline, Job, Runner가 맡는 역할
  • .gitlab-ci.yml이 실제 Job으로 실행되는 과정
  • 앱 소스 저장소와 Helm Chart 저장소를 나누는 이유
  • CI가 클러스터를 직접 변경하지 않고 Argo CD에 배포를 넘기는 방법
  • 작은 파이프라인을 직접 실행하고 로그와 아티팩트로 동작을 검증하는 방법

실습 환경과 치환값

이 글의 실습은 GitLab Runner가 등록되어 있고 컨테이너 이미지를 실행할 수 있다고 가정합니다. 아직 Harbor나 Argo CD를 연결하지 않습니다. 먼저 GitLab CI 자체의 실행 모델을 분명히 이해하는 것이 목적입니다.

표시 예시 독자가 바꿀 값
GitLab 주소 https://gitlab.lab.example.com 자신의 GitLab 주소
프로젝트 경로 student-01/web-ci-lab 자신의 네임스페이스와 프로젝트명
기본 브랜치 main 프로젝트 기본 브랜치가 다르면 변경
이미지 태그 후보 ${CI_COMMIT_SHA} GitLab이 실행 중 자동 제공

lab.example.com은 문서 전용 예약 도메인입니다. 실제 조직의 주소, 계정, 토큰을 본문이나 저장소에 복사하지 않습니다.

CI/CD를 한 문장으로 줄이면 무엇을 놓치는가

Continuous Integration

CI는 여러 개발자의 변경을 자주 통합하면서 변경이 합쳐져도 소프트웨어가 계속 작동하는지 자동으로 확인하는 과정입니다. 보통 다음 작업이 포함됩니다.

  1. 소스 형식과 정적 규칙을 검사합니다.
  2. 단위·통합 테스트를 실행합니다.
  3. 애플리케이션 또는 컨테이너 이미지를 빌드합니다.
  4. 취약점과 의존성을 검사합니다.
  5. 재사용할 수 있는 결과물을 레지스트리나 패키지 저장소에 올립니다.

CI가 성공했다는 말은 “이 변경으로 만든 특정 결과물이 정해진 검증을 통과했다”는 뜻이어야 합니다. “어딘가의 클러스터가 바뀌었다”는 뜻이 아닙니다.

Continuous Delivery와 Continuous Deployment

두 용어는 자주 CD로 합쳐 쓰지만 운영 게이트가 다릅니다.

방식 배포 가능한 상태까지 자동화 운영 반영
Continuous Delivery 자동 사람이 승인할 수 있음
Continuous Deployment 자동 검증을 통과하면 자동

어느 방식을 택해도 배포 상태는 추적 가능해야 합니다. 운영 승인을 사람이 하더라도 서버에서 임의 명령을 실행하는 대신, 승인된 Git 변경이 배포로 이어지게 만드는 편이 감사와 롤백에 유리합니다.

GitOps

GitOps는 배포 도구의 이름이 아니라 운영 모델입니다. 핵심은 다음 네 문장으로 설명할 수 있습니다.

  1. 시스템의 원하는 상태를 선언적으로 표현합니다.
  2. 그 선언을 버전 관리되고 불변인 저장소에 기록합니다.
  3. 소프트웨어 에이전트가 원하는 상태를 자동으로 가져옵니다.
  4. 에이전트가 실제 상태와 원하는 상태를 지속적으로 조정합니다.

Argo CD를 사용하는 경우 Git의 Helm Chart와 values가 원하는 상태이고, Kubernetes 리소스가 실제 상태이며, Argo CD가 둘의 차이를 계산해 수렴시키는 에이전트입니다.

GitLab은 정확히 무엇을 담당하는가

GitLab을 “Git 서버” 또는 “CI 도구” 한 단어로만 보면 각 기능의 경계가 흐려집니다.

구성요소 역할 남는 증거
Repository 소스와 선언 파일의 버전 관리 commit, branch, tag
Merge Request 변경 검토와 승인 diff, reviewer, discussion, approval
Pipeline 한 변경을 검증하는 전체 실행 상태, 시작 원인, commit SHA
Job 테스트·빌드처럼 하나의 실행 단위 로그, 종료 코드, 아티팩트
Runner Job을 실제로 수행하는 실행기 실행 환경, executor, Runner 식별자
CI/CD Variables 실행 시 필요한 설정과 자격증명 주입 변수 메타데이터와 보호 범위
Environment 배포 이력과 대상 환경 표현 deployment 기록

GitLab Runner는 GitLab 서버의 일부처럼 보이지만 실제로는 분리된 실행기입니다. Runner가 GitLab에 Job을 요청하고, 해당 Job의 소스를 체크아웃하고, .gitlab-ci.yml에 선언된 명령을 격리된 실행 환경에서 수행한 뒤 결과를 GitLab로 돌려줍니다. 따라서 Runner의 executor, 네트워크 접근, 캐시, 권한이 파이프라인의 보안 경계를 결정합니다.

.gitlab-ci.yml에서 Job이 만들어지는 과정

단계별 흐름 / 01GitLab CI와 Runner의 실행 순서
개발자commit push 또는 Merge Request
GitLabPipeline·Job 관리
01 / 05
코드 변경이 시작점

개발자가 commit을 push하거나 Merge Request를 만들면 GitLab이 pipeline 조건을 확인합니다.

1

개발자 → GitLabpush / Merge Request

단계를 선택하면 자동 재생이 멈춥니다. 선의 번호와 아래 설명을 함께 읽어 주세요.

전체 단계 한눈에 읽기
  1. 코드 변경이 시작점

    개발자가 commit을 push하거나 Merge Request를 만들면 GitLab이 pipeline 조건을 확인합니다.

    • 개발자 → GitLab: push / Merge Request
  2. 실행할 Job 결정

    GitLab은 workflow와 rules를 평가하고 Pipeline과 Job을 생성합니다.

    • GitLab → GitLab: workflow·rules 평가
    • GitLab → GitLab: Pipeline·Job 생성
  3. Runner가 Job을 요청

    Runner가 실행 가능한 Job을 먼저 요청합니다. GitLab은 Job 명세와 단기 Job Token을 반환합니다.

    • GitLab Runner → GitLab: 실행할 Job 요청
    • GitLab → GitLab Runner: Job 명세·단기 Token
  4. 환경을 준비하고 실행

    Runner가 실행 환경과 소스 checkout을 준비하면 그 환경에서 script가 실행됩니다.

    • GitLab Runner → 실행 환경: 환경 준비·checkout
    • 실행 환경 → 실행 환경: script 실행
  5. 결과를 GitLab에 기록

    실행 환경의 로그·종료 코드·아티팩트가 Runner로 돌아오고, Runner가 Job 결과를 GitLab에 업로드합니다.

    • 실행 환경 → GitLab Runner: 로그·종료 코드·아티팩트
    • GitLab Runner → GitLab: Job 결과 업로드
도식 원문
sequenceDiagram
    participant D as 개발자
    participant G as GitLab
    participant R as GitLab Runner
    participant E as 실행 환경
    D->>G: commit push 또는 Merge Request
    G->>G: workflow와 rules 평가
    G->>G: Pipeline과 Job 생성
    R->>G: 실행 가능한 Job 요청
    G-->>R: Job 명세와 단기 Job Token
    R->>E: 실행 환경 준비·소스 checkout
    E->>E: script 실행
    E-->>R: 로그·종료 코드·아티팩트
    R-->>G: Job 결과 업로드

파이프라인과 Job이 생성되고 실행되는 과정에서는 다음 세 설정을 확인합니다.

  • workflow: rules는 파이프라인 자체를 만들지 결정합니다.
  • Job의 rules는 생성된 파이프라인 안에 해당 Job을 포함할지 결정합니다.
  • needs는 stage 순서만 기다리지 않고 어떤 Job의 결과가 필요한지 DAG로 표현합니다.

Job의 script가 0으로 종료되면 성공하고, 0이 아닌 값으로 종료되면 기본적으로 실패합니다. 화면의 초록색 체크는 명령이 의도한 검증을 실제로 했을 때만 의미가 있습니다. 아무것도 검사하지 않고 echo success만 실행한 Job도 초록색이 될 수 있기 때문입니다.

CI와 GitOps CD의 경계

이 시리즈에서 사용할 최종 흐름은 다음과 같습니다.

단계별 흐름 / 02CI와 GitOps가 맡는 일
개발자push / Merge Request
앱 소스 저장소GitLab
GitLab CI테스트·Rootless BuildKit
01 / 05
소스 변경과 검증

앱 소스 저장소의 변경으로 CI가 테스트와 이미지 빌드를 실행합니다.

1

개발자 → 앱 소스 저장소push / MR

2

앱 소스 저장소 → GitLab CICI 실행

단계를 선택하면 자동 재생이 멈춥니다. 선의 번호와 아래 설명을 함께 읽어 주세요.

전체 단계 한눈에 읽기
  1. 소스 변경과 검증

    앱 소스 저장소의 변경으로 CI가 테스트와 이미지 빌드를 실행합니다.

    • 개발자 → 앱 소스 저장소: push / MR
    • 앱 소스 저장소 → GitLab CI: CI 실행
  2. 실행 이미지 저장

    CI가 만든 컨테이너 이미지는 Harbor에 push합니다.

    • GitLab CI → Harbor: 이미지 push
  3. 배포 선언을 Git에 기록

    CI는 Helm Chart 저장소의 image tag를 갱신해 원하는 상태를 commit합니다.

    • GitLab CI → Helm Chart 저장소: image tag 갱신 commit
  4. Argo CD가 상태를 맞추기

    Argo CD는 저장소를 pull하고 차이를 확인해 Kubernetes를 조정합니다. CI가 클러스터를 직접 배포하는 경로가 아닙니다.

    • Helm Chart 저장소 → Argo CD: pull·diff
    • Argo CD → Kubernetes: reconcile
  5. 클러스터가 이미지 가져오기

    Kubernetes가 Harbor에 image pull을 요청합니다. 이미지 데이터의 반환 방향은 이 요청 화살표와 반대입니다.

    • Kubernetes → Harbor: image pull 요청
도식 원문
flowchart LR
    dev["개발자"] -->|push / MR| source["GitLab 앱 소스 저장소"]
    source --> ci["GitLab CI<br/>테스트·Rootless BuildKit"]
    ci -->|이미지 push| harbor["Harbor<br/>검증된 컨테이너 이미지"]
    ci -->|image tag 갱신 commit| config["GitLab Helm Chart 저장소<br/>원하는 상태"]
    config -->|pull·diff| argo["Argo CD"]
    argo -->|reconcile| cluster["Kubernetes"]
    cluster -. image pull .-> harbor

이 구성에서 GitLab CI와 Argo CD는 다음과 같이 역할을 나눕니다.

  • GitLab CI는 테스트하고, 이미지를 만들고, Harbor에 올리고, Chart 저장소의 이미지 참조를 바꿉니다.
  • Argo CD는 Git을 읽고 Kubernetes를 바꿉니다.
  • CI에는 Kubernetes 관리자 자격증명이나 Argo CD 관리자 토큰이 필요하지 않습니다.

Argo CD의 자동 동기화는 이 구조를 공식적으로 지원합니다. 파이프라인이 추적 중인 Git 저장소에 변경을 남기면 Argo CD가 차이를 감지해 동기화하므로, CI가 Argo CD API 서버에 직접 접근할 이유가 사라집니다.

왜 소스 저장소와 배포 저장소를 나누는가

한 저장소에 모든 파일을 넣어도 기술적으로는 동작합니다. 그러나 변경 주기와 권한 주체가 다르면 분리가 유리합니다.

구분 앱 소스 저장소 배포 저장소
핵심 내용 코드, 테스트, Dockerfile Helm Chart, 환경별 values, Argo CD 선언
주 변경자 애플리케이션 개발자 CI와 플랫폼 운영자
성공 기준 테스트·빌드 성공 원하는 상태 검토·배포 건강성
민감 권한 Harbor push 배포 상태 변경
롤백 코드 revert 후 재빌드 이전 이미지 참조로 Git revert

저장소를 나누면 빌드된 바이너리를 다시 만들지 않고 같은 이미지 버전을 다음 환경으로 승격할 수 있습니다. 운영 환경의 변경에는 더 강한 승인 규칙을 적용할 수도 있습니다. 이러한 변경과 권한의 차이를 관리하기 위해 앱 소스와 배포 저장소를 분리합니다.

실습: 작은 GitLab Pipeline을 끝까지 관찰하기

이 실습에서는 외부 시스템 없이 validate → test → release-metadata 흐름을 만듭니다. 마지막 Job은 나중에 Harbor와 Chart 저장소가 사용할 이미지 태그 후보를 dotenv 아티팩트로 남깁니다.

1. 프로젝트 파일 만들기

GitLab에서 빈 private 프로젝트 student-01/web-ci-lab을 만든 뒤 clone합니다.

bash
git clone https://gitlab.lab.example.com/student-01/web-ci-lab.git
cd web-ci-lab

index.html을 만듭니다.

html
<!doctype html>
<html lang="ko">
  <head>
    <meta charset="utf-8" />
    <title>GitLab CI Lab</title>
  </head>
  <body>
    <h1>검증된 변경만 다음 단계로 이동합니다.</h1>
  </body>
</html>

나중에 이미지 빌드에서 사용할 Dockerfile도 함께 둡니다.

dockerfile
FROM nginx:1.27-alpine

COPY index.html /usr/share/nginx/html/index.html

2. .gitlab-ci.yml 작성

프로젝트 루트에 파일명을 정확히 .gitlab-ci.yml로 만듭니다.

yaml
stages:
  - validate
  - test
  - release

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

validate-html:
  stage: validate
  image: alpine:3.21
  script:
    - test -s index.html
    - grep -q '<!doctype html>' index.html
    - grep -q '<title>GitLab CI Lab</title>' index.html

content-test:
  stage: test
  image: alpine:3.21
  needs:
    - validate-html
  script:
    - grep -q '검증된 변경' index.html
    - '! grep -q "TODO" index.html'

release-metadata:
  stage: release
  image: alpine:3.21
  needs:
    - content-test
  script:
    - printf 'IMAGE_TAG=%s\n' "$CI_COMMIT_SHA" > release.env
    - printf 'SOURCE_REF=%s\n' "$CI_COMMIT_REF_NAME" >> release.env
    - cat release.env
  artifacts:
    reports:
      dotenv: release.env
    expire_in: 1 day

앞의 설정에서는 실행 조건과 검증 순서, 결과물 보존 범위를 다음과 같이 정했습니다.

  • 기본 브랜치 push와 Merge Request 파이프라인만 만듭니다.
  • 각 Job은 실제 파일 내용을 검사하며 실패 가능한 명령을 가집니다.
  • needs로 선행 검증이 성공한 경우에만 다음 Job을 실행합니다.
  • 가변적인 짧은 별칭보다 전체 ${CI_COMMIT_SHA}를 릴리스 식별자로 남깁니다.
  • 아티팩트의 보존 기간을 제한해 불필요한 저장 공간 사용을 막습니다.

3. 커밋하고 실행하기

bash
git add index.html Dockerfile .gitlab-ci.yml
git commit -m "ci: add validation pipeline"
git push -u origin main

GitLab의 Build → Pipelines에서 새 파이프라인을 엽니다. 세 Job이 차례로 성공하고 release-metadata Job의 아티팩트에 release.env가 있어야 합니다.

예상 내용은 다음과 같습니다. 실제 값은 현재 commit에 따라 달라집니다.

text
IMAGE_TAG=<40자리 commit SHA>
SOURCE_REF=main

4. 실패가 검출되는지 확인하기

index.html 본문에 TODO를 한 줄 추가해 별도 브랜치로 push하고 Merge Request를 만듭니다.

bash
git switch -c test/pipeline-failure
printf '\n<!-- TODO: remove before release -->\n' >> index.html
git add index.html
git commit -m "test: prove pipeline blocks TODO"
git push -u origin test/pipeline-failure

이번에는 content-test가 실패하고 release-metadata는 실행되지 않아야 합니다. 실패한 입력이 다음 단계로 넘어가지 않는지 확인해야 이 파이프라인이 검증 역할을 수행하는지 판단할 수 있습니다. 확인 후 해당 Merge Request를 닫거나 변경을 되돌립니다.

실제 파이프라인으로 확장할 때의 순서

작은 실습을 운영 흐름으로 확장하면 다음과 같은 단계가 됩니다.

  1. 정적 검사와 테스트를 통과합니다.
  2. Rootless BuildKit으로 이미지를 만들고 Harbor에 전체 commit SHA 태그로 push합니다.
  3. 필요하면 이미지 취약점 검사와 서명 검증을 통과시킵니다.
  4. 별도 GitLab Helm Chart 저장소의 개발 환경 values를 새 이미지 태그로 갱신합니다.
  5. Argo CD가 Git 변경을 감지하고 클러스터를 조정합니다.
  6. 운영 승격은 동일 이미지를 운영 values에 반영하는 Merge Request로 수행합니다.

Rootless BuildKit은 특권 컨테이너 없이 이미지를 빌드할 수 있는 GitLab 공식 경로입니다. Docker-in-Docker는 익숙한 CLI를 제공하지만 일반적으로 특권 Runner가 필요하므로 이 시리즈의 기본 경로로 사용하지 않습니다.

검증 체크리스트

  • 파이프라인 상세 화면의 commit SHA가 방금 push한 commit과 같습니까?
  • 각 Job이 어떤 Runner에서 실행됐는지 확인했습니까?
  • 실패하는 입력을 넣었을 때 실제로 다음 단계가 차단됩니까?
  • 릴리스 식별자가 전체 commit SHA로 남습니까?
  • CI 설정에 kubectl apply, Argo CD 관리자 토큰, Kubernetes 관리자용 kubeconfig가 없습니까?
  • 배포가 필요한 경우 클러스터 변경이 아니라 Chart 저장소의 Git 변경으로 끝납니까?

자주 발생하는 문제

파이프라인이 아예 생기지 않는다

workflow: rules와 이벤트 종류를 먼저 확인합니다. 위 예시는 기본 브랜치와 Merge Request만 허용하므로 일반 feature 브랜치 push만으로는 파이프라인이 생성되지 않습니다. GitLab CI Lint에서 병합된 설정을 검증하고, 프로젝트 루트의 파일명이 .gitlab-ci.yml인지 확인합니다.

Job이 Pending 상태에 머문다

실행 가능한 Runner가 없거나 Job tag와 Runner tag가 맞지 않는 경우가 많습니다. 프로젝트의 Settings → CI/CD → Runners에서 활성 Runner와 허용 범위를 확인합니다.

앞 Job이 실패했는데 뒤 Job이 실행된다

allow_failure: true, when: always, 잘못된 needs 구성을 확인합니다. 보안 검사나 배포 전 검증에 allow_failure를 습관적으로 사용하면 초록색 파이프라인이 실제 품질을 대표하지 못합니다.

변수는 등록했는데 Job에서 비어 있다

Protected 변수는 protected branch 또는 tag의 적격 파이프라인에서만 제공됩니다. 변수의 environment scope도 현재 Job의 environment와 맞아야 합니다. Merge Request 코드는 악의적으로 변수를 외부로 전송할 수 있으므로, 보호 변수를 보이게 만들기 위해 설정을 느슨하게 바꾸면 안 됩니다.

같은 commit인데 결과가 달라진다

latest 같은 가변 베이스 이미지, 버전 범위만 둔 의존성, 외부 저장소의 변경이 원인일 수 있습니다. 베이스 이미지 digest, lock 파일, 도구 버전을 고정하고 빌드 로그에 사용 버전을 남깁니다.

보안과 운영 원칙

  • CI 변수의 Masked 표시는 로그의 우발적 노출을 줄일 뿐, 악성 스크립트로부터 시크릿을 보호하는 보안 경계가 아닙니다.
  • 배포용 변수는 Protected로 두고 보호된 ref에서만 사용할 수 있게 합니다.
  • 개인용 토큰보다 목적이 좁은 Job Token, Project Access Token, Deploy Token을 우선합니다.
  • Runner를 신뢰 수준별로 분리합니다. 외부 기여 Merge Request가 운영 자격증명이 있는 Runner에서 실행되면 안 됩니다.
  • 이미지에는 latest 대신 commit SHA나 digest를 사용합니다.
  • CI가 클러스터 관리자 권한을 보유하지 않게 합니다. Git 변경 이후의 적용은 Argo CD가 담당합니다.
  • 파이프라인 로그와 아티팩트에는 비밀번호, 토큰, kubeconfig, 인증서 개인키를 출력하지 않습니다.

요약

GitLab에서는 소스 변경을 검토하고, 자동 검증과 빌드 결과를 확인합니다. 이 글의 GitOps 구성에서 GitLab CI는 검증된 이미지를 Harbor에 남기고 별도 Helm Chart 저장소의 원하는 상태를 변경하는 데까지 담당합니다. 클러스터에 적용하는 작업은 Argo CD가 맡습니다. Argo CD는 Git 변경을 읽어 실제 클러스터를 기록된 상태에 맞춥니다.

이 경계를 지키면 빌드 시스템에서 Kubernetes 관리자 자격증명을 제거할 수 있고, 모든 배포를 commit과 Merge Request로 설명할 수 있으며, 롤백도 Git revert라는 동일한 절차로 수행할 수 있습니다.

공식 참고자료