GitLab CI/CD 파이프라인 구축
완벽 가이드
빌드부터 배포까지 자동화하는 실전 예제
GitLab CI/CD란?
GitLab CI/CD는 GitLab에 내장된 지속적 통합/지속적 배포 도구다. 코드를 푸시하면 자동으로 테스트하고, 빌드하고, 배포까지 할 수 있다. 별도의 Jenkins나 CircleCI 같은 외부 도구 없이 GitLab 하나로 전체 DevOps 파이프라인을 구성할 수 있다는 게 장점이다.
왜 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들은 병렬로 실행된다. 위 예시에서 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 hourJob 설정 옵션
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
- linuxGitLab 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 --versionRunner 등록
# 대화형 등록
sudo gitlab-runner register
# 입력 항목:
# - GitLab URL (예: https://gitlab.com/)
# - Registration token (프로젝트 Settings > CI/CD > Runners에서 확인)
# - Runner 설명
# - Tags (쉼표로 구분)
# - Executor 선택 (docker, shell, kubernetes 등)프로젝트의 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 = 0Runner 관리 명령어
# 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:
- mainNode.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:
- buildPython + 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에서 설정하고, Masked와 Protected 옵션을 활성화해야 한다.
# 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.xmlneeds로 의존성 정의
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:
- mainKaniko (보안 강화)
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: neverinclude (설정 분리)
공통 설정을 별도 파일로 분리해서 재사용할 수 있다.
# .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.ymlproject/ ├── .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:latestparallel (병렬 실행)
# 동일한 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 testtrigger (다른 파이프라인 실행)
# 다른 프로젝트의 파이프라인 트리거
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트러블슈팅
자주 발생하는 문제
Runner가 없거나 태그가 맞지 않을 때 발생한다. Settings → CI/CD → Runners에서 Runner 상태를 확인하고, Job에 설정된 tags가 Runner의 tags와 일치하는지 확인한다.
# Runner 태그 확인이 필요한 경우
my-job:
tags:
- docker # 이 태그를 가진 Runner가 있어야 함
- linuxCannot 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캐시 키가 올바른지, 경로가 정확한지 확인한다. 또한 캐시는 같은 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 버튼 클릭참고 자료
'CI CD' 카테고리의 다른 글
| GitLab에 왜 데이터베이스가 필요할까? (0) | 2026.01.19 |
|---|---|
| GitLab 설치 방식: Omnibus vs Docker 어떤 방식을 선택해야 할까? (0) | 2026.01.19 |