Gitlab Runner를 이용한 CI/CD 구축하기
작성자 : 오예환 | 작성일 : 2023-07-25 | 수정일 : 2023-07-25
CI/CD를 구축하게 된 이유
진행하던 프로젝트를 POC 발표를 위해 갑작스럽게 배포하게 되었다. 급하게 배포를 하느라 CI/CD 같은 것은 생각하지 못했고, 실제 온프레미스로 존재하는 서버에 직접 접속하여 빌드부터 테스트, 배포까지 일일이 손으로 진행했다.
배포 과정은 이랬다.
배포 프로세스
[1] 코드 작업을 완료하고 변경사항을 main 브랜치에 push 한다.
[2] 테스트 서버에 ssh로 접근하여 main 브랜치에서 최신 코드를 가져온다.
[3] 테스트 서버에서 프로젝트를 빌드한다.
[4] 테스트 서버에서 배포 되고 있던 컨테이너에 빌드된 폴더를 가져와서 테스트한다.
[5] 테스트 서버에서 이상이 없을 경우 실행되고 있는 컨테이너를 도커 이미지로 빌드한다.
[6] 도커 이미지를 Docker Hub에 push 한다.
[7] 배포 서버에 ssh로 접근하여 이미지를 pull 받는다.
[8] 배포 서버에서 실행 중인 배포 컨테이너를 종료하고 새로운 이미지로 컨테이너를 실행한다.
배포 프로세스
코드 작업이 끝날 때마다 위 8단계를 반복했다. CI/CD를 구축하게 된 가장 큰 이유는 배포할 때마다 너무 많은 시간과 에너지를 뺏겨서였다. 그래서 CI/CD를 구축하여 이 작업들을 자동화하고자 했다.
사실 또 하나의 이유가 있었다. 테스트 서버에서 도커 컨테이너를 이미지로 빌드할 때마다 이미지 크기가 비정상적으로 커지는 문제가 있었다. 이 문제도 해결할 겸 CI/CD를 구축하기로 했다. (해당 문제의 원인은 도커 이미지 레이어 때문이었다.)
GitLab Runner란?
GitLab Runner는 GitLab에서 제공하는 오픈 소스 CI/CD 실행 에이전트다. CI/CD는 Continuous Integration (CI) 및 Continuous Deployment (CD)의 약어로, 개발자들이 코드 변경 사항을 자동으로 빌드, 테스트, 배포하는 프로세스를 뜻한다.
간단하게 말하면 GitLab Runner는 하나의 프로그램이라고 생각하면 된다. GitLab Runner를 실행시키고 GitLab에 등록하면, GitLab은 코드 저장소에 변경이 있을 때마다 GitLab Runner에게 알린다. GitLab Runner는 해당 소식을 받으면 개발자가 미리 작성해 둔 스크립트를 실행한다.
밑에서 좀 더 상세하게 설명해보겠다.
GitLab과 GitLab Runner 사이에는 어떤 일이 벌어질까?
사실 CI/CD를 구성하면서 GitLab과 GitLab Runner 사이에 어떤 일이 벌어지는지가 가장 궁금했다. 그래서 구글링, 공식 문서, 블로그, ChatGPT의 도움을 받아서 정보를 모았고, 방대한 정보들을 내가 이해한 대로 정리해보았다. 그렇기 때문에 틀릴 수 있다. 그럼 매우 간소화된 그림과 함께 설명을 해보겠다.
1. GitLab에서 제공하는 토큰으로 GitLab Runner를 등록
토큰 제공
GitLab Runner를 처음 실행하면 프로젝트를 등록해야 한다. 이때 GitLab 레포지토리의 URL과 토큰(GitLab에서 제공)을 입력하면, GitLab Runner는 입력한 정보들을 이용하여 GitLab에 등록 요청을 보낸다.
2. 유효한 토큰일 경우 등록 & GitLab Runner의 정보 저장
GitLab Runner의 정보 저장
GitLab에서 토큰 정보를 확인한 후 토큰이 유효하면 등록하고 GitLab Runner의 정보를 저장한다.
그 후 성공적으로 등록되었다고 응답한다. 해당 과정을 거치면 [GitLab 레포지토리 → Settings → CI/CD → Runners]에서 아래 그림처럼 성공적으로 Runner가 연동된 것을 확인할 수 있다.
(아래 그림에서 Set up a specific Runner manually 부분을 보면 URL과 토큰 정보가 있다. 이것을 1번 과정에서 이용한다.)
토큰 정보
3. 변경사항이 발생할 경우 GitLab에서 "job"이라는 메시지와 함께 작업 정보를 GitLab Runner에게 전달
job 메세지 전달
GitLab 서버는 코드 저장소를 계속 지켜보다가 변경 사항을 감지하면 GitLab Runner에게 Job 메시지를 보낸다. GitLab Runner는 해당 Job 메시지를 받아서 CI/CD 파이프라인을 실행한다.
여기서 **"Job 메시지"**란 우리가 작성한 .gitlab-ci.yml 파일에 정의된 CI/CD 파이프라인을 참조하여 만들어진 메시지다. 본론에서 더 자세히 설명하겠지만, .gitlab-ci.yml 파일에 대해 잠시 설명하자면 아래 그림과 같이 프로젝트 루트에 위치해 있다.
.gitlab-ci.yml 위치
해당 파일은 GitLab 레포지토리 페이지에서 CI/CD configuration을 이용해서 생성하거나, 로컬 스토리지에서 파일을 직접 만들고 commit → push 하는 방법이 있다.
4. .gitlab-ci.yml 파일에 정의된 대로 파이프라인을 실행 후 결과를 GitLab 서버에 응답
.gitlab-ci.yml 작업 실행
GitLab Runner는 .gitlab-ci.yml으로 만들어진 Job 메시지를 받아서 파이프라인에 작성된 작업들을 실행하고, 결과를 다시 GitLab 서버에 보낸다.
여기까지가 대략적인 GitLab과 GitLab Runner 사이에 일어나는 일이다. 본론에서는 더 자세하게 설명해보겠다.
어디까지 자동화할 것인가?
사실 배포 서버는 백엔드팀에서 관리하는 서버였고, 프론트엔드와 백엔드는 각각의 컨테이너에서 실행되고 있었다.
앞에서 "배포 서버에서 프론트엔드 이미지를 pull 받아서 컨테이너를 실행하고 있다"고 설명했다. 이 과정을 진행할 때 나는 배포 서버에 직접 ssh로 접근하여 프론트엔드 이미지를 pull 받고 컨테이너를 실행시켜주고 있었다.
하지만 이렇게 서버에 직접 접근하여 작업하는 것은 여러 가지 이유로 불안정하다고 생각했다. 그래서 백엔드팀과 협의하여 배포 서버는 백엔드팀에서 관리하고, 프론트엔드팀은 Docker Hub에 이미지를 올리는 것까지만 하기로 했다.
자동화 영역
그래서 위 그림에서 빨간색 테두리로 표시한 부분만 작업하기로 했다. GitLab 서버에 코드가 push 되면 빌드 → 이미지 생성 → 이미지 업로드까지를 CI/CD 파이프라인으로 작성했다.
이제 직접 CI/CD를 구축해보자!
CI/CD 구축하기
1. GitLab Runner 설치하기
우선 GitLab Runner를 설치해야 한다. 운영체제별로 설치 방법이 다르기 때문에 위 공식 문서를 참고하면 좋다. Docker를 이용하여 설치한다면 아래 내용을 따라해도 좋다.
Dockaer Container 설치
- Create the Docker volume:
docker volume create gitlab-runner-config- Start the GitLab Runner container using the volume we just created:
docker run -d --name gitlab-runner --restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v gitlab-runner-config:/etc/gitlab-runner \
gitlab/gitlab-runner:latest2. GitLab과 GitLab Runner 연동하기
GitLab & GitLab Runner 연동
Token 확인하기
GitLab 레포지토리 → Settings → CI/CD → Runners의 Expand를 클릭하면 토큰을 확인할 수 있다. Expand 클릭 시 3개의 러너가 존재하는데 우리는 Specific Runners를 이용한다.
token 확인하기
token 확인하기
실행해 두었던 GitLab Runner 컨테이너에 진입한다.
docker container exec -it gitlab-runner bash러너에 프로젝트를 등록한다. 등록 방법은 비대화식 모드와 대화식 모드가 존재한다. 나는 대화식 모드로 입력했다.
// 비대화식 모드
// ID와 TOKEN에는 1번 과정에서 Specific Runners부분에 작성되어있는 URL, token을 입력한다.
gitlab-runner register -n \
--url http://<IP> \
--registration-token <TOKEN> \
--description gitlab-runner \
--executor docker \
--docker-image docker:latest \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock
// 대화식 모드
gitlab-runner register
대화식 모드로 진행하면 하나씩 입력하도록 입력 메시지가 나온다. 잘 읽어보고 입력하면 된다.
URL → Token → description → tag → executor 순으로 입력하라고 나오는데, URL과 token은 Specific Runners 부분에 작성되어 있는 URL, token을 입력하면 되고, description과 tag는 그냥 엔터로 넘어가도 된다. executor에서는 docker를 입력하고, default docker image는 docker:latest로 입력한다.
모두 정상적으로 입력하면 등록이 성공적으로 되었다는 메시지가 나온다.
GitLab 레포지토리 → Settings → CI/CD → Runners에서 등록을 확인하자. (초록불: 성공, 빨간불: 실패)
token 확인하기
gitlab-runner 컨테이너 안에서 config.toml 파일 수정
sudo vi /etc/gitlab-runner/config.tomlconfig.toml 파일에서 "privileged = true"를 추가해줘야 한다.
config.toml 파일에 진입하면 아래 코드와 같은 형식일 것이다. (아래 코드는 예시이며 실제와 다를 수 있음. 형태만 참고)
[runners.docker] 밑에 privileged = false로 되어 있다면 true로 바꿔주고, 없다면 추가해주자.
[[runners]]
name = "My Docker Runner"
url = "https://gitlab.com/"
token = "YOUR_REGISTRATION_TOKEN"
executor = "docker"
[runners.docker]
tls_verify = false
image = "docker:latest"
privileged = true
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache"]
shm_size = 0해당 속성을 수정하는 이유는 나중에 GitLab Runner에서 파이프라인을 실행할 때 스크립트로 실행된 도커 안에서 도커를 실행해야 하는 경우가 발생하는데, 이를 Docker in Docker(DinD) 모드라고 한다.
DinD 모드로 설정하려면 GitLab Runner 컨테이너를 실행할 때 호스트의 Docker 소켓을 공유해야 하고, privileged = true로 설정하여 호스트의 Docker 데몬과 연동되도록 해야 한다.
소켓 공유의 경우 처음 GitLab Runner를 설치할 때 설정해 주었다. (-v /var/run/docker.sock:/var/run/docker.sock)
3. .gitlab-ci.yml 파일 작성하기
.gitlab-ci.yml 파일을 만드는 방법은 두 가지라고 앞서 소개했다.
첫 번째는 GitLab 레포지토리에 있는 CI/CD configuration 버튼을 눌러서 생성하는 방법이고, 두 번째는 로컬 스토리지에서 직접 파일을 만들어 작성하고 레포지토리에 push 하는 방법이다. 나는 로컬 스토리지에서 직접 파일을 만들어서 작성했다.
작업 중인 프로젝트의 root에서 .gitlab-ci.yml 파일을 만들자.
프로젝트의 .gitlab-ci.yml 파일 내용은 다음과 같다.
stages:
- build
- depoly
build:
stage: build
image: node:latest
script:
- echo 'start build...'
- npm install
- CI="false" npm run build
artifacts:
paths:
- build/
only:
- main
depoly:
stage: depoly
image: docker:latest
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
services:
- name: docker:dind
alias: docker
command: ["--tls=false"]
before_script:
- echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USERNAME" --password-stdin
script:
- echo 'start depoly...'
- docker login
- docker build . --platform linux/amd64 --tag ${CONTAINER_IMAGE}:latest
- docker push ${CONTAINER_IMAGE}:latest
- docker container ls -a
- docker images
only:
- main단계별 설명
stages
stages는 CI/CD 파이프라인에서 정의된 작업 단계들의 순서를 지정하는 부분이다. 파이프라인이 실행될 때 각 단계는 stages에서 지정된 순서대로 진행된다.
우리의 .gitlab-ci.yml 파일은 build와 deploy 두 단계로 구성되어 있다. 파이프라인의 실행은 "build" → "deploy" 순서로 진행된다.
stage - build
build: // 해당 stage의 이름
stage: build // stages에서 작성한 build 단계가 해당 섹션임을 연결
image: node:latest // 베이스 이미지
script: // 도커에서 실행될 스크립트
- echo 'start build...'
- npm install
- CI="false" npm run build
artifacts:
paths:
- build/ // 빌드 폴더를 캐싱
only:
- main // 메인 브랜치에서 수정사항이 생길 때만 실행해당 stage가 실행되면 node 베이스 이미지로 생성된 컨테이너가 실행되고, 그 컨테이너 안에는 레포지토리에 있는 파일들이 존재한다. 따라서 npm install을 통해서 프로젝트 소스 코드에 필요한 패키지들을 설치하고 빌드한다.
아래 이미지는 stage에서 ls를 통해 어떤 파일이 있는지 확인해본 결과다.
ls 결과
npm run build시 CI="false"를 작성한 이유는 빌드 중 경고(warning)를 에러로 처리하는 것을 방지하기 위해서다.
artifacts 명령어는 build 폴더를 캐싱하는 명령어다.
해당 명령어가 필요한 이유는 각각의 stage는 독립된 컨테이너를 실행하기 때문에, 모든 스크립트를 실행하고 나면 컨테이너는 종료되며 build 폴더는 사라진다. 따라서 artifacts 명령어를 사용하여 GitLab Runner가 실행되는 도커 컨테이너의 /builds/[그룹이름]/[프로젝트이름]에 캐싱되며, GitLab 서버에도 저장된다. (GitLab 서버에는 일정 기간 동안 보관)
build
build stage의 컨테이너가 실행되고 build 폴더가 생긴다.
deploy
build stage의 컨테이너가 종료되고 사라진다. 이때 artifacts를 설정하지 않으면 build 폴더도 사라지기 때문에 artifacts 명령어로 deploy stage의 컨테이너에 build 폴더를 전달한다.
stage - deploy
docker image push
depoly:
stage: depoly
image: docker:latest
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
services:
- name: docker:dind
alias: docker
command: ["--tls=false"]
before_script:
- echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USERNAME" --password-stdin
script:
- echo 'start depoly...'
- docker login
- docker build . --platform linux/amd64 --tag ${CONTAINER_IMAGE}:latest
- docker push ${CONTAINER_IMAGE}:latest
- docker container ls -a
- docker images
only:
- maindeploy stage에서는 Docker Hub에 로그인을 진행하고 build 폴더를 기존에 만들어두었던 Dockerfile을 이용해서 이미지로 빌드한다.
$DOCKER_HUB_PASSWORD, $DOCKER_HUB_USERNAME, ${CONTAINER_IMAGE}는 GitLab → Settings → CI/CD → Variables에서 설정할 수 있다.
variables
scripts에 docker build .은 컨테이너 안에 생성되는 프로젝트 파일들 중 Dockerfile을 이용하여 이미지를 만들겠다는 의미다. Dockerfile 코드는 다음과 같다.
FROM nginx:latest
# COPY default.conf /etc/nginx/conf.d/default.conf
RUN echo 'server {' > /etc/nginx/conf.d/default.conf \
&& echo ' listen 80 default_server;' >> /etc/nginx/conf.d/default.conf \
&& echo ' listen [::]:80 default_server;' >> /etc/nginx/conf.d/default.conf \
&& echo ' root /usr/share/nginx/html/build;' >> /etc/nginx/conf.d/default.conf \
&& echo ' index index.html;' >> /etc/nginx/conf.d/default.conf \
&& echo ' server_name rgnews.app;' >> /etc/nginx/conf.d/default.conf \
&& echo ' location / {' >> /etc/nginx/conf.d/default.conf \
&& echo ' try_files $uri $uri/ /index.html =404;' >> /etc/nginx/conf.d/default.conf \
&& echo ' }' >> /etc/nginx/conf.d/default.conf \
&& echo ' location /v2/models {' >> /etc/nginx/conf.d/default.conf \
&& echo ' proxy_pass http://192.168.159.96:8000;' >> /etc/nginx/conf.d/default.conf \
&& echo ' }' >> /etc/nginx/conf.d/default.conf \
&& echo '}' >> /etc/nginx/conf.d/default.conf
COPY build /usr/share/nginx/html/build
CMD ["nginx", "-g", "daemon off;"]베이스 이미지로 nginx를 사용했고, 만들어질 이미지의 컨테이너 안에 존재하는 default.conf 파일을 수정하는 코드를 작성한 뒤 build 폴더를 복사해왔다. 마지막으로 백그라운드로 nginx를 실행하는 코드를 작성해주었다.
4. 파이프라인 실행 확인
.gitlab-ci.yml 파일을 작성한 뒤 main 브랜치에 변화가 생기면, GitLab → CI/CD → Pipelines에서 파이프라인의 진행 상황을 확인할 수 있다.
pipelines
이렇게 GitLab Runner를 이용한 CI/CD 구축을 해보았다. CI/CD 구축 후 배포 컨테이너를 실행하는 명령어는 다음과 같다.
docker run -d -p 8080:80 --name test \
-e BACKEND_HOST=http://192.168.159.38:8080 \
-e TTS_HOST=http://192.168.158.38:8000 \
-d rgnewstmax/front:latest배운 점
처음에는 반복적인 수동 배포 작업을 개선하기 위해 시작했지만, CI/CD를 구축하면서 Docker in Docker, GitLab Runner의 동작 원리, artifacts 개념 등 많은 것을 배울 수 있었다. 특히 privileged 설정이나 소켓 공유 같은 보안과 관련된 설정들의 의미를 제대로 이해하게 되었다.
이 글을 작성하면서 단순히 "동작하니까 됐다"는 식으로 넘어갔던 부분들을 다시 찾아보고 정리하게 되었다. 특히 Docker in Docker가 왜 필요한지, privileged 모드가 실제로 어떤 의미를 갖는지 등 그동안 대충 알고 있었던 개념들을 명확하게 이해할 수 있었다.
요약
이 글에서는 수동 배포 과정을 자동화하기 위해 GitLab Runner를 활용한 CI/CD 파이프라인 구축 경험을 공유했다.
주요 내용:
-
배포 과정 자동화: 8단계에 걸친 수동 배포 프로세스를 자동화하여 시간과 에너지 절약
-
GitLab Runner 동작 원리: GitLab과 GitLab Runner 간의 통신 방식과 Job 메시지 처리 과정
-
Docker in Docker(DinD) 설정: CI/CD 파이프라인에서 컨테이너 내부에서 Docker를 실행하기 위한 privileged 설정 및 소켓 공유
-
.gitlab-ci.yml 작성: build와 deploy 두 단계로 구성된 파이프라인 스크립트 작성
-
artifacts 활용: stage 간 빌드 결과물을 공유하기 위한 캐싱 메커니즘
CI/CD 구축을 통해 코드 push부터 Docker Hub 이미지 업로드까지의 전체 과정을 자동화했으며, 이를 통해 배포 과정의 효율성을 크게 향상시킬 수 있었다.