Docker로 Spring Boot를 어디든 그대로 옮기기

2026-05-02AWS SAA-C03 스터디

Spring Boot 3 핵심 정리 시리즈 15편. Docker로 애플리케이션을 표준 컨테이너에 담아 어디든 똑같이 옮기는 법, Compose로 여러 컨테이너를 한 번에 관리하는 법, 그리고 Kubernetes로 컨테이너 항만을 자동 운영하는 법까지 — Spring Boot Maven 플러그인의 자동 이미지 빌드, Actuator와 K8s Probe 연동까지 짐을 표준 컨테이너에 담는 비유로 친절하게 풀어쓴 15편.

📚 Spring Boot 3 핵심 정리 · 15편 / 14편 — Docker로 Spring Boot를 어디든 그대로 옮기기

이 글은 Spring Boot 3 핵심 정리 시리즈의 15편입니다. 14편까지 따라오셨다면 이미 캐싱과 이벤트로 단일 애플리케이션의 성능과 구조를 다듬는 단계예요. 이번 15편에서는 그 애플리케이션을 어떻게 다른 환경으로 옮기고, 여러 인스턴스로 띄우고, 자동으로 운영할 것인가 — 즉 배포의 영역을 다룹니다.

핵심 도구는 세 가지 — Docker, Docker Compose, Kubernetes(K8s) 입니다. 이 셋의 관계는 단순해요. Docker는 애플리케이션 한 개를 표준 컨테이너에 담는 도구, Compose는 컨테이너 여러 개를 한 번에 묶는 도구, Kubernetes는 그 컨테이너들을 여러 서버에 걸쳐 자동으로 배포·확장·복구하는 항만 관리자입니다.

왜 컨테이너 배포가 처음엔 어렵게 느껴질까요

이유는 세 가지예요.

첫째, "내 로컬에서는 잘 되는데 서버에서는 안 된다" 는 고질병이 사라지지 않습니다. OS 버전, JVM 버전, 환경 변수, 포트 충돌 — 어디서 무엇이 다른지 일일이 추적하기 어려워요. 컨테이너는 이 문제 자체를 사라지게 하는 도구인데, 처음엔 "왜 이렇게까지 해야 하나" 싶을 수 있습니다.

둘째, 이미지(Image)와 컨테이너(Container)의 차이가 헷갈려요. "이미지를 다운받고 컨테이너로 실행한다"는 문장이 처음엔 잘 안 들어옵니다. 이름도 비슷비슷하고, 둘 다 "그 안에 애플리케이션이 들어 있다"는 식으로 설명되니까요.

셋째, Kubernetes는 추상도가 한 단계 더 올라가요. Pod·Deployment·Service·Ingress·ConfigMap·Secret — 명사가 너무 많고, 이게 왜 다 따로 있어야 하는지가 안 보입니다. 한 번에 다 보면 어지러워요.

해결법은 한 가지 비유예요. 컨테이너는 "표준 화물 컨테이너", Kubernetes는 "컨테이너 항만 관리자" 입니다. 화물선 시대에 컨테이너 표준이 만들어지면서 어느 항구·어느 트럭·어느 기차에서도 똑같이 옮길 수 있게 된 그 혁명을 소프트웨어 배포에 그대로 적용한 거예요. 이 비유 한 가지만 잡고 가면 흐름이 자연스럽게 따라옵니다.

Docker — 표준 컨테이너 비유

회사에서 거대한 짐을 외국으로 보낸다고 해 봅시다. 짐의 크기와 모양이 다 제각각이라면 트럭·배·기차마다 매번 다시 포장해야 하고, 받는 쪽에서도 매번 다른 방식으로 푸는 절차를 거쳐야 해요. 그런데 모든 짐을 표준 화물 컨테이너에 담아 보내면, 운반 수단이 무엇이든 컨테이너 단위로 그대로 옮길 수 있습니다.

Docker가 정확히 그 일을 합니다. 애플리케이션과 그 실행 환경(OS 라이브러리, JVM, 설정 파일) 전부를 하나의 컨테이너 이미지로 묶어서, 어떤 호스트에서도 동일하게 실행되도록 보장해요. "내 로컬에서는 잘 되는데 서버에서는 안 된다"는 문제가 근본적으로 사라집니다.

현대 클라우드 네이티브 환경에서 Docker는 사실상의 표준(de facto standard) 이 됐어요. 그리고 Spring Boot는 한 발 더 나아가, Maven/Gradle 플러그인으로 Dockerfile 없이도 최적화된 Docker 이미지를 자동 생성하는 기능을 내장하고 있습니다.

이미지 vs 컨테이너 — 설계도와 그 설계도로 지은 집

Docker를 처음 배울 때 가장 많이 헷갈리는 게 이미지(Image)와 컨테이너(Container)의 차이예요. 두 단어를 단순한 비유로 풀어 봅시다.

  • 이미지 = 설계도(blueprint) — 컨테이너를 만들기 위한 읽기 전용 템플릿이에요. 애플리케이션 코드, 런타임, 라이브러리, 설정 파일이 모두 포함된 불변의 스냅샷입니다.
  • 컨테이너 = 그 설계도로 지은 집 — 이미지를 기반으로 실제로 실행되는 인스턴스예요. 격리된 환경에서 동작하며, 호스트 커널을 공유하니 가상 머신보다 훨씬 가볍고 빠르게 시작됩니다.

같은 설계도(이미지)로 여러 채의 집(컨테이너)을 동시에 지을 수 있어요. 이게 컨테이너의 강력함입니다.

이미지 (Image)
  ├── Layer 1: Base OS (Ubuntu/Alpine)
  ├── Layer 2: JRE 17
  ├── Layer 3: Application JAR
  └── Layer 4: 설정 파일

컨테이너 (Container) = 이미지 + 실행 레이어(쓰기 가능)

여기서 시험 함정이 하나 있어요. 컨테이너 안에서 만든 데이터는 컨테이너가 삭제되면 사라집니다. 영속화하고 싶다면 Docker Volume이나 외부 저장소(데이터베이스 컨테이너 등)를 써야 해요. 이걸 모르고 컨테이너 안에 SQLite 파일을 두면 재시작 한 번에 데이터가 통째로 날아갑니다.

Docker 핵심 용어 정리

용어역할
Docker Engine컨테이너를 실행하는 런타임
Docker Hub공개 이미지 저장소 (docker.io)
Docker Registry이미지를 저장·배포하는 저장소 (공개·사설)
Docker Network컨테이너 간 통신을 위한 가상 네트워크
Docker Volume컨테이너 외부에 데이터를 영속 저장하는 공간

이 다섯 가지만 머리에 들어가면 Docker 명령어를 따라가는 데 큰 지장이 없습니다.

Spring Boot Docker 이미지 빌드 — Maven 플러그인이 정답

Spring Boot 2.3 이후로는 Dockerfile을 직접 작성하지 않아도 Maven/Gradle 플러그인으로 최적화된 OCI 이미지를 자동 생성할 수 있어요. 내부적으로 Cloud Native Buildpacks를 사용하니, 보안 패치도 자동으로 받아갑니다.

<!-- pom.xml — spring-boot-maven-plugin 설정 -->
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <!-- 이미지 이름 커스터마이징 -->
            <name>myrepo/${project.artifactId}:${project.version}</name>
            <!-- 빌드팩으로 생성되는 환경 변수 설정 -->
            <env>
                <BP_JVM_VERSION>17</BP_JVM_VERSION>
            </env>
        </image>
        <!-- Docker Hub 등에 바로 푸시할 경우 -->
        <docker>
            <publishRegistry>
                <username>${docker.username}</username>
                <password>${docker.password}</password>
                <url>https://index.docker.io/v1/</url>
            </publishRegistry>
        </docker>
    </configuration>
</plugin>
# 이미지 빌드 명령
./mvnw clean package spring-boot:build-image

# 빌드하면서 바로 Docker Hub에 푸시
./mvnw clean package spring-boot:build-image -Dspring-boot.build-image.publish=true

# Gradle의 경우
./gradlew bootBuildImage

여기서 정말 중요한 시험 함정 — Buildpack은 멀티 플랫폼 이미지 생성에 강점이 있어 Apple Silicon(M1/M2) Mac에서도 자연스럽게 호환됩니다. Dockerfile 직접 작성 방식보다 훨씬 적은 노력으로 안전한 이미지를 얻을 수 있어요.

전통적인 Dockerfile 방식

