본문 바로가기
MLOps

[Github, MLOps] ML 모델 CI/CD 를 위한 Jenkins 테스트 자동화 [2]

by ML_MJSHIN 2021. 10. 15.

Introduction


 지난 포스팅에서는 Jenkins와 Github을 연동하고 Create Job까지 해보는 기본적인 내용을 다루었습니다. 하지만, 해당 내용대로만 하고나서 ML 모델을 학습하고 실험결과까지 테스트를 해보는 python 프로그램은 작동을 시킬 수 없다는 것을 따라해보신 분들은 확인하셨을 것 같습니다. 

 

 특히나 docker pull로 다운받은 Jenkins의 이미지에는 python 도 설치되어 있지 않고 이것 저것 설정이 안되어 있기 때문에 회사에서 사용하는 AI 시스템들에서 요구하는 CUDA, Pytorch, Tensorflow 등을 실행할 수 있는 환경이 구성되어 있지 않습니다. 다시말해 ML 을 학습하고 추론하게 하는 dependency가 갖추어져 있지 않습니다. 

 

 그래서, 저희에게 필요한 방법이 이전 포스팅에서 만든 Jenkins의 Container에서 우리의 AI project가 동작 가능한 docker image를 가져와 container화 시키고 이 container에서 실험을 돌리게 하는 방법입니다. 이 것은 docker의 기본 이념인 "실행환경을 container로 추상화하고 동일한 인터페이스를 제공하여 환경에 구애받지 않고 애플리케이션을 신속하게 배포,확장 및 관리할 수 있게 한다."라는 이념에 딱 맞는 사용법입니다. 그래서, 이 방법을 통해 저희는 앞서 만든 Jenkins container에서 ML project를 실행시키기 위한 별도의 환경 구성을 하지 않고도 빌드 테스트를 할 수 있게 되는 것이죠. 

 

 이 포스팅에서는 제가 DooD 방법을 참조하였기에 Jenkins 안에서 DooD(docker out of docker)로 docker agent를 실행하는 방법에 대해 다루어 볼 것입니다.  이번 포스팅은 Docker에 대한 개념과 설명이 많으니까 주의해주세요. 왜 Docker에 대해서 알아야하냐? 라는 의문이이 있을 수 있지만, 사실 MLOps 는 DevOps랑 거의 비슷하다고 생각이 됩니다. 그 중 특히나 CI(Continuous Integration)을 툴을 사용하여 세팅하는 MLOps, DevOps의 개발자라면 docker관련해서는 어느정도 깊이있는 이해를 하는 것이 필요하다고 생각됩니다.

 

