- 지금까지 REST API 노출하고 컨테이너로 실행되는 PostgreSQL 데이터베이스를 통해 데이터를 저장하는 카탈로그 서비스 애플리케이션을 개발했다.
- 쿠버네티스 클러스터로 배포하기 전에 컨테이너 이미지로 패키징하고 수명주기를 관리하는 법을 알아야 한다.
- 여러 개의 컨테이너로 작업할 때는 도커 CLI가 효율적이지 않기 때문에 도커 컴포즈를 활용해 여러 컨테이너와 컨테이너의 라이프사이클을 관리한다.
1. 도커에서 컨테이너 이미지로 작업하기
- 도커 엔진은 클라이언트/서버 구조를 가지고 있다.
- 도커 CLI는 도커 서버와의 상호작용을 위해 사용하는 클라이언트다.
- 도커 서버는 도커 데몬을 통해 도커의 모든 리소스를 관리한다.
- 서버는 컨테이너 저장소와도 연결해 이미지를 업로드하고 다운로드할 수 있다.
[1] 컨테이너 이미지 이해
- 컨테이너 이미지 : 여러 개의 명령을 순서대로 실행한 결과물로, 각 명령의 실행한 결과로 레이어가 만들어진다.
- 각 이미지는 여러 개의 레이어로 이루어져 있으며, 각 층은 해당 명령어를 실행한 결과 생성된 변경 사항을 나타낸다.
- 이미지는 이 일련의 명령을 실행해 얻은 최종 결과물이고 컨테이너로 실행할 수 있다.
- 이미지는 베이스 이미지를 기반으로 만들거나 아예 처음부터 만들 수도 있다.
- 베이스 이미지를 기반으로 새로운 이미지를 생성하는 것이 가장 일반적이다.
- ex. 우분투 이미지로 부터 시작해 변경 사항 적용하기
- 우분투를 베이스 이미지로 사용한다.
- 자바 런타임 환경을 설치한다.
- java —version 명령을 실행한다.
- 걱 명령어는 하나의 레이어를 생성하고 최종적으로 컨테이너 이미지를 만든다.
- 컨테이너 이미지의 모든 레이어는 읽기 전용이다. 일단 적용되면 더 이상 수정할 수 없다. 무언가를 변경해야 하면 기존의 레이어위에 새로운 레이어를 적용해야 한다.
- 상위에 적용된 변경 사항은 하위 레이어에 영향을 미치지 않는다. → 카피 온 라이트 (copy-on-write : 원본 항목의 복사본을 상위 계층에 만들고 이 복사본을 변경한다.)
- 이미지가 컨테이너로 실행되면 컨테이너 레이어라고 부르는 마지막 한 레이어가 기존의 모든 레이어 위에 자동으로 적용된다. → 유일하게 쓰기가 가능한 레이어 (실행 중 생성되는 데이터를 저장함) → 휘발성이 있음 (컨테이너 삭제 시 전부 삭제됨)
[2] 도커파일을 통한 이미지 생성
- OCI 형식에 따라 도커파일이라고 부르는 특정 파일에 명령어를 순서대로 나열해 컨테이너 이미지를 정의할 수 있다.
# 새 이미지에 대한 베이스 이미지로 우분투 22.04를 지정한다.
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y default-jre
ENTRYPOINT ["java", "--version"]
- ENTRYPOINT : 컨테이너 실행을 위한 진입점 (진입점을 지정하기 않으면 컨테이너가 실행되지 않는다.)
- 가상 머신과 달리 컨테이너는 운영체제를 실행하기 위한 것이 아니라 작업을 실행하기 위한 것이다.
| 명령 | 설명 | 예 |
| FROM | 후속 명령어의 대상이 될 베이스 이미지를 정의한다. 도커파일에서 가장 처음에 와야 하는 명령이다. | FROM ubuntu:22.04 |
| LABEL | 키-값 형식에 따라 이미지에 메타 데이터를 추가한다. LABEL 명령은 여러 번 정의할 수 있다. | LABEL version=”1.2.1” |
| ARG | 사용자가 빌드 시에 전달할 수 있는 변수를 정의한다. ARG 명령은 여러 번 정의할 수 있다. | ARG JAR_FILE |
| RUN | 기존 레이어 위에서 인자로 전달된 명령어를 실행해 새로운 레이어를 생성한다. RUN 명령은 여러 번 정의할 수 있다. | RUN apt-get update && apt-get install -y default-jre |
| COPY | 호스트 파일 시스템의 파일 또는 디렉토리를 컨테이너 내부의 파일 또는 디렉토리로 복사한다. | COPY app-0.0.1.jar app.jar |
| USER | 모든 후속 명령어 및 컨테이너 실행을 위한 사용자를 정의한다. | USER sheldon |
| ENTRYPOINT | 이미지가 컨테이너로 실행될 때 프로그램을 정의한다. 여러 개의 ENTRYPOINT 명령이 있으면 마지막 명령만 고려된다. | ENTRYPOINT ["/bin/bash"] |
| CMD | 실행 중인 컨테이너에 대한 기본 설정을 지정한다. ENTRYPOINT 명령이 정의되어 있으면 그 명령의 인수로 전달되고 그렇지 않으면 실행 파일도 가능하다. 도커 파일에 여러 개의 CMD 명령이 있으면 마지막 명령만 고려된다. | CM[”sleep”, “10] |
$ docker build -t my-java-image:1.0.0 .
$ docker images my-java-image
REPOSITORY TAG IMAGE ID CREATED SIZE
my-java-image 1.0.0 d32c658f145d 14 seconds ago 577MB
- 각 이미지 레이어는 이전 이미지 레이어의 증분이며 도커는 모든 이미지 레이어를 캐시한다.
- 한 레이어를 변경하고 이미지를 다시 빌드하면 해당 레이어와 그 이후의 레이어만 다시 생성된다.
- 레지스트리에 저장된 이미지의 새 버전을 컨테이너로 실행하면 새로운 레이어만 다운로드 받기 때문에 런타임 성능이 향상된다.
- 자주 변경될 가능성이 많을수록 도커 파일의 뒤에 오도록 하는 것이 좋다.
- docker run 명령으로 실행할 수 있는데, 이 명령은 컨테이너를 시작하고 도커 파일에서 진입점으로 지정된 프로세스를 실행한다.
$ docker run --rm my-java-image:1.0.0
openjdk 11.0.27 2025-04-15
OpenJDK Runtime Environment (build 11.0.27+6-post-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 11.0.27+6-post-Ubuntu-0ubuntu122.04, mixed mode)
[3] 깃허브 컨테이너 저장소로 이미지 저장
- 자신의 이미지를 저장하려면 도커 허브 또는 애저 컨테이너 저장소처럼 클라우드 공급 업체가 제공하는 저장소 중 하나를 사용할 수 있다.
- 현재 프로젝트는 깃허브 컨테이너 저장소를 사용할 것이다.
- 깃허브 개인 토큰 생성 : repo 전체, write:package
$ docker login ghcr.io
username : github_name
password : github_token
- 컨테이너 이미지는 OCI 호환 컨테이너 저장소에서 채택한 공통의 명명 규칙을 따르는데, <container_registry>/<namespace>/<name>[:<tag>]: 형식이다.
- 컨테이너 저장소 : 이미지가 저장되는 컨테이너 저장소의 호스트 이름을 의미한다. 도커 엔진은 저장소를 지정하지 않으면 docker.io를 이미지 이름 앞에 추가하기 때문에 깃허브 컨테이너 저장소를 사용할 때는 호스트 이름인 ghcr.io를 명시적으로 지정해야 한다.
- 네임 스페이스 : 도커 허브나 깃허브 컨테이너 저장소를 사용할 때는, 소문자로 된 도커 또는 깃허브의 사용자 이름을 네임 스페이스로 사용한다. 다른 저장소에서는 저장소에 대한 경로일 수도 있다.
- 이름과 태그 : 이미지 이름은 이미지의 모든 버전을 포함하는 저장소를 의미한다. 특정 버전을 선택하기 위한 태그가 그 뒤에 올 수 있고, 태그가 없는 경우에는 기본 설정으로 latest 태그가 사용된다.
- 내가 만든 이미지를 깃허브 컨테이너 저장소에 업로드할 때는 [ghcr.io/<github-username>/<image_name>](http://ghcr.io/<github-username>/<image_name>) 형식을 따른다.
- 이전에 my-java-image:1.0.0 라는 이름으로 이미지를 만들었으므로 컨테이너 저장소에 저장하기 전에 이름을 완전하게 지정해야 한다. docker tag 명령어를 사용한다.
$ docker tag my-java-image:1.0.0 ghcr.io/suji-j/my-java-image:1.0.0
$ docker push ghcr.io/<github_username>/my-java-image:1.0.0 #깃허브 컨테이너 저장소로 푸시한다.

2. 스프링 부트 애플리케이션을 컨테이너 이미지로 패키지
- 카탈로그 서비스를 도커 컨테이너로 실행할 이미지로 만든다.
[1] 스프링 부트의 컨테이너화를 위한 준비
- 스프링 부트 애플리케이션을 컨테이너 이미지로 패키징한다는 것은 고립된 콘텍스트에서 계산 리소스와 네트워크를 가지고 애플리케이션을 실행한다는 것을 의미한다.
- 어떻게 네트워크를 통해 애플리케이션에 도달할 수 있을까?
- 어떻게 다른 컨테이너와 연결해 상호작용할 수 있을까?
(1) 포트 전달을 사용한 애플리케이션 서비스 노출
- 카탈로그 서비스를 컨테이너로 실행할 때 애플리케이션의 서비스를 노출하는 8080 포트를 로컬 컴퓨터의 8080 포트로 매핑했다. → http://localhost:8080 에서 애플리케이션을 사용할 수 있었다. (== 포트 포워딩, 포트 매핑, 포트 공개) → 외부에서 컨테이너화된 애플리케이션을 액세스하기 위해 사용한다.
- 기존 설정 상 컨테이너는 도커 호스트 내부의 격리된 네트워크로 연결한다.
- 자신의 로컬 네트워크에서 컨테이너로 연결하려면 포트 매핑을 명시적으로 설정해야 한다.
- 사용자 — GET http://localhost:8080 → 도커 네트워크 8080 -매핑→ 컨테이너 8080
(2) 서비스 발견을 위한 도커 내장 DNS 서버의 사용
- 카탈로그 서비스 애플리케이션은 jdbc:postgresql:/localhost:5432 URL을 통해 컨테이너로 실행 중인 PostgreSQL 데이터베이스 서버에 액세스할 수 있었는데 포트포워딩 덕분에 그렇게 할 수 있었다.
- 카탈로그 서비스를 컨테이너로 실행한다면 localhost는 더 이상 자신의 로컬 컴퓨터가 아닌 컨테이너 내부를 나타내기 때문에 더 이상 데이터베이스 컨테이너에 연결할 수 없다.
- 도커는 동일한 네트워크의 컨테이너가 호스트 이름이나 IP 주소가 아닌 컨테이너 이름을 사용해 서로를 찾을 수 있도록 해주는 DNS 서버를 내장하고 있다.
- 카탈로그 서비스와 PostgreSQL이 IP 주소 또는 호스트 이름 대신 컨테이너 이름을 사용해 서로 연결할 수 있도록 네트워크를 만든다.
$ docker network create catalog-network
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
8c40f7efa6de catalog-network bridge local
- 다음, PostgreSQL 컨테이너를 시작하면서 방금 만든 카탈로그 네트워크에 연결하도록 지정할 수 있다.
- —net 인수를 사용하면 컨테이너는 인수로 지정된 네트워크에 가입하고 도커 내장 DNS 서버를 사용한다.
$ docker run -d \\
--name polar-postgres \\
--net catalog-network \\
-e POSTGRES_USER=user \\
-e POSTGRES_PASSWORD=password \\
-e POSTGRES_DB=polardb_catalog \\
-p 5432:5432 \\
postgres:14.4
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
68bf49c6a95d postgres:14.4 "docker-entrypoint.s…" 58 seconds ago Up 57 seconds 0.0.0.0:5432->5432/tcp polar-postgres
[2] 도커파일로 스프링 부트 컨테이너화
- 스프링 부트를 사용하면 런타임 환경을 제외하고 애플맄이션이 실행하기 위해 필요한 모든 것을 독립 실행 가능한 JAR로 패키징할 수 있다.
- JAR 아티팩트 외에도 컨테이너 이미지에 필요한 것은 운영체제와 JRE 뿐이므로 컨테이너 이미지를 간단하게 만들 수 있다.
- 로컬에서 사용했던 것과 동일한 OpenJDK 배포판인 이클립스 테무린17을 사용한다.
- 카탈로그 서비스의 JAR 파일을 이미지에 복사해야 한다.
- JRE에서 애플리케이션을 실행하는 명령으로 컨테이너의 진입점을 선언한다.
FROM eclipse-temurin:17 # JRE가 이미 설치되어 있는 이클립스 테무린 배포판 우분투 베이스 이미지
WORKDIR workspace # 현재 작업 폴더를 workspace로 변경
ARG JAR_FILE=build/libs/*.jar # 프로젝트에서 애플리케이션 JAR 파일의 위치를 지정하는 빌드 인수
COPY ${JAR_FILE} catalog-service.jar # 애플리케이션 JAR 파일을 로컬 머신에서 이미지 안으로 복사한다.
ENTRYPOINT ["java", "-jar", "catalog-service.jar"] # 애플리케이션을 실행하기 위한 컨테이너 진입점을 지정한다.
- 카탈로그 서비스 애플리케이션을 JAR 아티팩트로 빌드해야 한다.
$ ./gradlew clean bootJar
- 도커파일 스크립트는 기본적으로 그래들에서 사용하는 경로인 build/lobs/에서 애플리케이션 JAR 파일을 복사하도록 작성됐다. 그래들을 사용하는 경우 JAR_FILE 인수 없이 다음과 같은 명령으로 컨테이너 이미지를 만들 수 있다.
- 버전이 지정되지 않았기 때문에 이미지가 자동으로 latest 태그로 표시된다.
$ docker build -t catalog-service .
$ docker images catalog-service
- docker run 명령에 두 가지 인수를 추가하여 처리할 수 있다.
- -p 9001:9001 : 컨테이너 내부의 9001 포트(카탈로그 서비스가 노출되면)를 로컬 호스트의 9001 포트에 매핑한다.
- —net catalog-network : 카탈로그 서비스 컨테이너를 이전에 만든 카탈로그 네트워크로 연결해 PostgreSQL 컨테이너를 사용할 수 있다.
- 카탈로그 서비스에 대한 spring.datasource.url 속성을 jdbc:postgresql:/localhost:5432/polardb_catalog로 지정했다.
- localhost를 가르키기 때문에 컨테이너 내에서 작동하지 않는다.
- 환경변수를 통해, 동일한 URL에서 localhost를 PostgreSQL 컨테이너 이름인 polar-postgres로 대체해 spring.datasource.url 속성을 덮어써야 한다.
$ docker run -d \\
--name catalog-service \\
--net catalog-network \\
-p 9001:9001 \\
-e SPRING_DATASOURCE_URL=jdbc:postgresql://polar-postgres:5432/polardb_catalog \\
-e SPRING_PROFILES_ACTIVE=testdata \\
-e SPRING_CLOUD_CONFIG_FAILFAST=false \\
catalog-service
[3] 프로덕션을 위한 컨테이너 이미지 빌드
- 앞에서 만든 이미지를 개선하는 방법을 알아본다.
(1) 성능
- 컨테이너 이미지를 만들 때 빌드할 때와 런타임의 성능을 고려해야 한다.
- OCI 이미지의 특징인 계층화된 아키텍처를 통해 이미지를 만들 때 변경되지 않은 레이어를 캐싱하고 재사용할 수 있다.
- 카탈로그 서비스 독립 실행형 JAR 파일을 이미지의 레이어로 복사했다. 결과적으로 애플리케이션에서 무언가를 변경할 때마다 전체 레이어를 다시 작성해야 한다.
- ex. 애플리케이션에서 새 REST 엔드 포인트를 추가하는 경우 : 스프링 라이브러리와 의존성은 변경되지 않고 오직 애플리케이션 코드만 변경되지만 모든 것이 함께 패키징되어 있기 때문에 전체 레이블을 다시 빌드해야 한다. → 스프링 부트를 통해 개선할 수 있다.
- 우버 JAR을 컨테이너 이미지에 넣는 것은 효율적이지 않다. JAR 아티팩트는 애플리케이션에서 사용되는 모든 의존성, 클래스 및 자원을 포함하고 있는 압축 아카이브이다. 이 모든 파일은 JAR 내에서 폴더의 계층 구조로 구성된다. 표준 JAR 아티팩트를 확장해 각 폴더를 다른 컨테이너 이미지 수준으로 배치할 수 있다.
- 계층화된 JAR 모드를 사용해 패키징된 애플리케이션은 컨테이너 이미지와 유사하게 여러 레이어로 만들어진다.
- 새로운 JAR 패키지를 사용할 때, JAR 아티팩트를 확장한 다음 각 JAR 레이어에 대해 다른 이미지 레이어를 만들 수 있다. 여기서 목표는 자주 변경되는 클래스를 자주 변경되지 않는 프로젝트 의존성 라이브러리와는 분리해 다른 계층에 두는 것이다.
- 기본적으로 스프링 부트 애플리케이션은 다음과 같은 계층으로 이루어진 JAR 아티팩트로 패키징된다.
- 의존성 계층 : 프로젝트에 추가된 모든 주요 의존성
- 스프링 부트 로더 계층 : 스프링 부트 로더 컴포넌트가 사용하는 클래스
- 스냅숏 의존성 계층 : 모든 스냅숏 의존성
- 애플리케이션 계층 : 애플리케이션 클래스 및 리소스
- 기존 애플리케이션에 새로운 REST 엔드 포인트를 추가하는 시나리오라면, 애플리케이션을 컨테이너화 할 때 application 계층만 작성하면 된다.
- 도커 파일을 수정한다.
- JAR 파일을 이미지로 복사하고 네 개의 계층으로 확장하기 위한 준비 작업을 수행한다.
- 원래의 JAR 파일은 이미지에 있으면 안 된다.
- 다단계 빌드 : 첫 번째 단계의 결과는 폐기 처분되고, 두 번째 단계에서 최종 컨테이너 이미지가 생성된다.
FROM eclipse-temurin:17 AS builder # 첫 번째 단계를 위한 OpenJDK 베이스 이미지
WORKDIR workspace
ARG JAR_FILE=build/libs/*.jar # 프로젝트 내에서 애플리케이션 JAR 파일의 위치를 지정하는 빌드 인수
COPY ${JAR_FILE} catalog-service.jar # 애플리케이션 JAR 파일을 로컬 머신에서 이미지의 'workspace' 폴더로 복사
RUN java -Djarmode=layertools -jar catalog-service.jar extract # 계층 JAR 모드를 적용해 아카이브에서 계층을 추출한다.
FROM eclipse-temurin:17 # 두 번째 단계를 위한 OpenJDK 베이스 이미지
WORKDIR workspace
COPY --from=builder workspace/dependencies/ ./ # 첫 번째 단계에서 추출한 JAR 계층을 두 번째 단계로 복사한다.
COPY --from=builder workspace/spring-boot-loader/ ./
COPY --from=builder workspace/snapshot-dependencies/ ./
COPY --from=builder workspace/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] # 스프링 부트 런처를 사용해 우버 JAR 대신 계층으로 애플리케이션을 시작한다.
(2) 보안
- 컨테이너는 기본적으로 루트 사용자로 실행되므로 잠재적으로 도커 호스트에 루트 액세스를 허용한다.
- 최소 권한의 원칙에 따라 필요 이상의 권한을 갖지 않는 사용자를 만들어 이 사용자가 도커 파일에 정의된 진입점 프로세스를 실행하게 하면 위험을 완화할 수 있다.
FROM eclipse-temurin:17 AS builder
WORKDIR workspace
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} catalog-service.jar
RUN java -Djarmode=layertools -jar catalog-service.jar extract
FROM eclipse-temurin:17
RUN useradd spring # spring 이라는 이름의 유저를 만든다.
USER spring # spring을 현재 유저로 설정한다.
WORKDIR workspace
COPY --from=builder workspace/dependencies/ ./
COPY --from=builder workspace/spring-boot-loader/ ./
COPY --from=builder workspace/snapshot-dependencies/ ./
COPY --from=builder workspace/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
- 시크릿을 컨테이너 이미지에 저장하면 안 된다.
- 도커 파일에서 최신 베이스 이미지와 라이브러리를 사용하는 것도 중요하다.
$ docker build -t catalog-service .
$ grype catalog-service
(3) 도커파일 대 빌드팩
- 도커파일은 매우 강력하고 결과를 완벽하게 제어할 수 있다. 그러나 관리 및 유지보수에 추가적인 노력이 필요하다.
- 클라우드 네이티브 빌드팩은 일관성, 보안, 성능 및 거버넌스에 중점을 둔 다른 접근법을 제공한다.
- 개발자는 도커파일을 작성하지 않고도 애플리케이션 소스 코드에서 프로덕션 환경에 적합한 OCI 이미지를 자동으로 생성할 수 있다.
[4] 클라우드 네이티브 빌드팩을 이용한 스프링 부트 컨테이너화
- 클라우드 네이티브 빌드팩 : CNCF가 ‘어느 클라우드에서나 실행할 수 있는 이미지로 애플리케이션 소스 코드를 변환’하기 위해 유지 관리하는 프로젝트
- 빌드팩이 제공하는 기능
- 애플리케이션 유형을 자동으로 감지해 도커파일 없이 패키지를 생성한다.
- 다양한 언어와 플랫폼을 지원한다.
- 캐시와 레이어를 통해 높은 성능을 갖는다.
- 재현 가능한 빌드를 보장한다.
- 보안 측면에서 모법 사례를 사용한다.
- 프로덕션 환경에 적합한 이미지를 만든다.
- 그랄VM을 사용해 네이티브 이미지의 빌드를 지원한다.
- 컨테이너 생성 프로세스는 애플리케이션을 컨테이너화하는 방법에 대한 모든 정보를 가지고 있는 빌더 이미지에 의해 조정된다.
- 이러한 정보는 애플리케이션의 특정 측면을 전담하는 빌드팩을 통해 제공된다.
- 패키토 빌더 컴포넌트는 일련의 기본 빌드팩 시퀀스를 사용해 실제 빌드 작업을 수행한다.
- 스프링 부트 플러그인에서 제공하는 빌드팩 통합은 카탈로그 서비스 프로젝트의 build.gradle 파일에서 설정할 수 있다.
bootBuildImage { // 빌드팩을 사용해 OCI 이미지를 빌드하기 위한 스프링 부트 플러그인 작업
imageName = "${project.name}" // 빌드할 이미지의 이름, 프로젝트 설정에서 정의한 이름과 암묵적인 latest 태그를 사용한다.
environment = ["BP_JVM_VERSION" : "17.*"] // 이미지에 설치할 JVM 버전
}
$ ./gradlew bootBuildImage
- 카탈로그 서비스를 컨테이너로 다시 실행한다.
$ docker run -d \\
--name polar-postgres \\
--net catalog-network \\
-e POSTGRES_USER=user \\
-e POSTGRES_PASSWORD=password \\
-e POSTGRES_DB=polardb_catalog \\
-p 5432:5432 \\
postgres:14.4
$ docker run -d \\
--name catalog-service \\
--net catalog-network \\
-p 9001:9001 \\
-e SPRING_DATASOURCE_URL=jdbc:postgresql://polar-postgres:5432/polardb_catalog \\
-e SPRING_PROFILES_ACTIVE=testdata \\
-e SPRING_CLOUD_CONFIG_FAILFAST=false \\
catalog-service
- 브라우저 창을 열고 http://localhost:9001/books 로 애플리케이션을 호출하고 제대로 작동하는지 확인한다.
$ docker rm -f catalog-service polar-postgres
$ docker network rm catalog-network
- 스프리 부트 2.4 이후로는 스프링 부트 플러그인 설정을 통해 이미지를 컨테이너 저장소로 직접 저장할 수도 있다. → 특정 컨테이너 저장소로 인증하기 위한 설정을 build.gradle 파일에 추가한다.
bootBuildImage {
imageName = "ghcr.io/<github_username>/catalog-service:latest"
environment = ["BP_JVM_VERSION" : "17.*"]
docker { # 컨테이너 저장소 연결을 설정하기 위한 섹션
publishRegistry { # 컨테이너 저장소 인증을 설정하기 위한 섹션, 값은 그래들 속성을 통해 전달된다.
username = project.findProperty("registryUsername")
password = project.findProperty("registryToken")
url = project.findProperty("registryUrl")
}
}
}
- 스프링 부트 플러그인을 사용하면 암호를 통해 저장소로 인증할 수 있지만 암호 대신 토큰을 사용해야 한다.
- bootBuildImage 작업을 실행해 새로운 이미지를 만들고 저장할 수 있다.
- —imageName 인수 : 컨테이너 저장소가 요구하는 대로 완전한 형식의 이미지 이름을 정의할 수 있다.
- —publishImage 인수 : 스프링 부트 플러그인이 컨테이너 저장소에 이미지를 직접 푸시하도록 지시할 수 있다.
$ ./gradlew bootBuildImage \\
--publishImage \\
-PregistryUrl=ghcr.io \\
-PregistryUsername=suji-j \\
-PregistryToken=<github-token>
- 깃허브 → 프로필 페이지 → 패키지 섹션 → catalog-service 항목 생성됨
- 아직 catlog-service 패키지는 소스 코드 저장소에 연결되지 않았다.
- 지금은 패키지를 삭제한다. 깃허브 액션을 사용해 이미지를 저장할 때 충돌이 일어난다.
3. 도커 컴포즈를 통한 스프링 부트 컨테이너의 관리
- 도커 컴포즈를 사용하면 시스템을 구성하는 모든 애플리케이션과 서비스를 한 곳에서 정의하고 라이프사이클을 함께 관리할 수 있다.
[1] 도커 컴포즈를 통한 컨테이너 라이플사이클 관리
- docker-compose.yml 파일의 루트 섹션
- version : 사용할 구문을 지정
- services : 실행할 모든 컨테이너에 대한 사양을 지정
- polar-deplyment 저장소 새로 만든다. → docker 폴더도 생성한다. → docker-compose.yml 파일 생성
version: "3.8" # 도커 컴포즈 구문 버전
services: # 실행할 모든 컨테이너를 나열하는 섹션
catalog-service: # catalog-service 컨테이너를 기술하는 섹션
depends_on:
- polar-postgres
image: "catalog-service" # 컨테이너를 실행하는 데 사용할 이미지
container_name: "catalog-service" # 컨테이너의 이름
ports: # 포트 매핑 목록을 위한 섹션
- 9001:9001
environment: # 환경 변수를 나열하는 섹션
- BPL_JVM_THREAD_COUNT=50 # 메모리 계싼을 위한 스레드의 수를 설정하는 패키토 빌드팩 환경 변수
- SPRING_DATASOURCE_URL=jdbc:postgresql://polar-postgres:5432/polardb_catalog
- SPRING_PROFILES_ACTIVE=testdata # testdata 프로파일을 활성화
polar-postgres: # poalr-postgres 컨테이너를 기술하는 섹션
image: "postgres:14.12"
container_name: "polar-postgres"
ports:
- 5432:5432
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=polardb_catalog
- 도커 컴포즈는 두 개의 컨테이너를 기본 설정상 같은 네트워크에 연결하기 때문에 이전처럼 명시적으로 네트워크를 지정할 필요가 없다.
$ docker compose up -d
- http://localhost:9001/books 로 작동 확인
[2] 스프링 부트 컨테이너 디버깅
- IDE에서 스프링 부트 애플리케이션을 표준 자바로 실행할 때 디버그 모드로 실행하도록 지정할 수 있다.
- 그러면 IDE는 애플리케이션을 실행하는 로컬 자바 프로세스에 디버거를 부착한다.
- 그러나 컨테이너 내에서 실행하면 프로세스가 로컬 컴퓨터에서 실행되지 않기 때문에 IDE가 더 이상 그렇게 할 수 없다.
- 컨테이너에서 실행되는 스프링 부트 애플리케이션을 로컬로 실행할 때와 거의 동일하기 디버깅할 수 있다.
- 먼저 컨테이너 내부의 JVM에게 특정 포트를 통해 디버그 연결을 듣도록 지시해야 한다. 패키토 빌드팩이 생성한 컨테이너 이미지는 디버그 모드에서 애플리케이션을 실행하기 위한 전용 환경 변수를 지원한다.
- 그 다음 디버그 포트를 컨테이너 외부로 노출하면 IDE가 그 포트를 통해 연결할 수 있다.
version: "3.8"
services:
catalog-service:
depends_on:
- polar-postgres
image: "catalog-service"
container_name: "catalog-service"
ports:
- 9001:9001
- 8001:8001 # JVM이 디버그 연결을 듣는 포트
environment:
- BPL_JVM_THREAD_COUNT=50
- BPL_DEBUG_ENABLED=true # 디버그 연결을 수확하기 위한 JVM 설정을 활성화
- BPL_DEBUG_PORT=8801 # 디버그 연결은 8001 포트를 통해 받는다.
- SPRING_DATASOURCE_URL=jdbc:postgresql://polar-postgres:5432/polardb_catalog
- SPRING_PROFILES_ACTIVE=testdata
$ docker compose up -d
- 도커 컴포즈는 PostgreSQL 컨테이너의 설정이 변경되지 않았다는 것을 인식하고 PostgreSQL 컨테이너에 대해서는 아무 일도 하지 않는다. 반면에 카탈로그 서비스 컨테이너는 새로운 설정으로 다시 로드한다.
$ docker compose down
4. 배포 파이프라인 : 패키지 및 등록
- 커밋 단계 : 개발자가 새로운 코드를 메인 브랜치에 커밋하면 빌드, 단위 테스트, 통합 테스트, 정적 코드 분석, 패키지 같은 여러 과정을 거치게 된다.
- 이 단계의 마지막에 실행 가능한 애플리케이션 아티팩트를 아티팩트 저장소로 등록한다. → 이 아티팩트가 릴리스 후보다.
[1] 커밋 단계에서 릴리스 후보 빌드
- 실행 가능한 아티팩트는 컨테이너 저장소에 등록될 컨테이너 이미지다.
- 아티팩트는 단 한 번만 생성한다는 것이 지속적 전달의 핵심적인 아이디어다.
- 커밋 단계의 마지막 과정에서 배포 파이프라인은 다음 단계에서 사용할 컨테이너 이미지를 생성한다.
- 개발자 푸시 - 소스 코드 체크 아웃 - 소스 코드 취약성 스캔 - 빌드 - 단위 테스트, 통합 테스트 - 컨테이너 이미지로 패키지 - OCI 이미지 취약성 스캔 - 컨테이너 이미지 등록 - 컨테이너 저장소
[2] 깃허브 액션을 통한 컨테이너 이미지 등록
- 깃허브 액션은 깃허브 저장소에서 직접 소프트웨어 워크플로를 자동화하는 데 사용할 수 있는 엔진이다.
- 워크플로 정의는 깃허브의 저장소 루트 아래의 .github/workflow/ 디렉터리에 저장된다.
- catalog-service의 commit-stage.yml을 열고 컨테이너 이미지를 만들 때 필요한 환경 변수를 정의한다.
env:
REGISTRY: ghcr.io # 깃허브 컨테이너 저장소를 사용한다.
IMAGE_NAME: suji-j/catalog-service # 이미지의 이름
VERSION: latest # 새 이미지를 latest로 태깅한다.
- 카탈로그 서비스를 컨테이너 이미지로 패키징하기 위해 로컬에서 사용했던 것과 동일한 전략인 스프링 부트 그래들 플러그인에서 제공하는 빌드팩 통합을 사용한다.
package: # 잡의 고유 식별자
name: Package and Publish
if: ${{ github.ref == 'refs/heads/main' }} # 잡을 main 브랜치에 대해서만 실행한다.
needs: [ build ] # build 잡이 성공적으로 수행된 경우에만 이 잡을 실행한다.
runs-on: ubuntu-22.04 # 우분투 22.04에서 잡을 실행
permissions:
contents: read # 현재 깃 저장소를 체크아웃하기 위한 권한
packages: write # 깃허브 컨테이너 저장소로 이미지를 업로드하기 위한 권한
security-events: write # 깃허브로 보안 이벤트를 제출하기 위한 권한
steps:
- name: Checkout source code
uses: actions/checkout@v3 # 현재 깃 저장소(catalog-service)를 체크아웃한다.
- name: Set up JDK
uses: actions/setup-java@v3 # 자바 런타임을 설치하고 설정한다.
with:
distribution: temurin
java-version: 17
cache: gradle
- name: Build container image
run: |
chmod +x gradlew
./gradlew bootBuildImage \\ # 컨테이너 이미지를 빌드하고 릴리스 후보를 위한 이름을 정의하기 위해 스프링 부트의 빌드팩 통합을 사용한다.
--imageName ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
- 애플리케이션을 컨테이너 이미지로 패키징한 후에는, 그라이프를 사용해 취약점을 스캔하고 깃허브에 보고서를 제출하기 위한 세부 단계를 추가한다.
- 마지막으로 컨테이너 저장소로 인증하고 릴리스 후보 이미지를 푸시한다.
- name: OCI image vulnerability scanning
uses: anchore/scan-action@v3 # 취약성 검사를 위해 그라이프를 사용해 릴리스 후보 이미지를 스캔한다.
id: scan
with: # 스캔할 이미지는 릴리스 후보다.
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
fail-build: false # 이미지에서 취약점이 발견되더라도 빌드를 실패로 만들지 않는다.
severity-cutoff: high
- name: Upload vulnerability report
uses: github/codeql-action/upload-sarif@v3 # 깃허브로 보안 취약성 리포트를 업로드한다.
if: success() || failure()
with:
sarif_file: ${{ steps.scan.outputs.sarif }}
- name: Log into container registry
uses: docker/login-action@v3 # 깃허브 컨테이너 저장소와 인증한다.
with:
registry: ${{ env.REGISTRY }} # 저장소 정보는 환경 변수로 정의된다.
username: ${{ github.actor }} # 깃허브 액션이 제공하는 현재 사용자의 깃허브 유저명
password: ${{ secrets.GITHUB_TOKEN }} # 저장소와 인증하기 위해 필요한 토큰
- name: Publish container image # 릴리스 후보를 저장소로 푸시한다.
run: docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
- 워크플로 실행을 완료하면 packages가 생성된다.

- 지금까지 REST API를 노출시키고 관계형 데이터베이스와 상호작용하는 스프링 부트 애플리케이션을 만들었고, 애플리케이션에 대한 단위 및 통합 테스트를 작성해봤다.
- 플라이웨이로 데이터베이스 스키마를 처리하게 함으로써 프로덕션 환경에 대한 준비를 끝냈다.
- 마지막으로 컨테이너 내에서 모든 것을 실행하고 이미지 생성, 도커, 클라우드 네이티브 빌드팩 및 취약성 검색을 구현했다.
- 다음 편에선, 쿠버네티스를 좀 더 자세히 공부하겠다!
반응형
'Programming' 카테고리의 다른 글
| [클라우드 네이티브 스프링 인 액션] 3-1. 리액티브 스프링: 복원력과 확장성 (0) | 2025.07.07 |
|---|---|
| [클라우드 네이티브 스프링 인 액션] 2-5. 스프링 부트를 위한 쿠버네티스 기초 (2) | 2025.07.02 |
| [클라우드 네이티브 스프링 인 액션] 2-3. 클라우드에서 데이터 저장과 관리 (4) | 2025.06.25 |
| [클라우드 네이티브 스프링 인 액션] 2-2. 외부화 설정 관리 (6) | 2025.06.19 |
| [클라우드 네이티브 스프링 인 액션] 2-1. 클라우드 네이티브 개발 (6) | 2025.06.13 |