본문 바로가기

(12.05) DevOps - kubernetes Volume, kubernetes 프론트/백엔드 연동 실습, 스케일링과 롤링업데이트

@starweb2025. 12. 10. 18:41

[ 16주차 - 1205 ]

    금일 커리큘럼
        ├ 09:00 ~ 12:00 Devops (Kubernetes Volume)
        └ 13:00 ~ 18:00 Devops (Kubernetes 프론트-백엔드 실습, 스케일링과 롤링 업데이트)

1. kubernetes - Volume

1.1 볼륨 종류

종류 설명 예시
emptyDir 파드 안에서만 공유 / 파드 삭제 시 데이터 삭제 임시 Cache
hostPath 노드 Host OS 파일 시스템 사용 개발 테스트
nfs 외부 NFS 스토리지 연결 사내 파일서버
PVC 저장소 요청(Persistent Volume Claim) 동적 할당
PV 스토리지 자체(Persistent Volume) 관리자 제공
  • pod는 기본적으로 휘발성 저장소를 사용하게됨 (일시적임)
  • volume을 사용하여 데이터를 영구적으로 저장하거나 여러 파드 간에 데이터를 공유할 수 있음
  • volume은 파드의 라이프사이클과 독립적으로 존재할 수 있음
  • 따라서 pod가 삭제되더라도 volume에 저장된 데이터는 유지될 수 있음

1.2 PV 예제

  • pv-hostpath.yml
  • 스토리지 크기는 1Gi = 1024MB (Mi)
  • 삭제정책 :
    • Retain : pvc 삭제시 pv는 남아있음
    • Recycle : 자동 초기화 후 재사용 (deprecated)
    • Delete : pvc 삭제시 pv도 삭제
apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /tmp/data        # 노드의 로컬 디렉토리 (테스트용)
  persistentVolumeReclaimPolicy: Retain # 삭제 정책 : Retain, Recycle, Delete
  • pv 생성시
kubectl apply -f pv-hostpath.yml
  • 해당 pv 확인
kubectl get pv
kubectl describe pv my-pv

# 출력 예시 (일부 생략)
Name:            my-pv
Labels:          <none>
Annotations:     <none>
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:
Status:          Available
Claim:
Reclaim Policy:  Retain
Access Modes:    RWO
Capacity:        1Gi
Node Affinity:   <none>
VolumeMode:      Filesystem

1.3 PVC 예제

  • deploy-volume.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: volume-demo-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: volume-demo
  template: # 파드 템플릿
    metadata:
      labels:
        app: volume-demo
    spec:
      containers:
      - name: nginx-container # 컨테이너 이름
        image: nginx:alpine
        ports:
        - containerPort: 80 # 컨테이너 포트
        volumeMounts:
        - name: my-data-volume
          mountPath: /data         # 컨테이너 내부 경로
      volumes:
      - name: my-data-volume
        persistentVolumeClaim:
          claimName: my-pvc        # PVC 이름
  • 디플로이먼트 pvc 생성시
kubectl apply -f deploy-volume.yml
  • pvc로 선언한 pod 확인
kubectl get pods

# 출력 예시
NAME                                 READY   STATUS    RESTARTS   AGE
volume-demo-deploy-5d6f7c9f7b-abcde   1/1     Running   0          2m

 

1.4 동작 상태 확인

  • pvc 로 선언한 pod 접속
# 해당 포드 내 접속
kubectl exec -it <pod이름> -- /bin/sh

# /data 디렉토리로 이동
cd /data

# 데이터 생성
echo "Hello Kubernetes Volume" > testfile.txt

# 디렉토리, 권한 확인
ls -l /data

1.5 pvc와 연결된 pv 확인

kubectl get pvc

# 출력 예시
NAME      STATUS   VOLUME   CAPACITY   ACCESSMODES   AGE
my-pvc    Bound    my-pv    1Gi        RWO           30s
STATUS 의미
Pending PV가 없어서 바인딩 못한 상태
Bound PV와 정상 연결된 상태
Released PVC는 삭제되었지만 PV는 남아있는 상태
Failed 바인딩 실패

1.6 pod 삭제 후 데이터 확인

  • 현재 pvc가 바인딩되어 있는 경우, pod 삭제해도 데이터는 유지됨
  • 삭제 후 확인해보기
kubectl delete pod <pod-name>

