Kubeon简介及高可用的实现

kubeon是一个用来一键安装kubernetes高可用集群的命令行工具,特性如下

  • 可以在一台中控机上管理多个集群
  • 使用堆叠etcd,提供外部lb、local-haproxyapiserver-updater三种高可用模式
  • 可以在国内网络环境下正常使用
  • 只需要手动下载这个命令行工具,不需要手动下载其它安装包,所以中控机需要能联网
  • 支持密码和密钥两种模式登录所有节点
  • 有一个直观的完善的cluster-info命令
  • 进行过中等规模集群验证(300+),足够的简单并易用,证书100年有效期缓解焦虑
  • 对新版本支持够快,并且免费(通常从1.x.1开始支持,不包括1.x.0)

常用操作

  • create:创建一个新集群
  • destroy:销毁一个存在的集群
  • add:添加一个节点
  • del:删除一个节点
  • upgrade:升级集群版本
  • use:选择一个存在的集群作为默认
  • exec:在集群上选择部分或全部节点执行命令
  • cp:在集群上选择部分或全部节点复制文件进去

更多常用操作和命令行使用请查看readme

新建流程概要

  1. kubeon会将除了命令行之外的其它资源都上传到doker hub和阿里云的hub
  2. 新建集群时,首先校验节点信息,主要是host是否可达,有无重复之类的
  3. 如果本地资源包不存在,直接从线上下载;如果存在,对比线上版本,不一致的才下载
  4. 所有资源包下载完成,按需copy到需要安装的节点上
  5. 进行节点的预处理,主要是设置内核参数、设置limit、通过原生系统的包管理安装一些包、删除一些包
  6. kubeon生成配置文件,并展开资源包,安装docker/containerd和kubelet
  7. 接下来是对kubeadm的包装:选定第一个master进行init,完成后join其它master,最后join所有的worker

高可用实现

local-haproxy

在不使用外部lb时的默认方案,通过--lb-mode=haproxy指定

这个是本地lb的实现,就是包装了一个haproxy,里边会直接写好配置供使用。每次添减cp节点时,kubeon会负责更新manifests里的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func main() {
cmd := &cobra.Command{
Args: cobra.NoArgs,
Use: "local-haproxy",
Short: "local-haproxy is a wrapper to haproxy",
RunE: func(cmd *cobra.Command, args []string) error {
return runE(flags, cmd, args)
},
SilenceUsage: true,
Version: version,
}
cmd.Flags().StringSliceVar(
&flags.hosts,
"host",
[]string{}, "master host",
)
}

func runE(flags *flagpole, cmd *cobra.Command, args []string) (err error) {
// 生成配置文件
err = setCfg(flags.hosts)
if nil != err {
return err
}

// 运行haproxy
run := exec.Command("haproxy", "-f", confPath)
run.Stdout = os.Stdout
run.Stderr = os.Stderr
return run.Run()
}

可以看到就是很简单的代码,提供一个local-haproxy命令,接收一组host/ip作为参数,启动时根据参数生成haproxy的配置文件,随后运行haproxy

docker镜像基于haproxy的官方镜像构建,实际运行时参数由kubeon去维护

1
2
3
4
5
6
7
8
9
10
11
12
13
ARG HAPROXY_VER

FROM haproxy:${HAPROXY_VER}

USER root

RUN mkdir -p /etc/haproxy && chown haproxy /etc/haproxy

USER haproxy

COPY local-haproxy /usr/local/bin/local-haproxy

CMD ["local-haproxy"]

apiserver-updater

一个全新的测试方案,借助kube-proxy,通过--lb-mode=updater指定

好的我终于实现了这里的思路。经测试无任何问题,但还是同样的问题:没有大规模验证。小规模集群仍然推荐本地lb

新建svc

这里通过kubeon去生成模版,需要新增一个sa去获取ep权限,并在deployment里使用这个sa

生成svc和ep的模版如下,我最终决定将apiserver放入kube-system

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: v1
kind: Service
metadata:
name: apiserver
namespace: kube-system
labels:
app.kubernetes.io/part-of: "kubeon"
app.kubernetes.io/component: "updater"
spec:
clusterIP: {{.ClusterLbIP}}
ports:
- name: https
port: 6443
targetPort: 6443
protocol: TCP

---
apiVersion: v1
kind: Endpoints
metadata:
name: apiserver
namespace: kube-system
labels:
app.kubernetes.io/part-of: "kubeon"
app.kubernetes.io/component: "updater"
subsets:
- addresses:
{{- range .MasterIPs}}
- ip: {{.}}
{{- end}}
ports:
- name: https
port: 6443
protocol: TCP

sa的声明代码如下,将其和一个ClusterRole绑定即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: apiserver-updater
labels:
app.kubernetes.io/part-of: "kubeon"
app.kubernetes.io/component: "updater"
rbac.authorization.k8s.io/aggregate-to-view: "true"
rules:
- apiGroups:
- ""
resources:
- "endpoints"
verbs:
- "get"
- "list"
- "watch"
- "create"
- "update"

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: apiserver-updater
namespace: kube-system
labels:
app.kubernetes.io/part-of: "kubeon"
app.kubernetes.io/component: "updater"
subjects:
- kind: ServiceAccount
name: kubeon-apiserver-updater
namespace: kube-system
apiGroup: ""
roleRef:
kind: ClusterRole
name: apiserver-updater
apiGroup: rbac.authorization.k8s.io

