6 분 소요

GCP 프로젝트 생성부터 jenkins 설치까지는 이전 글에서 다뤘다.

우선, 이전에 만든 jenkins 인스턴스에서 [유사하게 만들기]로 web 인스턴스를 생성하자.
생성 후에는 외부 고정 IP도 할당하자.
(기존에 있던 instance-1은 ubuntu 였으므로 일단 중지해놓자.)

스크린샷 2022-10-13 오후 9 31 17
현재 실행 중인 인스턴스는 아래와 같다.

  • jenkins: 젠킨스 서버, Centos7
  • web: 배포 서버, Centos7

젠킨스 서버에서는 Docker를 사용하지 않고,
배포 서버에서는 Docker를 사용할 것이다.


0. 시나리오

이 글을 참고했다.

스크린샷 2022-10-13 오후 9 32 22

  1. 코드를 작성한다.
  2. 깃헙에 Push한다.
  3. Webhook이 걸려 젠킨스에게 Job을 실행하라고 한다.
  4. 젠킨스가 Build해준 결과를 배포 서버에 넘긴다.
  5. 배포 서버에서 컨테이너를 실행해 자동 배포한다.


이번 글에서는 젠킨스 인스턴스(jenkins)에서 진행하고,
다음 글에서는 배포 인스턴스(web)에서 진행할 것이다.

1. 메타데이터 ssh 접근 설정

키 생성과 등록은 이전 글에서 이미 진행했다.

2. 젠킨스 서버 구동 확인

젠킨스 인스턴스에 SSH 접속한 뒤에 아래 커맨드를 실행한다.

# 젠킨스 상태 확인
$ sudo service jenkins status

# 만약 정지되어 있으면 아래 커맨드로 실행
$ sudo service jenkins start systemctl enable jenkins

http://[젠킨스 서버 외부IP]:9090으로 웹 브라우저에 접속한다.

3. Global Tool Configuration

이 글을 참고했다.

[Dashboard] > [Jenkins 관리] > [Global Tool Configuration]

만약 이런 에러가 발생한다면? 스크린샷 2022-10-14 오후 4 04 37

시도 1)

$ vi ~/.bash_profile

# 아래 코드 두 줄 추가
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.16.1.1-1.el7_9.x86_64
export PATH=$JAVA_HOME/bin:$PATH

# 변경사항 적용
$ source ~/.bash_profile

스크린샷 2022-10-14 오후 4 03 30

하지만 문제가 해결되지 않아 다시 원상복구 했다.

시도2)
java 다시 설치

# 설치할 수 있는 버전 확인
$ yum list java*jdk-devel

# 설치
$ sudo yum install java-11-openjdk-devel.x86_64

# 기본 자바 선택
$ sudo update-alternatives --config java

스크린샷 2022-10-14 오후 4 29 30

아래 코드로 java가 설치된 디렉토리를 확인해보자.

$ cd /usr/lib/jvm

스크린샷 2022-10-14 오후 4 39 08

스크린샷 2022-10-14 오후 4 35 49

해결되었다!

이제 바로 아래 있는 Git 설정도 해주자.
스크린샷 2022-10-14 오후 4 36 36

이제 Save 버튼을 누른다.

4. Publish Over SSH

이 글을 참고했다.

1) 설치

스크린샷 2022-10-13 오후 9 38 50

스크린샷 2022-10-13 오후 9 45 30

위 플러그인을 설치한다.

2) 공개키/개인키 확인

설정하기 전에, 공개키와 개인키를 확인해보자.

# 공개키 확인
$ cat .ssh/id_rsa.pub

# 개인키 확인
$ cat .ssh/id_rsa # 복사해두자!

이때 확인한 개인키를 복사해두자!

3) 설정

이제 Publish Over SSH 설정을 해보자.
스크린샷 2022-10-13 오후 10 15 58

[Jenkins 관리 > 시스템 설정]에 들어간다.
맨 아래 보면 Publish Over SSH 관련 설정이 있다.
아래와 같이 설정해주었다.
스크린샷 2022-10-13 오후 10 27 13

스크린샷 2022-10-13 오후 10 26 35

