[SpringBoot] Logback 파일과 Docker 볼륨 연동하기
Programming/Spring Boot

[SpringBoot] Logback 파일과 Docker 볼륨 연동하기

 

스프링부트 서버를 띄울 때 로컬에선 콘솔로 에러 로그를 확인하면 되지만, 원격 EC2에 서버를 띄우면 에러 로그 확인이 어려워집니다. 장애 대응 시 에러 로그를 바로 확인하는 것이 중요하기에, 이번 글에서는 에러 로그를 어떻게 하면 바로 확인할 수 있는지 알아보고자 합니다. 모든 설명은 EC2에 도커 컨테이너 기반으로 서버를 띄우고, 로깅 프레임워크로 Logback을 사용한다는 가정하에 진행하겠습니다.

 

 

Logback 적용 전

아직 Logback을 적용하지 않았다면 스프링부트 서버의 로그는 파일 형태가 아닌 표준 출력을 따를 것입니다. 따라서   docker logs  명령어를 통해 로그를 확인할 수 있습니다. docker logs 명령어는 컨테이너 내부에서 출력을 보여주는 명령어입니다. docker logs 컨테이너명 또는 컨테이너 ID를 입력하면 됩니다.

 

>> docker ps 
CONTAINER ID   IMAGE                                        COMMAND                  CREATED        STATUS                PORTS                                                        NAMES
be6643c84166   doonutmate/doonut-production-server:latest   "java -jar /app.jar"     14 hours ago   Up 14 hours           0.0.0.0:8081->8081/tcp, :::8081->8081/tcp                    production-server
34a328286f35   mysql/mysql-server:8.0                       "/entrypoint.sh mysq…"   5 days ago     Up 5 days (healthy)   33060-33061/tcp, 0.0.0.0:3307->3306/tcp, :::3307->3306/tcp   doonut-devdb

>> docker logs production-server
.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

 

 

만약 마지막에 쌓인 로그의 일부분만 확인하고 싶다면  --tail  옵션을 사용하면 됩니다.

>> docker logs --tail 15 production-server

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'imageService': Injection of autowired dependencies failed
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:498)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1416)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:597)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1417)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:910)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:788)
	... 27 common frames omitted

 

 

기본적으로 위와 같은 컨테이너 로그는 JSON 형태로 도커 내부에 저장됩니다. 저장되는 파일 경로는 다음과 같습니다.

/var/lib/docker/containers/${CONTAINER_ID}/${CONTAINER_ID}-json.log

 

 

cat 명령어를 통해 어떤 형태로 저장되는 지 확인할 수 있습니다. 정제되지 않은 JSON 형태이기에 실제 운영 환경에서 로그 확인 용으로 사용하기엔 무리가 있음을 알 수 있습니다.

>> cat /var/lib/docker/containers/be6643c8416684f82a30fcdcdfb302fdccb8d460f7dbf1ca085553e3e746fde1/be6643c8416684f82a30fcdcdfb302fdccb8d460f7dbf1ca085553e3e746fde1-json.log

{"log":"\n","stream":"stdout","time":"2024-02-16T15:09:24.40164305Z"}
{"log":"  .   ____          _            __ _ _\n","stream":"stdout","time":"2024-02-16T15:09:24.40326988Z"}
{"log":" /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\\n","stream":"stdout","time":"2024-02-16T15:09:24.403278125Z"}
{"log":"( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\\n","stream":"stdout","time":"2024-02-16T15:09:24.40328233Z"}
{"log":" \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )\n","stream":"stdout","time":"2024-02-16T15:09:24.403286323Z"}
{"log":"  '  |____| .__|_| |_|_| |_\\__, | / / / /\n","stream":"stdout","time":"2024-02-16T15:09:24.403289982Z"}
{"log":" =========|_|==============|___/=/_/_/_/\n","stream":"stdout","time":"2024-02-16T15:09:24.403293513Z"}
{"log":" :: Spring Boot ::                (v3.1.5)\n","stream":"stdout","time":"2024-02-16T15:09:24.410990721Z"}
{"log":"\n","stream":"stdout","time":"2024-02-16T15:09:24.411146602Z"}

 

 

Logback 적용 후

Logback을 적용했다면 표준 출력이 아닌 파일 형태로 로그를 남기게 됩니다. 이때 도커 볼륨 설정을 하지 않았다면 로그는 컨테이너 내부에만 남게 됩니다.

 

이를 확인하기 위해 도커 컨테이너 내부로 한번 들어가보겠습니다.

>> docker exec -it be6643c84166 /bin/bash