빌드팩보다 세밀한 제어가 필요할 때 사용해요. 멀티 스테이지 빌드와 레이어 캐싱을 활용하면 빌드 시간과 이미지 크기를 모두 줄일 수 있습니다.

# 멀티 스테이지 빌드 — 빌드 환경과 실행 환경 분리
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline    # 의존성 사전 다운로드 (레이어 캐싱 활용)
COPY src ./src
RUN ./mvnw clean package -DskipTests

# 실행 이미지 — JRE만 포함하여 이미지 크기 최소화
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app

# 보안 — root가 아닌 별도 사용자로 실행
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

# Spring Boot 레이어드 JAR 활용 (레이어별 복사로 캐싱 최적화)
COPY --from=builder /app/target/extracted/dependencies/ ./
COPY --from=builder /app/target/extracted/spring-boot-loader/ ./
COPY --from=builder /app/target/extracted/snapshot-dependencies/ ./
COPY --from=builder /app/target/extracted/application/ ./

# 헬스체크 설정
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD wget -q -O /dev/null http://localhost:8080/actuator/health || exit 1

EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

레이어드 JAR 추출은 Spring Boot 2.3+에서 제공하는 기능이에요.

# 레이어 목록 확인
java -Djarmode=layertools -jar target/myapp.jar list

# 레이어 추출 (Dockerfile에서 사용)
java -Djarmode=layertools -jar target/myapp.jar extract --destination target/extracted

빌드 방식 비교표

항목Buildpack (Maven Plugin)Dockerfile
Dockerfile 필요불필요필요
이미지 최적화자동 (레이어 캐싱)수동 설정 필요
보안 패치자동 (base image 업데이트)수동 관리
커스터마이징제한적완전한 제어
빌드 속도비교적 느림빠름 (캐시 활용 시)
권장 환경일반적인 Spring Boot 앱복잡한 커스텀 요구사항

판단 기준 — 특별한 이유가 없다면 Buildpack, 베이스 이미지 변경이나 OS 패키지 추가가 필요하면 Dockerfile.

Docker 핵심 명령어 — 일상 운영의 80%

여기서는 일상에서 가장 많이 쓰는 명령어만 모았어요. 이 정도만 알면 컨테이너 운영의 80% 이상을 커버합니다.

이미지 관리

# 이미지 목록 조회
docker images

# 이미지 상세 정보 (메타데이터, 레이어, 아키텍처 등)
docker image inspect myapp:0.0.1-SNAPSHOT

# 이미지 다운로드
docker pull mysql:8.0

# 이미지 삭제
docker rmi myapp:0.0.1-SNAPSHOT

# 사용하지 않는 이미지 일괄 삭제
docker image prune -a

컨테이너 실행

# 주요 옵션과 함께 실행
docker run \
    -d \                            # 백그라운드(detached) 실행
    --name gateway \                # 컨테이너 이름 지정
    -p 8080:8080 \                  # 호스트포트:컨테이너포트 매핑
    -e SPRING_PROFILES_ACTIVE=prod \  # 환경 변수 설정
    -v /host/data:/container/data \ # 볼륨 마운트
    --network my-network \          # 특정 네트워크에 연결
    myapp:0.0.1-SNAPSHOT

# Apple Silicon (M1/M2) Mac에서 플랫폼 지정
docker run --platform linux/amd64 \
    -d --name gateway -p 8080:8080 \
    myapp:0.0.1-SNAPSHOT

# 실행 중인 컨테이너 목록
docker ps

# 모든 컨테이너 (중지된 것 포함)
docker ps -a

# 컨테이너 중지·시작·재시작
docker stop gateway
docker start gateway
docker restart gateway

# 컨테이너 삭제
docker stop gateway && docker rm gateway

# 실시간 로그 (-f: follow)
docker logs -f gateway

# 실행 중인 컨테이너에 셸 접속
docker exec -it gateway sh

# 컨테이너 리소스 사용량 모니터링
docker stats

여기서 시험 함정이 하나 있어요. 컨테이너에서 다른 컨테이너에 접근할 때 localhost는 자기 자신을 가리킵니다. 같은 Docker 네트워크에 속한 다른 컨테이너에 접근하려면 컨테이너 이름(또는 서비스 이름) 으로 접속해야 해요.

