이전 글에서 웹 기반 Kubernetes 관리 시스템의 필요성과 전체 구조를 다뤘습니다.
이번 글에서는 실제로 제가 사용한 개발 과정 및 핵심 기능 구현 방법을 자세히 다뤄보겠습니다!
1. 핵심 기능 구현
🔹 SSH 기반 K8s 제어 시스템
가장 먼저 해결해야 할 문제는 웹에서 원격 서버의 kubectl 명령어를 자동으로 실행하는 것이었습니다.
lib/k8s-client.js - 핵심 SSH 실행 로직
const { exec } = require('child_process');
class K8sClient {
constructor() {
this.host = '192.168.0.10';
this.user = 'root';
this.password = 'rootPassword';
this.cache = new Map();
}
async executeCommand(command) {
const sshCommand = `sshpass -p '${this.password}' ssh -o StrictHostKeyChecking=no ${this.user}@${this.host} "${command}"`;
return new Promise((resolve, reject) => {
exec(sshCommand, { timeout: 30000 }, (error, stdout, stderr) => {
if (error) {
reject(new Error(`Command failed: ${error.message}`));
} else {
resolve(stdout.trim());
}
});
});
}
// kubectl 명령어들을 랩핑한 메서드들
async getPods() {
const result = await this.executeCommand('kubectl get pods -o json');
return JSON.parse(result).items;
}
async getDeployments() {
const result = await this.executeCommand('kubectl get deployments -o json');
return JSON.parse(result).items;
}
async getServices() {
const result = await this.executeCommand('kubectl get services -o json');
return JSON.parse(result).items;
}
}
module.exports = K8sClient;
🔹 Express API 서버 구성
server.js - RESTful API 구현
const express = require('express');
const cors = require('cors');
const path = require('path');
const K8sClient = require('./lib/k8s-client');
const app = express();
const k8sClient = new K8sClient();
app.use(cors());
app.use(express.json());
app.use(express.static('public'));
// 메인 페이지
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// API 엔드포인트들 (Pod, 배포 자동 생성/삭제기능 추가)
app.get('/api/pods', async (req, res) => {
try {
const pods = await k8sClient.getPods();
res.json(pods);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/deployments', async (req, res) => {
try {
const deployments = await k8sClient.getDeployments();
res.json(deployments);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 배포 스케일링
app.post('/api/scale', async (req, res) => {
const { deployment, replicas } = req.body;
const command = `kubectl scale deployment ${deployment} --replicas=${replicas}`;
try {
await k8sClient.executeCommand(command);
res.json({ success: true, message: `Scaled ${deployment} to ${replicas} replicas` });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 리소스 삭제
app.delete('/api/pods/:name', async (req, res) => {
const { name } = req.params;
const command = `kubectl delete pod ${name}`;
try {
await k8sClient.executeCommand(command);
res.json({ success: true, message: `Pod ${name} deleted` });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`K8s Web Manager running on http://localhost:${PORT}`);
});
2. 프론트엔드 SPA 구현
🔹 HTML 구조 설계
public/index.html - 탭 기반 인터페이스
현재는 Master, Work 노드 정보를 정적으로 가져오지만 추후 동적으로 자동 인식할 수 있게끔 변경할 예정
<!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="style.css">
</head>
<body>
<div class="container">
<header>
<h1>🚀 Kubernetes 테스트 자동화</h1>
<div class="cluster-info">
<span>마스터: 192.168.0.10</span>
<span>워커: 192.168.0.11, 192.168.0.12</span>
</div>
</header>
<nav class="tab-nav">
<button class="tab active" data-tab="overview">개요</button>
<button class="tab" data-tab="deployments">Deployments</button>
<button class="tab" data-tab="services">Services</button>
<button class="tab" data-tab="pods">Pods</button>
<button class="tab" data-tab="yaml">YAML 편집</button>
</nav>
<main class="content">
<div id="overview" class="tab-content active">
<div class="stats-grid">
<div class="stat-card">
<h3>클러스터 상태</h3>
<div id="cluster-status">Loading...</div>
</div>
<div class="stat-card">
<h3>Pod 현황</h3>
<div id="pod-summary">Loading...</div>
</div>
</div>
</div>
<div id="deployments" class="tab-content">
<div class="toolbar">
<button onclick="app.refreshDeployments()">🔄 새로고침</button>
</div>
<table id="deployments-table">
<thead>
<tr>
<th>이름</th>
<th>상태</th>
<th>복제본</th>
<th>생성 시간</th>
<th>작업</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<!-- 다른 탭들도 비슷한 구조... -->
</main>
</div>
<script src="script.js"></script>
</body>
</html>
🔹 JavaScript SPA 로직
public/script.js - 핵심 SPA 프론트엔드 로직
주요 기능들
: {Overview, Deployments, Services, Pods} 탭 기반 네비게이션, 실시간 데이터 자동고침 (5초), Pod 관리, Deployment 스케일링(Replica 개수 조정), 성공/에러 알림, Pod 실행시간 표기
class K8sWebManager {
constructor() {
this.currentTab = 'overview';
this.autoRefreshInterval = null;
this.init();
}
init() {
this.setupEventListeners();
this.loadOverview();
this.startAutoRefresh();
}
setupEventListeners() {
// 탭 전환
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const tabName = e.target.dataset.tab;
this.switchTab(tabName);
});
});
}
switchTab(tabName) {
// 기존 탭 비활성화
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// 새 탭 활성화
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
document.getElementById(tabName).classList.add('active');
this.currentTab = tabName;
this.loadTabData(tabName);
}
async loadTabData(tabName) {
switch(tabName) {
case 'overview':
await this.loadOverview();
break;
case 'deployments':
await this.loadDeployments();
break;
case 'services':
await this.loadServices();
break;
case 'pods':
await this.loadPods();
break;
}
}
async loadPods() {
try {
const response = await fetch('/api/pods');
const pods = await response.json();
this.renderPodsTable(pods);
} catch (error) {
console.error('Failed to load pods:', error);
this.showError('Pod 정보를 불러올 수 없습니다.');
}
}
renderPodsTable(pods) {
const tbody = document.querySelector('#pods-table tbody');
tbody.innerHTML = '';
pods.forEach(pod => {
const row = document.createElement('tr');
const status = pod.status.phase;
const restarts = pod.status.containerStatuses ?
pod.status.containerStatuses[0].restartCount : 0;
const age = this.calculateAge(pod.metadata.creationTimestamp);
const ip = pod.status.podIP || 'N/A';
const node = pod.spec.nodeName || 'N/A';
row.innerHTML = `
<td><input type="checkbox" data-pod="${pod.metadata.name}"></td>
<td>${pod.metadata.name}</td>
<td><span class="status ${status.toLowerCase()}">${status}</span></td>
<td>${restarts}</td>
<td>${age}</td>
<td>${ip}</td>
<td>${node}</td>
<td>
<button onclick="app.deletePod('${pod.metadata.name}')" class="btn-danger">🗑️</button>
<button onclick="app.viewLogs('${pod.metadata.name}')" class="btn-info">📋</button>
</td>
`;
tbody.appendChild(row);
});
}
async scaleDe deployment(deploymentName) {
const replicas = prompt(`${deploymentName}의 복제본 수를 입력하세요:`);
if (replicas === null) return;
try {
const response = await fetch('/api/scale', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
deployment: deploymentName,
replicas: parseInt(replicas)
})
});
const result = await response.json();
if (result.success) {
this.showSuccess(`${deploymentName}을 ${replicas}개로 스케일링했습니다.`);
this.loadDeployments(); // 새로고침
} else {
this.showError(result.error);
}
} catch (error) {
this.showError('스케일링 중 오류가 발생했습니다.');
}
}
calculateAge(timestamp) {
const now = new Date();
const created = new Date(timestamp);
const diffMs = now - created;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffDays > 0) return `${diffDays}d`;
if (diffHours > 0) return `${diffHours}h`;
return `${diffMins}m`;
}
startAutoRefresh() {
this.autoRefreshInterval = setInterval(() => {
this.loadTabData(this.currentTab);
}, 5000); // 5초마다 새로고침
}
showSuccess(message) {
// 성공 토스트 메시지 표시
this.showToast(message, 'success');
}
showError(message) {
// 에러 토스트 메시지 표시
this.showToast(message, 'error');
}
showToast(message, type) {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
}
// 애플리케이션 초기화
const app = new K8sWebManager();
3. 중요한 기술적 해결책
🔹 YAML 파일 안전한 저장
가장 까다로웠던 문제 중 하나가 특수문자가 포함된 명령어가 포함된 YAML 파일이 올바르게 배포되지 않았습니다.
해결 방안은 커맨드 처리 부분을 Base64 인코딩 처리하는 것이었습니다.
문제가 되었던 YAML 내용:
command: ["/bin/bash", "-c", "/app/dca/dca.sh start && tail -f /dev/null"]
해결책: Base64 인코딩
// k8s-client.js에 추가
async writeYamlFile(filename, content) {
// Base64로 인코딩해서 특수문자 문제 해결
const base64Content = Buffer.from(content).toString('base64');
const command = `echo '${base64Content}' | base64 -d > ~/k8s_test/${filename}`;
await this.executeCommand(command);
console.log(`YAML 파일 저장 완료: ${filename}`);
}
🔹 성능 최적화를 위한 캐싱 시스템
매번 탭을 선택할 때마다 객체 리스트를 새로 가져오는 문제가 있었습니다. 이에 따른 페이지 로딩 문제가 발생하였는데요.
API 호출을 줄이고 응답 속도를 향상시키기 위해 캐싱 시스템을 추가적으로 구현해주었습니다.
저는 임의로 10초 단위로 캐싱을 하도록 구성을 하였으나, 시간을 좀 더 늘리고 수동 새로고침 버튼을 구현하는 것도 좋은 방안이 될 것 같습니다.
// k8s-client.js에 캐싱 로직 추가
async getPodsWithCache() {
const cacheKey = 'pods';
const cached = this.cache.get(cacheKey);
// 10초 이내의 캐시는 재사용
if (cached && Date.now() - cached.timestamp < 10000) {
return cached.data;
}
// 캐시가 없거나 만료된 경우 새로 조회
const result = await this.executeCommand('kubectl get pods -o json');
const data = JSON.parse(result).items;
this.cache.set(cacheKey, {
data,
timestamp: Date.now()
});
return data;
}
4. 개발 중 이슈 해결한 부분들
사실 아래 두 가지 이슈의 명확한 원인은 아직 찾지 못하였습니다. 일단 임의로 해결하도록 우회처리하였고,
추후 명확한 원인과 해결책을 찾게 되면 글을 갱신할 예정입니다!
Challenge 1: SSH 연결 안정성
문제: 네트워크 불안정으로 간헐적 연결 실패
해결: Exponential Backoff 재시도 로직처리하여 해결
async executeCommandWithRetry(command, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await this.executeCommand(command);
} catch (error) {
if (attempt === retries) throw error;
// 지수적 증가 딜레이 (1초 → 2초 → 4초)
const delay = 1000 * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Challenge 2: 실시간 UI 업데이트
문제: 상태 변경 시 즉시 UI 반영이 되지 않아 Pods, Deployments, Services 변경사항이 동적 반영되지 않음
해결: 이벤트 기반 상태 관리하도록 처리하여 실시간 반영이 가능
class StateManager {
constructor() {
this.subscribers = {};
}
subscribe(event, callback) {
if (!this.subscribers[event]) {
this.subscribers[event] = [];
}
this.subscribers[event].push(callback);
}
emit(event, data) {
if (this.subscribers[event]) {
this.subscribers[event].forEach(callback => callback(data));
}
}
}
🔹 다음 글 예고
다음 글에서는 고급 기능들과 성능 최적화를 다뤄볼 예정입니다. 추가적으로 각 탭 별 인터페이스도 한 번 소개해보겠습니다.
- 이미지 기반 자동 YAML 생성
- 실시간 로그 스트리밍
- 벌크 작업 및 다중 선택
- 모니터링 대시보드 고도화
감사합니다!

'기술 공부 > Cloud&Container' 카테고리의 다른 글
| AWS 클라우드 아키텍처 구성 실습 - RDS+WordPress 멀티 인스턴스 구성 (0) | 2026.02.18 |
|---|---|
| AWS VPC 네트워킹 구성 실습 - Public/Private 서브넷과 ALB (0) | 2026.02.08 |
| [Kubernetes 실습 #3] kubectl 명령어의 한계와 웹 기반 관리 시스템 구상 (3) | 2025.08.10 |
| [Kubernetes 실습 #2] Podman 이미지로 Kubernetes 배포 및 서비스 구성하기 (1) | 2025.07.20 |
| [Kubernetes 실습 #1] Podman을 이용한 컨테이너 이미지 생성 및 로컬 레지스트리에 Push하기 (0) | 2025.07.19 |