Kubernetes的Service Catalog机制
Service Catalog是K8S提供的一套扩展API,利用它,集群内的应用程序可以轻松的使用外部管理的服务。这种外部服务的典型例子是云服务商提供的中间件即服务。
外部服务由遵循Open service broker API规范的Service Broker管理,SC能通过此API调用SB,在不需要知道这些外部服务如何被创建、管理的细节的前提下,实现:
- 列出外部服务
- Provision服务实例
- Bind服务实例
- 获取访问服务实例需要的凭证信息
Service Catalog基于API聚合来扩展K8S API Server,使用Etcd作为存储。支持Basic、OAuth 2.0 Bearer Token两种身份验证机制。
这套API的目标是,为开发人员、ISV(独立软件提供商)、SaaS提供商设计统一的、简单的、优雅的服务交付机制,以集成云原生平台中运行的应用程序。
现有的实现包括:
- Cloud Foundry:组件 Cloud Controller 负责注册Service Broker,并代表用户与SB交互
- Kubernetes:Service Catalog项目负责将SB集成到K8S
规范全文可以参考:https://github.com/openservicebrokerapi/servicebroker/blob/v2.14/spec.md。
Service Catalog安装API组servicecatalog.k8s.io,并提供以下资源。
资源 | 说明 |
ClusterServiceBroker | Service Broker在集群内部的表示,包含连接到SB所需的全部信息 |
ClusterServiceClass | 某个SB所管理的一种外部服务,当ClusterServiceBroker创建后,Service Catalog控制器会自动从SB获取可用的受管服务列表,并为每个受管服务创建一个ClusterServiceClass对象 |
ClusterServicePlan |
ClusterServiceClass的一种具体的变体,受管服务可以有不同的“计划”,例如免费版、付费版,或者高速版、低速版 类似于ClusterServiceClass,当ClusterServiceBroker创建后,Service Catalog控制器也会自动创建ClusterServicePlan对象 |
ServiceInstance | 一个实际Provision的ClusterServiceClass的实例,此对象创建后,SC控制器会连接到对应的SB,并指示SB提供对应的服务实例 |
ServiceBinding |
包含访问ServiceInstance所需的凭证。如果你的集群内应用需要访问外部服务实例,则需要创建该对象 ServiceBinding创建之后,SC控制器会创建包含目标服务实例的连接信息、访问凭证的Secret。此Secret应当被挂载到需要使用服务的Pod |
Service Catalog支持Kubernetes 1.7以上版本。
本章演示基于Helm的安装方式,你需要添加Chart仓库:
1 |
helm repo add svccat https://svc-catalog-charts.storage.googleapis.com |
搜索以获取最新的Chart版本信息:
1 2 3 |
helm search service-catalog # NAME CHART VERSION APP VERSION DESCRIPTION # svccat/catalog 0.2.1 service-catalog API server and controller-manager helm chart |
安装到K8S:
1 2 3 4 5 6 |
# 这里使用的Chart仓库https://chartmuseum.gmem.cc/public helm repo update helm delete catalog --purge helm fetch gmem/catalog --untar helm install --name catalog --namespace kube-system catalog -f kubeapps/overrides/development.yaml |
除了一些常见的Kubernete资源以外,还会安装一个APIService,这种对象表示一个特定的GroupVersion的中的API Server:
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 |
# kubectl get apiservice v1beta1.servicecatalog.k8s.io -o yaml apiVersion: apiregistration.k8s.io/v1 kind: APIService metadata: name: v1beta1.servicecatalog.k8s.io spec: caBundle: base64-encoded CA certificate of this api server # 能够处理的API组 group: servicecatalog.k8s.io # 这组API的处理优先级,数字越大优先级越高 groupPriorityMinimum: 10000 # 此API Server的服务器信息 service: name: catalog-apiserver namespace: kube-system # 能够处理的API版本 version: v1beta1 # 组内的API版本的优先级 versionPriority: 20 status: conditions: - lastTransitionTime: 2019-06-18T07:18:53Z message: all checks passed reason: Passed status: "True" type: Available |
开发SB,就是要实现Open service broker API规定的接口,结合实际业务场景来动态的提供受管服务。
Go语言下的OSB库包括:
库 | 说明 |
brokerapi | 用于构建SB的库 |
osb-broker-lib | 提供了OSB API的REST实现 |
go-open-service-broker-client | 一个SB客户端库 |
可以从osb-starter-pack这个示例项目开始,构建你的Service Broker。该项目使用了上面的osb-broker-lib、go-open-service-broker-client库。
克隆此项目:
1 2 3 4 |
cd $GOPATH/src mkdir -p github.com/pmorie cd github.com/pmorie git clone git://github.com/pmorie/osb-starter-pack |
如果使用我Fork的版本,调试时可以使用如下应用程序参数: --insecure --port 8001
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 55 56 57 |
func runWithContext(ctx context.Context) error { addr := ":" + strconv.Itoa(options.Port) // 下面的businessLogic是broker.Interface接口的实现 // broker.Interface为SB的核心接口 businessLogic, _ := broker.NewBusinessLogic(options.Options) // 用于暴露Prometheus指标 reg := prom.NewRegistry() osbMetrics := metrics.New() reg.MustRegister(osbMetrics) // 创建一个APISurface,此对象表示OSB REST API的接口 // 它能够将REST请求转换为对broker.Interface的调用 api, err := rest.NewAPISurface(businessLogic, osbMetrics) // 创建HTTP服务器, s := server.New(api, reg) // 如果需要验证K8S令牌 if options.AuthenticateK8SToken { // K8S客户端 k8sClient, err := getKubernetesClient(options.KubeConfig) if err != nil { return err } // 创建用户信息验证器 authz := middleware.SARUserInfoAuthorizer{ SAR: k8sClient.AuthorizationV1().SubjectAccessReviews(), } tr := middleware.TokenReviewMiddleware{ TokenReview: k8sClient.Authentication().TokenReviews(), Authorizer: authz, } // 并作为路由的中间件(请求拦截器) s.Router.Use(tr.Middleware) } glog.Infof("Starting broker!") // 启动服务,如果使用TLS则需要一些额外的处理 if options.Insecure { err = s.Run(ctx, addr) } else { if options.TLSCert != "" && options.TLSKey != "" { // 使用直接提供的证书数据 err = s.RunTLS(ctx, addr, options.TLSCert, options.TLSKey) } else { if options.TLSCertFile == "" || options.TLSKeyFile == "" { return nil } // 使用证书文件 err = s.RunTLSWithTLSFiles(ctx, addr, options.TLSCertFile, options.TLSKeyFile) } } return err } |
可以看到,HTTP/REST相关的技术细节,直接拷贝此示例的代码就可以了。我们需要做的,就是实现broker.Interface接口。
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 |
package broker import ( "net/http" osb "github.com/pmorie/go-open-service-broker-client/v2" ) // 此接口定义了SB需要实现的所有业务逻辑 type Interface interface { // 校验发送给此SB的OSB API版本 ValidateBrokerAPIVersion(version string) error // 获取此SB提供的服务目录,K8S的ServiceCatalog组件第一次和此SB通信时会调用该方法 GetCatalog(c *RequestContext) (*CatalogResponse, error) // 提供一个服务实例 // // 参数说明: // - osb.ProvisionRequest 请求对象,基于HTTP请求封装 // -RequestContext 用于获取原始HTTP请求、响应对象 Provision(request *osb.ProvisionRequest, c *RequestContext) (*ProvisionResponse, error) // 回收(deprovision)一个服务实例 // // 参数说明: // - a osb.DeprovisionRequest 请求对象,基于HTTP请求封装 // -RequestContext 用于获取原始HTTP请求、响应对象 Deprovision(request *osb.DeprovisionRequest, c *RequestContext) (*DeprovisionResponse, error) // 当K8S需要确认一个正在处理的异步操作的状态时,调用此方法 LastOperation(request *osb.LastOperationRequest, c *RequestContext) (*LastOperationResponse, error) // 执行绑定操作,该操作会创建供客户端使用(服务实例)的凭证,并非所有服务支持绑定操作 Bind(request *osb.BindRequest, c *RequestContext) (*BindResponse, error) // 删除绑定 Unbind(request *osb.UnbindRequest, c *RequestContext) (*UnbindResponse, error) // 更新服务实例的配置 Update(request *osb.UpdateInstanceRequest, c *RequestContext) (*UpdateInstanceResponse, error) } type RequestContext struct { // 用于细粒度的控制响应 Writer http.ResponseWriter // 原始的HTTP请求头 Request *http.Request } |
此结构即样例项目osb-starter-pack的 broker.Interface 实现:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
package broker import ( "net/http" "sync" "github.com/golang/glog" "github.com/pmorie/osb-broker-lib/pkg/broker" osb "github.com/pmorie/go-open-service-broker-client/v2" "reflect" ) // 参数是命令行选项 func NewBusinessLogic(o Options) (*BusinessLogic, error) { return &BusinessLogic{ async: o.Async, instances: make(map[string]*exampleInstance, 10), }, nil } type BusinessLogic struct { // 是否应当异步的处理请求 async bool // 互斥锁 sync.RWMutex // 所有已经Provision的实例(请求)列表 instances map[string]*exampleInstance } var _ broker.Interface = &BusinessLogic{} func truePtr() *bool { b := true return &b } func (b *BusinessLogic) GetCatalog(c *broker.RequestContext) (*broker.CatalogResponse, error) { // 这里仅仅简单的返回一个osb.Service对象,Service Catalog根据此对象创建ClusterServiceClass、ClusterServicePlan response := &broker.CatalogResponse{} osbResponse := &osb.CatalogResponse{ Services: []osb.Service{ { Name: "example-starter-pack-service", ID: "4f6e6cf6-ffdd-425f-a2c7-3c9258ad246a", Description: "The example service from the osb starter pack!", Bindable: true, PlanUpdatable: truePtr(), Metadata: map[string]interface{}{ "displayName": "Example starter pack service", "imageUrl": "https://avatars2.githubusercontent.com/u/19862012?s=200&v=4", }, Plans: []osb.Plan{ { Name: "default", ID: "86064792-7ea2-467b-af93-ac9694d96d5b", Description: "The default plan for the starter pack example service", Free: truePtr(), Schemas: &osb.Schemas{ ServiceInstance: &osb.ServiceInstanceSchema{ Create: &osb.InputParametersSchema{ // 前端应该根据类型来展示这些参数,供用户选择 Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "color": map[string]interface{}{ "type": "string", "default": "Clear", "enum": []string{ "Clear", "Beige", "Grey", }, }, }, }, }, }, }, }, }, }, }, } response.CatalogResponse = *osbResponse return response, nil } func (b *BusinessLogic) Provision(request *osb.ProvisionRequest, c *broker.RequestContext) (*broker.ProvisionResponse, error) { // 全局锁 b.Lock() defer b.Unlock() response := broker.ProvisionResponse{} // 将请求转换为代表Instance的结构 exampleInstance := &exampleInstance{ ID: request.InstanceID, ServiceID: request.ServiceID, PlanID: request.PlanID, Params: request.Parameters, } // 检查是否重复请求 if i := b.instances[request.InstanceID]; i != nil { if i.Match(exampleInstance) { // 完全重复请求,提示已经存在 response.Exists = true return &response, nil } else { // InstanceID已经被占用,这是个错误 description := "InstanceID in use" return nil, osb.HTTPStatusCodeError{ StatusCode: http.StatusConflict, Description: &description, } } } b.instances[request.InstanceID] = exampleInstance // 进行实际的Provision操作,创建出外部服务 if request.AcceptsIncomplete { // 可是Service Catalog这是个未完成的异步请求 response.Async = b.async } return &response, nil } func (b *BusinessLogic) Deprovision(request *osb.DeprovisionRequest, c *broker.RequestContext) (*broker.DeprovisionResponse, error) { // 销毁外部服务 b.Lock() defer b.Unlock() response := broker.DeprovisionResponse{} delete(b.instances, request.InstanceID) if request.AcceptsIncomplete { response.Async = b.async } return &response, nil } func (b *BusinessLogic) LastOperation(request *osb.LastOperationRequest, c *broker.RequestContext) (*broker.LastOperationResponse, error) { return nil, nil } func (b *BusinessLogic) Bind(request *osb.BindRequest, c *broker.RequestContext) (*broker.BindResponse, error) { b.Lock() defer b.Unlock() instance, ok := b.instances[request.InstanceID] if !ok { return nil, osb.HTTPStatusCodeError{ StatusCode: http.StatusNotFound, } } response := broker.BindResponse{ BindResponse: osb.BindResponse{ // 返回访问服务实例需要的凭证信息 Credentials: instance.Params, }, } if request.AcceptsIncomplete { response.Async = b.async } return &response, nil } func (b *BusinessLogic) Unbind(request *osb.UnbindRequest, c *broker.RequestContext) (*broker.UnbindResponse, error) { return &broker.UnbindResponse{}, nil } func (b *BusinessLogic) Update(request *osb.UpdateInstanceRequest, c *broker.RequestContext) (*broker.UpdateInstanceResponse, error) { response := broker.UpdateInstanceResponse{} if request.AcceptsIncomplete { response.Async = b.async } return &response, nil } func (b *BusinessLogic) ValidateBrokerAPIVersion(version string) error { return nil } type exampleInstance struct { ID string ServiceID string PlanID string Params map[string]interface{} } func (i *exampleInstance) Match(other *exampleInstance) bool { return reflect.DeepEqual(i, other) } |
可以通过Helm来部署示例项目,以体验如何K8S集群中提供、绑定外部服务。
1 |
helm install --name=broker-skeleton --namespace=kube-system charts/servicebroker |
当你部署了此示例项目,也就是创建了clusterservicebrokers.servicecatalog.k8s.io之后,Service Catalog会自动:
- 调用SB的ValidateBrokerAPIVersion接口确认SB支持API版本
- 调用SB的GetCatalog接口获取服务目录,创建clusterserviceclasses、clusterserviceplans对象
- 根据配置,前两个步骤可能周期性执行
如果一切顺利,应该会创建出ClusterServiceBroker对象broker-skeleton:
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 |
apiVersion: servicecatalog.k8s.io/v1beta1 kind: ClusterServiceBroker metadata: finalizers: - kubernetes-incubator/service-catalog labels: app: broker-skeleton chart: broker-skeleton--0.0.1 heritage: Tiller release: broker-skeleton name: broker-skeleton spec: # SC访问SB时,如何进行身份验证 authInfo: # 支持basic、bearer两种身份验证方式 bearer: secretRef: name: broker-skeleton namespace: kube-system # 是否禁用对SB服务器证书有效性的验证 insecureSkipTLSVerify: true # 如何重新获取SB提供的ServiceClass列表 # Duration 定期 # Manual 仅当此对象的Spec变更才触发一次 relistBehavior: Duration # 用户可以手工增加此字段,来触发relist relistRequests: 0 # ServiceBroker的监听地址 url: http://10.0.0.1:8001 # 访问SB时,校验其服务器端证书的CA证书 caBundle: PEM encoded CA bundle which will be used to validate a Broker's serving certificate status: conditions: - lastTransitionTime: 2019-06-18T12:43:12Z message: Successfully fetched catalog entries from broker. reason: FetchedCatalog status: "True" type: Ready lastCatalogRetrievalTime: 2019-06-18T12:43:12Z reconciledGeneration: 3 |
并且随后生成一个ClusterServiceClass对象:
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 |
apiVersion: servicecatalog.k8s.io/v1beta1 kind: ClusterServiceClass metadata: name: 4f6e6cf6-ffdd-425f-a2c7-3c9258ad246a # 所属的SB ownerReferences: - apiVersion: servicecatalog.k8s.io/v1beta1 blockOwnerDeletion: false controller: true kind: ClusterServiceBroker name: broker-skeleton spec: # 用户是否可以Bind到从此ClusterServiceClass提供的ServiceInstance bindable: true # 不可变的,所属的SB名称 clusterServiceBrokerName: broker-skeleton description: The example service from the osb starter pack! # 不可变的,OSB API使用的ID externalID: 4f6e6cf6-ffdd-425f-a2c7-3c9258ad246a externalMetadata: displayName: Example starter pack service imageUrl: https://avatars2.githubusercontent.com/u/19862012?s=200&v=4 # SB对外暴露此ClusterServiceClass的名称 externalName: example-starter-pack-service # 是否允许在ServiceInstance创建之后,修改ServicePlan planUpdatable: true status: removedFromBrokerCatalog: false |
以及一个ClusterServicePlan对象:
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 |
apiVersion: servicecatalog.k8s.io/v1beta1 kind: ClusterServicePlan metadata: name: 86064792-7ea2-467b-af93-ac9694d96d5b # 所属的SB ownerReferences: - apiVersion: servicecatalog.k8s.io/v1beta1 blockOwnerDeletion: false controller: true kind: ClusterServiceBroker name: broker-skeleton uid: dc7e6ac0-91c5-11e9-b33b-0697b7c57666 spec: # 不可变的,所属的SB名称 clusterServiceBrokerName: broker-skeleton # 所属的ClusterServiceClass clusterServiceClassRef: name: 4f6e6cf6-ffdd-425f-a2c7-3c9258ad246a description: The default plan for the starter pack example service externalID: 86064792-7ea2-467b-af93-ac9694d96d5b externalName: default # 提示此计划是否不需要付费 free: true # *runtime.RawExtension # 创建此SP的实例时,可以提供的参数 instanceCreateParameterSchema: properties: color: default: Clear enum: - Clear - Beige - Grey type: string type: object status: removedFromBrokerCatalog: false |
当你创建一个ServiceInstance之后,Service Catalog会自动:
- 调用SB的Provision接口,进行服务的Provisioning
- 更新ServiceInstance.Status字段
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 55 56 57 58 59 60 61 62 63 64 65 |
# kubectl get serviceinstances.servicecatalog.k8s.io example-instance -o yaml apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceInstance metadata: finalizers: - kubernetes-incubator/service-catalog name: example-instance namespace: default spec: # 所属的ServiceClass和ServicePlan clusterServiceClassExternalName: example-starter-pack-service clusterServiceClassRef: name: 4f6e6cf6-ffdd-425f-a2c7-3c9258ad246a clusterServicePlanExternalName: default clusterServicePlanRef: name: 86064792-7ea2-467b-af93-ac9694d96d5b externalID: 37b83e95-9271-11e9-b33b-0697b7c57666 # 发起Provision请求时提供的参数 parameters: color: Clear updateRequests: 0 # 最后修改此对象的用户的信息 userInfo: groups: - system:serviceaccounts - system:serviceaccounts:kube-system - system:authenticated uid: "" username: system:serviceaccount:kube-system:admin status: # 如果当前正有一个针对此实例的异步操作在进行中,设置为true asyncOpInProgress: false conditions: # 状态转换历史记录 - lastTransitionTime: 2019-06-19T09:18:38Z message: The instance was provisioned successfully reason: ProvisionedSuccessfully status: "True" type: Ready - lastTransitionTime: 2019-06-19T09:05:19Z message: "Communication with the ClusterServiceBroker timed out; operation will be retried ... " reason: ErrorCallingProvision status: "True" type: OrphanMitigation # 当前正在此实例上进行的操作 currentOperation: Provision deprovisionStatus: Required inProgressProperties: clusterServicePlanExternalID: 86064792-7ea2-467b-af93-ac9694d96d5b clusterServicePlanExternalName: default parameterChecksum: 5e69f3b690c6b40999c37fda091459d89d48f6c51ce176a99d7c38010209d140 parameters: color: Clear userInfo: groups: - system:serviceaccounts - system:serviceaccounts:kube-system - system:authenticated uid: "" username: system:serviceaccount:kube-system:admin observedGeneration: 1 operationStartTime: 2019-06-19T09:04:19Z orphanMitigationInProgress: true provisionStatus: "" reconciledGeneration: 0 |
当你创建一个ServiceBinding之后,Service Catalog会自动:
- 调用SB的Bind接口,进行服务绑定操作,获得访问服务实例所需要的凭证等信息
- 将凭证信息保存到ServiceBinding指定的Secrets中
应用程序通过挂载上述Secret即可获取访问服务示例的凭证信息。
当你删除一个ServiceBinding之后,Service Catalog会自动:
- 调用SB的Unbind接口
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 |
# kubectl get servicebindings.servicecatalog.k8s.io example-instance-binding -o yaml apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceBinding metadata: finalizers: - kubernetes-incubator/service-catalog name: example-instance-binding namespace: default spec: externalID: 4023295f-9278-11e9-b33b-0697b7c57666 instanceRef: name: example-instance # 传递给SB的绑定参数 parameters: {} # 当前命名空间中,存放此Binding的凭证信息的保密字典 secretName: example-instance-binding # 最后一次修改此绑定对象的用户信息 userInfo: groups: - system:serviceaccounts - system:serviceaccounts:kube-system - system:authenticated uid: "" username: system:serviceaccount:kube-system:admin status: asyncOpInProgress: false conditions: # 状态转换历史记录 - lastTransitionTime: 2019-06-19T09:54:38Z # 这个是正常状态 message: Injected bind result reason: InjectedBindResult status: "True" type: Ready externalProperties: userInfo: groups: - system:serviceaccounts - system:serviceaccounts:kube-system - system:authenticated uid: "" username: system:serviceaccount:kube-system:admin orphanMitigationInProgress: false reconciledGeneration: 1 unbindStatus: Required |
Leave a Reply