# 잘못된 설정 — 컨테이너에서 localhost는 자기 자신
SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/restdb

# 올바른 설정 — 같은 네트워크에서 서비스 이름으로 접근
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/restdb

이 함정은 Compose 환경에서 더 자주 등장합니다.

Docker Compose — 여러 컨테이너를 한 번에

docker run 명령어가 길어지면 관리가 어려워져요. 데이터베이스·인증 서버·게이트웨이·앱 컨테이너를 매번 따로 띄우고 네트워크를 연결하는 일도 번거롭습니다.

Docker Compose는 여러 컨테이너로 구성된 애플리케이션을 단일 YAML 파일(compose.yaml 또는 docker-compose.yml) 로 정의하고 관리하는 도구예요. 복잡한 docker run 명령어 대신 선언적 방식으로 전체 스택을 관리할 수 있습니다.

> 참고로 docker-compose (v1, Python 기반)는 구버전이고, 현재 표준은 Docker Desktop에 내장된 docker compose (v2, Go 기반)입니다. 명령어가 한 단어 차이(하이픈 vs 공백)예요.

compose.yaml 기본 구조

# compose.yaml
services:
  # MySQL 데이터베이스 서비스
  mysql:
    image: mysql:8.0
    container_name: mysql_db
    platform: linux/amd64  # Apple Silicon Mac에서 필요
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: restdb
      MYSQL_USER: restadmin
      MYSQL_PASSWORD: password
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql  # 데이터 영속화
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Spring Boot REST MVC 서비스
  rest-mvc:
    image: myapp-mvc:0.0.1-SNAPSHOT
    container_name: rest_mvc
    platform: linux/amd64
    depends_on:
      mysql:
        condition: service_healthy  # MySQL 헬스체크 통과 후 시작
    ports:
      - "8081:8081"
    environment:
      # 12-Factor App 원칙에 따라 환경 변수로 설정 주입
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/restdb
      - SPRING_DATASOURCE_USERNAME=restadmin
      - SPRING_DATASOURCE_PASSWORD=password
      - SPRING_PROFILES_ACTIVE=localdocker

  # Spring Authorization Server
  auth-server:
    image: myapp-auth-server:0.0.1-SNAPSHOT
    container_name: auth_server
    ports:
      - "9000:9000"

  # Spring Cloud Gateway
  gateway:
    image: myapp-gateway:0.0.1-SNAPSHOT
    container_name: gateway
    depends_on:
      - rest-mvc
      - auth-server
    ports:
      - "8080:8080"  # 외부에 노출되는 단일 진입점

volumes:
  mysql_data:  # 명명된 볼륨 — 컨테이너 삭제 후에도 데이터 유지

여기서 시험 함정이 하나 있어요. depends_on만으로는 서비스 준비 상태를 보장할 수 없습니다. depends_on은 컨테이너 시작 순서만 보장해요. 데이터베이스가 시작은 됐지만 아직 연결을 받을 준비가 안 됐을 수 있어요.

# 잘못된 예 — MySQL이 시작되긴 했지만 준비 안 됐을 수 있음
depends_on:
  - mysql

# 올바른 예 — healthcheck와 함께 사용
depends_on:
  mysql:
    condition: service_healthy  # 헬스체크 통과 후 시작

condition: service_healthy를 함께 써야 진짜 안전합니다.

Compose 핵심 명령어

# 전체 스택 시작 (백그라운드)
docker compose up -d

# 특정 서비스만 시작
docker compose up -d mysql

# 서비스 중지 (컨테이너는 유지)
docker compose stop

# 서비스 중지 + 컨테이너 제거 (볼륨은 유지)
docker compose down

# 서비스 중지 + 컨테이너 + 볼륨 모두 제거
docker compose down -v

# 서비스 상태 확인
docker compose ps

# 로그 확인 (모든 서비스)
docker compose logs -f

# 서비스 스케일 조정 (특정 서비스를 n개로 실행)
docker compose up -d --scale rest-mvc=3

docker compose stopdocker compose down의 차이가 시험에 자주 나와요. stop은 중지만(재시작 가능), down은 중지 + 삭제입니다.

환경 변수 분리 — 12-Factor App

