Docker-in-Docker 사용하기

Docker-in-Docker 사용하기

Docker-in-Docker(dind)를 사용해 CI/CD 작업을 실행하는 방법을 설명하는 문서예요. 등록된 러너가 Docker 실행기나 Kubernetes 실행기를 사용하고, 실행기가 Docker의 컨테이너 이미지를 사용해 CI/CD 작업을 실행하는 방식이에요.

Docker 실행기와 Kubernetes 실행기 양쪽에서 TLS 활성/비활성 구성, Unix 소켓 공유, 프록시 설정, 알려진 이슈를 코드 예시와 함께 옆에서 설명해 주는 방식으로 정리했어요.

출처: 문서

본문

Docker-in-Docker(dind)는 등록된 러너가 Docker 실행기Kubernetes 실행기를 사용하는 것을 의미해요. 실행기는 Docker가 제공하는 Docker의 컨테이너 이미지를 사용해 CI/CD 작업을 실행해요.

Docker 이미지에는 모든 docker 도구가 포함되어 있고, privileged 모드에서 이미지 컨텍스트로 작업 스크립트를 실행할 수 있어요.

항상 docker:24.0.5 같은 특정 버전의 이미지를 고정(pin)하세요. docker:latest 같은 태그를 사용하면 어떤 버전이 사용될지 제어할 수 없어요. 이는 새 버전이 출시될 때 비호환성 문제를 일으킬 수 있어요.

Docker 실행기와 함께 사용하기

Docker 실행기를 사용해 Docker 컨테이너에서 작업을 실행할 수 있어요.

Docker 실행기에서 TLS가 활성화된 Docker-in-Docker (권장)

Docker 데몬은 TLS 연결을 지원해요. 가능하면 TLS를 사용하세요. TLS는 Docker 19.03.12 이상에서 기본이며 GitLab.com 인스턴스 러너가 지원해요.

이 작업은 --docker-privileged를 활성화하는데, 이는 컨테이너의 보안 메커니즘을 사실상 비활성화하고 호스트를 권한 상승에 노출시켜요. 이는 컨테이너 탈출을 유발할 수 있어요. 자세한 내용은 런타임 권한과 Linux capabilities를 참고하세요.

TLS가 활성화된 Docker-in-Docker를 사용하려면:

  1. GitLab Runner 설치.
  2. 명령줄에서 GitLab Runner를 등록하세요. dockerprivileged 모드를 사용하세요.
    sudo gitlab-runner register -n \
      --url "https://gitlab.com/" \
      --registration-token REGISTRATION_TOKEN \
      --executor docker \
      --description "My Docker Runner" \
      --tag-list "tls-docker-runner" \
      --docker-image "docker:24.0.5-cli" \
      --docker-privileged \
      --docker-volumes "/certs/client"
    
    • 이 명령은 docker:24.0.5-cli 이미지(작업 수준에서 지정되지 않은 경우)를 사용하는 새 러너를 등록해요. 빌드 및 서비스 컨테이너를 시작하려면 privileged 모드를 사용해요. Docker-in-Docker를 사용하려면 항상 Docker 컨테이너에서 privileged = true를 사용해야 해요.
    • 이 명령은 서비스와 빌드 컨테이너에 /certs/client를 마운트하는데, 이는 Docker 클라이언트가 해당 디렉터리의 인증서를 사용하는 데 필요해요. 자세한 내용은 Docker 이미지 문서를 참고하세요.
  3. 이전 명령은 다음 예시와 유사한 config.toml 항목을 만들어요.
    [[runners]]
      url = "https://gitlab.com/"
      token = TOKEN
      executor = "docker"
      [runners.docker]
        tls_verify = false
        image = "docker:24.0.5-cli"
        privileged = true
        disable_cache = false
        volumes = ["/certs/client", "/cache"]
      [runners.cache]
        [runners.cache.s3]
        [runners.cache.gcs]
    
  4. 이제 작업 스크립트에서 docker를 사용할 수 있어요. docker:24.0.5-dind 서비스를 포함하세요.
    default:
      image: docker:24.0.5-cli
      services:
        - docker:24.0.5-dind
      before_script:
        - docker info
    
    variables:
      # When you use the dind service, you must instruct Docker to talk with
      # the daemon started inside of the service. The daemon is available
      # with a network connection instead of the default
      # /var/run/docker.sock socket. Docker 19.03 does this automatically
      # by setting the DOCKER_HOST in
      # https://github.com/docker-library/docker/blob/d45051476babc297257df490d22cbd806f1b11e4/19.03/docker-entrypoint.sh#L23-L29
      #
      # The 'docker' hostname is the alias of the service container as described at
      # https://docs.gitlab.com/ci/services/#accessing-the-services.
      #
      # Specify to Docker where to create the certificates. Docker
      # creates them automatically on boot, and creates
      # `/certs/client` to share between the service and job
      # container, thanks to volume mount from config.toml
      DOCKER_TLS_CERTDIR: "/certs"
    
    build:
      stage: build
      tags:
        - tls-docker-runner
      script:
        - docker build -t my-docker-image .
        - docker run my-docker-image /script/to/run/tests
    

