本文详细介绍如何使用 Kubernetes 部署 DjangoBlog,包括存储配置、服务编排、Nginx 反向代理、HTTPS 证书自动签发,以及 Whoosh 和 Elasticsearch 两种搜索引擎的配置方法。
项目 Docker 镜像已发布到 Docker Hub:liangliangyy/djangoblog
整体架构
部署完成后,整个系统由以下组件构成:
Internet
│
▼
Ingress (ingress-nginx + cert-manager)
│ HTTPS TLS 1.2/1.3
▼
Nginx Pod ← 静态文件服务 + 反向代理
│
▼
DjangoBlog Pod ← Gunicorn,端口 8000
├── MySQL Pod ← 数据库,端口 3306
├── Redis Pod ← 缓存,端口 6379
└── Elasticsearch ← 搜索(可选),端口 9200
各组件通过 Kubernetes ClusterIP Service 互联,外部流量只通过 Ingress 进入,内部服务不对外暴露。
前置条件
K8s 集群中需要提前安装:
- ingress-nginx:处理外部 HTTP/HTTPS 流量入口
- cert-manager:自动申请和续期 Let's Encrypt 证书
# 安装 ingress-nginx
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml
# 安装 cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
# 创建 Let's Encrypt ClusterIssuer
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
EOF
环境变量说明
DjangoBlog 的 settings.py 已经将所有关键配置通过环境变量读取,用户无需编写任何额外的 settings 文件,只需在 ConfigMap 里配置对应的环境变量即可。
完整的环境变量列表:
| 环境变量 | 说明 | 示例值 |
|---|---|---|
DJANGO_SECRET_KEY |
Django 密钥(必填) | 随机长字符串 |
DJANGO_DEBUG |
调试模式 | False |
DJANGO_MYSQL_DATABASE |
数据库名 | djangoblog |
DJANGO_MYSQL_USER |
数据库用户 | root |
DJANGO_MYSQL_PASSWORD |
数据库密码 | your-password |
DJANGO_MYSQL_HOST |
数据库地址 | db(K8s Service 名) |
DJANGO_MYSQL_PORT |
数据库端口 | 3306 |
DJANGO_REDIS_URL |
Redis 地址(设置后自动启用 Redis 缓存) | redis:6379 |
DJANGO_ELASTICSEARCH_HOST |
ES 地址(设置后自动切换 ES 搜索) | http://elasticsearch:9200 |
ELASTICSEARCH_USERNAME |
ES 用户名(可选) | elastic |
ELASTICSEARCH_PASSWORD |
ES 密码(可选) | your-es-password |
ELASTICSEARCH_API_KEY |
ES API Key(可选) | your-api-key |
DJANGO_EMAIL_USER |
发件邮箱 | [email protected] |
DJANGO_EMAIL_PASSWORD |
邮箱密码 | your-email-password |
DJANGO_EMAIL_HOST |
SMTP 服务器 | smtp.exmail.qq.com |
DJANGO_EMAIL_PORT |
SMTP 端口 | 587 |
MYSQL_ROOT_PASSWORD |
MySQL root 密码(MySQL 容器初始化用) | 与 DJANGO_MYSQL_PASSWORD 相同 |
MYSQL_DATABASE |
MySQL 初始化数据库名 | djangoblog |
第一步:创建命名空间
kubectl create namespace djangoblog
第二步:StorageClass 与持久化存储
StorageClass
使用本地存储,适合单节点或自建集群:
# storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
PersistentVolume
在宿主机节点上提前创建目录:
mkdir -p /mnt/local-storage-db
mkdir -p /mnt/local-storage-djangoblog
mkdir -p /mnt/resource
# 如果使用 Elasticsearch,还需要:
mkdir -p /mnt/local-storage-elasticsearch
对应的 PV 配置:
# pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-db
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/local-storage-db
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- <your-node-name> # 替换为实际节点名
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-djangoblog
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/local-storage-djangoblog
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- <your-node-name>
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-resource
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/resource
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- <your-node-name>
各 PV 用途说明:
| PV 名称 | 大小 | 宿主机路径 | 用途 |
|---|---|---|---|
| local-pv-db | 10Gi | /mnt/local-storage-db | MySQL 数据 |
| local-pv-djangoblog | 5Gi | /mnt/local-storage-djangoblog | Django collectstatic 静态文件 |
| local-pv-resource | 5Gi | /mnt/resource | 用户上传资源文件 |
| local-pv-elasticsearch | 5Gi | /mnt/local-storage-elasticsearch | ES 数据(使用 ES 时才需要) |
PVC
# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-pvc
namespace: djangoblog
spec:
storageClassName: local-storage
volumeName: local-pv-db
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: djangoblog-pvc
namespace: djangoblog
spec:
storageClassName: local-storage
volumeName: local-pv-djangoblog
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: resource-pvc
namespace: djangoblog
spec:
storageClassName: local-storage
volumeName: local-pv-resource
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
第三步:ConfigMap 配置
环境变量 ConfigMap
这是最核心的配置,将所有必要参数通过环境变量注入到 Django 容器:
# configmap.yaml(djangoblog-env)
apiVersion: v1
kind: ConfigMap
metadata:
name: djangoblog-env
namespace: djangoblog
data:
# 数据库
DJANGO_MYSQL_DATABASE: djangoblog
DJANGO_MYSQL_USER: root
DJANGO_MYSQL_PASSWORD: <your-mysql-password>
DJANGO_MYSQL_HOST: db # K8s Service 名,Pod 间 DNS 解析
DJANGO_MYSQL_PORT: "3306"
MYSQL_ROOT_PASSWORD: <your-mysql-password>
MYSQL_DATABASE: djangoblog
# Redis 缓存(设置后自动启用 Redis 替代内存缓存)
DJANGO_REDIS_URL: "redis:6379"
# Django 基础配置
DJANGO_DEBUG: "False"
DJANGO_SECRET_KEY: <your-secret-key> # 生产环境必须使用随机长字符串
# 邮件通知(可选)
DJANGO_EMAIL_USER: [email protected]
DJANGO_EMAIL_PASSWORD: <your-email-password>
DJANGO_EMAIL_HOST: smtp.exmail.qq.com
DJANGO_EMAIL_PORT: "587"
# Elasticsearch(可选,设置后自动切换搜索引擎)
# DJANGO_ELASTICSEARCH_HOST: http://elasticsearch:9200
注意:DJANGO_MYSQL_HOST: db 和 DJANGO_REDIS_URL: redis:6379 使用 K8s Service 名称。Pod 之间通过集群内 DNS 解析,无需配置 IP。
Nginx ConfigMap
# configmap.yaml(web-nginx-config)
apiVersion: v1
kind: ConfigMap
metadata:
name: web-nginx-config
namespace: djangoblog
data:
nginx.conf: |
user nginx;
worker_processes auto;
events {
worker_connections 1024;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
gzip_comp_level 8;
gzip_types text/plain application/javascript text/css application/xml;
include /etc/nginx/conf.d/*.conf;
}
djangoblog.conf: |
server {
server_name yourdomain.com;
listen 80;
keepalive_timeout 70;
# 静态文件直接由 Nginx 服务,不经过 Django
location /static/ {
expires max;
alias /code/djangoblog/collectedstatic/;
}
# 动态请求转发给 DjangoBlog(gunicorn)
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://djangoblog:8000;
break;
}
}
}
server {
server_name www.yourdomain.com;
listen 80;
return 301 https://yourdomain.com$request_uri;
}
Nginx 和 DjangoBlog 共享同一个 djangoblog-pvc,Nginx 直接读取 collectedstatic/ 里的静态文件,不经过 Python 进程。
第四步:Deployment 配置
DjangoBlog
# deployment.yaml(djangoblog 部分)
apiVersion: apps/v1
kind: Deployment
metadata:
name: djangoblog
namespace: djangoblog
spec:
replicas: 1
selector:
matchLabels:
app: djangoblog
template:
metadata:
labels:
app: djangoblog
spec:
# initContainer:每次 Pod 启动时重建 Whoosh 搜索索引
# 使用 Elasticsearch 时不需要此步骤,可删除
initContainers:
- name: init-whoosh-index
image: liangliangyy/djangoblog:latest
command: ["python", "manage.py", "rebuild_index", "--noinput"]
workingDir: /code/djangoblog
envFrom:
- configMapRef:
name: djangoblog-env
containers:
- name: djangoblog
image: liangliangyy/djangoblog:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: djangoblog-env
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: djangoblog
mountPath: /code/djangoblog/collectedstatic
- name: resource
mountPath: /code/djangoblog/static/
volumes:
- name: djangoblog
persistentVolumeClaim:
claimName: djangoblog-pvc
- name: resource
persistentVolumeClaim:
claimName: resource-pvc
MySQL
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
namespace: djangoblog
spec:
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mysql:latest
command:
- mysqld
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --max_connections=50
- --innodb_buffer_pool_size=128M
- --performance_schema=OFF # 关闭性能监控,节省内存
envFrom:
- configMapRef:
name: djangoblog-env
readinessProbe:
exec:
command: ["mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-p$(MYSQL_ROOT_PASSWORD)"]
initialDelaySeconds: 10
periodSeconds: 30
resources:
requests:
cpu: 50m
memory: 200Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: db-data
mountPath: /var/lib/mysql
volumes:
- name: db-data
persistentVolumeClaim:
claimName: db-pvc
Redis 与 Nginx
# Redis
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: djangoblog
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:latest
ports:
- containerPort: 6379
resources:
limits:
cpu: 200m
memory: 256Mi
---
# Nginx
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: djangoblog
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
- name: nginx-config
mountPath: /etc/nginx/conf.d/default.conf
subPath: djangoblog.conf
- name: djangoblog-pvc
mountPath: /code/djangoblog/collectedstatic # 与 Django 共享静态文件
volumes:
- name: nginx-config
configMap:
name: web-nginx-config
- name: djangoblog-pvc
persistentVolumeClaim:
claimName: djangoblog-pvc
Elasticsearch(可选)
项目内置了带 IK 中文分词插件的 ES 镜像,默认 replicas: 0 不启动,需要时改为 1:
apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
namespace: djangoblog
spec:
replicas: 0 # 需要 ES 时改为 1
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
containers:
- name: elasticsearch
image: liangliangyy/elasticsearch-analysis-ik:8.6.1 # 内置 IK 分词
env:
- name: discovery.type
value: single-node
- name: ES_JAVA_OPTS
value: "-Xms256m -Xmx256m"
- name: xpack.security.enabled
value: "false"
ports:
- containerPort: 9200
resources:
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: elasticsearch-data
mountPath: /usr/share/elasticsearch/data/
volumes:
- name: elasticsearch-data
persistentVolumeClaim:
claimName: elasticsearch-pvc
第五步:Service 配置
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: djangoblog
namespace: djangoblog
spec:
selector:
app: djangoblog
ports:
- port: 8000
targetPort: 8000
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: djangoblog
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: db
namespace: djangoblog
spec:
selector:
app: db
ports:
- port: 3306
targetPort: 3306
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: djangoblog
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: djangoblog
spec:
selector:
app: elasticsearch
ports:
- port: 9200
targetPort: 9200
type: ClusterIP
第六步:Ingress 配置
# gateway.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx
namespace: djangoblog
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.2 TLSv1.3"
spec:
ingressClassName: nginx
rules:
- host: yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
- host: www.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
tls:
- hosts:
- yourdomain.com
- www.yourdomain.com
secretName: djangoblog-tls-secret # cert-manager 自动创建
搜索引擎配置详解
DjangoBlog 支持两种搜索引擎,settings.py 会根据环境变量自动切换,无需修改代码:
方案一:Whoosh(默认,轻量推荐)
不需要任何额外配置,部署后开箱即用。Whoosh 是纯 Python 全文搜索引擎,使用 Jieba 做中文分词。
索引数据存储在容器内 BASE_DIR/whoosh_index 目录。由于 Deployment 中的 initContainer 在每次 Pod 启动时会自动执行 rebuild_index,索引会自动重建,无需持久化。
# settings.py 中的默认配置(无需手动设置)
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
}
}
优点:部署简单,无需额外 Pod,内存占用极低。 缺点:文章量大时重建索引较慢,搜索能力弱于 ES。
方案二:Elasticsearch(高性能可选)
ES 使用 IK 中文分词器(ik_max_word 分词、ik_smart 搜索),搜索质量和性能均优于 Whoosh。
第一步:将 Elasticsearch Deployment 的 replicas 改为 1,并创建 PVC:
# 在宿主机创建 ES 数据目录
mkdir -p /mnt/local-storage-elasticsearch
# 创建对应 PV 和 PVC(参考上方 PV 配置)
kubectl apply -f pv-elasticsearch.yaml
kubectl apply -f pvc-elasticsearch.yaml
# 启动 ES Pod
kubectl -n djangoblog scale deployment elasticsearch --replicas=1
第二步:在 djangoblog-env ConfigMap 中添加 ES 地址,settings.py 会自动切换搜索引擎:
data:
DJANGO_ELASTICSEARCH_HOST: "http://elasticsearch:9200"
# 若开启了认证(xpack.security.enabled=true),还需:
# ELASTICSEARCH_USERNAME: elastic
# ELASTICSEARCH_PASSWORD: <your-password>
# 或使用 API Key:
# ELASTICSEARCH_API_KEY: <your-api-key>
第三步:重启 DjangoBlog Pod,并手动创建 ES 索引:
kubectl -n djangoblog rollout restart deployment/djangoblog
# 等待 Pod 就绪后创建索引
kubectl -n djangoblog exec -it deploy/djangoblog -- \
python manage.py search_index --rebuild -f
两种方案对比:
| 对比项 | Whoosh | Elasticsearch |
|---|---|---|
| 中文分词 | Jieba | IK(ik_max_word/ik_smart) |
| 额外 Pod | 不需要 | 需要(约 256MB+ 内存) |
| 部署复杂度 | 低 | 中 |
| 索引构建 | initContainer 自动重建 | 手动执行一次后实时更新 |
| 搜索质量 | 一般 | 优秀 |
| 适用场景 | 小型博客 | 文章量大或搜索质量要求高 |
第七步:部署顺序
# 1. 创建命名空间
kubectl create namespace djangoblog
# 2. StorageClass
kubectl apply -f storageclass.yaml
# 3. 在宿主机创建目录
mkdir -p /mnt/local-storage-db /mnt/local-storage-djangoblog /mnt/resource
# 4. PV(注意修改 nodeAffinity 中的节点名)
kubectl apply -f pv.yaml
# 5. PVC
kubectl apply -f pvc.yaml
# 6. ConfigMap(先修改密码、域名等配置)
kubectl apply -f configmap.yaml
# 7. Service
kubectl apply -f service.yaml
# 8. Deployment
kubectl apply -f deployment.yaml
# 9. Ingress(先修改域名)
kubectl apply -f gateway.yaml
第八步:初始化
首次部署后执行数据库初始化:
# 等待 Pod 就绪
kubectl -n djangoblog wait --for=condition=ready pod -l app=djangoblog --timeout=120s
# 数据库迁移
kubectl -n djangoblog exec -it deploy/djangoblog -- python manage.py migrate
# 收集静态文件(Nginx 直接服务这些文件)
kubectl -n djangoblog exec -it deploy/djangoblog -- python manage.py collectstatic --noinput
# 创建管理员账号
kubectl -n djangoblog exec -it deploy/djangoblog -- python manage.py createsuperuser
验证与运维
# 查看所有 Pod 状态
kubectl -n djangoblog get pods
# 查看 DjangoBlog 日志
kubectl -n djangoblog logs -f deploy/djangoblog
# 查看证书状态
kubectl -n djangoblog get certificate
# 更新镜像(发布新版本)
kubectl -n djangoblog rollout restart deployment/djangoblog
# 数据库备份
POD=$(kubectl -n djangoblog get pod -l app=db -o jsonpath="{.items[0].metadata.name}")
kubectl -n djangoblog exec $POD -- \
mysqldump -u root -p'<password>' djangoblog > backup_$(date +%Y-%m-%d).sql
总结
整个方案的核心思路是:通过环境变量驱动配置,不需要修改任何代码或编写额外的 settings 文件。
几个关键点:
- 所有密码、连接地址都通过
djangoblog-envConfigMap 注入,切换环境只改 ConfigMap - 搜索引擎通过
DJANGO_ELASTICSEARCH_HOST环境变量自动切换,未设置则默认使用 Whoosh - Redis 缓存通过
DJANGO_REDIS_URL环境变量自动启用 - Nginx 与 DjangoBlog 共享同一个 PVC,实现静态文件的高效服务
- Ingress + cert-manager 自动处理 HTTPS 证书的申请与续期
完整配置文件可参考项目 GitHub 仓库。
本文由 liangliangyy 原创,转载请注明出处。
评论
0