12-Factor App 원칙에 따라 설정은 코드와 분리해야 해요. 환경 변수를 통해 같은 이미지를 여러 환경(dev·staging·prod)에 배포할 수 있습니다. 이 원칙은 16편에서 마이크로서비스 맥락에서 다시 자세히 풀어 갑니다.

# .env 파일 (Git에 커밋하지 않을 것)
MYSQL_ROOT_PASSWORD=my_secret_password
MYSQL_DATABASE=restdb
SPRING_PROFILES_ACTIVE=prod
# compose.yaml에서 .env 파일 참조
services:
  rest-mvc:
    env_file:
      - .env
    environment:
      SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-dev}  # 기본값: dev
# application.properties에서 환경 변수 읽기
spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/restdb}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:restadmin}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:password}

Kubernetes — 컨테이너 항만 관리자

Docker Compose는 단일 호스트(서버 한 대)를 위한 도구예요. 그런데 프로덕션에서는 서버 여러 대에 걸쳐 수십 개·수백 개의 컨테이너를 운영해야 합니다. 한 대가 다운되면 자동으로 다른 대로 옮겨야 하고, 트래픽이 몰리면 자동으로 인스턴스를 늘려야 하고, 새 버전을 배포할 때 무중단으로 전환해야 해요.

Kubernetes(K8s) 가 그 일을 합니다. 회사 비유로 풀면 — "컨테이너 항만 관리자" 예요. 화물 컨테이너가 어느 부두에 내려앉을지, 어디로 옮길지, 빈 부두를 어떻게 채울지, 망가진 컨테이너를 어떻게 교체할지 — 이 모든 걸 자동으로 처리합니다. 구글이 내부 시스템(Borg)을 기반으로 개발했고, 현재 CNCF(Cloud Native Computing Foundation)에서 관리해요.

자세한 사양과 튜토리얼은 Kubernetes 공식 문서에서 확인할 수 있습니다.

Kubernetes 핵심 구성

Cluster
├── Control Plane (마스터 노드)
│   ├── API Server: kubectl 명령어를 받는 진입점
│   ├── etcd: 클러스터 상태를 저장하는 분산 키-값 저장소
│   ├── Scheduler: Pod를 적절한 노드에 배치
│   └── Controller Manager: 원하는 상태 유지 감시
│
└── Worker Nodes (워커 노드)
    ├── kubelet: 노드에서 컨테이너 실행 에이전트
    ├── kube-proxy: 네트워크 라우팅
    └── Container Runtime: Docker, containerd 등

용어가 많지만 실무에서 매일 만지는 건 워커 노드 위에 떠 있는 오브젝트(Object) 들이에요. 명사 4개만 잡고 가면 80%가 끝납니다 — Pod, Deployment, Service, ConfigMap/Secret.

K8s 핵심 오브젝트 4종 — 명사 4개로 이해하기

Pod — 최소 배포 단위

Pod은 하나 이상의 컨테이너를 감싸는 K8s의 최소 실행 단위예요. 같은 Pod의 컨테이너들은 네트워크와 스토리지를 공유합니다. 회사 비유로 — "한 사무실에 함께 들어간 동료들" 같은 개념이에요. 같은 사무실이니 책상은 따로지만 공기·전기·인터넷은 함께 씁니다.

직접 Pod을 만들기보다는 보통 Deployment를 통해 관리해요. Pod 직접 생성 예시는 학습용입니다.

# Pod 직접 생성 (학습 목적)
apiVersion: v1
kind: Pod
metadata:
  name: spring-app-pod
  labels:
    app: spring-app
spec:
  containers:
    - name: spring-app
      image: myrepo/myapp-mvc:0.0.1-SNAPSHOT
      ports:
        - containerPort: 8080
      env:
        - name: SPRING_PROFILES_ACTIVE
          value: "kubernetes"
      resources:
        requests:
          memory: "256Mi"
          cpu: "250m"
        limits:
          memory: "512Mi"
          cpu: "500m"

여기서 시험 함정이 하나 있어요. Resource Limit을 설정하지 않으면 하나의 Pod이 노드 전체 리소스를 소비할 수 있습니다. 한 컨테이너가 메모리 누수를 일으키면 같은 노드의 다른 Pod까지 모두 영향을 받아요. 항상 requestslimits를 함께 적어 주세요.

