sayu.day

GitLab CI/CD 완벽 가이드 (하): 파이프라인 아키텍처와 고급 Job 제어

상편에서 .gitlab-ci.yml의 기본 구조와 변수, Runner를 다뤘습니다. 하편에서는 파이프라인이 커질 때 필요한 것들을 다룹니다. 수백 줄로 불어난 설정을 구조화하는 아키텍처 패턴, Job 실행을 정밀하게 제어하는 rules·needs·extends, 그리고 Triggers·Webhooks·API를 통한 외부 시스템 통합까지, 실무에서 가장 자주 부딪히는 순서대로 정리합니다. 특히 모노레포에서 변경된 서비스만 골라 빌드하는 동적 파이프라인 패턴은 그대로 가져다 쓸 수 있도록 예제를 갖췄습니다.

왜 파이프라인 아키텍처가 필요한가

단일 .gitlab-ci.yml이 수백 줄로 커지면 관리가 어려워집니다.

# 거대한 단일 파일의 문제점
stages:
  - build
  - test
  - deploy

# 50개 이상의 Jobs...
build-frontend:
  # ...
build-backend:
  # ...
build-mobile-ios:
  # ...
build-mobile-android:
  # ...
# ... 수백 줄

GitLab은 이 문제를 풀기 위한 네 가지 패턴을 제공합니다.

패턴 용도
include 설정 파일 분리 (동일 프로젝트 내)
Parent-Child 동적 파이프라인, 조건부 실행
Multi-Project 프로젝트 간 트리거
DAG Stage를 무시하고 Job 간 의존성 직접 정의

include: 설정 파일 분리

가장 기본적인 모듈화 방법입니다. 네 가지 소스에서 설정을 가져올 수 있습니다.

include:
  # 1. 로컬 파일
  - local: '/templates/docker.yml'

  # 2. 다른 프로젝트의 파일
  - project: 'my-group/ci-templates'
    ref: main
    file: '/templates/node.yml'

  # 3. 원격 URL
  - remote: 'https://example.com/ci/template.yml'

  # 4. GitLab 제공 템플릿
  - template: 'Auto-DevOps.gitlab-ci.yml'

실전에서는 역할별로 파일을 나누고 메인 파일에서 조립하는 구조를 권장합니다.

project/
├── .gitlab-ci.yml              # 메인 파일
├── .gitlab/
│   ├── ci/
│   │   ├── build.yml           # 빌드 Jobs
│   │   ├── test.yml            # 테스트 Jobs
│   │   └── deploy.yml          # 배포 Jobs
│   └── templates/
│       └── docker.yml          # 공통 템플릿
# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy

include:
  - local: '.gitlab/ci/build.yml'
  - local: '.gitlab/ci/test.yml'
  - local: '.gitlab/ci/deploy.yml'
# .gitlab/ci/build.yml
build-app:
  stage: build
  script:
    - npm run build

Parent-Child Pipeline

include가 파일을 나누는 수준이라면, Parent-Child는 파이프라인 자체를 계층화합니다. Parent Pipeline이 별도의 Child Pipeline을 트리거하는 구조입니다.

flowchart TB
    subgraph Parent [Parent Pipeline]
        P1[build]
        P2[trigger-child]
    end

    subgraph Child [Child Pipeline]
        C1[test-unit]
        C2[test-e2e]
        C3[deploy]
    end

    P1 --> P2
    P2 -->|trigger| C1 & C2
    C1 & C2 --> C3

정적 Child Pipeline

# .gitlab-ci.yml (Parent)
stages:
  - build
  - trigger

build:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/

trigger-child:
  stage: trigger
  trigger:
    include: .gitlab/child-pipeline.yml
    strategy: depend  # Child 완료까지 대기
# .gitlab/child-pipeline.yml (Child)
stages:
  - test
  - deploy

unit-test:
  stage: test
  script:
    - npm run test:unit

deploy:
  stage: deploy
  script:
    - ./deploy.sh

strategy 옵션에 따라 Parent가 Child를 기다릴지 결정됩니다.

옵션 동작
depend Child 완료까지 Parent Job 대기
(없음) Parent Job 즉시 완료, Child 비동기 실행

