Initial commit: k8s cluster manifests and scripts

This commit is contained in:
2026-04-01 15:34:02 +08:00
commit 2f04fa4473
49 changed files with 4109 additions and 0 deletions

35
apps/codimd/backup.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
set -o pipefail
APP_NAME="codimd"
BACKUP_DIR="./backup"
TIMESTAMP=$(date +%Y%m%d-%H%M)
DB_BACKUP_FILE="${BACKUP_DIR}/${APP_NAME}-db-${TIMESTAMP}.sql.gz"
UPLOADS_BACKUP_FILE="${BACKUP_DIR}/${APP_NAME}-uploads-${TIMESTAMP}.tar.gz"
# 1. 取得 Pod 名稱
DB_POD=$(kubectl get pod -l app=codimd-db -o jsonpath="{.items[0].metadata.name}" 2>/dev/null)
APP_POD=$(kubectl get pod -l app=codimd -o jsonpath="{.items[0].metadata.name}" 2>/dev/null)
mkdir -p $BACKUP_DIR
# --- A. 備份資料庫 ---
echo "--- 正在備份資料庫... ---"
if kubectl exec $DB_POD -- pg_dump -U codimd codimd | gzip > $DB_BACKUP_FILE; then
echo "✅ 資料庫備份成功: $(basename $DB_BACKUP_FILE) ($(stat -c%s $DB_BACKUP_FILE) bytes)"
else
echo "❌ 資料庫備份失敗!"
exit 1
fi
# --- B. 備份上傳檔案 (Uploads) ---
echo "--- 正在備份上傳圖片 (Uploads)... ---"
# 使用 tar 打包 Pod 內的 uploads 資料夾,並傳回本機
if kubectl exec $APP_POD -- tar zcf - -C /home/hackmd/app/public uploads > $UPLOADS_BACKUP_FILE; then
echo "✅ 圖片備份成功: $(basename $UPLOADS_BACKUP_FILE) ($(stat -c%s $UPLOADS_BACKUP_FILE) bytes)"
else
echo "❌ 圖片備份失敗!"
exit 1
fi
echo "--- ✨ 備份任務完成! ---"

Binary file not shown.

Binary file not shown.

2
apps/codimd/deploy.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
kubectl apply -f manifests/

View File

@@ -0,0 +1,14 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: codimd-cors
namespace: default
spec:
headers:
accessControlAllowMethods: ["GET", "OPTIONS", "PUT", "POST"]
accessControlAllowOriginList: ["*"]
accessControlAllowHeaders: ["*"]
accessControlMaxAge: 100
addVaryHeader: true
customResponseHeaders:
Cache-Control: "no-cache, no-store, must-revalidate"

View File

@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: codimd-secrets
type: Opaque
stringData:
POSTGRES_PASSWORD: "8xG94tL0uKlGiBEd"
CMD_DB_URL: "postgres://codimd:8xG94tL0uKlGiBEd@codimd-db/codimd"

View File

@@ -0,0 +1,21 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: codimd-db-pvc
spec:
storageClassName: longhorn
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: codimd-upload-pvc
spec:
storageClassName: longhorn
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi

View File

@@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: codimd-db
spec:
replicas: 1
strategy:
type: Recreate # 確保 RWO 磁碟順利切換節點
selector:
matchLabels:
app: codimd-db
template:
metadata:
labels:
app: codimd-db
spec:
containers:
- name: postgres
image: postgres:17-alpine
env:
- name: POSTGRES_USER
value: "codimd"
- name: POSTGRES_DB
value: "codimd"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: codimd-secrets
key: POSTGRES_PASSWORD
- name: PGDATA
value: "/var/lib/postgresql/data/pgdata"
volumeMounts:
- mountPath: "/var/lib/postgresql/data"
name: db-data
volumes:
- name: db-data
persistentVolumeClaim:
claimName: codimd-db-pvc

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: codimd-db
labels:
app: codimd-db
spec:
selector:
app: codimd-db
ports:
- protocol: TCP
port: 5432
targetPort: 5432

View File

@@ -0,0 +1,43 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: codimd-app
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: codimd
template:
metadata:
labels:
app: codimd
spec:
securityContext: # <-- 這裡開始
runAsUser: 0
fsGroup: 0 # <-- 結束
containers:
- name: codimd
image: hackmdio/hackmd:latest
env:
- name: CMD_DB_URL
valueFrom:
secretKeyRef:
name: codimd-secrets
key: CMD_DB_URL
- name: CMD_USECDN
value: "false"
- name: CMD_DOMAIN
value: "codimd.lotimmy.com"
- name: CMD_PROTOCOL_USESSL
value: "true"
- name: CMD_CSP_ENABLE
value: "false"
volumeMounts:
- mountPath: "/home/hackmd/app/public/uploads"
name: upload-data
volumes:
- name: upload-data
persistentVolumeClaim:
claimName: codimd-upload-pvc

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: codimd-service
labels:
app: codimd
spec:
selector:
app: codimd
ports:
- protocol: TCP
name: http
port: 3000
targetPort: 3000

