새소식

Spring

[Spring] Jib CI/CD 파이프라인 구축하기 (feat. Spring Rest Docs 를 돌려주세요..)

  • -

현재 프로젝트에서 Git branch 전략을 Git flow를 사용하고 있습니다(우아한형제들 - 우린 Git-flow를 사용하고 있어요)

 

기존에는 시간에 쫒기며 개발하느니라 쉘스크립트로 배포를 했었습니다.

미치는줄..

 

이 프로젝트에서는 다음 세가지 시나리오로 구분하여 CICD 배포 파이프라인을 구성했습니다.

  1. feature 브랜치 작업 후 Pull Request -> JAR 빌드 및 테스트
  2. develop 브랜치에 merge(push) -> Jib 빌드 -> 테스트서버 배포
  3. main 브랜치에 merge(push) -> Jib 빌드 -> 메인 서버 배포

 

일단 Jib 가 무엇일까요?

 

GitHub - GoogleContainerTools/jib: 🏗 Build container images for your Java applications.

🏗 Build container images for your Java applications. - GitHub - GoogleContainerTools/jib: 🏗 Build container images for your Java applications.

github.com

Jib는 구글에서 출시한 오픈소스입니다. 빌드 환경을 일일이 세팅해서 직접 도커 이미지를 직접 만들지 않고 한 방에 도커 이미지로 만들어줍니다.

 

다음 그림에서 위는 일반적인 docker 빌드 흐름이고, 아래는 오늘 진행해볼 Jib를 통한 빌드 흐름입니다.

 

출처: Google Cloud 공식문서

 

 

Scenario1. feature 브랜치 작업 후 Pull Request -> JAR 빌드 및 테스트

목적: 해당 PR의 빌드와 테스트가 정상적으로 작동함을 보장하여, develop 브랜치에 merge 시 문제 발생 요소를 줄입니다.

 

action-pr.yml

name: CI-workflow

on:
  pull_request

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Java JDK
        uses: actions/setup-java@v3.4.0
        with:
          distribution: 'adopt-hotspot'
          java-version: '11'

      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle
        run: ./gradlew build

 

이제 PR이 올라오거나, 해당 PR에 추가 커밋이 생길때마다 테스트 + 빌드 거친 결과를  ✔표시해줍니다.

 

 

Scenario2. develop 브랜치에 merge(push) -> Jib 빌드 및 테스트 서버 배포

목적: 테스트 개발 서버에서 해당 코드를 실제 운영 환경에서 동작하는지 테스트 할 수 있게 배포합니다.

 

여기에서는 위에서 설명드린 Jib를 사용하여 배포를 진행할 것이며, plug-in을 빌드 파일에 추가해주어야 합니다.

 

일단 기본적인 세팅은 다음과 같이 구성합니다.

 

build.gradle

plugins {
   .
   .
   id 'com.google.cloud.tools.jib' version '3.3.1'
}

jib {
   from {
      image = "adoptopenjdk/openjdk11"
   }
   to {
      image = "도커허브유저이름/이미지명"
      tags = ["latest"]
   }
}
.
.

기본적으로 빌드 이미지의 옵션들을 넣어주게 되는데, 일반적인 WAS에는 다양한 환경들이 필요합니다.

제가 진행하던 프로젝트에서는 빌드 조건에 다음이 있었습니다.

  • 서비스 운영을 위한 환경 변수가 담긴 파일을 전달해야 한다.
  • 빌드 파일에 Spring Rest Docs를 포함해야 한다.

 

JVM 컨테이너에 환경변수 전달하기

도커 컨테이너에 서비스 운영을 위한 환경 변수를 넘기는 방식에는 여러가지가 있지만, 기존에 사용하던 ~/.bashrc 파일을 직접 넘기는 방법을 생각해보았으나, linux 컨테이너가 아닌 jvm 컨테이너이기 때문에 불가능했습니다.

 

그래서 이것저것 찾아본 결과 .env 파일을 넣어주는 것이 여러가지 환경변수를 관리하는데에 가장 용이해 보였습니다.

 

build.gradle 파일에 다음과 활성화할 profiles를 정해주고, docker 실행 시점에 .env 파일을 넘겨주는 방식입니다.

jib {
   from {
   }
   to {
   }
   container {
      jvmFlags = ['-Dspring.profiles.active=prod', '-XX:+UseContainerSupport', '-Dserver.port=8080', '-Dfile.encoding=UTF-8']
   }
}
docker run --env-file /root/prod.env

이 옵션을 사용하면 /root/prod.env 에 선언된 환경 변수들을 jvm에 할당합니다

 

빌드 파일에 Spring Rest Docs 파일 포함하기

예상치 못하게 이 과정에서 많이 헤매었습니다.

별 다른 설정을 하지 않으면 asciidoctor 가 실행되지 않기 때문에, 테스트와 빌드를 하더라도 rest document 파일들이 생성되지 않습니다. 그래서 jib를 실행할 때 asciidoctor도 함께 실행되도록 dependsOn을 걸어줍니다.

 
tasks.named('jib') {
   dependsOn asciidoctor
}