동적 Child Pipeline과 모노레포 패턴

Parent-Child의 진짜 위력은 런타임에 Child Pipeline YAML을 생성할 때 나옵니다. 변경된 코드에 따라 파이프라인 모양 자체가 달라지므로, 모노레포에서 특히 효과적입니다.

변경된 디렉토리만 테스트하기

# .gitlab-ci.yml
stages:
  - generate
  - trigger

generate-pipeline:
  stage: generate
  script:
    - |
      # 변경된 디렉토리에 따라 동적으로 파이프라인 생성
      cat > child-pipeline.yml <<EOF
      stages:
        - test

      $(for dir in $(git diff --name-only HEAD~1 | cut -d/ -f1 | sort -u); do
        echo "${dir}-test:"
        echo "  stage: test"
        echo "  script:"
        echo "    - echo 'Testing $dir'"
        echo ""
      done)
      EOF
  artifacts:
    paths:
      - child-pipeline.yml

trigger-tests:
  stage: trigger
  trigger:
    include:
      - artifact: child-pipeline.yml
        job: generate-pipeline
    strategy: depend

변경된 서비스만 빌드하기

# .gitlab-ci.yml
stages:
  - detect
  - build

detect-changes:
  stage: detect
  script:
    - |
      # 변경된 서비스 감지
      for service in frontend backend api; do
        if git diff --name-only HEAD~1 | grep -q "^$service/"; then
          echo "$service" >> changed_services.txt
        fi
      done

      # 동적 파이프라인 생성
      echo "stages:" > child.yml
      echo "  - build" >> child.yml
      echo "" >> child.yml

      while read service; do
        cat >> child.yml <<EOF
      build-${service}:
        stage: build
        script:
          - cd ${service} && make build
      EOF
      done < changed_services.txt
  artifacts:
    paths:
      - child.yml

trigger-builds:
  stage: build
  trigger:
    include:
      - artifact: child.yml
        job: detect-changes
  rules:
    - exists:
        - changed_services.txt

rules:changes로 서비스별 파이프라인 분기

YAML을 동적으로 생성할 필요까지 없다면, rules:changes로 특정 경로 변경 시에만 해당 Child를 트리거하는 방식이 더 단순합니다.

# .gitlab-ci.yml
stages:
  - triggers

trigger-frontend:
  stage: triggers
  trigger:
    include: frontend/.gitlab-ci.yml
  rules:
    - changes:
        - frontend/**/*

trigger-backend:
  stage: triggers
  trigger:
    include: backend/.gitlab-ci.yml
  rules:
    - changes:
        - backend/**/*

trigger-shared:
  stage: triggers
  trigger:
    include: shared/.gitlab-ci.yml
  rules:
    - changes:
        - shared/**/*
