개인적으로 해보고 싶었던 쿠버네티스를 이용하여 컨테이너 인프라 환경을 구축해보려고합니다.
이전 회사에서 거대한 모놀리식 구조로 운영되던 서비스를 회사 장기 비전인 Java 전환과 MSA 전환을 위해 꼭 필요하다고 졸라서 해보라고 허락을 받고 공부하고 있었는데 결국 퇴사를 하게 되었네요.
기본적인 내용들은 컨테이너 인프라 환경 구축을 위한 쿠버네티스/도커 책을 통해 학습하였고, 추가로 참고한 내용들은 따로 공유드리겠습니다.
컨테이너 인프라 환경을 구축하기 위한 첫 걸음으로 Docker를 이용하여 컨테이너에 배포하는 방법을 알아보겠습니다.
준비
이전에 코프링 찍먹 해보려고 만들었던 프로젝트을 배포하는 실습을 해보겠습니다.
도커 준비
실습을 위해 로컬 환경에 Docker를 설치해야 합니다.
설치하는 방법은 다양한데 공식 문서를 따라서 차분하게 진행하시면 어렵지 않게 수행하실 수 있으므로 관련 링크만 남깁니다.
프로젝트 준비
제가 찍먹을 위해 만들었던 코프링 프로젝트는 스프링 공식 가이드의 Building web applications with Spring Boot and Kotlin입니다.
1
2
| mkdir blog && cd blog
curl https://start.spring.io/starter.zip -d language=kotlin -d type=gradle-project-kotlin -d dependencies=web,mustache,jpa,h2,devtools -d packageName=com.example.blog -d name=Blog -o blog.zip
|
위 명령을 사용하셨다면 Kotlin DSL + Gradle로 빌드할 수 있는 SpringBoot 프로젝트가 아래 구조로 만들어졌을 겁니다.
1
2
3
4
5
6
7
8
9
10
| .
├── Dockerfile
├── HELP.md
├── build
├── build.gradle.kts
├── gradle
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
|
저는 가이드 문서대로 이미 구현을 한 상태라 추가 작업 없이 확인할 수 있지만, 잘 배포 되었는지 확인하기 위한 간단한 REST 컨트롤러를 만들어주겠습니다.
패키지 경로 내에 있다면 어떤 이름으로 만들어도 상관은 없습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
| package com.example.blog
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/hello")
class HelloController {
@GetMapping("/")
fun hello() = "Hello"
}
|
네트워크 환경 준비 (공유기 사용시)
저와 같이 공유기를 사용한다면 외부 네트워크에서 정상 동작을 확인하기 위해 추가 설정이 필요합니다. 테스트를 환경이 공유기를 사용하지 않는다면 넘기셔도 좋습니다.
외부 네트워크를 통해 웹 애플리케이션에 접근하려면 애플리케이션이 배포된 서버의 IP와 웹 애플리케이션이 사용 중인 PORT 정보를 알아야 하는데, 저처럼 공유기를 사용하는 환경에서는 공유기가 IP를 할당 받게 됩니다.
라우터(공유기 등)으로 구성된 내부 네트워크를 서브넷 이라고 하며, 서브넷 자체적으로 내부 IP를 할당하기 때문에 서브넷 내부에서 서로 통신할 수 있습니다.
따라서 공유기에 들어온 요청을 연결된 내부 네트워크를 통해 웹 애플리케이션을 배포한 로컬 환경으로 요청을 전달해야 하는데, 이러한 처리를 포트 포워딩이라고 합니다.
포트 포워딩은 공유기가 받은 요청을 내부적으로 연결된 목적지에 전달하는 것이기 때문에 요청을 받는 공유기에 설정해야 하고, 대부분 공유기가 이러한 기능을 제공하고 있습니다.
현재 연결된 무선 인터넷의 IP 정보가 내부 IP이고, 외부 IP는 mac 기준 아래 명령으로 확인할 수 있습니다.
공유기 관리자 페이지는 내부 IP에서 마지막이 1로 바꿔 접근하면 됩니다. 저는 같은 경우는 http://192.168.0.1/ 이었습니다.

