8.2 与Kubernetes API服务器交互
我们已经知道,Downward API提供了一种简单的方式,将pod和容器的元数据传递给在它们内部运行的进程。但这种方式其实仅仅可以暴露一个pod自身的元数据,而且只可以暴露部分元数据。某些情况下,我们的应用需要知道其他pod的信息,甚至是集群中其他资源的信息。这种情况下Downward API方式将无能为力。
正如书中提到的,可以通过服务相关的环境变量或者DNS来获取服务和pod的信息,但如果应用需要获取其他资源的信息或者获取最新的信息,就需要直接与API服务器进行交互(如图8.4所示)。
在了解pod中的应用如何与Kubernetes API服务器交互之前,先在自己的本机上研究一下服务器的REST endpoit,这样我们可以大致了解什么是API服务器。
8.2.1 探究Kubernetes REST API
我们已经了解了Kubernetes不同的资源类型。但如果打算开发一个可以与Kubernetes API交互的应用,要首先了解API。
先尝试直接访问API服务器,可以通过运行 kubectl cluster-info
命令来得到服务器的URL。
$ kubectl cluster-info
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
Unable to connect to the server: dial tcp 172.17.0.2:8443: connect: no route to host
因为服务器使用HTTPS协议并且需要授权,所以与服务器交互并不是一件简单的事情,可以尝试通过curl来访问它,使用curl的 --insecure
(或-k)选项来跳过服务器证书检查环节,但这也不能让我们走得更远。
$ curl https://172.17.0.2:8443 -k
幸运的是,我们可以执行 kubectl proxy
命令,通过代理与服务器交互,而不是自行来处理验证过程。
通过Kubectl proxy访问API服务器
kubectl proxy
命令启动了一个代理服务来接收来自你本机的HTTP连接并转发至API服务器,同时处理身份认证,所以不需要每次请求都上传认证凭证。它也可以确保我们直接与真实的API服务器交互,而不是一个中间人(通过每次验证服务器证书的方式)。
运行代理很简单,所要做的就是运行以下命令:
$ kubectl proxy --address='0.0.0.0' --port=8001 --accept-hosts='.*'
我们也无须传递其他任何参数,因为kubectl已经知晓所需的所有参数(API服务器 URL、认证凭证等)。一旦启动,代理服务器就将在本地端口8001接收连接请求,让我们看一下它是如何工作的:
$ curl localhost:8001
看,我们发送请求给代理,代理接着发送请求给API服务器,然后代理将返回从服务器返回的所有信息,现在让我们开始研究。
通过Kubectl proxy研究Kubernetes API
我们可以继续使用curl,或者打开浏览器并且指向 http://localhost:8001
,看一下当我们访问这个基础的URL时,API服务器会返回什么。服务器的应答是一组路径的清单,如下所示。
代码清单8.7 API服务器的REST endpoint清单:http://localhost:8001
{
"paths": [
"/.well-known/openid-configuration",
"/api",
"/api/v1",
"/apis",
----------
"/apis/batch", # batchAPI组以及它的两个版本
"/apis/batch/v1",
"/apis/batch/v1beta1",
]
}
这些路径对应了我们创建Pod、Service这些资源时定义的API组和版本信息。
你或许已经发现在 /apis/batch/v1
路径下的 batch/V1
就是在第4章了解的Job资源API组和版本信息。同样, /api/V1
对应apiVersion:这里所说的V1指的是我们创建的基础资源(Pod、Service、ReplicationController等)。在Kubernetes最早期版本中提到的最基础的资源并不属于任何指定的组,原因是Kubernetes初期并没有使用API组的概念,这个概念是后期引入的。
注意 这些没有列入API组的初始资源类型现在一般被认为归属于核心的API组。
研究批量API组的REST endpoint
让我们来研究Job资源API,从路径 /apis/batch
下的内容开始(暂时忽略版本),如下面的代码清单所示。
代码清单8.8 在 /apis/batch
目录下的endpoint清单: curl http://localhost:8001/apis/batch
这个响应消息展示了包括可用版本、客户推荐使用版本在内的批量API组信息。让我们接着看一下 /apis/batch/V1
路径下的内容,如下面的代码清单所示。
代码清单8.9 在 batch/V1
中的资源类型: http://localhost:8001/apis/batch/v1
{
"kind": "APIResourceList",
"apiVersion": "v1",
"groupVersion": "batch/v1", #资源请单
"resources": [ #所有的资源类型
{
"name": "cronjobs",
"singularName": "",
"namespaced": true, #指定namesapce的job资源,
"kind": "CronJob",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"cj"
],
"categories": [
"all"
],
"storageVersionHash": "h/JlFAZkyyY="
}
]
}
像我们看到的一样,API服务器返回了在 batch/V1
目录下API组中的资源类型以及 REST endpoint
清单。除了资源的名称和相关的类型,API服务器也包含了一些其他信息,比如资源是否被指定了命名空间、名称简写(如果有的话,对于Job来说没有)、资源对应可以使用的动词列表等。
返回的列表描述了在API服务器中暴露的REST资源。"name":"jobs"
行的信息告诉我们API包含了 /apis/batch/V1/jobs
的endpoint,"verbs"数组告诉我们可以通过endpoint恢复、修改以及删除Job资源。对于某些特定的资源,API服务器暴露了额外的API endpoint(例如,通过 jobs/status
路径可以修改Job的状态)。
列举集群中所有的Job实例
通过在 /apis/batch/v1/jobs
路径运行一个GET请求,可以获取集群中所有Job的清单,如下面的代码清单所示。
代码清单8.10 Job清单:http://localhost:8001/apis/batch/v1/jobs
$ curl http://localhost:8001/apis/batch/v1/jobs
如果在集群中没有部署Job资源,那么items数组将是空的。可以尝试在Chapter08/my-job.yaml中部署Job,然后再次访问RESR endpoint从而得到与代码清单8.10中相同的输出信息。
通过名称恢复一个指定的Job实例
前面的endpoint返回了跨命名空间的所有Job的清单,如果想要返回指定的一个Job,需要在URL中指定它的名称和所在的命名空间。为了恢复在之前清单中的一个Job(name:my-job
; namespace:dfault
),需要访问路径:/apis/batch/v1/namespaces/default/jobs/my-job
,如下面的代码清单所示。
代码清单8.11 通过名称恢复一个指定命名空间下的资源
$ curl http://localhost:8001/apis/batch/v1/namespace/default/jobs/my-job
可以看到,我们得到了my-job这个Job资源的完整的JSON定义信息,和运行 $kubetcl get job my-job -o json
命令得到的信息完全一致。
$ kubectl get job my-job -o json
虽然不使用任何特定的工具,我们也可以访问Kubernetes REST API服务器,但如果要全面地研究REST API并与之交互,在本章最后会介绍更好的方式。暂时来看,像这样使用curl进行研究,对我们理解一个应用如何在pod中运行并与Kubernetes交互已经足够。
8.2.2 从pod内部与API服务器进行交互
我们已经知道如何从本机通过使用kubectl proxy与API服务器进行交互。现在我们来研究从一个pod内部访问它,这种情况下通常没有kubectl可用。因此,想要从pod内部与API服务器进行交互,需要关注以下三件事情:
- 确定API服务器的位置
- 确保是与API服务器进行交互,而不是一个冒名者
- 通过服务器的认证,否则将不能查看任何内容以及进行任何操作
接下来我们看一下交互如何实现。
运行一个pod来尝试与API服务器进行通信
首先需要一个pod以便从它内部发起与API服务器的交互。运行一个什么也不做的pod(在它仅有的容器内部运行一个sleep命令),然后通过 kubectl exec
在容器内部运行一个脚本,接下来在脚本中使用curl尝试访问API服务器。
因此,需要使用一个包含curl二进制的容器镜像。如果在Docker Hub中搜索,就会发现curlimages/curl镜像,可以使用这个镜像(也可以使用任何包含curl二进制的已有镜像或者自己打包的镜像)。pod的定义如下面的代码清单所示。
代码清单8.12 用来尝试与API服务器通信的pod:curl.yaml
apiVersion: v1kind: Podmetadata: name: curlspec: containers: - name: main image: curlimages/curl #使用curl command: ["sleep", "9999999"] #保持容器处于运行状态
在完成pod的创建后,在容器中运行kubectl exec来启动一个bash shell:
$ kubectl exec curl -it -- sh
我们现在已经做好了与API服务器交互的准备。
发现API服务器地址
首先,需要找到Kubernetes API服务器的IP地址和端口。这一点比较容易做到,因为一个名为kubernetes的服务在默认的命名空间被自动暴露,并被配置为指向API服务器。也许你应该记得,每次使用kubectl get svc命令显示所有服务清单时,都会看到这个服务。
$ kubectl get svc
在第5章中说过每个服务都被配置了对应的环境变量,在容器内通过查询 KUBERNETES_SERVICE_HOST
和 KUBERNETES_SERVICE_PORT
这两个环境变量就可以获取API服务器的IP地址和端口。
curl:/$ env | grep KUBERNETES_SERVICEKUBERNETES_SERVICE_PORT=443KUBERNETES_SERVICE_PORT_HTTPS=443KUBERNETES_SERVICE_HOST=10.96.0.1
同样,我们应该记得每个服务都可以获得一个DNS入口,所以甚至没有必要去查询环境变量,而只是简单地将curl指向 https://kubernetes
。公平地讲,如果我们不知道服务在哪个端口是可用的,既可以查询环境变量,也可以查看DNS SRV记录来得到实际的端口号。
之前展示的环境变量说明API服务器监听HTTPS协议默认的443端口,所以尝试通过HTTPS协议来访问服务器。
curl:/$ curl https://10.96.0.1
虽然最简单的绕开这一步骤的方式是使用推荐的-k选项(这也是我们在手工操作API服务器时通常会使用的方式),但还是来看一下更长(也是正确)的途径。我们应该通过使用curl检查证书的方式验证API服务器的身份,而不是盲目地相信连接的服务是可信的。
提示 在真实的应用中,永远不要跳过检查服务器证书的环节。这样做会导致你的应用验证凭证暴露给采用中间人攻击方式的攻击者。
验证服务器身份
在之前的章节中,在讨论Secret时,我们看到一个名为defalut-token-xyz的Secret被自动创建,并挂载到每个容器的/var/run/secrets/http://kubernetes.io/serviceaccount目录下。让我们查看目录下的文件,再次看一下Secret的内容。
curl:/$ ls /var/run/secrets/kubernetes.io/serviceaccountca.crt namespace token
Secret有三个入口(因此在Secret卷中有三个文件)。现在,我们关注一下ca.crt文件。该文件中包含了CA的证书,用来对Kubernetes API服务器证书进行签名。为了验证正在交互的API服务器,我们需要检查服务器的证书是否是由CA签发。curl允许使用-cacert选项来指定CA证书,我们来尝试重新访问API服务器:
curl:/$ curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt https://10.96.0.1
注意 我们可能看到一个比“Unauthorized”更长的错误描述。
到目前为止,我们已经取得进展,服务使用了我们信任的CA签名的证书,所以curl验证通过了服务器的身份,但Unauthorized这个响应提醒我们需要关注授权的问题。同时,看一下如何通过设置 CURL_CA_BUNDLE
环境变量来简化操作,从而不必在每次运行curl时都指定 --cacert
选项:
curl:/$ export CURL_BUNDLE=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
现在,我们可以不使用 --cacert
来访问API服务器:
curl:/$ curl https://10.96.0.1
这样操作相对便捷,我们的客户端(curl)现在信任API服务器,但API服务器并不确认访问者的身份,所以没有授权允许访问。
获得API服务器授权
我们需要获得API服务器的授权,以便可以读取并进一步修改或删除部署在集群中的API对象。为了获得授权,我们需要认证的凭证,幸运的是,凭证可以使用之前提到的default-token Secret来产生,同时凭证可以被存放在secret卷的token文件中。Secret这个名字就说明了它主要的作用。
可以使用凭证来访问API服务器,第一步,将凭证挂载到环境变量中:
curl:/$ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
此时,凭证已经被存放在TOKEN环境变量中,如下面的代码清单所示,可以在向API服务器发送请求时使用它。
代码清单8.13 获得API服务器的正确响应
curl:/$ curl -N "Authorization: Bearer $TOKEN" https://10.96.0.1
关闭基于角色的访问控制(RBAC)
如果我们正在使用一个带有RBAC机制的Kubernetes集群,服务账户可能不会被授权访问API服务器(或只有部分授权)。我们将在第12章详细了解服务账户和RBAC机制。目前最简单的方式就是运行下面的命令查询API服务器,从而绕过RBAC方式。
$ kubectl create clusterrolebinding permissive-binding \ --clusterrole=cluster-admin \ -- group=system:serviceaccount
这个命令赋予了所有服务账户(也可以说所有的pod)的集群管理员权限,允许它们执行任何需要的操作,很明显这是一个危险的操作,永远都不应该在生产的集群中执行,对于测试来说是没有问题的。
我们通过发送请求的HTTP头中的Authorization字段向API服务器传递了凭证,API服务器识别确认凭证并返回正确的响应,正如前面几个章节我们所做的,现在可以探索集群中所有的资源。
例如,可以列出集群中所有的pod,但前提是我们知道运行curl的pod属于哪个命名空间。
获取当前运行pod所在的命名空间
在本章的第一部分,我们了解了如何使用Downward API的方式将命名空间的属性传递到pod。如果你注意观察的话,或许注意到secret卷中也包含了一个叫作命名空间的文件。这个文件包含了当前运行pod所在的命名空间,所以我们可以读取这个文件来获得命名空间信息,而不是通过环境变量明确地传递信息到pod。文件内容挂载到NS环境变量中,然后列出所有的pod,如下面的代码清单所示。
代码清单8.14 获取当前pod所在命名空间中的所有pod清单
curl:/$ NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)curl:/$ curl -N "Authorization: Bearer $TOKEN" --cacert $CURL_BUNDLE https://10.96.0.1/api/v1/namespaces/$NS/pod
这就对了,通过使用挂载在secret卷目录下的三个文件,可以罗列出与当前pod运行在同一个命名空间下的所有pod的清单。使用同样的方式不仅可以使用GET请求,还可以使用PUT或者PATCH来检索和修改其他API对象。
简要说明pod如何与Kubernetes交互
我们来简单说明一下在pod中运行的应用如何正确访问Kubernetes的API:
- 应用应该验证API服务器的证书是否是证书机构所签发,这个证书是在ca.crt文件中。
- 应用应该将它在token文件中持有的凭证通过Authorization标头来获得API服务器的授权。
当对pod所在命名空间的API对象进行CRUD操作时,应该使用namespace文件来传递命名空间信息到API服务器。
定义 CRUD代表创建、读取、修改和删除操作,与之对应的HTTP方法分别是POST、GET、PATCH/PUT以及DELETE。
8.2.3 通过ambassador容器简化与API服务器的交互
使用HTTPS、证书和授权凭证,对于开发者来说看上去有点复杂。碰到过开发者在许多场景下关闭了对服务器证书验证的功能(当然笔者有时候也会这么做)。幸运的是,我们在保证安全性的前提下有办法简化通信的方式。
还记得在8.2.1中提到过的 kubectl proxy
命令吗?在本机上运行这个命令,从而可以更加方便地访问API服务器。向代理而不是直接向API服务器发送请求,通过代理来处理授权、加密和服务器验证。同样,也可以在pod中这么操作。
ambassador容器模式介绍
想象一下,如果一个应用需要查询API服务器(此外还有其他原因)。除了像之前章节讲到的直接与API服务器交互,可以在主容器运行的同时,启动一个ambassador容器,并在其中运行 kubecctl proxy
命令,通过它来实现与API服务器的交互。
在这种模式下,运行在主容器中的应用不是直接与API服务器进行交互,而是通过HTTP协议(不是HTTPS协议)与ambassador连接,并且由ambassador通过HTTPS协议来连接API服务器,对应用透明地来处理安全问题(见图8.6)。这种方式同样使用了默认凭证Secret卷中的文件。
因为在同一个pod中的所有连接共享同样的回送网络接口,所以我们的应用可以使用本地的端口来访问代理。
运行带有附加ambassador容器的CURL pod
为了通过操作来理解ambassador容器模式,我们像之前创建curl pod一样创建一个新的pod,但这次不是仅仅在pod中运行单个容器,而是基于一个多用途的kubectl-proxy容器镜像来运行一个额外的ambassador容器,这个镜像是之前创建的并已提交到Docker Hub。如果你想自己来编译它,可以在代码存档中找到Dockerfile镜像(在 /Chapter08/kubectl-proxy/
目录下)。
pod的manifest文件如以下代码清单所示。(tutum/curl已被废弃)
代码清单8.15 带有ambassador容器的 pod:curl-with-ambassador.yaml
apiVersion: v1kind: Podmetadata: name: curl-with-ambassadorspec: containers: - name: main image: curlimages/curl command: ["sleep", "9999999"] - name: ambassador #ambassador 容器 image: datawire/ambassador
pod的spec与之前非常类似,但pod名称是不同的,同时增加了一个额外的容器。运行这个pod,并且通过以下命令进入主容器:
$ kubectl exec -it curl-with-ambassador -c main bash
现在pod包含两个容器,我们希望在main容器中运行bash,所以使用 -c main
选项。如果想在pod的第一个容器中运行该命令,也无须明确地指定容器。但如果想在任何其他的容器中运行这个命令,就需要使用 -c
选项来说明容器的名称。
通过ambassador来与API服务器进行交互
接下来我们尝试通过ambassador容器来连接API服务器。默认情况下, kubectl proxy
绑定8001端口,由于pod中的两个容器共享包括回送地址在内的相同的网络接口,可以如下面的代码清单所示,将curl指向 localhost:8001
。
代码清单8.16 通过ambassador容器访问API服务器
root@curl-with-ambassador:/# curl localhost:8001
成功了!curl的输出打印结果与我们之前看到的响应相同,但这次,并不需要处理授权的凭证和服务器证书。
想要清楚地了解处理的细节,请参考图8.7。curl向在ambassador容器内运行的代理发送普通的HTTP请求(不包含任何授权相关的标头),然后代理向API服务器发送HTTPS请求,通过发送凭证来对客户端授权,同时通过验证证书来识别服务器的身份。
这是一个很好的例子,它说明了如何使用一个ambassador容器来屏蔽连接外部服务的复杂性,从而简化在主容器中运行的应用。ambassador容器可以跨多个应用复用,而且与主应用使用的开发语言无关。负面因素是需要运行额外的进程,并且消耗资源。
8.2.4 使用客户端库与API服务器交互
在之前的例子中,我们已经体验到了使用ambassador容器kubectl-proxy的好处,如果我们的应用仅仅需要在API服务器执行一些简单的操作,往往可以使用一个标准的客户端库来执行简单的HTTP请求。但对于执行更复杂的API请求来说,使用某个已有的Kubernetes API客户端库会更好一点。
使用已有的客户端库
目前,存在由API Machinery special interest group(SIG)支持的两个版本的Kuberbetes API客户端库。
- Golang client — https://github.com/kubernetes/client-go
- Python — https://github.com/kubernetes-incubator/client-python
注意 Kubernetes社区有大量的兴趣组和工作组,这些小组分别关注着Kubernetes生态系统中的某个特定部分。可以在https://github.com/kubernetes/community/blob/master/sig-list.md下看到它们的清单。
除了官方支持的两个库,这里列出了一些由用户贡献的针对不同语言的客户端库:
- Fabric8维护的Java客户端 — https://github.com/fabric8io/kubernetes-client
- Amdatu维护的Java客户端 — https://bitbucket.org/amdatulabs/amdatu-kubernetes
- tenxcloud维护的Node.js客户端 — https://github.com/tenxcloud/node-kubernetesclient
- GoDaddy维护的Node.js客户端 — https://github.com/godaddy/kubernetes-client
- PHP — https://github.com/devstub/kubernetes-api-php-client 另一个PHP客户端 — https://github.com/maclof/kubernetes-client
- Ruby — https://github.com/Ch00k/kubr
- 另一个Ruby客户端 — https://github.com/abonas/kubeclient
- Clojure — https://github.com/yanatan16/clj-kubernetes-api
- Scala — https://github.com/doriordan/skuber
- Perl — https://metacpan.org/pod/Net::Kubernetes
这些库通常支持HTTPS协议,并且可以处理授权操作,所以我们不需要使用ambassador容器。
一个使用Fabric8 Java库与Kubernetes进行交互的例子
为了说明如何通过客户端库与API服务器进行交互,以下的代码清单给出了一个例子说明如何使用Fabric8 Kubernetes客户端列出一个Java应用中的服务。
使用Swagger和OpenAPI打造你自己的库
如果我们选择的开发语言没有可用的客户端,可以使用Swagger API框架生成客户端库和文档。Kubernetes API服务器在/swaggerapi下暴露Swagger API定义,在/swagger.json下暴露OpenAPI定义。
想要了解更多关于Swagger框架的内容,请访问网站 http://swagger.io
。
使用Swagger UI研究API
在本章开头,已经提到了一种更好的研究Rest API的方式,而不是使用curl直接访问REST endpoint。正如在前面部分所提到的,Swagger不仅是描述API的工具,如果暴露了Swagger API定义,还能够提供一个用于查看REST API的web UI。
Kubernetes不仅暴露了Swagger API,同时也有与API服务器集成的Swagger UI。Swagger UI默认状态没有激活,可以通过使用 --enable-swagger-ui=true
选项运行API服务器对其进行激活。
提示 如果使用Minikube,可以在启动集群时,使用 minikube start--extra-config=apiserver.Features.Enable-SwaggerUI=true
选项来激活Swagger UI。
在激活UI后,可以通过以下地址在浏览器中打开它:
http(s)://:/swagger-ui
强烈建议尝试使用Swagger UI,通过它不仅可以浏览Kubernetes API,也可以使用它来进行交互(例如,可以 POST JSON 资源manifest、PATCH资源或者DELETE它们)。