flowchart TB
    subgraph Parent [Parent Pipeline]
        Check[변경 감지]
    end

    subgraph Children [Child Pipelines]
        F[Frontend Pipeline]
        B[Backend Pipeline]
        S[Shared Pipeline]
    end

    Check -->|frontend/* 변경| F
    Check -->|backend/* 변경| B
    Check -->|shared/* 변경| S

Multi-Project Pipeline

Parent-Child가 한 프로젝트 안의 계층화라면, Multi-Project Pipeline은 다른 프로젝트의 파이프라인을 트리거합니다. 라이브러리 배포 후 이를 사용하는 앱을 빌드하는 식의 프로젝트 간 연쇄에 씁니다.

# Project A의 .gitlab-ci.yml
stages:
  - build
  - trigger

build:
  stage: build
  script:
    - npm run build

trigger-project-b:
  stage: trigger
  trigger:
    project: my-group/project-b
    branch: main
    strategy: depend

variables로 다운스트림에 값을 넘길 수 있습니다.

trigger-deploy:
  stage: trigger
  trigger:
    project: my-group/deployment
    branch: main
  variables:
    DEPLOY_ENV: production
    IMAGE_TAG: $CI_COMMIT_SHA
flowchart LR
    subgraph Upstream [Project A - 라이브러리]
        U1[build]
        U2[publish]
        U3[trigger]
    end

    subgraph Downstream [Project B - 앱]
        D1[install]
        D2[build]
        D3[deploy]
    end

    U1 --> U2 --> U3
    U3 -->|trigger| D1
    D1 --> D2 --> D3

실전 예제: 마이크로서비스 배포

베이스 이미지를 빌드한 뒤 각 서비스 프로젝트를 트리거하고, 모두 완료되면 인프라 배포를 트리거하는 구성입니다.

# infrastructure/.gitlab-ci.yml (Parent)
stages:
  - build
  - trigger-services
  - deploy-infra

build-base-images:
  stage: build
  script:
    - docker build -t base-node:$CI_COMMIT_SHA ./base-images/node
    - docker push base-node:$CI_COMMIT_SHA

trigger-user-service:
  stage: trigger-services
  trigger:
    project: my-org/user-service
    branch: main
  variables:
    BASE_IMAGE_TAG: $CI_COMMIT_SHA

trigger-order-service:
  stage: trigger-services
  trigger:
    project: my-org/order-service
    branch: main
  variables:
    BASE_IMAGE_TAG: $CI_COMMIT_SHA

trigger-payment-service:
  stage: trigger-services
  trigger:
    project: my-org/payment-service
    branch: main
  variables:
    BASE_IMAGE_TAG: $CI_COMMIT_SHA

deploy-kubernetes:
  stage: deploy-infra
  trigger:
    project: my-org/k8s-manifests
    branch: main
  needs:
    - trigger-user-service
    - trigger-order-service
    - trigger-payment-service
  variables:
    SERVICES_VERSION: $CI_COMMIT_SHA

파이프라인 간 아티팩트 공유

Parent에서 만든 아티팩트는 needs로 Child에 전달할 수 있습니다.

# Parent
parent-job:
  script:
    - echo "data" > file.txt
  artifacts:
    paths:
      - file.txt

trigger-child:
  trigger:
    include: child.yml
  needs:
    - parent-job
# Child (child.yml)
child-job:
  script:
    - cat file.txt  # Parent의 아티팩트 사용 가능

프로젝트가 다른 경우에는 API로 직접 내려받습니다.

# Project B
downstream-job:
  script:
    - |
      # Project A의 아티팩트 다운로드
      curl --header "PRIVATE-TOKEN: $API_TOKEN" \
        "$CI_API_V4_URL/projects/123/jobs/$UPSTREAM_JOB_ID/artifacts" \
        --output artifacts.zip
      unzip artifacts.zip

rules: 조건부 Job 실행

아키텍처를 갖췄다면 다음은 개별 Job의 실행 조건입니다. rulesonly/except를 대체하는 조건부 실행 키워드입니다.

job:
  script:
    - echo "Hello"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: always
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual
    - when: never  # 기본값

중요

rules는 위에서 아래로 순차 평가하며, 첫 번째로 매칭된 rule이 적용되고 평가가 끝납니다. 매칭되는 rule이 없으면 Job이 실행되지 않습니다.

세 가지 조건 유형

if: 표현식 평가

rules:
  # 브랜치 조건
  - if: $CI_COMMIT_BRANCH == "main"

  # Pipeline Source
  - if: $CI_PIPELINE_SOURCE == "push"
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"

  # 변수 존재 여부
  - if: $DEPLOY_TOKEN

  # 정규식
  - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/

  # 복합 조건
  - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"

changes: 파일 변경 감지

build-frontend:
  rules:
    - changes:
        - frontend/**/*
        - shared/**/*

build-backend:
  rules:
    - changes:
        paths:
          - backend/**/*
        compare_to: main  # main 브랜치와 비교

exists: 파일 존재 확인

docker-build:
  rules:
    - exists:
        - Dockerfile
        - docker-compose.yml

npm-build:
  rules:
    - exists:
        - package.json

when 옵션

동작
on_success 이전 Stage 성공 시 (기본값)
always 항상 실행
never 실행 안 함
manual 수동 승인 필요
delayed 지연 후 실행
deploy-prod:
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
      allow_failure: false  # 블로킹 수동 Job

only/except에서 마이그레이션

# 이전 (only/except) - 더 이상 권장하지 않음
job:
  only:
    - main
  except:
    - tags

