快速入门 Kubernetes&CRD&Operator
Kubernetes
是一个容器管理系统。
具体功能:
- 基于容器的应用部署、维护和滚动升级
- 负载均衡和服务发现
- 跨机器和跨地区的集群调度
- 自动伸缩
- 无状态服务和有状态服务
- 广泛的
Volume
支持 - 插件机制保证扩展性
通过阅读Kubernetes指南和Kubernetes HandBook以及官方文档 或者 阅读 Kubernetes权威指南可以获得更好的学习体验。
在开始安装 Kubernetes
之前,我们需要知道:
1、Docker与Kubernetes
Docker
是一个容器运行时的实现,Kubernetes
依赖于某种容器运行时的实现。
2、Pod
Kubernetes
中最基本的调度单位是 Pod
,Pod
从属于 Node
(物理机或虚拟机),Pod
中可以运行多个 Docker
容器,会共享 PID、IPC、Network
和 UTS namespace
。Pod
在创建时会被分配一个 IP
地址,Pod
间的容器可以互相通信。
3、Yaml
Kubernetes
中有着很多概念,它们都算做是一种对象,如 Pod、Deployment、Service
等,都可以通过一个 yaml
文件来进行描述,并可以对这些对象进行 CRUD
操作(对应 REST
中的各种 HTTP
方法)。
下面一个 Pod
的 yaml
文件示例:
apiVersion: v1
kind: Pod
metadata:
name: my-nginx-app
labels:
app: my-nginx-app
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
- kind:对象的类别
- metadata:元数据,如
Pod
的名称,以及标签Label
【用于识别一系列关联的Pod
,可以使用Label Selector
来选择一组相同label
的对象】 - spec:希望
Pod
能达到的状态,在此体现了Kubernetes
的声明式的思想,我们只需要定义出期望达到的状态,而不需要关心如何达到这个状态,这部分工作由Kubernetes
来完成。这里我们定义了Pod中运行的容器列表,包括一个nginx
容器,该容器对外暴露了80
端口。
4、Node
Node
是 Pod
真正运行的主机,可以是物理机,也可以是虚拟机。为了管理 Pod
,每个 Node
节点上至少要运行 container runtime、kubelet
和 kube-proxy
服务。
5、Deployment
Deployment
用于管理一个无状态应用,对应一个 Pod
的集群,每个 Pod
的地位是对等的,对 Deployment
来说只是用于维护一定数量的 Pod
,这些 Pod
有着相同的 Pod
模板。
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx-app
spec:
replicas: 3
selector:
matchLabels:
app: my-nginx-app
template:
metadata:
labels:
app: my-nginx-app
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
可以对 Deployment
进行部署、升级、扩缩容等操作。
6、Service
Service
用于将一组 Pod
暴露为一个服务。
在 kubernetes
中,Pod
的 IP
地址会随着 Pod
的重启而变化,并不建议直接拿 Pod
的 IP
来交互。那如何来访问这些 Pod
提供的服务呢?使用 Service
。Service
为一组 Pod
(通过 labels
来选择)提供一个统一的入口,并为它们提供负载均衡和自动服务发现。
apiVersion: v1
kind: Service
metadata:
name: my-nginx-app
labels:
name: my-nginx-app
spec:
type: NodePort #这里代表是NodePort类型的
ports:
- port: 80 # 这里的端口和clusterIP(10.97.114.36)对应,即10.97.114.36:80,供内部访问。
targetPort: 80 # 端口一定要和container暴露出来的端口对应
protocol: TCP
nodePort: 32143 # 每个Node会开启,此端口供外部调用。
selector:
app: my-nginx-app
7、Kubernetes组件
- etcd 保存了整个集群的状态;
- apiserver 提供了资源操作的唯一入口,并提供认证、授权、访问控制、API 注册和发现等机制;
- controller manager 负责维护集群的状态,比如故障检测、自动扩展、滚动更新等;
- scheduler 负责资源的调度,按照预定的调度策略将 Pod 调度到相应的机器上;
- kubelet 负责维护容器的生命周期,同时也负责 Volume(CVI)和网络(CNI)的管理;
- Container runtime 负责镜像管理以及 Pod 和容器的真正运行(CRI);
- kube-proxy 负责为 Service 提供 cluster 内部的服务发现和负载均衡
安装 Kubernetes【Minikube】
minikube
为开发或者测试在本地启动一个节点的 kubernetes
集群,minikube
打包了和配置一个 linux
虚拟机、docker
与kubernetes
组件。
Kubernetes
集群是由 Master
和 Node
组成的,Master
用于进行集群管理,Node
用于运行 Pod
等 workload
。而minikube
是一个 Kubernetes
集群的最小集。
1、安装virtualbox virtualbox
2、安装minikube
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/
3、启用dashboard(web console)【可选】
minikube addons enable dashboard
4、启动minikube
minikube start
start
之后可以通过 minikube status
来查看状态,如果 minikube
和 cluster
都是 Running
,则说明启动成功。
5、查看启动状态
kubectl get pods
kubectl体验【以一个Deployment为例】
kubectl
是一个命令行工具,用于向 API Server
发送指令。我们以部署、升级、扩缩容一个 Deployment
、发布一个 Service
为例体验一下 Kubernetes
。
命令的通常格式为:
kubectl $operation $object_type(单数or复数) $object_name other params
- operation 如 get,replace,create,expose,delete 等。
- object_type 是操作的对象类型,如 pods,deployments,services
- object_name 是对象的 name
kubectl命令表:
Kubernetes kubectl create 命令详解
1、创建一个Deployment
可以使用 kubectl run
来运行,也可以基于现有的 yaml
文件来 create
。
kubectl run –image=nginx:1.7.9 nginx-app –port=80
或者
kubectl create -f my-nginx-deployment.yaml
# my-nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx-app
spec:
replicas: 3
selector:
matchLabels:
app: my-nginx-app
template:
metadata:
labels:
app: my-nginx-app
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
然后可以通过 kubectl get pods
来查看创建好了的 3
个Pod
。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-nginx-app-6f647db65c-9w8kx 0/1 ContainerCreating 0 8s
my-nginx-app-6f647db65c-dmhrx 0/1 ContainerCreating 0 8s
my-nginx-app-6f647db65c-rbp9s 0/1 ContainerCreating 0 8s
再通过 kubectl get deployments
来查看创建好了的 deployment
。
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
my-nginx-app 3/3 3 3 51s
这里有4列,分别是:
- DESIRED:Pod副本数量的期望值,即Deployment里面定义的replicas
- CURRENT:当前Replicas的值
- UP-TO-DATE:最新版本的Pod的副本梳理,用于指示在滚动升级的过程中,有多少个Pod副本已经成功升级
- AVAILABLE:集群中当前存活的Pod数量
2、删除掉任意一个Pod
Deployment
自身拥有副本保持机制,会始终将其所管理的 Pod
数量保持为 spec
中定义的 replicas
数量。
# 打开一个新终端
$ kubectl get pods -w -l app=my-nginx-app
# 删除指定pod
$ kubectl delete pods $pod_name
可以看出被删掉的 Pod
的关闭与代替它的 Pod
的启动过程。
3、缩扩容 缩扩容有两种实现方式:
- 一种是修改
yaml
文件,将replicas
修改为新的值,然后kubectl replace -f my-nginx-deployment.yaml
; - 另一种是使用
scale
命令:kubectl scale deployment my-nginx-app --replicas=2
4、更新
更新也是有两种实现方式,Kubernetes
的升级可以实现无缝升级,即不需要进行停机。
- 一种是 rolling-update 方式,重建 Pod,
edit/replace/set image
等均可以实现。比如说我们可以修改yaml
文件 - 另一种是
patch
方式,patch
不会去重建 Pod,Pod 的IP
可以保持。
#方法一
kubectl replace -f my-nginx-deployment.yaml
#方法二
kubectl set image $resource_type/$resource_name $container_name=nginx:1.9.1
# 查看
kubectl get pods -o yaml
5、暴露服务
暴露服务也有两种实现方式,一种是通过 kubectl create -f my-nginx-service.yaml
可以创建一个服务:
# my-nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-nginx-app
labels:
name: my-nginx-app
spec:
type: NodePort #这里代表是NodePort类型的
ports:
- port: 80 # 这里的端口和clusterIP,供内部访问。
targetPort: 80 # 端口一定要和 pod container 暴露出来的端口对应(上面配置的端口)
protocol: TCP
nodePort: 32143 # 每个 Node 会开启,此端口供外部调用。(每个Pod对外暴露的端口)
selector:
app: my-nginx-app
ports
中有三个端口,第一个 port
是 Pod
供内部访问暴露的端口,第二个 targetPort
是 Pod
的 Container
中配置的
另一种是通过 kubectl expose
命令实现。
minikube ip
返回的就是 minikube
所管理的 Kubernetes
集群所在的虚拟机 ip
。
minikube service my-nginx-app --url -p zzz-cluster
http://192.168.39.121:32143
CRD【CustomResourceDefinition】
CRD
是 Kubernetes
为提高可扩展性,让开发者去自定义资源(如 Deployment,StatefulSet
等)的一种方法。
Operator = CRD + Controller
CRD
仅仅是资源的定义,而 Controller
可以去监听 CRD
的 CRUD
事件来添加自定义业务逻辑。
关于CRD有一些链接先贴出来: Extend the Kubernetes API with CustomResourceDefinitions
如果说只是对 CRD
实例进行 CRUD
的话,不需要 Controller
也是可以实现的,只是只有数据,没有针对数据的操作。
一个 CRD
的 yaml
文件示例:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
# name must match the spec fields below, and be in the form: <plural>.<group>
name: crontabs.stable.example.com
spec:
# group name to use for REST API: /apis/<group>/<version>
group: stable.example.com
# list of versions supported by this CustomResourceDefinition
version: v1beta1
# either Namespaced or Cluster
scope: Namespaced
names:
# plural name to be used in the URL: /apis/<group>/<version>/<plural>
plural: crontabs
# singular name to be used as an alias on the CLI and for display
singular: crontab
# kind is normally the CamelCased singular type. Your resource manifests use this.
kind: CronTab
# shortNames allow shorter string to match your resource on the CLI
shortNames:
- ct
通过 kubectl create -f crd.yaml
可以创建一个 CRD
。
Operator
我们平时在部署一个简单的 Webserver 到 Kubernetes 集群中的时候,都需要先编写一个 Deployment 的控制器,然后创建一个 Service 对象,通过 Pod 的 label 标签进行关联,最后通过 Ingress 或者 type=NodePort 类型的 Service 来暴露服务,每次都需要这样操作,是不是略显麻烦,我们就可以创建一个自定义的资源对象,通过我们的 CRD 来描述我们要部署的应用信息,比如镜像、服务端口、环境变量等等,然后创建我们的自定义类型的资源对象的时候,通过控制器去创建对应的 Deployment 和 Service,是不是就方便很多了,相当于我们用一个资源清单去描述了 Deployment 和 Service 要做的两件事情。
Operator-SDK
Operator 是由 CoreOS 开发的,用来扩展 Kubernetes API,特定的应用程序控制器,它用来创建、配置和管理复杂的有状态应用,如数据库、缓存和监控系统。Operator 基于 Kubernetes 的资源和控制器概念之上构建,但同时又包含了应用程序特定的领域知识。创建Operator 的关键是CRD(自定义资源)的设计。
Workflow
Operator SDK
提供以下工作流来开发一个新的 Operator
:
- 使用 SDK 创建一个新的 Operator 项目
- 通过添加自定义资源(CRD)定义新的资源 API
- 指定使用 SDK API 来 watch 的资源
- 定义 Operator 的协调(reconcile)逻辑
- 使用 Operator SDK 构建并生成 Operator 部署清单文件
创建新项目
$ operator-sdk new cassandra-operator --repo=github.com/zealzhangz/cassandra-operator
$ tree
.
├── build
│ ├── Dockerfile
│ └── bin
│ ├── entrypoint
│ └── user_setup
├── cassandra-operator.iml
├── cmd
│ └── manager
│ └── main.go
├── deploy
│ ├── operator.yaml
│ ├── role.yaml
│ ├── role_binding.yaml
│ └── service_account.yaml
├── go.mod
├── go.sum
├── pkg
│ ├── apis
│ │ └── apis.go
│ └── controller
│ └── controller.go
├── tools.go
└── version
└── version.go
添加 API
接下来为我们的自定义资源添加一个新的 API
,按照上面我们预定义的资源清单文件,在 Operator
相关根目录下面执行如下命令:
# 等个半分钟就创建好了,大概新增了10个文件左右
$ operator-sdk add api --api-version=cassandra.zhangaoo.com/v1alpha1 --kind=CassandraService
添加控制器
上面我们添加自定义的 API
,接下来可以添加对应的自定义 API
的具体实现 Controller
,同样在项目根目录下面执行如下命令
$ operator-sdk add controller --api-version=cassandra.zhangaoo.com/v1alpha1 --kind=CassandraService
自定义 API
打开源文件 pkg/apis/cassandra/v1alpha1/cassandraservice_types.go
,需要我们根据我们的需求去自定义结构体 CassandraServiceSpec
,我们最上面预定义的资源清单中就有 size、image、ports
这些属性,所有我们需要用到的属性都需要在这个结构体中进行定义
type CassandraServiceSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
// Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
Size *int32 `json:"size"`
Image string `json:"image"`
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
Envs []corev1.EnvVar `json:"envs,omitempty"`
Ports []corev1.ServicePort `json:"ports,omitempty"`
}
type CassandraServiceStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
// Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
appsv1.StatefulSetStatus `json:",inline"`
}
定义完成后,在项目根目录下面执行如下命令:
$ operator-sdk generate k8s
实现业务逻辑
上面 API
描述声明完成了,接下来就需要我们来进行具体的业务逻辑实现了,编写具体的 controller
实现,打开源文件 pkg/controller/cassandraservice/cassandraservice_controller.go
,需要我们去更改的地方也不是很多,核心的就是 Reconcile
方法,该方法就是去不断的 watch 资源的状态,然后根据状态的不同去实现各种操作逻辑
// Reconcile reads that state of the cluster for a CassandraService object and makes changes based on the state read
// and what is in the CassandraService.Spec
// TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates
// a Pod as an example
// Note:
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
func (r *ReconcileCassandraService) Reconcile(request reconcile.Request) (reconcile.Result, error) {
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
reqLogger.Info("Reconciling CassandraService")
// Fetch the CassandraService instance
instance := &cassandrav1alpha1.CassandraService{}
err := r.client.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
// Request object not found, could have been deleted after reconcile request.
// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
// Return and don't requeue
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
//Service
// Check if this Service already exists
serviceFound := &corev1.Service{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, serviceFound)
if err != nil && errors.IsNotFound(err) {
reqLogger.Info("Creating a new Service", "Service.Namespace", instance.Namespace, "Service.Name", instance.Name)
// Define a new Service object
service := r.newServiceForCr(instance)
// Set CassandraService instance as the owner and controller
if err := controllerutil.SetControllerReference(instance, service, r.scheme); err != nil {
return reconcile.Result{}, err
}
err = r.client.Create(context.TODO(), service)
if err != nil {
return reconcile.Result{}, err
}
// Service created successfully - don't requeue
//return reconcile.Result{}, nil //todo
} else if err != nil {
return reconcile.Result{}, err
}
// Service already exists - don't requeue
reqLogger.Info("Skip reconcile: Service already exists", "Service.Namespace", instance.Namespace, "Service.Name", instance.Name)
//StatefulSet
// Check if this Service already exists
statefulSetFound := &appsv1.StatefulSet{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, statefulSetFound)
if err != nil && errors.IsNotFound(err) {
reqLogger.Info("Creating a new StatefulSet", "StatefulSet.Namespace", instance.Namespace, "StatefulSet.Name", instance.Name)
// Define a new StatefulSet object
statefulSet := r.newStatefulSetForCr(instance)
// Set CassandraService instance as the owner and controller
if err := controllerutil.SetControllerReference(instance, statefulSet, r.scheme); err != nil {
return reconcile.Result{}, err
}
err = r.client.Create(context.TODO(), statefulSet)
if err != nil {
return reconcile.Result{}, err
}
// StatefulSet created successfully - don't requeue
return reconcile.Result{}, nil
} else if err != nil {
return reconcile.Result{}, err
}
// Ensure the statefulset size is the same as the spec
reqLogger.Info("Matching size in spec")
size := instance.Spec.Size
if *statefulSetFound.Spec.Replicas != *size {
statefulSetFound.Spec.Replicas = size
err = r.client.Update(context.TODO(),statefulSetFound)
if err != nil{
reqLogger.Info("Failed to update StatefulSet: %v\n", err)
return reconcile.Result{}, err
}
reqLogger.Info("Spec was updated, so request is getting re-queued")
// Spec updated - return and requeue
return reconcile.Result{Requeue: true}, nil
}
// StatefulSet already exists - don't requeue
reqLogger.Info("Skip reconcile: CanssandraService already exists", "CanssandraService.Namespace", instance.Namespace, "CanssandraService.Name", instance.Name)
return reconcile.Result{}, nil
}
调试
$ operator-sdk up local
apiVersion: cassandra.zhangaoo.com/v1alpha1
kind: CassandraService
metadata:
name: cassandra
labels:
app: cassandra
spec:
# Add fields here
size: 3
image: gcr.io/google-samples/cassandra:v13
ports:
- containerPort: 7000
name: intra-node
- containerPort: 7001
name: tls-intra-node
- containerPort: 7199
name: jmx
- containerPort: 9042
name: cql
- port: 9042
resources:
limits:
cpu: "500m"
memory: 1Gi
requests:
cpu: "500m"
memory: 1Gi
env:
- name: MAX_HEAP_SIZE
value: 512M
- name: HEAP_NEWSIZE
value: 100M
- name: CASSANDRA_SEEDS
value: "cassandra-0.cassandra.default.svc.cluster.local"
- name: CASSANDRA_CLUSTER_NAME
value: "K8Demo"
- name: CASSANDRA_DC
value: "DC1-K8Demo"
- name: CASSANDRA_RACK
value: "Rack1-K8Demo"
- name: POD_IP
valueFrom:
ieldRef:
fieldPath: status.podIP
参考资料
本文由 zealzhangz 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为:
2020/01/06 20:54