kubectl get pods   # 새 파드 자동 생성 확인

# 새로 생성된 파드 접속
# testfile.txt 가 그대로 존재하는지 확인
kubectl exec -it <new-pod> -- /bin/sh
cd /data
ls -l

 

1.7 hostPath 실제 파일 확인

  • docker-desktop 환경에서 k8s 노드 파일시스템 접근
# wsl에서 docker-desktop 노드파일시스템 접속
wsl -d docker-desktop

# pvc 확인
ls /tmp/docker-desktop-root/var/lib/k8s-pvs
# 출력예시 : my-pvc

# pv 데이터 확인
cd /tmp/docker-desktop-root/var/lib/k8s-pvs
ls

# testfile.txt 파일 확인

1.8 요약 정리

  • PV(PersistentVolume): 스토리지 자체 (관리자가 미리 제공)
  • PVC(PersistentVolumeClaim): 스토리지 요청 (파드/사용자가 요청)
  • 둘이 매칭되면 Bound 상태가 되며 스토리지를 사용 가능
PV 생성  →  PVC 생성  →  PVC가 PV를 요청  →  바인딩(Bound)
→ Deployment에서 pvc 사용 (volumeMount)
/data 에 파일 쓰면 PV에 저장됨

hostPath 사용 시

  • 테스트용임
  • 실제 위치는 k8s 노드 파일시스템
  • Docker Desktop 환경에서는 WSL(docker-desktop) 에 저장됨

pod 삭제 시

  • PVC가 계속 존재하면 PV도 유지됨
  • 새 파드가 자동 생성되어도 데이터 사용 가능

삭제정책

  • persistentVolumeReclaimPolicy
    • Retain : pvc 삭제시 pv는 남아있음
    • Recycle : 자동 초기화 후 재사용
    • Delete : pvc 삭제시 pv도 삭제

2. 간단한 Kubernetes 프론트-백엔드 실습

2.1 frontend 관련 작성

디렉토리 구조

my-web-app
  ├── assets/
  │   ├── js/script.js
  │   └── css/style.css
  │
  ├── Dockerfile
  └── index.html
index.html 소스보기
  • 경로 : my-web-app/index.html
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Kubernetes 실습</title>
    <link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
    <div class="container">
        <h1>🚀 Hello Kubernetes!</h1>
        <p>안녕하세요! Kubernetes에 배포된 웹 애플리케이션입니다.</p>
        <div class="info">
            <p><strong>Pod 이름:</strong> <span id="hostname">Loading...</span></p>
            <p><strong>현재 시간:</strong> <span id="time"></span></p>
        </div>
        <button onclick="updateTime()">시간 업데이트</button>

        <div class="api-section">
            <h2>🔗 백엔드 API 연동</h2>
            <p id="api-status">연결 중...</p>
            <p><strong>API 응답:</strong> <span id="api-response">Loading...</span></p>
            <button onclick="callBackendAPI()">API 재호출</button>
        </div>
    </div>
    <script src="assets/js/script.js"></script>
</body>
</html>
style.css 소스보기
  • 경로 : my-web-app/assets/css/style.css
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
}

.container {
    background: white;
    padding: 40px;
    border-radius: 10px;
    box-shadow: 0 10px 30px rgba(0,0,0,0.3);
    text-align: center;
    max-width: 500px;
}

h1 {
    color: #333;
    margin-bottom: 20px;
}

p {
    color: #666;
    margin-bottom: 15px;
    font-size: 16px;
}

.info {
    background: #f5f5f5;
    padding: 20px;
    border-radius: 5px;
    margin: 20px 0;
}

.info p {
    margin: 10px 0;
}

button {
    background: #667eea;
    color: white;
    border: none;
    padding: 12px 30px;
    border-radius: 5px;
    cursor: pointer;
    font-size: 16px;
    transition: background 0.3s;
    margin: 5px;
}

button:hover {
    background: #5568d3;
}

.api-section {
    background: #e8f4f8;
    padding: 20px;
    border-radius: 5px;
    margin-top: 20px;
    border-left: 4px solid #667eea;
}

.api-section h2 {
    color: #333;
    font-size: 18px;
    margin-bottom: 15px;
}

#api-status {
    font-weight: bold;
    margin: 10px 0;
}
script.js 소스보기
  • 경로 : my-web-app/assets/js/script.js