# 현재 (rules) - 권장
job:
  rules:
    - if: $CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG == null

workflow:rules: 파이프라인 전역 제어

rules가 Job 단위라면, workflow:rules파이프라인 자체의 생성 여부를 제어합니다.

workflow:
  rules:
    # MR 파이프라인: MR 이벤트에서만
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

    # 브랜치 파이프라인: main, develop만
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_BRANCH == "develop"

    # 태그 파이프라인
    - if: $CI_COMMIT_TAG

    # 그 외: 파이프라인 생성 안 함

stages:
  - build
  - test

같은 커밋에 브랜치 파이프라인과 MR 파이프라인이 중복 생성되는 흔한 문제도 여기서 해결합니다.

workflow:
  rules:
    # MR 이벤트 시 브랜치 파이프라인 방지 (MR 파이프라인만 실행)
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never  # MR이 열려있으면 브랜치 파이프라인 스킵
    - if: $CI_COMMIT_BRANCH

needs: Stage를 넘어서는 DAG

기본 파이프라인은 Stage 단위로 순차 실행됩니다. needs를 쓰면 Stage를 무시하고 Job 간 직접 의존성(DAG, Directed Acyclic Graph)을 정의할 수 있습니다.

flowchart LR
    subgraph Stage-based [Stage 기반]
        direction TB
        S1[Stage 1] --> S2[Stage 2] --> S3[Stage 3]
    end

    subgraph DAG [DAG 기반]
        direction LR
        A[Job A] --> C[Job C]
        B[Job B] --> D[Job D]
        C --> E[Job E]
        D --> E
    end
stages:
  - build
  - test
  - deploy

build-frontend:
  stage: build
  script: make build-frontend

build-backend:
  stage: build
  script: make build-backend

test-frontend:
  stage: test
  needs: [build-frontend]  # build-frontend 완료 즉시 시작
  script: make test-frontend

test-backend:
  stage: test
  needs: [build-backend]
  script: make test-backend

deploy:
  stage: deploy
  needs: [test-frontend, test-backend]
  script: make deploy

build-frontend가 완료되면 build-backend를 기다리지 않고 즉시 test-frontend가 시작됩니다. 빌드 시간이 서로 다른 모노레포에서 전체 파이프라인 시간을 크게 줄여 줍니다.

flowchart LR
    BF[build-frontend] --> TF[test-frontend]
    BB[build-backend] --> TB[test-backend]
    TF --> D[deploy]
    TB --> D

needs 옵션

test:
  needs:
    - job: build
      artifacts: true   # 아티팩트 다운로드 (기본값)
      optional: false   # 필수 의존성 (기본값)

deploy:
  needs:
    - job: test
      artifacts: false  # 아티팩트 불필요
    - job: security-scan
      optional: true    # 실패해도 진행

parallel로 분할된 Job에 의존할 때는 매트릭스를 명시합니다.

test:
  parallel: 3
  script: run-tests.sh

report:
  needs:
    - job: test
      parallel:
        matrix:
          - RUNNER: [1, 2, 3]  # parallel 모든 인스턴스 대기

dependencies: 아티팩트만 제어하기

dependencies는 실행 순서에는 관여하지 않고 아티팩트 다운로드만 제어합니다.

build:
  stage: build
  script: make build
  artifacts:
    paths:
      - dist/

test:
  stage: test
  dependencies:
    - build  # build의 아티팩트만 다운로드
  script: make test

deploy:
  stage: deploy
  dependencies: []  # 아티팩트 다운로드 안 함
  script: make deploy
특성 needs dependencies
실행 순서 제어함 (DAG) 제어 안 함
아티팩트 기본 포함 전용 제어
Stage 무시 가능 불가능

needs를 사용하면 dependencies가 필요 없는 경우가 많습니다. needs: [job]은 해당 Job의 아티팩트를 자동으로 가져옵니다.

extends와 !reference: Job 템플릿

반복되는 Job 설정은 extends로 상속합니다. 점(.)으로 시작하는 Job은 실행되지 않는 템플릿입니다.

.test-template:
  stage: test
  image: node:20
  before_script:
    - npm ci
  cache:
    paths:
      - node_modules/

