kubeon 是一个用来一键安装kubernetes
高可用集群的命令行工具,特性如下
可以在一台中控机上管理多个集群
使用堆叠etcd,提供外部lb、local-haproxy
、apiserver-updater
三种高可用模式
可以在国内网络环境下正常使用
只需要手动下载这个命令行工具,不需要手动下载其它安装包,所以中控机需要能联网
支持密码和密钥两种模式登录所有节点
有一个直观的完善的cluster-info
命令
进行过中等规模集群验证(300+),足够的简单并易用,证书100年有效期缓解焦虑
对新版本支持够快,并且免费(通常从1.x.1开始支持,不包括1.x.0)
常用操作
create:创建一个新集群
destroy:销毁一个存在的集群
add:添加一个节点
del:删除一个节点
upgrade:升级集群版本
use:选择一个存在的集群作为默认
exec:在集群上选择部分或全部节点执行命令
cp:在集群上选择部分或全部节点复制文件进去
更多常用操作和命令行使用请查看readme
新建流程概要
kubeon会将除了命令行之外的其它资源都上传到doker hub和阿里云的hub
新建集群时,首先校验节点信息,主要是host是否可达,有无重复之类的
如果本地资源包不存在,直接从线上下载;如果存在,对比线上版本,不一致的才下载
所有资源包下载完成,按需copy到需要安装的节点上
进行节点的预处理,主要是设置内核参数、设置limit、通过原生系统的包管理安装一些包、删除一些包
kubeon生成配置文件,并展开资源包,安装docker/containerd和kubelet
接下来是对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 } 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_VERFROM haproxy:${HAPROXY_VER}USER rootRUN mkdir -p /etc/haproxy && chown haproxy /etc/haproxy USER haproxyCOPY 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=6443function 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 0fi 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