IpTime 공유기는 이런 식으로 나올 텐데, 구성은 대부분 같습니다.
외부 IP, 내부 IP 맞춰 입력하고 외부 포트는 포트 포워딩할 대상 포트 번호를 입력하고, 내부 포트는 로컬 환경에서 배포될 컨테이너의 호스트 포트 번호를 입력하면 됩니다.
외부 포트 같은 경우 http 포트 번호가 80이라서 설정했는데, 8비트 정수값이라면 어떤 값이 들어가도 상관없습니다.
다만 브라우저를 통해 접근할 때 외부_IP:외부_포트
로 접근하시면 됩니다.
컨테이너 빌드
Docker 컨테이너 배포를 위해 배포하고자 하는 프로젝트를 컨테이너 이미지로 빌드해야합니다.
프로젝트 빌드
일반적으로 Docker 컨테이너를 빌드할 때 이미지 크기 및 빌드 시간 최적화를 위해 실행 가능한 애플리케이션만을 이미지에 포함해 빌드합니다.
실습 같은 Spring boot 프로젝트 같은 경우 빌드를 완료한 후 생성되는 실행 파일인 .jar
파일만을 포함하는 것을 의미합니다.
해당 명령을 실행하여 프로젝트를 빌드하면 아래와 같은 구조로 빌드 결과물들이 나옵니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| .
├── classes
│ └── kotlin
├── generated
│ └── source
├── kotlin
│ ├── compileKotlin
│ ├── compileTestKotlin
│ ├── kaptGenerateStubsKotlin
│ └── kaptGenerateStubsTestKotlin
├── libs
│ ├── <프로젝트명>-<build.gradle에 설정한 버전>-plain.jar
│ └── <프로젝트명>-<build.gradle에 설정한 버전>.jar
├── reports
│ └── tests
├── resolvedMainClassName
├── resources
│ └── main
├── snapshot
│ └── kotlin
├── test-results
│ └── test
└── tmp
├── bootJar
├── jar
├── kapt3
└── test
|
Gradle 같은 경우 별도 설정을 하지 않았다면 build/libs
디렉터리에 <프로젝트명>-<build.gradle에 설정한 버전>
형태로 실행 파일을 만듭니다.
Plain JAR는 애플리케이션 코드만을 JAR 파일로 패키징 하는 방식으로 배포되는 파일의 크기가 작지만, 실행 시에 종속성을 관리해야 하는 번거로움이 있습니다.
일반적으로 SpringBoot 애플리케이션을 빌드할 때는 Fat JAR 방식으로 종속성을 내장하여 실행 가능한 형태로 배포하므로 Fat JAR를 컨테이너 이미지에 포함하면 됩니다.
지금은 직접 명령어를 통해서 프로젝트를 빌드하고 있지만, CI/CD 통해 변경된 내용을 반영하고 빌드해서 배포하는 과정을 자동화할 수 있습니다.
Dockerfile 작성
Dockerfile은 Docker 컨테이너 이미지를 빌드하기 위해 필요한 명령과 필요한 환경 변수 등을 기술한 텍스트 파일입니다.
Docker 이미지는 실행 가능한 애플리케이션과 해당 애플리케이션을 실행하는 데 필요한 모든 환경 및 종속성을 포함하는데, Dockerfile을 통해 이미지를 어떻게 구성하고 빌드해야 하는지에 대한 명령을 정의할 수 있습니다.
Dockerfile을 프로젝트 최상위 디렉터리에 만들고 docker build
명령을 실행하면 작성 내용에 맞춰 빌드 작업을 수행하게 됩니다.
그럼 간단하게 Dockerfile을 작성해보겠습니다. 위에서 언급한 것처럼 애플리케이션을 직접 실행할 수 있는 항목들만 컨테이너에 기술해주면 됩니다.
1
2
3
| FROM bellsoft/liberica-runtime-container:jre-17-slim-musl
COPY build/libs/*.jar app/app.jar
ENTRYPOINT ["java", "-jar","/app/app.jar"]
|
Dockerfile을 해석해보면
FROM
: 베이스 이미지를 선택합니다. Ubuntu 기반 Java 17 런타임 환경 이미지인 bellsoft/liberica-runtime-container:jre-17-slim-musl
로 설정하였습니다.- 베이스 이미지는 Docker Hub 에서 찾아볼 수 있고, 저는 jre 17 중 가장 용량이 작은 이미지로 선택했습니다.
- 기본적인 환경이 구성된 이미지는 대부분 Docker Hub에서 찾을 수 있고 해당 이미지의 이미지 레이어도 확인 가능합니다.
COPY
: 호스트 시스템의 디렉터리에서 컨테이너 내부 지정 위치로 복사합니다.- Java 실행파일을 컨테이너 환경 최상위 디렉터리에
app.jar
이름으로 복사하게 됩니다.
ENTRYPOINT
: 컨테이너가 실행될 때 실행할 명령어를 정의합니다.ENTRYPOINT
는 CMD
로 대체할 수도 있는데 명령어가 실행되는 방식이 조금 다릅니다. 이는 추후에 다뤄보겠습니다.
Docker 이미지 빌드
이제 Docker 이미지를 빌드해보겠습니다. 기본 사용법은 아래와 같습니다.
1
| docker build [OPTIONS] PATH | URL | -
|
여러 옵션이 있지만 -t
(--tag
) 옵션을 사용하여 이름과 태그를 설정할 수 있습니다. 이름:태그
형식으로 작성하고, 태그는 생략될 수 있습니다.
1
| docker build -t <이미지 이름:태그> <Dockerfile이 위치한 디렉터리 경로>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| [+] Building 9.0s (8/8) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 281B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/bellsoft/liberica-runtime-container:jre-17-slim-musl 2.6s
=> [auth] bellsoft/liberica-runtime-container:pull token for registry-1.docker.io 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 344B 0.0s
=> [1/2] FROM docker.io/bellsoft/liberica-runtime-container:jre-17-slim-musl@sha256:95ebc03f0f27568a915cc2616a8e9f12e3eb33f4c3197c71606c186 6.1s
=> => resolve docker.io/bellsoft/liberica-runtime-container:jre-17-slim-musl@sha256:95ebc03f0f27568a915cc2616a8e9f12e3eb33f4c3197c71606c186 0.0s
=> => sha256:95ebc03f0f27568a915cc2616a8e9f12e3eb33f4c3197c71606c186e84096969 529B / 529B 0.0s
=> => sha256:216addf7a20c81782208e43c78219ebf492c4ac44dc6ebd2fa4f04b7fe5573c9 1.77kB / 1.77kB 0.0s
=> => sha256:a53839d7740238f431dd368e69ab2fde8df89f7fe7df9fc1f1bb50aa59ccd6a1 39.73MB / 39.73MB 5.1s
=> => extracting sha256:a53839d7740238f431dd368e69ab2fde8df89f7fe7df9fc1f1bb50aa59ccd6a1 0.9s
=> [2/2] COPY build/libs/*.jar app/app.jar 0.1s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:9830437234c832dc3ad45e9833a27e89449889296bdf9ec9091f6680d648d8a8 0.0s
=> => naming to docker.io/library/kt-spring-docker-edit 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
|
저는 kt-spring-docker
로 이름을 설정하고 태그는 설정하지 않았습니다. 명령어를 실행하면 위와 같은 형식으로 빌드 과정을 출력합니다.
결과는 docker images
명령으로 확인할 수 있습니다.
1
2
3
4
| REPOSITORY TAG IMAGE ID CREATED SIZE
...
kt-spring-docker-edit latest 9830437234c8 7 minutes ago 164MB
...
|
컨테이너 배포
컨테이너 배포를 위한 기본 명령어는 아래와 같습니다.
1
| docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
|
다양한 옵션이 존재하지만 그 중 -p
(--publish
) 옵션은 호스트 포트와 컨테이너 포트를 연결하여 컨테이너를 실행할 수 있습니다.
1
| docker run -p <호스트_포트번호:컨테이너_포트번호> <컨테이너 식별자>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.2)
2023-08-30T17:29:41.102Z INFO 1 --- [ main] c.m.k.KotlinTutorialApplicationKt : Starting KotlinTutorialApplicationKt v0.0.1-SNAPSHOT using Java 17.0.8 with PID 1 (/app/app.jar started by root in /)
2023-08-30T17:29:41.124Z INFO 1 --- [ main] c.m.k.KotlinTutorialApplicationKt : No active profile set, falling back to 1 default profile: "default"
2023-08-30T17:29:46.689Z INFO 1 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-08-30T17:29:47.122Z INFO 1 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 375 ms. Found 2 JPA repository interfaces.
2023-08-30T17:29:51.800Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2023-08-30T17:29:51.850Z INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-08-30T17:29:51.851Z INFO 1 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.11]
2023-08-30T17:29:52.320Z INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-08-30T17:29:52.334Z INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 10170 ms
2023-08-30T17:29:53.238Z INFO 1 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2023-08-30T17:29:54.047Z INFO 1 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:71313eab-1da9-46b3-bcd5-df59c1121b0b user=SA
2023-08-30T17:29:54.057Z INFO 1 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2023-08-30T17:29:54.322Z INFO 1 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-08-30T17:29:54.538Z INFO 1 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.2.6.Final
2023-08-30T17:29:54.576Z INFO 1 --- [ main] org.hibernate.cfg.Environment : HHH000406: Using bytecode reflection optimizer
2023-08-30T17:29:55.140Z INFO 1 --- [ main] o.h.b.i.BytecodeProviderInitiator : HHH000021: Bytecode provider name : bytebuddy
2023-08-30T17:29:56.491Z INFO 1 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2023-08-30T17:29:58.111Z INFO 1 --- [ main] o.h.b.i.BytecodeProviderInitiator : HHH000021: Bytecode provider name : bytebuddy
2023-08-30T17:30:01.316Z INFO 1 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-08-30T17:30:01.550Z INFO 1 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-08-30T17:30:04.136Z WARN 1 --- [ 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
2023-08-30T17:30:06.776Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-08-30T17:30:06.861Z INFO 1 --- [ main] c.m.k.KotlinTutorialApplicationKt : Started KotlinTutorialApplicationKt in 28.216 seconds (process running for 31.678)
2023-08-30T17:30:47.268Z INFO 1 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-08-30T17:30:47.272Z INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-08-30T17:30:47.288Z INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 14 ms
|
docker ps
명령을 통해 현재 실행 중인 컨테이너를 확인할 수 있고, 앞서 설명해 드린 외부 IP를 통해 접근해보면 정상적으로 배포된 것을 확인할 수 있습니다.
1
2
3
| docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9d85dbbbf374 kt-spring-docker-edit-2 "java -jar /app/app.…" About an hour ago Up About an hour 0.0.0.0:8080->8080/tcp bold_chaum
|