View File

@@ -0,0 +1,28 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: codimd-ingress
labels:
app: codimd
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web, websecure
traefik.ingress.kubernetes.io/router.middlewares: default-codimd-cors@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- codimd.192.168.42.120.nip.io
secretName: nip-io-cert
rules:
- host: codimd.lotimmy.com
http: &http_rules
paths:
- path: /
pathType: Prefix
backend:
service:
name: codimd-service
port:
number: 3000
- host: codimd.192.168.42.120.nip.io
http: *http_rules

67
apps/codimd/restore.sh Executable file
View File

@@ -0,0 +1,67 @@
#!/bin/bash
# ==========================================
# CodiMD 一鍵完整還原腳本 (DB + Uploads)
# ==========================================
# 1. 自動偵測最新的備份檔案
LATEST_DB=$(ls -t ./backup/codimd-db-*.sql.gz 2>/dev/null | head -1)
LATEST_IMG=$(ls -t ./backup/codimd-uploads-*.tar.gz 2>/dev/null | head -1)
if [ -z "$LATEST_DB" ] || [ -z "$LATEST_IMG" ]; then
echo "❌ 錯誤: 在 ./backup 找不到完整的備份檔 (需要 .sql.gz 和 .tar.gz)"
exit 1
fi
echo "--- 📋 準備還原以下備份 ---"
echo "📅 資料庫: $LATEST_DB"
echo "🖼️ 圖片檔: $LATEST_IMG"
echo "------------------------------------------"
read -p "⚠️ 警告:這將會覆蓋目前容器內的所有資料!確定要執行嗎?(y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "操作已取消。"
exit 1
fi
# 2. 取得 Pod 名稱
DB_POD=$(kubectl get pod -l app=codimd-db -o jsonpath="{.items[0].metadata.name}" 2>/dev/null)
APP_POD=$(kubectl get pod -l app=codimd -o jsonpath="{.items[0].metadata.name}" 2>/dev/null)
if [ -z "$DB_POD" ] || [ -z "$APP_POD" ]; then
echo "❌ 錯誤: 找不到運作中的 CodiMD Pod請確認 K8s 資源狀態。"
exit 1
fi
# --- A. 還原資料庫 (含清空邏輯) ---
echo "--- 🗄️ 正在還原資料庫結構與內容... ---"
# 先清空 Schema 以避免重複匯入導致的鍵值衝突
kubectl exec -i $DB_POD -- psql -U codimd -d codimd -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" > /dev/null
zcat "$LATEST_DB" | kubectl exec -i $DB_POD -- psql -U codimd -d codimd > /dev/null
if [ $? -eq 0 ]; then
echo "✅ 資料庫還原成功。"
else
echo "❌ 資料庫還原發生錯誤。"
exit 1
fi
# --- B. 還原圖片檔案 ---
echo "--- 🖼️ 正在同步圖片檔案 (Uploads)... ---"
# 清空目前目錄避免新舊檔案混雜
kubectl exec $APP_POD -- sh -c "rm -rf /home/hackmd/app/public/uploads/*"
# 解壓縮並傳入 Pod
cat "$LATEST_IMG" | kubectl exec -i $APP_POD -- tar zxf - -C /home/hackmd/app/public/
# 修正權限確保 CodiMD 服務 (UID 1500) 擁有存取權
kubectl exec $APP_POD -- chown -R 1500:1500 /home/hackmd/app/public/uploads
echo "✅ 圖片還原與權限修正完成。"
# 3. 重啟服務以套用變更
echo "--- 🔄 正在重啟應用程式容器... ---"
kubectl rollout restart deployment codimd-app
kubectl rollout status deployment codimd-app
echo "------------------------------------------"
echo "✨ 還原程序已執行完畢。"
echo "目前資料已同步至備份檔日期:$(echo $LATEST_DB | grep -oE '[0-9]{8}-[0-9]{4}')"

