漂亮,nginx Ingress 已经通了。下一步我们做 new-api 部署前准备,目标是先把 K8s 的运维基础补齐,再验证它能访问你已有的 PostgreSQL 和 Redis。

接下来分 4 步:

1. 安装 metrics-server
2. 创建 new-api namespace
3. 准备 Secret / ConfigMap
4. 用临时 Pod 测试 PostgreSQL 和 Redis 连通性

1. 安装 metrics-server

作用:

metrics-server 用来采集节点和 Pod 的 CPU、内存指标。
安装后才能用 kubectl top nodes / kubectl top pods。

安装:

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.8.1/components.yaml

家庭自建 kubeadm 集群里,metrics-server 经常因为 kubelet 证书问题启动失败。先检查:

kubectl get pods -n kube-system | grep metrics

如果 Running,直接测试:

kubectl top nodes
kubectl top pods -A

如果不正常,执行这个 patch:

kubectl patch deployment metrics-server -n kube-system --type='json' -p='[
  {
    "op": "add",
    "path": "/spec/template/spec/containers/0/args/-",
    "value": "--kubelet-insecure-tls"
  }
]'

等它重启:

kubectl rollout status deployment metrics-server -n kube-system
kubectl top nodes

下面按 Helm 简单版 来装 PostgreSQL 和 Redis。这个版本适合你当前阶段:先让 new-api 在 K8s 里跑起来,后面再升级数据库 HA、备份和 Operator。

我们会放在同一个 namespace:

new-api

架构:

new-api Pod
  -> postgresql.new-api.svc.cluster.local:5432
  -> redis-master.new-api.svc.cluster.local:6379

0. 先确认 StorageClass

PostgreSQL 和 Redis 都需要 PVC。先看:

kubectl get storageclass

如果有默认 StorageClass,会看到类似:

local-path (default)

或者其他名字。

如果没有默认 StorageClass,先暂停,把输出发我。因为没有 StorageClass,PostgreSQL/Redis 的 PVC 会一直 Pending。


1. 创建 namespace

如果已经创建过,会提示已存在,没关系。

kubectl create namespace new-api

2. 添加 Bitnami Helm 仓库

Bitnami 官方现在推荐 OCI 方式,不一定需要 helm repo add。我们直接用 OCI:

oci://registry-1.docker.io/bitnamicharts/postgresql
oci://registry-1.docker.io/bitnamicharts/redis

Bitnami PostgreSQL 官方安装示例也是:

helm install my-release oci://registry-1.docker.io/bitnamicharts/postgresql

参考:Bitnami PostgreSQL Helm Chart


3. 准备密码

建议你先生成几个密码:

openssl rand -base64 24
openssl rand -base64 24
openssl rand -base64 24

分别用于:

PostgreSQL postgres 管理员密码
PostgreSQL newapi 用户密码
Redis 密码

假设你准备:

POSTGRES_ADMIN_PASSWORD=替换成你的管理员密码
POSTGRES_USER_PASSWORD=替换成你的 newapi 用户密码
REDIS_PASSWORD=替换成你的 Redis 密码

后面命令里手动替换。


4. 安装 PostgreSQL

这里创建:

数据库:newapi
用户:newapi
Service:postgresql.new-api.svc.cluster.local
端口:5432
PVC:20Gi

执行:

helm upgrade --install postgresql \
  oci://registry-1.docker.io/bitnamicharts/postgresql \
  --namespace new-api \
  --set auth.postgresPassword='替换成POSTGRES_ADMIN_PASSWORD' \
  --set auth.username='newapi' \
  --set auth.password='替换成POSTGRES_USER_PASSWORD' \
  --set auth.database='newapi' \
  --set primary.persistence.enabled=true \
  --set primary.persistence.size=20Gi

说明:

auth.username/auth.password:
创建业务用户 newapi。

auth.database:
创建业务数据库 newapi。

primary.persistence.size:
PostgreSQL 数据盘大小。

检查状态:

