티스토리 뷰

Flyway 란?

Flyway is an open-source database migration tool.

Flyway 공식 홈페이지를 보면 Flyway를 데이터베이스 마이그레이션 도구 라고 소개한다.


 

데이터베이스 마이그레이션 도구란?

데이터베이스 마이그레이션 도구는 버전 관리를 통해 여러 버전의 데이터베이스 스키마와 데이터 변경을 추적할 수 있고, 롤백 기능으로 변경 사항을 쉽게 되돌릴 수 있으며, 테스트 작업을 지원하여 데이터베이스 변경 사항을 안전하게 검증하고 적용할 수 있도록 도와줍니다


 

Flyway를 사용하면서 발생한 문제점

Flyway에는 규칙이 몇가지 존재하는데

  • migration 되었던 sql 파일이 변경될 경우 체크섬 검증이 실패하며, migration 이 수행되지 않습니다.
  • sql 파일 네이밍 규칙이 존재합니다.

팀에서 Flyway를 처음 도입하고 관련 규칙을 모두 이해하고 있었지만, 여러 가지 이유로 잘못된 SQL 파일이 merge되서 마이그레이션에 실패하는 경우가 자주 발생했습니다.

 

문제는 마이그레이션 실패 시 'repair' 명령을 사용하여 복구하고 'success' 상태를 변경하는 작업이 필요한데, 해당 작업을 수행하기 전까지 서버가 다운타임이 발생하면서 많은 불편함이 있었습니다.

 

그래서 pr이 merge 되기 전에 migration 검증의 필요성을 느꼈습니다.


 

Flyway  Validate

name: DB CI with Flyway

on:
  workflow_dispatch:
  pull_request:
    branches: ["backend"]

permissions:
  contents: read

jobs:
  build:
    runs-on: development
    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'corretto'

    - name: Create flyway.conf
      run: |
        touch flyway.conf
        echo "flyway.url=${{ secrets.DEV_DATASOURCE_URL }}" >> flyway.conf
        echo "flyway.user=${{ secrets.DEV_DATASOURCE_USERNAME }}" >> flyway.conf
        echo "flyway.password=${{ secrets.DEV_DATASOURCE_PASSWORD }}" >> flyway.conf
        echo "flyway.encoding=UTF-8" >> flyway.conf
        echo "flyway.outOfOrder=true" >> flyway.conf
        echo "flyway.locations=filesystem:src/main/resources/db/migration" >> flyway.conf
        echo "flyway.validateOnMigrate=true" >> flyway.conf
        echo "flyway.baselineVersion=0" >> flyway.conf
      working-directory: ./backend

    - name: flywayValidate
      run: ./gradlew -Dflyway.configFiles=flyway.conf flywayValidate --stacktrace
      working-directory: ./backend
Execution failed for task ':flywayValidate'.
14> Error occurred while executing flywayValidate
15 Validate failed: Migrations have failed validation
16 Detected resolved migration not applied to database: 5.
17 To fix this error, either run migrate, or set -ignoreMigrationPatterns='*:pending'.
18 Need more flexibility with validation rules? Learn more: https://rd.gt/3AbJUZE
19

그래서 처음 github action을 활용해서 Validate 스크립트를 작성했지만 

위와 같은 예외 메시지와 함께 flywayValidate은 작동하지 않았습니다.

왜냐하면 FlywayValidate는 "적용된 마이그레이션을 검증" 하기때문에

pending상태(새로운 SQL)가 존재할경우 에러를 반환합니다.


 

Flyway  Dry Runs

조금더 찾아보니 Dry Runs을 활용해서 미리 변경사항을 검증 가능하지만

해당 기능은 유료 플랜이기때문에 다른 방법이 필요했다.


 

Mysql Dump 후 실제  Mysql에 마이그레이션

        
- name: MYSQL 컨테이너 실행
  env:
    DOCKER_HEALTHCHECK_OPTIONS: --health-cmd='mysql -uroot -proot -e "show databases;"' --health-interval=5s --health-timeout=5s --health-retries=3
    # 도커 컨테이너 실행 후 MySQL 서버가 실행되어 접속 가능한지까지를 확인한다.
    # 접속까지 가능해야 healthy 상태이다.
  timeout-minutes: 5
    # 5분 안에 접속 가능하지 않으면 이 단계를 타임아웃 처리한다.
  run: |
    sudo docker run --rm --name $MYSQL_CONTAINER_NAME -e MYSQL_ROOT_PASSWORD=root -P \
    ${{ env.DOCKER_HEALTHCHECK_OPTIONS }} \
    -d mysql:8.0

    while [ "`sudo docker inspect -f {{.State.Health.Status}} $MYSQL_CONTAINER_NAME`" != "healthy" ]
    do
        echo "status: " `sudo docker inspect -f {{.State.Health.Status}} $MYSQL_CONTAINER_NAME`
        sleep 5
    done

- name: 개발 DB 덤프
     ...
- name: 덤프를 MySQL 컨테이너로 임포트
     ...
- name: flyway.conf 생성
     ...
- name: flywayMigrate 실행
     ...

- name: MYSQL 컨테이너 제거
  if: always()
  shell: bash {0}
  run: | 
    sudo docker rm -f $MYSQL_CONTAINER_NAME