이렇게만 하면 놀랍게도 로컬에서는 정상적으로 빌드가 성공합니다!

그러나 이상태로 바로 git action 에서 실행하게 되면, 알 수 없는 에러와 마주칩니다.

> Task :asciidoctor
2023-02-28T11:17:03.279+09:00 [main] WARN FilenoUtil : Native subprocess control requires open access to the JDK IO subsystem
Pass '--add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED' to enable.

음???

해당 에러를 마주하고는 많은 검색을 해봤지만 해당 오류에 대한 확실한 답을 얻지 못했습니다.

그래서 제가 추측한 결론은,

 

위에서 open 을 해달라고 하는 sun.nio.ch 와 java.io는 Spring Rest Docs가 의존하고 있는 패키지입니다.

여기에서 sun.nio.ch는 low layer에서의 IO 를 할 수 있게 도와주는 자바 패키지이며, Spring Rest Docs에서 HTTP 요청과 응답을 parsing 할 때, 네트워크 레이어에서의 캡처에 사용된다고 합니다.

 

그래서 로컬이 아닌 git action에서 제공하는 가상환경에서는 해당 패키지의 참조가 기본적으로 막혀있을 수 있겠다는 생각이 들었고,

--add-opens 라는 옵션을 줘서 해당 패키지의 접근을 허용해주는 옵션을 넣었습니다.

 

 

관련된 빌드 코드와 action 코드를 첨부합니다.

 

build.gradle

plugins {
   id 'com.google.cloud.tools.jib' version '3.3.1'
}

jib {
   from {
      image = "adoptopenjdk/openjdk11"
   }
   to {
      image = "도커허브유저/이미지이름"
      tags = ["latest"]
   }
   container {
      jvmFlags = ['-Dspring.profiles.active=prod', '-XX:+UseContainerSupport', '-Dserver.port=8080', '-Dfile.encoding=UTF-8']
      ports = ['8080']
      extraDirectories {
         paths {
            path {
               from = "${asciidoctor.outputDir}"
               into = "/app/resources/static/docs"
            }
         }
      }
   }
}

tasks.named('jib') {
   dependsOn asciidoctor
}

asciidoctor {
   forkOptions {
      jvmArgs('--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED')
      jvmArgs('--add-opens', 'java.base/java.io=ALL-UNNAMED')
   }
   dependsOn test
   configurations 'asciidoctorExtensions'
   inputs.dir snippetsDir
   sources {
      include("**/index.adoc")
   }
   baseDirFollowsSourceDir()
}

 

action-merge.yml

 

name: CI/CD-workflow

on:
  push:
    branches: ["develop"]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Java JDK
        uses: actions/setup-java@v3.4.0
        with:
          distribution: 'adopt-hotspot'
          java-version: '11'

      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle
        run: ./gradlew jib

 

 

이렇게 구성 후, 기분 좋은 파란불을 볼 수 있게 되었습니다.

도커 컨테이너도 잘 배포 되었네요.

 

EC2에 자동 배포(CD)

 

이제 마무리 단계로, 배포를 해주면 됩니다!

AWS 의 code deploy 를 사용해도 좋지만, 현재 네이버 클라우드 플랫폼을 사용하고 있어서 단순하게 SSH로 커맨드를 실행하는 것으로 사용하게 되었습니다. 추후에 AWS 마이그레이션을 하게 되면 code deploy를 사용해보면 좋겠네요.

 

반드시 needs: build 옵션을 넣어줘야, 빌드와 배포가 비동기로 돌아가지 않고 순차적으로 진행됩니다.

deploy:
    needs: build
    runs-on: ubuntu-latest

    steps:
      - name: Deploy FastApi MainServer(master)
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USERNAME }}
          password: ${{ secrets.SERVER_PASSWORD }}
          port: ${{ secrets.SERVER_PORT }}
          script: |
            docker stop 컨테이너명
            docker rm 컨테이너명
            docker run --pull=always -d --name 컨테이너명 -p 8080:8080 --network host --env-file /root/prod.env ${{ secrets.DOCKER_IMAGE }}

 

짠!

이제 테스트 개발 서버의 CI/CD 구축이 끝났습니다.

 

Scenario3. main 브랜치에 merge(push) -> 운영 서버 배포

목적: 테스트가 최종적으로 끝난 develop 브랜치의 기능을 운영 서버에 실제로 배포한다.

 

사실 위의 과정들을 모두 따라 하셨다면, 운영 서버 배포는 뭐가 따로 없습니다.

위의 시나리오 2번에서 브랜치만 변경해주면 되거든요!

on:
  push:
    branches: ["main"]

 

물론 서버가 분리되어 있을테니, SSH 환경 변수도 PROD_SERVER_~~ 이런식으로 새로 할당을 해야겠습니다.

 

 

이상 Jib 방식을 사용해서 빌드/테스트/운영 서버의 CI/CD 파이프라인을 구축해봤습니다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.