Jenkins - Jenkins dood(docker out of docker)로 실행시켜 agent docker 사용하기

2020. 3. 26. 00:11인프라/Jenkins

 

아마 이번 포스팅부터 Jenkins(젠킨스)에서 정말 쓸모 있는 부분을 다룰 것 같다. 젠킨스 파이프라인을 다루기 전에 Jenkins 안에서 dood(docker out of docker)로 docker agent를 실행하는 방법에 대해 다루어 볼 것이다. 실제로 많이 사용하는 유즈케이스이기도 하다. 그 이유는 필요한 라이브러리를 직접 설치하거나 하지 않고 Jenkins pipeline 스크립트 안에서 docker agent를 이용해 설치하면 별도로 의존성있는 라이브러리를 직접 설치하지 않아도 되고, 또한 Jenkins에서 Docker image를 빌드할때도 필요하다. 즉, 요즘 같이 도커가 대중화되있는 시대에는 거의 필수이지 않을까 싶다.(더 좋은 방법이 있다면 댓글로 알려주시면 감사하겠다.)

 

우리가 구성해볼 것은 docker 안에 docker를 띄우는 것이긴 하지만 "docker in docker" 방식처럼 독립적으로 띄우지는 않을 것이고, Host docker의 docker.sock을 공유해서 docker out of docker방식으로 띄워볼 것이다. docker in docker라는 것도 존재하지만 권장하는 방법이 아니기 때문에 우리는 docker out of docker로 실습을 진행해볼 것이다.

 

그렇다면 왜 docker in docker가 권장하지 않는 방법일까? 이유는 아래와 같다.

 

Docker in Docker(DinD)는 도커 안에 컨테이너 내부의 격리된 Docker 데몬을 실행하는 작업을 의미한다. 
즉, 도커데몬이 2개가 뜨는 것이다. CI 측면에서 접근하자면 Task를 수행하는 Agent가 Docker Client와
Docker Daemon 역할까지 하게되어 도커 명령을 수행하는데 문제가 없어진다. 하지만 아주 큰 단점이 존재한다.

호스트 도커 컨테이너가 privilieged mode로 실행되어야 한다.

> docker run --privileged --name dind -d docker:1.8-dind

privilieged 플래그를 사용한다면 호스트 컨테이너가 호스트 머신에서 할 수 있는 거의 모든 작업을 할 수 있게 된다.
이는 컨테이너를 실행하는데 큰 보안 위험을 초래할 수 있다.

출저 : https://aidanbae.github.io/code/docker/dinddood/

그렇다고, dood가 아예 단점이 없는 것은 아니다. 그 이유는 직접 띄워보면 알 수 있는 것이 jenkins container 내부에서 docker ps 명령을 날려보면 자기자신의 container가 떠있는 것이 목록에 보인다. 그 말은? 외부 호스트 도커에 떠있는 container에 접근이 가능하게 될 수도 있다는 것이다. 또한 docker.sock을 공유하기 위해 그만큼의 권한을 docker container 에게 제공해야한다. 조금씩 장단점이 있는 방법들인 것 같으니, 안전하게 사용하는 쪽이 좋은 방향인것 같다.

 

우선 우리는 이전 포스팅에서 도커 젠킨스 공식 이미지를 사용했었는데, 조금 변경할 필요가 생긴다. 그 이유는 공식 젠킨스 이미지 안에는 도커 엔진이 설치되어 있지 않기때문에 우리는 도커엔진이 깔려있는 젠킨스 이미지가 필요하기 때문에 커스텀하게 이미지를 다시 만들어 볼것이다.

 

1. jenkins-dood image 만들기

바로 실습에 들어간다. 우선 도커 엔진을 다운로드 받기 위한 스크립트를 파일로 작성하였다.

 

> docker_install.sh

#!/bin/sh
apt-get update && \
apt-get -y install apt-transport-https \
     ca-certificates \
     curl \
     gnupg2 \
     zip \
     unzip \
     software-properties-common && \
curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg > /tmp/dkey; apt-key add /tmp/dkey && \
add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
   $(lsb_release -cs) \
   stable" && \
apt-get update && \
apt-get -y install docker-ce

 

그리고 다음으로는 도커 이미지를 빌드하기 위한 Dockerfile이다.

 

#공식 젠킨스 이미지를 베이스로 한다.
FROM jenkins/jenkins:lts

#root 계정으로 변경(for docker install)
USER root