마지막으로 고려한 방법은 운영 중인 MySQL 데이터베이스에서 일부 데이터를 복제하여 테스트 데이터베이스에서 마이그레이션을 실행하는 방식입니다.

그러나 이 방식 역시 얼마 되지 않아 장애가 발생하는 문제가 있었습니다.

 


 

No space left on device

몇일뒤 장치에 남은 공간이 없다는 에러로 Actions가 실패하는 문제가 생겼습니다.

실제로 서버에서는 디스크 용량을 100% 사용중이였습니다.

원인을 확인하니 Docker Volume이 제거되지 않는 문제였습니다.

run --rm 옵션을 사용하면 컨테이너가 제거될 때 컨테이너와 연결된 익명 볼륨도 제거된다고 나와있는데 왜 제거가 되지않을까?

이유는 Mysql 컨테이너를 실행할때 생기는 볼륨이 익명 볼륨이 아니기 때문입니다.

그렇기 때문에 컨테이너를 종료할때 연결된 볼륨또한 모두 지워주는 옵션이 필요했습니다.


 

Mysql Dump 후 실제  Mysql에 마이그레이션  최종 스크립트

name: DB CI with Flyway

on:
  workflow_dispatch:
  pull_request:
    branches: ["backend"]

permissions:
  contents: read

jobs:
  validate:
    runs-on: development
    defaults:
      run:
        working-directory: ./backend
    env:
      MYSQL_CONTAINER_NAME: temp-mysql
      SOURCE_DB_HOSTNAME: ip-192-168-2-43.ap-northeast-2.compute.internal
      SOURCE_DB_PORT: 3306
      SOURCE_DB_SCHEMA: digginroom_dev
    steps:
    - uses: actions/checkout@v3

    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'corretto'
        
    - name: MYSQL 컨테이너 실행
      env:
        DOCKER_HEALTHCHECK_OPTIONS: --health-cmd='mysql -uroot -proot -e "show databases;"' --health-interval=5s --health-timeout=5s --health-retries=3
        # 도커 컨테이너 실행 후 MySQL 서버가 실행되어 접속 가능한지까지를 확인한다.
        # 접속까지 가능해야 healthy 상태이다.
      timeout-minutes: 5
      # 5분 안에 접속 가능하지 않으면 이 단계를 타임아웃 처리한다.
      run: |
        sudo docker run --name $MYSQL_CONTAINER_NAME -e MYSQL_ROOT_PASSWORD=root -P \
        ${{ env.DOCKER_HEALTHCHECK_OPTIONS }} \
        -d mysql:8.0
        
        while [ "`sudo docker inspect -f {{.State.Health.Status}} $MYSQL_CONTAINER_NAME`" != "healthy" ]
        do
            echo "status: " `sudo docker inspect -f {{.State.Health.Status}} $MYSQL_CONTAINER_NAME`
            sleep 5
        done

    - name: 개발 DB 덤프
      env:
        DUMP_OPTIONS: --single-transaction --no-tablespaces
        # 테이블 락킹 권한과 테이블 스페이스 열람 권한 (PROCESS 권한) 없이 덤프할 수 있다.
      run: |
        sudo docker exec $MYSQL_CONTAINER_NAME sh -c ' \
          mysqldump \
          -u ${{ secrets.DEV_DATASOURCE_USERNAME }} -p${{ secrets.DEV_DATASOURCE_PASSWORD }} \
          -h ${{ env.SOURCE_DB_HOSTNAME }} -P ${{ env.SOURCE_DB_PORT }} \
          -B ${{ env.SOURCE_DB_SCHEMA }} \
          ${{ env.DUMP_OPTIONS }} \
          --where="1 limit 100" \
        ' > dump.sql
        
    - name: 덤프를 MySQL 컨테이너로 임포트
      run: |
        sudo docker exec -i $MYSQL_CONTAINER_NAME sh -c ' \
          exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" \
        ' < dump.sql

    - name: flyway.conf 생성
      run: |
        DOCKER_PORT=`sudo docker inspect --format="{{(index (index .NetworkSettings.Ports \"3306/tcp\") 0).HostPort}}" $MYSQL_CONTAINER_NAME`;
        echo "MySQL Container Port: $DOCKER_PORT";

        touch flyway.conf
        echo "flyway.url=jdbc:mysql://127.0.0.1:$DOCKER_PORT/$SOURCE_DB_SCHEMA" >> flyway.conf
        echo "flyway.user=root" >> flyway.conf
        echo "flyway.password=root" >> flyway.conf
        echo "flyway.encoding=UTF-8" >> flyway.conf
        echo "flyway.outOfOrder=true" >> flyway.conf
        echo "flyway.locations=filesystem:src/main/resources/db/migration" >> flyway.conf

    - name: flywayMigrate 실행
      run: ./gradlew -Dflyway.configFiles=flyway.conf flywayMigrate --stacktrace >> $GITHUB_STEP_SUMMARY

    - name: MYSQL 컨테이너 제거
      if: always()
      shell: bash {0}
      run: | 
        sudo docker rm -v -f $MYSQL_CONTAINER_NAME

마무리 

실제 운영 중인 Mysql에서 일부 데이터를 복제한 test db에서 마이그레이션을 실행하는 방식으로

flyway로 인한 장애 없이 안정적으로 서비스를 진행할 수 있게 되었습니다.