CI CD

GitLab CI/CD 파이프라인 구축 가이드

JohnnyDeveloper 2025. 12. 16. 17:37

GitLab CI/CD 파이프라인 구축
완벽 가이드

빌드부터 배포까지 자동화하는 실전 예제

GitLab CI/CD란?

GitLab CI/CD는 GitLab에 내장된 지속적 통합/지속적 배포 도구다. 코드를 푸시하면 자동으로 테스트하고, 빌드하고, 배포까지 할 수 있다. 별도의 Jenkins나 CircleCI 같은 외부 도구 없이 GitLab 하나로 전체 DevOps 파이프라인을 구성할 수 있다는 게 장점이다.

CI/CD 파이프라인 흐름
flowchart LR A[코드 Push] --> B[Build] B --> C[Test] C --> D[Package] D --> E{Branch?} E -->|develop| F[Deploy to Dev] E -->|main| G[Deploy to Prod] style A fill:#e24329,color:#fff style B fill:#fc6d26,color:#fff style C fill:#fca326,color:#fff style D fill:#e24329,color:#fff style F fill:#28a745,color:#fff style G fill:#28a745,color:#fff

왜 GitLab CI/CD인가

  • 통합 환경: 소스 코드 관리와 CI/CD가 한 곳에 있어서 관리가 편하다
  • 무료 사용량: GitLab.com에서 월 400분의 무료 CI 시간을 제공한다
  • 자체 Runner: 자체 서버에 Runner를 설치하면 무제한으로 사용할 수 있다
  • Container Registry: Docker 이미지 저장소가 내장되어 있다
  • Auto DevOps: 설정 없이도 자동으로 파이프라인을 구성해준다

기본 개념

핵심 용어

용어 설명
Pipeline CI/CD 작업의 전체 흐름. 여러 Stage로 구성된다
Stage 파이프라인의 단계. 같은 Stage의 Job은 병렬 실행된다
Job 실제로 실행되는 작업 단위. 스크립트를 포함한다
Runner Job을 실제로 실행하는 에이전트
Artifact Job 실행 결과물. 다음 Job에 전달하거나 다운로드 가능
Cache 의존성 등을 저장해서 다음 파이프라인에서 재사용

파이프라인 구조

Stage와 Job의 관계
flowchart TB subgraph BuildStage[Build Stage] B1[build-frontend] B2[build-backend] end subgraph TestStage[Test Stage] T1[unit-test] T2[integration-test] T3[lint] end subgraph DeployStage[Deploy Stage] D1[deploy] end BuildStage --> TestStage --> DeployStage style B1 fill:#fc6d26,color:#fff style B2 fill:#fc6d26,color:#fff style T1 fill:#fca326,color:#fff style T2 fill:#fca326,color:#fff style T3 fill:#fca326,color:#fff style D1 fill:#28a745,color:#fff

같은 Stage 안의 Job들은 병렬로 실행된다. 위 예시에서 build-frontend와 build-backend는 동시에 실행되고, 둘 다 끝나야 Test Stage로 넘어간다.

.gitlab-ci.yml 기본 구조

GitLab CI/CD는 프로젝트 루트에 있는 .gitlab-ci.yml 파일을 읽어서 파이프라인을 구성한다.

가장 간단한 예제

# .gitlab-ci.yml stages: - build - test - deploy build-job: stage: build script: - echo "빌드 중..." - npm install - npm run build test-job: stage: test script: - echo "테스트 중..." - npm test deploy-job: stage: deploy script: - echo "배포 중..." only: - main

주요 키워드

# 전역 설정 default: image: node:20-alpine # 기본 Docker 이미지 # 스테이지 정의 (실행 순서) stages: - build - test - deploy # 전역 변수 variables: NODE_ENV: production # 모든 Job 전에 실행 before_script: - npm ci # 모든 Job 후에 실행 after_script: - echo "Job 완료" # Job 정의 build: stage: build script: - npm run build artifacts: paths: - dist/ expire_in: 1 hour