Deployment — Pod 복제와 롤링 업데이트

Deployment는 원하는 개수의 Pod 복제본(replica)을 유지하고, 무중단 롤링 업데이트를 지원합니다. 프로덕션에서 가장 많이 쓰는 오브젝트예요.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-mvc-deployment
  labels:
    app: spring-mvc
spec:
  replicas: 3  # 3개의 Pod 복제본 유지
  selector:
    matchLabels:
      app: spring-mvc
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1   # 업데이트 중 최대 1개 Pod 중단 허용
      maxSurge: 1         # 업데이트 중 최대 1개 추가 Pod 허용
  template:
    metadata:
      labels:
        app: spring-mvc
    spec:
      containers:
        - name: spring-mvc
          image: myrepo/myapp-mvc:0.0.1-SNAPSHOT
          ports:
            - containerPort: 8081
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8081
            initialDelaySeconds: 30
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8081
            initialDelaySeconds: 60
            periodSeconds: 30
          env:
            - name: SPRING_DATASOURCE_URL
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: datasource-url

replicas: 3은 "이 앱을 항상 3개 인스턴스로 유지해 주세요"라는 선언이에요. 한 개가 다운되면 K8s가 자동으로 새 Pod을 띄워 줍니다.

Service — Pod에 안정적인 접근

Pod의 IP는 재시작될 때마다 바뀌어요. Service는 그 위에 고정된 엔드포인트를 제공하고, 여러 Pod 간 로드밸런싱도 자동으로 수행합니다.

Service 타입은 세 가지가 있어요. 각자의 자리가 다릅니다.

타입접근 범위사용 사례
ClusterIP클러스터 내부만서비스 간 내부 통신 (기본값)
NodePort노드 IP + 지정 포트개발/테스트 외부 접근
LoadBalancer외부 로드밸런서프로덕션 외부 노출
ExternalName외부 DNS 이름 매핑외부 서비스 추상화
# ClusterIP — 클러스터 내부 통신용 (기본값)
apiVersion: v1
kind: Service
metadata:
  name: spring-mvc-service
spec:
  selector:
    app: spring-mvc
  ports:
    - protocol: TCP
      port: 8081        # Service가 노출하는 포트
      targetPort: 8081  # Pod 내 컨테이너 포트
  type: ClusterIP

---
# LoadBalancer — 클라우드 로드밸런서 생성 (프로덕션)
apiVersion: v1
kind: Service
metadata:
  name: gateway-loadbalancer
spec:
  selector:
    app: gateway
  ports:
    - port: 80
      targetPort: 8080
  type: LoadBalancer

ConfigMap & Secret — 설정 관리

12-Factor App 원칙에 따라 설정은 코드와 분리해야 한다고 했죠. K8s에서 그 자리는 ConfigMap(일반 설정)Secret(민감 데이터) 입니다.

# ConfigMap — 일반 설정 데이터
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  SPRING_PROFILES_ACTIVE: "kubernetes"
  LOG_LEVEL: "INFO"

---
# Secret — 민감한 데이터 (Base64 인코딩)
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  datasource-url: amRiYzpteXNxbDovL215c3FsOjMzMDYvcmVzdGRi
  username: cmVzdGFkbWlu
  password: cGFzc3dvcmQ=
# Secret 생성 (명령줄 — Base64 자동 처리)
kubectl create secret generic db-secret \
    --from-literal=datasource-url='jdbc:mysql://mysql:3306/restdb' \
    --from-literal=username='restadmin' \
    --from-literal=password='password'

여기서 정말 중요한 시험 함정 — 비밀번호를 ConfigMap에 저장하지 마세요. ConfigMap은 암호화되지 않습니다. 비밀번호·토큰·API 키 같은 민감한 데이터는 반드시 Secret에 넣어야 해요.

Ingress — 외부 HTTP/HTTPS 라우팅

Service만으로는 외부 도메인 라우팅이 부족해요. Ingress는 외부 HTTP/HTTPS 트래픽을 클러스터 내부 Service로 라우팅하는 게이트웨이 역할을 합니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: api.myapp.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: gateway-service
                port:
                  number: 8080

kubectl 핵심 명령어 — 일상 운영의 80%

# 클러스터 정보
kubectl cluster-info
kubectl get nodes

