개인 프로젝트에서 EC2 환경에서 Docker에 Spring Boot를 설치할 일이 생겼습니다.
다음에도 WAS를 띄우는 과정을 구글링해서 찾아볼 것 같아서 기록용으로 기록해보고자 합니다!
설치 과정은 도커가 설치되어 있는 EC2 Linux 환경과 Docker Hub 계정이 있는 환경으로 진행되었습니다.
전체 과정을 요약하면 다음과 같습니다.
- Spring Boot Dockerfile 생성
- Docker Image 생성
- Docker Image를 Docker Hub에 업로드
- EC2에서 Docker Hub에 업로드한 Docker Image 다운로드 받아서 WAS 실행
1. Spring Boot Dockerfile 생성
Docker Image를 생성하기 위한 Dockerfile을 생성합니다.
한번 이미지를 만들고 해당 이미지를 EC2에서 도커 컨테이너에 실행해도 되지만,
Docker Image를 생성하는 스크립트 파일인 Dockerfile을 만드는 이유는 서비스의 배포가 잦기 때문입니다.
Dockerfile이 아닌 Docker Image를 사용한다면 서비스가 배포가 될 때 이전 이미지는 사용할 수 없게됩니다.
Dockerfile을 사용한다면 변경된 업데이트 사항을 반영한 WAS 파일으로 Docker Image를 구성하기 때문에
지속되는 서비스 업데이트에 동기화되어 Docker Image를 생성할 수 있습니다.
이론은 여기까지 하고, 프로젝트 최상위 경로에 'Dockerfile' 이름으로 파일을 생성해봅시다.
이런 식으로 IntelliJ 사용 시 Dockerfile 이름을 인식해서 Docker Icon이 나오네요! (귀엽네요 ㅎㅎ)
본격적으로 Dockerfile에 Docker Image 구성 시 사용될 스크립트를 작성해봅시다.
(빌드 툴인 Maven/Gradle에 따라 작성 방법이 다른데, 저는 Gradle을 사용하기 때문에 Gradle 기반으로 작성했습니다.)
# 베이스 이미지 Amazon Corretto 17로 설정 (애플리케이션 Java Vendor)
FROM amazoncorretto:17
# 작업 디렉토리 설정 (원하는 경로로 수정 가능)
WORKDIR /shboard
# 변수 설정
ARG JAR_FILE=./build/libs/*.jar
# 빌드한 JAR 파일 원하는 경로에 복사
COPY ${JAR_FILE} /shboard/shboard.jar
# 어플리케이션 실행 명령어를 지정합니다.
# CMD / ENTRYPOINT
ENTRYPOINT ["nohup", "java", "-jar", "shboard.jar", ">", "app.log", "2>&1", "&"]
여러 명령어의 설명은 주석으로 달아놨습니다.
그 중에 맨 마지막 명령어인 CMD / ENTRYPOINT를 살펴보면,
두 명령어는 모두 애플리케이션 실행 명령어이지만, 다음과 같은 차이가 있습니다.
- CMD : 새로운 컨테이너 실행 시에만 명령어가 실행됩니다. (docker run 명령어로 새로운 컨테이너 실행 시)
- ENTRYPOINT : 새로운 컨테이너/기존 컨테이너 상관없이 컨테이너가 실행될 때 무조건 명령어가 실행됩니다. (docker run / docker start)
저는 WAS의 장애로 인해 도커 환경이 다운되었을 때 WAS 컨테이너를 재시작하는 경우를 고려해보았습니다.
이때, 도커 컨테이너를 다시 시작할 때는 도커 컨테이너를 새롭게 생성하는 docker run이 아닌 기존의 컨테이너를 재실행하는 docker restart를 사용할 것입니다.
따라서, 장애가 발생한 도커 컨테이너를 재실행할 때 애플리케이션도 실행되도록 하기위해서 저는 ENTRYPOINT를 사용했습니다.
이렇게 Dockerfile의 구성을 마치면 됩니다.
※ Gradle build 시 plain jar 생성 X 설정
기본적으로 Spring Boot 2.5 버전 이상에서는 Gradle 빌드 시 일반 jar파일과 plain jar 파일 2개가 생성됩니다.
위의 Dockerfile 설정에서 JAR_FILE 변수를 ./build/libs/*.jar로 잡아서 원치 않는 plain jar 파일으로 애플리케이션이 실행될 수 있으므로
Gradle build 시 plain jar를 생성하지 않는 설정을 bulid.gradle에 추가해줬습니다.
// plain jar 생성 X 설정
jar {
enabled = false
}
2. 생성한 Dockerfile로 Docker Image 생성 후 Docker Hub에 Push
위에서 생성한 Dockerfile을 기반으로 도커 컨테이너를 만들 수 있는 Docker Image를 생성 후에
Docker Hub에 생성한 Docker Image를 Push 해보도록 하겠습니다.
2-1. Docker Hub 로그인
먼저, 최종적으로 Docker Hub에 Docker Image를 Push할 것이기 때문에
생성할 Docker Image의 레포지토리 경로를 Docker Hub로 지정해야합니다.
그러므로 Docker Image를 생성하기 전에 Docker Hub에 로그인해줍시다.
(만약 Docker Hub 계정이 없다면, Docker Hub 홈페이지에서 생성해줍시다.)
docker login -u (username)
해당 명령어 실행 후 패스워드를 입력하면 로그인이 정상적으로 성공합니다.
2-2. Docker Hub Repository 경로로 Docker Image 생성
그 후 docker build 명령어를 통해 Dockerfile 기반으로 Docker Image를 생성할 수 있습니다.
# 기본 커맨드
# docker build -t [dockerHub ID]/[이미지명]:[태그명] [DockerFile위치]
# Dockerfile 위치에서 명령어를 실행하여 Dockerfile 위치를 현재 경로(.)로 지정
# 태그를 지정하지 않으면 'latest' 지정
docker build -t [dockerHub ID]/[이미지명] .
* 명령어 실행 시에 생성했던 Dockerfile 위치를 지정해야 하는데, 명령어 실행 위치를 Dockerfile 위치로 하고 실행하면 위와 같이 현재 경로(.)으로 간단하게 실행할 수 있습니다.
* -t 옵션은 생성되는 Docker Image에 tag를 줄 수 있는 옵션으로, 태그를 생략한다면 latest 태그가 지정됩니다.
위와 같이 docker build 명령어를 실행했다면 Docker Image가 Docker Hub의 레포지토리 경로로 생성되고 Docker Hub에 Push할 준비가 완료되었습니다.
생성 이후 docker images 명령어를 통해 이미지를 조회하면, Docker Hub 레포지토리 경로로
Docker Image가 잘 생성되었음을 확인할 수 있습니다.
2-3. 생성된 Docker Image를 Docker Hub에 Push
docker push 명령어를 통해 생성된 Docker Image를 Docker Hub에 Push 할 수 있습니다.
# tag는 생략 시 latest로 지정
docker push [DockerHub ID]/[image 파일명]:[tag명]
정상적으로 Docker Hub Repository 경로에 push 되었습니다.
Docker Hub에 정상적으로 Push 되었는지 확인하기 위해서는 Docker Desktop or Docker Hub에서 확인할 수 있습니다.
3. EC2에서 Docker Hub의 Image로 Docker Container 실행
docker run 명령어를 통해 Docker Container를 초기 생성, 실행할 수 있습니다.
docker run -d -p [host port]:[container port] --name [container name] [dockerHub ID]/[이미지명]
4. Trouble Shooting
3번까지해서 모든 과정이 끝났지만, 처음 실행했을 때 2가지 오류가 발생했었습니다.
발생했던 오류를 트러블 슈팅하는 과정을 끝으로 포스팅을 마무리하도록 하겠습니다.
4-1. 'exec format error' 오류
마지막 과정까지 완료 후에 docker ps -a로 도커 컨테이너의 상태를 조회했을 때,
실행한 컨테이너의 STATUS가 Exited로 종료 상태가 되는 것을 발견할 수 있었습니다.
그래서 컨테이너의 로그를 확인해봤습니다.
위와 같이 'exec /bin/sh: exec format error'와 같은 에러 로그가 남아있었습니다.
결과적으로 원인은 'CPU 아키텍쳐의 차이'였습니다.
Spring Boot의 Docker Image를 생성한 로컬 환경은 맥북 M1의 ARM 아키텍쳐 환경이었습니다.
그러나 Docker Image를 다운받아서 실행한 EC2의 환경은 x86 아키텍쳐 환경이었습니다.
(EC2를 구성할 때 ARM 아키텍쳐로 변경할 수 있었지만, 기본이 x86이라 x86으로 선택했었습니다.)
이처럼 Docker Image를 생성한 환경과 Docker Image를 실행하여 Docker Container를 생성한 환경이 다르기 때문에
아키텍쳐가 호환되지 않아서 exec format error라는 에러가 발생한 것입니다.
이러한 호환성 차이를 docker buildx 플러그인을 통해 멀티 아키텍쳐 Docker Image를 구성하여 해결할 수 있었습니다.
docker buildx는 멀티 아키텍쳐 빌드 등 다양한 빌드 옵션을 제공하는 CLI 플러그인입니다.
docker 19.03 버전부터 사용할 수 있습니다.
이전 2-2에서는 docker build를 통해 Docker Image를 다음과 같이 생성했었습니다.
# 기본 커맨드
# docker build -t [dockerHub ID]/[이미지명]:[태그명] [DockerFile위치]
# Dockerfile 위치에서 명령어를 실행하여 Dockerfile 위치를 현재 경로(.)로 지정
# 태그를 지정하지 않으면 'latest' 지정
docker build -t [dockerHub ID]/[이미지명] .
docker buildx를 사용하려면 먼저 docker buildx의 빌더를 생성해야 합니다.
$ docker buildx create --name multi-arch-builder --driver docker-container --bootstrap --use
위와 같이 빌더를 생성하고, 다음과 같이 빌더 목록을 확인하면 생성되었음을 알 수 있습니다.
docker buildx ls
빌더가 생성되었으면, docker build 명령어에 추가적인 buildx 명령어를 다음과 같이 사용해주면 됩니다.
# docker buildx build --platform [실행할 아키텍쳐 리스트] -t [dockerHub ID]/[이미지명]:[태그명] [DockerFile위치] [--load or --push]
docker buildx build --platform linux/amd64,linux/arm64 -t seongha111/shboard-was . --push
* --platform 다음에 아키텍쳐 리스트를 작성하여 멀티 아키텍쳐 Docker Image를 구성했습니다.
(linux/amd64 - x86 아키텍쳐, linux/arm64 - ARM 아키텍쳐로 구성했습니다.)
* 맨 마지막의 --load 옵션과 --push 옵션은 buildx의 옵션입니다.
- --load : 이미지를 만들고 호스트 Docker Image에 저장
- --push : Docker Registry(Docker Hub)로 바로 Push
처음에는 로컬에 생성한 Docker Image를 남기기 위해 --load 옵션을 사용하여 명령어를 실행했었습니다.
하지만, 다음과 같은 에러가 추가적으로 발생하며 Docker Image 생성이 되지 않았습니다.
ERROR: docker exporter does not currently support exporting manifest lists
찾아보니 멀티 아키텍쳐로 Docker Image를 생성할 때는 --load 옵션을 사용할 수 없도록 막혀 있었고
무조건 --push로 바로 레포지토리에 push해야 했습니다.
따라서 --push를 사용하여 Docker Hub에 Docker Image 생성과 함께 Push 해줬습니다.
저는 결론적으로 위와 같이 명령어를 작성하여 Docker Image를 생성하여 바로 Docker Hub에 Push 했습니다.
이렇게 docker buildx를 사용하여 멀티 아키텍쳐로 인한 exec format error를 해결할 수 있었습니다.
4-2. Docker Container 실행 시 bash 실행 문자열 관련 오류
해당 오류는 따로 에러 로그가 출력되는 것이 아니라서 제목으로 적어봤습니다.
위의 멀티 아키텍쳐 오류를 해결하고 Docker Container를 실행했을 때 바로 STATUS가 Exited로 종료되는 것을 확인했었습니다.
docker logs로 로그를 출력해도 아무런 에러 로그가 출력되지 않아서 난감했으나
docker ps -a의 COMMAND 부분에서 문자열 escape 관련 오류인 느낌이 강하게 들었습니다.
위의 사진과 같이 쌍따옴표 안에 따옴표가 들어가면서 뭔가 정상적으로 명령어를 실행하지 못해 발생한 문제 같았습니다.
결론적으로 해결은 Dockerfile의 ENTRYPOINT 명령어 부분의 형식을 변경하면서 해결할 수 있었습니다.
# 오류 발생 코드
ENTRYPOINT nohup java -jar shboard.jar > app.log 2>&1 &
# 오류 발생 X인 코드
ENTRYPOINT java -jar shboard.jar
# 해결한 코드
ENTRYPOINT ["nohup", "java", "-jar", "shboard.jar", ">", "app.log", "2>&1", "&"]
Docker 공식 문서에서 ENTRYPOINT 명령어를 볼 때 파라미터를 배열로 주지 않고 한 줄로 적어도 되는 것을 봤기 때문에
처음에 맨 처음 코드와 같이 한 줄로 실행 명령어를 나열해서 작성했었습니다.
두 번째 코드가 오류가 나지 않는 것으로 봐서 실행 명령어를 한 줄로 나열하는 방식은 부분적으로 가능한 것 같았습니다.
한 줄로 나열하게 되면 쌍따옴표 안에 따옴표로 명령어가 들어가서 '>', '&'와 같은 명령어가 무시되어 비정상적으로 동작하는게 아닐까 싶습니다.
결론적으로 맨 마지막 코드처럼 명령어를 배열에 담아서 전달함으로써 해결할 수 있었습니다.
※ Docker 로깅 관련
위의 2번째 에러가 발생한 이유는 애플리케이션에서 출력되는 로그를 app.log 파일로 저장하고자 했기 때문이었습니다.
이러한 로깅은 도커 컨테이너 자체에서 표준 출력과 표준 에러를 로그로 관리하기 때문에 필요없는 행위였습니다,, ㅎㅎ..
docker logs [도커 컨테이너 ID]를 통해 Spring Boot의 로그를 볼 수 있었습니다.
Reference
https://lucas-owner.tistory.com/48
'Infra > Docker' 카테고리의 다른 글
[Docker] Docker in Docker, 도커 안에서 도커 사용하기 (1) | 2024.01.28 |
---|