Kubernetes初探[1]:部署你的第一个ASP.NET Core应用到k8s集群
转自:https://www.cnblogs.com/rainingnight/p/first-aspnetcore-app-in-k8s.html
kubernetes简介
kubernetes是google基于borg开源的容器编排调度引擎,作为cncf(cloud native computing foundation)最重要的组件之一,它的目标不仅仅是一个编排系统,而是提供一个规范,可以让你来描述集群的架构,定义服务的最终状态,kubernetes可以帮你将系统自动得达到和维持在这个状态。
更直白的说,kubernetes可以让用户通过编写一个yaml或者json格式的配置文件,也可以是通过工具/代码生成或者是直接请求kubernetes api来创建应用,该配置文件中包含了用户想要应用程序保持的状态,不管整个kubernetes集群中的个别主机发生什么问题,都不会影响应用程序的状态,你还可以通过改变该配置文件或请求kubernetes api来改变应用程序的状态。
这意味着开发人员不需要在意节点的数目,也不需要在意从哪里运行容器以及如何与它们交流。开发人员也不需要管理硬件优化,或担心节点关闭(它们将遵循墨菲法则),因为新的节点会添加到kubernetes集群,同时kubernetes会在其他运行的节点中添加容器,kubernetes会发挥最大的作用。
总结:kubernetes是容器控制平台,可以抽象所有的底层基础设施(容器运行用到的基础设施)。
kubernetes——让容器应用进入大规模工业生产。
kubernetes另一个深入人心的特点是:它标准化了云服务提供商。比如,有一个azure、google云平台或其他云服务提供商的专家,他担任了一个搭建在全新的云服务提供商的项目。这可能引起很多后果,比如说:他可能无法在截止期限内完成;公司可能需要招聘更多相关的人员,等等。相对的,kubernetes就没有这个问题。因为不论是哪家云服务提供商,你都可以在上面运行相同的命令,以既定的方式向kubernetes api服务器发送请求,kubernetes会负责抽象,并在不同的云服务商中实现。
对于公司来说,这意味着他们不需要绑定到任何一家云服务商。他们可以计算其他云服务商的开销,然后转移到别家,并依旧保留着原来的专家,原来的人员,他们还可以花更少的钱。
pod
kubernetes有很多技术概念,同时对应很多api对象,最重要的也是最基础的对象就是pod。pod是kubernetes集群中运行部署应用的最小单元,并且是支持多个容器的。
pod的设计理念是支持多个容器在一个pod*享网络地址和文件系统,可以通过进程间通信和文件共享这种简单高效的方式组合完成服务。pod对多容器的支持是kubernetes最基础的设计理念。比如你运行一个操作系统发行版的软件仓库,一个nginx容器用来发布软件,另一个容器专门用来从源仓库做同步,这两个容器的镜像不太可能是一个团队开发的,但是他们一块儿工作才能提供一个微服务;这种情况下,不同的团队各自开发构建自己的容器镜像,在部署的时候组合成一个微服务对外提供服务。不过,在大多数情况下,我们只会在pod中运行一个容器,本文中的例子也是这样的。
pod 的另一个特征是:如果我们希望使用其他 rke
等技术的话,我们可以做到不依赖docker容器。
docker是kubernetes中最常用的容器运行时,但是pod也支持其他容器运行时。
总的来说,pod的主要特征包括:
-
每个pod可以在kubernetes集群内拥有唯一的ip地址;
-
pod可以拥有多个容器。这些容器共享同一个端口空间,所以他们可以通过
localhost
交流(可想而知它们无法使用相同的端口),与其他pod内容器的交流可以通过结合pod的ip来完成; -
一个pod内的容器共享同一个卷、同一个 ip、端口空间、ipc 命名空间。
定义一个pod
如下,我们定义一个最简单的pod:
apiversion: v1 kind: pod # 定义kubernetes资源的类型为pod metadata: name: demo-web # 定义资源的名称 labels: # 为pod贴上标签,后面会介绍其用处 app: demo-web spec: # 定义资源的状态,对于pod来说,最重要属性就是containers containers: # containers一个数组类型,如果你希望部署多个容器,可以添加多项 - name: web # 定义本pod中该容器的名称 image: rainingnight/aspnetcore-web # 定义pod启动的容器镜像地址 ports: - containerport: 80 # 定义容器监听的端口(与dockerfile中的expose类似,只是为了提供文档信息)
然后保存,我这里命名为demo-web-pod.yaml。
现在我们可以在终端中输入以下命令来创建该pod:
kubectl create -f demo-web-pod.yaml # 输出 # pod/demo-web created
可以使用如下命令,来查看kubernetes中的pod列表:
kubectl get pods # 输出 # name ready status restarts age # demo-web 1/1 running 0 65s
如果该pod还处于containercreating
状态的话,你可以在运行命令的时候加入--watch
参数,这样当pod变成运行状态的时候,会自动显示在终端中。
访问应用程序
在上面,我们成功部署了一个asp.net core mvc程序的pod,那么如何访问它呢?如果只是为了调试,我们可以使用转发端口的方式来快速访问:
kubectl port-forward demo-web 8080:80 # 输出 # forwarding from 127.0.0.1:8080 -> 80
然后我们再浏览器中访问:,显示如下:
如上,还展示了pod的主机名和ip,这是因为我在应用中添加了如下代码:
public void onget() { hostname = dns.gethostname(); hostip = dns.gethostentry(hostname).addresslist.firstordefault(x => x.addressfamily == addressfamily.internetwork).tostring(); }
不过,端口转发的方式只能在本机访问,为了从外部访问应用程序,我们需要创建kubernetes中的另外一种资源:service。
service
kubernetes中的service资源可以作为一组提供相同服务的pod的入口,这个资源肩负发现服务和平衡pod之间负荷的重任。
在kubernetes集群中,我们拥有提供不同服务的pod,那么service如何知道该处理哪个pod呢?
这个问题就用标签来解决的,具体分两个步骤:
- 给所有需要service处理的对象pod贴上标签。
- 在service中使用一个选择器(
label selector
),该选择器定义了所有贴有对应的标签的对象pod。
标签
标签提供了一种简单的方法用于管理kubernetes中的资源。它们用一对键值表示,且可以用于所有资源。
其实在上面的pod定义中,我们已经定义了标签:
metadata: name: demo-web labels: app: demo-web
如上,我们为pod附加了标签app:demo-web
,在查看pod的时候,可以使用--show-labels
参数来显示pod对应的标签:
ubectl get pods --show-labels # 输出 # name ready status restarts age labels # demo-web 1/1 running 0 1m52s app=demo-web
可以看到,我们的pod都拥有一个app=demo-web
标签。
定义service
现在,让我们为刚才创建的pod定义一个service:
apiversion: v1 kind: service # 定义kubernetes资源的类型为service metadata: name: demo-web-service # 定义资源的名称 spec: selector: # 指定对应的pod app: demo-web # 指定pod的标签为demo-web ports: - protocol: tcp # 协议类型 port: 80 # 指定service访问的端口 targetport: 80 # 指定service转发请求的端口 nodeport: 30000 type: nodeport # 指定service的类型,在这里使用nodeport来对外访问
如上,我们使用selector
属性来选择相应的标签,并把服务类型(type)设置为nodeport
,type
的取值有以下4种:
-
clusterip:默认值,通过集群的内部ip暴露服务,该模式下,服务只能够在集群内部可以访问。
-
nodeport:通过每个node上的ip和静态端口(nodeport)暴露服务,nodeport服务会路由到clusterip服务,这个clusterip服务会自动创建。
-
loadbalancer:使用云提供商的负载均衡器,可以向外部暴露服务,外部的负载均衡器可以路由到nodeport服务和clusterip服务。
-
externalname:通过返回cname和它的值,可以将服务映射到
externalname
字段的内容(如:foo.bar.example.com)。没有任何类型代理被创建,这只有 kubernetes 1.7 或更高版本的kube-dns才支持。
对于服务类型我们先了解这么多就可以了,后续会再来详细介绍。
然后使用如下命令创建service:
kubectl create -f demo-web-service.yaml # 输出 # service/demo-web-service created
使用如下命令来检查服务的状态:
kubectl get services # 输出 # name type cluster-ip external-ip port(s) age # demo-web-service nodeport 10.105.132.214 <none> 80:30000/tcp 10s
如上,它有一个cluster-ip为10.105.132.214
,因此我们可以在集群内使用10.105.132.214:80
来访问该服务,如果是在集群外部,可以使用nodeip:30000
来访问。
服务发现与负载均衡
在上面我们说到,service肩负着发现服务和平衡pod之间负荷的重任,那它是怎么做的呢?让我们先再添加一个pod:
apiversion: v1 kind: pod metadata: name: demo-web-copy labels: app: demo-web spec: containers: - name: web image: rainingnight/aspnetcore-web ports: - containerport: 80
如上,其定义与之前的pod一样,只是把name
改成了demo-web-copy
,然后创建pod:
kubectl create -f demo-web-copy.pod.yaml # 查看pod kubectl get pods # 输出 # name ready status restarts age # demo-web 1/1 running 0 10m # demo-web-copy 1/1 running 0 29s
现在我们使用nodeip:30000
来访问,打开两个浏览器窗口,多刷新几次,以便让它们分别路由到不同的pod,最终显示如下:
可以看到,我们新创建的pod已经通过servie来接受请求了,不需要修改成任何程序代码,kubernetes就已经帮我们实现了服务发现和负载均衡,是不是非常爽。
deployment
在上面我们手动部署了两个pod,但是这只是单机的玩法,它与直接使用docker容器相比并无太大优势,如果我们需要部署一千个实例,那就是一个痛苦的过程,或者我们又想快速更新和迅速回滚,这根本就是不可能的!
其实在k8s中,我们很少直接使用pod,更多的是使用kubernetes的另外一种资源:deployment。
deployment表示用户对kubernetes集群的一次更新操作。可以是创建一个新的服务或是更新一个新的服务,也可以是滚动升级一个服务。deployment可以帮助每一个应用程序的生命都保持相同的一点:那就是变化。此外,只有挂掉的应用程序才会一尘不变,否则,新的需求会源源不断地涌现,更多代码会被开发出来、打包以及部署,这个过程中的每一步都有可能出错。deployment可以自动化应用程序从一版本升迁到另一版本的过程,并保证服务不间断,如果有意外发生,它可以让我们迅速回滚到前一个版本。
deployment定义
现在,我们使用deployment来部署我们的pod,并实现在部署期间服务不间断服务,可以如下定义:
apiversion: apps/v1 kind: deployment # 定义kubernetes资源的类型为deployment metadata: name: demo-web-deployment # 定义资源的名称 labels: app: demo-web-deployment spec: # 定义资源的状态。 replicas: 2 # 定义我们想运行多少个pod,在这里我们希望运行2个 selector: matchlabels: # 定义该部署匹配哪些pod app: demo-web minreadyseconds: 5 # 可选,指定pod可以变成可用状态的最小秒数,默认是0 strategy: # 指定更新版本时,部署使用的策略 type: rollingupdate # 策略类型,使用rollingupdate可以保证部署期间服务不间断 rollingupdate: maxunavailable: 1 # 部署时最大允许停止的pod数量(与replicas相比) maxsurge: 1 # 部署时最大允许创建的pod数量(与replicas相比) template: # 用来指定pod的模板,与pod的定义类似 metadata: labels: # 根据模板创建的pod会被贴上该标签,与上面的matchlabels对应 app: demo-web spec: containers: - name: web image: rainingnight/aspnetcore-web imagepullpolicy: always # 默认是ifnotpresent,如果设置成always,则每一次部署都会重新拉取容器映像(否则,如果本地存在指定的镜像版本,就不会再去拉取) ports: - containerport: 80
保存为demo-web-deployment.yaml
,然后输入以下命令来创建deployment:
kubectl create -f demo-web-deployment.yaml # 输出 # deployment.apps/demo-web-deployent created
现在我们再来查看以下pod:
kubectl get pods # 输出 # name ready status restarts age # demo-web 1/1 running 0 4h28m # demo-web-copy 1/1 running 0 18m # demo-web-deployment-745f7997c4-d24bb 1/1 running 0 16s # demo-web-deployment-745f7997c4-jk9jn 1/1 running 0 16s
如上,我们有4个运行中的pod,其中前二个是我们手动创建的,其他两个是使用deployment创建的。
我们可以使用kubectl delete pod <pod-name>
删除一个deployment创建的pod,看看结果会怎样:
kubectl delete pod demo-web-deployment-745f7997c4-d24bb # 输出 # pod "demo-web-deployment-745f7997c4-d24bb" deleted
再次查看pod列表:
kubectl get pods # 输出 # name ready status restarts age # demo-web 1/1 running 0 31m # demo-web-copy 1/1 running 0 22m # demo-web-deployment-745f7997c4-jk9jn 1/1 running 0 3m39s # demo-web-deployment-745f7997c4-mrrw6 1/1 running 0 11s
可以看到,又重新创建了一个pod:demo-web-deployment-745f7997c4-mrrw6
,deployment会监控我们的pod数量,保持为我们预期的个数。
零停机时间部署(zero-downtime)
现在我们尝试以下零停机部署,首先修改deployment中的image
为:rainingnight/aspnetcore-web:1.0.0
,然后运行如下命令:
kubectl apply -f demo-web-deployment.yaml --record # 输出 # deployment.apps/demo-web-deployment configured
将kubectl的--record
设置为true
可以在annotation
中记录当前命令创建或者升级了该资源。这在未来会很有用,例如,查看在每个 deployment revision 中执行了哪些命令。
除了修改yaml外,还可以直接运行
kubectl set image deployment demo-web-deployment web=rainingnight/aspnetcore-web:1.0.0 --record
来达到同样的效果。
然后使用如下命令检查服务更新状态:
kubectl rollout status deployment demo-web-deployment # 输出 # waiting for deployment "demo-web-deployment" rollout to finish: 1 old replicas are pending termination... # waiting for deployment "demo-web-deployment" rollout to finish: 1 old replicas are pending termination... # waiting for deployment "demo-web-deployment" rollout to finish: 1 old replicas are pending termination... # waiting for deployment "demo-web-deployment" rollout to finish: 1 old replicas are pending termination... # waiting for deployment "demo-web-deployment" rollout to finish: 1 of 2 updated replicas are available... # waiting for deployment "demo-web-deployment" rollout to finish: 1 of 2 updated replicas are available... # deployment "demo-web-deployment" successfully rolled out
如上可以看到,新版本已经成功上线,并在这个过程中副本被逐个替换,也就意味着应用程序始终在线。
现在我们刷新一下浏览器,可以看到标题已经变成了home page - web-v1
:
回滚到前一个状态
如果突然发现新上线的版本有bug,需要紧急回滚到上一个版本,那对kubernetes来说也是非常简单的。
我们首先运行如下命令来查看历史版本:
kubectl rollout history deployment demo-web-deployment # 输出 # deployment.extensions/demo-web-deployment # revision change-cause # 1 <none> # 2 kubectl apply --filename=demo-web-deployment.yaml --record=true
如上,可以看到有2条历史,那么为什么第1条的change-cause
是<none>
呢,这就是因为我们第二次部署的时候使用了--record=true
参数。现在,我们想回滚到第一个版本,只需运行如下命令:
kubectl rollout undo deployment demo-web-deployment --to-revision=1 # 输出 # deployment.extensions/demo-web-deployment rolled back
再次刷新浏览器,标题又变回了home page - web
。
部署多个应用
现在我们再部署一个asp.net core webapi程序,并在刚才的web应用中调用它,形成一个最简单的微服务模式。
部署webapi
与前面的web应用的部署类似,就不用过多介绍,定义如下yaml:
apiversion: apps/v1 kind: deployment metadata: name: demo-api-deployment labels: app: demo-api-deployment spec: replicas: 2 selector: matchlabels: app: demo-api minreadyseconds: 5 strategy: type: rollingupdate rollingupdate: maxunavailable: 1 maxsurge: 1 template: metadata: labels: app: demo-api spec: containers: - name: api image: rainingnight/aspnetcore-api imagepullpolicy: always ports: - containerport: 80
然后创建deployment:
kubectl create -f demo-api-deployment.yaml
查看部署情况:
kubectl get pods # 输出 # name ready status restarts age # demo-api-deployment-66575d644-9wk7g 1/1 running 0 75s # demo-api-deployment-66575d644-fknpx 1/1 running 0 75s # demo-web-deployment-745f7997c4-h7fr8 1/1 running 0 9m23s # demo-web-deployment-745f7997c4-kvptm 1/1 running 0 9m23s
前面手动创建的2个pod已经被我删除了,因为用deployment创建的pod就好了。
现在我们为api应用也创建一个service,以便在web应用中访问它:
apiversion: v1 kind: service metadata: name: demo-api-service spec: selector: app: demo-api ports: - protocol: tcp port: 80 targetport: 80
因为我们的api应用是不需要在集群外部访问的,因此服务类型(type)不需要设置,使用默认的clusterip
就可以了。
部署servcie:
kubectl create -f demo-api-service.yaml
然后查看servie:
kubectl get services # 输出 # name type cluster-ip external-ip port(s) age # demo-api-service clusterip 10.111.25.49 <none> 80/tcp 26s # demo-web-service nodeport 10.105.132.214 <none> 80:30000/tcp 58m
可以使用浏览器来访问: 来验证一下是否部署成功。
调用服务
那么,还剩下最后一个问题,我们的web应用中如何获取到api应用的访问地址呢?
我们先看一下,在web应用的代码中是怎么调用api的:
// startup.cs public void configureservices(iservicecollection services) { services.addhttpclient("api", _ => _.baseaddress = new uri(configuration["apibaseurl"])); } // fetchdata.cshtml public class fetchdatamodel : pagemodel { private static httpclient _client; public fetchdatamodel(ihttpclientfactory httpclientfactory) { _client = httpclientfactory.createclient("api"); } public ilist<weatherforecast> forecasts { get; set; } public async task ongetasync() { var res = await _client.getstringasync("/api/sampledata/weatherforecasts"); forecasts = jsonconvert.deserializeobject<ilist<weatherforecast>>(res); } }
如上,我们首先注册了一个httpclient
,并从配置文件中读取apibaseurl
做为baseaddress
,然后在fetchdata
页面中使用httpclient
调用api服务。
因为在asp.net core中,默认情况下,环境变量中的配置是会覆盖appsettings.json
中的配置的,因此我们可以使用添加环境变量的方式来配置apibaseurl
。
修改demo-web-deployment.yaml
,添加env
属性,如下:
containers: - name: web image: rainingnight/aspnetcore-web imagepullpolicy: always ports: - containerport: 80 env: - name: apibaseurl value: "http://10.111.25.49"
然后在kubernetes中应用该配置:
kubectl apply -f demo-web-deployment.yaml
等待滚动更新完成,刷新浏览器,点击fetchdata
菜单:
如上,可以看到数据成功返回,但是直接使用集群ip10.111.25.49
,这也有点太低级了,其实我们可以直接使用域名:http://demo-api-service
,修改后如下:
env: - name: apibaseurl value: "http://demo-api-service"
提交到kubernetes,然后刷新浏览器,可以看到完美运行,这是因为coredns(kube-dns)帮我们完成了域名解析。
在kubernetes 1.11中,coredns已经实现了基于dns的服务发现的ga,可作为kube-dns插件的替代品。这意味着coredns将作为各种安装工具未来发布版本中的一个选项来提供。事实上,kubeadm团队选择将其作为kubernetes 1.11的默认选项。
coredns是一个通用的、权威的dns服务器,提供与kubernetes后向兼容但可扩展的集成。它解决了kube-dns所遇到的问题,并提供了许多独特的功能,可以解决各种各样的用例。
dns服务器监控kubernetes创建服务的api, 并为每个服务创建一组dns记录。如果在整个群集中启用了dns, 所有pod都会使用它作为dns服务器。比如我们的demo-api-service
服务,dns服务器会创建一条"my-service.my-ns"也就是10.111.25.49:demo-api-service.default
的dns记录,因为我们的web应用和api应用在同一个命名空间(default)中,所以可以直接使用demo-api-service
来访问。
总结
本文带领大家一步一步部署了一个最简单的asp.net core mvc + webapi的微服务程序,介绍了kubernetes中最基本的三个概念:pod,deployment,service,相信大家对kubernetes也有了一个全面的认识。
虽然kubernetes整个体系是非常复杂的,但是不用担心,一开始我们不用去求甚解,最重要的是先跑起来,后续我会和大家一起逐步深入,由简到繁,愉快的掌握kubernetes。
附本文所用示例代码:https://github.com/rainingnight/aspnetcoredocker。
相关资料: