
# 최상위 프로젝트 폴더 생성
mkdir my-blog-service && cd my-blog-service
# 1. API 서버 관련 폴더 및 파일 생성
mkdir api
touch api/server.js api/package.json api/Dockerfile
# 2. Nginx 프론트엔드 관련 폴더 및 파일 생성
mkdir nginx
touch nginx/index.html nginx/nginx.conf nginx/Dockerfile
# 3. 쿠버네티스 배포용 폴더 생성
mkdir k8s
# 4. Docker Compose 파일 생성
touch docker-compose.yml📦my-blog-service
┣ 📂api
┃ ┣ 📜Dockerfile
┃ ┣ 📜package.json
┃ ┗ 📜server.js
┣ 📂k8s
┣ 📂nginx
┃ ┣ 📜Dockerfile
┃ ┣ 📜index.html
┃ ┗ 📜nginx.conf
┗ 📜docker-compose.yml
volumes 설정을 해주는 것이 핵심입니다.api/package.json{
"name": "api",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.3"
}
}api/server.jsconst express = require('express');
const { Pool } = require('pg');
const app = express();
const port = 8000;
app.use(express.json()); // JSON 요청 본문을 파싱하기 위해 추가
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
// 서버 시작 시 테이블 생성 및 초기 데이터 삽입
const initDb = async () => {
await pool.query(`
CREATE TABLE IF NOT EXISTS posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
`);
const res = await pool.query('SELECT COUNT(*) FROM posts');
if (res.rows.count === '0') {
await pool.query(`
INSERT INTO posts (title, content) VALUES
('Docker로 개발하기', 'Docker Compose를 사용하면 정말 편리해요.'),
('Kubernetes란?', '컨테이너 오케스트레이션의 사실상 표준입니다.');
`);
}
};
initDb();
// 모든 글 조회 (Read)
app.get('/api/posts', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM posts ORDER BY id DESC');
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).send('Server Error');
}
});
// 새 글 작성 (Create)
app.post('/api/posts', async (req, res) => {
try {
const { title, content } = req.body;
if (!title) {
return res.status(400).json({ error: 'Title is required' });
}
const newPost = await pool.query(
'INSERT INTO posts (title, content) VALUES ($1, $2) RETURNING *',
[title, content]
);
res.status(201).json(newPost.rows);
} catch (err) {
console.error(err);
res.status(500).send('Server Error');
}
});
// 글 삭제 (Delete)
app.delete('/api/posts/:id', async (req, res) => {
try {
const { id } = req.params;
await pool.query('DELETE FROM posts WHERE id = $1', [id]);
res.status(204).send(); // No Content
} catch (err) {
console.error(err);
res.status(500).send('Server Error');
}
});
app.listen(port, () => {
console.log(`API server listening on port ${port}`);
});api/DockerfileFROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8000
CMD [ "npm", "start" ]nginx/index.html<!DOCTYPE html>
<html>
<head>
<title>My Docker & K8s Blog</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: auto; padding: 20px; }
.post { border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; border-radius: 5px; position: relative; }
.post h2 { margin-top: 0; }
form { margin-bottom: 20px; }
input, textarea { width: 100%; margin-bottom: 10px; padding: 8px; }
button { cursor: pointer; }
.delete-btn { position: absolute; top: 10px; right: 10px; color: red; cursor: pointer; border: 1px solid red; background: none; border-radius: 5px; }
</style>
</head>
<body>
<h1>Docker & Kubernetes 블로그</h1>
<form id="new-post-form">
<h3>새 글 작성</h3>
<input type="text" id="title" placeholder="제목" required>
<textarea id="content" placeholder="내용"></textarea>
<button type="submit">작성</button>
</form>
<hr>
<h2>글 목록</h2>
<div id="posts"></div>
<script>
const postsDiv = document.getElementById('posts');
const form = document.getElementById('new-post-form');
// 글 목록 가져오기 함수
const fetchPosts = async () => {
try {
const response = await fetch('/api/posts');
const posts = await response.json();
postsDiv.innerHTML = ''; // 목록 초기화
posts.forEach(post => {
const postDiv = document.createElement('div');
postDiv.className = 'post';
postDiv.innerHTML = `
<h2>${post.title}</h2>
<p>${post.content || ''}</p>
<button class="delete-btn" data-id="${post.id}">삭제</button>
`;
postsDiv.appendChild(postDiv);
});
} catch (error) {
console.error('Error fetching posts:', error);
}
};
// 새 글 작성 이벤트 리스너
form.addEventListener('submit', async (e) => {
e.preventDefault();
const title = document.getElementById('title').value;
const content = document.getElementById('content').value;
try {
await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
form.reset();
fetchPosts();
} catch (error) {
console.error('Error creating post:', error);
}
});
// 글 삭제 이벤트 리스너 (이벤트 위임)
postsDiv.addEventListener('click', async (e) => {
if (e.target.classList.contains('delete-btn')) {
const id = e.target.dataset.id;
if (confirm('정말로 이 글을 삭제하시겠습니까?')) {
try {
await fetch(`/api/posts/${id}`, { method: 'DELETE' });
fetchPosts();
} catch (error) {
console.error('Error deleting post:', error);
}
}
}
});
// 페이지 로드 시 글 목록 가져오기
fetchPosts();
</script>
</body>
</html>nginx/nginx.conf/api로 오는 요청을 API 서버로 전달(프록시)하는 설정입니다.server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
}
location /api/ {
# 'api'는 docker-compose.yml에 정의된 서비스 이름입니다.
proxy_pass http://api:8000;
}
}nginx/Dockerfileindex.html과 nginx.conf를 담은 커스텀 Nginx 이미지를 만듭니다.FROM nginx:alpine
COPY ./index.html /usr/share/nginx/html/index.html
COPY ./nginx.conf /etc/nginx/conf.d/default.confdocker-compose.ymlversion: '3.8'
services:
nginx:
build: ./nginx
ports:
- "8080:80"
depends_on:
- api
api:
build: ./api
environment:
# 'db'는 이 파일에 정의된 PostgreSQL 서비스 이름입니다.
- DATABASE_URL=postgres://user:password@db:5432/mydatabase
depends_on:
- db
db:
image: postgres:13
restart: always
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mydatabase
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:docker-compose up --build 명령어를 실행하고, 웹 브라우저에서 http://localhost:8080에 접속해보세요.
메인 페이지가 보인다면 로컬 개발 환경 구성에 성공한 것입니다.
k8s 폴더 안에 아래의 YAML 파일들을 모두 작성해주세요.k8s/01-namespace.yamlapiVersion: v1
kind: Namespace
metadata:
name: blog-servicek8s/02-db-secret.yamlapiVersion: v1
kind: Secret
metadata:
name: db-secret
namespace: blog-service
stringData:
POSTGRES_USER: "user"
POSTGRES_PASSWORD: "password"
POSTGRES_DB: "mydatabase"k8s/03-db-pvc.yamlapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-pvc
namespace: blog-service
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gik8s/04-db-deployment.yamlapiVersion: apps/v1
kind: Deployment
metadata:
name: db-deployment
namespace: blog-service
spec:
replicas: 1
selector:
matchLabels: { app: db }
template:
metadata: { labels: { app: db } }
spec:
containers:
- name: postgres
image: postgres:13
envFrom:
- secretRef: { name: db-secret }
ports:
- containerPort: 5432
volumeMounts:
- name: db-storage
mountPath: /var/lib/postgresql/data
volumes:
- name: db-storage
persistentVolumeClaim: { claimName: db-pvc }k8s/05-db-service.yamlapiVersion: v1
kind: Service
metadata:
name: db-service
namespace: blog-service
spec:
selector: { app: db }
ports:
- port: 5432k8s/06-api-deployment.yaml[YOUR_DOCKERHUB_ID]는 실제 ID로 변경 필요)apiVersion: apps/v1
kind: Deployment
metadata:
name: api-deployment
namespace: blog-service
spec:
replicas: 1
selector: { matchLabels: { app: api } }
template:
metadata: { labels: { app: api } }
spec:
containers:
- name: api
# CI/CD를 통해 빌드된 이미지를 사용합니다.
image: [YOUR_DOCKERHUB_ID]/my-blog-api:latest
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
value: "postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@db-service:5432/$(POSTGRES_DB)"
envFrom:
- secretRef: { name: db-secret }k8s/07-api-service.yamlapiVersion: v1
kind: Service
metadata:
name: api-service
namespace: blog-service
spec:
selector: { app: api }
ports:
- port: 8000k8s/08-nginx-deployment.yaml[YOUR_DOCKERHUB_ID]는 실제 ID로 변경 필요)apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: blog-service
spec:
replicas: 1
selector: { matchLabels: { app: nginx } }
template:
metadata: { labels: { app: nginx } }
spec:
containers:
- name: nginx
# CI/CD를 통해 빌드된 이미지를 사용합니다.
image: [YOUR_DOCKERHUB_ID]/my-blog-nginx:latest
ports:
- containerPort: 80k8s/09-nginx-service.yamlapiVersion: v1
kind: Service
metadata:
name: nginx-service
namespace: blog-service
spec:
selector: { app: nginx }
ports:
- port: 80k8s/10-ingress.yamlapiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: blog-ingress
namespace: blog-service
spec:
rules:
- host: "blog.example.com" # 실제 도메인으로 변경하거나 hosts 파일에 등록
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-service
port: { number: 80 }kubectl apply -f k8s/ 명령어 하나로 모든 리소스를 배포합니다.k8s/11-hpa.yamlapiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
namespace: blog-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-deployment
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50kubectl apply -f k8s/11-hpa.yaml 명령어로 HPA를 적용하면 모든 배포 과정이 마무리됩니다.