// 백엔드 API URL (기본값: 로컬호스트, Kubernetes 환경에서는 Service 이름 사용)
const API_URL = window.API_URL || 'http://localhost:8080';

// 현재 시간 업데이트 함수
function updateTime() {
    const now = new Date();
    // 한국 시간 형식으로 변환
    const timeString = now.toLocaleString('ko-KR', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit'
    });
    document.getElementById('time').textContent = timeString;
}

// 백엔드 API 호출 함수
async function callBackendAPI() {
    try {
        // API 호출 (/hello 엔드포인트)
        const response = await fetch(`${API_URL}/hello`);
        const message = await response.text();
        document.getElementById('api-response').textContent = message;
        document.getElementById('api-status').textContent = '✅ 연결 성공';
        document.getElementById('api-status').style.color = 'green';
    } catch (error) {
        document.getElementById('api-response').textContent = '연결 실패';
        document.getElementById('api-status').textContent = '❌ 백엔드 연결 실패';
        document.getElementById('api-status').style.color = 'red';
        console.error('API 호출 실패:', error);
    }
}

// 페이지 로드 시 실행
window.onload = function() {
    // 시간 업데이트
    updateTime();
    setInterval(updateTime, 1000);

    // 호스트명 표시
    document.getElementById('hostname').textContent = 'web-app-pod-' + Math.random().toString(36).substr(2, 9);

    // 백엔드 API 호출
    callBackendAPI();
};
style.css 소스보기
  • 경로 : my-web-app/assets/css/style.css
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
}

.container {
    background: white;
    padding: 40px;
    border-radius: 10px;
    box-shadow: 0 10px 30px rgba(0,0,0,0.3);
    text-align: center;
    max-width: 500px;
}

h1 {
    color: #333;
    margin-bottom: 20px;
}

p {
    color: #666;
    margin-bottom: 15px;
    font-size: 16px;
}

.info {
    background: #f5f5f5;
    padding: 20px;
    border-radius: 5px;
    margin: 20px 0;
}

.info p {
    margin: 10px 0;
}

button {
    background: #667eea;
    color: white;
    border: none;
    padding: 12px 30px;
    border-radius: 5px;
    cursor: pointer;
    font-size: 16px;
    transition: background 0.3s;
    margin: 5px;
}

button:hover {
    background: #5568d3;
}

.api-section {
    background: #e8f4f8;
    padding: 20px;
    border-radius: 5px;
    margin-top: 20px;
    border-left: 4px solid #667eea;
}

.api-section h2 {
    color: #333;
    font-size: 18px;
    margin-bottom: 15px;
}

#api-status {
    font-weight: bold;
    margin: 10px 0;
}
Dockerfile 소스보기
  • 경로 : my-web-app/Dockerfile
# Nginx 경량 이미지 사용
FROM nginx:alpine

# 작성한 HTML, CSS, JS 파일을 Nginx의 웹 루트로 복사
COPY index.html /usr/share/nginx/html/
# COPY style.css /usr/share/nginx/html/
# COPY script.js /usr/share/nginx/html/

COPY assets /usr/share/nginx/html/assets

# Nginx 포트
EXPOSE 80

# 컨테이너 실행 시 Nginx 실행
CMD ["nginx", "-g", "daemon off;"]

 

프론트 관련 Docker 이미지 빌드 및 실행

  • 이미지 빌드
docker build -t my-web-app:1.0 .
  • 컨테이너 실행
docker run -d -p 80:80 --name my-web-container my-web-app:1.0
  • 브라우저 접속 : http://localhost

2.2 backend 관련 작성

jdk 21, spring boot 4.0.0

디렉토리 구조

my-spring-boot-app
  ├── build/ ...
  ├── src/main/java/org/example/myspringbootapp/
  │   ├── Application.java
  │   └── ExamController.java
  │
  ├── build.gradle
  └── Dockerfile

 

build.gradle 의존성

  • 의존성 Spring Web MVC 추가
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

소스작성

Application.java 소스보기
package org.example.myspringbootapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MySpringBootAppApplication {

    public static void main(String[] args) {
        SpringApplication.run(MySpringBootAppApplication.class, args);
    }

}
ExamController.java 소스보기
package org.example.myspringbootapp.controller;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin(origins = "*") // f-end에서 api 호출 CORS
@RestController
public class ExamController {
    @GetMapping("/hello") // 프론트에서 받는 요청 경로
    public String hello() {
        return "Hello k8s";
    }

}
Dockerfile 소스보기
# Java 21 환경의 기본 이미지 사용
FROM eclipse-temurin:21-jdk

