如何从零开始用Kubebuilder构建Operator?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1598个文字,预计阅读时间需要7分钟。
介绍:假设计算一个Nginx的QPS(服务器一秒内处理的请求数)上限为500,如果外部访问的QPS达到600,为确保服务质量,必须扩容一个Nginx来分担请求。在Kubernetes环境中,如果外部请求超过限制,必须扩容。
介绍假设一个Nginx的QPS(服务器一秒内处理的请求数)上限为500,如果外部访问的QPS达到了600,为了保证服务质量,必须扩容一个Nginx来分摊请求。
在Kubernetes环境中,如果外部请求超过了单个Pod的处理极限,我们则可以增加Pod数量来达到横向扩容的目的。
假设我们的服务是无状态服务,我们来利用kubebuilder来开发一个operator,来模拟我们已上所述的场景。
项目初始化在开发 Operator 之前我们需要先提前想好我们的 CRD 资源对象,比如我们想要通过下面的 CR 资源来创建我们的Operator :
apiVersion: elasticweb.example.com/v1
kind: ElasticWeb
metadata:
name: elasticweb-sample
namespace: dev
spec:
image: nginx:1.17.1 # 镜像
port: 30003 # 外部访问的端口
singlePodsQPS: 800 # 单个 Pod 的 QPS
totalQPS: 2400 # 总 QPS
首先初始化项目,这里使用kubebuilder来构建我们的脚手架:
$ mkdir app-operator && cd app-operator
$ go mod init app-operator
$ kubebuilder init --domain example.com
kubebuilder init --domain example.com
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
...
脚手架创建完成后,然后定义资源API:
$ kubebuilder create api --group elasticweb --version v1 --kind El
asticWeb
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
...
这样我们的项目初始化就完成了,整体的代码结构如下:
$ tree -L 2
.
├── Dockerfile
├── Makefile
├── PROJECT
├── api
│ └── v1
├── bin
│ └── controller-gen
├── config
│ ├── crd
│ ├── default
│ ├── manager
│ ├── prometheus
│ ├── rbac
│ └── samples
├── controllers
│ ├── elasticweb_controller.go
│ └── suite_test.go
├── go.mod
├── go.sum
├── hack
│ └── boilerplate.go.txt
└── main.go
12 directories, 10 files
然后根据我们上面设计的 ElasticWeb 这个对象来编辑 Operator 的结构体即可,修改文件 api/v1/elasticweb_types.go 中的 ElasticWebSpec 结构体以及ElasticWebStatus结构体,ElasticWebStatus结构体主要用来记录当前集群实际支持的总QPS:
// api/v1/elasticweb_types.go
type ElasticWebSpec struct {
Image string `json:"image"`
Port *int32 `json:"port"`
// 单个pod的QPS上限
SinglePodsQPS *int32 `json:"singlePodsQPS"`
// 当前整个业务的QPS
TotalQPS *int32 `json:"totalQPS,omitempty"`
}
type ElasticWebStatus struct {
// 当前 Kubernetes 集群实际支持的总QPS
RealQPS *int32 `json:"realQPS"`
}
同样,为了打印的日志方便我们阅读,我们给ElasticWeb添加一个String方法:
// api/v1/elasticweb_types.go
func (e *ElasticWeb) String() string {
var realQPS string
if nil == e.Status.RealQPS {
realQPS = ""
} else {
realQPS = strconv.Itoa(int(*e.Status.RealQPS))
}
return fmt.Sprintf("Image [%s], Port [%d], SinglePodQPS [%d], TotalQPS [%d], RealQPS [%s]",
e.Spec.Image,
*e.Spec.Port,
*e.Spec.SinglePodsQPS,
*e.Spec.TotalQPS,
realQPS)
}
要注意每次修改完成需要执行make命令重新生成代码:
$ make
make
/Users/Christian/Documents/code/negan/app-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
api/v1/elasticweb_types.go
go vet ./...
go build -o bin/manager main.go
接下来我们就可以去控制器的 Reconcile 函数中来实现我们自己的业务逻辑了。
业务逻辑首先在目录 controllers 下面创建一个 resource.go文件,用来根据我们的ElasticWeb对象生成对应的deployment和service以及更新状态。
// controllers/resource.go
package controllers
import (
v1 "app-operator/api/v1"
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
)
var (
ElasticWebCommonLabelKey = "app"
)
const (
// APP_NAME deployment 中 App 标签名
APP_NAME = "elastic-app"
// CONTAINER_PORT 容器的端口号
CONTAINER_PORT = 8080
// CPU_REQUEST 单个POD的CPU资源申请
CPU_REQUEST = "100m"
// CPU_LIMIT 单个POD的CPU资源上限
CPU_LIMIT = "100m"
// MEM_REQUEST 单个POD的内存资源申请
MEM_REQUEST = "512Mi"
// MEM_LIMIT 单个POD的内存资源上限
MEM_LIMIT = "512Mi"
)
// 根据总QPS以及单个POD的QPS,计算需要多少个Pod
func getExpectReplicas(elasticWeb *v1.ElasticWeb) int32 {
// 单个pod的QPS
singlePodQPS := *elasticWeb.Spec.SinglePodsQPS
// 期望的总QPS
totalQPS := *elasticWeb.Spec.TotalQPS
// 需要创建的副本数
replicas := totalQPS / singlePodQPS
if totalQPS%singlePodQPS != 0 {
replicas += 1
}
return replicas
}
// CreateServiceIfNotExists 创建service
func CreateServiceIfNotExists(ctx context.Context, r *ElasticWebReconciler, elasticWeb *v1.ElasticWeb, req ctrl.Request) error {
logger := log.FromContext(ctx)
logger.WithValues("func", "createService")
svc := &corev1.Service{}
svc.Name = elasticWeb.Name
svc.Namespace = elasticWeb.Namespace
svc.Spec = corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "goproxy.cn
RUN go mod download
# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go
# Use distroless as minimal base image to package the manager binary
# Refer to github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]
接下来就是登陆docker了,我这边使用的docker hub,直接在命令行登陆即可。
$ docker login
Authenticating with existing credentials...
Login Succeeded
Logging in with your password grants your terminal complete access to your account.
For better security, log in with a limited-privilege personal access token. Learn more at docs.docker.com/go/access-tokens/
登陆成功后,就可以构建镜像了。
注意如果你用的是Mac M1的电脑,那么需要对Makefile做一小点修改,具体可见issues
.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
#KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) --arch=amd64 use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
接下来就是构建并将镜像推送到镜像仓库:
$ make docker-build docker-push IMG=<some-registry>/<project-name>:tag
$ make docker-build docker-push IMG=huiyichanmian/elasitcweb:v0.0.1
等待推送成功后,就可以根据IMG指定的镜像将控制器部署到集群中:
$ make deploy IMG=<some-registry>/<project-name>:tag
$ make deploy IMG=huiyichanmian/elasticweb:v0.0.1
同样,这里可能会遇到镜像gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0这个镜像拉不下来的情况,这里可以使用kubesphere/kube-rbac-proxy:v0.8.0进行替代。
可以直接修改config/default/manager_auth_proxy_patch.yaml或者使用docker tag进行改名。
部署完成后,系统会自动创建项目名- system的命名空间,我们的控制器所有东西都在这个namespace下。
最后如果要从集群中卸载operator也很简单:
$ make undeploy
参考:
xinchen.blog.csdn.net/article/details/113836090
本文共计1598个文字,预计阅读时间需要7分钟。
介绍:假设计算一个Nginx的QPS(服务器一秒内处理的请求数)上限为500,如果外部访问的QPS达到600,为确保服务质量,必须扩容一个Nginx来分担请求。在Kubernetes环境中,如果外部请求超过限制,必须扩容。
介绍假设一个Nginx的QPS(服务器一秒内处理的请求数)上限为500,如果外部访问的QPS达到了600,为了保证服务质量,必须扩容一个Nginx来分摊请求。
在Kubernetes环境中,如果外部请求超过了单个Pod的处理极限,我们则可以增加Pod数量来达到横向扩容的目的。
假设我们的服务是无状态服务,我们来利用kubebuilder来开发一个operator,来模拟我们已上所述的场景。
项目初始化在开发 Operator 之前我们需要先提前想好我们的 CRD 资源对象,比如我们想要通过下面的 CR 资源来创建我们的Operator :
apiVersion: elasticweb.example.com/v1
kind: ElasticWeb
metadata:
name: elasticweb-sample
namespace: dev
spec:
image: nginx:1.17.1 # 镜像
port: 30003 # 外部访问的端口
singlePodsQPS: 800 # 单个 Pod 的 QPS
totalQPS: 2400 # 总 QPS
首先初始化项目,这里使用kubebuilder来构建我们的脚手架:
$ mkdir app-operator && cd app-operator
$ go mod init app-operator
$ kubebuilder init --domain example.com
kubebuilder init --domain example.com
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
...
脚手架创建完成后,然后定义资源API:
$ kubebuilder create api --group elasticweb --version v1 --kind El
asticWeb
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
...
这样我们的项目初始化就完成了,整体的代码结构如下:
$ tree -L 2
.
├── Dockerfile
├── Makefile
├── PROJECT
├── api
│ └── v1
├── bin
│ └── controller-gen
├── config
│ ├── crd
│ ├── default
│ ├── manager
│ ├── prometheus
│ ├── rbac
│ └── samples
├── controllers
│ ├── elasticweb_controller.go
│ └── suite_test.go
├── go.mod
├── go.sum
├── hack
│ └── boilerplate.go.txt
└── main.go
12 directories, 10 files
然后根据我们上面设计的 ElasticWeb 这个对象来编辑 Operator 的结构体即可,修改文件 api/v1/elasticweb_types.go 中的 ElasticWebSpec 结构体以及ElasticWebStatus结构体,ElasticWebStatus结构体主要用来记录当前集群实际支持的总QPS:
// api/v1/elasticweb_types.go
type ElasticWebSpec struct {
Image string `json:"image"`
Port *int32 `json:"port"`
// 单个pod的QPS上限
SinglePodsQPS *int32 `json:"singlePodsQPS"`
// 当前整个业务的QPS
TotalQPS *int32 `json:"totalQPS,omitempty"`
}
type ElasticWebStatus struct {
// 当前 Kubernetes 集群实际支持的总QPS
RealQPS *int32 `json:"realQPS"`
}
同样,为了打印的日志方便我们阅读,我们给ElasticWeb添加一个String方法:
// api/v1/elasticweb_types.go
func (e *ElasticWeb) String() string {
var realQPS string
if nil == e.Status.RealQPS {
realQPS = ""
} else {
realQPS = strconv.Itoa(int(*e.Status.RealQPS))
}
return fmt.Sprintf("Image [%s], Port [%d], SinglePodQPS [%d], TotalQPS [%d], RealQPS [%s]",
e.Spec.Image,
*e.Spec.Port,
*e.Spec.SinglePodsQPS,
*e.Spec.TotalQPS,
realQPS)
}
要注意每次修改完成需要执行make命令重新生成代码:
$ make
make
/Users/Christian/Documents/code/negan/app-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
api/v1/elasticweb_types.go
go vet ./...
go build -o bin/manager main.go
接下来我们就可以去控制器的 Reconcile 函数中来实现我们自己的业务逻辑了。
业务逻辑首先在目录 controllers 下面创建一个 resource.go文件,用来根据我们的ElasticWeb对象生成对应的deployment和service以及更新状态。
// controllers/resource.go
package controllers
import (
v1 "app-operator/api/v1"
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
)
var (
ElasticWebCommonLabelKey = "app"
)
const (
// APP_NAME deployment 中 App 标签名
APP_NAME = "elastic-app"
// CONTAINER_PORT 容器的端口号
CONTAINER_PORT = 8080
// CPU_REQUEST 单个POD的CPU资源申请
CPU_REQUEST = "100m"
// CPU_LIMIT 单个POD的CPU资源上限
CPU_LIMIT = "100m"
// MEM_REQUEST 单个POD的内存资源申请
MEM_REQUEST = "512Mi"
// MEM_LIMIT 单个POD的内存资源上限
MEM_LIMIT = "512Mi"
)
// 根据总QPS以及单个POD的QPS,计算需要多少个Pod
func getExpectReplicas(elasticWeb *v1.ElasticWeb) int32 {
// 单个pod的QPS
singlePodQPS := *elasticWeb.Spec.SinglePodsQPS
// 期望的总QPS
totalQPS := *elasticWeb.Spec.TotalQPS
// 需要创建的副本数
replicas := totalQPS / singlePodQPS
if totalQPS%singlePodQPS != 0 {
replicas += 1
}
return replicas
}
// CreateServiceIfNotExists 创建service
func CreateServiceIfNotExists(ctx context.Context, r *ElasticWebReconciler, elasticWeb *v1.ElasticWeb, req ctrl.Request) error {
logger := log.FromContext(ctx)
logger.WithValues("func", "createService")
svc := &corev1.Service{}
svc.Name = elasticWeb.Name
svc.Namespace = elasticWeb.Namespace
svc.Spec = corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "goproxy.cn
RUN go mod download
# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go
# Use distroless as minimal base image to package the manager binary
# Refer to github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]
接下来就是登陆docker了,我这边使用的docker hub,直接在命令行登陆即可。
$ docker login
Authenticating with existing credentials...
Login Succeeded
Logging in with your password grants your terminal complete access to your account.
For better security, log in with a limited-privilege personal access token. Learn more at docs.docker.com/go/access-tokens/
登陆成功后,就可以构建镜像了。
注意如果你用的是Mac M1的电脑,那么需要对Makefile做一小点修改,具体可见issues
.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
#KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) --arch=amd64 use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
接下来就是构建并将镜像推送到镜像仓库:
$ make docker-build docker-push IMG=<some-registry>/<project-name>:tag
$ make docker-build docker-push IMG=huiyichanmian/elasitcweb:v0.0.1
等待推送成功后,就可以根据IMG指定的镜像将控制器部署到集群中:
$ make deploy IMG=<some-registry>/<project-name>:tag
$ make deploy IMG=huiyichanmian/elasticweb:v0.0.1
同样,这里可能会遇到镜像gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0这个镜像拉不下来的情况,这里可以使用kubesphere/kube-rbac-proxy:v0.8.0进行替代。
可以直接修改config/default/manager_auth_proxy_patch.yaml或者使用docker tag进行改名。
部署完成后,系统会自动创建项目名- system的命名空间,我们的控制器所有东西都在这个namespace下。
最后如果要从集群中卸载operator也很简单:
$ make undeploy
参考:
xinchen.blog.csdn.net/article/details/113836090