---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kubeon-apiserver-updater
namespace: kube-system
labels:
app.kubernetes.io/part-of: "kubeon"
app.kubernetes.io/component: "updater"

新建deployment

apiserver-updater服务启动时,会生成一个唯一的id,并通过k8s自带的选举机制确定主备,主节点会执行watchUpdate方法监听kubernetes服务的ep变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func main() {
id, err := os.Hostname()
if err != nil {
fmtError("Get hostname error: %v", err)
}
id = id + "_" + strconv.FormatInt(time.Now().UnixNano(), 16)
rl, err := resourcelock.New("endpoints", SystemNamespace, UpdateResourceName,
client.CoreV1(), client.CoordinationV1(),
resourcelock.ResourceLockConfig{
Identity: id,
})
if err != nil {
fmtError("Create resource lock error: %v", err)
os.Exit(1)
}
leaderelection.RunOrDie(context.TODO(), leaderelection.LeaderElectionConfig{
Lock: rl,
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
watchUpdate(ctx)
},
OnStoppedLeading: func() {
fmtInfo("Leader election lost")
},
},
Name: UpdateResourceName,
})
}

通过SharedInformer接口去监听ep的变化,有变化时去更新apiserver的ep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func watchUpdate(context.Context) {
stop := make(chan struct{})
defer close(stop)

sharedInformer := informers.NewSharedInformerFactoryWithOptions(client, 2*time.Hour,
informers.WithNamespace(DefaultNamespace),
informers.WithTweakListOptions(func(options *metav1.ListOptions) {
options.FieldSelector = "metadata.name=" + DefaultServiceName
}),
)
fmtInfo("Start watch endpoint[%s/%s]", DefaultNamespace, DefaultServiceName)
informer := sharedInformer.Core().V1().Endpoints().Informer()
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(addObj interface{}) {
updateSubsets(addObj.(*v1.Endpoints).Subsets)
},
UpdateFunc: func(oldObj, newObj interface{}) {
updateSubsets(newObj.(*v1.Endpoints).Subsets)
},
})
informer.Run(stop)
}

启动引导

通过systemd在每一个worker上运行一个一次性脚本完成

服务文件定义如下,网络可用并在kubelet之前执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Unit]
Description=Change host when worker startup
ConditionPathExists=/opt/kubeon/apiserver-startup.sh
Wants=network-online.target
After=network-online.target
Before=kubelet.target

[Service]
Type=forking
ExecStart=/opt/kubeon/apiserver-startup.sh
TimeoutSec=0
StandardOutput=tty
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

apiserver-startup.sh同样由kubeon负责生成,就是之前思路的具体实现

  • check_server:通过可用的手段检测ip是否可以连接
  • update_host:修改本机的/etc/hosts文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/env bash
TARGET_DOMAIN={{.TargetDomain}}
VIRTUAL_ADDR={{.VirtualAddr}}
REAL_ADDRS={{.RealAddrs}}
DEFAULT_PORT=6443

function check_server() {
IN_ADDR=$1
IN_PORT=$2
if [[ -e "/usr/bin/curl" ]]; then
curl -m 1 -s -k "https://${IN_ADDR}:${IN_PORT}/healthz" || echo -n no
elif [[ -e "/usr/bin/nc" ]]; then
nc -w 1 -z "${IN_ADDR}" "${IN_PORT}" && echo -n ok || echo -n no
elif [[ -e "/usr/bin/timeout" ]]; then
timeout 1 bash -c "</dev/tcp/${IN_ADDR}/${IN_PORT} &>/dev/null" && echo -n ok || echo -n no
else
bash -c "</dev/tcp/${IN_ADDR}/${IN_PORT} &>/dev/null" && echo -n ok || echo -n no
fi
}

function update_host() {
IN_ADDR=$1
sed -i -E "s/^[0-9a-f.:]+\s+${TARGET_DOMAIN}.*$/${IN_ADDR} ${TARGET_DOMAIN}/g" /etc/hosts
}

if [[ "$(check_server ${VIRTUAL_ADDR} ${DEFAULT_PORT})" == "ok" ]]; then
echo "virtual ip can provide services normally"
exit 0
fi

IS_ALREADY=false
for _ in {1..3}; do
if [[ ${IS_ALREADY} ]]; then
break
fi

for REAL_ADDR in ${REAL_ADDRS//,/ }; do
if [[ "$(check_server ${REAL_ADDR} ${DEFAULT_PORT})" == "ok" ]]; then
echo "replace host with available real ip ${REAL_ADDR}"
update_host ${REAL_ADDR}
IS_ALREADY=true
break
fi
done
done

for _ in {1..300}; do
sleep 5s
if [[ "$(check_server ${VIRTUAL_ADDR} ${DEFAULT_PORT})" == "ok" ]]; then
echo "virtual ip is available, task completed"
update_host ${VIRTUAL_ADDR}
exit 0
fi
done

Kubeon简介及高可用的实现
https://back.pub/post/kubernetes-ha-implement/
作者
Dash
发布于
2021年5月5日
许可协议