각각의 입력은 아래의 과정을 거치면 된다.

  1. 아래의 그림에서 Name은 가급적이면 GCP에서 인스턴스의 이름과 동일하게 지어주는것이 좋다. (나는 배포 인스턴스의 이름을 적었다.)
  2. Hostname은 IP를 적어주면 되는데 워커 인스턴스가 젠킨스와 같은 네트워크상에 있으면 내부IP를 사용해도 된다. (나는 내부 IP를 사용했다.)
    (같은 Region에 내부 IP가 비슷하다면 같은 네트워크)
  3. Username은 인스턴스와 연결된 Username에 대한 정보이다. GCP에서 로그인할때 사용하는 계정 ID를 적어주면 된다.
  4. Remote Directory는 홈 디렉토리를 사용했다. 홈 디렉토리 정보는 홈 경로(~)에서 스크립트 명령어 ‘pwd’를 입력하면 확인할 수 있다.
  5. Test Configuration을 클릭하여 정상적으로 설정이 되었는지 확인할 수 있다. 잘 되었다면 왼쪽 하단에 ‘Success’ 라는 출력을 확인할 수 있다.
  6. 모든 입력이 끝난 후에는 저장을 해주면 된다

5. Webhooks

스크린샷 2022-10-14 오후 2 35 35

6. Personal access tokens

스크린샷 2022-10-14 오후 2 39 39

스크린샷 2022-10-14 오후 2 40 00
🚨 생성된 secret은 다시는 확인할 수 없으니 복사 후 잘 보관해두자!!????

젠킨스에서 등록 (Dashboard>Jenkins 관리>Configure System)
스크린샷 2022-10-14 오후 2 45 02

이때, Credentials 는 처음에는 없을거고, Add를 눌러 아래와 같이 생성한다.
Secret에는 방금 생성한 webhook secret을 입력한다. 스크린샷 2022-10-14 오후 2 46 06

7. Jenkins Job 생성 (새로운 Item 생성)

CI / CD를 위한 잡을 만들어보자. 젠킨스 파일(선언적 파이프라인)로 만들어볼 것이다.
(나는 job-1이라는 이름으로 생성했다.)
스크린샷 2022-10-14 오후 2 54 37

깃헙에 PUSH 되면 자동으로 젠킨스가 일을 해야하므로 Hook만 걸어놓자.
스크린샷 2022-10-14 오후 2 54 50

스크린샷 2022-10-14 오후 2 54 57

스크립트는 아래와 같다. (일단 뼈대만 만들어놓자.)

pipeline {
    agent any

    stages {
        stage('Prepare') {
            steps {
                echo '=== Prepare ==='
            }
        }
        
        stage('Build') {
            steps {
                echo '=== Build ==='
            }
        }
        
        stage('Deploy') {
            steps {
                echo '=== Deploy ==='
            }
        }
    }
}

1) git clone

이 글을 참고했다.

이제 git clone을 진행하자.
Pipeline Syntax를 클릭하자.
스크린샷 2022-10-14 오후 3 07 16

Sample Step에서 git: Git을 선택하자.
스크린샷 2022-10-14 오후 3 17 26

🚨 주의!
레포지토리 주소 붙여넣을 시 .git 주소가 아닌, 주소창에서 레포지토리 URL 주소를 붙여넣는다.

Credentials는 깃허브 계정과 엑세스 토큰으로 새로 생성해 사용했다.
이 때, 엑세스 토큰은 직전에 생성한게 아니라 원래 사용하던 엑세스 토큰을 사용했다.
Kind는 Username with password로 사용한다 .
Username은 Github 계정명 Password는 Access Token 값을 사용한다.

Credentials까지 모두 추가되었다면 Generate Pipeline Script를 이용해서 생성된 Script를 복사해 Prepare에 붙여넣을 것이다.

2) Pipeline Script 작성

Prepare에는 방금 복사한 스크립트를 붙여넣을 것이다.
Build 부분에는, 빌드하기 위해 gradlew를 쓸 것이기 때문에 쉘 명령어 2줄만 추가해주자.
그럼 지금까지 작성한 파이프라인은 다음과 같다. (아직 끝나지 않았다!)

