티스토리 뷰
docker-compose를 이용하여 로컬 개발환경 구성하기
- Part 1 : docker-compose를 이용하여 로컬 개발환경 구성하기
- Part 2 : docker-compose를 이용하여 spring boot 프로젝트 연결하기(예정)
배경
최근 Gmarket Member Engineering 팀에서 Quilt - 로그인 개선
프로젝트를 진행하였습니다.
기존 닷넷 기반의 레거시 어플리케이션을 Java + Container 기반으로 전환하는 것을 시작으로, 수년간 쌓여온 기술 부채를 해결하고 보다 나은 회원 인증 체계를 개발하기 위한 첫 발을 내디뎠습니다.
이를 위하여 저희 팀에서는 지속 가능 하고 확장 가능한 개발환경 구성을 고민하였고, docker를 로컬 개발환경 구성에 이용했습니다.
그동안 Java 프로젝트 구성을 위해 1~2개의 port를 사용하는 어플리케이션들을 로컬에서 구동하고 디버깅하는 방식으로 진행했었습니다.
그러나 MSA기반의 로컬 개발 환경에서는 어플리케이션마다 포트를 서로 충돌되지 않게 설정해야 하고, 상황에 필요한 미들웨어들을 초기 설치하고 설정을 관리해야 하는데에 어려움이 있었습니다.
이런 어려움은 개인마다 로컬 개발환경을 조금 다르게 구성할 수 있고 이로 인해 서로 다른 개발 경험을 하게 되며, 초기 개발 환경 구성이 번거로워지는 등 개발 생산성을 저해하는 요소가 될 수 있습니다.
그래서 여러 개의 도커 컨테이너를 묶어서 관리할 수 있는 docker compose[1]를 이용하여 언제 어디서나 동일한 환경을 재현할 수 있고 디버깅할 수 있는 환경을 구성하였고, 본문을 통하여 그 경험을 소개해 드리고자 합니다.
개발환경을 구성해보자
1. spring boot 프로젝트 생성하기
아래는 본문에서 설명할 techblog-backend
샘플 어플리케이션의 구성도입니다. 우리는 로컬에서 운영과 동일한 환경을 docker-compose를 이용하여 구성할 예정입니다.
해당 환경의 특징은 아래와 같습니다.
1) 4개의 인스턴스가 각각의 port로 구동되어야 합니다. 이중 두 개의 개발 어플리케이션(subscriber
, api
)은 8080 포트를 중복으로 사용합니다.
2) 나머지는 오픈소스 미들웨어로 pub/sub기능을 위한 redis와 각종 proxy를 구성할 nginx 입니다.
3) hostname pub.gmarket.co.kr, sub.gmarketc.co.kr을 hosts파일에 정의하여 목적에 맞는 어플리케이션에 접근합니다.
127.0.0.1 pub.gmarket.co.kr
127.0.0.1 sub.gmarket.co.kr
2. docker-compose.yml 생성하기
Fig.1 - 어플리케이션 구성도
는 하나의 프로젝트이며 멀티 프로젝트입니다. 우선 Intellij 이용하여 해당 프로젝트를 생성해 보겠습니다.
전체 소스는 github에서 확인 가능합니다. : https://github.com/jayjlee29/gmarket-techblog-backend
이중 핵심이 되는 docker-compose.yml
파일을 살펴보겠습니다.
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"
nginx:
image: nginx
restart: always
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ports:
- 80:80
- 5005:5005
- 5006:5006
redis:
image: redis:6.2.6
subscriber:
build:
context: subscriber
dockerfile: Dockerfile
volumes:
- app_home:/opt/app
depends_on:
nginx:
condition: service_started
# builder:
# condition: service_completed_successfully
redis:
condition: service_started
restart: always
api:
build:
context: api
dockerfile: Dockerfile
volumes:
- app_home:/opt/app
depends_on:
nginx:
condition: service_started
# builder:
# condition: service_completed_successfully
redis:
condition: service_started
restart: always
volumes:
gradle_home:
app_home:
- docker-compose.yml 설명
- docker-compose에 정의된 service는 builder, nginx, redis, subscriber, api 5개로 구성되어 있습니다. 빌더 + 어플리케이션 서버군 이라고 생각하시면 됩니다 기능은 다음과 같습니다.
- builder : api, subscriber의 빌드를 담당합니다. build.gradle를 확인해보시면, 최종적으로 app_home이라는 docker volume의 /opt/app/{api/subscriber}/classes 경로에 jar와 *.class파일이 복사 됩니다.
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"
- nginx : proxy를 담당하고 있습니다. api, subscriber를 하나의 포트(80)를 통해서 라우팅 해줍니다. 또한 Remote Debugging을 위하여 5005, 5006을 forward 해주게 됩니다.
nginx: image: nginx restart: always volumes: - ./nginx.conf:/etc/nginx/nginx.conf ports: - 80:80 - 5005:5005 - 5006:5006
- redis : 샘플 techblog-backend 프로젝트의 pub/sub를 지원할 미들웨어입니다.
redis: image: redis:6.2.6
- api/subscriber : techblog-backend의 샘플 어플리케이션 입니다. 기능은 간단합니다. api는 publish하고 subscriber는 redis의 topic을 subscribe합니다. 또한 subscriber는 api형태의 long pulling형태로 샘플이 개발되었습니다.
subscriber: build: context: subscriber dockerfile: Dockerfile volumes: - app_home:/opt/app depends_on: nginx: condition: service_started redis: condition: service_started restart: always api: build: context: api dockerfile: Dockerfile volumes: - app_home:/opt/app depends_on: nginx: condition: service_started redis: condition: service_started restart: always
- docker volumes은 gradle_home, app_home을 생성했습니다.
- gradle_home은 반복되는 빌드시 사용될 gradle repo입니다.
- app_home은 위에서 언급한 대로 api, subscriber가 builder를 통하여 생성된 결과물(lib, class)이 /opt/app/{api/subscriber}/classes 경로에 복사됩니다.
volumes: gradle_home: app_home:
- 상세 설명에는 없지만 전체 docker-compose.yml의
subscriber
과api
service의 depends_on을 보시면builder
가 주석되어 있는 것을 보실 수 있습니다. 일부러 남겨놓은 것인데요 만일docker compose up
을 통해서 빌드 후 어플리케이션을 동작시키기 원한다면 주석을 해제하시면 됩니다. 참고로 알아두시면 도움이 됩니다.- condition의 service_completed_successfully는
builder
service가 동작 후 완전 종료된다면subscriber
,api
가 동작한다는 의미입니다. (docker compose - depends_on[3])
- condition의 service_completed_successfully는
builder: condition: service_completed_successfully
3. api, subscriber를 위한 Dockerfile 정의하기
docker-compose.yml
명세의 api, subscriber service를 살펴보면 각각의context
에Dockerfile
이 지정된 것을 확인하실 수 있습니다.- 이것은 redis, nginx와는 다르게 docker-compose에서 직접 이미지를 다운로드하여
service
를 생성하는 것이 아닌 지정된Dockerfile
을 이용하여 service image를 생성합니다. - Subscriber Dockerfile 예시
FROM azul/zulu-openjdk:11
ENV APPDIR=/opt/app/subscriber/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 -cp .:${APPDIR}/* com.gmarket.techblog.backend.subscriber.SubscriberApplication
EXPOSE 8080 5005
- Subscriber Dockerfile 설명
- subscriber dockerfile을 예시로 설명드리겠습니다.
FROM azul/zulu-openjdk:11
는 사용할 이미지 정보를 나타냅니다.builder
service의 이미지도 동일한 이미지가 지정된 것을 확인하실 수 있고 build와 runtime시 동일한 환경을 사용하는 것을 알 수 있습니다.ENV APPDIR=/opt/app/subscriber/classes
를 통하여 환경변수 APPDIR을 지정하고, 해당 경로는 각 어플리케이션의build.gradle
의 copyDeps task 정의된 경로와 동일합니다. 또한 docker-compose에 정의된volumes
app_home을 이용한다는 것을 확인 하실 수 있습니다.
이는 자신의 개발 PC 즉 host os의 파일시스템이 아닌 docker volume를 통하여 빌드된 결과 파일 격리되어 관리 되는 것을 아실수 있고, docker를 통해서 관리가 가능합니다.CMD
를 통하여docker run
시 기본으로 실행할 명령을 foreground로 실행 시킵니다.EXPOSE {PORT} ...
는 동일한 docker network상에서 다른 container와 통신하기 위하여 PORT를 공개 하는것입니다. 만일 host와도 통신이 필요하시면 추가로docker run ...
시-p
옵션을 이용하거나,docker-compose.yaml
에{service}.ports
forward 설정을 정의해야 합니다.
4. 어플리케이션 실행하기
이제 어플리케이션을 실행하기 위해서는 terminal에서 docker-compose cli[2]를 이용하여 실행시켜야 합니다.
- 어플리케이션 빌드하기
docker compose up builder
- 어플리케이션 실행하기
docker compose up api subscriber redis nginx
5. 디버깅하기
- 디버깅은 Intellij을 이용하려 합니다. Intellij가 아닌 다른 IDE에서도 Remote Debug 기능을 지원한다면 사용 가능합니다.
Run Configuration > Remote JVM Debug
을 통하여 api와 subscriber 디버그 항목을 생성하고 아래 그림처럼 설정 해줍니다.- 어플리케이션을 실행한 상태에서 방금 생성한 Remote JVM Debug를 실행하고 원하는 위치에 break point 설정하면 디버그 모드를 확인할 수 있습니다.
6. 수정된 코드 재빌드 하기
builder
service를 통하여 다시 빌드를 실행합니다.
docker compose start bulider
- 새로 반영될 내용이 있다면
builder
의 동작이 끝나고 /opt/app/{api/subscriber}/classes에 새로운 *.class, jar가 생성되고build.gradle
에 정의된 devtools dependency를 통하여 Auto Restart 기능에 의해 바로 확인하실 수 있습니다. - 수정된 소스가 동작중인 spring boot application 에 자동으로 반영되기 위하여 devtools의 auto restart기능[4]을 사용하였습니다.
7. 기타 docker-compose 명령어
- docker-compose.yml이 변경되었다면
docker compose up
만으로 service가 다시 생성됩니다. 그러나 만일 api/subscriber service의 Dockerfile이 변경되었다면 아래 명령을 통하여 변경된 이미지를 재 생성 시켜주셔야 합니다.
docker compose up --build
- 만일 docker-compose.yml이 변경되어도 만들어진 service를 재생성하고 싶지 않고 다시 구동하고 싶으시면 다음과 같이 하시면 됩니다
docker compose start
- 만일 service 선택적으로 빌드를 윈하신다면 다음과 같이 뒤에 service명을 입력하시면 됩니다.
docker compose up --build api
- 만일 docker compose를 통하여 생성된 모든 오브젝트(service, network, volume) 삭제하고 싶으시다면 다음과 같이 하시면 됩니다.
docker compose down
- 만일 생성된 image까지 삭제하고 싶으시다면
-rmi
옵션을 추가하시면 됩니다.
docker compose down --rmi all
평가
docker-compose를 이용하여 개발환경을 구성하면서 다음과 같은 장/단점을 경험할 수 있었습니다.
- 장점
- 언제/어디서나, 심지어 개발 PC의 OS가 다른 상황에서도 동일한 환경구성이 가능합니다.
- 모두 동일한 개발환경을 경험하기 때문에 개발환경에 이슈가 발생해도 소통이 쉽습니다.
- 복잡한 환경도 스크립트화 할 수 있기 때문에 자동화가 가능하고 조작이 쉽습니다.
docker-compose cli
를 이용하여 쉽게 애플리케이션을 관리할 수 있고 자동화가 가능합니다.
- 단점
- 여러 container가 동작을 하니 개발 pc의 사양이 높아야 합니다. (저희 팀 내의 mac pro m1을 사용하시는 분들은 쾌적하다고 하네요)
- 문제 발생 또는 형상이 변경될 경우 docker 기술에 대한 이해도가 부족하면 관리하기 어렵습니다.
최근 container의 활용도가 높아지고 운영에서 container가 차지하는 비중이 점점 늘고 있습니다.
다만 개발 시에 이런 container 기술이 아직은 잘 활용되지 않는 것 같아, 저희 팀의 경험을 공유하는 자리를 가지게 되었습니다.
감사합니다.
참고
[1] docker compose overview : https://docs.docker.com/compose/
[2] docker compose reference : https://docs.docker.com/compose/reference/
[3] docker compose - depends_on https://docs.docker.com/compose/compose-file/compose-file-v3/#depends_on
[4] devtools - Automatic Restart : https://docs.spring.io/spring-boot/docs/2.7.9/reference/html/using.html#using.devtools.restart
[5] example : https://github.com/jayjlee29/gmarket-techblog-backend
'Backend' 카테고리의 다른 글
Testcontainers로 통합테스트 만들기 (0) | 2023.04.26 |
---|---|
BigDecimal A to Z: 정확한 계산을 위한 숫자 처리 클래스 (0) | 2023.04.19 |
초보 개발자를 위한 Redis Cluster Migration 가이드라인 (1) | 2023.03.28 |
Redis Lua Script를 이용해서 API Rate Limiter개발 (4) | 2023.03.16 |
Java Logger의 또다른 식구, tinylog (0) | 2023.03.02 |