Part 2. 백엔드 개발과 Kubernetes

Ch 2. Kubernetes를 위한 애플리케이션 생성

Ch 2. Kubernetes를 위한 애플리케이션 생성


01. Kubernetes를 위한 애플리케이션 생성

Kubernetes를 위한 애플리케이션 생성

쿠버네티스에서 애플리케이션을 실행하기 위해서는 먼저 애플리케이션을 컨테이너 이미지로 만들어야 한다.

과거에는 서버에 직접 JDK를 설치하고, 빌드된 JAR 파일을 복사한 뒤 실행하는 방식이 일반적이었다. 하지만 Docker가 대중화된 이후에는 애플리케이션 실행에 필요한 모든 구성 요소를 하나의 컨테이너 이미지에 포함하는 방식이 보편화되었다.

Spring Project
      ↓
application.jar
      ↓
Container Image
      ↓
Container

이렇게 생성된 컨테이너 이미지는 Docker와 같은 컨테이너 런타임을 통해 실행될 수 있다.


Kubernetes에서의 실행 단위

컨테이너는 애플리케이션 실행의 기본 단위이지만 Kubernetes에서는 컨테이너를 직접 배포하지 않는다.

Kubernetes가 관리하는 가장 작은 배포 단위는 Pod 이다.

graph LR
    Container --> Pod
    Pod --> ReplicaSet
    ReplicaSet --> Deployment

Pod는 하나 이상의 컨테이너를 포함할 수 있으며, 같은 Pod 내부의 컨테이너들은 네트워크와 저장소를 비교적 자유롭게 공유할 수 있다.


Kubernetes의 주요 Workload

Kubernetes는 다양한 실행 목적에 맞는 Workload 객체를 제공한다.

graph LR
    CronJob
    Job
    Deployment
    StatefulSet
    DaemonSet

각 객체의 역할은 다음과 같다.

객체역할
Deployment일반적인 서버 애플리케이션 배포
StatefulSet상태를 가지는 애플리케이션 실행
DaemonSet모든 노드에 하나씩 Pod 실행
Job일회성 작업 실행
CronJob스케줄 기반 작업 실행

실제 운영 환경에서는 대부분 Deployment를 사용하며, ReplicaSet은 Deployment 내부에서 자동으로 관리된다. Deployment는 ReplicaSet의 기능에 더해 버전 관리, 롤링 업데이트, 롤백 기능까지 제공한다.


애플리케이션의 Kubernetes 배포 과정

일반적인 배포 과정은 다음과 같다.

graph LR
    A[Source Code]
    --> B[Container Image]

    B --> C[Deployment]

    C --> D[Pod]

    D --> E[Service]
  1. 애플리케이션을 컨테이너 이미지로 생성
  2. Deployment 작성
  3. Pod 생성
  4. Service 연결
  5. 외부 또는 내부 네트워크에 노출

배포 과정에서 필요에 따라 다음과 같은 설정을 추가할 수 있다.

  • Label
  • Node Selector
  • Affinity
  • Environment Variable
  • ConfigMap
  • Secret
  • Volume

애플리케이션의 Containerization

컨테이너 이미지를 생성하는 방법은 여러 가지가 있지만 대표적으로 Dockerfile과 Gradle Jib를 많이 사용한다.


Dockerfile 사용

FROM openjdk:19

COPY ./build/libs/my-app-*.jar /app.jar

CMD ["java", "-jar", "/app.jar"]
설정 설명
FROM
FROM openjdk:19

컨테이너 이미지의 베이스 이미지를 지정한다.

위 예제에서는 OpenJDK 19가 설치된 이미지를 사용한다.


COPY
COPY ./build/libs/my-app-*.jar /app.jar

빌드된 JAR 파일을 컨테이너 내부로 복사한다.

호스트 파일
↓
./build/libs/my-app.jar

컨테이너 내부
↓
/app.jar

CMD
CMD ["java", "-jar", "/app.jar"]

컨테이너가 실행될 때 수행할 명령어를 지정한다.


Docker 이미지 빌드

docker build -t my-repo/my-app:0.0.1 .
옵션 설명
-t
-t my-repo/my-app:0.0.1

생성될 이미지 이름과 태그를 지정한다.

Repository : my-repo/my-app
Tag        : 0.0.1

Gradle Jib 사용

Docker를 설치하지 않고도 이미지를 생성할 수 있다.

jib {
    from {
        image = "openjdk:19"
    }

    to {
        image = "my-repo/my-app"
        tags = ['latest', '0.0.1']
    }

    container {
        creationTime = "USE_CURRENT_TIMESTAMP"
    }
}
설정 설명
from
from {
    image = "openjdk:19"
}

