使用 Prometheus 监控 Kubernetes 集群

当你考虑基于 Kubernetes 的能力为自己的应用锦上添花的时候,就仿佛打开了一个潘多拉魔盒,你不知道这个盒子里到底有什么,就像你不知道你所依赖的 Kubernetes 集群和集群上的应用正在、将要发生什么。

无论选择什么架构,底层基于什么运行时,可观测性始终拥有极高的优先级。有一种说法是:如果你不知道怎么运维,就不要去尝试部署。这也算是一种非常朴实的,以终为始的思考方式。

话说回来,如果拥抱了 Kubernetes,那么我们所追求的 “可观测性” 是什么样子呢?对于微服务架构,我认为有几个方面可以作为及格线:

  1. 集群和应用状态的可观测性
  2. 集群和应用的日志
  3. 应用间流量、调用关系和请求状态的可观测性

简单来说,就是:监控、日志、跟踪,而 Prometheus 就是在 Kubernetes 监控比较成熟的解决方案。

Prometheus

Prometheus 一款基于时序数据库的开源监控告警系统,诞生自 SoundCloud 公司。Prometheus 的基本工作原理是通过 HTTP 协议,周期性的抓取被监控组件的状态,因此被监控组件只需要实现符合规范的 HTTP 接口便可以接入,对于默认没有提供 HTTP 接口的组件,比如 Linux、MySQL 等,Prometheus 支持使用 exporter 来收集信息,并代为提供 metrics 接口。

SoundCloud 公司博客上有一篇文章简单讲解了 Prometheus 架构以及工作原理。文章中认为 Prometheus 满足的四个特性:

  1. A multi-dimensional data model(多维度数据模型)
  2. Operational simplicity(方便的部署和维护)
  3. Scalable data collection and decentralized architecture(灵活的数据采集)
  4. A powerful query language(强大的查询语言)

第一项和第四项也是时序数据库的特点,但 Prometheus 为了方便部署,默认并没有内置任何额外的存储,而是选择自己实现,对于第四个特性 Prometheus 实现了 PromQL 查询语言,可以实现强大的查询规则。

随着版本的迭代,Prometheus 的特性早已不局限于此。

从 Prometheus 的架构图可以看到,有 4 个主要组件:

  1. Prometheus Server
  2. PushGateway
  3. AlertManager
  4. WebUI

其中只有 Prometheus Server 是最重要的组件,承担着数据的采集,Prometheus 采用 pull 的方式对被监控对象进行数据采集,但是如果需要被监控对象通过 Push 将自身的状态推送给 Prometheus,可以引入 PushGateway,被监控对象将主动的将状态 Push 到 PushGateway 上,而 Prometheus Server 定期去 PushGateway 采集。

AlertManager 和 WebUI 都不是必须组件,前者可以基于采集的数据设置报警,后者可以通过 Web 界面的方式实时展示监控数据。

Prometheus Operator

Prometheus 的部署方式非常多,得益于其简单的工作原理,因此只需要将 Prometheus Server 部署到能访问到被监控对象的环境里即可。

