K8s NFS 动态存储 + ECShop 应用部署学习笔记


一、实验目标与整体架构

本次实验的目标是:

  1. 在 master 节点上搭建 NFS 服务,供 Kubernetes 使用。
  2. 先使用 Pod + NFS 卷 验证静态挂载。
  3. 部署 NFS Client Provisioner,实现 PVC 的动态供给
  4. ecshop 命名空间中完成:
    • ResourceQuota / LimitRange 资源限制;
    • MySQL StatefulSet + Headless Service + Secret;
    • PHP-FPM Deployment + Service;
    • Nginx Deployment + ConfigMap + PVC + NodePort Service;
  5. 部署 MetalLB,为 LoadBalancer 类型的 Ingress 分配外部 IP。
  6. 部署 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.cloudcontrol-plane10.1.8.30Ubuntu 24.04 LTS6.8.0-31-genericcontainerd 1.7.20
worker31.shaka.cloudworker10.1.8.31Ubuntu 24.04 LTS6.8.0-31-genericcontainerd 1.7.20
worker32.shaka.cloudworker10.1.8.32Ubuntu 24.04 LTS6.8.0-31-genericcontainerd 1.7.20

Kubernetes 版本为 v1.30.2,集群中已经预装了 Calico、MetalLB、ingress-nginx 等基础组件。

镜像仓库使用内网代理:

  • hub.laoma.cloud
  • registry.cn-hangzhou.aliyuncs.com
  • 192.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

原因与解决办法:

  1. RBAC 权限不完整:ServiceAccount 缺少 eventsendpoints 等权限,导致 PVC 一直 Pending。
  2. provisioner 名称与镜像版本不匹配:老版本 fuseim.pri/ifs 存在 selfLink 兼容性问题。
  3. 后续统一改为 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.0
  • willdockerhub/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 关键点

  1. StatefulSet 使用 volumeClaimTemplates,会自动为每个副本创建 PVC,名称格式为 <template-name>-<statefulset-name>-<序号>,例如 mysql-data-mysql-0
  2. 日志中最初使用 ReadWriteMany,后来改为 ReadWriteOnce。MySQL 单副本场景通常使用 RWO 即可。
  3. 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.htmlindex.php
  • Nginx 默认不开启目录浏览,因此访问 / 返回 403。
  • HTTP 探针请求 / 得到 403,导致 Pod 被判为不健康并反复重启。

解决方法:

  1. 向共享目录写入测试首页:
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'
  1. 更稳妥地,把探针从 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

日志中最早版本有两个错误,后来逐一修正:

  1. 域名最初是 shop.laoma.cloud,后来改成 shop.shaka.cloud
  2. 后端最初写 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端口用途
mysqlClusterIP10.97.240.1393306/TCPPHP 访问 MySQL
nginx-ecshopClusterIP / NodePort10.108.43.8980/TCP,NodePort 30080前端入口
php-ecshopClusterIP10.96.170.1349000/TCPNginx 转发 PHP

Pod 状态:

Pod状态说明
mysql-01/1 RunningStatefulSet 数据库
nfs-subdir-external-provisioner1/1 Running动态存储控制器
nginx-ecshop1/1 RunningNginx
php-ecshop1/1 RunningPHP-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 安装页面。


十五、本实验常见问题清单

  1. NFS 挂载失败 / access denied

    • Pod 中的 NFS path 必须与 /etc/exports 导出的目录一致。
    • 修改 /etc/exports 后要执行 exportfs -rav 或重启 nfs-server
  2. PVC 一直 Pending

    • 检查 StorageClass 的 provisioner 是否与 provisioner Deployment 中的 PROVISIONER_NAME 一致。
    • 检查 ServiceAccount 的 RBAC 权限,尤其需要 persistentvolumespersistentvolumeclaimseventsendpoints
    • 检查 provisioner 的 Pod 日志:kubectl logs -n ecshop deploy/nfs-subdir-external-provisioner
  3. ImagePullBackOff / ErrImagePull

    • 镜像名要写完整仓库前缀,例如 hub.laoma.cloud/...
    • 网络环境内网无法直接拉取 Docker Hub 时,需要替换为内网镜像仓库。
  4. Nginx 403 / CrashLoopBackOff

    • 站点根目录为空且没有 index 文件时,Nginx 返回 403。
    • 可以先写入 index.htmlindex.php
    • 探针建议使用 tcpSocket: 80,避免对内容页面的依赖。
  5. exec 探针提示 wget: not found

    • Nginx 镜像不一定带 wget,不要用它作为探针命令。
  6. Pod 因 LimitRange 无法创建

    • requests.memory 不能低于命名空间 LimitRange 的 min.memory
  7. PHP 返回 File not found

    • Nginx 和 PHP-FPM 必须挂载同一份代码 PVC。
    • SCRIPT_FILENAME 必须与 PHP 容器中实际代码路径一致。
  8. HTTPS 返回默认假证书

    • 使用 --resolve domain:443:IP 而不是只加 Host 头,确保 SNI 正确。
    • 确认 Ingress 的 secretNamehosts 正确。

更多推荐