年轻人折腾的第一台独服 – PVE 7.4 + NAT 内网 + DHCP + K8S 多节点集群 + Istio + CertManager(letsencrypt)

By | 31 7 月, 2023

在狗云家抢了一台重庆联通的独服,每个月 200 块钱,10C 64G 800G,带宽入 50Mbps 出 30Mbps。性价比算是很不错了。有需要的可以去看看,不过不知道现在还有没有刚上线的时候那么大优惠了:https://www.dogyun.com/?ref=doraemon (有 aff,介意可自行去除后缀)。

机器下来之后,这么宽敞的“别墅”肯定不能像之前买 VPS 1C2G 那样扣扣索索的装 LNMP 之类的去跑网站。PVE + K8S 让我们搞起来,K8S 当然是要搞个多节点集群来玩完了。好了,下面开始正经的教程系列。

准备工作

操作前提是使用 PVE 7 系统(狗云默认提供),并通过下列方式先更新到最新版本:

设置 debian 中科大源:

cat > /etc/apt/sources.list <<EOF
deb http://mirrors.ustc.edu.cn/debian/ bullseye main non-free contrib
deb http://mirrors.ustc.edu.cn/debian/ bullseye-updates main non-free contrib
deb http://mirrors.ustc.edu.cn/debian/ bullseye-backports main non-free contrib
deb-src http://mirrors.ustc.edu.cn/debian/ bullseye main non-free contrib
deb-src http://mirrors.ustc.edu.cn/debian/ bullseye-updates main non-free contrib
deb-src http://mirrors.ustc.edu.cn/debian/ bullseye-backports main non-free contrib
deb http://mirrors.ustc.edu.cn/debian-security/ bullseye-security main non-free contrib
deb-src http://mirrors.ustc.edu.cn/debian-security/ bullseye-security main non-free contrib
EOF

删除企业源:

rm -rf /etc/apt/sources.list.d/pve-install-repo.list
echo "#deb https://enterprise.proxmox.com/debian/pve Bullseye pve-enterprise" > /etc/apt/sources.list.d/pve-enterprise.list

下载秘钥:

wget http://mirrors.ustc.edu.cn/proxmox/debian/proxmox-release-bullseye.gpg -O /etc/apt/trusted.gpg.d/proxmox-release-bullseye.gpg

添加国内源:

echo "deb http://mirrors.ustc.edu.cn/proxmox/debian/pve bullseye pve-no-subscription" >/etc/apt/sources.list.d/pve-install-repo.list

修改自带的CEPH源:

echo "deb https://mirrors.ustc.edu.cn/proxmox/debian/ceph-pacific bullseye main" > /etc/apt/sources.list.d/ceph.list

更新:

apt update -y && apt dist-upgrade -y

安装新内核:

apt install -y pve-kernel-6.2

调整启动顺序:

grep menuentry /boot/grub/grub.cfg | grep 6.2 | grep -v recovery
# 从上面找出来的记录里面,复制对应信息到 /etc/default/grub 中
emacs /etc/default/grub
# 把默认的 GRUB_DEFAULT="0" 改为类似下面的内容,具体是什么以你实际环境为准
GRUB_DEFAULT="Advanced options for Proxmox VE GNU/Linux>Proxmox VE GNU/Linux, with Linux 6.2.16-4-bpo11-pve"

更新引导并重启:

update-grub
reboot

重启后删除不必要的包:

apt --purge autoremove

NAT 网络配置

登录 Proxmox VE 后台,选择系统 -> 网络,然后点击创建 -> Linux Bridge。

填写 IP 和子网掩码,IP 地址填写个局域网的网段地址就行。其他项目不用填也不用改,保持默认。

IP 地址填写一个局域网地址:192.168.0.1/24 (没那么多,250+ 网络地址够用了)

上面的配置创建了一个新的 vmbr1 网桥分配了一个子网 192.168.0.1/24,宿主机(网关)在子网的 IP 是 192.168.0.1。

然后点击上面的“应用配置”:

创建完成之后,编辑 /etc/network/interfaces,然后在对应的 vmbr1 后面修改成相应的配置(注意不要直接替换为下面的文件,这个文件上面还有一堆 eth0 / vmbr0 的配置,不要删除):

