본문 바로가기

Spring

[Spring] MSA 프로젝트 만들기 (4)

2023.03.24 - [Spring] - [Spring] MSA 프로젝트 만들기 (3)

 

[Spring] MSA 프로젝트 만들기 (3)

2023.03.22 - [Spring] - [Spring] MSA 프로젝트 만들기 (2) [Spring] MSA 프로젝트 만들기 (2) 2023.03.19 - [Spring] - [Spring] MSA 프로젝트 만들기 (1) [Spring] MSA 프로젝트 만들기 (1) 2023.02.28 - [Server] - [Spring] 마이크로서

keylog.tistory.com

지금까지 MSA 구조로 프로젝트를 만들고 Spring Cloud Gateway에서 JWT 토큰의 유효성 검사 처리와 CORS 설정 그리고 요청을 처리할 수 있는 마이크로 서비스로 로드밸런싱 하는 것을 알아보았고 이전 글에서는 RestTemplate과 FeignClient를 사용하여 마이크로 서비스들간에 내부통신하는 방법까지 알아보았습니다. 이번 글에서는 각각의 마이크로 서비스에서 도커파일을 생성하고 젠킨스와 도커를 사용하여 CI-CD 파이프라인을 구축하는 방법과 배포할 때 작성했던 스크립트 코드를 보면서 특정 브랜치에 푸쉬 했을때 변경사항이 있는 마이크로 서비스들만 재배포하는 방법에 대해서 알아보겠습니다.

 

2023.02.03 - [Server] - Docker 컨테이너로 젠킨스 설치하기

 

Docker 컨테이너로 젠킨스 설치하기

1. Docker Desktop 설치 먼저 도커 컨테이너를 이용해 Jenkins를 컨테이너화 하고 배포를 진행할 것이기 때문에 각자 운영체제에 맞춰서 Docker Desktop을 설치해줍니다. https://www.docker.com/products/docker-desktop

keylog.tistory.com

 

배포를 위한 환경으로 도커와 젠킨스는 설치가 되어있다고 가정하겠습니다. 도커, 젠킨스 설치는 위 글을 통해서 진행해주시면 되겠습니다. 위 글과 다른 점은 배포 과정은 깃랩과 연동하여 진행되니 gitlab 플러그인을 설치해 주시고 컨테이너 형태로 설치된 젠킨스 안에서 docker 명령어를 실행해야 하기 때문에 아래 명령어를 통해 젠킨스에 접속하신 후 docker를 설치해줍니다.

 

docker exec -it [젠킨스 컨테이너 이름] /bin/bash

 

설치 방법은 아래 링크에서 확인하실 수 있습니다.

https://blog.opendocs.co.kr/?p=704 

 

[Setting | Docker] jenkins 설치 내부에서 docker 실행 | Opendocs Blog

젠킨스를 도커로 설치하고 내부에서 도커를 실행하기 위한(Docker In Docker) 방법을 정리한다. 작성일 : 2022-05-30 1> 젠킨스 설치시 옵션추가 젠킨스 설치시 아래 옵션을 추가해 도커의 소켓 파일 마

blog.opendocs.co.kr

 

1. 도커 허브 로그인 및 네트워크 생성

먼저 도커 이미지를 관리하기 위해 dockerhub 사이트에 접속하셔서 회원가입을 한 후 배포하고자 하는 환경에서 터미널을 사용해 docker login 명령어로 로그인해줍니다. 

 

그런다음 컨테이너 이름으로 통신하기 위해 docker network create --gateway [ip주소] --subnet [ip주소] [네트워크 이름] 명령어를 입력해서 네트워크를 생성해줍니다.

 

https://hub.docker.com/

 

Docker Hub Container Image Library | App Containerization

Deliver your business through Docker Hub Package and publish apps and plugins as containers in Docker Hub for easy download and deployment by millions of Docker users worldwide.

hub.docker.com

2. 도커 파일 생성

 