## 도커 컨테이너 내부
>> tail -20f doonut-support/log/error/error-2024-02-16.log

Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'cloud.aws.cloudfront.prefix' in value "${cloud.aws.cloudfront.prefix}"
	at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:180)
	at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126)
	at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:239)
	at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:210)
	at org.springframework.context.support.PropertySourcesPlaceholderConfigurer.lambda$processProperties$0(PropertySourcesPlaceholderConfigurer.java:200)
	at org.springframework.beans.factory.support.AbstractBeanFactory.resolveEmbeddedValue(AbstractBeanFactory.java:918)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1358)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:764)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:747)
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:145)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:492)
	... 39 common frames omitted

 

 

도커 볼륨

이쯤에서 잠시 도커 이미지와 컨테이너 구조에 대해 짚고 넘어가겠습니다.

 

도커 이미지로 컨테이너를 생성하면 이미지는 읽기 전용이 되며, 컨테이너의 변경 사항만 별도로 저장해서 각 컨테이너의 정보를 보존합니다. 예를 들어, 위에서 생성했던 스프링부트 서버는 이미지를 통해 생성되었지만, 그 안에 남긴 로그 파일은 컨테이너가 갖고 있습니다.

 

도커 이미지와 컨테이너 구조

 

이미지는 어떠한 경우로도 변경되지 않으며, 도커는 컨테이너 계층에 원본 이미지에서 변경된 사항을 저장합니다. 따라서 컨테이너를 삭제하면 컨테이너 계층에 저장된 로그 파일이 모두 삭제됩니다.

 

이를 방지하기 위해 컨테이너의 데이터를 영속적 데이터를 활용할 수 있는 방법이 몇가지 있습니다. 그중 가장 대표적인 방법은 바로 볼륨을 활용하는 것입니다.

 

볼륨을 활용하는 방법에는 3가지가 있습니다. 1) 호스트와 볼륨을 공유할 수도 있고, 2) 볼륨 컨테이너를 활용할 수도 있으며, 3) 도커과 관리하는 볼륨을 생성할 수도 있습니다.

 

이번 글에서는 도커과 관리하는 볼륨을 통해 로그 파일을 영속적으로 남기는 방향으로 진행하겠습니다.

 

 

도커 볼륨을 통한 로그 파일을 host에 남기기

도커에서 제공하는 볼륨 기능을 활용해 로그 파일을 보존할 수 있습니다.

이 방법을 사용하기 위해선 우선  docker volume create  명령어로 볼륨을 생성해야 합니다.

>> docker volume create --name dev-log
dev-log

 

 

 docker volume ls  명령어로 방금 생성한 볼륨을 확인할 수 있습니다.

>> docker volume ls
DRIVER    VOLUME NAME
local     dev-log

 

 

도커 컴포즈를 사용하면 여러개의 컨테이너를 손쉽게 관리할 수 있습니다. docker-compose.yml 파일에 아래와 같이 source에 방금 생성한 볼륨의 이름을 target에는 컨테이너 내 로그 파일 위치를 작성합니다.

version: '3'

services:
  web:
    container_name: production-server
    image: doonutmate/doonut-production-server:latest
    networks:
      - doonut_network
    ports:
      - 8081:8081
    environment:
      - "SPRING_PROFILES_ACTIVE=dev"
    volumes:
      - type: volume
        source: dev-log
        target: /doonut-support/log

volumes:
  dev-log:
    external: true

 

외부에서 생성한 볼륨을 이용하기 위해서는 마지막에  external: true   옵션을 적어주어야 합니다. 이 옵션을 설정하지 않으면 도커 컴포즈는 매번 새로운 볼륨을 생성합니다.

 

도커 볼륨의 위치를 알기 위해선  docker volume inspect   명령어를 사용해야 합니다.

>> docker volume inspect dev-log
[
    {
        "CreatedAt": "2024-02-11T14:03:08Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/dev-log/_data",
        "Name": "dev-log",
        "Options": null,
        "Scope": "local"
    }
]

 

Mountpoint의 위치를 들어가보면 컨테이너 내부에 작성된 로그 파일이 호스트의 파일에 영속적으로 보존된 것을 확인할 수 있습니다.

>> cd /var/lib/docker/volumes/dev-log/_data
>> ls -al
total 0
drwxr-xr-x. 5 root root 43 Feb 11 14:22 .
drwx-----x. 3 root root 19 Feb 11 14:03 ..
drwxr-xr-x. 2 root root 90 Feb 17 07:19 error
drwxr-xr-x. 2 root root 87 Feb 18 00:16 info
drwxr-xr-x. 2 root root 87 Feb 16 14:53 warn

 

 

 

마무리

이번 글에서는 도커 컨테이너 기반으로 스프링부트 서버를 띄웠을 때 로그를 어떻게 영속적으로 보존할지에 대해서 알아보았습니다. 도커 볼륨을 이용해 로그를 보존한다면 컨테이너가 삭제될 때에도 호스트에 남겨진 로그를 통해 로그를 확인할 수 있습니다.

 

참고 문헌