베이스 이미지를 지정한다.


to
to {
    image = "my-repo/my-app"
}

생성될 이미지 이름을 지정한다.


tags
tags = ['latest', '0.0.1']

하나의 이미지에 여러 태그를 부여할 수 있다.


creationTime
creationTime = "USE_CURRENT_TIMESTAMP"

이미지 생성 시간을 현재 시각으로 설정한다.


Deployment 스펙 작성

이미지를 만들었다면 이제 Kubernetes에 배포할 Deployment를 작성한다.

apiVersion: apps/v1
kind: Deployment

metadata:
  name: my-app

spec:
  replicas: 2

  selector:
    matchLabels:
      app: my-app

  template:
    metadata:
      labels:
        app: my-app

    spec:
      containers:
      - name: my-container
        image: my-repo/my-app:0.0.1

Deployment 설정 설명

apiVersion
apiVersion: apps/v1

Deployment API 버전


kind
kind: Deployment

생성할 객체 유형


metadata.name
metadata:
  name: my-app

Deployment 이름


replicas
replicas: 2

동시에 실행할 Pod 개수

replicas: 2

→ Pod 2개 생성

selector
selector:
  matchLabels:
    app: my-app

Deployment가 관리할 Pod를 식별하는 기준


template.metadata.labels
labels:
  app: my-app

생성될 Pod의 Label

selector와 동일해야 한다.


containers
containers:
- name: my-container

Pod 내부에서 실행될 컨테이너 정의


image
image: my-repo/my-app:0.0.1

실행할 컨테이너 이미지


환경 변수 추가

애플리케이션 실행 시 환경 변수를 주입할 수도 있다.

containers:
- name: my-container
  image: my-repo/my-app:0.0.1

  env:
  - name: SPRING_PROFILES_ACTIVE
    value: "dev"

env

env:

컨테이너 환경 변수 정의


SPRING_PROFILES_ACTIVE

- name: SPRING_PROFILES_ACTIVE
  value: "dev"

Spring Boot의 활성 프로파일 지정

application-dev.yml

설정이 적용된다.


Kubernetes 적용

Deployment YAML 작성이 완료되었다면 다음 명령어로 클러스터에 적용할 수 있다.

kubectl apply -f my-app.yaml

실행 흐름은 다음과 같다.

graph LR
    A[my-app.yaml]
    --> B[kubectl apply]
    --> C[Deployment]
    --> D[ReplicaSet]
    --> E[Pod]

Deployment가 생성되면 내부적으로 ReplicaSet이 생성되고, ReplicaSet이 실제 Pod를 생성 및 관리하게 된다.


정리

Kubernetes에서 애플리케이션을 배포하는 과정은 크게 다음 순서로 이루어진다.

Spring Project
↓
Jar Build
↓
Container Image 생성
↓
Deployment 작성
↓
Pod 생성
↓
Service 연결
↓
운영

실제 운영 환경에서는 Deployment를 중심으로 ConfigMap, Secret, Volume, Service, Ingress 등을 함께 사용하여 완전한 애플리케이션 실행 환경을 구성하게 된다.

02. 기본 스프링 애플리케이션 생성 및 컨테이너화

기본 스프링 애플리케이션 생성 및 컨테이너화

스프링 애플리케이션을 Kubernetes에 배포하려면 먼저 애플리케이션을 컨테이너 이미지 형태로 만들어야 한다.

Kubernetes는 소스 코드나 JAR 파일을 직접 실행하는 플랫폼이 아니다. Kubernetes는 컨테이너 이미지를 기반으로 Pod를 생성하고, Pod 안에서 컨테이너를 실행한다.

따라서 전체 흐름은 다음과 같다.

Spring Application
↓
JAR Build
↓
Container Image
↓
Image Repository Push
↓
Kubernetes Deployment

컨테이너 이미지 저장소

컨테이너 이미지를 만들었다면 Kubernetes 클러스터가 이미지를 가져올 수 있는 저장소에 이미지를 올려야 한다.

대표적인 방식은 다음과 같다.

로컬 이미지 저장소 사용

로컬 실습 환경에서는 Docker Desktop, kind, minikube 환경에서 로컬 이미지를 사용할 수 있다.

다만 실제 운영 환경에서는 로컬 이미지를 사용하는 방식이 적합하지 않다.


Private Registry 사용

회사 내부 환경에서는 보통 사설 이미지 저장소를 사용한다.

예를 들면:

  • Harbor
  • AWS ECR
  • Google Artifact Registry
  • Azure Container Registry

