OwnCloud On Kubernetes On OpenStack

背景

研究kubernetes有一段时间了,k8s作为容器编排领域的标准,相比传统架构,在应用发布,运行,维护上具有颠覆性的变革,这场变革以“云原生”为口号,如火如荼的发展起来。可以想象,在不远的将来,大家的应用都以标准的形式,运行在以k8s为代表容器平台上,k8s让devops真正融合在了一起,尤其对运维,k8s定义了对应用的标准运维方式,平台替人做了很多toil的运维工作,通过各种机制保障应用的可用性,这对运维来说,是最激动人心的。在IaaS平台上构建起来的容器平台,还可以通过API的方式消费IaaS平台的网络和存储等资源,真正将IaaS平台的弹性、灵活性利用起来,容器平台将作为最接近应用的基础平台,是一个公司非常重要的IT基础设施,这一如当年的Linux给业界带来的变革。

K8S相比OpenStack来说,我感觉其复杂度有过之而无不及,毕竟都是基础平台,功能强大,就意味着复杂,光是把里面的概念搞清楚,就需要花费一些时间,为了更深刻的理解这些概念,最好的办法,就是动手做,本篇就以OwnCloud在Kubernetes上的部署为例,掌握下Kubernetes涉及到的一些核心概念,OwnCloud是一个开源的文件共享系统,说通俗点,就是一个“网盘”系统,之所以选择用它做实验,主要有以下几个原因:

  1. 最近想给公司搭建一个内部网盘,让大家可以把一些资料集中到一起,方便存储和共享
  2. OwnCloud是一个典型的LMAP架构的应用,会用到负载均衡,代理,数据库,Web Server等,具有代表性
  3. OwnCloud有一个非常不错的helm chart,可以用来学习
  4. OwnCloud还可以跟S3对接,将数据存储到对象存储中,这个可以用来跑我们的Ceph RGW的业务

此外,本次测试使用的Kubernetes是运行在OpenStack平台上,PaaS和IaaS的结合,看看会擦出怎么样的火花。

环境准备

首先是有一个OpenStack环境,为了更好的跟Kubernetes结合,这个OpenStack平台有以下特点:

  1. OpenStack后端接的Ceph,这是为了让k8s通过storageclass功能,消费OpenStack平台的块存储资源
  2. 部署了负载均衡功能,使用的octavia,模式是active standby模式,这是为了测试k8s的load balancer类型的service
  3. 提供VLAN类型的网络,因为k8s本身的网络就已经有了封装,其底层的网络就尽量让其简单
  4. 还部署了Ceph的RGW功能,后面可以让owncloud和rgw进行对接

然后是Kubernetes环境,有如下特点:

  1. Kubernetes平台是起在OpenStack里的虚拟机
  2. 虚拟机运行在VLAN网络里,共有6台,3个master节点,3个node节点
  3. Kubernetes使用kubespary部署,这是一个部署生产环境使用的ansible项目,功能很强大,几乎支持k8s的各种功能
  4. Kubernetes的网络使用calico的ipip模式,这是kubespray的默认网络模式
  5. Kubernetes的service proxy mode使用ipvs
  6. Kubernetes和OpenStack进行了对接,即k8s平台的cloud provider是openstack,这样k8s就可以消费openstack的api,使用其上的资源了,本次测试主要是使用块存储和负载均衡。
  7. Kubernetes部署了nginx ingress controller和helm,ingress通过load balancer service暴露出去,因为owncloud是使用helm部署的,而且也支持ingress的方式,所以部署上了这两个功能。
  8. 创建了一个叫cinder的storageclass,跟openstack的cinder进行了集成

架构规划

本次测试,目的是尽量让其接近生产环境,owncloud是一个LAMP架构的应用,数据库使用MariaDB,采用主备模式,并且数据库的部署也是通过helm的方式,owncloud(Apache+PHP)则被ingress代理,ingress又通过load balancer类型的service暴露出去,其整体架构如下图:

node-1, node-2, node-3分别是kubernetes平台里的三个worker node,是openstack里的三个虚拟机;LB是load balancer类型的service,会对应的在openstack平台里创建出来octavia的lb;LB通过round robin的方式,通过ingress的service的nodeport,代理ingress实例,ingress是replica为1的deployment;Apache是跑在bitnami提供的owncloud镜像里的,里面包含了apache+php,是我们的app程序,也是一个replica为1的deployment,其又被ingress所代理;最后是数据库,数据库是一个主从架构的集群,使用statefulset运行,数据库又通过默认的ClusterIP类型的service的方式暴露给集群内部使用。

