GitLab CI 서비스(Services)
GitLab CI 서비스(Services)
CI/CD를 구성할 때 image 키워드로 잡이 실행될 컨테이너를 만드는 데 쓰는 이미지를 지정해요. 여기에 services 키워드를 쓰면 추가 이미지를 하나 더 지정할 수 있는데, 이 이미지는 첫 번째 컨테이너가 사용할 수 있는 추가 컨테이너를 만드는 데 쓰여요. 두 컨테이너는 서로 접근할 수 있고, 잡이 실행되는 동안 서로 통신할 수 있어요.
서비스 이미지로는 어떤 애플리케이션이든 실행할 수 있지만, 가장 흔한 용도는 데이터베이스 컨테이너를 실행하는 거예요. 예를 들면:
- MySQL
- PostgreSQL
- Redis
- JSON API를 제공하는 마이크로서비스의 예인 GitLab
서비스 간 네트워킹을 활성화하려면 FF_NETWORK_PER_BUILD를 true로 설정해야 해요. 이 플래그가 없으면 서비스가 제대로 동작하지 않을 수 있어요. 자세한 내용은 기능 플래그 문서를 참고하세요.
같은 컨테이너 안의 서비스 컨테이너에 대한 자세한 설명은 Docker 컨테이너 연결(링크) 문서를 참고해요.
출처: 문서
본문
서비스가 잡에 연결되는 방식
컨테이너 연결이 어떻게 동작하는지 자세히 이해하려면 컨테이너 연결하기를 읽어 보세요.
애플리케이션에 mysql을 서비스로 추가하면, 그 이미지로 만든 컨테이너가 잡 컨테이너에 연결돼요.
MySQL용 서비스 컨테이너는 mysql이라는 호스트 이름으로 접근할 수 있어요. 데이터베이스 서비스에 접근하려면 소켓이나 localhost 대신 mysql이라는 호스트에 접속하세요. 자세한 내용은 서비스 접근하기를 확인해요.
Docker 통합 워크플로
잡 실행 중에 Docker가 수행하는 단계를 높은 수준에서 정리하면 다음과 같아요.
- 서비스 컨테이너를 생성한다: mysql, postgresql, mongodb, redis.
- config.toml과 빌드 이미지의 Dockerfile에 정의된 모든 볼륨을 저장할 캐시 컨테이너를 생성한다(이전 예시의 ruby:4.0).
- 빌드 컨테이너를 생성하고 모든 서비스 컨테이너를 빌드 컨테이너에 연결한다.
- 빌드 컨테이너를 시작하고 잡 스크립트를 컨테이너로 보낸다.
- 잡 스크립트를 실행한다.
/builds/group-name/project-name/에 코드를 체크아웃한다.- .gitlab-ci.yml에 정의된 각 단계를 실행한다.
- 빌드 스크립트의 종료 상태를 확인한다.
- 빌드 컨테이너와 생성된 모든 서비스 컨테이너를 제거한다.
서비스 상태 확인 워크플로
서비스는 네트워크로 접근 가능한 추가 기능을 제공하도록 설계됐어요. MySQL이나 Redis 같은 데이터베이스일 수도 있고, Docker-in-Docker(DinD)를 쓸 수 있게 해주는 docker:dind일 수도 있어요. 사실상 CI/CD 잡이 진행되는 데 필요하고 네트워크로 접근되는 것이면 무엇이든 될 수 있죠.
이것이 제대로 동작하는지 확인하기 위해 러너는:
- 컨테이너가 기본적으로 노출하는 포트를 확인한다.
- 해당 포트에 접근할 수 있을 때까지 기다리는 특별한 컨테이너를 시작한다.
확인의 두 번째 단계가 실패하면 다음 경고를 출력해요: *** WARNING: Service XYZ probably didn't start properly. 이 문제는 다음과 같은 이유로 발생할 수 있어요.
- 서비스에 열려 있는 포트가 없는 경우.
- 서비스가 타임아웃 전에 제대로 시작되지 않아 포트가 응답하지 않는 경우.
대부분의 경우 이 경고는 잡에 영향을 주지만, 경고가 출력돼도 잡이 성공하는 상황이 있을 수 있어요. 예를 들면:
- 경고가 발생한 직후 서비스가 시작됐고, 잡이 처음부터 연결된 서비스를 사용하지 않는 경우. 이때 잡이 서비스에 접근해야 하는 시점에는 서비스가 이미 연결을 기다리고 있을 수 있어요.
- 서비스 컨테이너가 제공하는 네트워킹 서비스는 없지만 잡의 디렉터리로 뭔가를 하는 경우(모든 서비스는 잡 디렉터리를 /builds 아래 볼륨으로 마운트함). 이 경우 서비스는 제 역할을 하고, 잡이 그 서비스에 접속하려 하지 않으므로 실패하지 않아요.
서비스가 성공적으로 시작되면 before_script가 실행되기 전에 시작돼요. 즉, 서비스에 질의하는 before_script를 작성할 수 있죠.
서비스는 잡이 실패하더라도 잡이 끝나면 중지돼요.
서비스 이미지가 제공하는 소프트웨어 사용하기
서비스를 지정하면 네트워크로 접근 가능한 서비스가 제공돼요. 데이터베이스가 이런 서비스의 가장 단순한 예시예요.
services 기능은 정의된 서비스 이미지의 어떤 소프트웨어도 잡의 컨테이너에 추가하지 않아요.
예를 들어 잡에 다음 서비스가 정의되어 있다면, php, node, go 명령은 스크립트에서 사용할 수 없고 잡이 실패해요.
job:
services:
- php:8.4
- node:latest
- golang:1.25
image: alpine:3.23
script:
- php -v
- node -v
- go version
스크립트에 php, node, go를 사용할 수 있어야 한다면 다음 중 하나를 선택하세요.
- 필요한 모든 도구가 들어 있는 기존 Docker 이미지를 선택한다.
- 필요한 모든 도구를 포함한 나만의 Docker 이미지를 만들고 그 이미지를 잡에서 사용한다.
.gitlab-ci.yml 파일에 서비스 정의하기
잡마다 다른 이미지와 서비스를 정의하는 것도 가능해요.
default:
before_script:
- bundle install
test:4.0:
image: ruby:4.0
services:
- postgres:18
script:
- bundle exec rake spec
test:3.4:
image: ruby:3.4
services:
- postgres:17
script:
- bundle exec rake spec
또는 image와 services에 확장 설정 옵션을 전달할 수 있어요.
default:
image:
name: ruby:4.0
entrypoint: ["/bin/bash"]
services:
- name: my-postgres:18
alias: db,postgres,pg
entrypoint: ["/usr/local/bin/db-postgres"]
command: ["start"]
before_script:
- bundle install
test:
script:
- bundle exec rake spec
서비스 접근하기
서비스 별칭을 지정하지 않으면 빌드 컨테이너에서 두 호스트 이름으로 보고 접근할 수 있어요.
namespace-projectnamenamespace__projectname
밑줄(_)이 있는 호스트 이름은 RFC에 유효하지 않아서 타사 애플리케이션에서 문제가 생길 수 있어요.
서비스 호스트 이름의 기본 별칭은 이미지 이름에서 다음 규칙으로 만들어져요.
- 콜론(:) 뒤의 부분은 제거된다.
- 슬래시(/)를 이중 밑줄(__)로 바꾸면 기본 별칭(primary alias)이 만들어진다.
- 슬래시(/)를 단일 대시(-)로 바꾸면 보조 별칭(secondary alias)이 만들어진다.
기본 동작을 바꾸려면 서비스 별칭을 하나 이상 지정할 수 있어요.
서비스 연결하기
상호 의존적인 서비스들을 복잡한 잡에서 사용할 수 있어요. 예를 들어 외부 API가 자체 데이터베이스와 통신해야 하는 엔드투엔드 테스트 같은 경우죠.
API와, 그 API가 데이터베이스를 필요로 하는 프런트엔드 애플리케이션의 엔드투엔드 테스트 예:
end-to-end-tests:
image: node:latest
services:
- name: selenium/standalone-firefox:${FIREFOX_VERSION}
alias: firefox
- name: registry.gitlab.com/organization/private-api:latest
alias: backend-api
- name: postgres:18
alias: db postgres db
variables:
FF_NETWORK_PER_BUILD: 1 # activate container-to-container networking
POSTGRES_PASSWORD: supersecretpassword
BACKEND_POSTGRES_HOST: postgres
script:
- npm install
- npm test
이 방식이 동작하려면 잡마다 새 네트워크를 만드는 네트워킹 모드를 사용해야 해요.
서비스에 CI/CD 변수 전달하기
Docker 이미지와 서비스를 세부 조정하려고 .gitlab-ci.yml 파일에 직접 사용자 CI/CD 변수를 전달할 수도 있어요. 자세한 내용은 .gitlab-ci.yml에서 정의한 변수 문서를 참고하세요.
# 다음 변수들은 Postgres 컨테이너와 Ruby 컨테이너 양쪽에 자동으로 전달되어 각각 안에서 사용할 수 있습니다.
variables:
HTTPS_PROXY: "https://10.1.1.1:8090"
HTTP_PROXY: "https://10.1.1.1:8090"
POSTGRES_DB: "my_custom_db"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "example"
PGDATA: "/var/lib/postgresql/data"
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --data-checksums"
default:
services:
- name: postgres:18
alias: db
entrypoint: ["docker-entrypoint.sh"]
command: ["postgres"]
image:
name: ruby:4.0
entrypoint: ["/bin/bash"]
before_script:
- bundle install
test:
script:
- bundle exec rake spec
services에서 사용할 수 있는 설정
services:의 하위 키(서브키)에 대한 자세한 내용은 CI/CD YAML 레퍼런스를 확인하세요.
같은 이미지에서 여러 서비스 시작하기
확장된 Docker 구성 옵션이 나오기 전에는 다음 설정이 제대로 동작하지 않았어요.
services:
- mysql:latest
- mysql:latest
러너는 각각 mysql:latest 이미지를 사용하는 컨테이너 두 개를 시작했어요. 하지만 둘 다 기본 호스트 이름 규칙에 따라 mysql 별칭으로 잡 컨테이너에 추가돼서, 결국 둘 중 하나는 접근할 수 없게 됐죠.
확장된 Docker 구성 옵션이 나온 뒤에는 위 예시를 이렇게 쓰면 돼요.
services:
- name: mysql:latest
alias: mysql-1
- name: mysql:latest
alias: mysql-2
러너는 여전히 mysql:latest 이미지를 사용하는 컨테이너 두 개를 시작하지만, 이제 각각 .gitlab-ci.yml 파일에 설정한 별칭으로 접근할 수 있어요.
서비스에 명령 설정하기
super/sql:latest 이미지에 어떤 SQL 데이터베이스가 들어 있고, 이걸 잡의 서비스로 쓰고 싶다고 가정해 볼게요. 이 이미지는 컨테이너를 시작할 때 데이터베이스 프로세스를 시작하지 않는다고도 가정해요. 그러면 사용자가 데이터베이스를 시작하려면 수동으로 /usr/bin/super-sql run을 명령으로 실행해야 해요.
확장된 Docker 구성 옵션이 나오기 전에는 다음을 해야 했어요.
- super/sql:latest 이미지를 기반으로 나만의 이미지를 만든다.
- 기본 명령을 추가한다.
- 잡 설정에서 그 이미지를 사용한다.
my-super-sql:latest 이미지의 Dockerfile:
FROM super/sql:latest
CMD ["/usr/bin/super-sql", "run"]
.gitlab-ci.yml의 잡에서:
services:
- my-super-sql:latest
확장된 Docker 구성 옵션이 나온 뒤에는 .gitlab-ci.yml 파일에서 바로 명령을 설정할 수 있어요.
services:
- name: super/sql:latest
command: ["/usr/bin/super-sql", "run"]
command의 문법은 Dockerfile CMD와 비슷해요.
Kubernetes 실행기에서 서비스 컨테이너 이름으로 별칭 사용하기
이력
- 도입: GitLab 및 GitLab Runner 17.9
Kubernetes 실행기에서 서비스 별칭을 서비스 컨테이너 이름으로 사용할 수 있어요. GitLab Runner는 다음 조건에 따라 컨테이너 이름을 정해요.
- 서비스에 여러 별칭이 설정되면 서비스 컨테이너는 다음 조건을 만족하는 첫 번째 별칭으로 이름을 붙인다.
- 다른 서비스 컨테이너가 이미 사용하지 않는 별칭
- Kubernetes 라벨 이름 제약을 따르는 별칭
- 별칭으로 서비스 컨테이너 이름을 정할 수 없으면 GitLab Runner는
svc-i패턴으로 되돌아간다.
다음 예시들은 Kubernetes 실행기에서 별칭이 서비스 컨테이너 이름을 정하는 데 어떻게 쓰이는지 보여줘요.
서비스당 별칭 하나
다음 .gitlab-ci.yml 파일에서:
job:
image: alpine:latest
script:
- sleep 10
services:
- name: alpine:latest
alias: alpine
- name: mysql:latest
alias: mysql
시스템은 표준 build·helper 컨테이너 외에 alpine과 mysql이라는 이름의 컨테이너로 잡 Pod을 만들어요. 이 별칭들이 쓰이는 이유는:
- 다른 서비스 컨테이너가 사용하지 않기 때문이고,
- Kubernetes 라벨 이름 제약을 따르기 때문이에요.
하지만 다음 .gitlab-ci.yml에서는:
job:
image: alpine:latest
script:
- sleep 10
services:
- name: mysql:lts
alias: mysql
- name: mysql:latest
alias: mysql
시스템은 build·helper 컨테이너 외에 mysql과 svc-0이라는 컨테이너 두 개를 더 만들어요. mysql 컨테이너는 mysql:lts 이미지에 해당하고, svc-0 컨테이너는 mysql:latest 이미지에 해당해요.
서비스당 별칭 여러 개
다음 .gitlab-ci.yml 파일에서:
job:
image: alpine:latest
script:
- sleep 10
services:
- name: alpine:latest
alias: alpine,alpine-latest
- name: alpine:edge
alias: alpine,alpine-edge,alpine-latest
시스템은 build·helper 컨테이너 외에 컨테이너 네 개를 더 만들어요.
- alpine — alpine:latest 이미지의 컨테이너에 해당한다.
- alpine-edge — alpine:edge 이미지의 컨테이너에 해당한다(alpine 별칭은 앞선 컨테이너가 이미 사용 중).
이 예시에서 alpine-latest 별칭은 사용되지 않아요.
하지만 다음 .gitlab-ci.yml에서는:
job:
image: alpine:latest
script:
- sleep 10
services:
- name: alpine:latest
alias: alpine,alpine-edge
- name: alpine:edge
alias: alpine,alpine-edge
- name: alpine:3.21
alias: alpine,alpine-edge
build·helper 컨테이너 외에 컨테이너 여섯 개가 더 만들어져요.
-
alpine — alpine:latest 이미지의 컨테이너를 가리켜야 한다.
-
alpine-edge — alpine:edge 이미지의 컨테이너를 가리켜야 한다(alpine 별칭은 앞선 컨테이너가 이미 사용 중).
-
svc-0 — alpine:3.21 이미지의 컨테이너를 가리켜야 한다(alpine과 alpine-edge 별칭은 앞선 컨테이너들이 이미 사용 중).
-
svc-i패턴의i는 제공된 목록에서 서비스의 위치를 나타내는 게 아니에요. 대신 사용 가능한 별칭이 없을 때의 서비스 위치를 나타내요. -
잘못된 별칭(Kubernetes 제약을 충족하지 않는 별칭)을 제공하면 잡이 다음 오류로 실패해요(예시는 alpine_edge 별칭일 때). 별칭이 잡 Pod의 로컬 DNS 항목을 만드는 데도 쓰이기 때문에 이렇게 실패해요.
ERROR: Job failed (system failure): prepare environment: setting up build pod: provided host alias
alpine_edge for service alpine:edge is invalid DNS. a lowercase RFC 1123 subdomain must consist of lower
case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g.
'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*').
Check https://docs.gitlab.com/runner/shells/index/#shell-profile-loading for more information.
docker run(Docker-in-Docker)과 나란히 서비스 사용하기
docker run으로 시작한 컨테이너도 GitLab이 제공하는 서비스에 연결할 수 있어요.
서비스 부팅이 비싸거나 시간이 오래 걸리면, 테스트 대상 서비스는 한 번만 부팅하면서 여러 클라이언트 환경에서 테스트를 실행할 수 있어요.
access-service:
stage: build
image: docker:20.10.16
services:
- docker:dind # docker run에 필요
- traefik/whoami:latest
variables:
FF_NETWORK_PER_BUILD: "true" # activate container-to-container networking
script: |
docker run --rm --name curl \
--volume "$(pwd)":"$(pwd)" \
--workdir "$(pwd)" \
--network=host \
curlimages/curl:latest curl "http://traefik-whoami"
이 방식이 동작하려면 다음을 해야 해요.
- 잡마다 새 네트워크를 만드는 네트워킹 모드를 사용한다.
- Docker 소켓 바인딩과 함께 Docker 실행기를 사용하지 않는다. 꼭 써야 한다면, 위 예시에서 host 대신 이 잡을 위해 만들어진 동적 네트워크 이름을 사용한다.
서비스 컨테이너 로그 캡처하기
서비스 컨테이너에서 실행되는 애플리케이션이 만드는 로그는 나중에 검사하고 디버깅하기 위해 캡처할 수 있어요. 서비스 컨테이너가 성공적으로 시작됐지만 예상치 못한 동작 때문에 잡이 실패할 때 서비스 컨테이너 로그를 확인해 보세요. 로그에서 컨테이너 안 서비스의 누락되거나 잘못된 설정을 알 수 있어요.
CI_DEBUG_SERVICES는 서비스 컨테이너를 실제로 디버깅 중일 때만 활성화해야 해요. 서비스 컨테이너 로그를 캡처하면 저장 공간과 성능에 영향을 주기 때문이에요.
CI_DEBUG_SERVICES를 활성화하면 마스킹된 변수가 드러날 수 있어요. 이 변수가 활성화되면 서비스 컨테이너 로그와 CI 잡의 로그가 잡의 trace 로그로 동시에 스트리밍돼요. 즉 서비스 컨테이너 로그가 잡의 마스킹된 로그에 끼어들 수 있답니다. 그러면 변수 마스킹 메커니즘이 무산되고 마스킹된 변수가 드러날 수 있어요.
서비스 로깅을 활성화하려면 프로젝트의 .gitlab-ci.yml 파일에 CI_DEBUG_SERVICES 변수를 추가하세요.
variables:
CI_DEBUG_SERVICES: "true"
허용되는 값은 다음과 같아요.
- 활성화: TRUE, true, True
- 비활성화: FALSE, false, False
그 외 값은 오류 메시지를 출력하고 기능을 사실상 비활성화해요.
활성화되면 모든 서비스 컨테이너의 로그가 캡처되어 다른 로그와 함께 잡의 trace 로그로 스트리밍돼요. 각 컨테이너의 로그에는 그 컨테이너의 별칭이 접두사로 붙고 다른 색으로 표시돼요.
잡 실패를 진단하려면 로그를 캡처하려는 서비스 컨테이너의 로깅 수준을 조정할 수 있어요. 기본 로깅 수준으로는 충분한 문제 해결 정보가 나오지 않을 수 있거든요.
CI/CD 변수 마스킹을 참고하세요.
로컬에서 잡 디버깅하기
다음 명령은 루트 권한 없이 실행돼요. 내 사용자 계정으로 Docker 명령을 실행할 수 있는지 확인하세요.
먼저 build_script라는 파일을 만들어요.
cat <<EOF > build_script
git clone https://gitlab.com/gitlab-org/gitlab-runner.git /builds/gitlab-org/gitlab-runner
cd /builds/gitlab-org/gitlab-runner
make runner-bin-host
EOF
이 예시는 Makefile이 들어 있는 GitLab Runner 저장소를 사용해요. 그래서 make를 실행하면 Makefile에 정의된 대상을 실행하죠. make runner-bin-host 대신 내 프로젝트에 맞는 명령을 실행해도 돼요.
그다음 서비스 컨테이너를 만들어요.
docker run -d --name service-redis redis:latest
이 명령은 최신 Redis 이미지를 사용해 service-redis라는 서비스 컨테이너를 만들어요. 서비스 컨테이너는 백그라운드(-d)로 실행돼요.
마지막으로, 아까 만든 build_script 파일을 실행해서 빌드 컨테이너를 만들어요.
docker run --name build -i --link=service-redis:redis golang:latest /bin/bash < build_script
이 명령은 golang:latest 이미지에서 스폰된 build라는 컨테이너를 만들고 여기에 서비스 하나를 연결해요. build_script는 stdin으로 bash 인터프리터에 파이프되고, bash는 build 컨테이너에서 build_script를 실행해요.
테스트가 끝난 뒤 컨테이너를 제거하려면 다음 명령을 사용하세요.
docker rm -f -v build service-redis
이 명령은 build 컨테이너와 서비스 컨테이너, 그리고 컨테이너 생성 시 만들어진 모든 볼륨(-v)을 강제로(-f) 제거해요.
서비스 컨테이너 사용 시 보안
Docker 권한 모드(privileged mode)는 서비스에도 적용돼요. 즉 서비스 이미지 컨테이너가 호스트 시스템에 접근할 수 있다는 뜻이에요. 신뢰할 수 있는 출처의 컨테이너 이미지만 사용해야 해요.
공유 /builds 디렉터리
빌드 디렉터리는 /builds 아래 볼륨으로 마운트되고 잡과 서비스 사이에 공유돼요. 잡은 서비스가 실행된 후 /builds/$CI_PROJECT_PATH에 프로젝트를 체크아웃해요. 내 서비스가 프로젝트 파일에 접근해야 하거나 아티팩트를 저장해야 한다면, 디렉터리가 존재하고 $CI_COMMIT_SHA가 체크아웃될 때까지 기다리세요. 잡이 체크아웃을 끝내기 전에 만든 변경 사항은 체크아웃 과정에서 제거돼요.
서비스는 잡 디렉터리가 채워지고 처리할 준비가 됐는지 스스로 감지해야 해요. 예를 들어 특정 파일이 생길 때까지 기다리는 식이죠.
시작하자마자 바로 작업을 시작하는 서비스는 실패할 가능성이 커요. 잡 데이터가 아직 준비되지 않았을 수 있거든요. 예를 들어 컨테이너가 docker build 명령으로 DinD 서비스에 네트워크 연결을 만들어요. 서비스는 자체 API에 컨테이너 이미지 빌드를 시작하라고 지시하죠. Docker Engine은 Dockerfile에서 참조하는 파일에 접근할 수 있어야 하므로, 서비스 안에서 CI_PROJECT_DIR에 접근할 수 있어야 해요. 하지만 Docker Engine은 잡에서 docker build 명령이 호출되기 전까지는 그 접근을 시도하지 않아요. 이 시점에는 /builds 디렉터리가 이미 데이터로 채워져 있죠. 시작 직후 CI_PROJECT_DIR에 쓰려고 하는 서비스는 No such file or directory 오류로 실패할 수 있어요.
잡 데이터와 상호작용하는 서비스가 잡 자체로 제어되지 않는 시나리오에서는 Docker 실행기 워크플로를 고려해 보세요.
더 알아보기
서비스 사용법을 더 깊게 익히고 싶다면 MySQL, PostgreSQL, Redis 같은 개별 서비스 문서부터 살펴보는 걸 추천해요. 이미지·서비스의 확장 설정 옵션이 궁금하다면 Docker 이미지 사용 문서를 함께 읽어 보세요.