같은 저장소를 사용할 수 있다.


Docker Hub 사용

가장 쉽게 실습할 수 있는 방식은 Docker Hub를 사용하는 것이다.

Docker Hub를 사용하는 흐름은 다음과 같다.

hub.docker.com 접속
↓
회원가입 / 로그인
↓
Repositories 이동
↓
Repository 생성
↓
이미지 Push
↓
Kubernetes에서 이미지 Pull

컨테이너 이미지 생성 방식

스프링 애플리케이션을 컨테이너 이미지로 만드는 대표적인 방법은 두 가지다.

Dockerfile 사용

Dockerfile을 직접 작성해서 이미지를 생성하는 방식이다.

FROM openjdk:19

COPY ./build/libs/my-app-*.jar /app.jar

CMD ["java", "-jar", "/app.jar"]

Dockerfile 방식의 특징

  • Dockerfile을 직접 관리해야 한다
  • 이미지 구조를 세밀하게 제어할 수 있다
  • Docker build 명령어를 직접 실행해야 한다
docker build -t my-repo/my-app:0.0.1 .

Gradle Jib 사용

Gradle Jib는 Dockerfile 없이 Java 애플리케이션을 컨테이너 이미지로 만들어주는 Gradle 플러그인이다.

스프링 애플리케이션에서는 Jib를 사용하면 Dockerfile을 직접 작성하지 않아도 되고, Gradle Task만으로 이미지를 빌드하고 Registry에 Push할 수 있다.


Gradle Jib 설정

플러그인 추가

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'com.google.cloud.tools.jib' version '3.4.0'
}

또는 기존 plugins 블록을 쓰지 않는 구조라면 다음과 같이 추가할 수 있다.

buildscript {
    dependencies {
        classpath 'com.google.cloud.tools:jib-gradle-plugin:3.4.0'
    }
}

Jib 기본 설정 예시

jib {
    from {
        image = "openjdk:19"
    }

    to {
        image = "my-dockerhub-id/my-app"
        tags = ["latest", "0.0.1"]
    }

    container {
        creationTime = "USE_CURRENT_TIMESTAMP"
    }
}

Jib 설정 설명

from

from {
    image = "openjdk:19"
}

컨테이너 이미지의 기반이 되는 베이스 이미지를 지정한다.

Java 애플리케이션이 실행되어야 하므로 JDK 또는 JRE가 포함된 이미지를 사용한다.


to

to {
    image = "my-dockerhub-id/my-app"
    tags = ["latest", "0.0.1"]
}

빌드된 이미지를 어떤 Repository에 Push할지 지정한다.

예를 들어 Docker Hub 계정이 siksik이고 Repository 이름이 my-app이라면 다음과 같이 작성할 수 있다.

to {
    image = "siksik/my-app"
    tags = ["latest", "0.0.1"]
}

tags

tags = ["latest", "0.0.1"]

하나의 이미지에 여러 태그를 부여한다.

다만 운영 환경에서는 latest만 사용하는 것은 권장되지 않는다. 배포 버전을 명확하게 추적하기 어렵기 때문이다.

운영에서는 보통 다음과 같이 명확한 버전 태그를 사용한다.

0.0.1
1.0.0
2025.03.01
git-commit-hash

container

container {
    creationTime = "USE_CURRENT_TIMESTAMP"
}

컨테이너 이미지의 생성 시간을 설정한다.

Jib는 기본적으로 재현 가능한 빌드를 위해 이미지 생성 시간을 고정된 과거 시점으로 설정하는 경우가 있다. 실습이나 이미지 생성 시각 확인이 필요한 경우에는 USE_CURRENT_TIMESTAMP를 지정해서 현재 시간을 사용하도록 설정할 수 있다.


Jib Task 실행

Jib 플러그인을 추가하면 Gradle Task에 jib 작업이 생긴다.

./gradlew jib

이 명령어를 실행하면 다음 작업이 한 번에 수행된다.

애플리케이션 빌드
↓
컨테이너 이미지 생성
↓
Registry Push

즉 Dockerfile을 직접 작성하거나 docker build, docker push를 따로 실행하지 않아도 된다.


Docker Hub 인증

Docker Hub에 이미지를 Push하려면 인증이 필요하다.

Docker CLI 로그인

docker login

로그인 후 ./gradlew jib를 실행하면 Docker 인증 정보를 사용해서 이미지를 Push할 수 있다.


Jib에서 직접 인증 정보 지정

필요하다면 Jib 설정에 인증 정보를 직접 지정할 수도 있다.

