본문 바로가기

spring

Spring boot 도커 hub에 올리고 로컬 DB(MySQL)와 연동하기(feat. github actions)

Spring Boot(2.7.11)와 MySQL(8.0.32) 기반으로 애플리케이션을 만들기로 했다. 이 때 github actions를 활용해 CI/CD(CD까진 아니고 deploy를 하기 위한 환경을 준비했다고 해두자)

이 때 다음과 같은 요구사항을 만족하고자 했다.

전제 조건: github에 올린 소스를 clone 받아서 로컬 MySQL 서버와 연동하는 것이 아니라 docker hub에 업로드 한 app image를 pull 받아서 로컬에 이미 설치해 둔 MySQL과 연동한다. 이는 배포한 docker image를 다른 개발자들이 pull 받아서 로컬 MySQL 서버와 연동해 테스트하기 위함이다.

1. spring app은 docker image로 만들어 docker hub에 push 한다.

2. 이미 만들어 둔 docker-compose 파일을 활용해 app을 실행시킨다.

 

기본 환경 구성

JDK 설치 및 설정, Spring application 생성, Docker desktop 설치 및 Docker 계정 생성

 

Github Actions - Workflow 구성

.github/workflows/ 폴더 하위에 실행하고자 하는 yml 파일을 생성한다.

name: deploy

on:
  workflow_dispatch:
  push:
    branches: [ "devleop" ]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

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

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

      # 도커허브 로그인
      - name: Login to DockerHub
        uses: docker/login-action@v2
        with:
          username: ${{secrets.DOCKER_HUB_USERNAME}}
          password: ${{secrets.DOCKER_HUB_TOKEN}}

      # 도커 이미지 생성 및 도커허브 업로드
      - name: Create docker image and Upload to DockerHub
        env:
          NAME: {DockerHub 계정명}
          REPO: {DockerHub 레포 이름}
          IMAGE: {Docker 이미지 이름}
        run: |
          docker build -f ./dockerfile -t $IMAGE .
          docker tag $IMAGE:latest $NAME/$REPO:latest
          docker push $NAME/$REPO:latest

- name: deploy 라는 이름의 workflow 파일을 생성한다.

- on: workflow_dispatch로 수동으로 실행 가능하게 해두고, 'develop' branch의 push 이벤트가 발생했을 때만 동작하도록 한다.

- jobs: 실행할 job을 나열한다.

  - deploy: 실행할 job의 이름

  - runs-on: actions가 실행될 github 서버 환경의 운영체제

- steps: 위 job이 실행될 순서들을 나열한다.

- jdk를 세팅하고, build 한다. 이 과정이 끝나면 github actions 서버에 build 파일이 만들어진다.

- DockerHub 로그인: CLI 기반으로 DockerHub API에 로그인을 요청한다. docker에서 지원하는 login-action으로 username과 password를 전달하여 로그인한다. 개인정보이므로 github-actions secrets에 해당 정보들을 저장해둔다. 저장 방법은 'github actions secrets'로 검색하면 많은 글이 나오니 참고한다.

- Docker image 생성 및 DockerHub에 업로드: 프로젝트에 만들 dockerfile에 따라서 docker image를 build하고, tag를 세팅한 뒤에 DockerHub에 실제 push 한다.

 

Dockerfile 생성

github-actions workflows는 모두 작성하였지만 실제 동작하지는 않을 것이다. 명시한 dockerfile을 아직 생성하지 않았기 때문이다. 이제 repository root 경로에 Dockerfile을 생성한다.