#DIND(docker in docker)를 위해 docker 안에서 docker를 설치
COPY docker_install.sh /docker_install.sh
RUN chmod +x /docker_install.sh
RUN /docker_install.sh

RUN usermod -aG docker jenkins
USER jenkins

 

도커 이미지를 빌드할 준비가 되었다. 이제 도커 이미지를 빌드해보자. 

 

docker build -t 1223yys/jenkins-dind:latest .
docker push 1223yys/jenkins-dind

 

여기까지 크게 문제없이 따라왔다면 도커허브에 이미지가 push 되어 있을 것이다. 

 

 

그 다음은 Jenkins를 띄울 서버에 방금 만든 이미지로 Jenkins를 띄워보자.

 

docker run -d --name jenkins -p 8080:8080 -p 50000:50000 \
        -v /home/deploy/jenkins_v:/var/jenkins_home \
        -v /var/run/docker.sock:/var/run/docker.sock \
        1223yys/jenkins-dind:latest

 

중요한 것은 우리가 처음 이야기했던 것처럼 Host의 docker.sock을 공유해서 사용할 것임으로 볼륨마운트가 필요하다. 이제 젠킨스 컨테이너에 들어가서 도커엔진이 잘 떠있는지 확인하자.

 

> docker exec -it jenkins bash

jenkins@20ff53d54e94:/$ docker ps
CONTAINER ID        IMAGE                         COMMAND                  CREATED             STATUS              PORTS                                              NAMES
20ff53d54e94        1223yys/jenkins-dind:latest   "/sbin/tini -- /usr/…"   35 minutes ago      Up 35 minutes       0.0.0.0:8080->8080/tcp, 0.0.0.0:50000->50000/tcp   jenkins

 

어? 나는 띄운 컨테이너가 없는데 떠있는 컨테이너가 하나 보인다 ! 그 이유는 앞에서도 간단히 이야기 한 것처럼 호스트 도커엔진과 공유하고 있기 때문이다 ! 우리가 실행 시킨 젠킨스 컨테이너가 보인다. 만약 "docker.sock 볼륨마운트"에 관해 permission denied가 발생한다면 아래와 같이 호스트 머신에서 docker.sock에 접근할 수 있도록 권한을 부여하자. 

 

> sudo chmod 666 /var/run/docker.sock

 

그리고 계속해서 docker 명령어를 sudo로 붙여서 사용하는 사람이 있다면 아래 명령어를 참고하자.

 

> sudo groupadd docker
> sudo usermod -aG docker deploy

 

이제 간단하게 젠킨스 파이프라인 스크립트(Jenkinsfile)를 작성해 도커엔진이 정말 잘 동작하는지 확인하자.

 

pipeline {
    agent none

    environment {
        SLACK_CHANNEL = '#jenkins_notification'
    }

    stages {
        stage('Start') {
            steps {
                slackSend (channel: SLACK_CHANNEL, color: '#FFFF00', message: "STARTED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
            }
        }

        stage('Build & Test') {
            agent {
                docker {
                    image 'gradle:6.0-jdk13'
                    alwaysPull true
                }
            }
            steps {
                sh '''
                    gradle -version
                '''
            }
            post {
                always {
                    echo 'Test and Build post process !'
                }
            }
        }

        stage('Deploy') {
            steps {
                echo 'jenkins sample deploy ! '
            }
        }

    }
    post {
        success {
            slackSend (channel: SLACK_CHANNEL, color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
        failure {
            slackSend (channel: SLACK_CHANNEL, color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
    }
}

 

해당 파이프라인은 도커로 gradle을 내려받아 젠킨스 내부에 떠있는 도커의 gradle 컨테이너를 이용하여 gradle -version을 체크하는 간단한 스크립트이다. 블루오션으로 파이프라인 결과를 확인해보자 !

 

 

성공했다 ! 여기까지 Jenkins 컨테이너 안에 도커엔진을 설치하여 파이프라인에 docker를 사용할 수 있도록 구성하고 실습해보았다. 오늘까지는 실무에서 쓸만한 파이프라인 스크립트를 다루어나 하지는 않았지만, 다음 포스팅부터는 정말로 쓸만한 파이프라인을 구성해서 실습해볼 것이다. 만약 위 코드가 필요하다면 아래 깃헙주소를 참고하자.

 

 

 

yoonyeoseong/jenkins_sample

Contribute to yoonyeoseong/jenkins_sample development by creating an account on GitHub.

github.com