使用 Kubebuilder 构建 Kubernetes CRD Controller

前一篇文章(《如何使用 CRD 拓展 Kubernetes 集群》)通过一个 Demo 讲解 CRD 是什么,以及可以提供什么能力,本文继续基于这个 Demo(https://github.com/Coderhypo/KubeService ),来讲解一下如何构建一个 CRD Controller。

CRD Controller

对于 CRD(CustomResourceDefinition)本身来说,你把它理解为只是一个 OpenApi 的 Schema 一点也不过分,因为这也是它唯一的能力和作用,而对于广义的说法:“利用 CRD 实现了 xx 功能”,真正承担功能实现的,其实说的是 CRD Controller。

Kubernetes 本身就自带了一堆 Controller,Master 节点上的三大核心组件之一:Controller Manager,其实就是一堆 Controller 的集合。

Controller Manger 里有不少 Controller 和我们要实现的 CRD Controller 本质上做的事其实是一样,就是对特定资源进行管理。

而且 Kubernetes 中不同的 Controller 间的通讯方式也非常有意思,以通过 Deployment 创建 Pod 举例:

用户通过 Kubectl 创建 Deployment,APIServer 会对该请求进行权限、准入认证,然后将 Deployment 的资源存储到 ETCD 中,因为 Kubernetes 通过 ETCD 实现了 List-Watch 机制,因此对 Deployment 相关事件感兴趣的 Deployment Controller 会受到资源的 ADD 事件并处理,即为该 Deployment 创建 RS。

RS 创建请求在被 APIServer 接收之后,RS 的 ADD 事件将被发布,因此 ReplicaSet Controller 将会接收到该事件,并进行后续处理:即创建 Pod。

因此可以看到,得益于 Kubernetes 基于事件的工作方式,创建受 Deployment 管理的 Pod 这个动作,Deployment Controller 和 ReplicaSet Controller 都参与执行,但是这两个 Controller 之间并没有直接通讯。

也正是因为其基于事件的工作方式,我们可以自定义 Controller 来处理感兴趣的事件,包括但不局限于 CR 的创建、修改等。

Kubebuilder 和 Operator-SDK

对于 CRD Controller 的构建,有几个主流的工具,一个是 coreOS 开源的 Operator-SDK(https://github.com/operator-framework/operator-sdk ),另一个是 K8s 兴趣小组维护的 Kubebuilder(https://github.com/kubernetes-sigs/kubebuilder )。

Operator-SDK 是 Operator 框架的一部分,Operator 社区比较成熟而且活跃,甚至还有自己的 Hub(https://operatorhub.io/ ) 来让大家探索、分享有趣的 Operator。

Kubebuilder 与其说是个 SDK 不如说是一个代码生成器,通过符合格式的注释和数据结构来生成一个可运行的 Controller。在使用下来最大的感觉是基础设施还不是很完善,比如文档,在写这个 Demo 很多场景还是靠翻代码才找到怎么解决。

Kubebuilder quick start

可能得益于 Kubebuilder 偏代码生成器,所以使用 Kubebuilder 从零到一创建一个 Controller 是非常简单,想必会有很多文章或 topic 会以 “x 分钟创建 CRD Controller” 为题吧。

官方入门文档可见:https://book.kubebuilder.io/quick-start.html

创建项目

首先通过 Kubebuilder init 命令初始化项目,--domain flag arg 来指定 api group。

kubebuilder init --domain o0w0o.cn --owner "Hypo"

当项目创建好之后,会提醒你是否下载依赖,然后你会发现大半个 Kubernetes 的代码已经在你 GOPATH 里了 ┑( ̄Д  ̄)┍。

创建 Api

当项目创建好就可以创建 api:

kubebuilder create api --group app --version v1 --kind App
kubebuilder create api --group app --version v1 --kind MicroService

创建 api 的同时你会发现 Kubebuilder 会帮你创建一些目录和源代码文件:

  1. pkg.apis 里面包含了资源 AppMicroService 的默认数据结构
  2. pkg.controller 里面 AppMicroService 的两个默认 Controller

resource type

Kubebuilder 已经帮你创建和默认的结构:

// MicroService is the Schema for the microservices API
// +k8s:openapi-gen=true
type MicroService struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   MicroServiceSpec   `json:"spec,omitempty"`
    Status MicroServiceStatus `json:"status,omitempty"`
}

你要做的只是拓展,以 MicroService 举例:

type Canary struct {
    // +kubebuilder:validation:Maximum=100
    // +kubebuilder:validation:Minimum=1
    Weight int `json:"weight"`

    // +optional
    CanaryIngressName string `json:"canaryIngressName,omitempty"`

    // +optional
    Header string `json:"header,omitempty"`

    // +optional
    HeaderValue string `json:"headerValue,omitempty"`

    // +optional
    Cookie string `json:"cookie,omitempty"`
}

type DeployVersion struct {
    Name     string                `json:"name"`
    Template appsv1.DeploymentSpec `json:"template"`

    // +optional
    ServiceName string `json:"serviceName,omitempty"`

    // +optional
    Canary *Canary `json:"canary,omitempty"`
}

type ServiceLoadBalance struct {
    Name string             `json:"name"`
    Spec corev1.ServiceSpec `json:"spec"`
}

type IngressLoadBalance struct {
    Name string                        `json:"name"`
    Spec extensionsv1beta1.IngressSpec `json:"spec"`
}

type LoadBalance struct {
    // +optional
    Service *ServiceLoadBalance `json:"service,omitempty"`
    // +optional
    Ingress *IngressLoadBalance `json:"ingress,omitempty"`
}

// MicroServiceSpec defines the desired state of MicroService
type MicroServiceSpec struct {
    // +optional
    LoadBalance        *LoadBalance    `json:"loadBalance,omitempty"`
    Versions           []DeployVersion `json:"versions"`
    CurrentVersionName string          `json:"currentVersionName"`
}

完整的代码见:https://github.com/Coderhypo/KubeService/blob/master/pkg/apis/app/v1/microservice_types.go

controller 逻辑

如果之前没有了解过 Kubernetes 控制器的代码,可能会对默认生成的 Controller 很奇怪,默认 MicroService Controller 名为 ReconcileMicroService,其只有一个主要方法就是:

func (r *ReconcileMicroService) Reconcile(request reconcile.Request) (reconcile.Result, error)

在使这个 Controller Work 之前,需要 pkg.controller.microservice.micriservice_controller.go 中的 add 方法中注册要关注的事件,当任何感兴趣的事件发生时,Reconcile 便会被调用,这个函数的职责,就像 Deployment Controller 的 syncHandler 一样,当有事件发生时,去比对当前资源的状态和预期的状态是否一致,如果不一致,就去矫正。

比如 MicroService 通过 Deployment 管理版本,Reconcile 就要判断每个版本的 Deployment 是否存在,是否符合预期,不过不存在,就去创建,如果不符合,就去纠正。

具体 ReconcileMicroService 代码可见:https://github.com/Coderhypo/KubeService/blob/master/pkg/controller/microservice/microservice_controller.go

运行

当 CR 的结构已经确定和 Controller 代码完成之后,便可以尝试试运行一下。Kubebuilder 可以通过本地 kubeconfig 配置的集群试运行(对于快速创建一个开发集群推荐 minikube)。

首先记得在 main.go 中的 init 方法中添加 schema:

func init() {
    _ = corev1.AddToScheme(scheme)
    _ = appsv1.AddToScheme(scheme)
    _ = extensionsv1beta1.AddToScheme(scheme)
    _ = apis.AddToScheme(scheme)
    // +kubebuilder:scaffold:scheme
}

然后使 Kubebuilder 重新生成代码:

make

然后将 config/crd 下的 CRD yaml 应用到当前集群:

make install

在本地运行 CRD Controller(直接执行 main 函数也可以):

make run
2019/08/01 00:48 上午 posted in  Kubernetes

如何使用 CRD 拓展 Kubernetes 集群

在 6 月底 KubeCon 回来之后,就打算写几篇关于 CRD 的文章,还在 Twitter 上给人做了些许改进 CRD 相关文档的承诺,零零碎碎的事很多,直到现在才有时间落笔。不过在这一个多月里,我做了一个关于 CRD 的内部分享,两个 CRD Demo,向同事、客户数人解释 CRD 是什么东西,反而让我对这个东西更加的清晰。

我会分两到三篇文章介绍 CRD,这是第一篇,简单聊一下什么是 CRD。

太长不看版

  1. CRD 本身是 Kubernetes 的一种资源,允许我们自己自定义新的资源类型
  2. 除了 CRD 我们还要提供一个 controller 以实现自己的逻辑
  3. CRD 允许我们基于已有的 Kube 资源,拓展集群能力
  4. CRD 可以使我们自己定义一套成体系的规范,自造概念

什么是 CRD

CRD 本身是一种 Kubernetes 内置的资源类型,是 CustomResourceDefinition 的缩写,可以通过 kubectl get 命令查看集群内定义的 CRD 资源。

# kubectl get crd
NAME                         CREATED AT
apps.app.o0w0o.cn            2019-07-25T07:02:47Z
microservices.app.o0w0o.cn   2019-07-25T07:02:47Z

当和人聊 CRD 聊多了的之后,发现大家对 CRD 有一些常见的误区,因此,有些概念需要提前明确:

  1. 在 Kubernetes,所有的东西都叫做资源(Resource),就是 Yaml 里 Kind 那项所描述的
  2. 但除了常见的 Deployment 之类的内置资源之外,Kube 允许用户自定义资源(Custom Resource),也就是 CR
  3. CRD 其实并不是自定义资源,而是我们自定义资源的定义(来描述我们定义的资源是什么样子)

对于一个 CRD 来说,其本质就是一个 Open Api 的 schema,就像 Kuber Blog 的一篇文章(https://kubernetes.io/blog/2019/06/20/crd-structural-schema/ )讲到的,无论 R 还是 CR 都需要 Yaml 来描述,但是,如何确保 Yaml 描述的资源是规范的、合法的,那就是 schema 要做的事情,CRD 就其功能来讲,就是想集群注册一种新资源,并告知 ApiServer,这种资源怎么怎么被合法的定义。

控制器模式

在具体讲 CRD 之前先简单讲解一下控制器模式。对 Kubernetes 有过了解的就知道,我们可以通过创建 Deployment 来管理 Pod,而其实 Deployment 并没有直接创建 Pod,而是 Deployment 管理 RS,而 RS 管理 Pod,这其实就是控制器模式。

控制器模式允许基于已有的资源定义更高阶的控制器,来实现更复杂的能力,当然,具体的细节要更复杂,推荐我司杨老湿的文章 《浅析 Kubernetes 控制器的工作原理》(https://www.yangcs.net/posts/a-deep-dive-into-kubernetes-controllers/

CRD 能做什么

一般情况下,我们利用 CRD 所定义的 CR 就是一个新的控制器,我们可以自定义控制器的逻辑,来做一些 Kubernetes 集群原生不支持的功能。

拿一个具体的例子来讲,我用 Kubebulder 创建了一个简单的 CRD(https://github.com/Coderhypo/KubeService ),尝试在 Kubernetes 集群内置微服务管理。

我创建了两种资源,一个叫 App,负责管理整个应用的生命周期,另一个叫 MicroService,负责管理微服务的生命周期。

具体的逻辑结构可以这样理解:

App 可以直接管理多个 MicroService,并且,每个 MicroService 支持多个版本,得益于控制器模式,MicroService 可以为每个版本创建一个 Deployment,使得可以多个版本同时被部署。

如果只是管理应用的部署未免有些简单,MicroService 支持为每个微服务创建 ServiceIngress,以实现四层负载均衡和七层负载均衡:

并且,如果开启负载均衡的功能,MicroService 将为每个版本都创建一个 Service,因此,一个服务将拥有 n + 1 个 SVC,其中 n 是每个版本都拥有一个,而额外的 1 是微服务创建之后就不会再变动(名字和 clusterIP)的 SVC,而这个 SVC 的 Selector 会始终保持和 CurrentVersion 的 SVC 一致。

换句话讲,有一个稳定的 SVC 可以对其他组件提供当前版本的服务,而其他组件也有途径访问到特定版本的服务。这个 SVC + CurrentVersion 就非常轻松的实现了蓝绿发布的能力。

除了 SVC 外,MicroService 还基于 nginx ingress controller 的能力,实现了灰度发布的功能,同过修改 LoadBalance 里 canary 的配置,可以实现按 比例 / header / cookie 进行灰度发布。

这个例子中,AppMicroService 并没有创造 “新能力”,而只是通过对 Kubernetes 已有资源进行组合,就实现了新功能。

但是除了快速对微服务进行蓝绿、灰度之外,AppMicroService 还有没有新的价值呢,另一个看不到的价值就是管理的标准化,之前对应用下的任何操作都需要被翻译为 “Kube 语言”,即对那个 Deployment 或者 Ingress 管理,现在可以有一个统一的入口规范化管理。

总结

以一个简单的小 Demo 来描述什么是 CRD 很容易以偏概全,就我目前的思考,我认为 CRD 有两个非常重要的能力:

首先是功能上,CRD 使得 Kubernetes 已有的资源和能力变成了乐高积木,我们很轻松就可以利用这些积木拓展 Kubernetes 原生不具备的能力。

其次是产品上,基于 Kubernetes 做的产品无法避免的需要让我们将产品术语向 Kube 术语靠拢,比如一个服务就是一个 Deployment,一个实例就是一个 Pod 之类。但是 CRD 允许我们自己基于产品创建概念(或者说资源),让 Kube 已有的资源为我们的概念服务,这可以使产品更专注与解决的场景,而不是如何思考如何将场景应用到 Kubernetes。

2019/07/27 15:58 下午 posted in  Kubernetes

MongoDB 的 Kubernetes 部署方案

开放黑客松使用的数据库是 MongoDB,在容器化中,数据的安全问题是重中之重。

存储问题

有状态的应用在 Kubernetes 上部署时需要使用 PersistentVolume,但是如果 PV 底层的存储不可靠,即使使用 PV,依然不能保证数据安全。

在一般的使用场景下,应用需要定义一个 PersistentVolumeClaim 来描述需要的存储资源,并在 Pod 中使用该 PersistentVolumeClaim,集群会根据 PersistentVolumeClaim 里的描述,创建或寻找一个 PersistentVolume 与之绑定,因此当 Pod 对容器内的 Volume 进行读写时,数据会被持久化到 PersistentVolume 中。

对于 PersistentVolume 的创建有两种方式,第一种是由集群管理员手动创建若干个 PersistentVolume,当 PersistentVolumeClaim 被创建时,集群会寻找满足要求的 PersistentVolume 并与之绑定。当 PersistentVolumeClaim 被删除时,绑定关系解除,PersistentVolume 被触发回收策略。

第二种方式是创建默认的 StorageClass,并使用支持自动拓展的底层存储,当 PersistentVolumeClaim 被创建时,集群会自动在底层存储中创建一个 PersistentVolume 并与之绑定。当 PersistentVolumeClaim 被删除时,PersistentVolume 也会被自动清理。

但无论这两种哪个方式,都需要考虑底层存储是什么,也就是 PersistentVolume 的数据到底存放在哪里。如果是自建集群,可以考虑 Ceph,GlusterFS。

对于公有云服务,可以考虑服务提供商提供的存储服务,比如在华为云 CCE(托管 Kubernetes 集群)中,可以使用云硬盘来作为底层存储:https://support.huaweicloud.com/usermanual-cce/cce_01_0044.html

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongo-pv-claim
  namespace: ohp
  annotations:
    volume.beta.kubernetes.io/storage-class: sata
    volume.beta.kubernetes.io/storage-provisioner: flexvolume-huawei.com/fuxivol
  labels:
    failure-domain.beta.kubernetes.io/region: cn-north-1
    failure-domain.beta.kubernetes.io/zone: cn-north-1a
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 10Gi

单实例部署

对于没有高可用需求的场景,可以使用单实例部署,即只需要运行一个 MongoDB 实例,并为该实例挂载一个可持久化的存储。

apiVersion: v1
kind: Secret
metadata:
  name: mongo-auth
  namespace: ohp
type: Opaque
data:
  username: cm9vdAo=
  password: cGFzc3dvcmQK
---
apiVersion: v1
kind: Service
metadata:
  name: mongo
  namespace: ohp
spec:
  ports:
  - port: 27017
  selector:
    app: mongo
  clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mongo
  namespace: ohp
spec:
  selector:
    matchLabels:
      app: mongo
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mongo
    spec:
      containers:
      - image: mongo
        name: mongo
        env:
        - name: MONGO_INITDB_ROOT_USERNAME
          valueFrom:
            secretKeyRef:
              name: mongo-auth
              key: username
        - name: MONGO_INITDB_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mongo-auth
              key: password
        ports:
        - containerPort: 27017
          name: mongo
        volumeMounts:
        - name: db-persistent-storage
          mountPath: /data/db
      volumes:
      - name: db-persistent-storage
        persistentVolumeClaim:
          claimName: mongo-pv-claim

其中有两点要注意:

  1. Mongo 的认证用户名和密码配置在 Secret
  2. SVC 指定了 ClusterIP 为 None,意味着 Service 将直接解析为 PodIP
  3. Deployment 的发布策略为重建,因为 PV 只能挂载到一个 Pod 中,因此应该避免同时出现多个 Pod(该 Deployment 也不能扩容)

高可用集群

Mongo 做 HA 的方案有很多种,Kubernetes Blog 里有一篇讲如何使用 GCE 搭建 HA mongoDB,用到的是副本集的高可用方案,副本集的方案在 Kubernetes 中应是最简单的,只需要定义一个 StatefulSet 就可以解决。

通过 StatefulSet 维持多个 Pod,每个 Pod 都拥有一个 PV 进行持久化存储:

详细可以参考:Running MongoDB on Kubernetes with StatefulSets

2019/06/27 10:41 上午 posted in  Kubernetes

使用 kustomize 管理 Kubernetes 应用

随着 Kubernetes 1.14 的发布,大家发现原来只是 Kube 兴趣小组的 Yaml 管理工具 kustomize 被集成到 kubectl 中,从此大家可以利用 kubectl apply -k 将指定目录的 kustomization.yaml 应用到集群中。

什么是 kustomize

用工具肯定先搞清楚该工具的定位,kustomize(Github链接)在代码仓库的描述为:

Customization of kubernetes YAML configurations

kustomize 明显就是解决 kubernetes yaml 应用管理的问题的,然而对于 9102 年的现在,提到 Kube Yaml 的管理,肯定会想到 Helm,kustomize 是怎么解决管理问题的,其又和 helm 有什么不同,我们应该怎么取舍,都是下面我们要讨论的问题。

工作原理

kustomize 将对 K8s 应用定义为由 Yaml 表示的资源描述,对 K8s 应用的变更可以映射到对 Yaml 的变更上。而这些变更操作可以利用 git 等版本控制程序来管理,因此用户得以使用 git 风格的流程对 K8s 应用进行管理。

对于一个受 kustomize 管理的 App,都有若干个 Yaml 组成。Github 中描述了一个 Demo 目录结构如下:

~/someApp
├── deployment.yaml
├── kustomization.yaml
└── service.yaml

其中包含用来描述元数据的 kustomization.yaml,以及两个资源文件,其内容如下图所示:

该目录下的 Yaml 是不完整的,可以被补充、覆盖,因此该 Yaml 被称为 Base,而补充 Base 的 Yaml 被称作 Overlay,可以补充适用不同环境、场景的 overlay yaml 到该 app 中,从而使得 K8s 应用描述更完整,其目录结构为:

~/someApp
├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
    ├── development
    │   ├── cpu_count.yaml
    │   ├── kustomization.yaml
    │   └── replica_count.yaml
    └── production
        ├── cpu_count.yaml
        ├── kustomization.yaml
        └── replica_count.yaml

kustomize 通过描述文件的叠加,来生成完整的部署用 Yaml,可以直接使用 build 命令可以生成并部署:

kustomize build ~/someApp/overlays/development | kubectl apply -f -

在 Kubernetes 1.14 之后,也支持直接 apply 该应用。

kubectl apply -k ~/someApp/overlays/development

工作流

在 Kubernetes 应用管理系统中,应用的描述文件(Yaml)是一个非常核心的组成部分,用户通过描述文件来向集群声明自己应用的资源和服务编排要求,kustomize 也是社区对描述文件管理的一个重要的尝试(下图来自:从Kubernetes 1.14 发布,看技术社区演进方向)。

对于 kustomize,用户可以使用 Git 对 Kubernetes 应用进行管理,通过 fork 现有 App,拓展 Base 或者定制 overlay,基本流程如下:

在官方 Github 仓库中,有一篇完整的工作流文档可以参考:https://github.com/kubernetes-sigs/kustomize/blob/master/docs/zh/workflows.md

和 Helm 比较

通过对 kustomize 工作原理的了解,可以看到其和 Helm 的差别最直观的在于 Yaml 渲染上。Helm 通过编写 Yaml 模板,在部署时进行渲染,而 kustomize 是 overlay 叠加的方式,制定不同 patch,在部署时选择使用。

但更重要的,是 Helm 和 kustomize 致力解决的问题不同,Helm 更努力做一个自称体系的生态圈,可以方便管理应用的制品(镜像 + 配置),而对于一个已经发布的制品来说,Chart 相对固定、稳定,是一个静态的管理,更适合对外交付。而 kustomize 则不然,kustomize 管理的是正在变更的应用,可以随时 fork 出一个新版本,也可以创建新的 overlay 将应用推向新的环境,是一个动态的管理,而这种动态,非常适合集成到 DevOps 的流程中。

安装

kustomize 的安装非常简单,因为其本身只是一个 cli,只有一个二进制文件,可以在 release 页面下载:https://github.com/kubernetes-sigs/kustomize/releases

使用 kustomize 管理 K8s 应用

我们从零开始创建一个 Web 应用,并通过 kustomize 区分开发、测试、生产环境进行不同的配置并部署。完整的 Demo 可以见 Github:https://github.com/Coderhypo/kustomize-demo

首先在应用根目录中创建 base 文件夹,作为 base yaml 管理目录,在其中编写 kustomization.yaml:

commonLabels:
  app: dao-2048
resources:
  - deployment.yml
  - service.yml

对于该 Yaml 怎么编写,可以查看官方的实例:https://github.com/kubernetes-sigs/kustomize/blob/master/docs/zh/kustomization.yaml

然后基于 2048 镜像生成简单的 Deployment 和 Service Yaml 文件:

# kubectl run dao-2048 --image=daocloud.io/daocloud/dao-2048 --dry-run -o yaml > deployment.yml

# kubectl expose deployment.apps/dao-2048 --port=80 -o yaml --dry-run > service.yml

这样我们就获得了一个最基础的应用 Yaml,然后在应用根目录中创建 overlays 文件夹,以备添加 overlay,下面我们设计三个场景:

  1. 开发环境:需要使用 nodeport 将服务暴露,在容器中添加 DEBUG=1 的环境变量
  2. 测试环境:需要使用 nodeport 将服务暴露,在容器中添加 TEST=1 的环境变量,配置 CPU 和 Mem 的资源限制
  3. 生产环境:需要使用 nodeport 将服务暴露,在容器中添加 PROD=1 的环境变量,创建名为 pord 的 configmap,作为环境变量挂入到容器,并配置 CPU 和 Mem 的资源限制

开发环境

在 overlays 目录中创建 dev 目录,然后添加 kustomization.yaml:

bases:
- ../../base
patchesStrategicMerge:
- nodeport_patch.yaml
- env_patch.yaml
namespace: 2048-dev

在 nodeport_patch.yaml 中对 Service 进行修改:

apiVersion: v1
kind: Service
metadata:
  name: dao-2048
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  type: NodePort

在 env_patch.yaml 中对 Deployment 中的容器进行修改:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dao-2048
spec:
  strategy: {}
  template:
    spec:
      containers:
      - name: dao-2048
        env:
          - name: DEBUG
            value: "1"

对开发环境进行部署:

# kustomize build . | kubectl apply -f -
service/dao-2048 created
deployment.apps/dao-2048 created
# kubectl -n 2048-dev get all
NAME                            READY   STATUS             RESTARTS   AGE
pod/dao-2048-79b9555477-rjh8n   1/1     Running   0          12s

NAME               TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/dao-2048   NodePort   10.104.217.152   <none>        80:31983/TCP   12m

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/dao-2048   1/1     1            1           12s

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/dao-2048-79b9555477   1         1         1       12s

测试环境

测试环境可以基于开发环境进一步修改,我们可以直接 copy 开发环境的配置:

cp -r dev test

然后修改 env_patch.yaml,将 DEBUG 改为,TEST,并在 kustomization.yaml 中添加新的 yaml:

bases:
- ../../base
patchesStrategicMerge:
- nodeport_patch.yaml
- env_patch.yaml
- resource_patch.yaml
namespace: 2048-test

在 resource_patch.yaml 添加资源限制:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dao-2048
spec:
  strategy: {}
  template:
    spec:
      containers:
      - name: dao-2048
        resources:
            limits:
              cpu: 100m
              memory: 100Mi
            requests:
              cpu: 100m
              memory: 100Mi

在测试环境中部署:

# kustomize build . | kubectl apply -f -
service/dao-2048 created
deployment.apps/dao-2048 created
# kubectl -n 2048-test get all
NAME                            READY   STATUS    RESTARTS   AGE
pod/dao-2048-68d94b7d58-chkt5   1/1     Running   0          36s

NAME               TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/dao-2048   NodePort   10.106.215.65   <none>        80:30154/TCP   36s

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/dao-2048   1/1     1            1           36s

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/dao-2048-68d94b7d58   1         1         1       36s

生产环境

生产环境可以基于测试环境继续修改,同上复制,然后编辑 kustomization.yaml 文件:

bases:
- ../../base
patchesStrategicMerge:
- nodeport_patch.yaml
- env_patch.yaml
- resource_patch.yaml
resources:
- config.yaml
namespace: 2048-prod

和之前不同的是,configmap 在 base 中并不存在,因此需要作为新资源添加,在 config.yaml 编写 configmap 的描述:

apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: pord
data:
  prodconfig: someconfig

在 env_patch.yaml 中添加对 configmap 的使用:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dao-2048
spec:
  strategy: {}
  template:
    spec:
      containers:
      - name: dao-2048
        env:
          - name: PROD
            value: "1"
          - name: SOMECONFIG
            valueFrom:
              configMapKeyRef:
                name: pord
                key: prodconfig

部署生产环境应用:

# kustomize build . | kubectl apply -f -
configmap/pord created
service/dao-2048 created
deployment.apps/dao-2048 created
# kubectl -n 2048-prod get all
NAME                            READY   STATUS    RESTARTS   AGE
pod/dao-2048-577584dc8c-9drrx   1/1     Running   0          31s

NAME               TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
service/dao-2048   NodePort   10.102.62.5   <none>        80:31885/TCP   31s

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/dao-2048   1/1     1            1           31s

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/dao-2048-577584dc8c   1         1         1       31s
2019/05/13 19:42 下午 posted in  Kubernetes

在华为云使用 Kubeadm 搭建单 Master 集群

在优麒麟的黑客松活动中,使用了华为云的 ECS 作为实验环境载体。在前期测试中,华为云自己提供的 1.9 版本的 Kubernetes 服务,不知道是因为版本过低还是 Api Server 参数配置的缘故,无法使 Pod 内获得完全的 root 权限,导致优麒麟的镜像无法正常启动,因此采取了自建 Kube 集群的方式。

目前自建的是 1.13.2 版本的单 Master 集群,采用的工具是 Kubeadm,流程也可以参考官方文档:https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/

准备工作

创建华为云 ECS 实例若干,确保所有机器网络互通,关闭 SELinux、swap。

在所有的机器中安装依赖:

Docker

curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

kubeadm 和 kubelet

首先添加国内源(注意这个 apt key 需要科学上网才能拿到)

apt-get update && apt-get install -y apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
cat <<EOF > /etc/apt/sources.list.d/kubernetes.list
deb http://mirrors.ustc.edu.cn/kubernetes/apt kubernetes-xenial main
EOF
apt-get update

apt-get install kubelet=1.13.2-00 kubeadm=1.13.2-00 -y

查看 Kube 组件依赖的镜像

# kubeadm config images list
k8s.gcr.io/kube-apiserver:v1.13.2
k8s.gcr.io/kube-controller-manager:v1.13.2
k8s.gcr.io/kube-scheduler:v1.13.2
k8s.gcr.io/kube-proxy:v1.13.2
k8s.gcr.io/pause:3.1
k8s.gcr.io/etcd:3.2.24
k8s.gcr.io/coredns:1.2.6

k8s.gcr.io 在国内是无法访问的,可以曲线救国 pull 好并缓存在机器上。

CoreDNS 的镜像可以在 DockerHub 中拉取:

docker pull coredns/coredns:1.2.6
docker tag coredns/coredns:1.2.6 k8s.gcr.io/coredns:1.2.6

剩余的镜像在 DaoCloud 可以拉取到:

# docker images | grep gcr-mirror
daocloud.io/gcr-mirror/kube-controller-manager-amd64   v1.13.2             b9027a78d94c        3 months ago        146MB
daocloud.io/gcr-mirror/kube-proxy-amd64                v1.13.2             01cfa56edcfc        3 months ago        80.3MB
daocloud.io/gcr-mirror/kube-apiserver-amd64            v1.13.2             177db4b8e93a        3 months ago        181MB
daocloud.io/gcr-mirror/kube-scheduler-amd64            v1.13.2             3193be46e0b3        3 months ago        79.6MB
daocloud.io/gcr-mirror/etcd-amd64                      3.2.24              3cab8e1b9802        7 months ago        220MB
daocloud.io/gcr-mirror/kubernetes-dashboard-amd64      v1.8.3              0c60bcf89900        14 months ago       102MB
daocloud.io/gcr-mirror/pause-amd64                     3.1                 da86e6ba6ca1        16 months ago       742kB

修改 kubelet 的配置

首先是确定 docker 的 Cgroup Driver,如果不是 systemd,则需要修改 kubelet 的配置。

# docker info | grep -i cgroup
Cgroup Driver: cgroupfs

第二是因为优麒麟镜像需要特权执行,因此需要添加允许 Pod 使用特权的 flag 参数。综上,修改 kubelet 的配置文件:

# cat /etc/default/kubelet
KUBELET_EXTRA_ARGS="--cgroup-driver=cgroupfs --allow-privileged=true"

启动和配置集群

创建 master 节点(xxx.xxx.xxx.xxx,为该节点外网 ip,以支持外网访问):

kubeadm init  --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address=0.0.0.0 --apiserver-cert-extra-sans=xxx.xxx.xxx.xxx

init 完成后可以根据指示配置 kubeconfig,然后就可以在当前机器使用 kbuectl 控制集群了。

因为 init 时在证书里包含了外网地址,将 kubeconfig 下载到本地,并修改其中的 api server 地址为外网 ip 可以在本地访问该集群。

配置网络插件,因为目前没有需求使用网络隔离,因此使用了坑少的 flannel:

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

配置 Node 节点,可以用个 kubeadm 来生成 bootstrap token 以接入 node:

# kubeadm token create --print-join-command
kubeadm join 192.168.0.165:6443 --token eu6d1j.zax8lj1sfhy***** --discovery-token-ca-cert-hash sha256:6d29f0a87379711da7012c7b3f1e50af4eb32186e92a4e2fd89c58e30bd*****

直接在 node 执行稍等片刻便可接入。

踩坑

华为云的 DNS 配置有些奇怪,/etc/resolv.conf 中的 DNS 为 127.0.0.1,因此导致集群中的 CoreDNS 异常:CoreDNS 将主机中 /etc/resolv.conf 的 DNS 作为上游服务器,如果是 127.0.0.1,发生了循环。

[FATAL] plugin/loop: Seen "HINFO IN 8205794187887631643.5216586587165434789." more than twice, loop detected

讨论:https://github.com/coredns/coredns/issues/1986

集群中的 Pod 默认使用 CoreDNS 作为 DNS 服务器,因此现象是网络连接不正常(打不开网页,但是直接访问 ip 是正常的)。目前解决方式是在 CoreDNS 的 ConfigMap 中手动指定 114.114.114.114 作为上游服务器(也可以直接修改 /etc/resolv.conf)。

其他基础设施

2019/04/26 16:54 下午 posted in  Kubernetes