unit-test:
  extends: .test-template
  script:
    - npm run test:unit

integration-test:
  extends: .test-template
  script:
    - npm run test:integration
  services:
    - postgres:15

다중 상속도 가능하며, 나중에 정의된 값이 이전 값을 덮어씁니다.

.base:
  tags:
    - docker

.node:
  image: node:20

.cache:
  cache:
    paths:
      - node_modules/

build:
  extends:
    - .base
    - .node
    - .cache
  script:
    - npm run build

!reference는 전체 상속이 아니라 특정 키만 선택적으로 재사용합니다.

.setup:
  before_script:
    - echo "Setting up..."
  after_script:
    - echo "Cleaning up..."
  script:
    - echo "Default script"

.test-vars:
  variables:
    TEST_ENV: "test"

build:
  # .setup의 before_script만 가져옴
  before_script:
    - !reference [.setup, before_script]
    - echo "Additional setup"
  script:
    - npm run build
  variables:
    # .test-vars의 variables 병합
    !reference [.test-vars, variables]
# extends: 전체 병합
job1:
  extends: .template  # 모든 키 상속

# !reference: 선택적 재사용
job2:
  script:
    - !reference [.template, script]  # script만 가져옴

실전 예제: 고급 제어를 모두 적용한 파이프라인

지금까지의 키워드를 한 파일에 조합한 예제입니다.

# 전역 설정
default:
  image: node:20-alpine
  interruptible: true

variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm"

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_TAG

stages:
  - prepare
  - build
  - test
  - deploy

# 템플릿
.node-cache:
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - .npm/
      - node_modules/

.deploy-template:
  image: bitnami/kubectl:latest
  before_script:
    - kubectl config use-context $KUBE_CONTEXT

# Jobs
install:
  stage: prepare
  extends: .node-cache
  script:
    - npm ci
  artifacts:
    paths:
      - node_modules/
    expire_in: 1 hour

lint:
  stage: build
  needs: [install]
  script:
    - npm run lint
  allow_failure: true

build:
  stage: build
  needs: [install]
  script:
    - npm run build
  artifacts:
    paths:
      - dist/

unit-test:
  stage: test
  needs:
    - job: build
      artifacts: true
  script:
    - npm run test:unit
  coverage: '/Coverage: (\d+\.?\d*)%/'
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

e2e-test:
  stage: test
  needs: [build]
  image: mcr.microsoft.com/playwright:v1.40.0
  script:
    - npm run test:e2e
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - when: manual
      allow_failure: true

deploy-staging:
  stage: deploy
  extends: .deploy-template
  needs: [unit-test]
  environment:
    name: staging
    url: https://staging.example.com
  script:
    - kubectl apply -f k8s/staging/
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy-production:
  stage: deploy
  extends: .deploy-template
  needs: [unit-test, e2e-test]
  environment:
    name: production
    url: https://example.com
  script:
    - kubectl apply -f k8s/production/
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
      when: manual

Pipeline Triggers: 토큰 기반 외부 트리거

여기서부터는 파이프라인을 GitLab 바깥과 연결하는 방법입니다. Trigger Token을 쓰면 외부 시스템에서 파이프라인을 실행할 수 있습니다.

설정 위치는 Settings > CI/CD > Pipeline trigger tokens이며, Add trigger로 토큰을 생성합니다.

# 기본 트리거
curl --request POST \
  --form "token=YOUR_TRIGGER_TOKEN" \
  --form "ref=main" \
  "https://gitlab.com/api/v4/projects/PROJECT_ID/trigger/pipeline"

# 변수와 함께
curl --request POST \
  --form "token=YOUR_TRIGGER_TOKEN" \
  --form "ref=main" \
  --form "variables[DEPLOY_ENV]=production" \
  --form "variables[VERSION]=1.2.3" \
  "https://gitlab.com/api/v4/projects/PROJECT_ID/trigger/pipeline"

파이프라인 쪽에서는 $CI_PIPELINE_SOURCE == "trigger"로 트리거 여부를 감지합니다.