배포하고자 하는 마이크로 서비스 프로젝트 최상단에서 위와 같이 도커 파일을 생성해줍니다. 

  • FROM : 기본 이미지
  • VOLUME : 외부 마운트 포인트 생성
  • COPY : 파일 또는 디렉토리 추가
  • ENTRYPOINT : 컨테이너 기본 실행 명령어

3. 개인키 발급

젠킨스 공식 홈페이지를 보면 아래와 같이 젠킨스가 credentials로 사용할수 있는 타입이 명시되어있습니다. 젠킨스에서 명시한대로 credential을 생성하기 위해 gitlab 개인키를 발급 받습니다.

 

https://www.jenkins.io/doc/book/using/using-credentials/

 

Using credentials

To maximize security, credentials configured in Jenkins are stored in an encrypted form on the controller Jenkins instance (encrypted by the Jenkins instance ID) and are only handled in Pipeline projects via their credential IDs. This minimizes the chances

www.jenkins.io

 

개인키 발급은 위 화면 처럼 Preferences로 접속하여 Access Tokens 탭에서 생성하실 수 있습니다. 권한을 체크하고 Create Personal Access Token 버튼을 클릭하면 아래와 같이 개인키가 생성됩니다.

눈 버튼을 클릭해서 발급된 토큰을 확인할 수 있습니다. 한번만 공개되니 안전한 곳에 저장해놓는 것을 권장합니다.

 

4. Gitlab 연동

먼저 젠킨스 페이지에서 Dashboard -> Jenkins 관리 -> System 으로 이동한 후 아래와 같이 Connection name과 Gitlab host URL을 작성합니다. 그런다음 Add 버튼을 눌러 Credentials를 생성해 줍니다.

 

Kind를 Secret text로 선택하신 후 Secret에 처음 발급 받았던 gitlab 개인키를 복붙해주시고 credentials를 구분할 ID를 작성해 줍니다. 

 

 

마지막으로 credentials를 생성하신 후 Test Connection 버튼을 클릭하시면 아래와 같이 Success 문구가 뜬걸 확인할 수 있습니다.

 

5. Global Credentials 생성

나중에 Pipeline script 코드를 작성할 때 사용하기 위해 Dashboard -> Jenkins 관리 -> Manage Credentials -> global 순으로 이동하여 Global Credentials를 아래와 같이 생성해 줍니다.

 

Username 에 gitlab 아이디를 적어주고 Password는 gitlab에서 발급받은 개인키를 작성해 줍니다. 그 다음 Global Credentials를 식별하기 위해 ID 값을 지정해줍니다.

6. Pipeline 생성 및 webhook 연동

젠킨스와 Gitlab을 연동했으니 이제 CI-CD 파이프라인을 생성하겠습니다.

먼저 젠킨스 Dashboard 에서 새로운 아이템을 클릭하신 후 프로젝트 이름을 작성하고 Pipeline을 생성합니다.

 

 

Pipeline을 생성하셨으면 바로 다음 보이는 화면에서 Gitlab Connection을 지정해줍니다. 만약 Gitlab과 잘 연동 됐다면 아래와 같이 Connection Name이 매핑되어 있을 것입니다.

 

 

그런다음 Build Triggers에서 아래 항목을 체크해 줍니다. 

체크한 항목은 연결된 GitLab 리포지토리에 Push되거나 Merge 될 때마다 Jenkins가 자동으로 빌드를 트리거해야 함을 의미합니다.

"http://j8c209.p.ssafy.io:8080/project/{itemname}"

webhook URL은 GitLab이 Jenkins에게 새 이벤트를 알리는 데 사용할 URL입니다.

 

 

항목에 체크하신 후 아래로 내리면 보이는 고급 버튼을 클릭합니다. 그런다음 아래와 같이 Generate 버튼을 클릭하여 Secret token을 발급 받습니다. 여기서 발급 받은 Secret tokenwebhook URL 은 Gitlab에서 배포 자동화를 위해 webhook을 걸때 사용됩니다.

 

Secret token과 webhook URL을 모두 발급 받았으면 Gitlab 프로젝트에서 Settings -> webhook 페이지로 이동합니다.