# 리소스 조회
kubectl get pods
kubectl get pods -o wide        # IP, 노드 등 상세 정보
kubectl get deployments
kubectl get services
kubectl get all                 # 모든 리소스 조회

# 리소스 상세 정보
kubectl describe pod spring-mvc-abc123
kubectl describe deployment spring-mvc-deployment

# 리소스 생성/적용 — 선언적 방식 (권장)
kubectl apply -f deployment.yaml
kubectl apply -f .              # 현재 디렉터리의 모든 yaml 적용

# 리소스 삭제
kubectl delete -f deployment.yaml

# Pod 로그
kubectl logs spring-mvc-abc123
kubectl logs -f spring-mvc-abc123       # 실시간
kubectl logs spring-mvc-abc123 --previous  # 이전 컨테이너

# Pod 셸 접속
kubectl exec -it spring-mvc-abc123 -- sh

# 포트 포워딩 (로컬 개발/디버깅)
kubectl port-forward service/spring-mvc-service 8081:8081

# Deployment 스케일링
kubectl scale deployment spring-mvc-deployment --replicas=5

# 롤링 업데이트 이미지 변경
kubectl set image deployment/spring-mvc-deployment spring-mvc=myrepo/myapp-mvc:0.0.2

# 롤아웃 상태 확인
kubectl rollout status deployment/spring-mvc-deployment

# 이전 버전으로 롤백
kubectl rollout undo deployment/spring-mvc-deployment

여기서 시험 함정이 하나 있어요. 명령형(kubectl create)보다 선언형(kubectl apply -f)을 권장합니다. 선언형은 매니페스트 파일에 "원하는 상태"를 적어 두고 K8s가 그 상태를 유지하도록 하는 방식이에요. Git으로 인프라 변경 이력을 추적할 수 있고, 같은 명령을 여러 번 실행해도 결과가 같은 멱등성을 가집니다.

Spring Boot Actuator + K8s Probe 연동 — 환상의 짝궁

12편에서 다룬 Spring Boot Actuator는 K8s의 헬스체크 Probe와 그대로 맞물립니다. 별다른 설정 없이도 자연스럽게 통합돼요.

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application.properties
management.endpoint.health.probes.enabled=true
management.health.livenessState.enabled=true
management.health.readinessState.enabled=true

# Actuator 엔드포인트 노출
management.endpoints.web.exposure.include=health,info,metrics
# Kubernetes Deployment에서 Probe 설정
containers:
  - name: spring-app
    livenessProbe:
      httpGet:
        path: /actuator/health/liveness   # Spring Boot 제공
        port: 8080
      initialDelaySeconds: 60   # JVM 시작 시간 고려
      periodSeconds: 30
      failureThreshold: 3
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness  # Spring Boot 제공
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 10
      failureThreshold: 3

livenessreadiness의 차이가 시험에 자주 나와요.

  • liveness = "이 Pod이 살아 있나" — 실패하면 Pod 재시작
  • readiness = "이 Pod이 트래픽을 받을 준비가 됐나" — 실패하면 Service에서 트래픽 차단

JVM 시작 시간이 비교적 긴 편이라 initialDelaySeconds는 liveness 60초, readiness 30초 정도로 넉넉히 잡는 게 안전합니다.

Docker vs Compose vs Kubernetes 비교

항목DockerDocker ComposeKubernetes
관리 범위단일 컨테이너단일 호스트의 다중 컨테이너다중 호스트의 다중 컨테이너
사용 사례개발, 빌드로컬 개발, 소규모 배포프로덕션, 대규모 배포
자동 스케일링없음수동 (--scale)자동 (HPA)
자동 복구없음없음있음
로드밸런싱없음없음 (외부 도구 필요)내장
학습 곡선낮음낮음~중간높음
설정 파일Dockerfilecompose.yamlyaml (다수)

판단 기준 — 개발 = Docker, 로컬 멀티 컨테이너 = Compose, 프로덕션 클러스터 = Kubernetes.

자주 만나는 함정 5가지

1. Apple Silicon 아키텍처 불일치

Intel 기반으로 빌드된 이미지를 M1/M2 Mac에서 실행하면 오류가 나거나 성능이 크게 떨어져요.

# 특정 플랫폼 지정
docker run --platform linux/amd64 myapp:latest

