| 비교 항목 | GitLab CE (무료) | GitLab EE (유료) |
|---|---|---|
| 라이센스 | 오픈소스 (MIT) | 프로프라이어터리 (독점) |
| 기본 기능 | Git 리포지토리, CI/CD, 코드 리뷰 | CE의 모든 기능 + 고급 보안 및 관리 기능 |
| CI/CD | 포함 | 고급 CI/CD (병렬 실행, 실행 우선순위) |
| 보안 및 규정 준수 | 제한적 | 스캔, 감사 로그, SAML 인증 |
| 고급 DevOps 기능 | 없음 | 프로젝트 관리, Jira 통합 |
| Self-Hosted 지원 | 가능 | 가능 |
| SaaS(Cloud) 지원 | 가능 | 가능 (GitLab Ultimate) |
sudo apt update
sudo apt upgrade -y
# Install package
sudo apt install -y ca-certificates curl openssh-server tzdata
# GitLab CE Repository 추가
sudo apt install debian-archive-keyring lsb-release ca-certificates apt-transport-https software-properties-common -y
# GitLab 설치용 저장소 등록
curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash
# 패키지 저장소 업데이트 및 설치
sudo apt update
# GitLab CE 설치
# EXTERNAL_URL : GitLab에 접속할 주소
# 설치 시 이 변수를 넘겨주면 gitlab.rb 설정 파일에 자동으로 기록된다.
sudo EXTERNAL_URL="https://gitlab.example.com" apt-get install gitlab-ce
# HTTPS (SSL) 설정
sudo vi /etc/gitlab/gitlab.rb
# Let's Encrypt 활성화
letsencrypt['enable'] = true
# 외부에서 접속할 URL (반드시 https)
external_url 'https://gitlab.example.com'
# 인증서 만료 알림을 받을 이메일
letsencrypt['contact_emails'] = ['admin@example.com']
# 인증서 자동 갱신 활성화 (무료 인증서는 90일 만료라 자동 갱신 필수)
letsencrypt['auto_renew'] = true
# 30일마다 갱신 시도
letsencrypt['auto_renew_day_of_month'] = "*/30"
# 로그 저장 위치
letsencrypt['auto_renew_log_directory'] = '/var/log/gitlab/lets-encrypt'
sudo gitlab-ctl reconfigure
# 운영 및 관리 명령어
# 설정 적용
# gitlab.rb 파일을 수정했다면 무조건 실행해야 한다.
# Chef라는 자동화 도구가 내부적으로 돌면서,Nginx, DB 등을 설정에 맞게 다시 세팅
sudo gitlab-ctl reconfigure
# 서비스 재시작
# 설정 변경 없이 단순히 프로세스만 껐다 켤 때 사용
sudo gitlab-ctl restart
# 상태 확인
# 모든 서비스(Nginx, DB, Redis 등)가 run 상태인지 확인
sudo gitlab-ctl status
# 초기 root 비밀번호 확인 (설치 후 24시간 내에만 유효)
sudo cat /etc/gitlab/initial_root_password
/var/opt/gitlab/gitlab-rails/shared/lfs-objects/var/opt/gitlab/git-datamkdir gitlab-docker && cd gitlab-docker
vi docker-compose.yml
version: '3.6'
services:
gitlab:
image: gitlab/gitlab-ce:latest
container_name: gitlab
restart: always
hostname: '10.10.10.3' # VM의 IP로 고정
# 메모리 부족 에러 방지 (GitLab 권장 사항)
shm_size: '256m'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://10.10.10.3'
# SSH 포트 변경 (Host의 22번과 충돌 방지)
gitlab_rails['gitlab_shell_ssh_port'] = 2222
ports:
- '80:80'
- '443:443'
- '2222:22' # 호스트 2222 포트를 컨테이너 22로 연결
volumes:
# 호스트의 현재 폴더(./)에 데이터를 저장
- './config:/etc/gitlab'
- './logs:/var/log/gitlab'
- './data:/var/opt/gitlab'
# ID : root
# 비밀번호 확인
docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password
📌 트러블슈팅
ERROR: for gitlab Cannot start service gitlab: failed to set up container networking: driver failed programming external connectivity on endpoint gitlab (5d7a6f2f7d86f3098f1bfe1becd17c3fc21bed2115a6c73522fae6f75d1472f6): failed to bind host port 0.0.0.0:443/tcp: address already in use
- 이미 포트 443을 다른 프로그램이 사용하고 있어서 Docker가 실행되지 못함
- 443 포트를 사용하고 프로세스 확인 :
sudo netstat -tulpn | grep :443- 글쓴이는 Docker로 GitLab을 설치하기 전 apt install로 설치를 했었기 때문에 다음의 단계를 진행
- 기존 GitLab 서비스 중지 : sudo gitlab-ctl stop
- 기존 GitLab 제거 : sudo apt-get remove gitlab-ce gitlab-ee
- 포트가 비었는지 확인 : sudo netstat -tulpn | grep :443
.gitlab-ci.yml에 정의된 명령어들을 순차적으로 실행한다.docker run -d --name gitlab-runner --restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
gitlab/gitlab-runner:latest
docker exec -it gitlab-runner gitlab-runner register