pipeline {
    agent any

    stages {
        stage('Prepare') {
            steps {
                echo '=== Prepare ==='
                git credentialsId: 'access-token', url: 'https://github.com/minju412/docker-test'
            }
        }
        
        stage('Build') {
            steps {
                echo '=== Build ==='
                sh '''
                    chmod +x gradlew
                    ./gradlew clean build -x test
                '''
            }
        }
        
        stage('Deploy') {
            steps {
                echo '=== Deploy ==='
            }
        }
    }
}

이번에는 Deploy를 채울 것이다.
job-1에서 Pipeline Syntax를 클릭하자.

Sample Step에서 sshPublisher: Send build artifacts over SSH를 선택하자.

스크린샷 2022-10-14 오후 2 16 47

스크린샷 2022-10-14 오후 3 37 44

  1. 소스파일의 위치, 빌드한 결과는 build/libs/—.jar에 위치한다.
  2. 소스파일에서 원본 파일의 디렉토리를 어디까지 포함할 것인지에 대한 설정 (build/libs는 제거된다.)
  3. remote directory 기준으로 배포될 경로를 적는다. (결과 : /home/swanjj11/deploy)
  4. 전송을 마치고 실행할 Shell 명령어를 적는다. 이따 ‘init.sh’이라는 파일을 실행할 것이다.

Generate Pipeline Script를 클릭한 뒤, 생성된 Snipet을 ‘Deploy’에 복붙하자.
최종 완성된 파이프라인 스크립트는 아래와 같다.
Dashboard > job-1 > 구성에서 Pipeline Script를 작성하는 부분에 아래 코드를 붙여넣자.

pipeline {
    agent any

    stages {
        stage('Prepare') {
            steps {
                echo '=== Prepare ==='
                git credentialsId: 'access-token', url: 'https://github.com/minju412/docker-test'
            }
        }
        
        stage('Build') {
            steps {
                echo '=== Build ==='
                sh '''
                    chmod +x gradlew
                    ./gradlew clean build
                '''
            }
        }
        
        stage('Deploy') {
            steps {
                echo '=== Deploy ==='
                sshPublisher(publishers: [sshPublisherDesc(configName: 'web', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: 'sh init.sh', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: 'deploy/', remoteDirectorySDF: false, removePrefix: 'build/libs', sourceFiles: 'build/libs/*.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)
                ])
            } #마지막에 verbose를 true로 주면 트러블 슈팅하기 용이하다
        }
    }
}

이제 젠킨스에서 할 일은 끝났다.

다음 글에서 진행할 내용은 아래와 같다.

  • 배포 서버에 도커파일 생성
  • 배포 서버에 init.sh 작성
  • 깃헙에 push 해보기
  • 자동 배포 됐는지 확인

========================

8. 민감 정보 추가하기

이 글을 참고했다.

1) 프로젝트 application.yml 변경

민감 정보를 제거한 뒤에 깃허브에 업로드 하자.
(민감 정보는 따로 기록해두었다가 이후에 젠킨스에서 string parameter로 등록할 것이다.)

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${MYSQL_IP}:${MYSQL_PORT}/${MYSQL_NAME}?${MYSQL_OPT}
    username: ${MYSQL_USERNAME}
    password: ${MYSQL_PASSWORD}

  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        globally_quoted_identifiers: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect

  cache:
    type: redis
  redis:
    host: ${REDIS_IP}
    port: ${REDIS_PORT}

  devtools:
    livereload:
      enabled: false

  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
#    log-resolved-exception: false

  # ==== 페이징 할 때 기본값, 20개씩 조회 ==== #
  data:
    web:
      pageable:
        default-page-size: 20

  # ==== S3 파일 업로드 용량 설정 ==== #
  servlet:
    multipart:
      max-file-size: 3MB #업로드 파일 크기 제한
      max-request-size: 5MB #업로드 파일 크기 총량 제한

# ==== JWT ==== #
jwt:
  token:
    jwt-secret-key: ${JWT_SECRET}

# === AWS S3 === #
cloud:
  aws:
    credentials:
      access-key: ${S3_ACCESS_KEY}
      secret-key: ${S3_SECRET_KEY}
    s3:
      bucket: posts-image-bucket
    region:
      static: ap-northeast-2
    stack:
      auto: false

# === Firebase === #
fcm:
  api-url: ${FIREBASE_URL}

logging.level:
  org.hibernate.SQL: debug

2) 젠킨스에서 String Parameter 선언

이 글을 참고했다.

