Contents
为什么需要 Kubernetes Operator?
在查阅 Kubernetes Operators 由来的时候,看到阿里云的这篇文章[8]. 这篇文章的作者之一邓洪超是下文学习的 etcd operator 的作者之一。这篇文章讲述了 Operator 的历史,写出了历史大片的感觉,值得一看。
按照我的理解,Operator 的就是一个运维人员,只是这个运维人员是软件而不是人类。 对于无状态的应用,像是 web server, 其实用 k8s 本身的 deployment 就足够了,一个 pod 坏了,自动部署一个新的 pod, 又有弹性又高可用。但是对于一些稍微复杂的应用,尤其是有状态,有特定拓扑的应用,就需要特定的逻辑才能管理起来。 比如 etcd, 如果有一半以上的节点挂了,那么就会出现 quorum lost, 集群无法工作。还是 etcd, 里面的数据需要定期备份,万一整个集群挂了,也能及时地恢复。 这些操作可以通过人类实现,也可以通过软件实现,Operator 就是这样的软件,它会自动管理 k8s 上一个特定的复杂应用。
于是,Operator 也成为了应用软件在 Kubernetes 上的一种标准化交付方式。应用的开发者可以定义好如何运维这个应用,用户想用某个应用,只需要安装上相应的 Operator,那么 Operator 就会自动完成整个应用生命周期的管理,包括安装、升级、备份、恢复等等。
如果把饼画得大一点,在 k8s 里,下有管理 k8s 节点的 Operator, 上有管理应用的 Operator, 那么这整个平台都是自动管理的,可以做到免运维,这很云计算。
CR & CRD
在 Kubernetes 里,Pod, ReplicaSet 这些都是一个 Resource. 我们也可以定义自己的一个 Resource, 例如,可以定义一个叫 EtcdCluster 的 Resource, 这个 Resource 实际上包含 Pod, Service 等一系列构成一个 etcd cluster 集群的资源。 定义这个 CR 的东西,叫做 Custom Resource Definition. 有了这个 Definition, 就可以创建多个这个 Definition 所定义的 CR. 我的类比是,CRD 是 Class, CR 是 Instance.
运行一个 etcd Operator
在本地环境中,可以通过 minikube[5] 来测试 Kubernetes. 首先,需要安装上 VirtualBox 或者 KVM. 我在 RHEL8 中安装了 KVM. 按照文档[5]下载并安装 minikube, 指定 kvm 作为驱动,启动一个本地的 Kubernetes 集群。
$ kubectl get po -A
下载预先写好的配置文件,用以部署 etcd operator.
$ cd chapters/ch03
首先,创建一个 CRD. 这个 CRD 定义 etcdcluster 这个 Custom Resource.
$ cat etcd-operator-crd.yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: etcdclusters.etcd.database.coreos.com
spec:
group: etcd.database.coreos.com
names:
kind: EtcdCluster
listKind: EtcdClusterList
plural: etcdclusters
shortNames:
- etcdclus
- etcd
singular: etcdcluster
scope: Namespaced
version: v1beta2
versions:
- name: v1beta2
served: true
storage: true
customresourcedefinition.apiextensions.k8s.io/etcdclusters.etcd.database.coreos.com created
$ kubectl get crd
NAME CREATED AT
etcdclusters.etcd.database.coreos.com 2020-08-07T10:07:28Z
然后,创建一个 Service account, 这个 Service account 是 Operator 用来操作集群资源的用户。Kubernetes 使用 Role-Based Access Control (RBAC) 来控制权限。 在 Kubernetes 中,普通用户的 account 是通过 OpenID 等外部源管理的。而 Service account 则不是给人类用户使用的,是给 Kubernetes 上的程序操作集群资源使用的。
apiVersion: v1
kind: ServiceAccount
metadata:
name: etcd-operator-sa
serviceaccount/etcd-operator-sa created
$ kubectl get sa
NAME SECRETS AGE
default 1 5d4h
etcd-operator-sa 1 4s
有了 Service account, 我们还需要创建一个 Role, 给这个 Service account 使用。可以理解为, Service account 是一个用户,这个用户需要一个角色(role)才能获得这个 Role 所指定的权限。将 Role 赋予 Service account 的动作是 Rolebinding.
创建一个 Role, 这个 Role 定义了与资源对应的允许的动作:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: etcd-operator-role
rules:
- apiGroups:
- etcd.database.coreos.com
resources:
- etcdclusters
- etcdbackups
- etcdrestores
verbs:
- '*'
- apiGroups:
- ""
resources:
- pods
- services
- endpoints
- persistentvolumeclaims
- events
verbs:
- '*'
- apiGroups:
- apps
resources:
- deployments
verbs:
- '*'
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
创建一个 rolebinding, 关联 Service account 和 Role.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: etcd-operator-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: etcd-operator-role
subjects:
- kind: ServiceAccount
name: etcd-operator-sa
namespace: default
role.rbac.authorization.k8s.io/etcd-operator-role created
$ kubectl create -f etcd-operator-rolebinding.yaml
rolebinding.rbac.authorization.k8s.io/etcd-operator-rolebinding created
$ kubectl get roles
NAME CREATED AT
etcd-operator-role 2020-08-09T06:28:02Z
$ kubectl get rolebindings
NAME ROLE AGE
etcd-operator-rolebinding Role/etcd-operator-role 12s
然后,我们可以部署 Operator 的真身。其实 Operator 也是一个 Pod, 里面运行的是控制器,这个控制器会监控之前定义的 Customer Resource 的状况,并进行相应的运维操作。这个 Deployment 里,指定了它所使用的 Service account.
apiVersion: apps/v1
kind: Deployment
metadata:
name: etcd-operator
spec:
selector:
matchLabels:
app: etcd-operator
replicas: 1
template:
metadata:
labels:
app: etcd-operator
spec:
containers:
- name: etcd-operator
image: quay.io/coreos/etcd-operator:v0.9.4
command:
- etcd-operator
- --create-crd=false
env:
- name: MY_POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
imagePullPolicy: IfNotPresent
serviceAccountName: etcd-operator-sa
deployment.apps/etcd-operator created
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
etcd-operator 1/1 1 1 9s
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
etcd-operator-d455d6d75-46vm4 1/1 Running 0 2m21s
至此,Custom Resource (EtcdCluster) 已经定义好了,控制这个 CR 的 Operator (etcd-operator) 也部署好了。现在,我们可以在 Kubernetes 中创建一个 EtcdCluster 资源,只要给定 EtcdCluster 资源的参数,剩下的事情 etcd-operator 就会代劳了。
apiVersion: etcd.database.coreos.com/v1beta2
kind: EtcdCluster
metadata:
name: example-etcd-cluster
spec:
size: 3
version: 3.1.10
etcdcluster.etcd.database.coreos.com/example-etcd-cluster created
$ kubectl get etcdclusters
NAME AGE
example-etcd-cluster 9s
稍等片刻,就能看到 etcd 相关的 pod 就自动创建了,相关的 etcd service 也一并创建了。
NAME READY STATUS RESTARTS AGE
etcd-operator-d455d6d75-46vm4 1/1 Running 0 22m
example-etcd-cluster-4hzzqvjqtq 1/1 Running 0 85s
example-etcd-cluster-5sbgjbbbsg 1/1 Running 0 2m5s
example-etcd-cluster-lcdrrpc9jp 1/1 Running 0 45s
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
example-etcd-cluster ClusterIP None <none> 2379/TCP,2380/TCP 2m28s
example-etcd-cluster-client ClusterIP 10.105.69.22 <none> 2379/TCP 2m28s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5d5h
测试这个 etcd 集群,往它上面存一些数据。
/ # export ETCDCTL_API=3
/ # export ETCDCSVC=http://example-etcd-cluster-client:2379
/ # etcdctl --endpoints $ETCDCSVC put foo bar
OK
/ # etcdctl --endpoints $ETCDCSVC get foo
foo
bar
测试增加 etcd 节点数量。修改 etcd-cluster-cr.yaml:
apiVersion: etcd.database.coreos.com/v1beta2
kind: EtcdCluster
metadata:
name: example-etcd-cluster
spec:
size: 4
version: 3.1.10
可以看到会自动增加一个 etcd pod.
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
etcd-operator-d455d6d75-46vm4 1/1 Running 0 91m
example-etcd-cluster-4hzzqvjqtq 1/1 Running 0 70m
example-etcd-cluster-5sbgjbbbsg 1/1 Running 0 71m
example-etcd-cluster-lcdrrpc9jp 1/1 Running 0 70m
example-etcd-cluster-zmq8mwtfqk 0/1 Init:0/1 0 7s
尝试升级 etcd. 修改 etcd-cluster-cr.yaml 里面的版本信息。
apiVersion: etcd.database.coreos.com/v1beta2
kind: EtcdCluster
metadata:
name: example-etcd-cluster
spec:
size: 4
version: 3.2.13
观察 pod 的变化,可以看到在一段时间后,describe 这些 pod, 可以看到版本也变了。
$ kubectl describe pod/example-etcd-cluster-4hzzqvjqtq
Annotations: etcd.version: 3.2.13
尝试删掉其中一个 pod, Operator 会自动拉起新的 pod.
NAME READY STATUS RESTARTS AGE
etcd-operator-d455d6d75-46vm4 1/1 Running 0 98m
example-etcd-cluster-5sbgjbbbsg 1/1 Running 1 78m
example-etcd-cluster-d9x7mzkmfg 0/1 Running 0 33s
example-etcd-cluster-lcdrrpc9jp 1/1 Running 1 76m
example-etcd-cluster-zmq8mwtfqk 1/1 Running 1 6m47s
kubectl delete pod/example-etcd-cluster-4hzzqvjqtq
测试结束,删掉相关资源。
$ kubectl delete -f etcd-operator-role.yaml
$ kubectl delete -f etcd-operator-rolebinding.yaml
$ kubectl delete -f etcd-operator-crd.yaml
$ kubectl delete -f etcd-operator-deployment.yaml
$ kubectl delete -f etcd-cluster-cr.yaml
etcd Operator 的逻辑是怎样的?
作为经典的学习案例,我们可以查看 etcd Operator 是如何实现的。它的源码可以从[6]获得。
可以看到,除了控制节点的数量和版本之外,Operator 还提供了 backup/restore 的功能,对于这种有状态的应用,自动的备份和恢复可以提高 etcd 的容灾能力,无需运维人员手工操作。
Operator 的核心逻辑在 reconcile 方法上。Kubernetes 会时不时地触发 reconcile, 通常是在 CR 相关资源发生变动的时候触发,但也可以随时触发,这就要求 Operator/reconcile 的操作是幂等的。[2]
查看 etcd-operator/pkg/cluster/reconcile.go,可以看到 reconcile 方法的入口:
// - it tries to reconcile the cluster to desired size.
// - if the cluster needs for upgrade, it tries to upgrade old member one by one.
func (c *Cluster) reconcile(pods []*v1.Pod) error {
c.logger.Infoln("Start reconciling")
defer c.logger.Infoln("Finish reconciling")
defer func() {
c.status.Size = c.members.Size()
}()
sp := c.cluster.Spec
running := podsToMemberSet(pods, c.isSecureClient())
if !running.IsEqual(c.members) || c.members.Size() != sp.Size {
return c.reconcileMembers(running)
}
c.status.ClearCondition(api.ClusterConditionScaling)
if needUpgrade(pods, sp) {
c.status.UpgradeVersionTo(sp.Version)
m := pickOneOldMember(pods, sp.Version)
return c.upgradeOneMember(m.Name)
}
c.status.ClearCondition(api.ClusterConditionUpgrading)
c.status.SetVersion(sp.Version)
c.status.SetReadyCondition()
return nil
}
它的逻辑是这样的[7]. 先调谐 etcd 节点的数量,再调谐 etcd 节点的版本,而且,在每次 reconcile 操作中,只修改一个资源。
查看 reconcileMembers 的实现。它会先剔除不属于 etcd 集群的但是又意外运行的 pod, 然后,将 pod 的数量调谐为 etcdCluster 指定的数量。
// - running pods on k8s and cluster membership
// - cluster membership and expected size of etcd cluster
// Steps:
// 1. Remove all pods from running set that does not belong to member set.
// 2. L consist of remaining pods of runnings
// 3. If L = members, the current state matches the membership state. END.
// 4. If len(L) < len(members)/2 + 1, return quorum lost error.
// 5. Add one missing member. END.
func (c *Cluster) reconcileMembers(running etcdutil.MemberSet) error {
c.logger.Infof("running members: %s", running)
c.logger.Infof("cluster membership: %s", c.members)
unknownMembers := running.Diff(c.members)
if unknownMembers.Size() > 0 {
c.logger.Infof("removing unexpected pods: %v", unknownMembers)
for _, m := range unknownMembers {
if err := c.removePod(m.Name); err != nil {
return err
}
}
}
L := running.Diff(unknownMembers)
if L.Size() == c.members.Size() {
return c.resize()
}
if L.Size() < c.members.Size()/2+1 {
return ErrLostQuorum
}
c.logger.Infof("removing one dead member")
// remove dead members that doesn't have any running pods before doing resizing.
return c.removeDeadMember(c.members.Diff(L).PickOne())
}
值得留意的是,无论是增加节点还是删除节点,在一次 reconcile 中,只增加或者删除一个节点。升级节点也是,我们可以看到很多 PickOne*** 的方法调用。
if c.members.Size() == c.cluster.Spec.Size {
return nil
}
if c.members.Size() < c.cluster.Spec.Size {
return c.addOneMember()
}
return c.removeOneMember()
Machine Config Operator
https://github.com/openshift/machine-config-operator
//TODO
用 Operator SDK 创建一个 Operator
那要怎么开始写一个 Operator 呢?要从头开始写各种 API 是件痛苦的事情。使用 Operator SDK, 我们可以用 Go 语言编写核心的逻辑,剩下的事情可以交给 SDK 来处理。Operator SDK 也支持 Ansible 和 Helm,不过相比于 Go, 它们的灵活性相对较低。
安装 Operator SDK 比较简单的方法是直接下载 binary.[9]
动物书给的例子是用 v0.11.x 版本的 SDK, 目前看官方文档的最新的版本是 v0.19.2[10],它要求 Go 版本 > 1.13, 而我目前 RHEL8 的版本是 1.11.5,啥都不兼容。卸载 RHEL8 本身的 golang, 直接从 golang 网站下载最新版本安装。
按照 Operator SDK 给出的例子,用 operator-sdk 创建一个项目,并创建 API.
$ cd $HOME/projects/memcached-operator
$ operator-sdk init --domain=example.com --repo=github.com/example-inc/memcached-operator
$ operator-sdk create api --group=cache --version=v1alpha1 --kind=Memcached
修改 api/v1alpha1/memcached_types.go,定义这个 Operator 的输入参数,以及状态值。
type MemcachedSpec struct {
// +kubebuilder:validation:Minimum=0
// Size is the size of the memcached deployment
Size int32 `json:"size"`
}
// MemcachedStatus defines the observed state of Memcached
type MemcachedStatus struct {
// Nodes are the names of the memcached pods
Nodes []string `json:"nodes"`
}
修改完后,用以下命令让 SDK 生成相关的 code.
$ make manifests
接下来修改 opertor 的主要逻辑。可以直接用官方提供的源码:
https://github.com/operator-framework/operator-sdk/blob/v0.19.x/example/kb-memcached-operator/memcached_controller.go.tmpl
将上述源码替换到 controllers/memcached_controller.go 中。
然后,build 这个 Operator.
$ make install
要部署这个 Operator,我们需要将它的镜像上传到公共的 Repo 中。注册一个 quay.io 的账号,然后 docker-build 和 docker-push 到 quay 中。
$ export USERNAME=<quay-username>
$ make docker-build IMG=quay.io/$USERNAME/memcached-operator:v0.0.1
$ make docker-push IMG=quay.io/$USERNAME/memcached-operator:v0.0.1
我在 docker-build 的过程中,遇到了以下问题:
occurred
这个通过安装 kubebuilder 得到解决。
make: *** [Makefile:71: docker-build] Error 1
这个通过将用户加到 subuid 和 subgid 解决。
rhel-liveuser:100000:65536
splunk:165536:65536
testuser:231072:65536
siwu:100000:65536
$ cat /etc/subgid
rhel-liveuser:100000:65536
splunk:165536:65536
testuser:231072:65536
siwu:100000:65536
另一个问题是卡在 go mod download 这一步,没有任何输出。
STEP 8: RUN go mod download
^^ taking forever to download. no verbose info.
这是因为 RHEL8.2 自带的 podman 有已知的问题,使用 Fedora 32 最新的 podman 可以解决问题。
用 OLM (Operator Lifecycle Manager) 管理 Operators
参考文档
[1] 免费的动物书 https://www.redhat.com/cms/managed-files/cl-oreilly-kubernetes-operators-ebook-f21452-202001-en_2.pdf
[2] https://www.openshift.com/blog/7-best-practices-for-writing-kubernetes-operators-an-sre-perspective
[3] https://coreos.com/blog/introducing-operators.html
[4] https://coreos.com/blog/introducing-the-etcd-operator.html
[5] https://minikube.sigs.k8s.io/docs/start/
[6] https://github.com/coreos/etcd-operator
[7] https://github.com/coreos/etcd-operator/blob/master/doc/design/reconciliation.md
[8] https://developer.aliyun.com/article/685522
[9] https://sdk.operatorframework.io/docs/install-operator-sdk/
[10] https://sdk.operatorframework.io/docs/golang/quickstart/