triggered-deploy:
  script:
    - echo "Deploying version $VERSION to $DEPLOY_ENV"
  rules:
    - if: $CI_PIPELINE_SOURCE == "trigger"
  needs: []  # 다른 Job 대기 없이 즉시 실행

Webhooks: 외부 이벤트로 파이프라인 실행

Trigger Token을 URL에 포함하면 Webhook 엔드포인트로 쓸 수 있습니다.

https://gitlab.com/api/v4/projects/PROJECT_ID/ref/REF_NAME/trigger/pipeline?token=TOKEN
# GitHub → GitLab 트리거
# GitHub 저장소의 Webhooks에 등록
https://gitlab.com/api/v4/projects/12345/ref/main/trigger/pipeline?token=abc123

# AWS SNS → GitLab 트리거
# Lambda를 통해 변환 후 트리거

Webhook으로 트리거된 파이프라인은 $TRIGGER_PAYLOAD로 페이로드에 접근할 수 있습니다.

process-webhook:
  script:
    - echo "$TRIGGER_PAYLOAD" | jq .
    - export EVENT_TYPE=$(echo "$TRIGGER_PAYLOAD" | jq -r '.event_type')
    - |
      if [ "$EVENT_TYPE" = "release" ]; then
        ./deploy-release.sh
      fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "trigger"

GitLab API로 파이프라인 제어

Personal Access Token을 쓰면 파이프라인 생성부터 취소까지 API로 다룰 수 있습니다.

# 파이프라인 생성
curl --request POST \
  --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "ref": "main",
    "variables": [
      {"key": "DEPLOY_ENV", "value": "staging"},
      {"key": "DEBUG", "value": "true"}
    ]
  }' \
  "https://gitlab.com/api/v4/projects/PROJECT_ID/pipeline"

# 특정 파이프라인 상태 조회
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.com/api/v4/projects/PROJECT_ID/pipelines/PIPELINE_ID"

# 최근 파이프라인 목록
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.com/api/v4/projects/PROJECT_ID/pipelines?per_page=5"

# Job 재시도
curl --request POST \
  --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.com/api/v4/projects/PROJECT_ID/jobs/JOB_ID/retry"

# 파이프라인 취소
curl --request POST \
  --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.com/api/v4/projects/PROJECT_ID/pipelines/PIPELINE_ID/cancel"

CI Job Token: 파이프라인 안에서의 인증

CI_JOB_TOKEN은 파이프라인 실행 중에만 유효한 임시 토큰입니다. 별도 토큰 발급 없이 다른 프로젝트를 트리거하거나 아티팩트를 받을 때 씁니다.

notify-other-project:
  script:
    # 다른 프로젝트 파이프라인 트리거
    - |
      curl --request POST \
        --form "token=$CI_JOB_TOKEN" \
        --form "ref=main" \
        "https://gitlab.com/api/v4/projects/OTHER_PROJECT_ID/trigger/pipeline"
download-artifacts:
  script:
    - |
      curl --header "JOB-TOKEN: $CI_JOB_TOKEN" \
        --output artifacts.zip \
        "https://gitlab.com/api/v4/projects/PROJECT_ID/jobs/JOB_ID/artifacts"
    - unzip artifacts.zip

접근을 허용할 프로젝트는 Settings > CI/CD > Token Access에서 관리합니다.

flowchart LR
    subgraph ProjectA [Project A]
        PA_Job[Job]
    end

    subgraph ProjectB [Project B]
        PB_API[GitLab API]
        PB_Token[Token Access 설정]
    end

    PA_Job -->|CI_JOB_TOKEN| PB_API
    PB_Token -->|허용| PA_Job

ChatOps: Slack 알림과 배포 승인

배포 결과를 Slack으로 알리는 것은 외부 통합의 가장 흔한 형태입니다.

notify-slack:
  stage: .post
  script:
    - |
      curl -X POST -H 'Content-type: application/json' \
        --data '{
          "channel": "#deployments",
          "username": "GitLab CI",
          "text": "✅ Pipeline succeeded for $CI_PROJECT_NAME",
          "attachments": [{
            "color": "good",
            "fields": [
              {"title": "Branch", "value": "'$CI_COMMIT_BRANCH'", "short": true},
              {"title": "Commit", "value": "'$CI_COMMIT_SHORT_SHA'", "short": true}
            ]
          }]
        }' \
        $SLACK_WEBHOOK_URL
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: on_success

