家用主机跑云原生全家桶 KuberSphere 3.1
1.1 项目概述
本篇文章记录了我个人使用家用主机,在内网环境下,尽可能接近生产环境的配置,完成k8s平台KubeSphere实验环境的搭建。
1.2 主机配置
实验采用单主机的配置,主机安装Proxmox操作系统作为虚拟化平台,主机配置如下:
硬件 | 配置 | 总容量 |
---|---|---|
处理器(s) | AMD Ryzen 7 3700X | 8核 16线程 |
内存 | 海盗船32GB *4 | 128 GB |
主板 | 华擎B450 钢铁大版 | |
NVME | HP SSD EX920 M2 | 1TB |
SATA SSD | 光威 弈Pro 1T *4 | 4 TB |
SATA SSD(ZFS Cache) | Intel SSD 1T *2 | 2 TB |
网卡 | 板载 | 1G |
其中,光威 弈Pro SSD 组成ZFS Pool,Intel SSD 组成ZFS Pool Cache 。原本机房有淘汰的万兆网卡使用,可惜主板插了显卡,第二个PCI-E插槽无论如何都识别不到,只好放弃。
1.3 虚拟机规划
根据自己的多次实验和经验总结,假如按照生产高可用的要求安装Kubernetes,虚拟机规划有如下几种,根据资源占用情况排序:
- All-in-One套餐:,仅采用一台主机实现,ETCD、Kube-Master节点和Worker节点均只有一个,且位于同一台主机上。
- 乞丐套餐:ETCD、Kube-Master位于单节点上,3个Worker节点位于3台不同的主机上。
- 入门套餐:ETCD、Kube-Master、Worker位于同一个节点上,共3台。
- 生产准备套餐:ETCD、Kube-Master位于同节点上、各3个节点,3个Worker节点位于3台不同的主机上。
- 超级豪华套餐:ETCD组件与Kube-master分离,分别部署于不同主机上。
ETCD对网络吞吐量和磁盘IO要求非常高,在未对ETCD调优的前提下,单机ETCD需部署在固态硬盘上;
同时ETCD组成集群时,不仅要求ETCD部署在固态硬盘上,还要求网络至少是万兆以上。否则容易出现集群脑裂的现象。
根据多次安装KuberSphere全家桶失败实验总结,影响KuberSphere的稳定性原因非常多,但最主要的还是K8s的基础组件ETCD。除了网络原因外(没有使用万兆网卡、与集群共用网络),使用虚拟机完成3台ETCD部署对虚拟化主机磁盘IO要求依然很高,即使将3台虚机放在单个NVME存储上,或者放在多个SSD组成的ZFSPool内,仍然有一定的几率容易集群脑裂,进而导致Kubernetes故障。
所以为了保障集群稳定性,退而求其次,采用乞丐套餐,使用4台虚拟机完成本次容器化平台搭建。
主机类型 | 数量 | CPU | 内存 | 磁盘 | 操作系统 |
---|---|---|---|---|---|
Kube-Master & ETCD | 1 | 2C1T | 8Gb | NVME 64G | Debain 9 |
Kube-Worker | 3 | 2C2T | 20GB | ZFS-SSD 64G | Debain 9 |
1.4 网络规划
家用的环境下,我只有一个网件小路由作为边界路由,但毕竟只是千兆家用路由,实现LB、BGP之类的功能是不可能的,只能依靠软件实现。由于是单台主机,所有的虚拟机都在同一个主机上,使用ipref测试虚拟机之间的网络能够达到10GB/s,万兆通讯可以满足。
网络组件如下:
Kubernetes集群 CNI:Cacilo
内网&Kubernetes集群 DNS:CoreDNS
LB:PorterLB
IP规划:
组件 | 地址 |
---|---|
Kube-Master&ETCD | 10.0.0.172 |
Kube-Worker-1 | 10.0.0.30 |
Kube-Worker-2 | 10.0.0.173 |
Kube-Worker-3 | 10.0.0.60 |
EIP | 10.0.0.150-10.0.0.170 |
由于家用路由不支持BGP协议,PorterLB只能采用Layer2的方式实现LB共享出来的EIP。
且由于在KubeSphere内,每个项目(Namespace)都有自己独立的应用路由(Ingress),建议EIP主要分配给应用路由,实现业务的外网访问功能。
为了最大程度复用集群内的组件,CoreDNS可作为内网的DNS使用
1.5 存储规划
受制于单主机,存储的性能是实现云原生最大的阻碍,在经过多次惨痛的教训和实验后总结如下:
-
不建议采用Ceph:
单台主机以虚拟机的方式实现Ceph性能非常低,除了内存、网络占用高之外,磁盘IO也上不去,即使是采用万兆网络+磁盘直通+SSD+ZFSPool的方式也无法满足ETCD、Redis等组件的要求,毕竟SATA3的限制在600MB/S。而采用NVME 速度虽然最高能满足PCI-E 3的速度3500MB/s,但是家用机基本上只有2个M.2接口,做不了直通,只能采用3台虚拟机实现,容量少、性能大打折扣。
-
不建议采用NFS:
NFS存在单点故障,需要采用DRBD或ZFS的方式实现冗余,在实践的过程中模拟一台k8s虚拟机Down了的工况,虚拟机重启后业务上无法写入,需要重启NFS服务。此外,诸如Prometheus、ElasticSearch等观测性组件官方也不建议部署在NFS上。
-
采用LongHorn方式:
实测采用LongHorn方式性能比Ceph、NFS高很多。
-
采用OpenEBS的local-volume 方式:
最接近本地存储性能的方式,且KubeSphere官方安装工具kubekey默认支持此模式。
2. KubeSphere搭建
KubeSphere的安装步骤官方已经写的非常清晰,网上也有很多类似的教程,这里不过多描述。
搭建步骤建议如下:
- 下载KubeKey
- 各个主机安装必要安装包
- 安装Kubernetes
- 安装第三方分布式存储或CSI插件(如LongHorn、Rook等)
- 最小化安装KubeSphere
- 启用可插拔组件
3. Kubernetes常用配套基础插件
3.1 负载均衡PorterLB
LoadBalance插件是我在安装完Kubernetes和存储插件后第一个需要安装的配套插件。
总的来说MetalLB和PorterLB都支持BGP模式及Layer2模式,两个组件都测试过区别不大,虽然MetalLB发起较早,但考虑平台统一性,最终还是选择了青云配套的PorterLB。
3.1.1 PorterLB安装与配置
PorterLB安装可以仅采用单条命令安装。(推荐)
kubectl apply -f https://raw.githubusercontent.com/kubesphere/porter/master/deploy/porter.yaml
也可以Helm的方式或KubeSphere内置应用方式安装
3.1.2 PorterLB配置(重要)
由于与网络限制,我的PorterLB仅能在Layer2方式下运行,需要对kube-proxy的配置进行更改
kubectl edit configmap kube-proxy -n kube-system
在配置中找到ipvs的选项strictARP,设置为True(重要,否则设置不生效)
ipvs:
strictARP: true
当节点上有多网卡时需要给node打上指定网卡ip的标签
kubectl annotate nodes master1 layer2.porter.kubesphere.io/v1alpha1="网卡IP"
接下来就是根据网络规划配置EIP,这里由于需要指定网卡,虚拟化部署的好处就体现出来了。
apiVersion: network.kubesphere.io/v1alpha2
kind: Eip
metadata:
name: porter-layer2-eip
spec:
address: 10.0.0.150-10.0.0.170
#若单张网卡,不需要选定网卡
#interface: eth0
protocol: layer2
3.1.3 PorterLB使用
若使用PorterLB除了需要将Service的Tpye改成LoadBalancer之外,还需要再annotations内添加两个参数:
kind: Service
apiVersion: v1
metadata:
name: porter-layer2-svc
annotations:
# 必须添加
lb.kubesphere.io/v1alpha1: porter
# 必须添加,若采用BGP方式则填写BGP
protocol.porter.kubesphere.io/v1alpha1: layer2
# 必须添加,名称与EIP配置相同,告知PorterLB使用哪个EIP配置
eip.porter.kubesphere.io/v1alpha2: porter-layer2-eip
spec:
selector:
app: porter-layer2
type: LoadBalancer
ports:
- name: http
port: 80
targetPort: 8080
externalTrafficPolicy: Cluster
3.1.4 在KubeSphere中使用PorterLB和应用路由
由于EIP数量有限,为每个Service分配一个IP不现实,可以给Ingress分配一个IP统一暴露服务,减少EIP的使用。
通过开启KubeSphere内的项目(NameSpace)网关,KubeSphere会在kubesphere-controls-system内生成一个kubesphere-router-dev-namespace的Deployment和Service(实际上是魔改的Nginx-Ingress-Controller),我们可以通过kubectl或ks-console界面的方式设置该Service使用LB暴露出去。设置方法如下:
-
左侧导航栏进入项目设置下的高级设置页面,然后点击设置网关。
-
在弹出的对话框中选择网关的访问方式为LoadBalancer。
-
在设置网关注解中 添加两个注解:
protocol.porter.kubesphere.io/v1alpha1 值 layer2
eip.porter.kubesphere.io/v1alpha2值 porter-layer2-eip
- 点击保存后可以见到外部地址已被分配。
此时若以应用域名的方式访问10.0.0.156这个地址,则会跳转到对应的业务上。
3.2 CoreDNS 改造
每次配置业务Ingress的时候,配置nip域名不够美观,而配置自定义域名又需要改每台主机的host文件,非常的繁琐。在搭建了LB后,我们可以通过LB暴露CoreDNS,统一配置主机使用CoreDNS,省去了改host文件的麻烦。
由于CoreDNS在Kube-system NameSpace内,这部分属于kubeSphere的system-workspace,无法在图形界面上操作,只能使用Kubectl命令进行修改。
3.2.1 CoreDNS Service改造
CoreDNS Service改造非常简单,只需要增加注解以及将Tpye改为LoadBalancer即可
kubectl -n kube-system edit svc coredns
kind: Service
apiVersion: v1
metadata:
name: coredns
namespace: kube-system
labels:
addonmanager.kubernetes.io/mode: Reconcile
eip.porter.kubesphere.io/v1alpha2: eip-sample-pool
k8s-app: kube-dns
kubernetes.io/cluster-service: 'true'
kubernetes.io/name: coredns
annotations:
prometheus.io/port: '9153'
prometheus.io/scrape: 'true'
#增加注解
lb.kubesphere.io/v1alpha1: porter
protocol.porter.kubesphere.io/v1alpha1: layer2
finalizers:
- finalizer.lb.kubesphere.io/v1alpha1
spec:
ports:
- name: dns
protocol: UDP
port: 53
targetPort: 53
nodePort: 32560
- name: dns-tcp
protocol: TCP
port: 53
targetPort: 53
nodePort: 31864
- name: metrics
protocol: TCP
port: 9153
targetPort: 9153
nodePort: 31888
selector:
k8s-app: kube-dns
clusterIP: 10.233.0.3
clusterIPs:
- 10.233.0.3
#类型修改
type: LoadBalancer
sessionAffinity: None
externalTrafficPolicy: Cluster
3.2.2 CoreDNS ConfigMap改造
kubectl -n kube-system edit svc coredns
我们可以通过增加host和rewrite配置,在hosts插件中增加ingress配置的域名,这里以ingress为例,指向ingress.deerjoe.com 10.0.0.156地址,同时配置harbor域名rewrite至ingress。
这里仅当抛砖引玉,更多详细配置请参照CoreDNS官方文档
...
host{
10.0.0.156 ingress.deerjoe.com.
fallthrough
}
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
rewrite name harbor.deerjoe.com. ingress.deerjoe.com.
...
我们也可以通过在coredns的configmap中修改Corefile,配置deerjoe.com的域名解析,并使用file插件引用配置文件。
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
### rewrite相当于CNAME的功能,当访问服务时xxx.deerjoe.com跳转到ingress.deerjoe.com
### 再由K8s的Ingress跳转到对应的服务
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
## 配置deerjoe域名访问,引用deerjoe.db文件
deerjoe.com:53 {
errors
health {
lameduck 5s
}
file /etc/coredns/deerjoe.db deerjoe.com
forward . /etc/resolv.conf {
max_concurrent 1000
}
}
同时在同个configmap中增加deerjoe.db,该文件严格按照RFC 1035-style格式编写
deerjoe.com. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600
deerjoe.com. IN NS b.iana-servers.net.
deerjoe.com. IN NS a.iana-servers.net.
www.deerjoe.com. IN A 10.0.0.191
ingress.deerjoe.com. IN A 10.0.0.156
harbor.deerjoe.com. IN CNAME ingress.deerjoe.com.
最终yaml为
kind: ConfigMap
apiVersion: v1
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |-
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
deerjoe.com:53 {
errors
health {
lameduck 5s
}
file /etc/coredns/deerjoe.db deerjoe.com
forward . /etc/resolv.conf {
max_concurrent 1000
}
}
deerjoe.db: >+
deerjoe.com. IN SOA sns.dns.icann.org.
noc.dns.icann.org. 2015082541 7200 3600 1209600 3600
deerjoe.com. IN NS b.iana-servers.net.
deerjoe.com. IN NS a.iana-servers.net.
www.deerjoe.com. IN A 10.0.0.191
ingress.deerjoe.com. IN A 10.0.0.156
harbor.deerjoe.com. IN CNAME ingress.deerjoe.com.
3.2.3 测试CoreDNS
通过查看coredns 的Service获取EXTERNAL-IP
kubectl -n kube-system get svc coredns
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
coredns LoadBalancer 10.233.0.3 10.0.0.157 53:32560/UDP,53:31864/TCP,9153:31888/TCP 56d
可见,CoreDNS的地址为EXTERNAL-IP地址,配置将PC主机DNS 10.0.0.157。
使用dig或ping测试harbor.deerjoe.com域名,最终指向10.0.0.156 则配置成功。
3.2.4 Pod内访问CoreDNS自定义域名
配置完上述步骤后,个人主机可以通过CoreDNS访问自定义的域名,可这个时候在任意一个pod内访问CoreDNS内自定义的域名却有一定的机率无法解析。在尝试更改CoreDNS和/etc/resolv.conf无果后,发现了Kube-system的namespace下可以看到一个名为nodelocaldns的DaemonSet。百度了一下NodeLocalDNS,
这是因为KubeSphere使用了NodeLocal DNS Cache 的方案提升CoreDNS性能和可靠性。
通过分析其挂在的configmap,有这么一段配置
...
.:53 {
errors
cache 30
reload
loop
bind 169.254.25.10
forward . /etc/resolv.conf #留意该配置
prometheus :9253
}
可见,nodelocaldns除了解析k8s集群内的DNS,剩下的请求都跳转至/etc/resolv.conf。而/etc/resolv.conf默认没有配置DNS指向coredns,自然无法解析coredns自定义的域名。那么是否在/etc/resolv.conf增加由Porter暴露出来的CoreDNS的地址就能够解析
了呢?非也,实践证明coredns 在复用宿主机*/etc/resolv.conf*时,未严格按照nameserver的顺序去解析域名,在Pod内会有一定的机率无法正确解析。
最终解决方案将nodelocaldns的configmap
修改为forward . $CoreDNS_IP 即可。
...
.:53 {
errors
cache 30
reload
loop
bind 169.254.25.10
#forward . /etc/resolv.conf
forward . $CoreDNS_IP #替换$CoreDNS_IP 为真实IP
prometheus :9253
}
$CoreDNS_IP可以是集群内CoreDNS Service的Cluster IP或者是CoreDNS经过Loadblacer暴露出来的EIP
3.3 Cert-Manager 证书管理
由于在内网环境下,没有对外的IP,做不了域名验证,无法使用Let’s Encrypt颁发和自动更新证书,这里参照cert-manager管理内网k8s开发环境证书 一文管理自签证书。
3.3.1 安装Cert-Manager
由于Cert-Manager默认配置的Secret默认放在Cert-Manager pod所在的Namespace内,建议安装时装在对应企业空间的项目(NameSpace)内。
-
进入企业空间>应用管理>应用仓库>添加仓库
-
新建仓库jetstack,地址为https://charts.jetstack.io
-
回到项目部署Cert-Manager,并修改value.yaml配置文件
-
配置文件中需要修改installCRDs: True,然后部署。
-
检查CRD是否存在,否则手动安装
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.crds.yaml
3.3.2 配置Cert-Manager
-
配置自签Issuer
### 基于Namespace apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: selfsigned-issuer namespace: dev-namespace spec: selfSigned: {} --- ### 基于集群 apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigned-cluster-issuer spec: selfSigned: {}
-
创建发行者
apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigned-issuer spec: selfSigned: {}
-
创建CA证书
apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: deerjoe-selfsigned-ca namespace: dev-namespace spec: isCA: true commonName: deerjoe-selfsigned-ca secretName: root-secret #CA保存的secret名 privateKey: algorithm: RSA size: 4096 issuerRef: name: selfsigned-issuer kind: ClusterIssuer group: cert-manager.io
-
创建集群CA发行者
apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: deerjoe-ca-issuer namespace: dev-namespace spec: ca: secretName: root-secret
-
创建CA发行者
apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: deerjoe-ca-issuer namespace: dev-namespace spec: ca: secretName: root-secret
3.3.3 创建证书并验证
方法一:
创建通配符证书*.deerjoe.com,证书文件储存在secret:site-deerjoe-com-tls 中
# site-example-com.certificate.example-com.yaml
# 参考:https://cert-manager.io/docs/usage/certificate/
# api参考:https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1alpha3.Certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: site-deerjoe-com ###
namespace: dev-namespace ### 站点所在命名空间
spec:
# Secret names are always required.
secretName: site-deerjoe-com-tls ### Secret名字
duration: 2160h # 90d
renewBefore: 360h # 15d
privateKey:
algorithm: RSA
size: 4096
subject:
organizations:
- Deerjoe Inc. ###
commonName: "*.deerjoe.com" ###
isCA: false
dnsNames:
- "*.deerjoe.com" ###
- "deerjoe.com"
issuerRef:
name: deerjoe-ca-issuer ### 使用CA Issuer
kind: Issuer ### CA Issuer
group: cert-manager.io
在使用Ingress时直接引用secret即可。
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
name: test
namespace: dev-namespace
annotations:
kubesphere.io/creator: admin
nginx.ingress.kubernetes.io/service-upstream: 'true'
spec:
tls:
- hosts:
- nginx.deerjoe.com
### 直接引用 secret
secretName: site-deerjoe-com-tls
rules:
- host: nginx.deerjoe.com
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
serviceName: nginx-nc8bql
servicePort: 80
方法二:
在Ingress中加入注解,该方法无需创建通配符证书,Cert-manager会根据ingress给出的secretName,自动签发ingress内host的证书,并写入对应的secret中。
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
name: test
namespace: dev-namespace
annotations:
kubesphere.io/creator: admin
nginx.ingress.kubernetes.io/service-upstream: 'true'
cert-manager.io/issuer: deerjoe-ca-issuer # 告知cert-manager使用签发的issuer
#cert-manager.io/cluster-issuer: deerjoe-ca-issuer # cluster-issuer使用这个annotations
spec:
tls:
- hosts:
- nginx.deerjoe.com
secretName: nginx-cert # 此时的secretName为cert-manager创建并存储的secret名称.
rules:
- host: nginx.deerjoe.com
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
serviceName: nginx-nc8bql
servicePort: 80
使用谷歌浏览器访问nginx.deerjoe.com,查看证书。
注:
- 留意查看生成的secret,假如secretName为nginx-cert 而NameSpace 下有一个名称为nginx-cert-xxx的secret,说明证书生成失败。请检查yaml配置的issuer或Clusterissuer是否正确,同时使用kubectl describeCertificate && kubectl describe certificaterequest 查看证书的状态。
- Chrome、Edge访问https无法跳过TLS认证时,请在当前页面输入“thisisunsafe”跳过,火狐不存在此问题。
3.3.4 将证书保存至本地
假设Harbor的域名为harbor.deerjoe.com,我们将dev-namespace的 site-deerjoe-com-tls证书设置为docker ssl证书。
创建文件夹
mkdir -p /etc/docker/certs.d/harbor.deerjoe.com
获取CA
kubectl -n dev-namespace get secret site-deerjoe-com-tls -ojson | jq -r '.data | ."ca.crt"' | base64 -d > /etc/docker/certs.d/harbor.deerjoe.com/ca.crt
获取tls.key
kubectl -n dev-namespace get secret site-deerjoe-com-tls -ojson | jq -r '.data | ."tls.key"' | base64 -d > /etc/docker/certs.d/harbor.deerjoe.com/harbor.deerjoe.com.key
获取tls.crt
kubectl -n dev-namespace get secret site-deerjoe-com-tls -ojson | jq -r '.data | ."tls.crt"' | base64 -d > /etc/docker/certs.d/harbor.deerjoe.com/harbor.deerjoe.com.cert
复制到docker文件夹后重启docker,并使用docker login测试
systemctl restart docker
docker login harbor.deerjoe.com
# 出现以下字样证明配置成功
#Username: admin
#Password:
#WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
#Configure a credential helper to remove this warning. See
#https://docs.docker.com/engine/reference/commandline/login/#credentials-store
#Login Succeeded