kubectl get pods -n new-api -o wide
kubectl get pvc -n new-api
kubectl get svc -n new-api

等待 PostgreSQL Running:

kubectl wait --for=condition=ready pod \
  -l app.kubernetes.io/instance=postgresql \
  -n new-api \
  --timeout=300s

测试连接:

kubectl run pg-test \
  -n new-api \
  --rm -it \
  --image=postgres:16-alpine \
  --restart=Never \
  --env PGPASSWORD='替换成POSTGRES_USER_PASSWORD' \
  -- psql -h postgresql.new-api.svc.cluster.local -p 5432 -U newapi -d newapi -c 'select 1;'

成功会看到:

 ?column?
----------
        1

5. 安装 Redis

这里先用单实例 Redis:

架构:standalone
Service:redis-master.new-api.svc.cluster.local
端口:6379
PVC:8Gi

执行:

helm upgrade --install redis \
  oci://registry-1.docker.io/bitnamicharts/redis \
  --namespace new-api \
  --set architecture=standalone \
  --set auth.enabled=true \
  --set auth.password='替换成REDIS_PASSWORD' \
  --set master.persistence.enabled=true \
  --set master.persistence.size=8Gi

说明:

architecture=standalone:
单实例 Redis,学习阶段够用。

auth.password:
Redis 访问密码。

master.persistence.size:
Redis 数据盘大小。

检查:

kubectl get pods -n new-api -o wide
kubectl get pvc -n new-api
kubectl get svc -n new-api

等待 Redis Running:

kubectl wait --for=condition=ready pod \
  -l app.kubernetes.io/instance=redis \
  -n new-api \
  --timeout=300s

测试连接:

kubectl run redis-test \
  -n new-api \
  --rm -it \
  --image=redis:7-alpine \
  --restart=Never \
  -- redis-cli -h redis-master.new-api.svc.cluster.local -p 6379 -a '替换成REDIS_PASSWORD' ping

成功会返回:

PONG

6. 创建 new-api 要用的 Secret

先生成两个固定密钥:

openssl rand -hex 32
openssl rand -hex 32

分别作为:

SESSION_SECRET
CRYPTO_SECRET

创建 Secret:

kubectl create secret generic new-api-secret \
  -n new-api \
  --from-literal=SESSION_SECRET='TV6KasGSCn8lS4h48Jl86rEoQMolzNe5' \
  --from-literal=CRYPTO_SECRET='TV6KasGSCn8lS4h48Jl86rEoQMolzNe5' \
  --from-literal=SQL_DSN='postgresql://newapi:TV6KasGSCn8lS4h48Jl86rEoQMolzNe5@postgresql.new-api.svc.cluster.local:5432/newapi' \
  --from-literal=REDIS_CONN_STRING='redis://:TV6KasGSCn8lS4h48Jl86rEoQMolzNe5@redis-master.new-api.svc.cluster.local:6379/0' \
  --dry-run=client -o yaml | kubectl apply -f -

注意:SQL_DSN 里的密码要和你安装 PostgreSQL 时的:

auth.password

一致。


7. 创建 new-api 普通配置 ConfigMap

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: new-api-config
  namespace: new-api
data:
  TZ: "Asia/Shanghai"
  STREAMING_TIMEOUT: "300"
  MAX_REQUEST_BODY_MB: "32"
  ERROR_LOG_ENABLED: "true"
EOF

检查:

kubectl get cm,secret -n new-api

8. 当前阶段完成标准

执行:

kubectl get pods -n new-api -o wide
kubectl get pvc -n new-api
kubectl get svc -n new-api

你应该看到:

postgresql-0 Running
redis-master-0 Running
PVC Bound
Service postgresql
Service redis-master

连接测试:

select 1 成功
Redis PONG 成功

到这里,new-api 的数据库和缓存就准备好了。


9. 常用查看命令

PostgreSQL 日志:

kubectl logs -n new-api statefulset/postgresql

Redis 日志:

kubectl logs -n new-api statefulset/redis-master

