수동 배포를 하다 보면 실수가 생긴다. 빌드를 까먹거나, 테스트를 안 돌리거나, 잘못된 브랜치를 배포하거나. GitLab CI/CD를 쓰면 코드를 push할 때 자동으로 테스트 → 빌드 → 배포까지 돌릴 수 있다. .gitlab-ci.yml 파일 하나면 된다.
기본 구조
프로젝트 루트에 .gitlab-ci.yml을 만든다:
stages:
- test
- build
- deploy
variables:
NODE_VERSION: "20"
test:
stage: test
image: node:${NODE_VERSION}-alpine
script:
- npm ci
- npm run lint
- npm test
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
build:
stage: build
image: node:${NODE_VERSION}-alpine
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
only:
- main
- develop
deploy:
stage: deploy
image: alpine:latest
script:
- apk add --no-cache rsync openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts
- rsync -avz --delete dist/ $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/
only:
- main
when: manual
stages에서 순서를 정의하고, 각 job이 어떤 stage에 속하는지 지정한다. 같은 stage의 job은 병렬로 실행된다.
환경변수 설정
SSH 키, 서버 주소 같은 민감한 정보는 절대 yml 파일에 넣으면 안 된다. GitLab에서:
Settings → CI/CD → Variables에서 추가:
SSH_PRIVATE_KEY: 배포용 SSH 개인 키 (Type: File 또는 Variable, Protected + Masked)DEPLOY_HOST: 서버 주소 (예:192.168.1.100)DEPLOY_USER: SSH 사용자명DEPLOY_PATH: 배포 경로
Protected를 체크하면 protected 브랜치에서만 사용 가능하고, Masked를 체크하면 로그에 값이 노출되지 않는다.
Docker 이미지 빌드 + 배포
컨테이너 기반으로 배포한다면:
build-image:
stage: build
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
deploy-docker:
stage: deploy
image: alpine:latest
script:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts
- ssh $DEPLOY_USER@$DEPLOY_HOST "
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY &&
docker pull $CI_REGISTRY_IMAGE:latest &&
cd /srv/app && docker compose up -d --no-deps app
"
only:
- main
when: manual
GitLab의 Container Registry를 활용하면 이미지 저장소를 따로 만들 필요가 없다. $CI_REGISTRY 같은 변수는 GitLab이 자동으로 제공한다.
GitLab Runner 설치
self-hosted GitLab을 쓰고 있다면 Runner를 직접 설치해야 한다:
# Docker 기반 Runner 설치
docker run -d --name gitlab-runner --restart always \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:latest
Runner 등록:
docker exec -it gitlab-runner gitlab-runner register
URL과 토큰을 물어보는데, GitLab 프로젝트 Settings → CI/CD → Runners에서 확인할 수 있다. executor는 docker를 선택하고, 기본 이미지는 alpine:latest를 넣으면 된다.
캐시와 아티팩트
빌드 시간을 줄이려면 캐시를 활용한다. node_modules를 캐싱하면 npm ci 시간이 확 줄어든다:
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
package-lock.json이 바뀔 때만 캐시가 갱신된다. 아티팩트는 stage 간에 파일을 전달할 때 쓴다. build에서 만든 dist/를 deploy에서 사용하는 식이다.
브랜치별 전략
개발 흐름에 맞게 파이프라인을 분기할 수 있다:
deploy-staging:
stage: deploy
script:
- echo "Deploying to staging..."
environment:
name: staging
url: https://staging.example.com
only:
- develop
deploy-production:
stage: deploy
script:
- echo "Deploying to production..."
environment:
name: production
url: https://example.com
only:
- main
when: manual
when: manual은 파이프라인에서 수동으로 클릭해야 실행된다. 프로덕션 배포는 실수 방지를 위해 수동으로 해두는 게 안전하다. environment를 설정하면 GitLab UI에서 배포 현황을 볼 수 있다.
CI/CD를 한번 세팅하면 배포 스트레스가 사라진다. push만 하면 나머지는 파이프라인이 알아서 하니까.