目标:
掌握kubernetes operator的开发,并弄清楚operator的原理
1 安装
1.1 安装go开发环境
cd /etc
cp -rf yum.repos.d yum.repos.d_bak
cd /etc/yum.repos.d/
rm -rf ./*
wget http://mirrors.aliyun.com/repo/Centos-7.repo
mv Centos-7.repo CentOs-Base.repo
yum install epel-release -y
yum install golang -y
1.2 导入go环境变量
mkdir -p $HOME/go
mkdir -p $HOME/go/bin
mkdir -p $HOME/go/src
mkdir -p $HOME/go/pkg
cat >>$HOME/.bash_profile<<EOF
export GOROOT=/usr/lib/golang
export GOPATH=$HOME/go
export GO111MODULE=on
export GOPROXY=https://goproxy.io
EOF
source $HOME/.bash_profile
然后修改
vim $HOME/.bash_profile
添加:
export PATH=$PATH:/usr/lib/golang/bin:$HOME/go/bin
source $HOME/.bash_profile
注意:
若无法安装epel-release,则
yum clean all
yum makecache
yum update
1.3 安装delve
mkdir -p $GOPATH/src/github.com/go-delve/delve
git clone https://github.com/go-delve/delve.git $GOPATH/src/github.com/go-delve/delve
cd $GOPATH/src/github.com/go-delve/delve
make install
1.4 安装operator-sdk
mkdir -p $GOPATH/src/github.com/operator-framework
cd $GOPATH/src/github.com/operator-framework
git clone https://github.com/operator-framework/operator-sdk
cd operator-sdk
git checkout v0.12.0
make dep
make install
1.5 验证go环境安装完成
[root@localhost ch1_dir]# go version
go version go1.13.3 linux/amd64
[root@localhost ch1_dir]# whereis go
go: /usr/bin/go
1.6 查看go环境变量
go env
1.7 验证go环境正常
cd $GOPATH/src
vim main.go
写入如下内容
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
执行:
go run ./main.go
1.8 关于环境变量
GOROOT: golang的安装目录
GOPATH:是工作空间,用来存放包的目录。
包含3个子目录:
src:源码, .go等,go run, go install等命令的当前工作路径,在此路径下执行尚书命令
pkg:编译生成的中间文件,比如.a, golang编译包时
bin:编译后生成可执行文件
GOPATH下的src目录就是接下来开发程序的主要目录,所有的源码都是放在这个目录下面,那么一般我们的做法就是一个目录一个项目,
例如: $GOPATH/src/mymath 表示mymath这个应用包或者可执行应用
参考:
https://www.cnblogs.com/enduo/p/9078313.html
2 使用
2.1 创建operator
operator-sdk new dozer-operator
2.2 生成api
operator-sdk add api --api-version=timers.estimer.com/v1alpha1 --kind=Timer
operator-sdk add api --api-version=timers.estimer.com/v1alpha1 --kind=AuditRecord
operator-sdk add api --api-version=timers.estimer.com/v1alpha1 --kind=Action
operator-sdk add api --api-version=timers.estimer.com/v1alpha1 --kind=Execution
2.2 生成controller
operator-sdk add controller --api-version=timers.estimer.com/v1alpha1 --kind=Timer
2.3 修改各个的types.go
例如修改:
pkg/apis/timers/v1alpha1/timer_types.go
修改如下内容:
......
type TimerSpec struct {
Name map[string]interface{} `json:"name"`
Labels []map[string]interface{} `json:"labels"`
}
// TimerStatus defines the observed state of Timer
// +k8s:openapi-gen=true
type TimerStatus struct {
Nodes []string `json:"nodes"`
}
......
可根据实际内容修改。
注意:
Trigger map[string]interface{} `json:"trigger"`
中的`json:"trigger"` 表示从json中的trigger中取出内容赋予Trigger字段。
//可以选择的控制字段有三种:
// -:不要解析这个字段
// omitempty:当字段为空(默认值)时,不要解析这个字段。比如 false、0、nil、长度为 0 的 array,map,slice,string
// FieldName:当解析 json 的时候,使用这个名字
type StudentWithOption struct {
StudentId string //默认使用原定义中的值
StudentName string `json:"sname"` // 解析(encode/decode) 的时候,使用 `sname`,而不是 `Field`
StudentClass string `json:"class,omitempty"` // 解析的时候使用 `class`,如果struct 中这个值为空,就忽略它
StudentTeacher string `json:"-"` // 解析的时候忽略该字段。默认情况下会解析这个字段,因为它是大写字母开头的
}
参考:
https://www.cnblogs.com/fengxm/p/9917686.html
2.4 更新代码
执行:
operator-sdk generate k8s
2.5 编写controller逻辑
1) 修改协调逻辑
具体在:
pkg/controller/timer/timer_controller.go中的
func (r *ReconcileTimer) Reconcile(request reconcile.Request) (reconcile.Result, error)
方法,实现自己的协调逻辑。
2)修改事件过滤
具体在:
pkg/controller/timer/timer_controller.go中的
func add(mgr manager.Manager, r reconcile.Reconciler)
2.6 调试operator
在一个安装了上述golang环境的pod中,开启一个窗口执行:
operator-sdk up local --namespace timed-task --enable-delve --verbose
在一个安装了上述golang环境的pod中,开启另一个窗口执行:
dlv connect 0.0.0.0:2345
加断点:
b main.main
b timer_controller.go:41
b timer_controller.go:86
注意:
请将41,86替换为自己真正想要打断点的代码行号。
注意:
执行operator-sdk up local --namespace timed-task --enable-delve --verbose
命令之前,确保2345和8333端口没有被占用。
2.7 制作镜像
operator-sdk build qingyuanluo/dozer-operator:v0.0.1
sed -i 's|REPLACE_IMAGE|qingyuanluo/dozer-operator:v0.0.1|g' deploy/operator.yaml
docker push qingyuanluo/dozer-operator:v0.0.1
3 原理
3.1 operator作用与组成
作用: 简化复杂有状态应用管理,其通过CRD扩展 Kubernetes API 来自动创建、管理和配置应用实例。
但群集仍需要控制器来监视其状态并协调资源以匹配声明。
通过扩展Kubernetes定义Custom Controller,观察应用并根据实际状态执行自定义任务
部署: Operator以deployment的形式部署到K8S中。
构建Operator:
为了创建自定义Operator,我们需要如下资源:
Custom Resource(CR)spec,定义我们要观测的应用对象,以及为CR定义的API
Custom Controller,用来观测CR
Custom code,决定Custom Controller如何协调CR
Operator,管理Custom Controller
Deployment,定义Operator和自定义资源
参考:
http://dockone.io/article/8769
3.2 operator框架
3.2.1) Operator Framework作用
用于快速开发 Operator 的工具包,该框架包含两个主要的部分:
Operator SDK: 根据自己需求来构建operator应用
Operator Lifecycle Manager OLM: 安装管理operator
3.2.2) Operator Framework开发工作流
1)使用 SDK 创建一个新的 Operator 项目
2)通过添加自定义资源(CRD)定义新的资源 API
3)指定使用 SDK API 来 watch 的资源
4)定义 Operator 的协调(reconcile)逻辑
5)使用 Operator SDK 构建并生成 Operator 部署清单文件
3.3 operator监控的资源
控制器检测的资源:
一级资源和二级资源。
对于replicaSet,一级资源是ReplicaSet本身,指定运行的
Docker镜像以及Pod的副本数。
二级资源是Pod,当ReplicaSet定义发生改变(例如镜像版本或Pod数量),
或者Pod发生改变(例如某个Pod被删除),控制器则会通过推出新版本来协调集群状态,
或者对Pod数量扩缩容。
对于DaemonSet,一级资源是DaemonSet本身,而二级资源是Pod和Node。不同之处在于,控制器也会监测集群Node变化,以便在群集增大或缩小时添加或删除Pod。
明确识别出Operator的主要和二级资源是至关重要的,因为他们将决定其控制器的行为。
3.4 operator的原理
Operator 实际上作为kubernetes自定义扩展资源注册到controller-manager,通过list and watch的方式监听对应资源的变化,然后在周期内的各个环节做相应的协调处理。所谓的处理就是operator实现由状态的应用的核心部分,当然不同应用其处理的方式也不同。
参考:
https://github.com/operator-framework/operator-sdk/blob/master/doc/user/install-operator-sdk.md
https://feisky.gitbooks.io/kubernetes/apps/operator.html
http://dockone.io/article/8769
https://github.com/operator-framework/operator-sdk
http://dockone.io/article/8733
https://www.jianshu.com/p/628aac3e6758
http://www.imooc.com/article/294186
https://www.cnblogs.com/liabio/p/11723809.html
4 sample-controller源码分析
1)代码主入口
sample-controller/main.go
代码如下:
func main() {
klog.InitFlags(nil)
flag.Parse()
// set up signals so we handle the first shutdown signal gracefully
stopCh := signals.SetupSignalHandler()
cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
if err != nil {
klog.Fatalf("Error building kubeconfig: %s", err.Error())
}
kubeClient, err := kubernetes.NewForConfig(cfg)
if err != nil {
klog.Fatalf("Error building kubernetes clientset: %s", err.Error())
}
exampleClient, err := clientset.NewForConfig(cfg)
if err != nil {
klog.Fatalf("Error building example clientset: %s", err.Error())
}
kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)
exampleInformerFactory := informers.NewSharedInformerFactory(exampleClient, time.Second*30)
controller := NewController(kubeClient, exampleClient,
kubeInformerFactory.Apps().V1().Deployments(),
exampleInformerFactory.Samplecontroller().V1alpha1().Foos())
// notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(stopCh)
// Start method is non-blocking and runs all registered informers in a dedicated goroutine.
kubeInformerFactory.Start(stopCh)
exampleInformerFactory.Start(stopCh)
if err = controller.Run(2, stopCh); err != nil {
klog.Fatalf("Error running controller: %s", err.Error())
}
}
分析:
大体流程就是:
s1)读取 kubeconfig 配置,构造用于事件监听的 Kubernetes Client
s2)这里创建了两个,一个监听普通事件,一个监听 Foo 事件
s3)基于 Client 构造监听相关的 informer
s4)基于 Client、Informer 初始化自定义 Controller,监听 Deployment 以及 Foos 资源变化
s5)开启 Controller
接下来分析Controller是如何处理事件的
2) 查看Controller的定义
代码在:
sample-controller/controller.go
代码如下:
// Controller is the controller implementation for Foo resources
type Controller struct {
// kubeclientset is a standard kubernetes clientset
kubeclientset kubernetes.Interface
// sampleclientset is a clientset for our own API group
sampleclientset clientset.Interface
deploymentsLister appslisters.DeploymentLister
deploymentsSynced cache.InformerSynced
foosLister listers.FooLister
foosSynced cache.InformerSynced
// workqueue is a rate limited work queue. This is used to queue work to be
// processed instead of performing it as soon as a change happens. This
// means we can ensure we only process a fixed amount of resources at a
// time, and makes it easy to ensure we are never processing the same item
// simultaneously in two different workers.
workqueue workqueue.RateLimitingInterface
// recorder is an event recorder for recording Event resources to the
// Kubernetes API.
recorder record.EventRecorder
}
分析:
Controller 的关键成员即两个事件的 Listener(appslisters.DeploymentLister、listers.FooLister)
这两个成员将由 main 函数传入参数进行初始化
此外,为了缓冲事件处理,这里使用队列暂存事件,相关成员即为 workqueue.RateLimitingInterface
record.EventRecorder 用于记录事件
3) 分析Controller的构造过程
// NewController returns a new sample controller
func NewController(
kubeclientset kubernetes.Interface,
sampleclientset clientset.Interface,
deploymentInformer appsinformers.DeploymentInformer,
fooInformer informers.FooInformer) *Controller {
// Create event broadcaster
// Add sample-controller types to the default Kubernetes Scheme so Events can be
// logged for sample-controller types.
utilruntime.Must(samplescheme.AddToScheme(scheme.Scheme))
klog.V(4).Info("Creating event broadcaster")
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(klog.Infof)
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events("")})
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerAgentName})
controller := &Controller{
kubeclientset: kubeclientset,
sampleclientset: sampleclientset,
deploymentsLister: deploymentInformer.Lister(),
deploymentsSynced: deploymentInformer.Informer().HasSynced,
foosLister: fooInformer.Lister(),
foosSynced: fooInformer.Informer().HasSynced,
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Foos"),
recorder: recorder,
}
klog.Info("Setting up event handlers")
// Set up an event handler for when Foo resources change
fooInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: controller.enqueueFoo,
UpdateFunc: func(old, new interface{}) {
controller.enqueueFoo(new)