auto vmbr1
iface vmbr1 inet static
    address 192.168.0.1/24
    gateway x.x.x.254  # 独立服务器 IP 的前面三段 + 最后一段是 254
    bridge-ports none
    bridge-stp off
    bridge-fd 0
    post-up echo 1 > /proc/sys/net/ipv4/ip_forward
    post-up iptables -t nat -A POSTROUTING -s '192.168.0.0/24' -o vmbr0 -j MASQUERADE
    post-down iptables -t nat -D POSTROUTING -s '192.168.0.0/24' -o vmbr0 -j MASQUERADE

启用网桥:

ifup vmbr1

显示信息:

ip address show dev vmbr1

可以看到类似于下面的输出:

查看 iptables 配置是否生效:

iptables -L -t nat

可以看到类似于下面的输出:

重启网络:

systemctl restart networking

确认没有问题之后重启系统:

reboot

DHCP 配置

上面配置好的 NAT 网络是没有 DHCP 的,需要手工设定 IP,不是很方便,这里自己部署一个本地的 DHCP Server 来解决这个问题。

apt install isc-dhcp-server -y

安装过程会直接启动失败,没有关系。现在来修改 /etc/default/isc-dhcp-server 这个文件内容,将 INTERFACESv4 调整为 vmbr1 也就是我们要通过 DHCP Server 发 IP 的那个 Linux Bridge,如下:

接下来继续编辑 /etc/dhcp/dhcpd.conf 文件,调整成下面的这个样子:

option domain-name "doraemonext.com";
option domain-name-servers 223.5.5.5, 223.6.6.6;
default-lease-time 600;
max-lease-time 7200;
ddns-update-style none;
authoritative;
log-facility local7;
subnet 192.168.0.0 netmask 255.255.255.0 {
   range 192.168.0.100 192.168.0.149;
   option subnet-mask 255.255.255.0;
   option domain-name-servers 223.5.5.5, 223.6.6.6;
   option domain-name "doraemonext.com";
   option routers 192.168.0.1;
   option netbios-name-servers 192.168.0.1;
   option netbios-node-type 8;
   get-lease-hostnames true;
   use-host-decl-names true;
   default-lease-time 600;
   max-lease-time 7200;
   interface vmbr0;
}

其中需要换成你自己的就是 option domain-name / option domain-name-servers / range 之类的信息。range 表示可以下发的 IP 范围是什么。

保存后,重启 DHCP Server:

systemctl restart isc-dhcp-server

可以看到当前已经没有报错了。

其他准备

此时可以先安装个 Windows 虚拟机(vmbr1),上面已经把 NAT 内网和 DHCP 都搞好了,安装不存在任何问题,然后把科学上网的环境搞一下,然后借用这个 Windows 上的本地 SOCKS 代理,把宿主机上的 v2raya 透明代理搞好,这里不说明了。后续步骤默认没有任何网络访问问题。

关于 3389 端口的映射,可以在 /etc/network/interfaces 中设置对 NAT 网络的转发:

post-up iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 3389 -j DNAT --to 192.168.0.100:3389
post-down iptables -t nat -D PREROUTING -i vmbr0 -p tcp --dport 3389 -j DNAT --to 192.168.0.100:3389

配置 K8S 虚拟机模板

在 PVE 中创建虚拟机,如下:

在磁盘页面把默认的磁盘删掉,因为后面会添加新的:

注意网络选择 vmbr1,关闭防火墙:

创建完毕后,不要启动。然后增加一个 cloud-init 设备:

然后通过 PVE 的镜像下载功能,下载 http://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img 镜像。最终下载到的目录应该是:/var/lib/vz/template/iso/ubuntu-22.04-server-cloudimg-amd64.img

下载好后,进入物理机 Shell,将镜像导入硬盘:

qm importdisk 150 /var/lib/vz/template/iso/ubuntu-22.04-server-cloudimg-amd64.img local-lvm --format=qcow2

上面的 150 是刚才新建的虚拟机的 ID。接下来返回 PVE 页面的虚拟机管理界面,双击刚才导入的“未使用的磁盘0”,然后选择 SCSI 之后添加:

接下来在选项中设置系统启动顺序,将刚才添加的 SCSI:1 调整到第一位:

进入 cloud-init 界面配置用户名和登录方式,推荐使用宿主机 ssh-key 的方式。因为前面已经配置好了 DHCP,所以可以直接使用 DHCP。

设置完上面的事情后可以启动虚拟机了,确认可以正常登录后即可关机。然后在硬件里面调整磁盘大小,注意只能新增:

我这里增加了 80GB 存储。保存退出。然后进入选项中,将 QEMU Guest Agent 打开:

之后再次启动虚拟机,执行下列命令:

# 安装 QEMU Guest Agent
sudo apt-get install qemu-guest-agent
sudo systemctl start qemu-guest-agent

# 配置虚拟机
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

# 设置所需的 sysctl 参数,参数在重新启动后保持不变
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

# 应用 sysctl 参数而不重新启动
sudo sysctl --system

# 通过运行以下指令确认 br_netfilter 和 overlay 模块被加载
lsmod | grep br_netfilter
lsmod | grep overlay

# 通过运行以下指令确认 net.bridge.bridge-nf-call-iptables、net.bridge.bridge-nf-call-ip6tables 和 net.ipv4.ip_forward 系统变量在你的 sysctl 配置中被设置为 1
sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward

继续配置容器运行时:

# 安装 containerd
sudo apt-get update && sudo apt-get install -y containerd

# 配置 containerd
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml

# 修改为 SystemdCgroup
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
cat /etc/containerd/config.toml | grep SystemdCgroup

# 配置 containerd 服务
sudo systemctl enable containerd
sudo systemctl restart containerd
sudo systemctl status containerd

安装 kubelet/kubeadm/kubectl:

sudo apt-get update && sudo apt-get install -y apt-transport-https ca-certificates curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-archive-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

做完上述事情后,这个虚拟机就可以关机了,然后开始制作虚拟机模板。

之后用这个模板克隆四个 VM 出来(一个 control-plane 控制平面,三个 worker),如下,注意是完整克隆:

最终效果如下:

然后对每个 VM 设置一下静态 IP 信息(在 cloud-init 中):

启动所有克隆好后的 VM,登录进去之后,将所有机器的 /etc/machine-id 调整为不同的内容并强制保存。

接下来的内容在所有的节点上执行,拉取镜像:

sudo kubeadm config images pull

接下来的内容仅在所有 control-plane 角色的 VM 中执行下列命令:

sudo kubeadm init --control-plane-endpoint=192.168.0.150 --node-name control-plane --pod-network-cidr=10.244.0.0/16

mkdir -p HOME/.kube
sudo cp -i /etc/kubernetes/admin.confHOME/.kube/config
sudo chown (id -u):(id -g) $HOME/.kube/config

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

上面两个地址,一个是 control-plane 自身的 IP,一个是 Pod 的 CIDR 地址,因为后面打算用 flannel 插件,就保持默认的 10.244.0.0/16 就行。

这个时候,如果你在 control-plane 上去看 Pod,可以看到如下的内容:

接下来加入其他的所有 worker 节点,在那些 worker VM 中执行下列的命令来加入集群(该命令已在上述 control-plane 安装的输出结果的最后):

kubeadm join 192.168.0.150:6443 --token xxxxxxx.e3yxxxxxwt8gixx8 --discovery-token-ca-cert-hash sha256:xxxxxx8

如果将来想再看上述的命令,可以执行:

sudo kubeadm token create --print-join-command

至此,一个可用的 K8S 集群部署完毕。

MetalLB 安装

现在 K8S Node 是在 NAT 内网中的,K8S Pod 在另一个 flannel 的 NAT 内网中。现在如果想暴露服务的话,虽然可以用 nodeport,但是不太好用。所以可以使用 MetalLB 来启用 LoadBalancer,这样就可以生成我们设置的 192.168.0.1/24 内网中的 IP 了。

先编辑下 kube-proxy 的转发模式,调整到 strictARP mode:

kubectl edit configmap -n kube-system kube-proxy

将其中的 mode 和 ipvs.strictARP 设置成下面的内容:

apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
  strictARP: true

然后保存退出。之后安装 MetalLB:

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.10/config/manifests/metallb-native.yaml

部署完毕后,可以看到下面的几个 Pod:

接下来开始分配 IP 范围,将下列文件写入到本地的 metallb.yaml 中:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: static-ip-address-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.0.200-192.168.0.230
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: advertisement
  namespace: metallb-system

上面的 addresses 填写你自己准备分配给它的 NAT IP 地址范围。现在,如果有新的 Service 使用 LoadBalancer,那么就会直接分配一个 NAT 内的 192.168.0.xxx 的 IP 了。

安装 Istio

在 ~/.kube/config 存在的情况下,执行下列命令(1.18.1 是当前的最新版本):

curl -L https://istio.io/downloadIstio | sh -
cd istio-1.18.1
export PATH=PWD/bin:PATH
istioctl install --set profile=default -y

然后设置所有 default 下的 Pod 都接受 Istio 的托管:

kubectl label namespace default istio-injection=enabled

当部署完成后,你应该会看到多了这两个 Pod:

同时,通过 kubectl get svc -n istio-system,你也会看到一个生成了 LoadBalancer 的 istio-ingressgateway:

这里生成的 192.168.0.201 (按你自己实际环境中的为准) 就是我们将来所有服务的流量入口。

测试服务部署

写入 hello.yaml 文件:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-app
spec:
  selector:
    matchLabels:
      app: test-app
  template:
    metadata:
      labels:
        app: test-app
    spec:
      containers:
      - name: test-app
        image: nginxdemos/hello
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: testpage
spec:
  selector:
    app: test-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

然后 kubectl apply -f hello.yaml,等待服务拉起。

然后创建一个 Istio Gateway(后面不再赘述执行 kubectl 的命令):

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: hello-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "doraemonext.net"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: hello-virtual-service
  namespace: default
spec:
  hosts:
  - "doraemonext.net"
  gateways:
  - istio-system/hello-gateway
  http:
  - match:
    - uri:
        exact: /
    route:
    - destination:
        host: testpage
        port:
          number: 80

然后从 Ingress 的入口直接去 curl,可以看到已经返回 Hello 200 了(这个 192.168.0.201 就是前面 kubectl get svc -n istio-system 中看到的 ingress 的 LoadBalancer 地址):

宿主机安装 Nginx

为了能够打通最后一公里,也就是让请求宿主机公网 IP 的请求打到 Istio Ingress 网关侧,需要在宿主机上开个 Nginx 直接针对所有 80/443 的请求直接以 stream 的形式转发到 Istio Ingress LoadBalancer 上(我们把 nginx 当四层转发来用)。

操作流程如下:

apt install nginx -y
emacs /etc/nginx/nginx.conf

将其中的 http 整段删除,然后粘贴下面的 stream 配置:

stream {
    server {
        listen 80;
        proxy_pass 192.168.0.201:80;
    }

    server {
        listen 443;
        proxy_pass 192.168.0.201:443;
    }
}

然后重启 nginx 并设置开机自启动:

systemctl restart nginx
systemctl enable nginx

此时,我们通过浏览器去打开自己绑定的域名(此处使用 doraemonext.net 来示例),已经可以看到 Hello World 了。

CertManager 安装及证书自动申请

刚才配置的都是 80 的,那么现在网站对外提供服务肯定都是 https 的,K8S 上我们可以通过 CertManager 来管理和自动申请证书。此处以 LetEncrypt 为例。

安装 CertManager:

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

等待部署完成后,会发现多了 3 个 Pod:

然后下发集群证书的 ClusterIssuer 和 Certificate:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
  namespace: istio-system
spec:
  acme:
    email: doraemonext@gmail.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt
    solvers:
    - http01:
        ingress:
          class: istio
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: doraemonext-net-cert-prod
  namespace: istio-system
spec:
  secretName: doraemonext-net-cert-prod
  duration: 2160h # 90d
  renewBefore: 360h # 15d
  isCA: false
  privateKey:
    algorithm: RSA
    encoding: PKCS1
    size: 2048
  usages:
    - server auth
    - client auth
  dnsNames:
    - "doraemonext.net"
  issuerRef:
    name: letsencrypt
    kind: ClusterIssuer
    group: cert-manager.io

