나는 문어~ 꿈을 꾸는 문어~
젠킨스로 Build Test Deploy 파이프라인 만들기 (Jenkins, Flask, Docker, EC2) 본문
젠킨스로 Build Test Deploy 파이프라인 만들기 (Jenkins, Flask, Docker, EC2)
harrykur139 2022. 8. 25. 02:08이전 포스팅에서는 머신러닝 모델을 서빙하는 Flask 서버를 만들고 이를 도커 컨테이너로 실행했다. 이를 AWS의 EC2 인스턴스에 띄워놓고 외부에서 접속하는 작업까지 해보았다. 이는 아마 머신러닝 모델을 서비스에서 사용하기 위한 가장 간단하고 기본적인 형태일 것이다. 이것을 기반으로 조금 더 전처리 코드나 입력 값의 형태가 복잡한 모델도 서빙할 수 있을 것 같다.
https://harrykur139.tistory.com/26
머신러닝 모델을 서빙해보자 (ec2 + sklearn + flask + docker)
머신러닝을 서비스에서 사용하기 위해서는 모델링과 서빙이 필요하다. 누군가 성능이 아주 좋은 모델을 만들었다고 해도 이를 서비스하기 위해서는 많은 고민이 필요하다. 모델을 어떤 서버에
harrykur139.tistory.com
여기서 한 단계 고도화시키기 위해 jenkins를 사용해 볼 것이다. 사실 한 단계만 고도화시키려고 했는데 공부하다가 너무 어려워서 잠깐 기절했다.
Jenkins는 CI(Continuous Integration)과 CD(Continuous Deployment)를 할 수 있게 도와주는 툴이다. 처음에는 그냥 쉘스크립트 몇 개 짜놓고 이어붙이면 끝나는 것인 줄 알았다. 그런데 젠킨스 자체도 생각보다 복잡했고 코드를 짜고부터 배포까지의 각 단계가 나에게는 그렇게 익숙한 작업이 아니다보니 학습하는데 꽤 많은 어려움을 겪었다.
그래서 무엇을 할 것인가
코드를 짜고 github에 푸시를 하면 내가 설계해놓은 파이프라인들이 하나씩 실행되어 배포까지 자동으로 되게 만들 것이다. 구체적으로는 다음과 같다.
- 코드를 작성하고 커밋한 후 푸시한다.
- 코드 푸시를 인식한 젠킨스가 서버에 최신 코드를 내려받는다. (젠킨스와 Flask test, Flask deploy는 모두 하나의 EC2 인스턴스에서 동작한다. 실제 운영에서는 각각의 기능에 맞게 인스턴스를 분리하는게 맞을 것이다.)
- Test 도커파일을 빌드하고 Unit test를 진행한다. 테스트가 통과되지 않으면 더 이상 파이프라인은 진행되지 않는다.
- Deploy 도커파일을 빌드한다.
- Dockerfile을 Docker hub로 푸시한다.
- 배포한다.
이 과정을 모두 파이프라인으로 만든 후, 배포된 서버 코드를 수정한 후 요청을 보내 수정된 값이 반영이 된 것을 확인하면 이 포스팅이 끝날 것이다.
서버 코드는 이전 포스팅에서 만든 코드를 활용할 것이다. 링크는 아래와 같다.
https://github.com/euiraekim/flask-model-serving
젠킨스 설치 및 구동 (Docker)
시작하기 전에 EC2 인스턴스를 생성하고 docker와 git을 설치해야한다. 이 과정은 다루지 않는다. 접속을 위해 보안그룹을 열고 하는 작업들도 다루지 않는다. 젠킨스에 접속하기 위해 8080번 포트를, Flask 서버에 접속하기 위해 5000번 포트를 열어두도록 하자.
도커 컨테이너에서 젠킨스를 실행할 것이기 때문에 해당 이미지를 docker hub에서 받아오자.
sudo docker pull jenkins/jenkins:lts-jdk11
젠킨스 컨테이너를 실행하자.
sudo docker run --name jenkins-docker -d -p 8080:8080 \
-v /home/jenkins:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/bin/docker:/usr/bin/docker \
-u root jenkins/jenkins:lts-jdk11
이 부분부터 매우 힘들었다. 명령어에 대하여 한줄한줄 알아보자.
- 1줄 : 컨테이너를 실행하는데 컨테이너 이름은 jenkins-docker다. -d는 백그라운드로 실행한다는 뜻이고 -p 옵션은 호스트의 8080번 포트로 들어온 요청을 컨테이너의 8080번 포트로 매핑하겠다는 것이다.
- 2줄 : 호스트의 /home/jenkins 폴더를 컨테이너의 /var/jenkins_home과 연결한다. 이렇게 하면 컨테이너를 껐다 키더라도 jenkins의 정보가 남기 때문에 다시 컨테이너를 켜도 이전에 하던 작업을 이어서 할 수 있다.
- 3, 4줄 : 도커를 실행하는데 필요한 호스트의 파일을 컨테이너로 연결한다. 나중에 젠킨스 컨테이너에서 도커를 사용할 일이 많다. 빌드, 푸시, 컨테이너 실행 등이다. 이런 작업을 하려면 젠킨스에서 도커 명령어를 사용할 수 있어야 한다. 그러기 위해서는 컨테이너 안에 도커를 깔거나, 컨테이너에서 호스트 도커를 사용할 수 있어야하는데, 내가 사용하는 방법은 후자고 이러한 방식을 DooD (Docker out of Docker)라고 한다.
- 5줄 : root 사용자로 컨테이너를 실행한다.
EC2의 아이피를 8080번 포트로 접속하면 아래 화면이 뜨면서 초기 비밀번호를 입력하라고 한다.
아래 명령어를 쳐서 해당 부분을 복사해 입력하자.
sudo docker logs jenkins-docker
Install suggested plugins를 클릭하여 젠킨스가 제안하는 플러그인들을 설치하고 회원가입하고 팍팍 넘어가주자. 그러면 아래와 같은 메인 페이지를 볼 수 있다.
Credentials 설정하기
CI CD 파이프라인을 만들려면 어쩔수 없이 다른 서버들에 접속을 해야한다. 예를 들면 깃허브나 도커허브다. 접속을 하기 위해서는 access token나 비밀번호같은게 필요한데 이를 젠킨스에 설정해놔야한다.
우선 깃허브의 Personal access token을 발급받자. 발급 방법은 구글링하면 천지 삐까리다.
그리고 젠킨스 메인화면의 왼쪽에 Jenkins 관리(혹은 Manage Jenkins)를 누르고 Security 카테고리에서 Manage Credentials를 누르자. 그러면 아래와 같은 화면이 뜨는데 여기서 global을 누르고
Add Credentials를 누르자.
Credential의 정보를 입력하고 저장하자.
- Username : github username
- Password : github access token
- ID : 해당 토큰에 붙여주는 이름이다. jenkins pipeline을 작성할 때 이 ID를 통해 credential에 접근할 수 있다.
이제 Dockerhub의 access token을 등록해야한다. github와 동일한 방법으로 하되 Username에는 dockerhub의 username을 넣어주면 되겠다.
다 등록하면 아래와 같이 2개의 토큰이 생성된 것을 확인할 수 있다.
Github webhook 등록하기
젠킨스로 파이프라인을 만들고 직접 버튼을 눌러 빌드를 할 수 있다. 하지만 우리가 원하는 수준의 자동화는 커밋 후 푸시를 하면 자동으로 파이프라인 코드가 진행되고 배포까지 되는 것이다. 그렇게 하기 위해서는 github의 webhook을 등록하여 젠킨스와 연동을 해야한다.
서버코드가 있는 깃헙 레포지토리로 가서 Settings를 누르고 Webhooks를 누르고 Add webhook을 누른다.
아래와 같이 등록해주면 깃헙과 젠킨스가 연동이 되고 push를 했을 때 젠킨스가 이를 인식할 수 있게 된다. Payload URL에는 젠킨스의 url을 적어주자. 물론 아직 젠킨스 서버에 깃허브 링크를 설정하지 않았기 때문에 이 설정도 해줘야한다.
젠킨스 설정, 파이프라인 등록하기
파이프라인을 실행하기 위해서는 우선 등록을 해야한다. 젠킨스 메인 페이지에서 새로운 Item(혹은 New Item)을 누르자. 그리고 이름을 알아서 설정하고 우리는 파이프라인을 작성하여 젠킨스를 실행할 것이기 때문에 Pipeline을 누르고 OK 버튼을 누르자.
Github project에 내 레포지토리의 url을 집어넣어야한다.
아래 이미지에 체크되어있는 부분을 체크해주자. 위에서 등록한 웹훅이 동작했을 때 이 파이프라인이 동작하게 한다는 의미다.
이제 파이프라인을 어떤 식으로 동작하게 할 지 설정해야한다. Definition을 SCM(source code management)에서 스크립트를 가져오게 바꿔주고 SCM은 Git으로 설정한 후 URL을 입력하고 아까 등록한 github Credentials를 설정하면 된다. 아주 직관적이다. 그리고 main branch도 설정한다.
맨 아래쪽에 Script Path를 Jenkinsfile로 설정하는 부분이다. 아마 디폴트로 저렇게 되어있을 것인데, 이는 레포지토리의 루트 폴더 아래에 있는 Jenkinsfile이라는 이름을 가진 파일에 적혀있는 pipeline을 실행하겠다는 뜻이다.
젠킨스 파이프라인 만들기
깃허브 웹훅도 등록했고 젠킨스의 모든 설정을 다 마쳤다. 이제 파이프라인만 만들면 된다.
아래가 파이프라인이다. Jenkinsfile이라는 파일을 루트 경로에 만들고 아래 파이프라인을 입력 후 저장하면 된다. 파이프라인의 각각에 대한 설명은 주석에 써놓았다.
pipeline {
// 어떤 젠킨스 agent가 이 파이프라인을 처리할지 설정
agent any
// 환경 변수들 설정
environment {
GITHUB_URL = 'https://github.com/euiraekim/flask-model-serving.git'
GITHUB_CRED_ID = 'github-token'
GITHUB_BRANCH = 'main'
DOCKERHUB_REPO = 'harrykur139/flask-ml-server'
// 이렇게 하여 credentials의 id로 credentials를 가져와 사용할 수 있다.
DOCKERHUB_CRED = credentials('dockerhub-token')
CONTAINER_NAME = 'flask-ml-container'
}
stages {
// 깃허브 소스코드를 가져오는 부분
stage('Clone') {
steps {
echo 'Clone start'
// git plugin을 사용하여 레포지토리 코드를 클론
git branch: GITHUB_BRANCH, credentialsId: GITHUB_CRED_ID, url: GITHUB_URL
sh 'ls'
echo 'Clone end'
}
}
// test용 Dockerfile(Dockerfile.test)을 빌드하고 실행한다.
// unit test를 실행하여 테스트가 실패하면 여기서 파이프라인이 종료된다.
stage('Test') {
steps {
echo 'Test start'
sh 'docker build -f Dockerfile.test -t test-image .'
sh 'docker run test-image'
echo 'Test end'
}
}
// 배포용 이미지를 빌드한다.
// 그리고 image tag 명령어로 latest tag말고 BUILD_NUMBER tag도 달아준다.
// BUILD_NUMBER는 젠킨스의 빌드넘버다. 등록한 파이프라인을 몇 번 실행했냐라고 보면 된다.
stage('Build') {
steps {
echo 'Build start'
sh 'docker image build -t $DOCKERHUB_REPO:latest .'
sh 'docker image tag $DOCKERHUB_REPO:latest $DOCKERHUB_REPO:$BUILD_NUMBER'
echo 'Build end'
}
}
// 빌드한 이미지를 Dockerhub로 푸시한다.
stage('Push') {
steps {
echo 'Push start'
sh 'echo $DOCKERHUB_CRED_PSW | docker login -u $DOCKERHUB_CRED_USR --password-stdin'
sh 'docker push $DOCKERHUB_REPO:$BUILD_NUMBER'
sh 'docker push $DOCKERHUB_REPO:latest'
echo 'Push end'
}
}
// 서버에 컨테이너를 실행하여 Flask 서버를 배포한다.
// BUILD_NUMBER가 1일 경우(파이프라인 첫 번째 실행) 컨테이너를 실행만 하고
// BUILD_NUMBER가 1이 아니면 기존 컨테이너를 중단, 삭제한 후 실행한다.
stage('Deploy') {
steps {
echo 'Deploy start'
script{
if (BUILD_NUMBER == "1") {
sh 'docker run --name $CONTAINER_NAME -d -p 5000:5000 $DOCKERHUB_REPO'
}
else {
sh 'docker stop $CONTAINER_NAME'
sh 'docker rm $CONTAINER_NAME'
sh 'docker run --name $CONTAINER_NAME -d -p 5000:5000 $DOCKERHUB_REPO'
}
}
echo 'Deploy end'
}
}
}
}
젠킨스 파이프라인 실행하기
위에서 젠킨스에 Item을 등록했기 때문에 메인 화면에는 아래와 같이 뜬다. 오른쪽 빨간 박스인 재생 버튼을 누르면 파이프라인이 실행된다. 물론 웹 훅을 등록해놓았기 때문에 깃허브 푸시를 하면 자동으로 실행되지만, 이렇게 수동으로 실행을 할 수도 있다. 빌드가 끝난 후 왼쪽 빨간 박스를 누르고 들어가보자.
첫 번째 빌드가 아주 기가 막히게 들어갔다. 아 참고로 젠킨스에서는 이 파이프라인을 실행하는 것을 빌드라고 하는 것 같다. (pipeline 내부의 빌드 stage를 의미하는 것이 아니다.)
Deploy까지 문제 없이 성공했으므로 실제로 요청을 날려서 테스트해보자. Postman을 사용해서 요청했다. status가 200으로 정확히 요청이 성공한 것을 확인할 수 있다.
이제 실제로 웹 훅을 사용하여 파이프라인이 잘 실행되는지, 그리고 바뀐 코드가 잘 반영이 되는 지 확인해보자.
소스코드의 app.py에서 prediction 엔드포인트 부분이다. 여기서 status를 200이 아닌 2000으로 바꿔보자. 아무 의미없지만 바뀌는 것을 확인하기 위한 수정이다.
@app.route('/predict', methods=['GET'])
def predict():
feature_dict = request.get_json()
x = [feature_dict[col] for col in COLUMNS]
x = np.array(x).reshape(1, -1)
pred = model.predict(x)[0]
return {
# 200을 2000으로 바꿈
'status': 2000,
'class': CLASSES[pred]
}
이제 코드를 커밋하고 푸시해보자.
git add .
git commit -m "predict의 status를 200에서 2000으로 수정"
git push origin main
푸시 후에 메인화면을 보면 빌드가 진행되는 것을 실시간으로 확인할 수 있다. 빌드가 완료되면 아래와 같은 화면을 볼 수 있다.
도커 허브의 레포지토리에도 TAG가 1, 2, latest인 이미지들이 잘 올라간 것을 확인할 수 있다. 아래는 도커허브 레포지토리 화면이다.
이제 실제로 새로운 코드가 배포되었는지 확인해보자. Postman으로 동일하게 요청한 결과, status 값이 2000으로 요청을 오는 것을 확인할 수 있다.
'MLOPS & 백엔드' 카테고리의 다른 글
AWS 네트워크 구성하고 모델 서빙하기 (2) 모델 서빙 서버 생성 및 로드밸런서 사용 (EC2, ALB, Flask) (0) | 2022.10.10 |
---|---|
AWS 네트워크 구성하고 모델 서빙하기 (1) 네트워크 구축 (VPC, Subnet, Internet gateway, NAT gateway, Route Table) (0) | 2022.10.09 |
Airflow를 간단히 사용해보자. (데이터 전처리, 모델 학습) (0) | 2022.08.30 |
머신러닝 모델을 서빙해보자 (ec2 + sklearn + flask + docker) (1) | 2022.08.11 |
도커를 시작해보자 (hello-world) (0) | 2022.01.28 |