반대로 Slack에서 승인 버튼을 눌러 배포를 진행시키는 흐름도 만들 수 있습니다. Slack 버튼 클릭이 Lambda를 거쳐 GitLab API를 호출하는 구조입니다.

# Slack 버튼 클릭 → Lambda → GitLab API
request-approval:
  stage: deploy
  script:
    - |
      curl -X POST -H 'Content-type: application/json' \
        --data '{
          "text": "🚀 Production deployment pending",
          "attachments": [{
            "text": "Approve deployment?",
            "callback_id": "deploy_'$CI_PIPELINE_ID'",
            "actions": [
              {"name": "approve", "text": "Approve", "type": "button", "style": "primary"},
              {"name": "reject", "text": "Reject", "type": "button", "style": "danger"}
            ]
          }]
        }' \
        $SLACK_WEBHOOK_URL
  environment:
    name: production
    action: prepare

외부 CI/CD와 GitOps 연동

Jenkins, GitHub Actions에서 GitLab 트리거

기존 CI 시스템과 GitLab을 병행하는 조직이라면 Trigger Token으로 연결합니다.

// Jenkinsfile
pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'make build'
            }
        }
        stage('Trigger GitLab') {
            steps {
                sh '''
                    curl --request POST \
                      --form "token=${GITLAB_TRIGGER_TOKEN}" \
                      --form "ref=main" \
                      --form "variables[JENKINS_BUILD]=${BUILD_NUMBER}" \
                      "https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/trigger/pipeline"
                '''
            }
        }
    }
}
# .github/workflows/trigger-gitlab.yml
name: Trigger GitLab Pipeline

on:
  push:
    branches: [main]

jobs:
  trigger:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger GitLab
        run: |
          curl --request POST \
            --form "token=${{ secrets.GITLAB_TRIGGER_TOKEN }}" \
            --form "ref=main" \
            --form "variables[GITHUB_SHA]=${{ github.sha }}" \
            "https://gitlab.com/api/v4/projects/${{ secrets.GITLAB_PROJECT_ID }}/trigger/pipeline"

GitLab CI → ArgoCD

CI에서 ArgoCD를 직접 조작하는 방식입니다.

deploy-argocd:
  stage: deploy
  image: argoproj/argocd:latest
  script:
    # ArgoCD 로그인
    - argocd login $ARGOCD_SERVER --username admin --password $ARGOCD_PASSWORD --insecure

    # 이미지 태그 업데이트
    - argocd app set $APP_NAME --helm-set image.tag=$CI_COMMIT_SHA

    # Sync 트리거
    - argocd app sync $APP_NAME --prune

    # 배포 완료 대기
    - argocd app wait $APP_NAME --timeout 300
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

Image Updater 패턴

더 GitOps다운 방식은 CI가 이미지만 빌드해 푸시하고, ArgoCD Image Updater가 레지스트리를 감시해 자동 배포하는 것입니다.

# CI는 이미지만 빌드 & 푸시
# ArgoCD Image Updater가 자동으로 감지하여 배포

build-and-push:
  stage: build
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

    # SemVer 태그도 푸시 (Image Updater가 감지)
    - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:v$VERSION
    - docker push $CI_REGISTRY_IMAGE:v$VERSION
flowchart LR
    subgraph CI [GitLab CI]
        Build[Build & Push Image]
    end

    subgraph Registry [Container Registry]
        Images[(Images)]
    end

    subgraph GitOps [GitOps]
        Updater[ArgoCD Image Updater]
        ArgoCD[ArgoCD]
        K8s[Kubernetes]
    end

    Build --> Images
    Updater -->|Scan| Images
    Updater -->|Update| ArgoCD
    ArgoCD -->|Sync| K8s

CI/CD 분리 원칙: CI는 아티팩트 생성, CD는 GitOps Agent가 담당합니다.

Scheduled Pipelines: 정기 실행

Build > Pipeline schedules > New schedule에서 cron 형식으로 등록하고, rules로 스케줄 전용 Job을 분리합니다.