查看 Helm release:

helm list -n new-api

查看 PostgreSQL values:

helm get values postgresql -n new-api

查看 Redis values:

helm get values redis -n new-api

下一步我们就可以写:

new-api Deployment
new-api Service
new-api Ingress

先 1 副本跑通,然后再改 2-3 副本验证多副本。

我看了你的博客,整理得挺清楚。现在下一步就是:部署 new-api 本体,也就是创建:

Deployment
Service
Ingress

不过先改一个小点:new-api 官方文档里 PostgreSQL 的 SQL_DSN 推荐格式是:

postgresql://user:password@host:5432/database

你博客里现在写的是:

host=postgresql.new-api.svc.cluster.local port=5432 ...

这个格式 PostgreSQL 驱动有时也能识别,但为了贴近 new-api 官方示例,建议改成 URL 格式。官方文档也写了 SQL_DSNREDIS_CONN_STRINGSESSION_SECRETCRYPTO_SECRET 这些变量;镜像用的是 calciumion/new-api:latest。参考:New API 环境变量文档new-api GitHub README

假设你的密码变量还在当前 shell 里:

echo $POSTGRES_USER_PASSWORD
echo $REDIS_PASSWORD

如果没有,就重新 export 一次。

一、更新 Secret

kubectl create secret generic new-api-secret \
  -n new-api \
  --from-literal=SESSION_SECRET='替换成你的SESSION_SECRET' \
  --from-literal=CRYPTO_SECRET='替换成你的CRYPTO_SECRET' \
  --from-literal=SQL_DSN='postgresql://newapi:替换成POSTGRES_USER_PASSWORD@postgresql.new-api.svc.cluster.local:5432/newapi' \
  --from-literal=REDIS_CONN_STRING='redis://:替换成REDIS_PASSWORD@redis-master.new-api.svc.cluster.local:6379/0' \
  --dry-run=client -o yaml | kubectl apply -f -

注意:如果密码里有 @:/#?& 这类字符,URL 里可能需要转义。学习阶段建议密码先用字母数字下划线,少踩坑。

二、创建 new-api 的 PVC

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: new-api-data
  namespace: new-api
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 5Gi
EOF

三、部署 new-api

cat <<'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: new-api
  namespace: new-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: new-api
  template:
    metadata:
      labels:
        app: new-api
    spec:
      containers:
        - name: new-api
          image: calciumion/new-api:latest
          imagePullPolicy: IfNotPresent
          args:
            - "--log-dir"
            - "/app/logs"
          ports:
            - containerPort: 3000
          envFrom:
            - configMapRef:
                name: new-api-config
            - secretRef:
                name: new-api-secret
          env:
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          volumeMounts:
            - name: data
              mountPath: /data
            - name: logs
              mountPath: /app/logs
          readinessProbe:
            httpGet:
              path: /api/status
              port: 3000
            initialDelaySeconds: 20
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 6
          livenessProbe:
            httpGet:
              path: /api/status
              port: 3000
            initialDelaySeconds: 60
            periodSeconds: 30
            timeoutSeconds: 5
            failureThreshold: 3
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: new-api-data
        - name: logs
          emptyDir: {}
EOF

四、创建 Service

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: new-api
  namespace: new-api
spec:
  type: ClusterIP
  selector:
    app: new-api
  ports:
    - name: http
      port: 3000
      targetPort: 3000
EOF

五、创建 Ingress

cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: new-api
  namespace: new-api
spec:
  ingressClassName: nginx
  rules:
    - host: k8s-ai.jihw.top
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: new-api
                port:
                  number: 3000
EOF

六、检查状态

kubectl get pods -n new-api -o wide
kubectl get svc -n new-api
kubectl get ingress -n new-api
kubectl logs -n new-api deploy/new-api --tail=100

集群内测试:

如果你的 MetalLB 外部访问还是有问题,就先用 NodePort 访问 ingress-nginx:

curl -I -H "Host: k8s-ai.jihw.top" http://192.168.3.214:32658