Helm Chart

本次测试使用到两个helm chart,mariadb和owncloud,owncloud里面依赖了mariadb chart,部署owncloud chart时,会自动安装mariadb的chart,为了解耦,我们分开部署,把mariadb当成external db去部署,owncloud里也支持external database。

MariaDB的chart里面构成如下:

  1. 数据库实例使用statefulset部署,主从的replica都为1
  2. 通过service将数据库实例暴露给集群
  3. 数据库配置使用configmap保存
  4. 数据库密码使用secret保存
  5. 数据库使用到的数据目录,通过storageclass和pvc的形式进行申明挂载

OwnCloud的chart构成如下:

  1. owncloud的app实例使用deployment部署,replica为1
  2. owncloud deployment通过service暴露给集群内部
  3. ingress代理了owncloud service
  4. owncloud的密码,使用secret保存
  5. 使用到了两个pvc,一个是apache的数据目录,一个是owncloud的数据目录

综上,以上两个chart,有statefulset, deployment, service, cofigmap, secret, storageclass, pvc, ingress等概念,基本上包含了kubernetes里面最主要的核心概念。具体细节,可以直接看代码,这里就不展开讨论了。

部署测试

因为helm已经做了很好的封装,在前置条件都准备好的前提下,部署其实是非常简单的,我们只需要定制helm使用到的参数即可。本次测试,使用到的参数如下:

部署mariadb,使用到如下参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# cat mariadb-values.yaml
rootUser:
password: password

db:
user: owncloud
password: password
name: owncloud

master:
persistence:
storageClass: cinder
size: 50Gi

slave:
persistence:
storageClass: cinder
size: 50Gi

这里的重点是使用之前已经预定义好的cinder storageclass,为master和slave分配存储资源。

部署owncloud,使用到如下参数:

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
# cat owncloud-values.yaml
ingress:
enabled: true
hosts:
- name: owncloud.ustack.com
tls: false

owncloudHost: owncloud.ustack.com
owncloudUsername: admin
owncloudPassword: admin
owncloudEmail: admin@unitedstack.com

externalDatabase:
host: 10.233.26.90
user: owncloud
password: password
database: owncloud

mariadb:
enabled: false

service:
type: ClusterIP

persistence:
apache:
storageClass: cinder
size: 10Gi
owncloud:
storageClass: cinder
size: 100Gi

这里的重点是开启了ingress,它默认是关闭的,ingress的host写上域名,数据库使用的是external database,填写上mariadb的service clusterip,owncloud的service类型定义成ClusterIP,默认是Load Balancer,为的是让它被ingress代理,最后分配两个存储资源。

部署只需要两条命令就可以搞定:

1
2
helm install -f mariadb-values.yaml stable/mariadb --name mariadb
helm install -f owncloud-values.yaml stable/owncloud --name owncloud

然后可以通过如下命令查看部署状态:

1
2
3
4
5
helm list
helm status mariadb
helm status owncloud
kubectl get pods -w --namespace default -l release=mariadb
kubectl get pods -w --namespace default -l release=owncloud

观察到pod都是running并且ready之后,说明部署完成,在本地为lb的vip做域名解析到owncloud.ustack.com,然后访问 http://owncloud.ustack.com 即可。

遇到问题

在测试的时候,遇到两个问题:

  1. externalDatabase里面的host不能写成service对应的域名,即 mariadb.default.svc.cluster.local,使用域名的话,在owncloud的pod启动时,会卡在数据库初始化上,观察了下,这个初始化,是使用nodejs写的,还不清楚为什么卡住,换成service的clusterip,就好了,理论上,应该填写service对应的域名,会比较好些。
  2. owncloud这里的初始化好像没什么作用,我们已经在externalDatabase里传了数据库用户名和密码等信息,然后owncloud的用户名和密码,把这些当成环境变量传给了容器,但是在访问url时,还是会让你在界面上填写这些参数,而且进pod里看这些配置,都是没有配置上的,得在面板填写,安装之后,才会生成,感觉它这个初始化的脚本有问题。