Docker 실행기에서 TLS가 비활성화된 Docker-in-Docker

때로는 TLS를 비활성화해야 하는 합당한 이유가 있어요. 예를 들어 사용 중인 GitLab Runner 구성을 제어할 수 없는 경우가 그렇죠.

  1. 명령줄에서 GitLab Runner를 등록하세요. dockerprivileged 모드를 사용하세요.
    sudo gitlab-runner register -n \
      --url "https://gitlab.com/" \
      --registration-token REGISTRATION_TOKEN \
      --executor docker \
      --description "My Docker Runner" \
      --tag-list "no-tls-docker-runner" \
      --docker-image "docker:24.0.5-cli" \
      --docker-privileged
    
  2. 이전 명령은 다음 예시와 유사한 config.toml 항목을 만들어요.
    [[runners]]
      url = "https://gitlab.com/"
      token = TOKEN
      executor = "docker"
      [runners.docker]
        tls_verify = false
        image = "docker:24.0.5-cli"
        privileged = true
        disable_cache = false
        volumes = ["/cache"]
      [runners.cache]
        [runners.cache.s3]
        [runners.cache.gcs]
    
  3. 작업 스크립트에 docker:24.0.5-dind 서비스를 포함하세요.
    default:
      image: docker:24.0.5-cli
      services:
        - docker:24.0.5-dind
      before_script:
        - docker info
    
    variables:
      # When using dind service, you must instruct docker to talk with the
      # daemon started inside of the service. The daemon is available with
      # a network connection instead of the default /var/run/docker.sock socket.
      #
      # The 'docker' hostname is the alias of the service container as described at
      # https://docs.gitlab.com/ci/services/#accessing-the-services
      #
      DOCKER_HOST: tcp://docker:2375
      #
      # This instructs Docker not to start over TLS.
      DOCKER_TLS_CERTDIR: ""
    
    build:
      stage: build
      tags:
        - no-tls-docker-runner
      script:
        - docker build -t my-docker-image .
        - docker run my-docker-image /script/to/run/tests
    

Docker-in-Docker와 빌드 컨테이너 사이의 공유 볼륨에서 Unix 소켓 사용

Docker 실행기에서 TLS가 활성화된 Docker-in-Docker(권장) 방식의 volumes = ["/certs/client", "/cache"]에 정의된 디렉터리는 빌드 간에 영속적이에요. Docker 실행기 러너를 사용하는 여러 CI/CD 작업이 Docker-in-Docker 서비스를 활성화했다면 각 작업이 그 디렉터리 경로에 쓰게 되어 충돌이 발생할 수 있어요.