스크린샷 2022-10-25 오전 12 54 59

중요🌟
만약에 String Parameter의 value에 / 기호가 포함되어 있다면,
/ 기호 앞에는 escape 문자(\)를 넣어주어야 한다!

만약 abc/def 라는 값이면 abc\/def가 되는 것이고,
만약 abc//def라면 abc\/\/def가 되는 것이다.

3) sed 스크립트 추가

Build 이전에 넣어준다.
$ 문자 앞에는 \을 넣어주어야 한다.

pipeline {
    agent any

    stages {
        stage('Prepare') {
            steps {
                echo '=== Prepare ==='
                git credentialsId: 'access-token', url: 'https://github.com/minju412/docker-test'
            }
        }

        stage('Set String Parameters') {
            steps {
                echo '=== Set String Parameter ==='

                echo '01. REDIS :: IP, Port Setting'
                sh '''
                    sed -i "s/\${REDIS_IP}/\${REDIS_IP}/" "\${WORKSPACE}/src/main/resources/application.yml"
                    sed -i "s/\${REDIS_PORT}/\${REDIS_PORT}/" "\${WORKSPACE}/src/main/resources/application.yml"
                '''

                echo '02. Database :: UserName, Password Setting'
                sh '''
                    sed -i "s/\${MYSQL_USERNAME}/\${MYSQL_USERNAME}/" "\${WORKSPACE}/src/main/resources/application.yml"
                    sed -i "s/\${MYSQL_PASSWORD}/\${MYSQL_PASSWORD}/" "\${WORKSPACE}/src/main/resources/application.yml"
                '''

                echo '03. Database :: IP, Port, Name, Opt Setting'
                sh '''
                    sed -i "s/\${MYSQL_IP}/\${MYSQL_IP}/" "\${WORKSPACE}/src/main/resources/application.yml"
                    sed -i "s/\${MYSQL_PORT}/\${MYSQL_PORT}/" "\${WORKSPACE}/src/main/resources/application.yml"
                    sed -i "s/\${MYSQL_NAME}/\${MYSQL_NAME}/" "\${WORKSPACE}/src/main/resources/application.yml"
                    sed -i "s/\${MYSQL_OPT}/\${MYSQL_OPT}/" "\${WORKSPACE}/src/main/resources/application.yml"
                '''

                echo '04. Jwt :: Secret Setting'
                sh '''
                    sed -i "s/\${JWT_SECRET}/\${JWT_SECRET}/" "\${WORKSPACE}/src/main/resources/application.yml"
                '''

                echo '05. AWS S3 :: Secret Setting'
                sh '''
                    sed -i "s/\${S3_ACCESS_KEY}/\${S3_ACCESS_KEY}/" "\${WORKSPACE}/src/main/resources/application.yml"
                    sed -i "s/\${S3_SECRET_KEY}/\${S3_SECRET_KEY}/" "\${WORKSPACE}/src/main/resources/application.yml"
                '''

                echo '06. Firebase :: Url Setting'
                sh '''
                    sed -i "s/\${FIREBASE_URL}/\${FIREBASE_URL}/" "\${WORKSPACE}/src/main/resources/application.yml"
                '''
            }
        }
        
        stage('Build') {
            steps {
                echo '=== Build ==='
                sh '''
                    chmod +x gradlew
                    ./gradlew clean build --exclude-task test
                    ls -al ./build
                '''
            }
        }
        
        stage('Deploy') {
            steps {
                echo '=== Deploy ==='
                sshPublisher(publishers: [sshPublisherDesc(configName: 'web', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: 'sh init.sh', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: 'deploy/', remoteDirectorySDF: false, removePrefix: 'build/libs', sourceFiles: 'build/libs/*.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)
                ])
            }
        }
    }
}

추가한 절차를 통해서 application.yml의 정보가 바뀐다.
Deploy에서 마지막에 verbose를 true로 주면 트러블 슈팅하기 용이하다

4) 파라미터와 함께 빌드

스크린샷 2022-10-25 오전 1 27 48

참고
이 방법 말고 조금 더 효율적으로 관리하기 위해서 Spring Cloud Config Server를 이용할 수도 있는데,
궁금하다면 이 글을 참고하자.



💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기

태그:

카테고리:

업데이트: