글 작성자: beaniejoy
목표
- DB migration 용도의 Jenkinsfile 작성해보기
- Jenkins pipeline을 통해 flyway migration 자동화 적용
- Jenkins에서 item에 버튼 하나 누르면 알아서 migration 작업 진행하도록 적용
- DB 테이블에 대한 migration 이력을 프로젝트에서 파일(.sql)로 관리 가능

해당 글을 보시기 전에 Jenkins 서버 설치가 되어있어야 합니다. Jenkins 설치 방법은 여러 블로그 글에 자세하고 친절하게 소개를 하고 계시더라구요. (아니면 ChatGPT로,,,)
AWS 클라우드 서버 내 Jenkins 서버 설치에 대해서 이전에 제가 작성했던 글도 있는데요. 허접하지만 이거 참고하셔도 좋습니다.
(https://beaniejoy.tistory.com/95)

 

[Jenkins] Lightsail에 Jenkins server 구축해보고 Spring project 빌드해보기

목적 - AWS Lightsail을 통해 생성한 instance에 jenkins server 구축한 내용 기록용 목표 - AWS Lightsail에 띄운 instance에 jenkins server 띄우기 - jenkins server 기본적인 설정 - jenkins item 생성 후 spring project build 해

beaniejoy.tistory.com

참고로 저는 개인 프로젝트를 위해 Jenkins 서버를 처음에 AWS에서 설치하고 CI/CD 프로세스까지 적용해보았는데요.
슬프게도 돈이 없는 저는 매달 3~4만원의 비용이 크게 작용해서 local에서 Virtual Box로 CentOS를 설치한 다음 거기에 Jenkins 서버를 설치했습니다 ㅜㅜ

어떠한 방식이든 좋으니 Jenkins 서버를 설치하시기만 하면 됩니다.

이번 게시글을 통해 최종적으로 적용된 프로젝트 구조는 다음과 같습니다.

dongne-cafe-api
 ├ ...
 ├ ⭐️db/				> flyway migration 관련 내용 관리
    ├ migration/		> migration 적용될 sql 파일 관리
    ├ seed/			> seed data insert를 위한 repeatable migration 관리
    ├ flyway-local.conf	> local profile용 flyway config 파일
    ├ flyway-prod.conf	> prod profile(배포)용 flyway config 파일
    └ ...
 ├ dongne-account-api/
 ├ dongne-common/
 ├ dongne-service-api/
 ├ ...
 ├ scripts/	> Jenkins 배포 관련 ansible & bash script 관리
 ├ ...
 ├ ⭐️Jenkinsfile-db-migration	> "db migration"(flyway)용 Jenkins pipeline
 ├ Jenkinsfile-service		> "service-api" build 및 deploy 용 Jenkins pipeline
 └ ...

db 디렉토리와 Jenkinsfile-db-migration이 이번 게시글에서 살펴볼 내용입니다.

 

📌 1. DB migration 작업을 애플리케이션 CI/CD pipeline과 분리한 이유가 무엇인가요?

Jenkins에 flyway runner plugin을 적용해보기 전에 바로 위에 언급된 프로젝트 구조를 한 번 살펴보겠습니다.

살펴보면 Jenkins pipeline을 두 개 적용한 것이 있는데요. DB migration(Jenkinsfile-db-migration)용도와 service-api CI/CD용도(Jenkinsfile-service-api) 두 가지의 pipeline이 있습니다.

생각해보면 두 개의 pipeline으로 구성하기보다 CI/CD 과정에 DB migration stage를 포함해서 한꺼번에 적용하는 것이 더 좋지 않겠나는 궁금증도 있을 수 있습니다.

하지만 DB migration은 애플리케이션 CI/CD와 분리되어야 하는 이유는 명확합니다. 성격이 다르고 둘다 동시에 진행하는 것은 큰 리스크가 있다고 생각했습니다. CI/CD는 애플리케이션 빌드와 배포 프로세스 자체에 의미를 두고 있고 애플리케이션 코드와 관련이 있습니다. DB table 구조와 데이터에 대한 이력을 관리하고 적용하는 DB migration과 애플리케이션 코드는 성격 자체가 서로 맞질 않다고 생각했습니다.

또한 CI/CD 안에 DB migration 과정을 포함하게 되면 예기치 않은 위험한 상황에 놓일 수 있습니다. DB migration위한 sql을 작성하는 것은 결국 개발자인데 테이블 구조를 잘못 수정하였거나 잘못된 데이터를 기입할 수 있습니다. CI/CD pipeline 실행 과정에서 DB migration 수행시 version 체크와 sql 문법 오류 같은 부분만 체크하게 되는데, 잘못 수정된 내용에 대해서는 검출을 하지 못할 수 있습니다.
이렇게 되면 개발자 입장에서 실환경 배포를 나가는데 잘못된 DB 정보에 대해서 인지하지 못하는 것이고 그대로 적용이 되어 심각한 오류 상황을 마주하게 될 수 있습니다. 

이러한 이유들로 DB migration 과정을 따로 구별된 pipeline으로 구성하였습니다.
이를 통해 DB 테이블 구조 변경이 필요할 때 Jenkins에서 해당 pipeline item으로 먼저 migration 작업을 진행하고 실환경 나가기 전에 제대로 DB 수정이 이루어졌는지에 대해서 충분히 확인 및 검토할 수 있을 것이라 생각했습니다.

 

📌 2. Jenkins에서 flyway runner plugin 설치 및 기본 설정

이제 본격적으로 Jenkins에 flyway runner plugin를 적용해보겠습니다.

먼저 flyway runner plugin을 설치해보려합니다.
Jenkins에 접속 > Jenkins 관리 > 플러그인 관리 > Available Plugins > flyway 검색

위의 과정을 통해 flyway 검색하면 flyway runner plugin이 나올 것입니다. 바로 restart 없이 install 진행 버튼을 클릭합니다.
설치 진행이 완료되면 다음과 같이 적용된 것을 확인 할 수 있습니다.

FlywayRunner 플러그인 설치 이후 Jenkins 관리 > Global Tool Configuration에 들어갑니다.

Tool 설정에 들어가서 flyway 모듈 설정을 위와 같이 하면 됩니다. Version 같은 경우 사용하고 싶은 버전을 Jenkins가 설치된 OS에 따라서 적절하게 설정하면 됩니다.

여기까지 완료되면 기본적인 flyway runner plugin 설치 및 설정은 완료입니다.

 

📌 3. Jenkins Pipeline 적용하기

 

🔖 3-1. item 생성

DB migration용 Jenkins pipline을 적용하기 위해 새로운 item 생성을 해줍시다. 저 같은 경우는 "dongne-db-migration" 이름으로 이미 생성된 pipeline item이 있기 때문에 위와 같이 빨간색 주의문구가 나오고 있습니다. 적절한 이름으로 생성하시면 됩니다. 생성하실 때 item 종류로 Pipeline으로 체크하고 생성하시면 됩니다.

 

🔖 3-2. item 기본 설정

생성한 item에 대해서 기본 설정을 진행해줍시다.
Do not allow concurrent builds: 같은 item에 대해 동시에 여러 번 build를 진행하지 못하도록 방지
오래된 빌드 삭제: 5개 정도로 해서 보관할 최대갯수를 설정해줍시다. (각자 원하는 방향으로 설정)

매개변수(parameter)로 branch 이름의 파라미터를 설정해줍니다.

pipeline script를 어디껄로 참조할 것인지에 대한 설정입니다. git을 통해 가져온 repository 내에 있는 script 파일을 참조하고자 합니다. repository의 url 설정해주고 credentials 정보를 설정해줍니다.
(여기서는 git repository 자체가 public 성격이라 따로 credentials 내용을 설정하지 않았습니다. 하지만 보통 실무에서는 private repository나 github enterprise 같은 구축형 플랫폼을 통해서 관리되기 때문에 거의 필수로 설정하고 있습니다.)

repository 설정 이후 어느 특정 branch에서 가져올지에 대해서도 설정해줍니다. 위에서 적용했던 branch parameter에서 빌드시 입력한 브랜치에서 pipeline script를 가져오겠다는 의미입니다.

Script Path는 pipeline 빌드할 script의 위치를 설정하는 것인데요. 저는 Jenkinsfile script 파일 위치가 repository root path에 있기 때문에 단순히 파일명만 입력하였습니다.

여기까지 하면 pipeline을 build 해볼 수 있는 item 기본 설정까지 완료입니다.

 

📌 4. pipeline script 작성하기

실제 pipeline을 작동시킬 script를 작성해보겠습니다.

stage를 크게 4단계로 잡았습니다.

  1. Init
    - 기본 설정 내용 및 환경 변수 내용 확인
    - DB Connection 정보 가져오기(username, password)
    - 필요한 정보에 대한 변수 설정
  2. DB Version Info
    - flyway info 명령 수행
    - target이 되는 DB에 적용된 migration version 정보를 테이블 형태로 보여줌
  3. DB Migrate
    - flyway migrate 명령 수행
    - 테이블에 대한 DDL 정보들을 담은 migration용 sql 파일을 참고하여 실제 migrate 수행
  4. DB Validate
    - flyway validate 명령 수행
    - 프로젝트에서 지정한 migration sql 내용들과 DB에 적용된 migration 정보를 비교해 불일치 여부 체크
    - migration 수행한 이후 validate를 수행함으로써 migration이 성공적으로 잘 이루어졌는지 체크하기 위함

flyway 수행을 위해서 위에서 설치했던 FlywayRunner Jenkins 플러그인을 사용하였습니다.

 

🔖 4-1. pipeline syntax를 통해 flywayrunner 플러그인 적용하기

enum Stage {
    ALL, INFO, MIGRATE, VALIDATE;
    Stage() {}
}

def executeConditionAllOr(stage) {
    return params.FLYWAY_COMMAND == Stage.ALL.name() || params.FLYWAY_COMMAND == stage.name()
}

// Run flyway runner Jenkins Plugin
def executeFlywayRunner(stage) {
    if (stage == Stage.ALL) {
        return
    }

    flywayrunner installationName: 'flywaytool-jenkins',
            flywayCommand: "$stage".toLowerCase(),
            commandLineArgs: "-configFiles=${MIGRATION_WORKSPACE}/flyway-${PROJECT_PROFILE}.conf",
            credentialsId: "${DB_CONNECTION_CREDENTIAL}",
            url: "jdbc:mysql://${DATABASE_HOST}:3306/dongne",
            locations: "filesystem:${MIGRATION_WORKSPACE}/migration"
}

pipeline {
	agent any
    
    //...
    
}

Jenkinsfile은 groovy 문법으로 이루어져있습니다. 위의 코드는 pipeline에서 stage block을 구성하기 위한 공통 내용들을 메소드와 enum class로 먼저 정의를 했습니다.

"executeFlywayRunner" function은 각 stage별로 수행하는 flyway 명령을 실질적으로 flywayRunner 플러그인이 처리해주는 부분입니다. 

  • installationName
    - 위에서 Jenkins 관리 > Global Tool Configuration에서 설정했던 flyway installation 이름을 기입합니다.
  • flywayCommand
    - flyway를 통해 수행할 명령어를 기입합니다.
    - 여기서 적용된 flyway command는 info, migrate, validate 세 가지입니다.
  • commandLineArgs
    - flyway command를 수행할 때 같이 적용할 args(명령어 옵션)내용을 기입합니다.
    - 여기서는 configFiles 옵션을 적용했습니다. flyway를 수행할 때 참고하는 설정 내용입니다.
  • credentialsId
    - Jenkins에서 DB Connection에 대한 정보를 저장한 credentials id값을 넣어줍니다.
  • url
    - 연결할 DB url 내용을 기입합니다.
  • locations
    - flyway 명령을 수행할 때 기준이되는 migration 정보들을 담고있는 디렉토리 경로를 입력합니다.
    - filesystem으로 하고 Jenkins가 돌아가고 있는 서버에서 migration 정보가 있는 디렉토리의 절대경로를 기입했습니다.

위의 내용을 무작정 따라서 타입핑할 필요 전혀없습니다. Jenkins는 pipeline syntax라는 것을 제공해주는데 위와 같은 plugin들을 pipeline script에 적용하기 쉽게 일종의 템플릿을 제공해줍니다.

Pipeline Syntax에 들어가서 flywayrunner 플러그인을 선택하고 위의 항목대로 내용을 입력해줍니다.
입력을 완료하면 Generate Pipeline Script 버튼이 있는데요. 버튼을 클릭하면 위의 script 코드 처럼 flywayrunner pipeline용 script 완성본을 보여줍니다. 복사 붙여넣기해서 넣으면 됩니다.

그리고 바로 위의 사진과 같이 Pipeline Syntax에서 flywayrunner script를 클릭하면 credentials 정보 기입하는 칸이 나오는데요. 아무것도 등록하지 않았다면 Add 버튼을 눌러 새로 등록하면 됩니다.

Add 버튼을 누르면 위와 같이 Credentials 정보 기입하는 항목들이 나오는데요. DB Connection을 위해 DB에 대한 username, password만 기입하셔도 됩니다. 

 

🔖 4-2. pipeline stage 적용하기

이제 본격적으로 4단계의 stage를 적용해보겠습니다.

pipeline {
    agent any

    // Disallow concurrent executions of the Pipeline.
    options { disableConcurrentBuilds() }

    parameters {
        // Choice of flyway target command
        choice(
                name: 'FLYWAY_COMMAND',
                choices: ["${Stage.ALL}", "${Stage.INFO}", "${Stage.MIGRATE}", "${Stage.VALIDATE}"],
                description: 'target flyway command'
        )
    }
    
    //...
    
}

우선 Jenkins에서 해당 pipeline이 동시에 build되지 않도록 options 부분에 disableConcurrentBuilds를 적용합니다.

그다음 Jenkins parameter하나를 설정했는데요. pipeline build를 시작하기 전에 특정 stage만 실행하고 싶을 때를 위해 선택지를 만들어두었습니다.

pipeline {

    //...
    stages {
        stage('Init') {
            steps {
                script {
                    sh 'whoami'
                    sh 'printenv'

                    PROJECT_PROFILE = 'prod'

                    // DB Connection Credential
                    DB_CONNECTION_CREDENTIAL = 'b873f9bf-03cc-4daf-be4f-7e00194aa2a0'
                    withCredentials([
                            usernamePassword(
                                    credentialsId: "${DB_CONNECTION_CREDENTIAL}",
                                    usernameVariable: 'username',
                                    passwordVariable: 'password'
                            )
                    ]) {
                        DATABASE_USERNAME = "${username}"
                        DATABASE_PASSWORD = "${password}"
                    }

                    // flyway migration directory
                    MIGRATION_WORKSPACE = "${WORKSPACE}/db"
                }
            }
        }

        stage('DB Version Info') {
            when {
                expression { executeConditionAllOr(Stage.INFO) }
            }
            steps { executeFlywayRunner(Stage.INFO) }
        }

        stage('DB Migrate') {
            when {
                expression { executeConditionAllOr(Stage.MIGRATE) }
            }
            steps { executeFlywayRunner(Stage.MIGRATE) }
        }

        stage('DB Validate') {
            when {
                expression { executeConditionAllOr(Stage.VALIDATE) }
            }
            steps { executeFlywayRunner(Stage.VALIDATE) }
        }
    }
}

가장 첫 단계인 Init에서 중요한 것은 Jenkins에 저장되어 있는 credentials 정보를 가져옵니다. DB Connection을 위한 username과 password를 가져오기 위함입니다. 이렇게 해야 github에 올릴 프로젝트의 pipeline script에서 DB connection 정보와 같은 민감한 데이터를 노출하지 않고 관리할 수 있습니다.

그다음 DB Info, Migrate, Validate stage를 차례대로 적용합니다. 위에서 만들어두었던 executeFlywayRunner function을 그대로 활용하면 됩니다.

when 구절은 choice parameter(ALL, INFO, MIGRATE, VALIDATE)에서 선택한 값을 기준으로 특정 stage만 실행되도록 하는 실행조건절입니다.

 

📌 5. 결론

위의 내용은 참고만 하셔도 됩니다.

중요한 것은 애플리케이션을 실제 배포하기 전에 꼭 DB migration 내용이 잘 적용되었는지 체크하는 것이 중요한데요.
작년에 실무에서 한 번 DDL 작업 요청을 깜빡하고 실환경 배포를 나간 적이 있었습니다. 실무에서는 실환경 배포 프로세스에서 DB 정보가 제대로 반영이 되었는지에 대해서 체크하지 않고 진행되기 때문에 누락된 내용을 아예 인지하지 못했습니다.
그나마 다행히 어드민쪽 배포였고 그리고 베타환경에서 이를 감지해서 뒤늦게 DDL ALTER 쿼리를 신청해서 잘 처리했습니다. 이 때를 기점으로 꼼꼼하게 보는 습관을 들였고 또한 애플리케이션 배포 전에 DB에 대한 변경지점 체크의 중요성도 알게 되었습니다.

단순 칼럼 추가와 같이 명확하게 구분이 되는 내용에 대해서는 애플리케이션 빌드와 실행시 JPA 같은 persistence 단에서 감지가 가능할 수도 있겠으나 이를 감지하지 못하고 배포가 완료가 되는 경우 사용자가 실제 서비스를 사용할 때 충분히 오류가 발생할 수도 있습니다.

이러한 개발자의 깜빡(?)이슈같은 실수를 방지하고자 제 개인 프로젝트에서는 이러한 부분을 충분히 고려했고 flyway를 사용하기에 이르렀는데요. Jenkins를 이용해 CI/CD 프로세스를 만들면서 애플리케이션 빌드 전에 DB migration 체크 과정을 넣었고 DB migration 전용 pipeline을 따로 구성함으로써 개발자의 실수를 알아서 잡아주고 관리도 명확하게 할 수 있게끔 pipeline을 구성하게 되었습니다.
(실무에서 flyway를 사용할 수 있으면 좋겠지만 그렇지 못해 아쉬운 마음을 제 개인 프로젝트에서나마 풀었네요,,, ㅎㅎ)

이 글을 보시는 분들도 DB migration을 어떻게 하면 효율적으로 관리할 수 있는지에 대해 고민해보시면 좋을 것 같고 위의 허접한 내용보다 훨씬 더 좋은 방법이 분명 있을거라 생각합니다. 있으면 저한테도 공유해주시면 감사하겠습니다 ㅎㅎ

위에 적용한 script 내용이 있는 github repository 링크 남겨놓겠습니다.

 

Flyway migrate, validate 용도의 Jenkinsfile 구분 by beaniejoy · Pull Request #52 · beaniejoy/dongne-cafe-api

 

github.com

틀린 내용에 대한 피드백 언제나 환영합니다. 긴 글 읽어주셔서 감사합니다.