jib {
    to {
        image = "my-dockerhub-id/my-app"
        auth {
            username = "my-dockerhub-id"
            password = "my-password"
        }
    }
}

다만 비밀번호를 build.gradle에 직접 작성하는 것은 피해야 한다.

실무에서는 보통 다음과 같은 방식을 사용한다.

- 환경 변수
- Gradle properties
- CI/CD Secret

환경 변수를 이용한 인증 예시

jib {
    to {
        image = "my-dockerhub-id/my-app"
        auth {
            username = System.getenv("DOCKER_USERNAME")
            password = System.getenv("DOCKER_PASSWORD")
        }
    }
}

이미지 Push 이후 Kubernetes에서 사용

이미지를 Repository에 Push했다면 Deployment에서 해당 이미지를 사용할 수 있다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app

spec:
  replicas: 2

  selector:
    matchLabels:
      app: my-app

  template:
    metadata:
      labels:
        app: my-app

    spec:
      containers:
      - name: my-container
        image: my-dockerhub-id/my-app:0.0.1

Deployment 적용

kubectl apply -f my-app.yaml

배포 확인

kubectl get pods
kubectl get deployment

전체 흐름 정리

graph LR
    A[Spring Application]
    --> B[Gradle Build]

    B --> C[Jib]

    C --> D[Container Image]

    D --> E[Docker Hub]

    E --> F[Kubernetes Deployment]

    F --> G[Pod]

Dockerfile vs Jib

구분DockerfileGradle Jib
Dockerfile 필요필요불필요
Docker Daemon 필요필요불필요
Java 최적화직접 구성자동 최적화
빌드/Push수동 명령Gradle Task
세밀한 이미지 제어높음상대적으로 낮음

정리

스프링 애플리케이션을 Kubernetes에 올리기 위한 핵심 과정은 다음과 같다.

1. Spring 애플리케이션 생성
2. JAR 빌드
3. 컨테이너 이미지 생성
4. 이미지 Repository Push
5. Deployment에서 이미지 참조
6. Kubernetes에 배포

Dockerfile 방식도 가능하지만, Java/Spring 애플리케이션에서는 Gradle Jib를 사용하면 훨씬 간단하게 이미지를 만들고 Registry에 Push할 수 있다.

03. 스프링 애플리케이션을 쿠버네티스에 올리기

스프링 애플리케이션을 쿠버네티스에 올리기

컨테이너 이미지를 생성하고 이미지 저장소(Docker Hub, ECR, ACR 등)에 업로드했다면, 이제 Kubernetes에서 해당 이미지를 이용해 애플리케이션을 실행할 수 있다.

일반적인 배포 과정은 다음과 같다.

Container Image 생성
↓
Image Repository Push
↓
Namespace 생성
↓
Deployment 생성
↓
Pod 실행
↓
Service 연결
↓
Ingress 연결
↓
외부 서비스 제공

Namespace 생성

애플리케이션을 배포하기 전에 먼저 Namespace를 생성하는 경우가 많다.

Namespace는 Kubernetes 클러스터 내부를 논리적으로 분리하기 위한 단위이다.

예를 들어 다음과 같이 환경별로 Namespace를 나눌 수 있다.

dev
staging
production

Namespace 생성

apiVersion: v1
kind: Namespace

metadata:
  name: my-app

적용

kubectl apply -f namespace.yaml

또는

kubectl create namespace my-app

명령어를 사용할 수도 있다.


Deployment 생성

이미지 저장소에 업로드된 이미지를 이용하여 Deployment를 생성한다.

apiVersion: apps/v1
kind: Deployment

metadata:
  name: my-app
  namespace: my-app

spec:
  replicas: 2

  selector:
    matchLabels:
      app: my-app

  template:
    metadata:
      labels:
        app: my-app

    spec:
      containers:
      - name: my-app
        image: my-repo/my-app:0.0.1

        ports:
        - containerPort: 8080

        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "dev"

주요 설정 설명

replicas
replicas: 2

동시에 실행할 Pod 개수이다.

Pod 2개 생성

image
image: my-repo/my-app:0.0.1

실행할 컨테이너 이미지를 지정한다.

Kubernetes는 이미지 저장소에서 해당 이미지를 다운로드한 후 컨테이너를 생성한다.


env
env:
- name: SPRING_PROFILES_ACTIVE
  value: "dev"

환경 변수를 주입한다.

Spring Boot에서는 활성 프로파일을 지정할 때 자주 사용한다.


Deployment 적용

작성한 Deployment를 클러스터에 적용한다.

kubectl apply -f deployment.yaml