그런 다음 이동한 페이지에서 아래와 같이 URL과 Secret token을 입력하고 Push events에 trigger를 걸 branch의 이름을 작성합니다. 비워두면 모든 branch에 적용됩니다.

 

 

모든 정보를 기입하고 Add webhook을 클릭합니다. 그리고 Test 버튼을 클릭하여 Push events를 발생해보면 Hook executed successfully: HTTP 200 메시지를 받을 수 있습니다.

 

 

7. Pipeline 스크립트 코드 작성하기

webhook 연동까지 하셨으면 처음에 Pipeline을 생성하고 webhook URL와 Secret token을 발급 받았던 화면에서 Pipeline script 코드를 작성해 줍니다. 아래 파이프라인 코드를 보면서 주석을 통해 의미를 파악해 보겠습니다.

pipeline {
    agent any
    
    tools {
        gradle 'Gradle7.6'
    }
    
    // 빌드 커맨드와 마이크로 서비스 선언
    environment {
        BUILD_COMMAND = ' ./gradlew clean build -x test'
        CHALLENGE_PROJECT='challenge-service'
        USER_PROJECT='user-service'
        PAY_PROJECT='pay-service'
        EUREKA_PROJECT='eureka-service'
        GATEWAY_PROJECT='api-gateway-service'
    }
    
    stages {
        // 깃랩 프로젝트 코드를 클론해오는 코드
        stage('github clone') {
            steps {
                git branch: 'develop',
                // Global Credentials ID
                credentialsId: 'deukToken',
                url: 'https://lab.ssafy.com/s08-blockchain-contract-sub2/S08P22C209'
            }
        }
		
        // 프로젝트 빌드
        stage('Build') {
            // parallel - 프로젝트 병렬 처리
            parallel {
                stage('build-challenge-service'){
                    // 해당 프로젝트에 changeset이 발생했을때 동작
                    when {
                        changeset "backend/challenge-service/**"
                    }
                    steps{
                        dir('backend/challenge-service') {
                        	// environment 에서 선언한 빌드 커맨드
                            sh "$BUILD_COMMAND"
                        }
                    }
                }
                stage('build-user-service'){
                    when {
                        changeset "backend/user-service/**"
                    }
                    steps{
                        dir('backend/user-service') {
                            sh "$BUILD_COMMAND"
                        }
                    }
                }
                stage('build-pay-service'){
                    when {
                        changeset "backend/pay-service/**"
                    }
                    steps{
                        dir('backend/pay-service') {
                            sh "$BUILD_COMMAND"
                        }
                    }
                }
                stage('build-eureka-service'){
                    when {
                        changeset "backend/eureka-service/**"
                    }
                    steps{
                        dir('backend/eureka-service') {
                            sh "$BUILD_COMMAND"
                        }
                    }
                }
                stage('build-api-gateway-service'){
                    when {
                        changeset "backend/apigateway-service/**"
                    }
                    steps{
                        dir('backend/apigateway-service') {
                            sh "$BUILD_COMMAND"
                        }
                    }
                }

            }
        }
        
        // 빌드된 도커 이미지를 도커 허브에 push 하기 위한 작업
        stage('Backup & Copy'){
            // 병렬 처리
            parallel{
                stage('backup-copy-challenge-service'){
                    // 해당 프로젝트에 changeset이 발생했을 떄 동작
                    when{
                        changeset "backend/challenge-service/**"
                    }
                    steps{
                        dir('backend/challenge-service') {
                            sh 'docker build -t ssafyc209/${CHALLENGE_PROJECT} .'
                            sh 'docker push ssafyc209/${CHALLENGE_PROJECT}'
                        }
                    }
                }
                stage('backup-copy-user-service'){
                    when{
                        changeset "backend/user-service/**"
                    }
                    steps{
                        dir('backend/user-service') {
                            sh 'docker build -t ssafyc209/${USER_PROJECT} .'
                            sh 'docker push ssafyc209/${USER_PROJECT}'
                        }
                    }
                }
                stage('backup-copy-pay-service'){
                    when{
                        changeset "backend/pay-service/**"
                    }
                    steps{
                        dir('backend/pay-service') {
                            sh 'docker build -t ssafyc209/${PAY_PROJECT} .'
                            sh 'docker push ssafyc209/${PAY_PROJECT}'
                        }
                    }
                }
                stage('backup-copy-eureka-service'){
                    when{
                        changeset "backend/eureka-service/**"
                    }
                    steps{
                        dir('backend/eureka-service') {
                            sh 'docker build -t ssafyc209/${EUREKA_PROJECT} .'
                            sh 'docker push ssafyc209/${EUREKA_PROJECT}'
                        }
                    }
                }
                stage('backup-copy-api-gateway-service'){
                    when{
                        changeset "backend/apigateway-service/**"
                    }
                    steps{
                        dir('backend/apigateway-service') {
                            sh 'docker build -t ssafyc209/${GATEWAY_PROJECT} .'
                            sh 'docker push ssafyc209/${GATEWAY_PROJECT}'
                        }
                    }
                }
            }
        }

        // 배포 명령어
        stage('Deploy'){
            // 병렬 처리
            parallel{
                stage('deploy-challenge-service'){
                    // 해당 프로젝트에서 changeset이 발생했을 떄 동작
                    when{
                        changeset "backend/challenge-service/**"
                    }
                    steps{
                        // 기존에 해당 프로젝트 이름으로 만들어진 컨테이너가 실행중이라면 정지 후 삭제
                        sh 'docker stop ${CHALLENGE_PROJECT} || true && docker rm ${CHALLENGE_PROJECT} || true'
                        // docker hub에 재 업로드된 이미지를 받아서 컨테이너 재실행
                        sh 'docker run -d --network devday-network --name ${CHALLENGE_PROJECT} -e "eureka.client.serviceUrl.defaultZone=http://eureka-service:8761/eureka/" ssafyc209/${CHALLENGE_PROJECT}'
                    }
                }
                stage('deploy-user-service'){
                    when{
                        changeset "backend/user-service/**"
                    }
                    steps{
                        sh 'docker stop ${USER_PROJECT} || true && docker rm ${USER_PROJECT} || true'
                        sh 'docker run -d --network devday-network --name ${USER_PROJECT} -e "eureka.client.serviceUrl.defaultZone=http://eureka-service:8761/eureka/" ssafyc209/${USER_PROJECT}'
                    }
                }
                stage('deploy-pay-service'){
                    when{
                        changeset "backend/pay-service/**"
                    }
                    steps{
                        sh 'docker stop ${PAY_PROJECT} || true && docker rm ${PAY_PROJECT} || true'
                        sh 'docker run -d --network devday-network --name ${PAY_PROJECT} -e "eureka.client.serviceUrl.defaultZone=http://eureka-service:8761/eureka/" ssafyc209/${PAY_PROJECT}'
                    }
                }
                stage('deploy-eureka-service'){
                    when{
                        changeset "backend/eureka-service/**"
                    }
                    steps{
                        sh 'docker stop ${EUREKA_PROJECT} || true && docker rm ${EUREKA_PROJECT} || true'
                        sh 'docker run -d -p 8761:8761 --network devday-network --name ${EUREKA_PROJECT} ssafyc209/${EUREKA_PROJECT}'
                    }
                }
                stage('deploy-api-gateway-service'){
                    when{
                        changeset "backend/apigateway-service/**"
                    }
                    steps{
                        sh 'docker stop ${GATEWAY_PROJECT} || true && docker rm ${GATEWAY_PROJECT} || true'
                        sh 'docker run -d -p 8000:8000 --network devday-network --name ${GATEWAY_PROJECT} -e "eureka.client.serviceUrl.defaultZone=http://eureka-service:8761/eureka/" ssafyc209/${GATEWAY_PROJECT}'
                    }
                }
            }
        }
    }
}

 

여기 까지 진행하신 다음 푸쉬나 머지를 하면 아래와 같이 스크립트 코드에서 선언한 step에 맞게 CI-CD 과정을 확인하실 수 있고 성공화면까지 확인하실 수 있습니다.