3~4달 전에 도커와 젠킨스를 이용해서 CI/CD를 구성했지만 현시점에 기억을 다시 살리고자 정리를 진행해 보았습니다.
DOCKER
우선 도커를 쓰는 이유는 동일한 환경에서도 IMAGE를 통해 안전하게 배포하고 컨테이너를 통해 관리할 수 있어서 사용했다.
VM과 비교가 많이 된다고 들었는데 VM의 경우 하나의 머신에 OS까지 전부 들어있게 된다. 예를 들면 윈도우 환경에서 맥 OS를 가진 VM, 리눅스를 가진 VM을 구동시킬 수 있지만 무겁다는 문제가 발생하게 되는데!
도커를 사용하면 컨테이너에 OS를 설치하지 않게 된다. 컨테이너는 호스트 OS의 커널을 공유한다고 한다.
도커를 사용하면 3가지의 개념을 알아야 한다.
DOCKER FILE
도커 이미지를 생성하기 위한 명령어 or 레시피를 의미한다. 도커 파일에 적어놓은 명령어를 수행해서 도커 컨테이너를 만들기 위한 이미지를 생성할 수 있다.
IMAGE
애플리케이션을 실행하기 위한 모든 구성이 들어가 있는 스냅샷이라고 생각한다. 스프링부트를 생각하면 모든 코드, JAR 파일, 의존성 등등 실행하기 위한 모든 부품들이 하나의 이미지라는 것으로 만들어지게 된다. 이미지는 읽기 전용이며 한번 생성된 이미지는 불변의 성질을 가진다.
도커 컨테이너
만들어놓은 IMAGE를 하나의 컨테이너로 만들어서 실행시킬 수 있다. 스프링 부트, mysql, 젠킨스 등 다양한 프로그램을 하나의 IMAGE로 만들고, 해당 이미지를 컨테이너로 만들어서 동작시킨다.
내가 사용하고 있는 도커파일은 아래와 같다.
FROM openjdk:17-alpine
ARG JAR_FILE=/build/libs/restapi-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} /restapi.jar
ENTRYPOINT ["java","-jar","/restapi.jar"]
- FROM
생성할 이미지의 베이스가 될 이미지를 뜻한다. 아래의 코드는 openjdk 17 이미지를 기본으로 사용하겠다는 소리.
FROM openjdk:17-alpine
- ARG
변수를 선언하는 키워드
gradlr로 빌드한. jar 파일의 위치를 변수로 설정한 것. JAR_FILE이라는 변수에 해당 위치를 설정했다.
ARG JAR_FILE=/build/libs/restapi-0.0.1-SNAPSHOT.jar
- COPY
JAR_FILE 변수에 지정한 파일을 restapi.jar라는 이름으로 컨테이너에 추가한다.
COPY ${JAR_FILE} /restapi.jar
- ENTRYPOINT
컨테이너가 시작될 때 수행할 명령어를 의미한다.
jar을 실행시킬 때 java -jar ~~~. jar 이렇게 실행시키기 때문에 아래처럼 명령어를 작성.
ENTRYPOINT ["java","-jar","/restapi.jar"]
도커파일의 작성이 끝났다. 이 상태로 도커에서 깃허브 repo를 클론하고 만들어놓은 docker file을 이용해서 docker build를 실행하면 이미지가 생성된다. 이후 컨테이너를 만들면 ENTRYPOINT가 동작해서. jar 빌드까지 수행하게 된다.
근데 이거를 수동으로 해도 문제는 없지만 젠킨스나 깃허브 액션을 이용하면 자동화를 진행할 수 있다.
젠킨스
CI/CD를 지원하는 툴이다. 위에서 도커파일을 만들었지만 클론, 도커 빌드, 컨테이너 생성 등 전부 개발자가 직접 수동으로 해줘야 하는데 이걸 스크립트를 짜서 특정 행동을 했을 때 자동으로 스크립트가 실행되도록 만들 수 있다.
즉 배포, 빌드 과정을 자동화를 진행할 수 있다는 것.
ec2 내부에 젠킨스를 설치할 수도 있고 도커에 젠킨스를 설치할 수도 있다.
나는 도커에 설치를 선택했고 해당 방식은 Jenkins In Docker라고 한다.
ec2에 직접 젠킨스를 설치해도 되지만 도커에 설치하면 컨테이너로 격리된 환경을 제공해서 항상 동일한 환경에서 실행되도록 할 수 있다. 또한 젠킨스 이미지를 받아서 컨테이너를 만들기만 하면 되기에 설치 과정이 간단하다.
도커 허브에서 젠킨스 이미지를 제공하기에 이를 이용하면 쉽게 컨테이너를 구축할 수 있다.
이전에 CI/CD를 구성해 놨지만 다시 처음부터 진행해 보고자 기존의 젠킨스 컨테이너를 삭제하고 새로 진행.
먼저 젠킨스 이미지를 받아줍니다.
docker pull jenkins/jenkins
docker images 명령어를 통해 현재 이미지를 전부 확인할 수 있다.
이제 해당 jenkins 이미지를 통해 젠킨스 컨테이너를 생성해줘야 하는데 먼저 하나의 폴더를 만들어주자.
sudo mkdir /home/opendocs/jenkins
위 명령어를 이용해서 폴더를 하나 만든 다음에 아래 명령어를 통해 젠킨스 컨테이너를 생성한다.
sudo docker run --name jenkins -d -p 9090:8080 -p 50000:50000 -v /home/opendocs/jenkins:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock -u root jenkins/jenkins:latest
명령어를 하나씩 살펴보자.
먼저 run을 통해 이미지를 컨테이너로 실행하겠다는 명령
sudo docker run
해당 컨테이너의 이름을 의미
--name jenkins
젠킨스 컨테이너를 백그라운드에서 실행하도록 설정.
이거를 설정하지 않으면 터미널에 젠킨스 로그가 뜬다고 들었다. 그래서 백그라운드로 돌리는 것
-d
외부에서 9090으로 요청이 들어오면 내부 컨테이너의 8080으로 전달.
-p 9090:8080
외부에서 50000으로 요청이 들어오면 내부 컨테이너의 50000으로 전달.
-p 50000:50000
위에서 mkdir을 통해 /home/opendocs/jenkins 폴더를 하나 만들어줬었다.
기본적으로 젠킨스의 데이터들이 /var/jenkins_home에 저장되는데 만약에 컨테이너가 삭제된다면 이 설정들이 전부 날아갈 수 있기에 ec2의 폴더를 하나 만들어서 마운트를 시켜주는 것.
-v /home/opendocs/jenkins:/var/jenkins_home
즉 데이터를 보존하기 위함이다.
젠킨스에서 도커 명령어를 사용할 수 있게 도커 데몬 소켓을 마운트.
-v /var/run/docker.sock:/var/run/docker.sock
root 권한을 주도록 설정.
-u root
사용할 이미지 파일
jenkins/jenkins:latest
그러면 아래와 같이 컨테이너가 생성된 것을 확인할 수 있다.
ec2 주소:9090으로 접속하면 아래와 같이 사이트가 뜨면 정상입니다.
아래 명령어를 통해 초기 비밀번호를 찾을 수 있다.
sudo docker logs jenkins
그리고 추천 플러그인을 설치하면 된다.
만약에 이때 ec2가 터질 수 있다. 프리티어를 사용하면 메모리가 부족하기 때문에 터지는 것인데 이때 메모리 스왑을 통해 해결하면 된다.(자세한 내용은 다른 블로그에 많기에 생략)
자 이제 해야 하는 일은 pr이 감지되면 repo를 클론 해서 컨테이너를 만들어 서버를 돌리도록 구성해야 한다.
토큰 생성
깃허브 설정 → Developer settings → Personal access tokens → Fine-grained beta 토큰 생성
이 토큰을 통해 젠킨스랑 깃허브가 서로 상호작용을 진행할 수 있다.
젠킨스 Credentials 등록
젠킨스 관리 -> 시스템 설정 -> 깃허브 서버로 가서 Add를 눌러준다.
이후 아래처럼 Kind는 Secret text로 설정하고 serect에 깃허브 토큰 키를 입력해 준다.
그리고 Credentials를 선택하고 테스트 연결을 눌러서 성공이 뜨면 된다.
여기까지 하면 깃허브와 젠킨스의 연결이 진행된 것이다.
Github Webhook 설정
웹훅을 통해 특정 브랜치에 pr을 하면 감지해서 동작하도록 설정해야 한다. 나는 브랜치마다 구분하고 싶어서 특정 라벨을 같이 날렸을 경우 감지하도록 설정했다.
우선 젠킨스에서 generic Webhook Trigger Plugin을 설치해 준다.
이후 깃허브 프로젝트 repo의 웹훅 관리로 들어가서 Add webhook을 해준다.
Payload URL은 웹훅이 트리거가 되면, 즉 우리가 설정해 놓은 동작이 감지되면 깃허브에서 HTTP POST 요청을 보내는 대상을 의미한다. 이 대상은 젠킨스가 되어야 하는 것.
트리거는 PR만 잡아주도록 설정. 그리고 Add 누르면 된다.
젠킨스 아이템 생성
이름은 마음대로 정하고 파이프라인으로 아이템을 생성.
이제 General로 가서 설정을 진행해야 한다.
Build Trigger 같은 경우는 우리가 설정해 준 generic webhook trigger로 설정하면 된다.
설명을 읽어보면 http://젠킨스주소/generic-webhook-trigger/invoke로 요청이 날아오는 것을 감지한다고 한다.
우리는 pr을 하면 깃허브에서 위 주소로 HTTP POST 요청을 보내도록 했기에 감지할 수 있는 것.
아래 post content parameters 눌러서 아래 파라미터를 추가하면 된다.
LABEL의 경우 name 앞에. 이 2개 있다.(조심)
이게 무슨 의미를 하는지 하나씩 알아보자.
일단 Name of variable은 전부 변수의 이름을 의미한다.(IF_MERGED, BRANCH, LABEL)
나머지 Expression은 깃허브 웹훅 페이로드에서 PR 객체의 merged 필드를 추출해서 머지되면 true, 아니면 false
대상 브랜치의 이름을 추출, PR 객체의 labels 배열에서 name 필드를 추출.
즉 특정 대상 브랜치로 머지가 됐고 라벨이 포함되어 있으면 파이프라인을 동작시키도록 구성하는 것이다.
이렇게 사용했던 이유는 브랜치별로 파이프라인을 구성할 수도 있다는 장점이 있었다.(프런트, 백을 하나의 repo에서 나눠야 하는 경우)
토큰의 경우 backend로 백엔드 파이프라인 전용이라고 정해주었다.
그리고 토큰은 새로 add를 진행해 준다. serect text를 선택한 후 Token에 넣어줄 이름을 secret에 넣어서 만들어준다.
필터는 아래와 같이 설정해 준다. IF_MERGED가 true, BRANCH가 backend, LABEL에 backend가 있으면 동작하도록 필터를 걸어준다.
여기까지 했으면 깃허브 웹훅 url을 다시 설정해주어야 한다.
http://젠킨스주소/generic-webhook-trigger/invoke 여기 뒤에? token=backend를 붙여줘야 한다.
http://젠킨스주소/generic-webhook-trigger/invoke?token=backend 이렇게
파이프라인 작성
나는 yml을 이그노어 처리해 두었기 때문에 yml을 하나 만들어서 젠킨스 컨테이너에 집어넣어 주려고 한다. 그리고 해당 yml을 복사해서 같이 build를 진행할 수 있도록 만들어주면 된다.
우선 ec2에서 yml 파일을 하나 만들고 서버 로직으로 바꿔둔다.
그다음 아래 명령어를 통해 jenkins 컨테이너 안으로 yml 파일을 복사해 주었다. 여기서 yml 폴더는 젠킨스 컨테이너에서 직접 들어가서 만들어주면 된다.
docker cp application.yml jenkins:/var/jenkins_home/workspace/yml/application.yml
1. 깃허브 클론
pipeline {
agent any
environment {
DOCKER_IMAGE = 'restapi'
DOCKER_TAG = 'latest'
}
stages{
stage('깃허브 클론'){
steps{
git branch: '브랜치 이름', credentialsId: '젠킨스 CredentialsId', url: 'https://github.com/qkrrlgus114/restapi.git'
}
}
}
}
브랜치, id, url을 본인의 것으로 설정해서 클론을 진행해 준다.
2. yml 복사
stage('YML 복사'){
steps{
sh 'mkdir -p /var/jenkins_home/workspace/backend-pipeline/backend/src/main/resources' // resources 폴더 생성
sh 'cp -r /var/jenkins_home/workspace/yml/. /var/jenkins_home/workspace/backend-pipeline/backend/src/main/resources/' // YAML 파일 복사
}
}
위는 resources 폴더를 생성해 주고 아래는 yml 파일을 복사해 주는 코드.
이렇게 하면 클론 받은 backend에 yml을 복사해서 넣어준다.
3. jar 빌드
stage('jar 빌드 진행'){
steps{
dir('backend'){
sh './gradlew clean bootJar'
}
}
}
이렇게 하고 진행했더니 아래처럼 권한이 없다고 나온다.
아래와 같이 gradlew를 사용하기 위해서 아래 코드를 추가해 준다.
chmod +x gradlew
stage('jar 빌드 진행') {
steps {
dir('backend') {
sh '''
echo '빌드 프로세스 시작'
chmod +x gradlew
./gradlew clean bootJar
'''
}
}
}
정상적으로 jar 파일 생성된 것을 확인할 수 있다.
4. 이전 이미지 삭제
이미지를 생성하기 전에 이전에 동일한 이미지가 있다면 지우고 시작해야 한다.
stage('이전 이미지 삭제') {
steps {
script {
sh '''
echo '이전 Docker 이미지 삭제'
docker rmi $(docker images -q ${DOCKER_IMAGE}:${DOCKER_TAG}) || true
'''
}
}
}
5. docker file 실행
이제 docker file을 통해 이미지를 생성해야 한다. 그러기 위해서는 젠킨스 내부에서 도커 명령어를 써야 하기에 docker in docker를 쓰기로 했다.
아래 명령어로 도커를 설치해 준다.(젠킨스 내부에서)
apt-get update
apt-get install -y docker.io
그리고 docker --version을 치면 버전이 뜨면 설치가 성공적으로 된 것.
도커 이미지를 만들도록 설정. 도커 파일을 실행한다.
stage('jar 이미지 생성'){
steps{
dir('backend'){
script{
dockerImage = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
}
}
}
}
여기까지 실행하면 아래처럼 이미지가 생성된다.
이제 해당 이미지로 컨테이너를 만들면 된다.
6. 기존 컨테이너 중지
동일한 이름의 컨테이너가 있다면 중지하고 삭제부터 진행한다.
stage('기존 컨테이너 중지 및 제거') {
steps {
script {
sh '''
echo '기존 Docker 컨테이너 중지 및 제거'
docker stop springboot || true
docker rm springboot || true
'''
}
}
}
7. 도커 컨테이너 실행
이제 이미지를 컨테이너로 실행하면 된다.
stage('도커 컨테이너 실행') {
steps {
script {
dockerImage.run('-d -p 호스트포트:컨테이너포트 --name springboot ${DOCKER_IMAGE}:${DOCKER_TAG}')
}
}
}
총 7단계로 파이프라인을 만들어서 동작시켰다.
성공적으로 배포까지 진행이 완료됐다.
어렵고 어려운 배포의 세계.
'프로젝트 > RESTAPI 추천 서비스' 카테고리의 다른 글
확장성이 좋은 oauth 코드로 리팩토링하기 (0) | 2024.06.27 |
---|---|
도커 허브를 추가하여 이미지 백업을 구성하기. (0) | 2024.06.23 |
서비스 내에서 발생하는 쿼리를 분석하고 개선하기 (0) | 2024.06.12 |
certbot SSL 인증서 갱신하기 (1) | 2024.06.11 |