5. .gitlab-ci.yml 작성
stages:
- build
- test
build-job:
stage: build
script:
- echo "Starting build..."
- mkdir build_output
- echo "build outputs..." > build_output/result.txt
artifacts:
paths:
- build_output/
test-job:
stage: test
script:
- echo "String test..."
- grep "build outputs" build_output/result.txt
- echo "Success Test!"
.gitlab-ci.yml 파일이 존재하기만 하면 모든 Push 이벤트에 대해 실행하는 것이 기본 설정workflow: # 파일의 최상단
rules:
- if: $CI_COMMIT_BRANCH == "main"
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: always[Stage: build] [Stage: test] [Stage: deploy]
+-----------+ +-----------+ +-------------+
| Job A | ------> | Job C | -----> | Job E |
+-----------+ (대기) +-----------+ (대기 +-------------+
| Job B | | Job D |
+-----------+ +-----------+
(A, B 동시 실행) (C, D 동시 실행)needs 키워드 : 스테이지 순서를 무시하고 needs에 적힌 잡이 끝나면 바로 시작같은 stage인 build_a 와 build_b는 동시에 실행된다.
- 만약 needs가 없었다면 : test 잡은 build_a와 build_b가 모두 종료되어야 실행된다.
(마찬가지로 test_a와 test_b 동시에 시작)
- 하지만 needs가 있기 때문에 : build b가 끝나지 않았어도 build_a가 끝났다면
test_a, deploy_a가 실행될 수 있다.
stages:
- build
- test
- deploy
image: alpine
build_a:
stage: build
script:
- echo "This job builds something quickly."
build_b:
stage: build
script:
- echo "This job builds something else slowly."
test_a:
stage: test
needs: [build_a]
script:
- echo "This test job will start as soon as build_a finishes."
- echo "It will not wait for build_b, or other jobs in the build stage, to finish."
test_b:
stage: test
needs: [build_b]
script:
- echo "This test job will start as soon as build_b finishes."
- echo "It will not wait for other jobs in the build stage to finish."
deploy_a:
stage: deploy
needs: [test_a]
script:
- echo "Since build_a and test_a run quickly, this deploy job can run much earlier."
- echo "It does not need to wait for build_b or test_b."
environment: production
deploy_b:
stage: deploy
needs: [test_b]
script:
- echo "Since build_b and test_b run slowly, this deploy job will run much later."
environment: production
자주 쓰이는 시스템 변수 (Predefined Variables)
# 커밋 해시값의 앞 8자리. 버전 태그로 가장 많이 씀.
# 1a2b3c4d
CI_COMMIT_SHORT_SHA
# 현재 빌드 중인 브랜치 이름 또는 태그 이름.
# main, develop
CI_COMMIT_REF_NAME
# 파이프라인이 왜 실행됐는지 (푸시, 웹 클릭, 스케줄 등).
# push, web, schedule
CI_PIPELINE_SOURCE
# 이 프로젝트 전용 도커 이미지 저장소 주소.
# registry.gitlab.com/group/project
CI_REGISTRY_IMAGE
# 레지스트리에 로그인하기 위한 일회용 ID.
# gitlab-ci-token
CI_REGISTRY_USER
# 레지스트리에 로그인하기 위한 일회용 패스워드.
# (자동 생성된 토큰)
CI_REGISTRY_PASSWORD
stages:
- test
- package
- deploy
# 전역 변수 설정
variables:
# 생성될 이미지의 풀 네임 (예: registry.gitlab.com/my-group/my-project:1a2b3c4d)
DOCKER_IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
DOCKER_IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
unit_test_job:
stage: test
image: python:3.9-slim # 테스트를 수행할 환경(이미지)
script:
- echo "테스트를 시작합니다..."
- python --version
- echo "테스트 통과!"
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: always
build_push_docker_job:
stage: package
image: docker:24.0.5
services:
- docker:24.0.5-dind # Docker-in-Docker: 컨테이너 안에서 도커를 실행하기 위한 서비스
# 변수: DIND 사용 시 TLS 인증 끄기 (설정 간소화)
variables:
DOCKER_TLS_CERTDIR: ""
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
script:
- echo " 도커 이미지를 빌드합니다..."
- docker build -t $DOCKER_IMAGE_TAG -t $DOCKER_IMAGE_LATEST .
- echo " GitLab 레지스트리로 푸시합니다..."
- docker push $DOCKER_IMAGE_TAG
- docker push $DOCKER_IMAGE_LATEST
after_script:
- docker logout
deploy_prod_job:
stage: deploy
image: alpine:latest
# environment: GitLab UI의 'Environments' 메뉴에서 배포 이력을 볼 수 있게 해줌
environment:
name: production
url: http://10.10.10.3
script:
- echo "운영 서버(Production)에 배포를 시작합니다..."
- echo "배포할 이미지 $DOCKER_IMAGE_TAG"
- echo "SSH 접속 및 컨테이너 교체 명령 실행 중..."
- echo "배포 완료!"
# 수동 배포 설정 (실수로 배포되는 것 방지)
when: manual
간단한 Django 프로젝트 준비
Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py makemigrations && python manage.py migrate
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
프로젝트 - Settings - CI/CD - Variables 등록

gitlab-ci.yml
stages:
- build-push
variables:
DOCKER_TLS_CERTDIR: ""
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
build_and_push_to_ecr:
stage: build-push
image: docker:latest
before_script:
- echo "AWS CLI 설치 및 ECR 로그인을 준비합니다..."
- apk add --no-cache aws-cli
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_DOMAIN
script:
- echo "Django 이미지를 빌드합니다..."
- docker build -t $ECR_REGISTRY_URL:$IMAGE_TAG -t $ECR_REGISTRY_URL:latest .
- echo "AWS ECR로 이미지를 푸시합니다..."
- docker push $ECR_REGISTRY_URL:$IMAGE_TAG
- docker push $ECR_REGISTRY_URL:latest
- echo "배포(푸시) 완료!"
ECR에 이미지 배포 완료

aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_DOMAIN
[AWS_ACCESS_KEY_ID와SECRET_KEY가 로드되고 사용되는 시점 ]
- 이 두 값은 명령어 라인에는 직접 적혀있지 않지만, GitLab CI 설정에서 등록했기 때문에 리눅스 환경 변수 영역에 존재하고 있다.
- 로드 시점
aws ecr get-login-password명령어가 실행되면, AWS CLI 프로그램이 메모리에 로드된다.- 이 프로그램은 실행되자마자 내부적으로 자격 증명 공급자 체인 로직을 수행한다.
- 가장 먼저 현재 쉘의 환경 변수를 스캔한다. 여기서 AWS_ACCESS_KEY_ID와 AWS_SECRET_ACCESS_KEY 값을 찾아내어 메모리로 읽어 들인다.
- 사용 시점
- AWS CLI는 ECR 서비스에 임시 인증 토큰을 발급해달라는 API 요청을 생성한다.
- 이때 메모리에 로드한 Secrey Key를 사용하여 이 API 요청 패킷에 전자 서명(SigV4 Signing)을 한다.
- 서명된 요청을 AWS ECR 서버로 전송한다. AWS 서버는 서명을 검증하여 이 요청이 유효한 사용자가 보낸 것임을 확인한다.
| 를 통해 AWS CLI가 표준 출력으로 뱉어낸 인증 토큰을 Docker CLI의 표준 입력으로 전달
[ Docker 로그인 과정 ]- Docker CLI 실행
docker login명령어가 실행된다.--username AWS: ID는 무조건 AWS라는 고정된 문자열을 사용한다. (ECR의 규칙)--password-stdin: 비밀번호는 키보드로 치지 않고, 파이프를 통해 들어오는 표준 입력(stdin)에서 읽겠다는 옵션- $ECR_DOMAIN : 인증할 서버 주소
- 인증 처리
- Docker 클라이언트는 전달받은 비밀번호와 ID를 가지고 $ECR_DOMAIN 서버에 HTTPS 요청을 보낸다.
- ECR 서버는 토큰을 검증하고, 유효하다면 로그인 성공 응답을 보낸다.
- 결과 저장
- 로그인이 성공하면 Docker는 로컬 파일 시스템(
~/.docker/config.json)에 이 도메인에 접근할 때는 이 토큰을 사용하라는 정보를 저장한다.- 이후 docker push 명령어가 실행될 때, Docker는 이 config.json 파일을 참조하여 저장된 토큰을 헤더에 실어 이미지를 업로드한다.
include:
- template: Auto-DevOps.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.ymlinclude:
- project: 'my-comnay/devops/ci-templates'
ref: main
file: 'build/docker-build.yml'.으로 시작) : .templatess 처럼 Job 이름 앞에 .을 붙이면 파이프라인에서 실제로 실행되지 않는다. 오직 참조용으로만 존재한다.& )와 Alias( * )&이름 : 이 코드 블록을 '이름'으로 저장하겠다는 의미*이름 : 저장된 '이름'의 코드 블록을 여기에 그대로 복사해 넣겠다는 의미<<: : 저장된 블록의 키-값 쌍을 현재 위치에 병합
# 템플릿
.slack_notification_template: &slack_notification
- |
(apk add --no-cache curl || (apt-get update && apt-get install -y curl)) 2>&1 >/dev/null || true
# curl 설치 확인
echo "Curl version: $(curl --version | head -n 1)"
# 현재 작업 상태에 따라 메시지 색상과 아이콘 결정
if [ "$CI_JOB_STATUS" == "success" ]; then
COLOR="#366a64"
ICON="✅"
STATUS_MSG="성공"
else
COLOR="#ff0000"
ICON="❌"
STATUS_MSG="실패"
fi
# Slack으로 보낼 JSON 데이터 구성
PAYLOAD=$(cat <<EOF
{
"attachments": [
{
"color": "$COLOR",
"title": "$ICON 빌드 알림 : $CI_PROJECT_NAME",
"title_link": "$CI_PIPELINE_URL",
"text": "작업: *$CI_JOB_NAME*\n상태: *$STATUS_MSG*\n요청자: $GITLAB_USER_NAME",
"mrkdwn_in": ["text"]
}
]
}
EOF
)
# webhook 전송
curl -X POST -H 'Content-type: application/json' --data "$PAYLOAD" "$SLACK_WEBHOOK_URL"
# 실제 빌드 작업 정의
build_job:
stage: build
image: alpine:latest
script:
- echo "Starting build..."
- sleep 2
- echo "Success build"
after_script: *slack_notification # 템플릿 사용
deploy_job:
stage: deploy
image: alpine:latest
script:
- echo "Starting deploy"
- exit 1 # 강제 실패
after_script: *slack_notification
.templates:
.deploy_templates: &deploy_template
image: alpine:latest
variables: # 기본값
DEPLOY_ENV: "staging"
SERVER_IP: "10.10.10.10"
script:
- echo "배포 시작. 환경=$DEPLOY_ENV"
- echo "대상 서버 $SERVER_IP로 접속 중..."
- echo "배포 스크립트 실행 완료"
deploy_dev:
stage: deploy
<<: *deploy_template # 템플릿 내용을 여기세 복사
variables: # 템플릿의 변수 덮어쓰기
DEPLOY_ENV: "deployment"
SERVER_IP: "10.10.10.3"
deploy_prod:
stage: deploy
<<: *deploy_template
variables:
DEPLOY_ENV: "production"
SERVER_IP: "192.168.10.100"
when: manual
Condition 분기 및 include 전략 (환경별 파일 분리)
include: rules를 사용하면, 특정 브랜치일 때 특정 설정 파일만 가져오는 식으로 필요한 파일만 가져와서 사용할 수 있다.stages:
- build
- test
- deploy
include:
- local: '/ci/common.yml'
- local: '/ci/dev-deploy.yml'
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
- local: '/ci/prod-deploy.yml'
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
develop 브랜치에 푸시하면 : common.yml + dev-deploy.yml 내용만 실행됨. (운영 배포 실수 방지)
main 브랜치에 푸시하면 : common.yml + prod-deploy.yml 내용만 실행됨.
<<: *anchor로 복사된 내용들이 전부 하나로 합쳐진 최종 결과를 보여준다.curl --header "PRIVATE-TOKEN: glpat-내토큰값..." \
--data "{\"content\": \"$(cat .gitlab-ci.yml | sed 's/"/\\"/g')\"}" \
"https://gitlab.example.com/api/v4/ci/lint"Reference : https://somaz.tistory.com/322