K8s NFS 动态存储 + ECShop 应用部署
K8s NFS 动态存储 + ECShop 应用部署学习笔记
一、实验目标与整体架构
本次实验的目标是:
- 在 master 节点上搭建 NFS 服务,供 Kubernetes 使用。
- 先使用
Pod + NFS 卷验证静态挂载。 - 部署 NFS Client Provisioner,实现 PVC 的动态供给。
- 在
ecshop命名空间中完成:- ResourceQuota / LimitRange 资源限制;
- MySQL StatefulSet + Headless Service + Secret;
- PHP-FPM Deployment + Service;
- Nginx Deployment + ConfigMap + PVC + NodePort Service;
- 部署 MetalLB,为 LoadBalancer 类型的 Ingress 分配外部 IP。
- 部署 ingress-nginx 并配置 HTTPS/TLS,最终通过域名访问 ECShop。
最终形成一条完整链路:
用户
-> https://shop.shaka.cloud
-> MetalLB 分配的外部地址 10.1.8.40
-> ingress-nginx-controller
-> ecshop/nginx-ecshop Service (80)
-> nginx pod
-> 静态文件由 nginx 返回
-> *.php 请求转发到 php-ecshop:9000
-> php-fpm pod
-> mysql-headless:3306
-> mysql-0 StatefulSet pod
-> NFS 动态 PV(ecshop StorageClass)
二、环境信息
从日志中 kubectl get nodes -o wide 可以看到:
| 节点 | 角色 | IP | 系统 | 内核 | 容器运行时 |
|---|---|---|---|---|---|
| master30.shaka.cloud | control-plane | 10.1.8.30 | Ubuntu 24.04 LTS | 6.8.0-31-generic | containerd 1.7.20 |
| worker31.shaka.cloud | worker | 10.1.8.31 | Ubuntu 24.04 LTS | 6.8.0-31-generic | containerd 1.7.20 |
| worker32.shaka.cloud | worker | 10.1.8.32 | Ubuntu 24.04 LTS | 6.8.0-31-generic | containerd 1.7.20 |
Kubernetes 版本为 v1.30.2,集群中已经预装了 Calico、MetalLB、ingress-nginx 等基础组件。
镜像仓库使用内网代理:
hub.laoma.cloudregistry.cn-hangzhou.aliyuncs.com192.168.42.200(课程资料服务器)
三、NFS 服务端与客户端配置
3.1 master 节点:安装并启动 NFS 服务
日志中的命令有时带有终端输入错误,例如:
systremctl restart bhf nfs-server.service
mkdir -p -m /777 777 /ecshop
这些是课堂敲击时产生的笔误,正确写法如下:
# 安装 NFS 服务端
apt install -y nfs-kernel-server
# 创建共享目录,权限设置为 777
mkdir -p -m 777 /ecshop
# 编辑导出配置
cat > /etc/exports <<'EOF'
/ecshop *(rw,sync,no_root_squash,no_all_squash)
EOF
# 重启 NFS 服务并重新导出
systemctl restart nfs-server
exportfs -rav
exportfs -rav 的典型输出:
exporting *:/ecshop
/ecshop <world>(sync,wdelay,hide,no_subtree_check,sec=sys,rw,secure,no_root_squash,no_all_squash)
3.2 /etc/exports 参数解释
| 参数 | 含义 |
|---|---|
* | 允许所有主机访问 |
rw | 可读写 |
sync | 写入时同步落盘,数据更安全 |
no_root_squash | 客户端 root 用户保留 root 权限 |
no_all_squash | 不将所有用户映射为匿名用户 |
3.3 worker 节点:安装客户端并验证
在 worker31、worker32 上执行:
apt install -y nfs-common
showmount -e master30
验证成功后,应能看到 master 导出的 /ecshop 共享目录。
四、静态 NFS 卷验证:pod-with-nfs.yaml
先创建一个使用 NFS 卷的 nginx Pod:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
run: nginx
spec:
volumes:
- name: nfs
nfs:
server: 10.1.8.30
path: "/ecshop" # 日志中最初误写成 /nfsshares
containers:
- name: nginx
image: hub.laoma.cloud/library/nginx
volumeMounts:
- name: nfs
mountPath: "/usr/share/nginx/html"
应用:
kubectl create ns ecshop
kubectl config set-context --current --namespace ecshop
kubectl apply -f pod-with-nfs.yaml
kubectl describe pod nginx
4.1 第一次失败:mount.nfs: access denied
日志中的关键错误:
MountVolume.SetUp failed for volume "nfs" : mount failed: exit status 32
mount.nfs: access denied by server while mounting 10.1.8.30:/nfsshares
原因:Pod 中配置的 NFS 路径是 /nfsshares,但服务端实际导出的是 /ecshop。
解决:把 YAML 中的 path 改为 /ecshop,并确保 /etc/exports 正确导出 /ecshop。修正后 Pod 状态变为 Running。
4.2 验证结果
kubectl get pods
kubectl describe pod nginx
关键信息:
Status: Running
Node: worker31.shaka.cloud/10.1.8.31
Volumes:
nfs:
Type: NFS
Server: 10.1.8.30
Path: /ecshop
说明 NFS 挂载成功后,nginx 容器的 /usr/share/nginx/html 已经指向 NFS 共享目录。
五、NFS Client Provisioner 动态存储
静态卷需要管理员提前创建 PV,不利于自动化。实验通过 nfs-subdir-external-provisioner 实现:创建 PVC 后自动在 NFS 目录下创建子目录并绑定 PV。
5.1 早期版本踩坑
日志中最先尝试的 provisioner 名称是 fuseim.pri/ifs,出现两类错误:
cannot start watcher for PVC ...: events is forbidden:
User "system:serviceaccount:ecshop:nfs-client-provisioner" cannot list resource "events"
Unexpected error getting claim reference to claim ...: selfLink was empty, can't make reference
原因与解决办法:
- RBAC 权限不完整:ServiceAccount 缺少
events、endpoints等权限,导致 PVC 一直 Pending。 - provisioner 名称与镜像版本不匹配:老版本
fuseim.pri/ifs存在selfLink兼容性问题。 - 后续统一改为
cluster.local/nfs-subdir-external-provisioner,并使用hub.laoma.cloud/sig-storage/nfs-subdir-external-provisioner:v4.0.2。
5.2 最终可用的 nfs-provisioner.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-subdir-external-provisioner-sa
namespace: ecshop
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: nfs-subdir-external-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: run-nfs-subdir-external-provisioner
subjects:
- kind: ServiceAccount
name: nfs-subdir-external-provisioner-sa
namespace: ecshop
roleRef:
kind: ClusterRole
name: nfs-subdir-external-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-subdir-external-provisioner
namespace: ecshop
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-subdir-external-provisioner
template:
metadata:
labels:
app: nfs-subdir-external-provisioner
spec:
serviceAccountName: nfs-subdir-external-provisioner-sa
containers:
- name: nfs-subdir-external-provisioner
image: hub.laoma.cloud/sig-storage/nfs-subdir-external-provisioner:v4.0.2
env:
- name: PROVISIONER_NAME
value: cluster.local/nfs-subdir-external-provisioner
- name: NFS_SERVER
value: 10.1.8.30
- name: NFS_PATH
value: /ecshop
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
volumes:
- name: nfs-client-root
nfs:
server: 10.1.8.30
path: /ecshop
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ecshop
provisioner: cluster.local/nfs-subdir-external-provisioner
reclaimPolicy: Delete
volumeBindingMode: Immediate
应用并检查:
kubectl apply -f nfs-provisioner.yaml
kubectl get sc ecshop
kubectl get pod -n ecshop
StorageClass 结果:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
ecshop cluster.local/nfs-subdir-external-provisioner Delete Immediate
5.3 镜像问题
日志中曾尝试多种镜像并出现 ImagePullBackOff:
registry.cn-hangzhou.aliyuncs.com/open-whale/nfs-client-provisioner:v4.0.0willdockerhub/nfs-client-provisioner:v4.0.0- 旧版
registry.cn-hangzhou.aliyuncs.com/open-ali/nfs-client-provisioner:latest
最终统一使用:
hub.laoma.cloud/sig-storage/nfs-subdir-external-provisioner:v4.0.2
六、ResourceQuota 与 LimitRange
6.1 创建 ResourceQuota
kubectl create quota ecshop \
--hard=cpu=4,memory=8G,requests.cpu=4,requests.memory=8G,requests.storage=500G
查看:
kubectl describe quota ecshop
创建 PVC 后,requests.storage 开始计数。日志中显示 requests.storage = 4294967296,也就是约 4 Gi。
6.2 LimitRange
apiVersion: v1
kind: LimitRange
metadata:
name: ecshop
namespace: ecshop
spec:
limits:
- type: Container
max:
memory: 2048Mi
cpu: "1"
min:
memory: 256Mi
cpu: 100m
default:
memory: 512Mi
cpu: 500m
defaultRequest:
memory: 256Mi
cpu: 200m
kubectl describe limitranges ecshop 结果:
Container cpu 100m 1 200m 500m
Container memory 256Mi 2Gi 256Mi 512Mi
这条 LimitRange 在后面 Nginx 排错中非常关键:当 Deployment 的 requests.memory 写成 128Mi 时,低于 min=256Mi,Pod 无法创建。
七、MySQL 部署
7.1 Secret
先在命名空间中创建 MySQL 凭证:
kubectl create secret generic mysql \
--namespace ecshop \
--from-literal=mysql-root-password=123 \
--from-literal=ecshop-db-user=ecshop \
--from-literal=ecshop-db-password=123
日志中后来使用 YAML 重新创建 Secret,密码为 Laoma@123:
apiVersion: v1
kind: Secret
metadata:
name: mysql
namespace: ecshop
type: Opaque
stringData:
mysql-root-password: "Laoma@123"
ecshop-db-user: "ecshop"
ecshop-db-password: "Laoma@123"
说明:
stringData中的值是课堂实验环境凭证,实际生产环境应使用更安全的密码和 Secret 管理方式。
7.2 Headless Service 与普通 ClusterIP Service
StatefulSet 需要 Headless Service 为每个 Pod 提供稳定的 DNS 名称:
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
namespace: ecshop
spec:
clusterIP: None
selector:
app: mysql
ports:
- port: 3306
targetPort: 3306
---
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: ecshop
spec:
type: ClusterIP
selector:
app: mysql
ports:
- port: 3306
targetPort: 3306
7.3 StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: ecshop
spec:
serviceName: mysql-headless
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: hub.laoma.cloud/library/mysql:5.7
ports:
- containerPort: 3306
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql
key: mysql-root-password
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: mysql
key: ecshop-db-user
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql
key: ecshop-db-password
- name: MYSQL_DATABASE
value: ecshop
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
startupProbe:
exec:
command:
- sh
- -c
- mysql -uroot -p${MYSQL_ROOT_PASSWORD} -e 'SELECT 1;'
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 30
timeoutSeconds: 3
livenessProbe:
exec:
command:
- sh
- -c
- mysqladmin ping -uroot -p${MYSQL_ROOT_PASSWORD}
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- sh
- -c
- mysql -uroot -p${MYSQL_ROOT_PASSWORD} -e 'SHOW DATABASES;'
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 2
successThreshold: 1
failureThreshold: 2
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ecshop
resources:
requests:
storage: 10Gi
7.4 关键点
- StatefulSet 使用
volumeClaimTemplates,会自动为每个副本创建 PVC,名称格式为<template-name>-<statefulset-name>-<序号>,例如mysql-data-mysql-0。 - 日志中最初使用
ReadWriteMany,后来改为ReadWriteOnce。MySQL 单副本场景通常使用 RWO 即可。 - PVC 会先进入
Pending,provisioner 创建 PV 后变为Bound。
最终结果:
mysql-data-mysql-0 Bound pvc-7aa33e5c-... 10Gi RWO ecshop
7.5 排错记录
日志中出现过:
0/3 nodes are available: pod has unbound immediate PersistentVolumeClaims
原因是 PVC 还未成功绑定。等 provisioner 正常工作后,mysql-0 才成功调度并运行。
八、PHP-FPM 部署
8.1 Deployment + Service
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-ecshop
namespace: ecshop
labels:
app: php-ecshop
spec:
replicas: 1
selector:
matchLabels:
app: php-ecshop
template:
metadata:
labels:
app: php-ecshop
spec:
containers:
- name: php-fpm
image: hub.laoma.cloud/library/php:7.2-fpm-ecshop
ports:
- containerPort: 9000
name: fpm
env:
- name: DB_HOST
value: mysql-headless
- name: DB_PORT
value: "3306"
- name: DB_NAME
value: ecshop
- name: DB_USER
valueFrom:
secretKeyRef:
name: mysql
key: ecshop-db-user
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql
key: ecshop-db-password
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
exec:
command:
- bash
- -c
- exec 6<>/dev/tcp/127.0.0.1/9000
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- bash
- -c
- exec 6<>/dev/tcp/127.0.0.1/9000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
---
apiVersion: v1
kind: Service
metadata:
name: php-ecshop
namespace: ecshop
spec:
selector:
app: php-ecshop
ports:
- port: 9000
targetPort: 9000
type: ClusterIP
8.2 镜像名问题
日志中第一次写的是:
image: php:7.2-fpm-ecshop
结果 Pod 状态为:
ImagePullBackOff / ErrImagePull
原因是没有写镜像仓库地址,导致无法从默认仓库拉取。改成:
image: hub.laoma.cloud/library/php:7.2-fpm-ecshop
之后 Pod 正常运行。
8.3 共享代码 PVC
最初的 PHP Pod 没有挂载 Nginx 的代码目录,导致访问 PHP 文件时出现 File not found.。日志中通过 kubectl patch 给 PHP Deployment 挂载同一个 PVC:
kubectl patch deploy -n ecshop php-ecshop --type=strategic -p '{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "php-fpm",
"volumeMounts": [
{"name": "nginx-html", "mountPath": "/var/www/html"}
]
}
],
"volumes": [
{"name": "nginx-html", "persistentVolumeClaim": {"claimName": "nginx-html-pvc"}}
]
}
}
}
}'
随后执行滚动更新:
kubectl rollout status deploy -n ecshop php-ecshop --timeout=120s
这样 Nginx 和 PHP-FPM 就共享同一份 ECShop 代码:
- Nginx 挂在
/usr/share/nginx/html - PHP-FPM 挂在
/var/www/html
九、Nginx 部署与排错
9.1 ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-conf
namespace: ecshop
data:
default.conf: |
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php-ecshop:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
9.2 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nginx-html-pvc
namespace: ecshop
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ecshop
resources:
requests:
storage: 10Gi
9.3 最终 Deployment + NodePort Service
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-ecshop
namespace: ecshop
spec:
replicas: 1
selector:
matchLabels:
app: nginx-ecshop
template:
metadata:
labels:
app: nginx-ecshop
spec:
containers:
- name: nginx
image: hub.laoma.cloud/library/nginx:1.24
ports:
- name: http
containerPort: 80
volumeMounts:
- name: nginx-html
mountPath: /usr/share/nginx/html
- name: nginx-conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
readinessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 15
periodSeconds: 10
volumes:
- name: nginx-html
persistentVolumeClaim:
claimName: nginx-html-pvc
- name: nginx-conf
configMap:
name: nginx-conf
items:
- key: default.conf
path: default.conf
---
apiVersion: v1
kind: Service
metadata:
name: nginx-ecshop
namespace: ecshop
spec:
type: NodePort
selector:
app: nginx-ecshop
ports:
- name: http
port: 80
targetPort: 80
nodePort: 30080
9.4 Nginx 排错一:403 Forbidden / CrashLoopBackOff
日志中的错误:
directory index of "/usr/share/nginx/html/" is forbidden
HTTP probe failed with statuscode: 403
nginx-ecshop-... CrashLoopBackOff
原因:
- PVC 刚创建时目录为空,没有
index.html或index.php。 - Nginx 默认不开启目录浏览,因此访问
/返回 403。 - HTTP 探针请求
/得到 403,导致 Pod 被判为不健康并反复重启。
解决方法:
- 向共享目录写入测试首页:
POD=$(kubectl get pod -n ecshop -l app=nginx-ecshop -o jsonpath='{.items[0].metadata.name}')
kubectl exec -it -n ecshop $POD -- sh -c 'echo "<h1>test page</h1>" > /usr/share/nginx/html/index.html'
- 更稳妥地,把探针从 HTTP GET 改为 TCP Socket,只要 80 端口能建立连接就算健康:
readinessProbe:
tcpSocket:
port: 80
livenessProbe:
tcpSocket:
port: 80
9.5 Nginx 排错二:wget 不存在
日志中曾把探针写成:
livenessProbe:
exec:
command: ["wget", "-q", "--spider", "http://127.0.0.1"]
结果:
OCI runtime exec failed: exec: "wget": executable file not found in $PATH
原因:该 Nginx 镜像中没有 wget。所以最终改为 tcpSocket 探针。
9.6 Nginx 排错三:LimitRange 限制
日志中 Deployment 曾把内存请求写成:
requests:
memory: 128Mi
但命名空间 LimitRange 要求最小内存为 256Mi,因此 ReplicaSet 一直无法创建 Pod:
minimum memory usage per Container is 256Mi, but request is 128Mi
解决:
kubectl patch deploy -n ecshop nginx-ecshop --type=json -p='[
{"op":"replace","path":"/spec/template/spec/containers/0/resources/requests/memory","value":"256Mi"},
{"op":"replace","path":"/spec/template/spec/containers/0/resources/limits/memory","value":"512Mi"}
]'
9.7 NodePort 访问
因为 Pod 运行在 worker 节点,从 master 直接 curl 127.0.0.1:30080 会失败:
curl: (7) Failed to connect to 127.0.0.1 port 30080
正确方式是通过 Pod 所在节点的 IP 或任意 worker IP 访问:
curl http://10.1.8.31:30080/
curl http://10.224.214.217/ # Pod IP
日志中最终成功返回:
<h1>test page</h1>
十、ECShop 代码部署
10.1 下载并解压
wget http://192.168.42.200/course-materials/softwares/stage01/ECShop_V4.1.20_UTF8.zip
unzip ECShop_V4.1.20_UTF8.zip
日志中解压后目录为:
ECShop_V4.1.20_UTF8_release20250416/source/ecshop/
10.2 复制代码到 NFS PVC 目录
先在 master 上查看 PVC 对应的 NFS 子目录:
kubectl get pvc -n ecshop
ls /ecshop
NFS provisioner 会为 PVC 创建形如下面的目录:
/ecshop/ecshop-nginx-html-pvc-pvc-4c31d66c-d41b-4a1f-b05f-c4c2107fff18/
复制代码并授权:
cp -a ECShop_V4.1.20_UTF8_release20250416/source/ecshop/* \
/ecshop/ecshop-nginx-html-pvc-pvc-4c31d66c-d41b-4a1f-b05f-c4c2107fff18/
chmod 777 -R \
/ecshop/ecshop-nginx-html-pvc-pvc-4c31d66c-d41b-4a1f-b05f-c4c2107fff18/
10.3 PHP 与 MySQL 连通性测试
在共享目录创建 php-mysql.php:
<?php
$conn = @new mysqli('mysql', 'root', '123');
if ($conn->connect_error) {
die('MySQL : ' . $conn->connect_error);
}
echo 'MySQL ';
?>
访问:
curl http://10.108.43.89/php-mysql.php
输出:
MySQL
说明 PHP-FPM 已经能通过 mysql Service 连接 MySQL。
10.4 phpinfo 验证
PHP Deployment 挂载共享 PVC 后,写入 phpinfo.php:
PHP=$(kubectl get pod -n ecshop -l app=php-ecshop -o jsonpath='{.items[0].metadata.name}')
kubectl exec -it -n ecshop $PHP -- sh -c 'echo "<?php phpinfo(); ?>" > /var/www/html/phpinfo.php'
curl http://10.1.8.31:30080/phpinfo.php
返回的 PHP 信息中可以看到:
PHP Version 7.2.34
SCRIPT_FILENAME = /var/www/html/phpinfo.php
日志中还出现过一次权限错误:
cannot create /var/www/html/phpinfo.php: Permission denied
这是因为 NFS 目录权限不足,需要先在 NFS 服务端对目录执行 chmod 777 -R。
十一、MetalLB
集群中已经安装 MetalLB,实验需要配置地址池和 L2 通告。
11.1 IPAddressPool
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 10.1.8.40-10.1.8.80
11.2 L2Advertisement
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
应用:
kubectl apply -f ippool.yaml
kubectl apply -f L2.yaml
11.3 MetalLB 镜像替换
日志中对官方 YAML 做了镜像源替换:
sed -i 's/quay.io/hub.laoma.cloud/g' \
metallb-0.14.8/config/manifests/metallb-native.yaml
kubectl apply -f metallb-0.14.8/config/manifests/metallb-native.yaml
查看 MetalLB Pod:
kubectl get pods -n metallb-system
结果:
controller-d6499775f-fx82j 1/1 Running
speaker-9qswl 1/1 Running
speaker-k5kq4 1/1 Running
speaker-mmbxr 1/1 Running
十二、ingress-nginx 与 HTTPS
12.1 部署 ingress-nginx
日志中解压 ingress-nginx-controller-v1.11.2.tar.gz,并替换镜像源:
tar -xf ingress-nginx-controller-v1.11.2.tar.gz
sed -i 's/registry.k8s.io/hub.laoma.cloud/g' \
ingress-nginx-controller-v1.11.2/deploy/static/provider/cloud/deploy.yaml
sed -i 's/@sha256.*//' \
ingress-nginx-controller-v1.11.2/deploy/static/provider/cloud/deploy.yaml
kubectl apply -f \
ingress-nginx-controller-v1.11.2/deploy/static/provider/cloud/deploy.yaml
Service 已经由 MetalLB 分配外部地址:
ingress-nginx-controller LoadBalancer 10.96.50.226 10.1.8.40 80:32336/TCP,443:31495/TCP
12.2 创建 TLS Secret
生成自签名证书:
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
-keyout tls.key -out tls.crt \
-subj "/CN=shop.shaka.cloud" \
-addext "subjectAltName=DNS:shop.shaka.cloud"
创建或更新 Secret:
kubectl create secret tls ingress-nginx-tls -n ecshop \
--key=tls.key --cert=tls.crt \
--dry-run=client -o yaml | kubectl apply -f -
检查:
kubectl get secrets ingress-nginx-tls -n ecshop
输出:
NAME TYPE DATA AGE
ingress-nginx-tls kubernetes.io/tls 2 10s
12.3 Ingress
最终 Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-tls-shop
namespace: ecshop
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- shop.shaka.cloud
secretName: ingress-nginx-tls
rules:
- host: shop.shaka.cloud
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-ecshop
port:
number: 80
日志中最早版本有两个错误,后来逐一修正:
- 域名最初是
shop.laoma.cloud,后来改成shop.shaka.cloud。 - 后端最初写
nginx,但实际 Service 名是nginx-ecshop。
12.4 Ingress 验证
kubectl describe ingress ingress-tls-shop -n ecshop
成功后的关键信息:
Address: 10.1.8.40
TLS: ingress-nginx-tls terminates shop.shaka.cloud
Rules:
shop.shaka.cloud / nginx-ecshop:80 (10.224.214.217:80)
十三、TLS 排错:默认假证书与 SNI
13.1 现象
使用下面命令测试时,一直看到默认假证书:
curl -kv -H "Host: shop.shaka.cloud" https://10.1.8.40/
输出:
subject: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate
13.2 原因
curl -H "Host: ..." 只修改 HTTP 请求头,不会修改 TLS SNI。当直接访问 IP 时,curl 发送的 SNI 是 10.1.8.40,Ingress 控制器没有匹配到 shop.shaka.cloud 的证书,因此返回默认假证书。
13.3 解决
使用 --resolve 让 curl 同时把 SNI 和 Host 都设置为 shop.shaka.cloud:
curl -kv --resolve shop.shaka.cloud:443:10.1.8.40 \
https://shop.shaka.cloud/ 2>&1 | grep -E "subject:|strict-transport|HTTP/"
此时返回:
subject: CN=shop.shaka.cloud
HTTP/2 302
strict-transport-security: max-age=31536000; includeSubDomains
13.4 HSTS 处理
即使给 Ingress 添加 nginx.ingress.kubernetes.io/hsts: "false",响应中仍出现 HSTS 头。日志中的原因是 HSTS 由 ingress-nginx 全局配置控制。最终通过修改 ConfigMap 关闭:
kubectl patch cm -n ingress-nginx ingress-nginx-controller \
--type=merge -p '{"data":{"hsts":"false"}}'
kubectl rollout restart deploy -n ingress-nginx ingress-nginx-controller
kubectl rollout status deploy -n ingress-nginx ingress-nginx-controller --timeout=120s
关闭后访问静态文件:
curl -ki --resolve shop.shaka.cloud:443:10.1.8.40 \
https://shop.shaka.cloud/animated_favicon.gif | head -15
结果:
HTTP/2 200
content-type: image/gif
content-length: 207
此时响应头中已经不再包含 strict-transport-security。
十四、完整部署后的服务状态
日志中 kubectl get svc -n ecshop 最终结果:
| Service | 类型 | ClusterIP | 端口 | 用途 |
|---|---|---|---|---|
| mysql | ClusterIP | 10.97.240.139 | 3306/TCP | PHP 访问 MySQL |
| nginx-ecshop | ClusterIP / NodePort | 10.108.43.89 | 80/TCP,NodePort 30080 | 前端入口 |
| php-ecshop | ClusterIP | 10.96.170.134 | 9000/TCP | Nginx 转发 PHP |
Pod 状态:
| Pod | 状态 | 说明 |
|---|---|---|
| mysql-0 | 1/1 Running | StatefulSet 数据库 |
| nfs-subdir-external-provisioner | 1/1 Running | 动态存储控制器 |
| nginx-ecshop | 1/1 Running | Nginx |
| php-ecshop | 1/1 Running | PHP-FPM |
Ingress 访问 ECShop:
curl -k --resolve shop.shaka.cloud:443:10.1.8.40 https://shop.shaka.cloud/
日志中返回:
HTTP/2 302
location: ./install/index.php
x-powered-by: PHP/7.2.34
说明 HTTPS 入口已经打通,并由 PHP 应用给出重定向,可进入 ECShop 安装页面。
十五、本实验常见问题清单
-
NFS 挂载失败 / access denied
- Pod 中的 NFS
path必须与/etc/exports导出的目录一致。 - 修改
/etc/exports后要执行exportfs -rav或重启nfs-server。
- Pod 中的 NFS
-
PVC 一直 Pending
- 检查 StorageClass 的
provisioner是否与 provisioner Deployment 中的PROVISIONER_NAME一致。 - 检查 ServiceAccount 的 RBAC 权限,尤其需要
persistentvolumes、persistentvolumeclaims、events、endpoints。 - 检查 provisioner 的 Pod 日志:
kubectl logs -n ecshop deploy/nfs-subdir-external-provisioner。
- 检查 StorageClass 的
-
ImagePullBackOff / ErrImagePull
- 镜像名要写完整仓库前缀,例如
hub.laoma.cloud/...。 - 网络环境内网无法直接拉取 Docker Hub 时,需要替换为内网镜像仓库。
- 镜像名要写完整仓库前缀,例如
-
Nginx 403 / CrashLoopBackOff
- 站点根目录为空且没有
index文件时,Nginx 返回 403。 - 可以先写入
index.html或index.php。 - 探针建议使用
tcpSocket: 80,避免对内容页面的依赖。
- 站点根目录为空且没有
-
exec 探针提示
wget: not found- Nginx 镜像不一定带
wget,不要用它作为探针命令。
- Nginx 镜像不一定带
-
Pod 因 LimitRange 无法创建
requests.memory不能低于命名空间 LimitRange 的min.memory。
-
PHP 返回 File not found
- Nginx 和 PHP-FPM 必须挂载同一份代码 PVC。
SCRIPT_FILENAME必须与 PHP 容器中实际代码路径一致。
-
HTTPS 返回默认假证书
- 使用
--resolve domain:443:IP而不是只加Host头,确保 SNI 正确。 - 确认 Ingress 的
secretName和hosts正确。
- 使用
更多推荐



所有评论(0)