nightly-test:
  script:
    - npm run test:full
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
      variables:
        FULL_TEST: "true"

daily-backup:
  script:
    - ./backup.sh
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TYPE == "backup"

Schedule 설정에 변수를 지정하면 하나의 파이프라인으로 여러 스케줄을 구분할 수 있습니다.

# Schedule 설정에서 SCHEDULE_TYPE=security 지정

security-scan:
  script:
    - trivy image $CI_REGISTRY_IMAGE:latest
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TYPE == "security"

실전 예제: 빌드부터 GitOps 배포, 알림까지

외부 통합 요소를 모두 조합한 마무리 예제입니다. 이미지를 빌드하고, GitOps 레포에 태그 업데이트 커밋을 만들고, 결과를 Slack으로 알립니다.

stages:
  - build
  - deploy
  - notify

variables:
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

build:
  stage: build
  script:
    - docker build -t $DOCKER_IMAGE .
    - docker push $DOCKER_IMAGE
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

# ArgoCD 배포 트리거
trigger-argocd:
  stage: deploy
  image: curlimages/curl:latest
  script:
    - |
      # GitOps 레포에 이미지 태그 업데이트 PR 생성
      curl --request POST \
        --header "PRIVATE-TOKEN: $GITOPS_TOKEN" \
        --header "Content-Type: application/json" \
        --data '{
          "branch": "update-'$CI_COMMIT_SHORT_SHA'",
          "commit_message": "Update image to '$DOCKER_IMAGE'",
          "actions": [{
            "action": "update",
            "file_path": "apps/myapp/values.yaml",
            "content": "image:\n  tag: '$CI_COMMIT_SHA'"
          }]
        }' \
        "https://gitlab.com/api/v4/projects/GITOPS_PROJECT_ID/repository/commits"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

# Slack 알림
notify-success:
  stage: notify
  script:
    - |
      curl -X POST -H 'Content-type: application/json' \
        --data '{
          "channel": "#deployments",
          "text": "✅ '$CI_PROJECT_NAME' deployed successfully",
          "attachments": [{
            "color": "good",
            "fields": [
              {"title": "Version", "value": "'$CI_COMMIT_SHORT_SHA'"},
              {"title": "Pipeline", "value": "'$CI_PIPELINE_URL'"}
            ]
          }]
        }' \
        $SLACK_WEBHOOK_URL
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: on_success

notify-failure:
  stage: notify
  script:
    - |
      curl -X POST -H 'Content-type: application/json' \
        --data '{
          "channel": "#deployments",
          "text": "❌ '$CI_PROJECT_NAME' deployment failed!",
          "attachments": [{
            "color": "danger",
            "fields": [
              {"title": "Pipeline", "value": "'$CI_PIPELINE_URL'"}
            ]
          }]
        }' \
        $SLACK_WEBHOOK_URL
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: on_failure

정리

하편에서 다룬 내용을 한 표로 요약합니다.

영역 키워드 용도
아키텍처 include 설정 파일 분리
아키텍처 trigger: include: Parent-Child, 동적 파이프라인
아키텍처 trigger: project: Multi-Project 트리거
Job 제어 rules 조건부 실행 (if, changes, exists)
Job 제어 workflow:rules 파이프라인 생성 자체를 제어
Job 제어 needs DAG 의존성, Stage 무시
Job 제어 dependencies 아티팩트 다운로드 제어
Job 제어 extends, !reference 템플릿 상속과 선택적 재사용
외부 통합 Trigger Token, Webhook 외부 시스템에서 파이프라인 실행
외부 통합 API, CI_JOB_TOKEN 파이프라인 생성·조회·재시도·취소
외부 통합 ArgoCD, Schedule GitOps 배포와 정기 실행

설정이 커지면 include와 Parent-Child로 나누고, 실행 조건은 rules와 workflow:rules로 좁히고, 속도는 needs 기반 DAG로 끌어올리고, 바깥 세계와는 Trigger와 API로 연결한다. 이 네 문장이 하편의 전부입니다. 기본기가 필요하다면 상편: 파이프라인 구조, 변수, Runner를 먼저 읽어 보시기 바랍니다.

참고 자료