Job 설정 옵션

my-job: stage: test image: node:20 # 이 Job에서 사용할 이미지 variables: # Job 레벨 변수 DATABASE_URL: "localhost" before_script: # 이 Job 전에 실행 - npm install script: # 실제 실행할 명령어 (필수) - npm test after_script: # 이 Job 후에 실행 (실패해도 실행됨) - echo "테스트 완료" rules: # 실행 조건 - if: $CI_COMMIT_BRANCH == "main" allow_failure: false # 실패해도 파이프라인 계속 진행할지 timeout: 30 minutes # Job 타임아웃 retry: 2 # 실패 시 재시도 횟수 tags: # 특정 Runner에서 실행 - docker - linux

GitLab Runner 설정

GitLab Runner는 실제로 Job을 실행하는 에이전트다. GitLab.com의 공유 Runner를 사용할 수도 있고, 자체 서버에 설치할 수도 있다.

Runner 설치 (Linux)

# GitLab Runner 저장소 추가 curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash # 설치 sudo apt install gitlab-runner # 버전 확인 gitlab-runner --version

Runner 등록

# 대화형 등록 sudo gitlab-runner register # 입력 항목: # - GitLab URL (예: https://gitlab.com/) # - Registration token (프로젝트 Settings > CI/CD > Runners에서 확인) # - Runner 설명 # - Tags (쉼표로 구분) # - Executor 선택 (docker, shell, kubernetes 등)
💡
Registration Token 찾기

프로젝트의 Settings → CI/CD → Runners로 가면 Registration Token을 확인할 수 있다. 프로젝트별 Runner 또는 그룹 Runner로 등록할 수 있다.

Docker Executor 설정

Docker Executor를 사용하면 각 Job이 깨끗한 컨테이너 환경에서 실행된다.

# /etc/gitlab-runner/config.toml concurrent = 4 check_interval = 0 [[runners]] name = "my-docker-runner" url = "https://gitlab.com/" token = "YOUR_TOKEN" executor = "docker" [runners.docker] tls_verify = false image = "alpine:latest" privileged = true # Docker-in-Docker 사용 시 필요 disable_entrypoint_overwrite = false oom_kill_disable = false disable_cache = false volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"] shm_size = 0

Runner 관리 명령어

# Runner 상태 확인 sudo gitlab-runner status # 등록된 Runner 목록 sudo gitlab-runner list # Runner 시작/중지 sudo gitlab-runner start sudo gitlab-runner stop # Runner 등록 해제 sudo gitlab-runner unregister --name "runner-name" # 모든 Runner 등록 해제 sudo gitlab-runner unregister --all-runners

실전 파이프라인 예제

Spring Boot + Gradle 프로젝트

# .gitlab-ci.yml image: gradle:8-jdk21 stages: - build - test - package - deploy variables: GRADLE_OPTS: "-Dorg.gradle.daemon=false" # Gradle 캐시 설정 cache: key: ${CI_COMMIT_REF_SLUG} paths: - .gradle/ - build/ build: stage: build script: - gradle assemble artifacts: paths: - build/libs/*.jar expire_in: 1 hour test: stage: test script: - gradle test artifacts: when: always reports: junit: build/test-results/test/*.xml package: stage: package image: docker:24 services: - docker:24-dind variables: DOCKER_TLS_CERTDIR: "/certs" before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA only: - main - develop deploy-dev: stage: deploy script: - echo "개발 서버 배포" - ssh deploy@dev-server "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA && docker-compose up -d" environment: name: development url: https://dev.example.com only: - develop deploy-prod: stage: deploy script: - echo "운영 서버 배포" environment: name: production url: https://example.com when: manual only: - main

Node.js + React 프로젝트

