Informer 机制Kubernetes 中使用 http 进行通信,如何不依赖中间件的情况下保证消息的实时性,可靠性和顺序性等呢?答案就是利用了 Informer 机制。Informer 的机制,降低了 Kubernetes 各个组件跟 Etcd 与 Kubernetes API Server 的通信压力。Informer 的实现机制通过一种叫作 ListAndWatch 的方法,把 APIServer 中的 API 对象缓存在了本地,并负责更新和维护这个缓存。ListAndWatch通过 APIServer 的 LIST API“获取”所有最新版本的 API 对象;然后,再通过 WATCH API 来“监听”所有这些 API 对象的变化;注册相应的事件,之后如果监听到的事件变化就会调用事件对应的EventHandler,实现回调Informer 机制架构设计简单介绍一下其中几个组件及其作用: Reflector:用于监控指定的k8s资源,当资源发生变化时,触发相应的变更事件,如Added事件、Updated事件、Deleted事件,并将器资源对象放到本地DeltaFIFO Queue中;DeltaFIFO:DeltaFIFO是一个先进先出的队列,可以保存资源对象的操作类型;Indexer:用来存储资源对象并自带索引功能的本地存储,Reflector从DeltaFIFO中将消费出来的资源对象存储至Indexer开发背景 由于 K8S 内置了许多的 Controller 来对各种各样的资源进行 List & Watch,因此也会产生各种不同的事件(Event),其中部分事件是需要我们作为告警来处理的,比如 ReadinessProbe Failed 这种事件,我们需要在其到达失败阈值之前获得通知并做及时处理。然而事件诞生的机制是随机性的,定时巡检一是有滞后性,二是无疑会增加 api server 的压力。因此,我们同样可以利用 Informer 中的回调机制,当监听的资源对象产生变化时,触发我们预设好的 Handler 函数,然后把消息推送出来开发流程 代码结构同样是先贴出代码结构├── dockerfile ├── go.mod ├── go.sum ├── lib │ └── client.go //用于生成K8S rest client ├── models │ └── webHookSink.go //从启动参数中获取构造请求需要的元素 └── sink │ └── webhook.go //请求的构造以及模版的渲染 ├── handlers //各类处理K8S资源对象的handler │ ├── cmhandler.go │ └── eventHandler.go ├── main.go //构造informer讲一下大致流程:首先main中构建informer,指定监听的资源对象(Events),并加入指定的 Handler。func main() { flag.Var(&sink.ArgSinks, "sink", "external sink(s) that receive events") client := lib.K8sRestClientInPod() factory := informers.NewSharedInformerFactory(client, 0) //eventsInformer := factory.Events().V1().Events() eventGVR := schema.GroupVersionResource{ Group: "", Version: "v1", Resource: "events", } eventsInformer, _ := factory.ForResource(eventGVR) eventsInformer.Informer().AddEventHandler(&handlers.EventHandler{}) factory.Start(wait.NeverStop) go startHTTPServer() select {} }这里的Handler是一个需要实现OnAdd,OnUpdate,OnDelete函数的接口,分别对应了资源的添加、更新、删除。我们可以在 OnAdd() 中实现我们想要的逻辑,比如处理什么事件,不处理什么事件,来抑制某些事件的告警。我们通过flag.Var去接启动项参数,这里我们需要自己实现一个结构体,包括了 Set() 和 String() 方法,其中 String() 方法会对我们传入的启动项参数(本质是一串url字符串)进行解构和提取,获得我们构造推送到机器人所需要的请求信息。type WebHookSink struct { uri *url.URL HeaderMap map[string]string Endpoint string Method string BodyTemplate string } // NewWebHookSink 构造函数 func NewWebHookSink() WebHookSink { return WebHookSink{ HeaderMap: map[string]string{"Content-Type": "application/json"}, Method: http.MethodPost, BodyTemplate: bodyTemplate, Endpoint: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=6400ebf4-4a4a-4b84-8667-933495f48c2f", } } func (w WebHookSink) String() string { return "" } func (w WebHookSink) Set(s string) error { e := os.ExpandEnv(s) uri, err := url.Parse(e) if err != nil { return err } else { w.uri = uri } if len(uri.Host) > 0 { w.Endpoint = uri.String() } else { klog.Errorf("uri host's length is 0 and pls check your uri: %v", uri) } opts := uri.Query() if len(opts["method"]) >= 1 { w.Method = opts["method"][0] } // set header of webHook w.HeaderMap = parseHeaders(opts["header"]) return nil }当触发了 OnAdd Handler ,就会根据已经获取到的元素来构造请求并发送。// Send send msg to generic webHook func Send(event *v1.Event) (err error) { body, err := RenderBodyTemplate(event) fmt.Println(body) if err != nil { klog.Errorf("Failed to RenderBodyTemplate,because of %v", err) return err } bodyBuffer := bytes.NewBuffer([]byte(body)) req, err := http.NewRequest(ArgSinks.Method, ArgSinks.Endpoint, bodyBuffer) // append header to http request if ArgSinks.HeaderMap != nil && len(ArgSinks.HeaderMap) != 0 { for k, v := range ArgSinks.HeaderMap { req.Header.Set(k, v) } } if err != nil { klog.Errorf("Failed to create request,because of %v", err) return err } resp, err := http.DefaultClient.Do(req) klog.Info(resp) if err != nil { klog.Errorf("Failed to send event to sink,because of %v", err) return err } defer resp.Body.Close() if resp != nil && resp.StatusCode != http.StatusOK { body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } err = fmt.Errorf("failed to send msg to sink, because the response code is %d, body is : %v", resp.StatusCode, string(body)) klog.Errorln(err) return err } return nil }其中,因为钉/企微的 Webhook 机器人有固定的消息模版用于展示,我们需要在预先定义好的模版中填充关于事件的一些信息字段,这些字段可以表示成事件对象(结构体)的成员变量名。func RenderBodyTemplate(event *v1.Event) (body string, err error) { var tpl bytes.Buffer tp, err := template.New("body").Parse(ArgSinks.BodyTemplate) if err != nil { klog.Errorf("Failed to parse template,because of %v", err) return "", err } // https://github.com/AliyunContainerService/kube-eventer/issues/165 event.Message = strings.Replace(event.Message, `"`, ``, -1) if err := tp.Execute(&tpl, event); err != nil { klog.Errorf("Failed to renderTemplate,because of %v", err) return "", err } return tpl.String(), nil } 具体的表示方法,可以在template包中可以看到:至此,编码阶段就完成了。部署测试流程因为其不对外对内提供服务,所以只需要部署deployment就行了;又由于其是在体内访问api server,所以需要为其配置对应的权限:每个pod在创建时默认会挂载所处命名空间下的 default ServiceAccount 中的 ca.crt 和 token 文件到容器内的 /var/run/secrets/kubernetes.io/serviceaccount 路径下面,我们需要为其创建 ServiceAccount,CluserRole 以及 ClusterRolebinding,并在 deployment 中指定 serviceaccountapiVersion: apps/v1 kind: Deployment metadata: labels: name: event-informer name: event-informer namespace: ops spec: replicas: 1 selector: matchLabels: app: event-informer template: metadata: labels: app: event-informer annotations: scheduler.alpha.kubernetes.io/critical-pod: '' spec: dnsPolicy: ClusterFirstWithHostNet serviceAccount: event-informer containers: - image: event-informer name: event-informer command: - "/app/httpserver" - --sink=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxx env: # If TZ is assigned, set the TZ value as the time zone - name: TZ value: "Asia/Shanghai" volumeMounts: - name: localtime mountPath: /etc/localtime readOnly: true - name: zoneinfo mountPath: /usr/share/zoneinfo readOnly: true resources: requests: cpu: 100m memory: 100Mi limits: cpu: 500m memory: 250Mi volumes: - name: localtime hostPath: path: /etc/localtime - name: zoneinfo hostPath: path: /usr/share/zoneinfo --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: event-informer rules: - apiGroups: - "" resources: - events verbs: - get - list - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: event-informer roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: event-informer subjects: - kind: ServiceAccount name: event-informer namespace: ops --- apiVersion: v1 kind: ServiceAccount metadata: name: event-informer namespace: ops当pod首次运行时,已经存在的 Events 会各自触发一次 OnAddHandler,我们也可以在钉/企微上观察到对应的事件告警消息:
前言 本文仅简单介绍 kubectl 插件开发模式,不做深入扩展。仅通过开发一个 kubectl pods 命令,来展示开发 kubectl 的方法和技巧首先介绍一个 kubectl 常用的插件库 krew:GitHub - kubernetes-sigs/krew: 📦 Find and install kubectl plugins其本身也是一个 kubectl 插件,我们可以通过 kubectl plugin list 查看所有可执行的插件。krew 是对插件的一种扩展 ,以 kuebctl krew 运行,可以通过这个工具来管理和安装公网上发布的插件。最简单的插件模式要构造一个最简单的插件,只需要编译出一个可执行文件 kubectl-test,并放在机器环境变量$PATH 所在的路径下就完成了。可以通过执行 kubectl test 来测试。命令行 Args 传参需求实际的命令行工具可能需要我们通过 Args 传入各种参数以满足不同的需求。这里介绍一个非常实用的CLI命令行工具库:CobraGitHub - spf13/cobra: A Commander for modern Go CLI interactions事实上,kubectl 也正是基于此库开发的,该工具可以为我们自动生成命令 Use(用法)、Example(实例)以及可以传入对应的执行函数,也可以装 kubectl 本来就带的通配的Args参数合并到我们要构建的插件中。首先我们通过 generic 的方法生成restClient,这里的cfgFlags 里包含了例如 --namespacce 这样的 flag,用于之后我们的 flag 合并(或者叫继承)。func InitClient() *kubernetes.Clientset { cfgFlags = genericclioptions.NewConfigFlags(true) config, err := cfgFlags.ToRawKubeConfigLoader().ClientConfig() if err != nil { log.Fatalln(err) } c, err := kubernetes.NewForConfig(config) if err != nil { log.Fatalln(err) } return c }我们首先构建一个 Cobra Command,定义其用法,参数传递,以及执行的逻辑func RunCmd(run func(cmd *cobra.Command, args []string) error) { cmd := &cobra.Command{ Use: "kubectl pods [flags]", Short: "list pods", Example: "kubectl pods [flags]", SilenceUsage: true, RunE: run, } MergeFlags(cmd) //添加参数 //BoolVar用来支持 是否 cmd.Flags().BoolVar(&ShowLabels, "show-labels", false, "kubectl pods --show-labels") cmd.Flags().StringVar(&Labels, "labels", "", "kubectl pods --labels=\"app=nginx\"") err := cmd.Execute() if err != nil { log.Fatalln(err) } }其中RunE的入参是一个函数,是调用命令执行的内容,我们需要实现其中的逻辑,例如打印 pod 的信息;而执行命令所带的参数可以通过cmd.Flags().GetString("key")获取到。MergeFlags中封装的是——cfgFlags.AddFlags(cmd.Flags())这步会把像 --namespace 这种 flag 都定义进去;此外,我们可以还可以通过调用 cmd.Flags() 下的方法来自定义 flag;如上代码所示,我们完成了 namespace,labels 等 flag 的定义,但具体的实现我们还要在 run 函数中完成。最后,运行 cmd。我们可以通过 restClient 去请求 apiserver 获得 pod 列表,并根据上一步中获得的 flag 去过滤数据;这里再介绍一个用于命令行的库:tablewriterhttps://github.com/olekukonko/tablewriter专门用于构建命令行执行返回结果的制表过程,我们可以使用 README.md 中的 Example 去渲染我们的 pod 列表并输出。最后同样是通过编译放入 $PATH 路径并执行,有如下效果:
开发背景operator模式 in K8SKubernetes 为自动化而生。无需任何修改,你即可以从 Kubernetes 核心中获得许多内置的自动化功能。 你可以使用 Kubernetes 自动化部署和运行工作负载, 甚至 可以自动化 Kubernetes 自身。Kubernetes 的 Operator 模式概念允许你在不修改 Kubernetes 自身代码的情况下, 通过为一个或多个自定义资源关联控制器来扩展集群的能力。 Operator 是 Kubernetes API 的客户端, 充当自定义资源的控制器。operator 的 作用及其构成使用 Operator 可以自动化的事情包括:.按需部署应用.获取/还原应用状态的备份.处理应用代码的升级以及相关改动。例如,数据库 schema 或额外的配置设置.发布一个 service,要求不支持 Kubernetes API 的应用也能发现它模拟整个或部分集群中的故障以测试其稳定性在没有内部成员选举程序的情况下,为分布式应用选择首领角色以上释义来自官方文档;我以组件化的表达方式去描述 operator,它大概由三部分组成,包括了:CRDwebhookcontrollercrd 是指我们定义了什么样的一种资源,定义它需要什么字段(schema),这些字段的值描述了我们对于这个资源的预期(终态);webhook 是指我们在描述这个资源时的校验规则(显式),以及它必须包含什么字段(隐式);controller 里实现了list&watch自定义资源,去不断触发 Reconsile 函数来响应,这个过程叫做调谐循环(Reconsile Loop)。在本文中,我们将使用 kubebuilder 脚手架 撸一个简单的 redisCluster 出来。operator 实现简易流程创建一个 New Project 并创建 go.mod。初始化工程。kubebuilder init --domain xxx.com创建api,其中定义了 CR 的 GVK。kubebuilder create api --group myapp --version v1 --kind Redis 生成了如下的目录结构:├── Dockerfile ├── Makefile ├── PROJECT ├── README.md ├── api ├── bin ├── config ├── controllers ├── go.mod ├── go.sum ├── hack └── main.go 首先到 /api/v1/redis_types.go 文件下对 CRD 的 api 添加字段,也就是我们最后 apply 的 yaml 中包含的字段。// RedisSpec defines the desired state of Redis type RedisSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // Foo is an example field of Redis. Edit redis_types.go to remove/update //Foo string `json:"foo,omitempty"` // +kubebuilder:validation:Maximum:=40000 // +kubebuilder:validation:Minimum:=80 Port int `json:"port,omitempty"` //我们添加了一个Port字段 }添加了一个 Port 字段,最终我们会创建一个 ContainerPort 为其值的 redis pod,其中,我们可以通过添加注释的方式去限制Port字段的范围为 80~40000,还可以通过正则表达式来校验,参考文档:CRD Validation - The Kubebuilder Book在 /config/samples/myapp_v1_redis.yaml 中,我们可以添加刚才的字段以及属性值,以应用测试用。apiVersion: myapp.xxx.com/v1 kind: Redis metadata: name: myredis spec: port: 1011至此,我们完成了对 CRD 的基本定义,然后执行make install安装 crd 到集群里,这里其实是通过 kubectl 和 kustomize 去 apply 的,所以会安装 CRD 到我本机的~/.kube/config中配置的集群里去。接下里就要实现 controller ,包括其中最核心的 Reconcile 调谐函数,目录在 /controller/redis_controller.go。func (r *RedisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) // TODO(user): your logic here redis := &myappv1.Redis{} if err := r.Get(ctx, req.NamespacedName, redis); err != nil { fmt.Println(err) } else { fmt.Println("得到对象", redis) // TODO client-go创建pod } return ctrl.Result{}, nil }以上代码实现了一段最简单的控制器逻辑,当然我们也可以继续扩充,使用 client-go 以及获取到的 redis 对象来创建对应的 redis pod。编写完成后,我们可以通过执行 make run 本地启动 controller,并通过执行 kubectl apply redis.yaml 来测试控制器。当测试完成后,我们需要实际应用 controller 到我们的集群中去时,执行make docker-build docker-push IMG=registryurl推送镜像,然后执行make deploy IMG=registryurl将 controller 部署到集群中去。至此,我们就完成了最简单的 operator 开发。关键功能(持续更新)参考 deployment 和 pod 之前的关系,在实际的应用过程中,我们可能还要实现以下功能:状态显示事件支持watch 子资源集联删除webhook校验yaml字段有效性副本伸缩处理这部分后续将缓慢更新。状态显示指的是 kubectl get 我们的 crd 时,命令行返回结果的字段展示;以及 kubectl describe 对应资源时yaml里面的字段找到 redis CRD 对象的 schema,其位于 /api/v1/redis_types.go// Redis is the Schema for the redis API type Redis struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec RedisSpec `json:"spec,omitempty"` Status RedisStatus `json:"status,omitempty"` }其中已经为我们定义好了 type struct RedisStatus ,这里面的字段都是用于展示资源列表中资源的状态字段,默认为空。我们可以在结构体内添加我们预期的状态字段:// RedisStatus defines the observed state of Redis type RedisStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file RedisNum int `json:"num"` }当 Reconcile 函数中触发了副本的伸缩,num 字段需要更新时,我们可以做如下处理: redis.Status.RedisNum = 预期值 err := r.Status().Update(ctx, redis)至此,我们可以通过 kubectl describe 对应资源获得其中的 num 字段以及值。另外,我们需要将这个字段在 kubectl get 的返回结果也展示出来,通过在 Redis 结构体上添加注释即可://+kubebuilder:printcolumn:name="Num",type=integer,JSONPath=`.status.num` //+kubebuilder:object:root=true //+kubebuilder:subresource:status // Redis is the Schema for the redis API type Redis struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec RedisSpec `json:"spec,omitempty"` Status RedisStatus `json:"status,omitempty"` }具体的添加规则以及范例参考,可以转到:Generating CRDs - The Kubebuilder Book
技术背景什么是admission controller?admission controller是一段代码,它会在请求通过认证和授权之后、对象被持久化之前拦截到达 API 服务器的请求。控制器编译进 kube-apiserver 可执行文件,并且只能由集群管理员配置。这有点类似于插件的概念,官方提供了一系列的插件来帮助我们实现一些api层面的简单处理:使用准入控制器 | Kubernetes我们也可以通过自己编写一段代码二次开发来实现更为高级更为复杂的需求,官方有具体的实例,该实例的用途是only allow pods to pull images from specific registry.即仅允许pod从指定的镜像仓库拉取image,否则apiserver将会拒绝本次请求。应用场景从运维角度,我们也可以借此约束非集群管理员的某些行为来实现安全运维的目的,或者通过admission controller的另外一个用途:修改请求,通过自动为k8s声明式的api自动注入配置规则,为非集群管理员的一些非约性的apply行为解绑。举几个简单场景:1.开发人员可能只有对应namespace下deployment的create,upduate,delete权限,,我们通过自动注入私仓的dockerconfig.json的secret对象作为imagePullSecret,来让开发人员尽可能少关注与应用本身描述无关的,诸如此类的配置选项。2.传统微服务架构依赖注册中心,因此在pod终止时如何从注册中心下线来实现优雅停机成为了问题,为了实现低代码侵入,将这部分问题的解决下沉到运维层面,我们可以使用k8s提供prestop机制来实现pod在收到sigterm信号之前就处理掉这个问题。具体实现接下来我们来看具体实现代码结构├── go.mod ├── go.sum ├── lib │ ├── config.go //用于配置tls证书和密钥 │ ├── convert.go //将具体的error转换成webhook回调返回的对象 │ ├── pods.go //输入请求对象,return一个返回对象,需要我们在其中实现判断逻辑 │ └── schema.go //存放了用于将byte转换成请求对象的反序列化器 ├── main.go //http server并配置tls,反序列化后调用pods.go获得返回对象其中的大部分代码都可以在kubernetes/main.go at release-1.21 · kubernetes/kubernetes · GitHub 中获取到。main.go中httpserver的handler:http.HandleFunc("/pods", func(writer http.ResponseWriter, request *http.Request) { var body []byte if request.Body != nil { if data, err := ioutil.ReadAll(request.Body); err == nil { body = data } } //第二步 reqAdmissionReview := v1.AdmissionReview{} //请求 resAdmissionReview := v1.AdmissionReview{ //响应 ---完整的对象在 https://kubernetes.io/zh-cn/docs/reference/access-authn-authz/extensible-admission-controllers/#response TypeMeta: metav1.TypeMeta{ Kind: "AdmissionReview", APIVersion: "admission.k8s.io/v1", }, } //第三步,把body decode成对象 deserializer := lib.Codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(body, nil, &reqAdmissionReview); err != nil { resAdmissionReview.Response = lib.ToV1AdmissionResponse(err) } else { resAdmissionReview.Response = lib.AdmitPods(reqAdmissionReview) } resAdmissionReview.Response.UID = reqAdmissionReview.Request.UID marshal, _ := json.Marshal(resAdmissionReview) writer.Write(marshal) })签发证书完成编码后,我们需要生成假证书,可以通过cfssl工具生成,参考下面这篇文章:手把手-安装-cfssl - 光速狼 - 博客园 这里说下大概的步骤——1.创建ca配置文件 (ca-config.json)"ca-config.json":可以定义多个 profiles,分别指定不同的过期时间、使用场景等参数;后续在签名证书时使用某个 profile;"signing":表示该证书可用于签名其它证书;生成的 ca.pem 证书中 CA=TRUE{ "signing": { "default": { "expiry": "87600h" }, "profiles": { "server": { "usages": ["signing"], "expiry": "87600h" } } } }2.创建ca证书签名(ca-csr.json){ "CN": "Kubernetes", //SelfSignedCA "key": { "algo": "rsa", "size": 2048 }, "names": [ { "C": "zh", "L": "bj", "O": "bj", "OU": "CA" } ] }3.生成ca证书和私钥cfssl gencert -initca ca-csr.json | cfssljson -bare ca——生成ca私钥 ca-key.pem 和 ca公钥ca.pem4.创建服务端证书签名(server-csr.json) ——这个服务端可以是etcd,docker,apiserver等{ "CN": "admission", //etcd "key": { "algo": "rsa", "size": 2048 }, "names": [ { "C": "zh", "L": "bj", "O": "bj", "OU": "bj" } ] }5.生成服务端证书server.pem和私钥server-key.pem,也就是签发证书cfssl gencert \ -ca=ca.pem \ -ca-key=ca-key.pem \ -config=ca-config.json \ -hostname=myhook.ops.svc \ -profile=server \ server-csr.json | cfssljson -bare server注意hostname与service的fqdn域名相对应。创建对象 1.cat ca.pem | base64获取ca公钥填入admission webhook资源yaml的cabundleapiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: name: myhook webhooks: - clientConfig: caBundle: | LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURoakNDQW02Z0F3SUJBZ0lVZU41ZU5td29t RXZMREFvSUpnYjhTVUZUandFd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1NURUxNQWtHQTFVRUJoTUNl bWd4Q3pBSkJnTlZCQWNUQW1KcU1Rc3dDUVlEVlFRS0V3SmlhakVMTUFrRwpBMVVFQ3hNQ1EwRXhF ekFSQmdOVkJBTVRDa3QxWW1WeWJtVjBaWE13SGhjTk1qSXdPREF5TURJeU56QXdXaGNOCk1qY3dP REF4TURJeU56QXdXakJKTVFzd0NRWURWUVFHRXdKNmFERUxNQWtHQTFVRUJ4TUNZbW94Q3pBSkJn TlYKQkFvVEFtSnFNUXN3Q1FZRFZRUUxFd0pEUVRFVE1CRUdBMVVFQXhNS1MzVmlaWEp1WlhSbGN6 Q0NBU0l3RFFZSgpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFMVk5rY1pUcWpkdHJD Yzh3YkhqMXlVejhucHl2QkVECkJxNmFyZElOU2NBRldyT0wzNVRJVmNOZnFQRWlhbkxhbjJsbkFG dWp1UE9WaFhYOS9CMzhoSkU1QjJBeXhTYXgKYzdxM1lUWnpYS0xsQ2c1UVc0Wlk2SVFVdTdGbHZ2 T0RIRjEyNTl6OEd4dGFsdjQ1Z0pYSDV5Nnp4WlNBMEJxZgpURnR3bHFRb1krMVN3QkhtZ2lBRWpX ekxZV0cydHVRRndkZW9YR2tWd0Y2dkwzM1NoM09yb1ZHQTQ2aVRiMUdKCkgxenBKNWpVYlpZbFAx SFVta0R4dnF1NDJJaGJnN1lPNWIvUktMaWpvVVJza0p2d1dzb3dvVU03dU1GSllrdUsKdFc3MlJ3 S1UwNnoxN20xUmxZZlpzWkt2NU9ONjd3RlR4M0plQUlTcno1OHBKN2cwaUwySTh0MENBd0VBQWFO bQpNR1F3RGdZRFZSMFBBUUgvQkFRREFnRUdNQklHQTFVZEV3RUIvd1FJTUFZQkFmOENBUUl3SFFZ RFZSME9CQllFCkZPWWRwMzJkL09VanM0VkpMMDZyRTJtZlAwZkZNQjhHQTFVZEl3UVlNQmFBRk9Z ZHAzMmQvT1VqczRWSkwwNnIKRTJtZlAwZkZNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFrYXVN T01OQU9DTUN2QmFqR0JhYUhXRFpBekdvTAo5cHJhWWRDWEwvQkw4Z25qczJsZmRjcWZZclpUM3pY Q3IzNXlmUEJSZitNUFRtKzdlQkF5bHlCeWlNK0xPcDRMCkM5MEVWT2NUM0hxK2EvUlBURjJEbmxB emwva1JkMGN3RFM2WTdLUGovQWxlc3FzVUNQVXVLbVlnb3hadmNqa04KU1NYVEs4VWk2Vncyekd2 MzU5bFR0QjA3Y3paZjhYR09xeEZpQi9tUUVERldOOGxxYkF3b2k1NHVZbHlsaXowcApyWFp0cHhy N0tza0dHb08rcTVEdTRwVnZUNlFUakd6NzNlYktacnRieURsbzBnbDdCZmxPTHBEM1d0WWw0b3N4 CmlYZ3NYaVNxdmNUcEkrbGJLNUQ3dlMrS1pybXpmaFozWWRWdW9mR083UCtsMFUwKzZaUytjMi9v Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K service: name: myhook namespace: ops path: /pods failurePolicy: Fail sideEffects: NoneOnDryRun name: myhook.xxx.com admissionReviewVersions: ["v1", "v1beta1"] namespaceSelector: matchExpressions: - key: env operator: In values: ["prod", "pre", "dev", "test"] rules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE"] resources: ["pods"] ——service对应webhook的service,namespaceSelector通过label限定作用域命名空间,rules匹配GVK。2.把上一节生成服务端私钥和证书创建成tls类型的secretkubectl create secret tls myhook --cert=server.pem --key=server-key.pem -n ops3.在我们的webhook代码里添加tls认证tlsConfig := lib.Config{ CertFile: "/etc/webhook/certs/tls.crt", KeyFile: "/etc/webhook/certs/tls.key", } server := http.Server{ Addr: ":443", TLSConfig: lib.ConfigTLS(tlsConfig), } err := server.ListenAndServeTLS("", "") if err != nil { panic(err) }通过交叉编译我们得到myhook可执行文件。CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build4. 创建webhook deployment(仅测试用,需要指定节点)apiVersion: apps/v1 kind: Deployment metadata: name: myhook namespace: ops spec: replicas: 1 selector: matchLabels: app: myhook template: metadata: labels: app: myhook spec: nodeName: k8s1 containers: - name: myhook image: alpine:3.12 imagePullPolicy: IfNotPresent command: ["/app/myhook"] volumeMounts: - name: hooktls mountPath: /etc/webhook/certs readOnly: true - name: app mountPath: /app ports: - containerPort: 443 volumes: - name: app hostPath: path: /root/app - name: hooktls secret: secretName: myhook --- apiVersion: v1 kind: Service metadata: name: myhook namespace: ops labels: app: myhook spec: type: ClusterIP ports: - port: 443 targetPort: 443 selector: app: myhook——把服务端证书和私钥通过secret的方式挂在到容器里,并通过443端口对外提供服务。测试api最后可以通过apply一个新的pod来测试是否通过webhook的验证规则,如果未通过,则不会继续持久化对象。如果通过,则会触发对apiserver的回调。% kubectl apply -f newpod.yaml Error from server: error when creating "newpod.yaml": admission webhook "myhook.xxx.com" denied the request: container's image must be from private hub.
《Kubernetes权威指南第五版 4.5.2节》 --- apiVersion: v1 kind: Pod metadata: name: webapp1 labels: app: webapp1 spec: hostname: webapp-1 subdomain: mysubdomain containers: - name: webapp1 image: kubeguide/tomcat-app:v1 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: mysubdomain spec: selector: app: webapp1 clusterIP: None ports: - port: 8080 在这个yaml文件中创建了一个 pod,并指定了hostname和subdomain 同时创建了一个service, 书中提到 service的名称必须要和subdomain保持一致。 测试发现如果不保持一致 那么在其他Pod的容器中执行 "wget webapp-1.mysubdomain.default.svc.cluster.local:8080"就会报错提示无法解析 "webapp-1.mysubdomain.default.svc.cluster.local",请问这是为什么
有哪些服务只适合上虚拟机,而不是容器?或者说,上虚拟机比上容器更合适?
关于 K8s中的external-traffic-policy是 在下面的这篇文章中 提到了 值为local的情况 "https://blog.csdn.net/agonie201218/article/details/122215040" (https://link.segmentfault.com/?enc=44bz3Eh2oNjwwh%2FbctmvcA%3D%3D.%2BQsF2Bihux0RM7eG0TMRQMwP%2B%2F%2BX0HNHtehlp9nEcwyutq%2F9QIUKeBImpEwdS70dozeykTukoA%2F5JpDldRvY%2BA%3D%3D) 问题: 如果值为local,外部流量到达某一个Node节点, 这个Node节点本身可能并不包含被访问服务的Pod吧?此时因为设置了local,又不让转发,岂不是本地请求就失败了? 备注参考: "https://kubernetes.io/zh-cn/docs/tasks/access-application-clu..." (https://link.segmentfault.com/?enc=6f1x75x3lsb0XDKUDAUMzg%3D%3D.yTFcm8Fj73UarvLiFBkfjKbrqdhHcfW2xiyrxIO7NTfkxgXuBccSIyxoB3Ee1K8%2FXhS7gHIjhi9ai7DH0aB00msqlTGIy7LsRY33GrFPRJe5j5AAyYeTS74WNHErIhOswjcOWZDjOCE9%2B%2BdISoi9SA%3D%3D)
rancher-desktop with k3s 如何做基于hostpath的本地持久化? 我希望用rancher-desktop,搭配k3s,在我的mac上部署一个postgres,并且创建数据库所需的文件我希望持久化到一个我指定的路径"/Users//Workspaces/postgres/pg_data" 但持久化到我的Mac上这个需求始终不能解决,每次rancher-desktop都是自己创建了一个映射到lima虚拟机内的"/var/lib/rancher/k3s/storage/"pv,但是我如果把rancher-desktop with k3s换成Docker-desktop with k8s,就没有问题,可以顺利完成,yaml文件是一样的: # 安装postgres # 定义一个命名空间 apiVersion: v1 kind: Namespace metadata: name: postgresd --- # 定义配置 apiVersion: v1 kind: ConfigMap metadata: name: postgres-config namespace: postgresd data: POSTGRES_DB: postgres MAX_CONNECTIONS: "10000" LOG_MIN_DURATION_STATEMENT: "500ms" --- # 定义存储卷 apiVersion: v1 kind: PersistentVolume metadata: name: postgres-data-pv spec: capacity: storage: 10Gi # 根据实际需求设置存储容量 accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Delete # storageClassName: local-path hostPath: path: "/Users/hulei/Workspaces/postgres/pg_data" # nodeAffinity: # required: # nodeSelectorTerms: # - matchExpressions: # - key: kubernetes.io/hostname # operator: In # values: # - lima-rancher-desktop --- # 定义用户名、密码等敏感信息 apiVersion: v1 kind: Secret metadata: name: postgres-secret namespace: postgresd type: Opaque data: postgres-user: cG9zdGdyZXM= # Base64编码的用户名,这里是"postgres" postgres-password: U2VjdXJlUGFzc3dvcmQ= # Base64编码的密码,这里是"SecurePassword" --- apiVersion: apps/v1 kind: StatefulSet metadata: name: postgres-statefulset namespace: postgresd spec: replicas: 1 selector: matchLabels: app: postgres serviceName: postgres-serive template: metadata: labels: app: postgres spec: containers: - name: postgresd image: postgres:16.2-alpine3.19 ports: - containerPort: 5432 name: postgresd-port env: - name: POSTGRES_USER valueFrom: secretKeyRef: name: postgres-secret key: postgres-user - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: postgres-secret key: postgres-password - name: POSTGRES_DB valueFrom: configMapKeyRef: name: postgres-config key: POSTGRES_DB volumeMounts: - name: postgres-data mountPath: /var/lib/postgresql/data subPath: data volumeClaimTemplates: - metadata: name: postgres-data spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 5Gi # storageClassName: local-path --- # 定义将5432端口映射到kind cluster 5432端口的service apiVersion: v1 kind: Service metadata: namespace: postgresd name: postgres-service spec: selector: app: postgres ports: - port: 5432 targetPort: 5432 type: ClusterIP 我想知道如何在rancher-desktop中实现基于hostpath的本地持久化? 在我的mac上运行上述yaml文件后,"kubectl get pv" 总是得到如下结果: "image.png" (https://wmprod.oss-cn-shanghai.aliyuncs.com/c/user/20241002/505ec739119cb7782cece189834cb696.png) k3s会自己创建一个pv,不会用我创建的pv,这是为什么?
"image.png" (https://wmprod.oss-cn-shanghai.aliyuncs.com/c/user/20240930/de77f8a88435c4d36f460daa28105594.png) Service流量就永远出不了发起访问的客户端的那个节点 这句话对吗? 我的理解:PodA内通过 ClusterIp访问访问服务A,应该会通过PodA所在节点 的kube-proxy的iptables 负载均衡的路由到 服务A的Endpoint吧? 所以PodA 可能会访问到其他节点的PodB提供的服务A,怎么会永远出不了发起访问的客户端的那个节点呢?
gke 没有统一的对外 ip 怎么设置白名单? gke k8s 的每个 node 都有独立的公网 ip,这导致一个问题, 我在 gcp 开了一个虚拟安装了一个 redis 并设置了访问密码,但是为了安全,我还要设置一个 ip 白名单 但是我的后端程序在 gke,gke k8s 的每个 node 都有独立的公网 ip。这导致一个问题,我不知道怎么为 redis 绑定 ip 白名单了。因为我的服务会被 k8s 调度到任何一个 node,每个 node 的公网 ip 又不一样! «为什么不走内网?运维告诉我,谷歌 gke 访问 gcp 只能公网ip 访问,也不知道真的假的?»