上面的信息按需改动,注意 issuerRef 引用的是上面的 ClusterIssuer,两个名字要改一起改。

当下发完成后,就可以看下证书申请情况了:

kubectl describe certificate doraemonext-net-cert-prod -n istio-system

如果 Events 里面出现了 Created new CertificateRequest resource “xxxxx”,那么就继续向下追:

kubectl describe certificaterequest xxxxx -n istio-system

这里就会等待证书签发了,稍微等会儿,如果你能看到类似于 Normal CertificateIssued 21s cert-manager-certificaterequests-issuer-acme Certificate fetched from issuer successfully 的信息,就说明已经证书签发成功了。

那么我们接下来就是要让 Istio Ingress Gateway 加载这个证书。

将刚才我们下发的 Istio Gateway 稍作调整:

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: hello-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "doraemonext.net"
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: doraemonext-net-cert-prod
    hosts:
    - "doraemonext.net"

也就是加了 443 这块,注意 credentialName 和刚才的证书名称保持相同。然后重新 kubectl apply -f 它。

这次我们再次用 https 打开浏览器:

会发现证书已经生效了。

接下来我们需要将 80 直接强制跳转到 443,毕竟都有证书了,怎么还能继续用 80。

重新修改一下刚才的 Istio VirtualService 文件:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: hello-virtual-service
  namespace: default
spec:
  hosts:
  - "doraemonext.net"
  gateways:
  - istio-system/hello-gateway
  http:
  - match:
    - port: 80
    redirect:
      authority: "doraemonext.net:443"
      scheme: https
  - match:
    - port: 443
    route:
    - destination:
        host: testpage
        port:
          number: 80

现在,我们就算使用 http://doraemonext.net 去访问,也会被强制跳转到 https://doraemonext.net 了。

Prometheus/Kiali 安装及可视化 Mesh 网络

都跑 Istio 了,不装个 Kiali 可视化就说不过去了:

kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.18/samples/addons/prometheus.yaml
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.18/samples/addons/kiali.yaml

回到刚才我们的 istio-1.18.1 目录下,装一下官方的 demo,体验一下:

kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml
kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml

官方的 Demo 默认给的都是 80 端口的,那我们肯定要和证书绑定一下。这里我提前设置了 bookinfo.doraemonext.net/kiali.doraemonext.net 的 A 记录指向了宿主机 IP。

先申请 bookinfo.doraemonext.net 的证书。通过 kubectl edit cert doraemonext-net-cert-prod -n istio-system 打开刚才我们的证书 YAML,然后将其中的 dnsNames 增加一个 bookinfo.doraemonext.net/kiali.doraemonext.net:

然后再像刚才一样,通过 kubectl describe cert doraemonext-net-cert-prod -n istio-system 来观察 Events,稍等一会儿,看到下面的提示就是成功了:

接下来我们小小的改动一下官方示例中的网关文件: emacs samples/bookinfo/networking/bookinfo-gateway.yaml,主要是把 443 加上去:

  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: doraemonext-net-cert-prod
    hosts:
    - "bookinfo.doraemonext.net"

把上面的文件加到 bookinfo-gateway 下面,80 的后面一段即可,保存后重新 kubectl apply -f 即可生效,然后重新访问 https://bookinfo.doraemonext.net/productpage

接下来给 Kiali 加一下 Gateway 和 VirtualService 让它暴露到 kiali.doraemonext.net 上:

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: kiali-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https-kiali
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: doraemonext-net-cert-prod
    hosts:
    - "kiali.doraemonext.net"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: kiali-vs
  namespace: istio-system
spec:
  hosts:
  - "kiali.doraemonext.net"
  gateways:
  - kiali-gateway
  http:
  - route:
    - destination:
        host: kiali
        port:
          number: 20001
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: kiali
  namespace: istio-system
spec:
  host: kiali
  trafficPolicy:
    tls:
      mode: DISABLE

然后访问下 https://kiali.doraemonext.net ,整个服务的网络拓扑图就出来了:

折腾了这么多,才跑了 48% 的内存:

给狗老板打 call 😋

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注