但对于 K8s 来言,因为集群内的网络环境相对封闭、 Pod 的 IP 易变等特点,CoreOS 开源了通过 Operator(CRD) 的方式管理和部署 Prometheus (https://github.com/coreos/prometheus-operator )。

之前介绍 CRD 的文章(如何使用 CRD 拓展 Kubernetes 集群)也讲到,CRD 提供的能力取决于 CRD Controller,Prometheus Operator 便是这么一种 Controller,承担了监听自定义资源的变更,并完成后续的管理工作的职责。

安装 Operator

Prometheus Operator 的安装非常简单,在 Git 仓库的根目录下,直接 kubectl apply 其中的 bundle.yaml(镜像在 quay.io 下,需要科学上网):

git clone https://github.com/coreos/prometheus-operator.git
kubectl apply -f prometheus-operator/bundle.yaml

基本概念

Prometheus Operator 将会托管 Prometheus 的部署、管理工作,而且基于 K8s 中的 CRD,Prometheus Operator 新引入了若干 CR(自定义资源):

  1. Prometheus:描述将要部署的 Prometheus Server 集群
  2. ServiceMonitor/PodMonitor:描述了 Prometheus Server 的 Target 列表
  3. Alertmanager:描述 Alertmanager 集群
  4. PrometheusRule:描述 Prometheus 的告警规则

Prometheus Operator 的设计理念可以参考文档:https://github.com/coreos/prometheus-operator/blob/master/Documentation/design.md

工作原理

Prometheus Operator 通过监听上面的自定义资源(CR)的变动,并执行后续管理逻辑,入下图:

通过创建类型为 Prometheus 的资源(此处的 Prometheus 指的是 Prometheus Operator 定义的自定义资源),该 Prometheus 会通过 label selector 选择相关的 ServiceMonitor,而 ServiceMonitor 通过定义选择 Service 的 label selector 来选定需要监控的 Service ,并通过该 Service 对应的 Endpoints 获得需要监控的 Pod ip 列表。

监控应用 Demo

我们根据官方 User Guides 简单介绍一下如何使用 prometheus-operator 对应用进行监控,更多细节可以参考:https://github.com/coreos/prometheus-operator/blob/master/Documentation/user-guides/getting-started.md

部署被监控对象

通过 Deployment 部署有 3 个副本的应用:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: example-app
  template:
    metadata:
      labels:
        app: example-app
    spec:
      containers:
      - name: example-app
        image: fabxc/instrumented_app
        ports:
        - name: web
          containerPort: 8080

然后创建 Service 使得该服务可以提供稳定访问入口:

kind: Service
apiVersion: v1
metadata:
  name: example-app
  labels:
    app: example-app
spec:
  selector:
    app: example-app
  ports:
  - name: web
    port: 8080

注意 Service 中定义了 app=example-app 的 label,这是 ServiceMonitor 的选择依据。

部署监控

根据 Service 中定义的 Label,我们可以定义 ServiceMonitor

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: example-app
  labels:
    team: frontend
spec:
  selector:
    matchLabels:
      app: example-app
  endpoints:
  - port: web

ServiceMonitor 定义了 team=frontend 的 label,这也是 Prometheus 选择 ServiceMonitor 的依据。因此可以创建 Prometheus

apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: prometheus
spec:
  serviceAccountName: prometheus
  serviceMonitorSelector:
    matchLabels:
      team: frontend
  resources:
    requests:
      memory: 400Mi
  enableAdminAPI: false

这时就发现,已经有 prometheus 实例被启动了:

# kubectl get po
NAME                                   READY   STATUS    RESTARTS   AGE
example-app-66db748757-bfqx4           1/1     Running   0          103m
example-app-66db748757-jqsh5           1/1     Running   0          103m
example-app-66db748757-jtbpc           1/1     Running   0          103m
prometheus-operator-7447bf4dcb-lzbf4   1/1     Running   0          18h
prometheus-prometheus-0                3/3     Running   0          100m

prometheus 本身提供了 WebUI,因此我们可以创建 SVC 是其暴露到集群外部访问(最好不要在公网环境做这样的操作):

apiVersion: v1
kind: Service
metadata:
  name: prometheus
spec:
  type: NodePort
  ports:
  - name: web
    nodePort: 30900
    port: 9090
    protocol: TCP
    targetPort: web
  selector:
    prometheus: prometheus

这时就可以看到集群内,Demo 应用的监控信息:

集群监控

通过这个自定义的 Demo 应该可以了解到,Prometheus 是通过 SVC 发起 HTTP 访问来获取数据,而集群监控,只不过是让 Prometheus 有能力获得 Kubernetes 组件的监控接口。同时,Prometheus 也支持以 DaemonSet 的形式部署 Node exporter,来直接收集集群节点信息。

而 Kubernetes 组件的监控数据的采集形式,则取决于集群的部署方式。如果是二进制文件方式部署,可以直接在 Node 上安装 Prometheus 并采集数据;而如果是容器部署,可以为 Kubernetes 组件创建 SVC,后续操作便和集群应用的监控方式一致了,相关文档可以参考:https://coreos.com/operators/prometheus/docs/latest/user-guides/cluster-monitoring.html

2019/8/4 posted in  Kubernetes

使用 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/8/1 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/7/27 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/6/27 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/5/13 posted in  Kubernetes