# 빌드된 JAR 파일 경로
ARG JAR_FILE=build/libs/my-spring-boot-app-0.0.1-SNAPSHOT.jar

# 최종적으로 해당 jar를 app.jar라는 이름으로 복사
COPY ${JAR_FILE} app.jar

# 컨테이너 8080 포트 오픈 (Spring Boot 기본)
EXPOSE 8080

# 컨테이너가 실행될 때 Spring Boot JAR 실행
ENTRYPOINT ["java", "-jar", "/app.jar"]

백엔드 Docker 이미지 빌드 및 실행

  • 스프링 우선 빌드
./gradlew build -x test
  • 이미지 빌드
docker build -t my-spring-boot-app .
  • 컨테이너 실행
docker run -d -p 8080:8080 --name my-spring-boot-container my-spring-boot-app:1.0

2.3 프론트-백엔드 연동 테스트 확인

docker에서 각 컨테이너가 모두 실행된 상태에서 브라우저로 접속

  • http://localhost


3. 스케일링과 롤링 업데이트

실행 중인 Pod 개수를 늘리거나 줄여서 트래픽 부하에 대응하는 기능

  • 수평확장
    • Pod의 개수를 늘리거나 줄이는 방식
    • 쿠버네티스의 기본 스케일
  • 수직확장
    • Pod의 리소스(CPU, 메모리 등)를 늘리거나 줄이는 방식
    • 일반적으로 권장되지 않음 (재시작 필요)

3.1 예시용 디플로이먼트 작성

  • exam-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: exam-deployment
spec:
  replicas: 2               # 기본 pod 2개
  selector:
    matchLabels:
      app: nginx-app
  template:
    metadata:
      labels:
        app: nginx-app
    spec:
      containers:
      - name: nginx-container
        image: nginx:alpine      # 기본 버전
        ports:
        - containerPort: 80
# 배포
kubectl apply -f exam-deployment.yml

# 배포 확인
kubectl get deployments
kubectl get pods

3.2 수동 스케일링 방식

  • pod 수 늘리거나 줄이기
# 수동 스케일링 (pod 수 5개로 변경)
kubectl scale deployment exam-deployment --replicas=5

# 상태 확인 (label로 필터링)
kubectl get pods -l app=nginx-app

3.3 자동 스케일링 방식 (HPA) - 비권장

  • CPU 사용량 기준으로 자동 스케일링 설정
# 자동 스케일링 설정 (최소 2개, 최대 10개, CPU 사용량 50% 기준)
kubectl autoscale deployment exam-deployment --min=2 --max=10 --cpu-percent=50

# hpa 리소스 상태 확인
kubectl get hpa

# 해당 hpa 삭제
kubectl delete hpa exam-deployment
# 원래대로 복구
kubectl scale deploy exam-deployment --replicas=2

3.4 롤링 업데이트

서비스 중단 없이 새 버전을 배포

  • 이미지 버전 업데이트시
# 이미지 버전 업데이트 (nginx:1.24-alpine 으로 변경)
kubectl set image deployment/exam-deployment nginx-container=nginx:1.24-alpine
# kubectl set image deployment/web-deployment web-container=my-web-app:2.0

# 롤아웃 상태 확인 (배포 상태 모니터링)
kubectl rollout status deployment web-deployment

# 실시간 pod 상태 확인 (-w : watch 모드)
kubectl get pods -w -l app=nginx-app
  • 롤백 (이전 버전으로 복구)
# 해당 디플로이먼트의 버전 히스토리 확인
kubectl rollout history deployment exam-deployment

# 이전 버전으로 롤백
# 바로 이전 버전
kubectl rollout undo deployment exam-deployment
# 리비전 방식 (특정 버전으로 복구)
kubectl rollout undo deployment exam-deployment --to-revision=1

# 롤아웃 상태 확인
kubectl rollout status deployment exam-deployment
starweb
@starweb :: starweb 님의 블로그

starweb 님의 블로그 입니다.

공감하셨다면 구독도 환영합니다!

목차