1. 서론
빌드 도구인 gradle을 이용해 스프링부트 어플리케이션 build를 성공했다면, 실행 가능한 JAR 파일을 얻었을 것이다. 우리는 이제 이 JAR 파일을 사용해서 WAS가 내장된 스프링부트 어플리케이션에 대한 도커 이미지를 만들고 컨테이너로 구동시킬 것이다. 이전에 우리는 배포 서버로 AWS EC2 인스턴스를 활용하기로 했다. 이전 포스팅들의 흐름대로라면, EC2 인스턴스에는 Docker가 설치된 상태이고, 자신의 로컬 PC에는 빌드된 JAR 파일이 존재하는 상태이다. 도커 컨테이너는 배포 서버인 EC2 인스턴스 안에 띄워야하므로, 현재 EC2 인스턴스 내부에는 활용해야 하는 JAR 파일이 존재하지 않는다는 문제가 발생한다. 이를 해결하기 위한 2가지 방법이 있다.
- Docker hub를 이용한다. : 자신의 로컬 PC에도 docker를 설치하여 프로젝트 경로에서 빌드된 JAR 파일을 이용해 도커 이미지를 만든 뒤, 이를 Docker hub에 push한다. 이후 EC2 인스턴스에 접속하여 docker pull 명령어로 해당 이미지를 다운로드하면 된다.
- JAR 파일 자체를 옮긴다. : 로컬에서 빌드해서 나온 산출물인 JAR 파일을 FTP 프로토콜을 사용해서 EC2 인스턴스 서버에 전송한다. FileZilla와 같은 무료 FTP 클라이언트 소프트웨어를 사용하면 간편하게 파일을 전송할 수 있다.
우리가 Java 어플리케이션 등을 개발할 때 자신만의 로컬 환경에서 먼저 이것저것 테스트하는 것이 편리한 것처럼, Docker 또한 로컬 PC에서도 환경을 구축하여 테스트해보고 이를 이용하는 것이 여러모로 편리하다. 게다가 매번 JAR 파일과 같은 필요한 자원을 EC2 인스턴스에 전송하는 것은 번거로우므로, Docker hub를 이용해서 한 개의 서버에서 작업을 한 뒤 본인이 만든 도커 이미지를 다른 서버에서 활용해보자.
2. Docker Desktop
먼저 우리의 로컬 PC에도 Docker를 설치할 것이다. 자신 PC의 OS가 Windows라면 GUI 환경을 제공하는 공식 프론트엔드인 Docker Desktop 어플리케이션을 설치하는 것을 추천한다. 도커를 Windows에서 자유자재로 활용하기 위해서는 Docker Desktop과 WSL2를 설치해야 한다. 설치 방법은 정리가 잘 돼 있는 해당 블로그를 참조하자.
모두 설치가 완료되었다면, Sign in 버튼을 눌러 도커 계정에 로그인을 하자. 계정이 없다면 회원가입을 하면 된다.
3. Dockerfile 작성
이전 포스팅([Docker] Spring boot App 배포 (3) - Web App 구동 및 빌드)의 내용처럼 gradlew 스크립트 파일을 활용해 빌드를 완료한 상태라면, 스프링부트 프로젝트 경로에 build 디렉토리가 생성됐을 것이다. 이제 build/libs에 존재하는 Executable JAR 파일을 이용해 도커 이미지를 만들기 전, 우리는 이미지를 구성하기 위한 설정 파일인 Dockerfile부터 작성해야 한다.
파일 편집기(필자는 VScode)를 활용해 프로젝트 경로에 Dockerfile이라는 파일명을 가진 파일을 작성해보자.
Dockerfile
FROM openjdk:17
ARG JAR_FILE=build/libs/*-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
- FROM openjdk:17 : 컨테이너가 구동될 때, JAR 파일을 실행하고 Java 프로그램을 구동시켜야하기 때문에 베이스 이미지로 openjdk 17버전을 선택한다.
- ARG JAR_FILE=build/libs/*-SNAPSHOT.jar : 도커 이미지 빌드 시에만 사용될 변수를 지정한다. 현재 프로젝트 경로 기준으로 build/libs 디렉토리에 존재하는 -SNAPSHOT.jar로 끝나는 Executable JAR 파일의 경로를 JAR_FILE 이름으로 저장한다.
- COPY ${JAR_FILE} app.jar : 현재 작업 경로(${JAR_FILE})에 존재하는 파일을 이미지 내부로 복사시킨다. 즉, 해당하는 JAR 파일이 /app.jar의 경로로 이미지 내부에 저장되는 것이다.(경로 생략 시 루트 경로임.)
- ENTRYPOINT ["java","-jar","/app.jar"] : 컨테이너 구동 시 컨테이너 내부에서 실행되는 커맨드이다. 베이스 이미지가 jdk17이므로, java -jar /app.jar의 명령어로 Java 프로그램, 즉 WAS가 내장된 스프링부트 어플리케이션을 구동시킨다.
4. 스프링부트 어플리케이션 이미지 생성
해당 프로젝트 경로에 Dockerfile을 작성했다면, 이제 터미널을 써서 도커 이미지를 빌드해보자.
PS C:\close\shds\urcarcher\urcarcher-be> docker build . -t inthebleakmidwinter/test
[+] Building 15.2s (7/7) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.1s
=> [internal] load metadata for docker.io/library/openjdk:17 1.8s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/2] FROM docker.io/library/openjdk:17@sha256:528707081fdb9562eb819128a9f85ae7fe000e2fbaeaf9f87662e7b3f38cb7 1.3s
=> => resolve docker.io/library/openjdk:17@sha256:528707081fdb9562eb819128a9f85ae7fe000e2fbaeaf9f87662e7b3f38cb7 1.2s
=> => sha256:528707081fdb9562eb819128a9f85ae7fe000e2fbaeaf9f87662e7b3f38cb7d8 1.04kB / 1.04kB 0.0s
=> => sha256:98f0304b3a3b7c12ce641177a99d1f3be56f532473a528fda38d53d519cafb13 954B / 954B 0.0s
=> => sha256:5e28ba2b4cdb3a7c3bd0ee2e635a5f6481682b77eabf8b51a17ea8bfe1c05697 4.45kB / 4.45kB 0.0s
=> [internal] load build context 11.5s
=> => transferring context: 89.72MB 9.7s
=> [2/2] COPY build/libs/*-SNAPSHOT.jar app.jar 0.4s
=> exporting to image 1.3s
=> => exporting layers 1.3s
=> => writing image sha256:b083d22522808c9f597dc9ec7209ee99583dc930d4dab81cacac75fefe4ba5b7 0.0s
=> => naming to docker.io/inthebleakmidwinter/test 0.0s
WARNING: current commit information was not captured by the build: git was not found in the system: exec: "git.exe": executable file not found in %PATH%
What's next:
View a summary of image vulnerabilities and recommendations → docker scout quickview
- docker build <경로> : 명령인자에 명시된 경로에서 Dockerfile 파일명을 가진 파일을 자동으로 인식하고 도커 이미지 빌드 작업을 명령한다. "."은 현재 경로를 의미한다.
- -t(--tag) <이미지명>:<태그> : 이미지의 이름과 태그를 지정한다. 태그를 생략할 경우 default 값으로 latest가 지정된다. 즉 이미지의 이름은 inthebleakmidwinter/test가 된다. 이미지 이름을 의도적으로 "<docker hub id>/<repository name>"와 같은 형식으로 지정했는데 그 이유는 후술한다.
PS C:\close\shds\urcarcher\urcarcher-be> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
inthebleakmidwinter/test latest b083d2252280 2 hours ago 561MB
- docker images 명령어로 현재 로컬에 저장된 도커 이미지를 확인할 수 있다.
Docker Desktop에서도 확인할 수 있다.
5. Docker hub에 push하기
PS C:\close\shds\urcarcher\urcarcher-be> docker push inthebleakmidwinter/test
Using default tag: latest
The push refers to repository [docker.io/inthebleakmidwinter/test]
dc9fa3d8b576: Mounted from inthebleakmidwinter/springdemo
27ee19dc88f2: Mounted from inthebleakmidwinter/springdemo
c8dd97366670: Mounted from inthebleakmidwinter/springdemo
latest: digest: sha256:72d06b9fef9650402d190726a6f735ac7b599a7b038d44f4767f3a1a3b7308d9 size: 1166
- docker push <docker hub id>/<repository name>:<tag> : tag를 생략하고, 이미지 이름을 "<docker hub id>/<repository name>"과 동일하게 맞춘 상태에서 명령어를 입력하면 <docker hub id>가 가지고 있는 <repository name> 이름을 가진 respository에(없을 경우 자동으로 생성됨.) 자동으로 lastest 태그가 달려서 push된다.
Docker hub에서 push된 이미지를 확인할 수 있다.
*Repository는 이미지 저장소이다. <repository name>은 이미지의 이름이라고 생각해도 무방하며, 하나의 이미지 저장소(이미지 이름)는 태그로 구분되는 다수의 이미지를 저장할 수 있다.
6. EC2 인스턴스에 도커 이미지 pull 받기
이제 EC2 인스턴스 서버에 우리의 이미지를 가져오자.
root@ip-xxx-xx-xx-xx:~# docker pull inthebleakmidwinter/test
Using default tag: latest
latest: Pulling from inthebleakmidwinter/test
38a980f2cc8a: Already exists
de849f1cfbe6: Already exists
a7203ca35e75: Already exists
51f8c8951986: Pull complete
Digest: sha256:72d06b9fef9650402d190726a6f735ac7b599a7b038d44f4767f3a1a3b7308d9
Status: Downloaded newer image for inthebleakmidwinter/test:latest
docker.io/inthebleakmidwinter/test:latest
- docker pull <docker hub id>/<repository name>:<tag> : 마찬가지로 태그를 생략할 시 default tag로 latest가 지정되고, <docker hub id>와 <repository name>에 해당하는 이미지를 가져온다.
root@ip-xxx-xx-xx-xx:~# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
inthebleakmidwinter/test latest b083d2252280 7 minutes ago 561MB
docker images로 현재 가진 도커 이미지들을 확인할 수 있다.
7. 도커 컨테이너 구동
어플리케이션의 정보가 담겨 있는 도커 이미지가 준비되었다. 이제 이미지를 토대로 컨테이너를 띄워, EC2 인스턴스 서버에 스프링부트 어플리케이션을 구동시킬 것이다.
root@ip-xxx-xx-xx-xx:~# docker run --name test-container -d -p 7070:8080 --restart unless-stopped inthebleakmidwinter/test
df07a08607a06fe9070a40d03d97b426c53f9452c8133a542c4c19114c64c4af
- docker run <이미지 이름>:<태그> : 해당하는 이미지로부터 만든 새로운 컨테이너를 실행시키는 명령어이다. 여러가지 옵션을 사용해서 컨테이너가 실행되도록 할 수 있다.
- --name : 컨테이너의 이름을 명명한다.
- -d(--detach) : 컨테이너가 백그라운드에서 실행되도록 하는 명령어이다. 입력시 container id를 출력한다.
- -p(--publish) <호스트 포트>:<컨테이너 포트> : 호스트(EC2 인스턴스 서버)와 컨테이너 같의 포트를 매핑시킨다. 즉, 호스트의 특정 포트를 컨테이너의 내부 포트와 연결해서 외부와 컨테이너 간의 통신을 가능하게 한다. 7070이 호스트의 포트고, 8080이 컨테이너의 내부 포트이다. 스프링부트에 내장된 WAS인 톰캣은 default로 8080 포트를 사용하기 때문에 컨테이너 내부에서 작동되고 있는 스프링부트 어플리케이션은 해당 컨테이너의 8080 포트를 사용하고 있는 것이다. 따라서 이를 호스트의 7070 포트와 바인딩시키면, 해당 호스트의 7070 포트로 접근하는 경우는 이 컨테이너의 8080 포트로 접근하는 것과 동치이다.
- --restart : 컨테이너의 재시작 옵션으로, 명령 인자로 no, on-failure, always, unless-stopped를 줄 수 있다.
no : 컨테이너를 자동으로 재시작하지 않는다. default 값이다.
on-failure[:max-retries] : 컨테이너가 정상적으로 종료되지 않은 경우에만 재시작한다.(exit code가 0이 아닌 경우) max-retries를 명시할 경우 최대 시도 횟수를 지정할 수 있다.
always : 컨테이너가 stop되는 모든 경우 항상 재시작한다.(exit code 상관 없음.)
unless-stopped : 컨테이너를 직접 stop 시키는 경우를 제외하고 항상 재시작시킨다.
root@ip-xxx-xx-xx-xx:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
df07a08607a0 inthebleakmidwinter/test "java -jar /app.jar" 2 minutes ago Up 2 minutes 0.0.0.0:7070->8080/tcp, :::7070->8080/tcp test-container
- docker ps : 현재 "구동 중인" 컨테이너 목록을 불러온다. -a(--all) 옵션을 붙이면 중지된 컨테이너까지 불러올 수 있다.
root@ip-xxx-xx-xx-xx:~# docker logs test-container
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.2)
2024-11-27T16:10:13.187Z INFO 1 --- [urcarcher-be] [ main] c.urcarcher.be.UrcarcherBeApplication : Starting UrcarcherBeApplication v0.0.1-SNAPSHOT using Java 17.0.2 with PID 1 (/app.jar started by root in /)
2024-11-27T16:10:13.206Z INFO 1 --- [urcarcher-be] [ main] c.urcarcher.be.UrcarcherBeApplication : No active profile set, falling back to 1 default profile: "default"
2024-11-27T16:10:17.160Z INFO 1 --- [urcarcher-be] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2024-11-27T16:10:17.486Z INFO 1 --- [urcarcher-be] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 304 ms. Found 14 JPA repository interfaces.
2024-11-27T16:10:20.317Z INFO 1 --- [urcarcher-be] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2024-11-27T16:10:20.401Z INFO 1 --- [urcarcher-be] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2024-11-27T16:10:20.402Z INFO 1 --- [urcarcher-be] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.26]
2024-11-27T16:10:20.980Z INFO 1 --- [urcarcher-be] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2024-11-27T16:10:20.982Z INFO 1 --- [urcarcher-be] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 7613 ms
2024-11-27T16:10:23.054Z INFO 1 --- [urcarcher-be] [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2024-11-27T16:10:23.405Z INFO 1 --- [urcarcher-be] [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.5.2.Final
2024-11-27T16:10:23.590Z INFO 1 --- [urcarcher-be] [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2024-11-27T16:10:24.657Z INFO 1 --- [urcarcher-be] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2024-11-27T16:10:24.774Z INFO 1 --- [urcarcher-be] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2024-11-27T16:10:25.204Z INFO 1 --- [urcarcher-be] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.mariadb.jdbc.Connection@768e40af
2024-11-27T16:10:25.207Z INFO 1 --- [urcarcher-be] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2024-11-27T16:10:25.445Z WARN 1 --- [urcarcher-be] [ main] org.hibernate.orm.deprecation : HHH90000025: MariaDBDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)
2024-11-27T16:10:47.952Z INFO 1 --- [urcarcher-be] [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2024-11-27T16:10:51.347Z INFO 1 --- [urcarcher-be] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-11-27T16:11:02.668Z INFO 1 --- [urcarcher-be] [ main] o.s.d.j.r.query.QueryEnhancerFactory : Hibernate is in classpath; If applicable, HQL parser will be used.
2024-11-27T16:11:59.929Z WARN 1 --- [urcarcher-be] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-11-27T16:12:02.281Z INFO 1 --- [urcarcher-be] [ main] r$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name vanillaAuthorizingService
2024-11-27T16:12:40.286Z INFO 1 --- [urcarcher-be] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator'
2024-11-27T16:13:06.023Z INFO 1 --- [urcarcher-be] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2024-11-27T16:13:06.984Z INFO 1 --- [urcarcher-be] [ main] c.urcarcher.be.UrcarcherBeApplication : Started UrcarcherBeApplication in 175.167 seconds (process running for 179.976)
- docker logs <컨테이너 이름> : 해당 컨테이너 내부 콘솔 기록을 불러온다. 정상적으로 작동된 모습이다. 만약 에러가 발생한 경우에도 어떤 에러가 발생했는지 이 명령어로 확인할 수 있다.
8. EC2 인스턴스 포트포워딩
docker run 명령어로 호스트(EC2 인스턴스 서버)의 포트와 컨테이너의 내부 포트를 연결했다는 말은, 외부에서 호스트의 7070 포트로 접근하면 우리가 띄운 스프링부트 어플리케이션으로 접근할 수 있다는 뜻이다. 즉, 배포를 한 것이다!
그러면 이제 브라우저 주소창에 http://<EC2 인스턴스 Public IP>:7070/<EndPoint>를 입력하면 해당 스프링부트 어플리케이션에 http request를 보내고 response를 받을 수 있다.
다만, 여기서 한가지 짚고 넘어가야 할 사항이 있다. 3. AWS EC2 인스턴스 시작에서 인스턴스를 생성할 때, 해당 인스턴스에 대한 네트워크 설정에서 "보안 그룹"을 새로 생성해서 할당했다. 우리는 여기서 인바운드 규칙에 주목해야 한다.
초기 보안그룹 인바운드 규칙에서는 22번 포트만 개방되어 있을 것이다.
우리는 외부에서 호스트의 7070포트로 접근이 가능하도록 해야하기 때문에, 해당 인스턴스의 7070포트를 개방시켜야 한다. 즉, 포트포워딩을 해야한다.
인바운드 규칙 편집에 들어가서 다음과 같이 규칙을 추가하고 저장하면, 인스턴스의 7070 포트가 개방된다. 이제 외부에서 해당 인스턴스의 7070 포트로 접근할 수 있는 것이다!(0.0.0.0/0은 IPv4로부터의 "모든" 접근을 허용한다는 뜻이다.) 다른 포트를 개방할 경우도 마찬가지이다.
이제 포트포워딩까지 완료했으니 외부에서 해당 어플리케이션으로 접근해보자. 전술했듯이, 인스턴스의 Public IP와 개방된 포트 번호로 접근 가능하다.
Public IP, 포트 번호, 그리고 특정 엔드포인트(/actuator/health)를 통해 스프링부트 어플리케이션에 접근해 http response를 받은 모습이다. 이제 외부에서도 접근이 가능하게 되었다. 배포를 성공한 것이다.
이해를 위해 현재 상태를 도식화해보면 다음과 같다.
+ 스프링부트 Actuator
build.gradle
...
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-actuator'
...
}
...
build.gradle에 위와 같은 의존성을 추가하면 스프링부트의 액추에이터를 활용할 수 있게 된다. 액추에이터는 해당 스프링부트 어플리케이션에 다양한 엔드포인트를 활성화시키는데, 그 중 하나가 위에 쓰인 /actuator/health이다. 이 엔드포인트로 요청을 보내면, 현재 어플리케이션의 건강 상태를 제공 받는다.(UP, DOWN 등...) 이를 health check라고 한다. 따라서 이 health 정보를 통해 어플리케이션에 문제가 발생한 경우 이를 빠르게 인지할 수 있다. 이후 Actuator는 Jenkins와 같은 툴을 사용하는 경우 health check에 요긴하게 쓰이므로 배포 시 여러모로 알아두면 좋은 라이브러리이다. 해당 블로그에 Actuator에 대한 자세한 설명이 나와 있다.
References
- https://velog.io/@arnold_99/Docker-Docker-Hub
- https://myanjini.tistory.com/entry/%EC%9C%88%EB%8F%84%EC%9A%B0%EC%97%90-%EB%8F%84%EC%BB%A4-%EB%8D%B0%EC%8A%A4%ED%81%AC%ED%83%91-%EC%84%A4%EC%B9%98
- https://velog.io/@whdghk1908/Docker-Image-Build
- https://www.lainyzine.com/ko/article/how-to-upload-docker-images-with-docker-push/
- https://docs.docker.com/reference/cli/docker/container/run/
- https://kkang-joo.tistory.com/70
- https://velog.io/@zenon8485/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EC%95%A1%EC%B6%94%EC%97%90%EC%9D%B4%ED%84%B0
'Docker' 카테고리의 다른 글
[Docker] Spring boot App 배포 (3) - Web App 구동 및 빌드 (1) | 2024.11.26 |
---|---|
[Docker] Spring boot App 배포 (2) - EC2 기본 + Docker 설치 (1) | 2024.11.23 |
[Docker] Spring boot App 배포 (1) - Docker 기본 (1) | 2024.11.19 |