Docker
info
이번 포스트에서는 IntelliJ 또는 Gradle 커맨드를 통해 수동으로 Jar 파일을 생성하고, 이를 Docker 이미지로 빌드하여 Docker 컨테이너로 실행하는 방법에 대해 알아본다. 특히, Docker 환경에서의 실행을 위해 설정과 코드를 수정하고 Jar 파일을 생성하는 과정은 수동으로 진행하며, 컨테이너 실행 단계는 docker-compose를 통해 자동화할 예정이다. docker-compose 설치 방법도 함께 다룬다.
먼저, IntelliJ를 사용하여 Jar 파일을 생성하는 방법을 살펴보자. IntelliJ에서 프로젝트를 열고, 프로젝트 구조 설정에서 아티팩트를 추가한 후, 빌드 메뉴를 통해 Jar 파일을 생성할 수 있다. Gradle을 사용하는 경우, build.gradle 파일에 필요한 설정을 추가하고, 터미널에서 Gradle 빌드 명령어를 실행하여 Jar 파일을 생성할 수 있다.
다음으로, 생성된 Jar 파일을 Docker 이미지로 빌드하는 방법을 알아보자. 이를 위해 프로젝트 루트 디렉토리에 Dockerfile을 작성한다. Dockerfile에는 베이스 이미지 설정, Jar 파일 복사, 그리고 실행 명령어를 정의한다. 그런 다음, 터미널에서 Docker 빌드 명령어를 실행하여 Docker 이미지를 생성할 수 있다.
마지막으로, Docker Compose를 사용하여 Docker 컨테이너를 실행하는 방법을 설명하겠다. 먼저, docker-compose를 설치한다. 설치가 완료되면, 프로젝트 루트 디렉토리에 docker-compose.yml 파일을 작성한다. 이 파일에는 서비스 정의, 빌드 설정, 포트 매핑 등의 정보를 포함한다. 터미널에서 Docker Compose 실행 명령어를 입력하면, 정의된 설정에 따라 Docker 컨테이너가 실행된다.
이 과정을 통해 IntelliJ 또는 Gradle을 사용하여 생성한 Jar 파일을 Docker 이미지로 빌드하고, Docker Compose를 통해 컨테이너를 실행할 수 있다. 이를 통해 개발 환경을 보다 효율적으로 관리하고 배포할 수 있다.
Docker 및 Docker-compose 설치
$ brew install --cask docker
$ brew install docker-compose
Docker Network 설정
지금까지 여러 서비스들이 동일한 PC에서 실행되어 같은 내부 IP 대역폭에서 통신할 수 있었다. 그러나 Docker의 경우, 가상화된 머신이기 때문에 별도의 설정 없이는 같은 네트워크로 인식되지 않아 내부 IP로 통신할 수 없다. 이를 해결하기 위해 Docker Network 설정을 통해 동일한 네트워크 환경을 구축해 보자.
네트워크 생성
gateway와 subnet을 지정하여 도커 네트워크를 생성한다.
# 생성
$ docker network create --gateway 172.18.0.1 --subnet 172.18.0.0/16 ecommerce-network
# 네트워크 상태 확인 ("Containers"부분을 확인해보면 아직 네트워크에 등록된 컨테이너가 없기 때문에 비어있는 것을 확인할 수 있다.)
$ docker network inspect ecommerce-network
스프링부트 내부에 Dockerfile을 만들고 하기 내용을 입력한다.
스프링클라우드 강의에 나온 서비스를 예시로 하며, 하기 서비스 순으로 설명한다.
- config service
- Discovery Service
- Gateway Service
- Prometheus & Grafana
참고
도커 이미지 푸시 시, 전체 이미지가 매번 업로드되는 것은 아니다. 도커는 이미지의 레이어를 기반으로 작동하며, 각 레이어는 변경된 부분만 업로드된다.
레이어 기반 구조: 도커 이미지는 여러 레이어로 구성되어 있다. 이미 동일한 레이어가 리포지토리에 존재한다면, 해당 레이어는 다시 업로드되지 않는다. 즉, 변경된 레이어만 새로운 레이어로 추가되어 푸시된다.
캐싱: 도커는 로컬에 저장된 이미지 레이어를 캐시하여, 이미 푸시된 레이어는 반복적으로 업로드하지 않는다. 이로 인해 푸시하는 시간과 대역폭이 절약된다.
결과적으로, 도커 이미지를 푸시할 때는 변경된 부분만 업로드되므로 전체 이미지가 매번 업로드되는 것은 아니다. 이 점은 도커의 효율성을 높이는 중요한 기능 중 하나이다.
config service
#베이스 이미지를 지정한다. 베이스 이미지가 로컬에 존재하지 않으면 Docker는 자동으로 해당 이미지를 Docker Hub(또는 다른 설정된 이미지 레지스트리)에서 다운로드한다.
FROM openjdk:17-ea-11-jdk-slim
#컨테이너에서 /tmp 디렉토리를 볼륨으로 지정한다. 이는 컨테이너가 종료되더라도 /tmp 디렉토리의 데이터를 유지할 수 있게 한다. 주로 애플리케이션이 임시 파일을 저장하는 데 사용된다.
VOLUME /tmp
#암호화키가 컨테이너 안으로 복사하도록 구현. 호스트 머신의 encrypt/keystore/encryptionKey.jks 파일을 컨테이너의 루트 디렉토리에 encryptionKey.jks라는 이름으로 복사한다.
COPY encrypt/keystore/encryptionKey.jks encryptionKey.jks
#호스트 머신의 build/libs/config-1.0.jar 파일을 컨테이너의 루트 디렉토리에 Config.jar라는 이름으로 복사한다.
COPY build/libs/config-1.0.jar Config.jar
#컨테이너가 시작될 때 실행할 명령을 지정한다. 여기서는 java -jar Config.jar 명령을 실행하여 JAR 파일을 실행한다. ENTRYPOINT는 컨테이너가 시작될 때 항상 실행되는 명령을 정의한다.
ENTRYPOINT ["java", "-jar", "Config.jar"]
spring:
application:
name: config-service
rabbitmq:
host: rabbitmq # IP가 아닌 컨테이너 이름으로 수정
port: 5672
username: guest
passowrd: guest
...
encrypt:
key-store:
localtion: file:/encryptionKey.jks # 컨테이너 키 경로로 수정
password: 1q2w3e4r
alias: encryptionKey
# jar 생성
$ gradle build
# docker image 생성
docker build -t dadaok/config:1.0 .
Discovery Service
FROM openjdk:17-ea-11-jdk-slim
VOLUME /tmp
COPY build/libs/discovery-1.0.jar DiscoveryService.jar
ENTRYPOINT ["java", "-jar", "DiscoveryService.jar"]
spring:
application:
name: discovery
cloud:
config:
uri: http://config-service:8888 # config 서버 이름으로 입력해 준다.
name: discovery
# jar 생성
$ gradle build
# docker image 생성
docker build -t dadaok/config:1.0 .
Gateway Service(나머지 서비스들은 Gateway과 동일하다.)
FROM openjdk:17-ea-11-jdk-slim
VOLUME /tmp
COPY build/libs/gateway-1.0.jar GatewayService.jar
ENTRYPOINT ["java", "-jar", "GatewayService.jar"]
# jar 생성
$ gradle build
# docker image 생성
docker build -t dadaok/gateway:1.0 .
Prometheus & Grafana(prometheus.yml 수정)
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["prometheus:9090"]
- job_name: "user-service"
scrape_interval: 15s
metrics_path: "/user-service/actuator/prometheus"
static_configs:
- targets: ["user-service:8000"] # localhost:{port}로 등록되어 있는 targets 부분을 {container-name}:{port}형식으로 변경
도커 이미지 확인
생성된 도커 이미지를 확인한다.
$ docker images
도커 명령어
아래 깃 링크의 3번째 커맨드 명령어를 사용한다.
https://github.com/jenkinsci/docker/blob/master/README.md
# 도커 로그인
docker login
# Dockerfile을 사용하여 Docker 이미지를 빌드(로컬 환경에만 저장 된다)
docker build --tag=cicd-project -f Dockerfile
# 이미지를 외부 레지스트리(예: Docker Hub, AWS ECR, Google Container Registry 등)로 전송하려면 docker push 명령어를 사용해야 한다.
docker push <your-dockerhub-username>/cicd-project:latest
# 현재 로컬 Docker 환경에 저장된 모든 Docker 이미지를 목록으로 보여준다
docker images
# 이미지에 대한 상세 정보를 출력한다
docker image inspect cicd-project:latest
# 실행 명령어
docker run --name <도커서비스이름> -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --restart=on-failure jenkins/jenkins:lts-jdk17
# 현재 실행 중인 컨테이너 목록 확인
docker ps
# 모든 컨테이너 목록 확인 (실행 중이거나 중지된 컨테이너 모두)
docker ps -a
# 도커 컨테이너 중지
docker stop <도커서비스이름>
# 도커 컨테이너 삭제
docker rm <도커서비스이름>
# 도커 이미지 삭제
docker rmi <도커이미지이름>
옵션 설명
- docker run: 새로운 컨테이너를 생성하고 실행하는 명령어이다.
- -d: 컨테이너를 백그라운드에서 실행한다. 이 옵션을 사용하면 터미널이 컨테이너의 로그에 붙잡히지 않고 바로 돌아온다.
- -v jenkins_home:/var/jenkins_home: 볼륨을 마운트한다. 호스트의 jenkins_home 디렉토리를 컨테이너의 /var/jenkins_home 디렉토리에 연결한다. 이렇게 하면 Jenkins 데이터를 호스트에 저장할 수 있어 컨테이너를 삭제해도 데이터가 유지된다.
- -p 8080:8080: 호스트의 8080 포트를 컨테이너의 8080 포트에 매핑한다. Jenkins의 웹 인터페이스에 접근하기 위해 사용된다.
- -p 50000:50000: 호스트의 50000 포트를 컨테이너의 50000 포트에 매핑한다. 이는 Jenkins 에이전트와의 통신을 위해 사용된다.
- –restart=on-failure: 컨테이너가 실패할 경우 자동으로 재시작한다. 컨테이너가 종료 코드가 0이 아닌 경우에만 재시작한다.
- –name <서버이름>: 생성될 컨테이너의 이름을 지정한다. <서버이름> 부분을 원하는 이름으로 대체해야 한다.서버이름>서버이름>
- jenkins/jenkins:lts-jdk17: 사용할 이미지와 태그를 지정한다. 여기서는 Jenkins의 LTS(Long Term Support) 버전 중 JDK 17이 포함된 이미지를 사용한다.
Docker 이미지 실행
# RabbitMQ:
docker run -d --name rabbitmq -e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest -p 15672:15672 -p 5672:5672 -p 15671:15671 -p 5671:5671 -p 4369:4369 rabbitmq:management
# Config Service:
docker run -d --name config-service -e spring.profiles.active=default -p 8888:8888 dadaok/config:1.0
# Discovery Service:
docker run -d --name discovery-service -e spring.cloud.config.uri=http://config-service:8888 -p 8761:8761 dadaok/discovery:1.0
# Gateway Service:
docker run -d --name gateway-service -e spring.cloud.config.uri=http://config-service:8888 -e spring.rabbitmq.host=rabbitmq -e eureka.client.serviceUrl.defaultZone=http://discovery-service:8761/eureka/ -p 8000:8000 dadaok/gateway:1.0
# User Service:
docker run -d --name user-service -e spring.cloud.config.uri=http://config-service:8888 -e spring.rabbitmq.host=rabbitmq -e spring.zipkin.base-url=http://zipkin:9411 -e eureka.client.serviceUrl.defaultZone=http://discovery-service:8761/eureka/ -e logging.file=/api-logs/users-ws.log dadaok/user:1.0
# Order Service:
docker run -d --name order-service -e spring.zipkin.base-url=http://zipkin:9411 -e eureka.client.serviceUrl.defaultZone=http://discovery-service:8761/eureka/ -e spring.datasource.url=jdbc:mariadb://mariadb:3306/mydb -e logging.file=/api-logs/orders-ws.log dadaok/order:1.0
# Zipkin:
docker run -d --name zipkin -p 9411:9411 openzipkin/zipkin
# Prometheus:
docker run -d --name prometheus -p 9090:9090 -v ./thirdparty/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus
# Grafana:
docker run -d --name grafana -p 3000:3000 grafana/grafana
# MariaDB:
docker run -d --name mariadb -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=mydb -p 3306:3306 mariadb:latest
# Zookeeper:
docker run -d --name zookeeper -p 2181:2181 --network ecommerce-network --ip 172.18.0.100 wurstmeister/zookeeper
# Kafka:
docker run -d --name kafka -p 9092:9092 -e KAFKA_ADVERTISED_HOST_NAME=172.18.0.101 -e KAFKA_CREATE_TOPICS="test:1:1" -e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 -v /var/run/docker.sock:/var/run/docker.sock --network ecommerce-network --ip 172.18.0.101 wurstmeister/kafka
docker-compose를 통한 배포
version: "3.6"
services:
rabbitmq:
container_name: rabbitmq
image: rabbitmq:management
environment:
RABBITMQ_DEFAULT_USER: "guest"
RABBITMQ_DEFAULT_PASS: "guest"
networks:
- ecommerce-network
ports:
- "15672:15672"
- "5672:5672"
- "15671:15671"
- "5671:5671"
- "4369:4369"
config-service:
container_name: config-service
image: dadaok/config:1.0
environment:
spring.profiles.active: "default"
ports:
- "8888:8888"
networks:
- ecommerce-network
depends_on:
- rabbitmq
discovery-service:
container_name: discovery-service
image: dadaok/discovery:1.0
environment:
spring.cloud.config.uri: "http://config-service:8888"
ports:
- "8761:8761"
networks:
- ecommerce-network
depends_on:
- config-service
gateway-service:
container_name: gateway-service
image: dadaok/gateway:1.0
environment:
spring.cloud.config.uri: "http://config-service:8888"
spring.rabbitmq.host: "rabbitmq"
eureka.client.serviceUrl.defaultZone: "http://discovery-service:8761/eureka/"
ports:
- "8000:8000"
networks:
- ecommerce-network
depends_on:
- discovery-service
user-service:
container_name: user-service
image: dadaok/user:1.0
environment:
spring.cloud.config.uri: "http://config-service:8888"
spring.rabbitmq.host: "rabbitmq"
spring.zipkin.base-url: "http://zipkin:9411"
eureka.client.serviceUrl.defaultZone: "http://discovery-service:8761/eureka/"
logging.file: "/api-logs/users-ws.log"
depends_on:
- gateway-service
- zipkin
- rabbitmq
order-service:
container_name: order-service
image: dadaok/order:1.0
environment:
spring.zipkin.base-url: "http://zipkin:9411"
eureka.client.serviceUrl.defaultZone: "http://discovery-service:8761/eureka/"
spring.datasource.url: "jdbc:mariadb://mariadb:3306/mydb"
logging.file: "/api-logs/orders-ws.log"
depends_on:
- gateway-service
- zipkin
- rabbitmq
# third-party
zipkin:
container_name: zipkin
image: openzipkin/zipkin
ports:
- "9411:9411"
networks:
- ecommerce-network
prometheus:
container_name: prometheus
image: prom/prometheus
ports:
- "9090:9090"
networks:
- ecommerce-network
volumes:
- ./thirdparty/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
container_name: grafana
image: grafana/grafana
ports:
- "3000:3000"
networks:
- ecommerce-network
mariadb:
container_name: mariadb
image: mariadb:latest
environment:
MYSQL_ROOT_PASSWORD: "root"
MYSQL_DATABASE: "mydb"
ports:
- "3306:3306"
networks:
- ecommerce-network
zookeeper:
image: wurstmeister/zookeeper
ports:
- "2181:2181"
networks:
ecommerce-network:
ipv4_address: 172.18.0.100
kafka:
image: wurstmeister/kafka
ports:
- "9092:9092"
environment:
KAFKA_ADVERTISED_HOST_NAME: 172.18.0.101
KAFKA_CREATE_TOPICS: "test:1:1"
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
volumes:
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- zookeeper
networks:
ecommerce-network:
ipv4_address: 172.18.0.101
networks:
ecommerce-network:
external: true