FROM adoptopenjdk:11-jre-hotspot
COPY build/libs/healthCheck-0.0.1-SNAPSHOT.jar /app/healthcheck.jar
EXPOSE 8080
CMD ["java", "-jar", "/app/healthcheck.jar"]

 - 보통 다른 글들에서 COPY 할 때 build/libs/*.jar 로 많이 소개되어 있을텐데, 필자의 경우 COPY할 때 'When using COPY with more than one source file, the destination must be a directory and end with a /'오류가 발생했다.

- 이는 app을 build 하면 /build/libs 폴더 하위에 'build/libs/*.jar'에 부합하는 2개 이상의 파일이 찾아졌기 때문이다. 실제 copy할 때 2개 이상의 파일은 파일명이 아니라 directory 하위에 복사가 되어야 하는데, 파일명을 1개로 명시해버렸기 때문에 오류가 난 것이다.

- 원인은 build.gradle plugins에 있다. start.spring.io에서 스프링부트 기반 프로젝트를 생성했다면 plugins에 'java'와 'org.springframework.boot'가 있을 것이다. 두 플러그인이 다음의 2개 build 파일을 생성한다. '*.0.0.1.SNAPSHOT.jar' 파일과 '*.0.0.1.SNAPSHOT-plain.jar' 파일 2개가 생성되는데, plain 파일은 실제 실행가능한 파일이 아니고 의존성 분석 등의 작업을 위해 사용된다.

- 이렇게만 해두고 'docker build -f dockerfile -t myApp .' 명령으로 실제 build를 해보면 성공한다. docker image 생성까지는 성공한 것이다.

 

소스 수정하고 develop branch에 push 하기

소스를 수정해서 develop branch에 push 해보자. push event에 github-actions workflow를 설정해두었으므로 자동으로 실행이 완료될 것이다. 여기까지 왔으면 앱의 docker image 파일을 만들어서 github-actions를 통해 해당 image 파일이 미리 생성해둔 DockerHub 계정의 repository에 업로드가 되었을 것이다.

사실 여기서 build에 실패할 수 있는데, 이는 DB 의존성이 있을 경우에 test가 통과되지 못해서다. /src/test/~ 경로에 기본적으로 생성되어 있는 테스트 파일이 하나 있을 것이다. @SpringbootTest 환경에서 contextLoads() 하는 테스트인데, 실제 스프링부트가 실행되는 것과 동일한 설정이 필요하다. 아예 없애버리면 좋겠지만, 그것보다는 /src/test/resources/~ 경로에 새로운 application.yml을 만들어 테스트를 위한 DB를 가져가는 것이 좋다. 필자는 인메모리 h2 database 의존성을 추가한 뒤에 설정해주었다.

이제 DockerHub에 올라가 있는 image를 pull 받아서 내 local PC에서 실행시키는 일이 남았다. 물론 수동으로 pull 받아서 실행시켜도 되지만, 이 또한 자동화하고 싶기 때문에 clone 받은 소스의 docker-compse 명령만 실행하면 이미지를 pull 받아 실행까지 되게 하고 싶다.

 

docker-compose.yml 설정하기

services:

  web:
    image: sun/healthcheck:latest
    platform: linux/amd64
    container_name: healthcheck-web
    restart: always
    ports:
      - "8080:8080"
    environment:
      - "SPRING_PROFILES_ACTIVE=local"

- 구동할 services 들을 나열하게 되는데, springboot web 하나만을 띄울 것이기 때문에 web이라는 service 하나만 설정해두었다.

- pull 받을 image는 '레포/이미지:tag' 형식이다.

- image가 구동될 플랫폼을 설정해야 하는데, linux/amd64로 설정되어 있다. 필자의 mac platform이 이와 달라서 수동으로 플랫폼을 설정해주었다.

- container_name은 원하는대로 설정하면 된다.

- ports 설정은 docker 가상 컨테이너가 네트워크 통신 규칙을 어떻게 따를지 결정하는 것이다. 이를 설정하지 않으면 가상 컨테이너가 네트워크 통신이 안된다. network in/outbound 규칙과 비슷한 것으로 보인다.

- environment 설정은 하지 않아도 되는데, application-local/dev/prod.yml 파일 중 어떤 설정 파일을 참조할 것인지 명시하기 위해 넣었다. active profiles 설정을 local을 기본으로 해두었다.

- 'docker-compose up' 명령을 실행하여 이미지를 pull 받아서 실행되면 좋겠지만, 현재 만들어둔 app은 DB에 대한 의존성이 있어 에러가 난다.

 

DB 의존성 해결

- 사실 도커로 띄우지 않고 로컬에서 테스트해보아도 DB 의존성이 있으면 실행이 안된다. 그래서 열심히 application-local.yml 파일에 datasource 관련 설정(driver, url, username/password 등)을 하고, mysql 서버를 실행해야 그제서야 서버가 실행된다.

- 이 때 사실 docker-compose에 MySQL image를 하나 더 실행해서 의존성을 연결해도 된다. 그런데 필자가 하고싶었던 것은 로컬에 이미 띄워둔 MySQL 서버에 연결해서 테스트하는 것이었다. profiles 설정도 local로 해두어서 application-local.yml 설정 파일을 읽는데, 왜 현재 실행 중인 3306 포트의 서버와 connect 할 수 없는 것인지 의아했다.

- Docker 컨테이너가 내 로컬 PC(호스트)의 localhost에 직접 접근할 수 없는 문제다. docker 컨테이너 내부에서는 localhost가 컨테이너 자체로 loopback 되기 때문에 실제 로컬 PC의 서비스를 참조하려면 다른 방식으로 접근해야 한다.

- 기존에는 healthcheck라는 DB 스키마에 접근하기 위해 application-local.yml datasource url을 'jdbc:mysql://localhost:3306/healthcheck'로 설정해두었다.

- 도커 컨테이너에서 접근해야 하기 때문에 해당 설정을 바꾸어서 image를 다시 push 한 뒤에 실행했다. 'jdbc:mysql://host.docker.internal:3306/healthcheck'

- 이제 github-actions도 실행되고 해당 이미지를 pull 받아 내 로컬 PC에서 실행 중인 MySQL 서버와도 정상적으로 통신이 되었다.