概念解析

像service, ingress, pv, pvc这些概念相近,很容易让人迷惑,下面结合本次测试,对这些概念做一个梳理,确定下关键点,其次,最让人感兴趣的就是OpenStack里面资源是如何被Kubernetes使用的,这个测试主要用到了两种OpenStack资源,一个是cinder的块存储,一个是octavia的lb。

1. StorageClass, PVC, PV

上面的每一个pvc,都对应的创建了一个pv,每一个pv都对应一个cinder云硬盘,并且被挂载到了pod所在的node上:

1
2
3
4
5
6
7
[root@kube-node-3 ~]# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
vda 253:0 0 80G 0 disk
└─vda1 253:1 0 80G 0 part /
vdb 253:16 0 50G 0 disk /var/lib/kubelet/pods/b994a0b2-5941-11e9-8b54-fa163e23db6f/volumes/kubernetes.io~cinder/pvc-b992cdd5-5941-11e9-8b54-fa163e23db6f
vdc 253:32 0 100G 0 disk /var/lib/kubelet/pods/898d489a-5949-11e9-8b54-fa163e23db6f/volumes/kubernetes.io~cinder/pvc-897de9b3-5949-11e9-8b54-fa163e23db6f
vdd 253:48 0 10G 0 disk /var/lib/kubelet/pods/898d489a-5949-11e9-8b54-fa163e23db6f/volumes/kubernetes.io~cinder/pvc-897cabc0-5949-11e9-8b54-fa163e23db6f
1
2
3
4
5
6
7
8
9
10
11
12
13
[root@kube-master-1 ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-mariadb-master-0 Bound pvc-b98a678c-5941-11e9-8b54-fa163e23db6f 50Gi RWO cinder 5h10m
data-mariadb-slave-0 Bound pvc-b992cdd5-5941-11e9-8b54-fa163e23db6f 50Gi RWO cinder 5h10m
owncloud-owncloud-apache Bound pvc-897cabc0-5949-11e9-8b54-fa163e23db6f 10Gi RWO cinder 4h15m
owncloud-owncloud-owncloud Bound pvc-897de9b3-5949-11e9-8b54-fa163e23db6f 100Gi RWO cinder 4h15m

[root@kube-master-1 ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-897cabc0-5949-11e9-8b54-fa163e23db6f 10Gi RWO Delete Bound default/owncloud-owncloud-apache cinder 4h15m
pvc-897de9b3-5949-11e9-8b54-fa163e23db6f 100Gi RWO Delete Bound default/owncloud-owncloud-owncloud cinder 4h14m
pvc-b98a678c-5941-11e9-8b54-fa163e23db6f 50Gi RWO Delete Bound default/data-mariadb-master-0 cinder 5h10m
pvc-b992cdd5-5941-11e9-8b54-fa163e23db6f 50Gi RWO Delete Bound default/data-mariadb-slave-0 cinder 5h10m

StorageClass是最高度的抽象,它定义了要使用的存储的具体类型,比如是使用块存储,还是nfs,而pvc则是storageclass的消费者,pvc只关心需要多少存储资源,至于这个存储资源,怎么来,是由storageclass来定义的,pv则是具体的创建出来的存储资源了,pv会和对应的pvc进行绑定。

2. Ingress

上面创建的ingress对象,最终表现出来的是对应的在ingress nginx的nginx.conf中添加了配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## start server owncloud.ustack.com
server {
server_name owncloud.ustack.com ;

listen 80;

set $proxy_upstream_name "-";

location / {

set $namespace "default";
set $ingress_name "owncloud.ustack.com-owncloud";
set $service_name "owncloud-owncloud";
set $service_port "80";
set $location_path "/";
...
## end server owncloud.ustack.com

添加完配置之后,nginx会自动通过lua进行重载,使配置生效,可见,ingress的工作其实非常轻量级。Ingress其实就是一个反向代理,代理后端的service,之前一直理解的是每一个ingress都会创建一个nginx实例,其实并不是,而是所有的service都使用同一个nginx,通过host参数,来为每一个service在nginx.conf中添加vhost,然后对其进行代理,这样做的好处显而易见,就是可以通过一个统一的入口,代理后端的多个服务,ingress有点类似于是service的service,明白这一点,是理解ingress的关键。

Ingress的另一个关键点是,ingress其实也是一个集群内对象,默认是不对集群外暴露的,而如果你想将ingress暴露出去,还是要通过service的方式进行,即给ingress之前再加一个service,可以是nodeport,或者是lb。

1
2
3
4
5
6
7
[root@kube-master-1 ~]# kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
owncloud.ustack.com-owncloud owncloud.ustack.com 10.0.127.3 80 4h26m

[root@kube-master-1 ~]# kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx LoadBalancer 10.233.43.201 10.0.127.3 80:30827/TCP,443:31659/TCP 13h

本例使用lb类型的service将ingress暴露到集群外,而lb则是使用的openstack的lb功能。

3. Service

Service这个概念在Kubernetes中,是使用最广泛的一个概念了,从服务的角度来讲,每一个服务都需要对外暴露一个统一的入口,比如本例中的数据库,owncloud等。Service本质上,其实也是一个代理,代理后端的pod,有3种代理模式:userspace, iptables, ipvs,其中ipvs是通过lvs实现的,在大规模场景下,会简化管理,提高性能。在本例中,使用ipvs模式,可以通过如下命令看到ipvs的代理效果:

1
2
3
4
5
6
7
8
[root@kube-master-1 ~]# ipvsadm -ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 172.17.0.1:31659 rr
-> 10.233.71.130:443 Masq 1 0 0
TCP 10.0.127.3:80 rr
-> 10.233.71.130:80 Masq 1 0 0

此外,service对集群外进行暴露,通过有两种方式,一个是nodeport,一个是load balancer,nodeport只在kubernetes集群内部就可以实现,通过iptables+ipvs规则实现,效果就是可以通过任一一个节点的IP地址+自动分配的node port,就可以访问到这个service,如上例中31659就是一个node port,需要注意的一个关键点是,这些规则会在每一个k8s的节点添加,由于节点之间是内网联通的,因此访问任一一个节点的ip地址,都可以访问到这个service。而load balancer类型的service,则需要依赖集群外部的load balancer服务去实现,这种通常是需要有cloud provider的,即依赖于底层IaaS平台的负载均衡服务,为其创建LB,然后通过node port将节点添加到lb的member中,其中的关键点是,lb类型的service,是构建在nodeport基础上的,它是先建立了node port,然后通过node port,将node添加到lb的后端服务中,如本例中,将ingress通过lb service进行暴露的方式,在OpenStack LB服务中,会看到如下的内容:



1
2
3
[root@kube-master-1 ~]# kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx LoadBalancer 10.233.43.201 10.0.127.3 80:30827/TCP,443:31659/TCP 13h

可以看到,ingress nginx controller通过两个listener,将80和443端口暴露出去,对应的有两pool,每个pool中有3个member,分别就是3个work node,而member中,又通过service的node port,将其添加到pool的member中,这样就完美实现了OpenStack为Kubernetes中的Ingress对象提供负载均衡的功能。

此外,还有statefulset, deployment等概念,这里就不再阐述了,比较好理解,官方文档解释的也比较清楚。

总结

本文通过对OwnCloud在Kubernetes上进行部署为例,介绍了Kubernetes中的几个重点概念,同时,也展示了Kubernetes在OpenStack平台上展示出来的威力,真心感觉,构建在IaaS平台之上的PaaS才是未来的主流发展方向,IaaS为PaaS提供了各种资源,而PaaS又像最终的应用提供了编排能力,以及对资源进行了进一步的抽象,IaaS应该由像Kubernetes这样的PaaS平台通过API的方式进行消费,而不是由人直接消费,这才是运维自动化的核心。

附录

  1. openstack cinder storage class
1
2
3
4
5
6
7
8
9
# cat cinder-storage-class.yaml

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: cinder
provisioner: kubernetes.io/cinder
parameters:
availability: nova
  1. openstack loadbalancer service for ingress nginx
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
# cat openstack-lb-ingress.yaml
kind: Service
apiVersion: v1
metadata:
name: ingress-nginx
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
annotations:
service.beta.kubernetes.io/openstack-internal-load-balancer: "true"
spec:
#externalTrafficPolicy: Local
type: LoadBalancer
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
ports:
- name: http
port: 80
targetPort: http
- name: https
port: 443
targetPort: https

---
作者

hackerain

发布于

2019-04-08

更新于

2023-03-11

许可协议