# .gitlab-ci.yml image: node:20-alpine stages: - install - lint - test - build - deploy cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/ - .npm/ install: stage: install script: - npm ci --cache .npm --prefer-offline artifacts: paths: - node_modules/ expire_in: 1 hour lint: stage: lint script: - npm run lint needs: - install test: stage: test script: - npm run test -- --coverage coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' artifacts: reports: coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml needs: - install build: stage: build script: - npm run build artifacts: paths: - dist/ expire_in: 1 week needs: - install - lint - test pages: stage: deploy script: - mv dist public artifacts: paths: - public only: - main needs: - build

Python + FastAPI 프로젝트

# .gitlab-ci.yml image: python:3.12-slim stages: - lint - test - build - deploy variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache" cache: paths: - .pip-cache/ - .venv/ before_script: - python -m venv .venv - source .venv/bin/activate - pip install --upgrade pip - pip install -r requirements.txt lint: stage: lint script: - pip install ruff - ruff check . - ruff format --check . test: stage: test services: - postgres:15 variables: POSTGRES_DB: test_db POSTGRES_USER: test_user POSTGRES_PASSWORD: test_password DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db" script: - pip install pytest pytest-cov - pytest --cov=app --cov-report=xml coverage: '/TOTAL.*\s+(\d+%)/' artifacts: reports: coverage_report: coverage_format: cobertura path: coverage.xml build: stage: build image: docker:24 services: - docker:24-dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker build -t $CI_REGISTRY_IMAGE:latest . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - docker push $CI_REGISTRY_IMAGE:latest only: - main

환경 변수와 시크릿

변수 정의 방법

GitLab CI/CD에서 변수를 정의하는 방법은 여러 가지가 있다.

# 1. .gitlab-ci.yml에서 정의 variables: NODE_ENV: production APP_VERSION: "1.0.0" # 2. Job 레벨에서 정의 build: variables: BUILD_TYPE: release script: - echo $BUILD_TYPE # 3. GitLab UI에서 정의 (Settings > CI/CD > Variables) # 민감한 정보는 여기서 관리한다

GitLab 사전 정의 변수

GitLab이 자동으로 제공하는 변수들이 있다. 자주 쓰는 것들을 정리했다.

변수 설명
CI_COMMIT_SHA 커밋 SHA 전체
CI_COMMIT_SHORT_SHA 커밋 SHA 앞 8자리
CI_COMMIT_BRANCH 브랜치명
CI_COMMIT_TAG 태그명 (태그 푸시 시)
CI_PIPELINE_ID 파이프라인 고유 ID
CI_JOB_ID Job 고유 ID
CI_PROJECT_NAME 프로젝트 이름
CI_REGISTRY Container Registry 주소
CI_REGISTRY_IMAGE 이 프로젝트의 이미지 경로
CI_REGISTRY_USER Registry 로그인 사용자
CI_REGISTRY_PASSWORD Registry 로그인 비밀번호

시크릿 관리

⚠️
민감한 정보는 절대 코드에 넣지 마라

API 키, 비밀번호 등은 Settings → CI/CD → Variables에서 설정하고, MaskedProtected 옵션을 활성화해야 한다.

# GitLab UI에서 변수 설정 후 사용 deploy: script: - echo "Deploying with secret key" - curl -H "Authorization: Bearer $API_SECRET_KEY" https://api.example.com/deploy # 또는 파일로 사용 (Type: File로 설정 시) - cat $GOOGLE_CREDENTIALS > /tmp/gcloud-key.json - gcloud auth activate-service-account --key-file=/tmp/gcloud-key.json

캐싱과 아티팩트

Cache vs Artifacts

구분 Cache Artifacts
용도 의존성 저장 (node_modules 등) 빌드 결과물 전달
범위 파이프라인 간 공유 같은 파이프라인 내 Job 간 전달
저장 위치 Runner 로컬 또는 S3 GitLab 서버
다운로드 불가능 UI에서 다운로드 가능
보장성 없을 수도 있음 항상 있음

Cache 설정