이 충돌을 해결하려면 Docker-in-Docker 서비스와 빌드 컨테이너 사이의 공유 볼륨에서 Unix 소켓을 사용하세요. 이 방식은 성능을 향상시키고 서비스와 클라이언트 사이에 보안 연결을 설정해요.

빌드와 서비스 컨테이너 사이에 공유 임시 볼륨이 있는 샘플 config.toml:

[[runners]]
  url = "https://gitlab.com/"
  token = TOKEN
  executor = "docker"
  [runners.docker]
    image = "docker:24.0.5-cli"
    privileged = true
    volumes = ["/runner/services/docker"] # Temporary volume shared between build and service containers.

Docker-in-Docker 서비스는 docker.sock을 만듭니다. Docker 클라이언트는 Docker Unix 소켓 볼륨을 통해 docker.sock에 연결해요.

job:
  variables:
    # This variable is shared by both the DinD service and Docker client.
    # For the service, it will instruct DinD to create `docker.sock` here.
    # For the client, it tells the Docker client which Docker Unix socket to connect to.
    DOCKER_HOST: "unix:///runner/services/docker/docker.sock"
  services:
    - docker:24.0.5-dind
  image: docker:24.0.5-cli
  script:
    - docker version

Docker 실행기에서 프록시가 활성화된 Docker-in-Docker

docker push 명령을 사용하려면 프록시 설정을 구성해야 할 수 있어요.

자세한 내용은 dind 서비스 사용 시 프록시 설정을 참고하세요.

Kubernetes 실행기와 함께 사용하기

Kubernetes 실행기를 사용해 Docker 컨테이너에서 작업을 실행할 수 있어요.

Kubernetes에서 TLS가 활성화된 Docker-in-Docker (권장)

Kubernetes에서 TLS가 활성화된 Docker-in-Docker를 사용하려면:

  1. Helm 차트를 사용해 values.yml 파일을 업데이트해 볼륨 마운트를 지정하세요.
    runners:
      tags: "tls-dind-kubernetes-runner"
      config: |
        [[runners]]
          [runners.kubernetes]
            image = "ubuntu:20.04"
            privileged = true
          [[runners.kubernetes.volumes.empty_dir]]
            name = "docker-certs"
            mount_path = "/certs/client"
            medium = "Memory"
    
  2. 작업에 docker:24.0.5-dind 서비스를 포함하세요.
    default:
      image: docker:24.0.5-cli
      services:
        - name: docker:24.0.5-dind
          variables:
            HEALTHCHECK_TCP_PORT: "2376"
      before_script:
        - docker info
    
    variables:
      # When using dind service, you must instruct Docker to talk with
      # the daemon started inside of the service. The daemon is available
      # with a network connection instead of the default
      # /var/run/docker.sock socket.
      DOCKER_HOST: tcp://docker:2376
      #
      # The 'docker' hostname is the alias of the service container as described at
      # https://docs.gitlab.com/ci/services/#accessing-the-services.
      #
      # Specify to Docker where to create the certificates. Docker
      # creates them automatically on boot, and creates
      # `/certs/client` to share between the service and job
      # container, thanks to volume mount from config.toml
      DOCKER_TLS_CERTDIR: "/certs"
      # These are usually specified by the entrypoint, however the
      # Kubernetes executor doesn't run entrypoints
      # https://gitlab.com/gitlab-org/gitlab-runner/-/issues/4125
      DOCKER_TLS_VERIFY: 1
      DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
    
    build:
      stage: build
      tags:
        - tls-dind-kubernetes-runner
      script:
        - docker build -t my-docker-image .
        - docker run my-docker-image /script/to/run/tests
    

Kubernetes에서 TLS가 비활성화된 Docker-in-Docker