Docker out of Docker


 Docker를 활용하는 방식에는 크게 두가지 DinD(docker in docker)와 DooD(docker out of docker) 로 나눌 수 있습니다. 단순하게 도커 이미지를 만들고 container로 이미지를 띄우는 (docker run 하는 것을 띄운다고도 표현합니다) 경우에는 docker client라는 요소와 docker daemon이라는 요소에 대해서는 자세히 알 필요가 없습니다. 하지만, 저희는 지금 DooD 와 DinD라는 개념을 알아야 하기 때문에 적당히 이해하는 것이 필요합니다.

 

 앞선 포스팅에서 docker image를 pull 해 왔던 장소는 "Registry" 라고 생각하시면 됩니다. 그리고, docker pull, run과 같은 명령어를 입력해서 저희가 입력한 명령어에 해당되는 docker 작업이 host server에서 이루어질 수 있도록 docker daemon에게 명령어를 전달하는 것이 Client의 개념입니다. 

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

 이 구조에서, 앞선 포스팅의 Jenkins container를 구현하였다고 가정해보는 것이 필요해집니다. 그리고, 해당 container내에서 Introduction에서 언급한 ML project의 docker image를 가져와서 container화 시키고 ML project를 실행해서 학습을 진행시켜 CI/CD 의 자동 테스트를 하고자 한다고 가정해보겠습니다.

 

 이때, Jenkins 의 container는 docker 가 설치되어있지 않기때문에 Jenkins container에서 docker를 사용하는 방법은 크게 2가지로 정리가 가능해집니다.

  1. host server에 위치한 docker daemon를 이용하는 방법 
  2. Jenkins container안에 docker를 설치하고 내부에서 ML project의 docker container를 띄우는 방법

 여기서 1번의 방법이 DooD가 되고, 2번의 방법이 DinD가 됩니다. (보통 DinD는 보안적인 문제 때문에 추천하지 않는 방법이기 때문에 저도 DooD로 선택하였습니다. DinD의 간단한 내용은 https://medium.com/dtevangelist/docker-in-docker-fb54252e3188을 참조하세요.) 그런데 문제는 1번의 DooD를 선택한다면 어떻게 host server에게 docker command를 전달해야하는지 입니다. 

 

 DooD 방법은 host servere의 docker socket을 Jenkins container에 볼륨 세팅을 통해 공유하고 host server의 docker daemon을 이용해 CI를 수행하는 docker command를 실행하는 것입니다. 말이 어려운데, Jenkins container에서 ML project 이미지를 container화 하고 project를 돌리겠다는 것입니다.

 

 그리고 이 볼륨 셋팅을 공유한다는 것은 docker run 과정에서 -v 옵션을 통해서 이루어집니다.

docker run -v /var/run/docker.sock:/var/run/docker.sock ...

 

Jenkins Image 변경


 그러면 이제 Jenkins container를 만들어 보도록 하겠습니다. 문제는 Dockerhub에서 제공하는 공식 Jenkins 이미지에서 docker가 설치되어 있지 않기 때문에 저희는 docker engine이 설치된 이미지가 필요합니다. 그래서 Dockerfile파일로 새로운 이미지를 만들도록 하겠습니다. 

FROM jenkins/jenkins:lts 
USER root 

ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update

RUN apt-get update && \ 
	apt-get -y install apt-transport-https \ 
    ca-certificates \ 
    curl \ 
    gnupg2 \ 
    wget \
    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로 image를 만든 뒤에 확인해줍시다.

docker built -t "jenkins:python" .
docker images

REPOSITORY        TAG                               IMAGE ID       CREATED          SIZE
jenkins           python                            1b41f1008f63   54 seconds ago   1.64GB

 위와 같이 "jenkins:python" 이미지가 잘 완성이 되었다면 컨테이너로 띄워줍니다. 이때 -v 옵션에 주의해서 작성해주세요. 특히 docker.sock 파일은 잊으면 DooD가 안되므로 꼭 해주어야 합니다. 

docker run -d -p 60008:8080 -v ~/jenkins_home:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock --name=jenkins jenkins:python
 
docker ps

CONTAINER ID   IMAGE                                  COMMAND                  CREATED         STATUS        PORTS                                               NAMES
0339e1bf4300   jenkins:python                         "/sbin/tini -- /usr/…"   2 seconds ago   Up 1 second   50000/tcp, 0.0.0.0:60008->8080/tcp                  jenkins

 container가 잘 띄워졋다면 해당 container에 접속해서 docker 가 host server의 docker.sock과 잘 연결되었는지도 확인해 봅시다. 

docker exec -it 0339e1bf4300 /bin/bash
root@0339e1bf4300:/# docker ps

CONTAINER ID   IMAGE                                  COMMAND                  CREATED          STATUS          PORTS                                               NAMES
0339e1bf4300   jenkins:python                         "/sbin/tini -- /usr/…"   17 seconds ago   Up 16 seconds   50000/tcp, 0.0.0.0:60008->8080/tcp                  jenkins

 위와 같이 잘 설정이 되었다면 Jenkins container에서 host server의 도커 소켓을 사용하게 되는 것이므로 host servere의 이미지 및 컨테이너 접속등을 모두 진행이 가능해집니다.

전체적인 구조


 한가지 더 중요한 docker image가 필요합니다.  저희가 DooD 구조를 선택한 이유는 ML project가 작동하는 docker image를 docker run으로 띄우고 띄워진 docker container에서 master 로 push가 되어 PR이 생성된 새로운 코드를 작동시켜 변경된 코드가 기존의 ML project를 여전히 잘 동작시키고 있는지를 확인하는 것 입니다.  

 

 대략적인 구조도는 다음과 같습니다. 열심히 최신 AI Engine 프로젝트를 받아와서 개발을 합니다. 이때, AI Engine이 작동하는 docker image가 있고 그 image를 container로 만들어서 그 속에서 작업을 하고 있다고 가정하겠습니다. 

 

 그 후, 일정한 개발이 진행되면 Push를 하고 Pull Request (PR)을 진행하게 됩니다. 이때, PR이 생성되면 Jenkins 에서 이를 인식하고 Push된 코드가 반영된 AI Engine 코드를 Pull해 옵니다. 그러면, 이 변화된 코드가 잘 작동하는 것인지를 확인하는 일이 남았습니다.

 

 저희는 이때 AI Engine이 작동하는 docker image를 container로 run 시킵니다. 이 과정에서 docker in docker가 아니라 docker out of docker 형태로 작동을 시킨 후 해당 image에서 변화된 코드를 실행시켜 작동 테스트를 진행하게 됩니다.

Jenkins Create Job [1]


 이제 다시 Create Job을 통해서 ML project 가 실행되도록 해주어야 합니다. Build Section 외에는 이전 포스팅과 동일하게 진행이 되니 https://mjshin.tistory.com/3 을 참조해주세요. 이전 포스팅에서 제가 다루지 않았던 부분이 Build 섹션입니다. 이 부분이 제일 중요한 부분이니까 잘 봐주세요.

 

 실제로 Jenkins의 Pipeline을 사용해서 정의하고 실행시키면 더 좋지만 저희는 기본적인 Jenkins의 사용법을 다루고 있으니 잘 아시는 분들에게는 답답하실 수도 있습니다 ㅎㅎ.. 추후에 Pipeline을 사용한 셋팅도 포스팅 할 예정입니다. 

 

 우선 저희는 위의 구조도에서 보여드린 것 처럼 DooD 형식으로 Jenkins docker에서 실험을 진행 할 AI Engine container를 띄워야 합니다. 이때, 동시에 여러 테스트를 진행할 계획은 없기 때문에 1개의 container만 존재하도록 하고 싶기에 docker ps 명령어를 사용해서 현재 host server 에서 실행 중인 container 목록을 불러옵니다.

 

 그리고, test 진행을 위해 사용할 container 이름인 jenkins_test 를 검색합니다. 이렇게 하는 이유는 혹시나 test 진행 후에 docker container가 남아 있는 경우 새롭게 업데이트 된 코드를 기반으로 container image를 다시 만들고 docker container를 만들기 위해서 입니다. 

 

 아래의 코드는 다음과 같은 내용입니다.

  1. "docker rm -f jenkins_test" jenkins_test container가 현재 host server에서 작동중이라면 docker rm -f jenkins_test로 해당 container를 종료시킵니다.
  2. "docker build --tag ts_engine:test ." Push로 업데이트 된 코드를 기반으로 Dockerfile 설정을 따라서 docker image를 생성합니다. 이때, 생성된 이미지의 이름은 ts_engine이며 tag는 test 입니다.

"docker run -dit --runtime=nvidia -v <jenkins container와 volume을 share한 jenkins_home/workspace/job 이름 >:<jenkins_test container 내부에서 github code를 저장하고 싶은 위치> --name jenkins_test ts_engine:test

 위에서 제일 중요한 부분은 3번입니다. 여기서 이전 포스팅의 jenkins container를 생성할 때 저희는 jenkins_home이라는 폴더를 만들어서 host server와 jenkins container가 공유하는 폴더를 생성했습니다. 꼭 이 폴더의 "workspace/job 이름"을 지정해주세요. 

 

 Job 이름을 지정하는 이유는 Jenkins가 작동하여 Push된 코드를 받아오는 위치이므로 해당 코드를 사용하여 ML project들을 테스트하기 위함입니다.

 

 그리고 마지막으로 python3를 통해 구현된 ML project가 있을 것이기 때문에, docker exec 명령어를 통해서 저희는 docker container에 main.py를 수행하도록 명령을 전달합니다. 

 

Jenkins Create Job [2]


 저희가 추가적으로 진행해야 할 부분들이 있습니다. 아마 이전 포스팅대로 소스 코드 관리 섹션을 구성하셨다면 workspace에는 master의 코드만이 pull되어 저장되고 우리가 Push로 새롭게 수정하여 PR을 만든 코드가 생성되지 않습니다. 그래서 다음과 같이 수정이 필요합니다.

 우선 Repository URL과 Credentials는 이전 포스팅과 동일하게 유지합니다.  (Name 부분 비워두셔도 됩니다.) 중요한 부분은 Refspec 입니다. Refspec은 저희가 push 한 새로운 코드가 담긴 branch를 찾는 방법입니다.

 

 * 과 + 가 정규표현식과 유사하다고 생각하시면 됩니다. refspec 이란 것은 간략하게 <src branch>:<dest branch> 로 구성하게 되며, 이때 저 같은 경우는 이 포스팅을 위해 origin (주로 upstream 이라고 하죠) 에 branch를 만들 때, mf-myungjae-shin/test_project 와 같이 브랜치를 만들었습니다.

 

 그러므로 origin/mf-myungjae-shin 까지 고정이며 PR을 만들때 마다 /test_project가 변하도록 하였습니다. PR 마다 변하는 부분을 *로 설정하시면 됩니다.

 

 그리고, 이와 대응되도록 아래의 Branch Specifier (Build 할 데이터를 가져올 branch 를 설정) 는 */mf-myungjae-shin/* 로 왼쪽 *는 잡다한 구간을 의미하고 저희는 항상 mf-myungjae-shin으로 한번 나눈 후 뒤쪽의 *가 PR 마다 변하므로 *를 설정해줍니다.

 

 이렇게 Refspec과 Branch Specifier를 정의하면 마지막 단계를 빼고 모든 준비가 끝났습니다.  아래 Reference에 참조한 사이트에 있지만 혹시 Refspec이 이해가 안가신다면 이렇게 정리가 될 수 있습니다.

  • feature: +refs/heads/feature/*:refs/remotes/origin/feature/*
  • develop: +refs/heads/develop:refs/remotes/origin/develop
  • release: +refs/heads/release/*:refs/remotes/origin/release/*
  • master: +refs/heads/master:refs/remotes/origin/master
  • all branch: +refs/heads/*:refs/remotes/origin/*

 저희는 이제 빌드 유발 섹션만을 남겨두었습니다! 이때, 제 화면과 다른 분들은 Plug-In 목록에서 Generic Webhook Trigger 플러그인을 설치해주시면 똑같이 하실 수 있습니다.

 

 이 플러그인은 Jenkins 에서 Github 나 GitLab 와 연동할 때특정 브랜치(Branch) 에만 작동되게 하기 위해서 Execute Shell 를 활용할 때 사용합니다. 

 

 Generic Webhook Trigger의 체크박스를 체크해준 뒤, Post content parameters를 추가해줍니다. Variable은 ref, Experession은 $.ref 를 지정해줍니다. 그리고 JSON Path를 클릭해주세요. 이것은 Github에서 Jenkins 를 호출할때 JSON 포맷으로 관련된 데이터를 넘겨주기 때문입니다.

 

 JSON 포맷이기 때문에 아래와 같이 설정을 하면 우리가 정규표현식을 사용하여 정보를 가져올 수 있게 됩니다. 이때, “$.ref”“refs/heads/mf-myungjae-shin/test_project” 식의 값을 가지고 오고, 이 값은 이 플러그인에서 $ref 라는 변수에 할당되게 됩니다. 

 그리고 조금 더 내려와서 Token 섹션에 다음과 같이 편한 이름을 설정해주세요. 이 이름은 Github 프로젝트의 Settings -> Webhook에서 Payload URLhttp://<host 주소>/generic-webhook-trigger/invoke?token=<Token 이름>와 같이 등록해주세요. 

 더 아래쪽으로 내려와서 Optional filter 섹션에서는 다음과 같이 Expression과 Text을 등록해주시면 됩니다. Expression에서의 정규표현식을 따르는 저희가 위에서 정의한 $ref branch 만을 가져와서 Jenkins가 테스트를 수행한다는 것을 의미합니다.

 

 제 포스팅의 예시를 따르면 origin에서 mf-myungjae-shin/test_project가 되겠네요. 만약 원하시는 다른 브랜치에만 반응하도록 하기 위해서는 “refs/heads/[브랜치 이름]” 과 같이 정규표현식 없이 딱 지정해주셔도 됩니다.

 이제 모든 준비가 끝났기 때문에 코드를 수정하시고 github에서 push를 하신 후에 Jenkins의 dashboard에서 작동을 확인하실 차례입니다. 저 같은 경우는 다음과 같이 25번째에서 성공했고 26번째 다른 push에서 실험을 대기중인 모습입니다. 

 

정리


 이렇게 2개의 포스팅으로 ML project를 DooD를 통해서 자동으로 Jenkins가 테스트 해주는 시스템을 구성하였습니다. 하지만 사실 Pipeline이라는 좋은 방법이 존재하는데 그것도 나중에 포스팅에서 다룰 예정이니 기다려주세요 ㅎㅎ 그 전에는 이 포스팅으로 기본적인 Jenkins 와 MLOps에 대한 이해를 하시고 적용해보시면 좋을 것 같습니다. 

 

Plug-In


Generic Webhook Trigger 은 추가로 설치해주세요 :) 

 

 

 

DooD 에서의 권한 문제 


DooD 방법으로 docker command를 전달하는 과정에서 permission error 가 발생하는 경우가 있습니다 (https://aidanbae.github.io/code/docker/dinddood/). 이런 경우에는 Jenkins container 속에서 도커 소켓이 있는 곳을 한번 ls -l 로 살펴보고 linux group id 를 확인해야 합니다. Jenkins container의 group id가 host server의 group id 와 맞춰주는 것으로 해결 할 수 있다고 합니다.

 

 

 

Reference

https://medium.com/crocusenergy/1-docker-%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-74be2b6fd6ae

 

1. Docker 는 무엇인가?

모든 기술에 있어 그 기술이 왜 탄생 하였는지와 왜 사용하는지가 가장 중요한 것이 아닐까 생각이 된다. 따라서, 막연한 개념 보단 저자의 경험을 토대로 서두를 작성하고자 한다.

medium.com

https://coding-start.tistory.com/329

 

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

아마 이번 포스팅부터 Jenkins(젠킨스)에서 정말 쓸모 있는 부분을 다룰 것 같다. 젠킨스 파이프라인을 다루기 전에 Jenkins 안에서 dood(docker out of docker)로 docker agent를 실행하는 방법에 대해 다루어..

coding-start.tistory.com

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

 

DinD(docker in docker)와 DooD(docker out of docker)

DinD(docker in docker)와 DooD(docker out of docker) - 아이단의 블로그

aidanbae.github.io

https://jeongupark-study-house.tistory.com/76

 

Docker - Docker란 무엇이고 왜 사용했는가?

프로젝트를 진행하면서 doker라는 것을 사용하였습니다. 제가 아는 지식은 docker를 사용하여 필요한 시스템이나 서버의 image를 만들고 container에 그 이미지를 담아 가상머신(VM)에서 실행하는 것으

jeongupark-study-house.tistory.com

https://freedeveloper.tistory.com/464

 

Jenkins Generic Webhook Trigger를 이용한 GitHub branch별 push event WebHook 설정

GitLab 에서 WebHook 을 설정해서 사용하다가 GitHub로 이전할 일이 생겼는데 GitHub에는 branch 별로 push를 하면 branch 명으로 판단해서 WebHook 을 동작시키는 기능이 없었다 그래서 검색을 하다가 알게 된

freedeveloper.tistory.com

https://linux.systemv.pe.kr/generic-webhook-trigger-%EC%84%A4%EC%A0%95/

 

Generic WebHook Trigger 설정 - Voyager of Linux

Jenkins 에서 Github 나 GitLab 와 연동하기 위해서 Generic WebHook Trigger 플러그인을 많이 사용한다. 검색을 해보면 사용법이 아주 많이 나오는데, 특정 브랜치(Branch) 에만 작동되게 하기 위해서 Execute Shel

linux.systemv.pe.kr