# 전역 캐시 설정 cache: key: ${CI_COMMIT_REF_SLUG} # 브랜치별로 캐시 분리 paths: - node_modules/ - .npm/ # 또는 파일 기반 캐시 키 cache: key: files: - package-lock.json # 이 파일이 바뀌면 캐시 갱신 paths: - node_modules/ # Job별 캐시 설정 build: cache: key: build-cache paths: - build/ policy: push # 캐시를 저장만 함 test: cache: key: build-cache paths: - build/ policy: pull # 캐시를 가져오기만 함

Artifacts 설정

build: stage: build script: - npm run build artifacts: paths: - dist/ - build/ exclude: - dist/**/*.map # 소스맵 제외 expire_in: 1 week # 보관 기간 when: on_success # 성공 시에만 저장 test: stage: test script: - npm test artifacts: when: always # 실패해도 저장 reports: junit: test-results.xml coverage_report: coverage_format: cobertura path: coverage/cobertura.xml

needs로 의존성 정의

needs를 사용하면 Stage 순서와 관계없이 특정 Job이 끝나면 바로 실행할 수 있다. Artifacts도 자동으로 가져온다.

build-frontend: stage: build script: - npm run build:frontend artifacts: paths: - dist/frontend/ build-backend: stage: build script: - gradle build artifacts: paths: - build/libs/ # Stage가 test지만 build-frontend만 끝나면 바로 실행 test-frontend: stage: test needs: - build-frontend # build-frontend의 artifacts 자동 다운로드 script: - npm run test:frontend deploy: stage: deploy needs: - job: build-frontend artifacts: true - job: build-backend artifacts: true script: - echo "배포"

Docker 빌드와 배포

Docker-in-Docker (DinD)

build-image: stage: build image: docker:24 services: - docker:24-dind variables: DOCKER_TLS_CERTDIR: "/certs" DOCKER_DRIVER: overlay2 before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA only: - main

Kaniko (보안 강화)

Kaniko는 privileged 모드 없이 Docker 이미지를 빌드할 수 있어서 더 안전하다.

build-image: stage: build image: name: gcr.io/kaniko-project/executor:v1.19.0-debug entrypoint: [""] script: - mkdir -p /kaniko/.docker - echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64)\"}}}" > /kaniko/.docker/config.json - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --destination $CI_REGISTRY_IMAGE:latest only: - main

멀티 스테이지 Dockerfile

# Dockerfile # Build stage FROM gradle:8-jdk21 AS builder WORKDIR /app COPY . . RUN gradle build -x test # Production stage FROM eclipse-temurin:21-jre-alpine WORKDIR /app COPY --from=builder /app/build/libs/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

Docker Compose 배포

deploy: stage: deploy image: docker:24 before_script: - apk add --no-cache openssh-client - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - mkdir -p ~/.ssh - chmod 700 ~/.ssh - ssh-keyscan -H $DEPLOY_SERVER >> ~/.ssh/known_hosts script: - ssh $DEPLOY_USER@$DEPLOY_SERVER " cd /app && docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY && docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA && docker-compose down && docker-compose up -d " environment: name: production url: https://example.com when: manual only: - main

고급 기능

rules (조건부 실행)

only/except보다 rules가 더 유연하고 권장된다.

build: script: - npm run build rules: # main 브랜치면 항상 실행 - if: $CI_COMMIT_BRANCH == "main" when: always # MR이면 실행 - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: always # 태그 푸시면 실행 - if: $CI_COMMIT_TAG when: always # 특정 파일이 변경되면 실행 - changes: - src/**/* - package.json when: on_success # 그 외에는 실행하지 않음 - when: never

include (설정 분리)

공통 설정을 별도 파일로 분리해서 재사용할 수 있다.