Kubernetes에서 TLS가 비활성화된 Docker-in-Docker를 사용하려면 이전 예시를 다음에 맞게 조정해야 해요.

  • values.yml 파일에서 [[runners.kubernetes.volumes.empty_dir]] 섹션을 제거.
  • DOCKER_HOST: tcp://docker:2375로 포트를 2376에서 2375로 변경.
  • DOCKER_TLS_CERTDIR: ""로 Docker가 TLS 없이 시작하도록 지시.

예를 들어:

  1. Helm 차트를 사용해 values.yml 파일을 업데이트하세요.
    runners:
      tags: "no-tls-dind-kubernetes-runner"
      config: |
        [[runners]]
          [runners.kubernetes]
            image = "ubuntu:20.04"
            privileged = true
    
  2. 이제 작업 스크립트에서 docker를 사용할 수 있어요. docker:24.0.5-dind 서비스를 포함하세요.
    default:
      image: docker:24.0.5-cli
      services:
        - name: docker:24.0.5-dind
          variables:
            HEALTHCHECK_TCP_PORT: "2375"
      before_script:
        - docker info
    
    variables:
      # When using dind service, you must instruct Docker to talk with
      # the daemon started inside of the service. The daemon is available
      # with a network connection instead of the default
      # /var/run/docker.sock socket.
      DOCKER_HOST: tcp://docker:2375
      #
      # The 'docker' hostname is the alias of the service container as described at
      # https://docs.gitlab.com/ci/services/#accessing-the-services.
      #
      # This instructs Docker not to start over TLS.
      DOCKER_TLS_CERTDIR: ""
    build:
      stage: build
      tags:
        - no-tls-dind-kubernetes-runner
      script:
        - docker build -t my-docker-image .
        - docker run my-docker-image /script/to/run/tests
    

Docker-in-Docker의 알려진 이슈

Docker-in-Docker는 권장되는 구성이지만 다음 이슈를 인지해야 해요.

  • docker-compose 명령: 이 명령은 이 구성에서 기본적으로 사용할 수 없어요. 작업 스크립트에서 docker-compose를 사용하려면 Docker Compose 설치 지침을 따르세요.
  • 캐시: 각 작업은 새 환경에서 실행돼요. 모든 빌드가 자체 Docker 엔진 인스턴스를 얻으므로 동시 작업은 충돌하지 않아요. 하지만 레이어 캐싱이 없기 때문에 작업이 더 느릴 수 있어요. Docker 레이어 캐싱을 참고하세요.
  • 스토리지 드라이버: 기본적으로 이전 버전의 Docker는 각 작업에 대해 파일 시스템을 복사하는 vfs 스토리지 드라이버를 사용해요. Docker 17.09 이후는 권장 스토리지 드라이버인 --storage-driver overlay2를 사용해요. 자세한 내용은 OverlayFS 드라이버 사용을 참고하세요.
  • 루트 파일 시스템: docker:24.0.5-dind 컨테이너와 러너 컨테이너는 루트 파일 시스템을 공유하지 않으므로, 작업의 작업 디렉터리를 하위 컨테이너의 마운트 지점으로 사용할 수 있어요. 예를 들어 하위 컨테이너와 공유하려는 파일이 있다면 /builds/$CI_PROJECT_PATH 아래에 하위 디렉터리를 만들어 마운트 지점으로 사용할 수 있어요. 더 자세한 설명은 이슈 #41227을 참고하세요.
    variables:
      MOUNT_POINT: /builds/$CI_PROJECT_PATH/mnt
    script:
      - mkdir -p "$MOUNT_POINT"
      - docker run -v "$MOUNT_POINT:/mnt" my-docker-image
    

더 알아보기

Docker 이미지 빌드의 다양한 접근 방식(shell 실행기, 소켓 바인딩, 파이프 바인딩)과 Docker 데몬 없이 빌드하는 대체 방법은 Docker를 사용해 Docker 이미지 빌드하기 문서를 참고하세요. 특히 privileged 모드의 보안 영향과 Docker 레이어 캐싱을 함께 익히면 실무에서 더 안정적인 빌드를 구성할 수 있어요.