44
apps/codimd/status.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
APP_NAME="codimd"
echo "=== [ $APP_NAME 資源狀態 ] ==="
# 1. 檢查 Pods
echo "--- Pods ---"
kubectl get pods -l "app in (codimd, codimd-db)" -o wide
echo ""
# 2. 檢查 Services
echo "--- Services ---"
kubectl get svc -l "app in (codimd, codimd-db)"
echo ""
# 3. 檢查 PVC (儲存)
echo "--- Storage (PVC) ---"
kubectl get pvc | grep codimd
echo ""
# 4. 檢查存取入口 (Ingress)
echo "--- Access Points (Ingress) ---"
kubectl get ingress -l "app=codimd" 2>/dev/null || kubectl get ingress | grep codimd
echo ""
# 5. 檢查資料庫連通性 (如果 Pod 在 Running)
DB_POD=$(kubectl get pod -l app=codimd-db -o jsonpath="{.items[0].metadata.name}" 2>/dev/null)
POD_STATUS=$(kubectl get pod $DB_POD -o jsonpath="{.status.phase}" 2>/dev/null)
if [ "$POD_STATUS" == "Running" ]; then
echo "--- Database Connection Check ---"
if kubectl exec $DB_POD -- psql -U codimd -d codimd -c "SELECT 1;" > /dev/null 2>&1; then
echo "✅ Database is Ready and Accessible."
else
echo "❌ Database is Running but Connection Failed."
fi
else
echo "⏳ Waiting for Database Pod to be Ready..."
fi

12
apps/codimd/stop.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
echo "--- 安全停止 CodiMD (保留資料) ---"
# 只刪除 Deployment, Service, Ingress, Middleware
# 故意跳過 02-codimd-pvc.yaml 和 01-codimd-secrets.yaml
kubectl delete -f manifests/03-codimd-db-deployment.yaml --ignore-not-found
kubectl delete -f manifests/05-codimd-app-deployment.yaml --ignore-not-found
kubectl delete -f manifests/04-codimd-db-svc.yaml --ignore-not-found
kubectl delete -f manifests/06-codimd-app-svc.yaml --ignore-not-found
kubectl delete -f manifests/07-codimd-ingress.yaml --ignore-not-found
echo "✅ 服務已停止Longhorn 磁碟資料安全保留。"

View File

@@ -0,0 +1,55 @@
# Makefile: 適用於 docker-compose 專案,自動取用當前資料夾為專案名
COMPOSE := docker-compose
COMPOSE_FILE := docker-compose.yml
PROJECT_NAME := $(notdir $(CURDIR)) # 依照當前資料夾自動命名
# 可選自訂 .env 變數(預設 docker-compose 自動載入)
# ENV_FILE := .env
.PHONY: help up down build restart logs ps exec clean prune
help:
@echo "🔧 Docker Compose Makefile 指令:"
@echo " make up - 啟動所有服務 (背景執行)"
@echo " make up-fg - 前景執行(方便除錯)"
@echo " make down - 停止並移除服務容器"
@echo " make build - 重新建構服務映像檔"
@echo " make restart - 重啟所有服務"
@echo " make logs - 查看服務日誌"
@echo " make ps - 顯示服務狀態"
@echo " make exec - 進入主要容器 shell"
@echo " make clean - 清理所有服務資料(含 volume"
@echo " make prune - 清除未使用的資源"
up:
$(COMPOSE) -p $(PROJECT_NAME) -f $(COMPOSE_FILE) up -d
up-fg:
$(COMPOSE) -p $(PROJECT_NAME) -f $(COMPOSE_FILE) up
down:
$(COMPOSE) -p $(PROJECT_NAME) -f $(COMPOSE_FILE) down
build:
$(COMPOSE) -p $(PROJECT_NAME) -f $(COMPOSE_FILE) build
restart:
$(COMPOSE) -p $(PROJECT_NAME) -f $(COMPOSE_FILE) down
$(COMPOSE) -p $(PROJECT_NAME) -f $(COMPOSE_FILE) up -d
logs:
$(COMPOSE) -p $(PROJECT_NAME) -f $(COMPOSE_FILE) logs -f --tail=100
ps:
$(COMPOSE) -p $(PROJECT_NAME) -f $(COMPOSE_FILE) ps
exec:
$(COMPOSE) -p $(PROJECT_NAME) -f $(COMPOSE_FILE) exec app sh
clean:
$(COMPOSE) -p $(PROJECT_NAME) -f $(COMPOSE_FILE) down -v --remove-orphans
prune:
docker system prune -af --volumes

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

View File

@@ -0,0 +1,42 @@
services:
database:
image: postgres:17-alpine
container_name: codimd_database
environment:
- POSTGRES_USER=codimd
- POSTGRES_PASSWORD=C0d1MdP@ss2025
- POSTGRES_DB=codimd
volumes:
- "database-data:/var/lib/postgresql/data"
restart: always
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
- codimd_net
codimd:
image: hackmdio/hackmd:latest
container_name: codimd_app
environment:
- CMD_DB_URL=postgres://codimd:C0d1MdP@ss2025@database/codimd
- CMD_USECDN=false
depends_on:
- database
ports:
- "3001:3000"
volumes:
- upload-data:/home/hackmd/app/public/uploads
restart: always
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
- codimd_net
volumes:
database-data: {}
upload-data: {}
networks:
codimd_net:
driver: bridge

Binary file not shown.