# .gitlab-ci.yml include: # 로컬 파일 - local: '.gitlab/ci/build.yml' - local: '.gitlab/ci/test.yml' - local: '.gitlab/ci/deploy.yml' # 다른 프로젝트의 파일 - project: 'my-group/ci-templates' ref: main file: '/templates/docker.yml' # 원격 URL - remote: 'https://example.com/ci-template.yml' # GitLab 제공 템플릿 - template: Security/SAST.gitlab-ci.yml
프로젝트 구조
project/
├── .gitlab-ci.yml
├── .gitlab/
│   └── ci/
│       ├── build.yml
│       ├── test.yml
│       └── deploy.yml
└── src/

extends (상속)

# 공통 설정 정의 .docker-job: image: docker:24 services: - docker:24-dind variables: DOCKER_TLS_CERTDIR: "/certs" before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY # 상속받아서 사용 build-image: extends: .docker-job stage: build script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA push-latest: extends: .docker-job stage: deploy script: - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest - docker push $CI_REGISTRY_IMAGE:latest

parallel (병렬 실행)

# 동일한 Job을 여러 개 병렬로 실행 test: stage: test parallel: 5 script: - npm run test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL # 매트릭스로 조합 생성 test: stage: test parallel: matrix: - NODE_VERSION: ["18", "20", "21"] OS: ["alpine", "slim"] image: node:${NODE_VERSION}-${OS} script: - npm test

trigger (다른 파이프라인 실행)

# 다른 프로젝트의 파이프라인 트리거 trigger-deploy: stage: deploy trigger: project: my-group/deploy-project branch: main strategy: depend # 트리거된 파이프라인이 끝날 때까지 대기 # 자식 파이프라인 실행 generate-config: stage: build script: - generate-ci-config > generated-config.yml artifacts: paths: - generated-config.yml child-pipeline: stage: deploy trigger: include: - artifact: generated-config.yml job: generate-config strategy: depend

트러블슈팅

자주 발생하는 문제

⚠️
Job이 pending 상태에서 멈춤

Runner가 없거나 태그가 맞지 않을 때 발생한다. Settings → CI/CD → Runners에서 Runner 상태를 확인하고, Job에 설정된 tags가 Runner의 tags와 일치하는지 확인한다.

# Runner 태그 확인이 필요한 경우 my-job: tags: - docker # 이 태그를 가진 Runner가 있어야 함 - linux
⚠️
Docker 명령어 실행 실패

Cannot connect to the Docker daemon 에러가 나면 Docker-in-Docker 서비스 설정을 확인한다.

# Docker-in-Docker 올바른 설정 build: image: docker:24 services: - docker:24-dind variables: DOCKER_HOST: tcp://docker:2376 DOCKER_TLS_CERTDIR: "/certs" DOCKER_CERT_PATH: "/certs/client" DOCKER_TLS_VERIFY: 1
⚠️
Cache가 작동하지 않음

캐시 키가 올바른지, 경로가 정확한지 확인한다. 또한 캐시는 같은 Runner에서만 공유된다.

# 캐시 디버깅 build: cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/ script: - echo "Cache key: ${CI_COMMIT_REF_SLUG}" - ls -la node_modules/ || echo "node_modules not cached" - npm ci

디버깅 팁

# 변수 값 확인 debug: script: - echo "Branch: $CI_COMMIT_BRANCH" - echo "SHA: $CI_COMMIT_SHA" - echo "Registry: $CI_REGISTRY_IMAGE" - env | grep CI_ # 모든 CI 변수 출력 # 디렉토리 구조 확인 check-files: script: - pwd - ls -la - find . -name "*.jar" -o -name "*.war" # 실패해도 계속 진행 test: script: - npm test || true # 실패해도 Job은 성공으로 처리 # 또는 allow_failure: true

파이프라인 검증

# 로컬에서 .gitlab-ci.yml 검증 # GitLab API 사용 curl --header "Content-Type: application/json" \ --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ --data @.gitlab-ci.yml \ "https://gitlab.com/api/v4/ci/lint" # 또는 GitLab UI에서 # CI/CD > Editor > Validate 버튼 클릭

참고 자료