배포 상태 확인

Deployment가 정상적으로 생성되었는지 확인한다.

kubectl get deployment -n my-app

실행 중인 Pod 확인

kubectl get pods -n my-app

상세 정보 확인

kubectl describe pod <pod-name> -n my-app

컨테이너 내부 접속

애플리케이션이 정상적으로 실행되었는지 확인하기 위해 컨테이너 내부에 접속할 수 있다.

kubectl exec -it <pod-name> -- sh

또는

kubectl exec -it <pod-name> -- bash

로그 확인

Spring Boot 애플리케이션 로그 확인

kubectl logs <pod-name>

실시간 로그 확인

kubectl logs -f <pod-name>

Service 연결

Deployment만 생성하면 Pod는 실행되지만 네트워크를 통해 접근하기 어렵다.

Service를 생성하여 Pod를 네트워크에 연결한다.

apiVersion: v1
kind: Service

metadata:
  name: my-app-service
  namespace: my-app

spec:
  selector:
    app: my-app

  ports:
  - port: 80
    targetPort: 8080

  type: ClusterIP

Service의 역할

Service
↓
Pod1
Pod2
Pod3

Service는 여러 Pod에 대한 고정된 접근 지점을 제공한다.

또한 Pod 수가 변경되더라도 Service 주소는 유지된다.


Ingress 연결

외부 사용자가 애플리케이션에 접근할 수 있도록 Ingress를 연결한다.

apiVersion: networking.k8s.io/v1
kind: Ingress

metadata:
  name: my-app-ingress

spec:
  rules:
  - host: api.example.com

    http:
      paths:
      - path: /
        pathType: Prefix

        backend:
          service:
            name: my-app-service

            port:
              number: 80

네트워크 흐름

graph LR
    Client
    --> Ingress

    Ingress
    --> Service

    Service
    --> Pod

Deployment 파일 관리 방식

Deployment YAML 파일은 여러 방식으로 관리할 수 있다.


애플리케이션 저장소 내부 관리

project
 ├── src
 ├── build.gradle
 ├── Dockerfile
 └── k8s
     ├── deployment.yaml
     ├── service.yaml
     └── ingress.yaml

가장 단순한 방식이다.

애플리케이션 코드와 Kubernetes 설정을 함께 관리할 수 있다.


별도 Infrastructure 저장소 관리

application-repository

infrastructure-repository
 ├── dev
 ├── staging
 └── production

운영 환경에서는 Infrastructure Repository를 별도로 두는 경우도 많다.

GitOps(ArgoCD, FluxCD) 환경에서는 이 방식이 많이 사용된다.


이미지 버전 관리

Kubernetes 운영 시 가장 중요한 것 중 하나가 이미지 태그 관리이다.


좋지 않은 예

image: my-repo/my-app:latest

왜 문제가 될까?

latest 태그는 항상 가장 최근 이미지를 의미한다.

따라서 다음과 같은 문제가 발생할 수 있다.

Pod A → old latest

Pod B → new latest

같은 Deployment인데도 서로 다른 버전의 애플리케이션이 실행될 수 있다.


롤백도 어려움

latest

만 사용하면 이전 버전이 무엇인지 알 수 없다.

따라서 장애 발생 시 롤백이 매우 어려워진다.


권장 방식

image: my-repo/my-app:1.0.0

또는

image: my-repo/my-app:2025.08.15

또는

image: my-repo/my-app:git-a1b2c3

처럼 명확한 버전을 사용하는 것이 좋다.


버전 업데이트

새로운 이미지를 배포하는 경우

image: my-repo/my-app:0.0.1

image: my-repo/my-app:0.0.2

로 변경한 후 다시 적용한다.

kubectl apply -f deployment.yaml

Deployment는 자동으로 Rolling Update를 수행한다.


배포 흐름

graph LR
    A[my-app 0.0.1]
    --> B[Image Push]

    B --> C[Deployment Update]

    C --> D[Rolling Update]

    D --> E[0.0.2 Pod]

정리

스프링 애플리케이션을 Kubernetes에 배포하는 과정은 다음과 같다.

1. 애플리케이션 빌드
2. 컨테이너 이미지 생성
3. 이미지 저장소 Push
4. Namespace 생성
5. Deployment 생성
6. Pod 실행 확인
7. Service 연결
8. Ingress 연결
9. 외부 서비스 제공

운영 환경에서는 latest 태그 사용을 피하고, 명확한 버전 태그를 사용하여 배포와 롤백이 가능하도록 관리하는 것이 중요하다.


© 2020. All rights reserved.

SIKSIK