티스토리 뷰
docker-compose를 이용하여 다수의 spring boot 프로젝트 연결하기
개요
Part1에서 소개한 예제에서는 하나의 git repo로 구성된 java 멀티 프로젝트를 docker-compose를 이용하여 구성했습니다.
이번 Part2에서는 다른 git repo의 어플리케이션을 이용하여 Part1에서 생성했던 backend 서버 어플리케이션과 함께 동작하도록 구성해보겠습니다.
편의상 Part 2의 프로젝트는 frontend로 예제를 작성하였습니다.
개발 블로그에서 소개하는 코드들은 gmarket-techblog-frontend's github 예제를 통하여 확인이 가능합니다.
조금 더 복잡한 개발 환경을 구성해보자
- docker-compose 프로젝트 기준으로 위와 같이 구성될 예정입니다.
- docker-compose는
docker compose
명령이 실행되거나--project-directory
로 지정된 위치를 기준으로 프로젝트를 구성합니다 - 아래 설명에서 두 개의 프로젝트를 어떻게 네트워크를 통합하고 연결되는지 설명할 예정입니다.
- 위 Network 구성도는 container 간의 네트워크 구성도입니다.
- Part 1에서 소개해 드렸던 구조는 하나의 docker compose 프로젝트를 통하여 구성되기 때문에 container구성과 네트워크 구성이 이해하기 쉬웠지만, Part 2에서는 기존 backend의 networks 영역에 frontend를 추가하고 backend의 api서버와 nginx(proxy)를 통하여 내부 통신하도록 구성할 예정입니다.
- 이때 backend의 nginx를 통하여 frontend 어플리케이션의 reverse proxy 를 구성할 예정입니다.
Frontend 생성하기
0. 사전 준비 - hosts 파일에 frontend.gmarket.co.kr를 추가하자
- Part 1 에서와 마찬가지로 frontend를 위한 호스트를 설정합니다.
- 개발 시 localhost 보다는 명확한 도메인을 사용하는 것이 개발의 효율을 높일 수 있는 장점이 있습니다.
- 이를 위하여 nginx의
proxy_pass
기능을 이용하여 도메인명에 따른 어플리케이션으로 reverse proxy 하기 위함입니다. - 맥에서는 /etc/hosts, 윈도우는 C:\Windows\System32\drivers\etc\hosts 경로에 위치합니다.
127.0.0.1 frontend.gmarket.co.kr
1. frontend docker-compose 구성하기
- 다음은 frontend의 docker-compose.yml의 내용입니다. Part 1에서 소개드린 backend의 docker-compose.yml 내용과 크게 다르지 않지만
networks
설정이 새로 추가된 것을 확인하실 수 있습니다.
version: '3.9'
services:
builder:
image: azul/zulu-openjdk:11
volumes:
- .:/opt/build
- type: volume
source: app_home
target: /opt/app
volume:
nocopy: true
- type: volume
source: gradle_home
target: /opt/.gradle:rw
working_dir: /opt/build
command: "/opt/build/gradlew copyDeps --gradle-user-home=/opt/.gradle -x test"
cleanBuilder:
image: azul/zulu-openjdk:11
volumes:
- .:/opt/build
- type: volume
source: app_home
target: /opt/app
volume:
nocopy: true
- type: volume
source: gradle_home
target: /opt/.gradle:rw
working_dir: /opt/build
command: "/opt/build/gradlew cleanDeps copyDeps --gradle-user-home=/opt/.gradle -x test"
frontend:
build:
context: ./
dockerfile: Dockerfile
volumes:
- app_home:/opt/app
restart: always
external_links:
- nginx:pub.gmarket.co.kr
networks:
- techblog-backend_default
volumes:
gradle_home:
app_home:
networks:
techblog-backend_default:
external: true
networks.techblog-backend_defualt
객체는 backend의 docker-compose.yml 를 통하여 생성된 네트워크 객체입니다. 따로 명칭을 명명하지 않으면 기본으로project(directory)명_default
로 생성됩니다.- frontend의 docker-compose.yaml에서
techblog-backend_defualt
네트워크 객체를 사용하여 frontend와 backend의 네트워크를 공유하려 합니다. 이때 frontend입장에서는 backend는 외부 프로젝트이기 때문에external: true
옵션을 통하여 설정합니다. - 다음은 frontend의 docker-compose.yml의
services.frontend.networks
를 확인하실 수 있습니다. frontend 또한docker-compose up...
시 기본으로project(directory)명_default
네트워크가 생성됩니다. 그러나services.frontend.networks
설정에 의해 frontend는 backend의 네트워크에 편입되게 됩니다. - 최종적으로
Fig.2 - Network 구성도
와 같은 구성이 완성되게 됩니다.
2. backend에 구성된 nginx를 이용하여 frontend 프로젝트의 어플리케이션 proxy하기
- 다음은 part1에서 소개드린 backend의 nginx.conf를 다시 살펴보겠습니다.
- frontend 또한 로컬에서 80 포트 사용하길 원합니다. 그러나 이미 backend를 통하여 80 포트인 nginx가 구동되고 있으며 reverse proxy로 동작중입니다.
- backend의 nginx.conf에 다음 설정을 추가고, frontend는 backend의 nginx를 통하여 80 포트 서비스를 로컬에 제공할 예정입니다.
server {
listen [::]:80;
listen 80;
server_name frontend.gmarket.co.kr; # 서비스할 도메인으로 변경
location / {
# 기본값으로 설정
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
set $upstream frontend;
proxy_pass http://$upstream:8080;
}
}
- 위 설정은
frontend.gmarket.co.kr
도메인으로 접근 시frontend
서비스로 proxy 하기 위한 http 블록 설정입니다. set $upstream frontend
를 통하여 구성한 이유는 최초 backenddocker compose up ...
시 nginx에 구동 실패를 막기 위해서입니다.proxy_pass http://frontend:8080
를 직접 정의시 네트워크에서 해당 서비스를 찾지 못하고 구동 오류를 발생시키기 때문에 변수화 시켰습니다.- 추가로 backend 입장에서 외부 프로젝트로서 추가된 frontend를 찾기 위하여 아래와 같이
resolver 127.0.0.11
를 nginx.conf 설정합니다. 127.0.0.11
ip는docker exec -it {nginx service full name} cat /etc/resolv.conf
를 통하여 nameserver ip로 확인이 가능합니다.
resolver 127.0.0.11; # docker compose network의 nameserver 주소로 변경함 'cat /etc/resolv.conf로 확인'
- 아래는 frontend 서버 어플리케이션 디버깅을 위한 stream 블록의 설정입니다. 5007 포트로 접근시 frontend로 연결됩니다. (stream 블록에서는 frontend를 변수화 시키지 않아도 별다른 오류가 발생하지 않았습니다. )
server {
listen 5007;
proxy_pass frontend:5005;
}
- 위와 같이 nginx의 5007 포트를 설정하셨다면 intellij에서 아래와 같이 Remote JVM Debug 설정을 통하여 디버깅이 가능합니다.
- Remote JVM Debug Configuration을 처음 생성하면 기본값은 Host: localhost, Port: 5005 입니다. 이 부분을 frontend에서 설정한 값과 맞춰 변경하겠습니다.
- Remote JVM Debug에서 사용하는 JDWP는 http와는 다른 tcp 프로토콜입니다. 때문에 http 프로토콜처럼 헤더의 host명을 이용하여 reverse proxy가 불가능합니다. 오직 port로만 가능하기 때문에 다소 불편하더라도 port를 디버깅하기 원하는 어플리케이션 수만큼 할당해야만 합니다.
- Host 주소는 localhost가 맞습니다. 그러나 frontend.gmarket.co.kr도 127.0.0.1을 가리키고 있기 때문에 본 글에서는 상관없을 듯합니다 (혹시 hosts가 정의되지 않았다면 오동작할 수 있으니 주의해 주세요)
- frontend의 Remote JVM Debug의 Port는 5007로 정의했습니다. 그러나 실제 container내부에서는 5005로 오픈되어 있고 dockerfile에서도 5005로 expose된 것을 확인하실 수 있습니다.
- 그러나 docker host(개발 pc) 영역에서는 5007 포트로 listening 중이고 backend의 docker-compose.yml와 nginx.conf를 통하여 5007 포트를 nginix를 이용하여 frontend 어플리케이션의 5005포트로 reverse proxy 하는 것을 확인하실 수 있습니다.
3. frontend를 위한 Dockerfile 정의하기
- 다음은 frontend를 위한 Dockerfile 입니다.
ROM azul/zulu-openjdk:11
ENV APPDIR=/opt/app/frontend/classes
WORKDIR ${APPDIR}
ENV JAVA_DEBUG_OPT="-Xdebug -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
#CMD java $JAVA_DEBUG_OPT -Djava.security.egd=file:/dev/./urandom -jar ${APPDIR}/app.jar
CMD java ${JAVA_DEBUG_OPT} -Djava.security.egd=file:/dev/./urandom -cp .:${APPDIR}/* com.gmarket.techblog.frontend.client.TechblogFrontendWebfluxApplication
EXPOSE 8080 5005
- backend와 동일하게 app_home의
/opt/app/frontend/classes
경로에 빌드된 결과물이 위치됩니다.
4. external link 설정하기
- Part 2의 네트워크 구성을 보면 frontend는 backend의 api서버와 내부 통신합니다.
- 이때 frontend의 application.yaml를 확인해 보시면 아래와 같이 api서버의 호스트명을 지정한 것을 확인하실 수 있습니다.
api:
host: http://pub.gmarket.co.kr
- 위 설정이 정상동작 하기 위해서는 frontend container에는 api를
http://pub.gmarket.co.kr
을 통하여 접근이 가능해야 합니다. - 다음은 frontend의
external_links
설정을 통하여 backend의 nginx를 pub.gmarket.co.kr로 설정했으며 nginx.conf에 의해 api서버로 reserves proxy 됩니다.
frontend:
build:
context: ./
dockerfile: Dockerfile
volumes:
- app_home:/opt/app
restart: always
external_links:
- nginx:pub.gmarket.co.kr
networks:
- techblog-backend_default
5. 모든 어플리케이션 실행하기
- 이제 모든 프로젝트를 구동시킬 차례입니다. 본 예제에서는 networks 객체를 backend의 기본 객체로 사용하기 때문에 backend -> frontend 순으로 동작시켜야 합니다.
- backend build & run
docker compose builder && docker compose api subscriber
- frontend build & run
docker compose builder && docker compose frontend
- frontend clean & build 기능 추가
- Part 1에서는 없었지만 Part 2 작성 시 필요하다고 생각되어 clean & builder 기능도 추가했습니다.
- 만일 app_home의 classes를 모두 지우고 다시 빌드시킬 때 사용하시면 됩니다.
- 샘플 작업 당시 gradle dependency를 삭제할 때 필요 했습니다. :)
docker compose cleanBuilder
- frontend와 backend 프로젝트가 정상적으로 구동되었다면 브라우저 http://frontend.gmarket.co.kr 주소를 통하여 접근이 가능합니다.
- 그 기능은 아래와 같습니다.
- "Publish to serverTopic" 버튼 클릭 시 input에 입력된 "hello - dev.gmarket.co.kr"이 api 서버를 통하여 redis pub/sub의
serverTopic
Topic에 publish 됩니다. - "Get serverTopic's payload'는 publish 된 모든 메시지를 가져오기 & 보여주기입니다.
- "Publish to serverTopic" 버튼 클릭 시 input에 입력된 "hello - dev.gmarket.co.kr"이 api 서버를 통하여 redis pub/sub의
맺음말
- 최종적으로 2개의 프로젝트를 통합하여 로컬 개발환경을 만들고 활용하면서 다음과 같은 장/단점을 경험할 수 있었습니다.
- 장점
- Part 1에서도 언급했지만 간편하게 로컬 개발 구성을 완성할 수 있었고 누구나 동일한 환경을 경험할 수 있습니다. :)
- Part 2를 통하여 다수의 프로젝트를 통합하고 개발/운영 환경과 비슷한 환경을 로컬 환경에서 경험할 수 있습니다.
- 만일 운영/개발 환경도 container 환경이라면 더욱더 동일한 환경으로 로컬에서 확인하실 수 있습니다.
- 단점
- 물론 Part 1에서도 언급했자만, 개발 pc의 사양이 조금 높아야 합니다.
- 아무래도 2개 이상의 프로젝트를 통합하기 때문에 다양한 이슈가 발생할 가능성이 높습니다. 때문에 docker 기술에 대한 이해도가 Part 1에 비해서 조금 더 높아야 합니다 ^^;
이것으로 docker compose를 이용한 개발 환경 구성 이야기를 마치겠습니다.
감사합니다.
참고
[1] backend : https://github.com/jayjlee29/gmarket-techblog-backend
[2] frontend : https://github.com/jayjlee29/gmarket-techblog-frontend
[3] Networking in Compose : https://docs.docker.com/compose/networking/
'Backend' 카테고리의 다른 글
코루틴(Coroutine)에 대하여 (1) | 2023.06.14 |
---|---|
Apache Commons IO 취약점 파헤치기 (0) | 2023.06.07 |
달리는 인증 서비스의 NoSQL을 바꾸자. - 실전편 (0) | 2023.05.10 |
달리는 인증 서비스의 NoSQL을 바꾸자. - 전략편 (0) | 2023.05.03 |
Testcontainers로 통합테스트 만들기 (0) | 2023.04.26 |
댓글