浏览器访问的话,Win11 hosts 暂时写:

192.168.3.214 k8s-ai.jihw.top

然后打开:

http://k8s-ai.jihw.top:32658

等单副本访问成功、初始化后台成功之后,再考虑扩成 2 副本。现在先别急着多副本,先把 new-api 跑起来。

是的,要优化成:域名绑定一个会漂移的业务 VIP,而不是绑定某一台节点 IP

推荐架构:

new-api.k8s.local
192.168.3.231  业务 VIP,由 keepalived 漂移
HAProxy
ingress-nginx NodePort
192.168.3.214:32658
192.168.3.215:32658
192.168.3.216:32658

以后 Win11 hosts 只写:

192.168.3.231 new-api.k8s.local

访问:

http://new-api.k8s.local

即使 192.168.3.214 关闭,VIP 漂移到其他节点,也还能访问。

一、先确认 NodePort

在任意 master 执行:

kubectl get svc -n ingress-nginx ingress-nginx-controller

确认类似:

80:32658/TCP
443:31847/TCP

下面我按你现在的端口 3265831847 写。

二、三个节点都配置 HAProxy

192.168.3.214192.168.3.215192.168.3.216 都执行:

sudo sysctl -w net.ipv4.ip_nonlocal_bind=1

echo 'net.ipv4.ip_nonlocal_bind=1' | sudo tee /etc/sysctl.d/99-haproxy-nonlocal-bind.conf

sudo sysctl --system

映射端口

在文件末尾追加:

sudo tee -a /etc/haproxy/haproxy.cfg >/dev/null <<'EOF'
frontend ingress_http_vip
    bind 192.168.3.231:80
    mode tcp
    option tcplog
    default_backend ingress_http_nodeport

backend ingress_http_nodeport
    mode tcp
    balance roundrobin
    option tcp-check
    server k8s-master1 192.168.3.214:32467 check
    server k8s-master2 192.168.3.215:32467 check
    server k8s-master3 192.168.3.216:32467 check

frontend ingress_https_vip
    bind 192.168.3.231:443
    mode tcp
    option tcplog
    default_backend ingress_https_nodeport

backend ingress_https_nodeport
    mode tcp
    balance roundrobin
    option tcp-check
    server k8s-master1 192.168.3.214:32419 check
    server k8s-master2 192.168.3.215:32419 check
    server k8s-master3 192.168.3.216:32419 check
EOF

检查并重载:

sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy

三、三个节点都配置 keepalived 业务 VIP

先查看网卡名:

ip route get 192.168.3.1

你会看到类似:

dev ens33

记住这个网卡名,下面假设是 ens33,如果你的不是,就替换。

编辑:

不要删原来的 API VIP 配置,在末尾追加一个新的 vrrp_instance

k8s-master1 / 192.168.3.214

sudo tee -a /etc/keepalived/keepalived.conf >/dev/null <<'EOF'
vrrp_instance VI_INGRESS {
    state MASTER
    interface ens33
    virtual_router_id 52
    priority 120
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass ingressvip
    }

    virtual_ipaddress {
        192.168.3.231
    }
}
EOF

k8s-master2 / 192.168.3.215

sudo tee -a /etc/keepalived/keepalived.conf >/dev/null <<'EOF'
vrrp_instance VI_INGRESS {
    state BACKUP
    interface ens33
    virtual_router_id 52
    priority 110
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass ingressvip
    }

    virtual_ipaddress {
        192.168.3.231
    }
}
EOF

k8s-master3 / 192.168.3.216

sudo tee -a /etc/keepalived/keepalived.conf >/dev/null <<'EOF'
vrrp_instance VI_INGRESS {
    state BACKUP
    interface ens33
    virtual_router_id 52
    priority 100
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass ingressvip
    }

    virtual_ipaddress {
        192.168.3.231
    }
}
EOF

然后三个节点都执行:

sudo systemctl restart keepalived
sudo systemctl status keepalived --no-pager