# 멀티 아키텍처 빌드
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

Spring Boot Buildpack은 자동으로 멀티 아키텍처 이미지를 만들어 주니 이 함정이 거의 없어요.

2. 컨테이너 이름 충돌

같은 이름의 컨테이너가 이미 존재하면 오류가 발생합니다.

# 오류: "The container name '/gateway' is already in use"
# 해결 — 기존 컨테이너 중지 후 삭제
docker stop gateway && docker rm gateway

# 또는 --rm 옵션으로 종료 시 자동 삭제
docker run --rm --name gateway -p 8080:8080 myapp:latest

3. localhost 함정

이미 위에서 언급한 — 컨테이너 안에서 localhost는 자기 자신이에요. 다른 컨테이너로 접근하려면 컨테이너 이름·서비스 이름을 사용합니다.

4. 민감 정보를 Dockerfile에 하드코딩

# 절대 금지 — 이미지 레이어에 비밀번호가 영구 저장됨
ENV DB_PASSWORD=my_secret_password

# 올바른 방법 — 런타임 환경 변수로 주입
# docker run -e DB_PASSWORD=my_secret_password ...

K8s에서는 Secret을 통해 안전하게 주입할 수 있어요.

5. 에러 응답에 스택 트레이스 노출

이건 7번째 함정처럼 모든 환경에서 공통이에요. 컨테이너든 일반 배포든, 에러 응답에 내부 스택 트레이스가 나가면 보안 위험이 됩니다. 17편에서 자세히 풀어 갑니다.

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 15편 컨테이너·배포의 핵심이에요. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • Docker = 표준 컨테이너 비유 — OS·JVM·앱·설정을 한 박스로 묶어 어디든 그대로 옮기기
  • 이미지 = 설계도, 컨테이너 = 그 설계도로 지은 집, 같은 이미지로 여러 컨테이너 실행 가능
  • 컨테이너 안 데이터는 삭제 시 사라짐 — 영속화는 Volume 또는 외부 저장소
  • Spring Boot 2.3+ — ./mvnw spring-boot:build-imageDockerfile 없이 자동 이미지 빌드
  • Buildpack 권장(멀티 아키텍처·자동 보안 패치), Dockerfile은 세밀 제어 필요할 때
  • 컨테이너에서 localhost는 자기 자신 — 다른 컨테이너 접근 시 컨테이너 이름·서비스 이름 사용
  • Compose의 depends_on은 시작 순서만 보장 — 준비 상태는 condition: service_healthy와 함께
  • docker compose stop = 중지만, docker compose down = 중지 + 삭제, down -v는 볼륨까지 삭제
  • 12-Factor App — 설정은 환경 변수로 외부화, .env 파일·SPRING_* 환경 변수
  • Kubernetes = 컨테이너 항만 관리자 — 다중 호스트의 자동 배포·스케일·복구
  • K8s 명사 4개 — Pod / Deployment / Service / ConfigMap·Secret
  • Pod = 최소 배포 단위, Deployment가 Pod 복제본 + 롤링 업데이트 관리
  • Service 3종 — ClusterIP(내부 기본), NodePort(개발 외부), LoadBalancer(프로덕션 외부)
  • 비밀번호는 Secret, 일반 설정은 ConfigMap — ConfigMap은 암호화 X
  • kubectl apply -f(선언형) > kubectl create(명령형) — Git으로 인프라 이력 관리
  • Actuator + K8s Probe — /actuator/health/liveness(재시작) + /actuator/health/readiness(트래픽)
  • liveness 실패 = Pod 재시작, readiness 실패 = 서비스에서 트래픽 차단
  • Resource limits 항상 설정 — 한 Pod이 노드 전체 자원 소비 방지
  • Apple Silicon — --platform linux/amd64 또는 Buildpack 자동 멀티 아키텍처
  • 민감 정보를 Dockerfile ENV에 박지 말 것 — 런타임 환경 변수 또는 Secret으로 주입

Docker의 자세한 사양은 Docker 공식 문서에서, Kubernetes의 깊은 활용은 Kubernetes 공식 문서에서 확인할 수 있어요.

시리즈 다른 편

같은 시리즈의 다른 글들도 같은 친절 톤으로 묶어 정리되어 있어요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!