四、测试 VIP

在任意节点执行:

ip a | grep 192.168.3.231

正常只会有一台机器持有这个 VIP。

在 Win11 hosts 里改成:

192.168.3.231 k8s-ai.jihw.top
192.168.3.231 longhorn.k8s.local

然后访问:

http://k8s-ai.jihw.top/setup

测试端口:

Test-NetConnection 192.168.3.231 -Port 80

高可用测试:

# 关掉当前持有 192.168.3.231 的那台节点
sudo systemctl stop keepalived

然后在另一台节点看:

ip a | grep 192.168.3.231

如果 VIP 漂移成功,Win11 继续访问:

http://new-api.k8s.local

这套方案比你现在的 192.168.3.214:32658 好很多:域名绑定的是业务 VIP,后端任意一台节点挂掉,HAProxy 会自动跳过;当前持有 VIP 的节点挂掉,keepalived 会把入口漂移到另一台。

自定义域名证书

可以,下一步分两段做:先把 HTTP 域名访问跑通,再做 Let’s Encrypt HTTPS 证书

先确认你现在的入口链路是:

k8s-ai.jihw.top -> 192.168.3.231 -> HAProxy -> ingress-nginx NodePort -> new-api

一、先改 new-api Ingress 域名

在 master 节点执行:

cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: new-api
  namespace: new-api
spec:
  ingressClassName: nginx
  rules:
    - host: k8s-ai.jihw.top
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: new-api
                port:
                  number: 3000
EOF

检查:

kubectl get ingress -n new-api

然后在 Win11 测试:

Test-NetConnection k8s-ai.jihw.top -Port 80

浏览器访问:

http://k8s-ai.jihw.top/setup

如果能打开,说明域名入口已经正常。

二、安装 cert-manager

如果你还没装 cert-manager,执行:

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.20.2/cert-manager.yaml

等待启动:

kubectl get pods -n cert-manager

三个 Pod 都 Running 后再继续。

三、安装阿里云 DNS webhook

helm repo add cert-manager-alidns-webhook https://devmachine-fr.github.io/cert-manager-alidns-webhook
helm repo update

helm upgrade --install alidns-webhook \
  cert-manager-alidns-webhook/alidns-webhook \
  -n cert-manager \
  --set groupName=jihw.top

检查:

kubectl get pods -n cert-manager

四、创建阿里云 DNS AccessKey Secret

建议在阿里云 RAM 创建专用 AccessKey,先给 AliyunDNSFullAccess,后面熟悉后再收窄权限。

kubectl create secret generic alidns-secret \
  -n cert-manager \
  --from-literal=access-key-id='你的AccessKeyId' \
  --from-literal=access-key-secret='你的AccessKeySecret'

注意这里放在 cert-manager 命名空间。

五、创建 Let’s Encrypt Prod ClusterIssuer

这里直接使用正式证书。注意两点:

1. email 必须换成真实邮箱,不能写“你的邮箱”这种中文占位。
2. 这里安装的是 devmachine-fr/cert-manager-alidns-webhook 0.3.1,
   webhook 配置字段要用 accessTokenSecretRef / secretKeySecretRef。

如果字段写成 accessKeyIdRef / accessKeySecretRef,Challenge 会报类似错误:

failed to load secret "cert-manager/": resource name may not be empty

创建正式证书 Issuer:

cat <<'EOF' | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-alidns-prod
spec:
  acme:
    email: 2455191080@qq.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-alidns-prod-account-key
    solvers:
      - dns01:
          webhook:
            groupName: jihw.top
            solverName: alidns-solver
            config:
              regionId: cn-hangzhou
              accessTokenSecretRef:
                name: alidns-secret
                key: access-key-id
              secretKeySecretRef:
                name: alidns-secret
                key: access-key-secret
EOF

检查 Issuer:

kubectl get clusterissuer letsencrypt-alidns-prod

应该看到:

READY   True

如果不是 True,先看原因:

kubectl describe clusterissuer letsencrypt-alidns-prod
kubectl -n cert-manager logs deploy/cert-manager --tail=100

六、给 Ingress 加 TLS 并申请证书

下面这个 Ingress 会自动创建名为 k8s-ai-jihw-top-tls 的 Certificate,并把证书保存到同名 Secret。

cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: new-api
  namespace: new-api
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-alidns-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - k8s-ai.jihw.top
      secretName: k8s-ai-jihw-top-tls
  rules:
    - host: k8s-ai.jihw.top
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: new-api
                port:
                  number: 3000
EOF

查看申请状态:

kubectl get certificate -n new-api
kubectl get certificaterequest,order,challenge -n new-api
kubectl describe certificate k8s-ai-jihw-top-tls -n new-api

正常流程大概是:

CertificateRequest  Approved=True
Order               pending -> valid
Challenge           pending -> valid
Certificate         Ready=True

成功后会看到 TLS Secret:

kubectl get secret k8s-ai-jihw-top-tls -n new-api

也可以看证书有效期:

kubectl describe certificate k8s-ai-jihw-top-tls -n new-api | grep -E "Not Before|Not After|Renewal Time|Ready"

七、测试 HTTPS

Test-NetConnection k8s-ai.jihw.top -Port 443

浏览器访问:

https://k8s-ai.jihw.top

再看状态:

kubectl get certificate -n new-api
kubectl describe certificate k8s-ai-jihw-top-tls -n new-api

八、以后申请其他域名证书

以后只要 cert-manager、alidns-webhook、alidns-secretletsencrypt-alidns-prod 都已经存在,就不需要重复安装。只需要给新的服务写 Ingress。

假设要给 api.example.com 申请证书,命名空间是 demo,服务名是 demo-api,端口是 3000

cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-api
  namespace: demo
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-alidns-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: api-example-com-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: demo-api
                port:
                  number: 3000
EOF

然后查对应命名空间:

kubectl get certificate -n demo
kubectl get certificaterequest,order,challenge -n demo
kubectl describe certificate api-example-com-tls -n demo

九、常见问题排查

如果 Certificate 一直是 False,先看这几个:

kubectl get clusterissuer letsencrypt-alidns-prod
kubectl describe clusterissuer letsencrypt-alidns-prod

kubectl get certificate -A
kubectl get certificaterequest,order,challenge -A
kubectl describe challenge -n new-api

kubectl -n cert-manager logs deploy/cert-manager --tail=200
kubectl -n cert-manager logs deploy/alidns-webhook --tail=200

如果看到:

Referenced issuer does not have a Ready status condition

说明 ClusterIssuer 还没 Ready,常见原因是邮箱写错,尤其不能写中文占位。

如果看到:

failed to load secret "cert-manager/": resource name may not be empty

说明 alidns webhook 字段名写错了。当前这个 webhook 要写:

accessTokenSecretRef:
  name: alidns-secret
  key: access-key-id
secretKeySecretRef:
  name: alidns-secret
  key: access-key-secret

如果改过 ClusterIssuer 后,旧的 Challenge 一直卡在删除中,可以清理旧资源再让 cert-manager 重建:

kubectl -n new-api delete challenge --all --wait=false
kubectl -n new-api delete order --all --wait=false
kubectl -n new-api delete certificaterequest --all --wait=false

如果某个旧 Challenge 有 finalizer 卡住,而且确认它没有成功写入 DNS 记录,可以移除 finalizer:

kubectl -n new-api patch challenge <challenge-name> \
  --type=json \
  -p='[{"op":"remove","path":"/metadata/finalizers"}]'

DNS-01 成功写入后,可以用下面命令看 TXT 记录传播:

dig +short TXT _acme-challenge.k8s-ai.jihw.top @223.5.5.5
dig +short TXT _acme-challenge.k8s-ai.jihw.top @8.8.8.8

后续申请其他域名时,优先复用 letsencrypt-alidns-prod,只需要新增或修改对应服务的 Ingress。