1. Introduction
1.绪论
After working for a while with Kubernetes, we’ll soon realize that there’s a lot of boilerplate code involved. Even for a simple service, we need to provide all required details, usually taking the form of a quite verbose YAML document.
在使用Kubernetes工作一段时间后,我们很快就会意识到有很多模板代码涉及。即使是一个简单的服务,我们也需要提供所有需要的细节,通常采取相当冗长的YAML文档的形式。
Also, when dealing with several services deployed in a given environment, those YAML documents tend to contain a lot of repeated elements. For instance, we might want to add a given ConfigMap or some sidecar containers to all deployments.
另外,当处理部署在特定环境中的几个服务时,这些YAML文档往往包含很多重复的元素。例如,我们可能想在所有部署中添加一个给定的ConfigMap或一些sidecar容器。
In this article, we’ll explore how we can stick to the DRY principle and avoid all this repeated code using Kubernetes admission controllers.
在这篇文章中,我们将探讨如何坚持DRY原则,避免使用Kubernetes接纳控制器的所有这些重复代码。
2. What’s an Admission Controller?
2.什么是录取控制器?
Admission controllers are a mechanism used by Kubernetes to pre-process API requests after they’ve been authenticated but before they’re executed.
许可控制器是Kubernetes使用的一种机制,在API请求被验证后但被执行前对其进行预处理。
The API server process (kube-apiserver) already comes with several built-in controllers, each in charge of a given aspect of API processing.
API服务器进程(kube-apiserver)已经内置了几个控制器,每个控制器负责API处理的某个方面。
AllwaysPullImage is a good example: This admission controller modifies pod creation requests, so the image pull policy becomes “always”, regardless of the informed value. The Kubernetes documentation contains the full list of the standard admission controllers.
AllwaysPullImage是一个好例子。这个接纳控制器修改了pod创建请求,因此图像拉动策略变成了 “总是”,而不考虑通知值。Kubernetes文档包含标准接纳控制器的完整列表。
Besides those built-in controllers, which actually run as part of the kubeapi-server process, Kubernetes also supports external admission controllers. In this case, the admission controller is just an HTTP service that processes requests coming from the API server.
除了那些实际上作为kubeapi-server进程的一部分运行的内置控制器,Kubernetes还支持外部接纳控制器。在这种情况下,接纳控制器只是一个HTTP服务,处理来自API服务器的请求。
In addition, those external admission controllers can be dynamically added and removed, hence the name dynamic admission controllers. This results in a processing pipeline that looks like this:
此外,这些外部接纳控制器可以被动态地添加和删除,因此被称为动态接纳控制器。这导致了一个处理管道,看起来像这样。
Here, we can see that the incoming API request, once authenticated, goes through each of the built-in admission controllers until it reaches the persistence layer.
在这里,我们可以看到,传入的API请求一旦经过认证,就会经过每个内置的接纳控制器,直到到达持久层。
3. Admission Controller Types
3.接纳控制器类型
Currently, there are two types of admission controllers:
目前,有两种类型的接纳控制器。
- Mutating admission controllers
- Validation admission controllers
As their names suggest, the main difference is the type of processing each does with an incoming request. Mutating controllers may modify a request before passing them downstream, whereas validation ones can only validate them.
正如它们的名字所示,主要的区别在于它们对传入请求的处理类型。混合型控制器可以在将请求传递给下游之前修改请求,而验证型控制器只能验证请求。
An important point about those types is the order in which the API server executes them: mutating controllers come first, then validation controllers. This makes sense, as validation will only occur once we have the final request, possibly changed by any of the mutating controllers.
关于这些类型,很重要的一点是API服务器执行它们的顺序:首先是变异控制器,然后是验证控制器。这是有道理的,因为只有当我们得到最终的请求时,验证才会发生,可能会被任何一个变异控制器所改变。
3.1. Admission Review Requests
3.1.入学审查请求
The built-in admission controllers (mutating and validating) communicate with external admission controllers using a simple HTTP Request/Response pattern:
内置的接纳控制器(突变和验证)使用简单的HTTP请求/响应模式与外部接纳控制器通信。
- Request: an AdmissionReview JSON object containing the API call to process in its request property
- Response: an AdmissionReview JSON object containing the result in its response property
Here’s an example of a request:
下面是一个请求的例子。
{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
"request": {
"uid": "c46a6607-129d-425b-af2f-c6f87a0756da",
"kind": {
"group": "apps",
"version": "v1",
"kind": "Deployment"
},
"resource": {
"group": "apps",
"version": "v1",
"resource": "deployments"
},
"requestKind": {
"group": "apps",
"version": "v1",
"kind": "Deployment"
},
"requestResource": {
"group": "apps",
"version": "v1",
"resource": "deployments"
},
"name": "test-deployment",
"namespace": "test-namespace",
"operation": "CREATE",
"object": {
"kind": "Deployment",
... deployment fields omitted
},
"oldObject": null,
"dryRun": false,
"options": {
"kind": "CreateOptions",
"apiVersion": "meta.k8s.io/v1"
}
}
}
Among the available fields, some are particularly important:
在现有的领域中,有些是特别重要的。
- operation: This tells whether this request will create, modify or delete a resource
- object: The resource’s specification details being processed.
- oldObject: When modifying or deleting a resource, this field contains the existing resource
The expected response is also an AdmissionReview JSON object, with a response field instead response:
预期的响应也是一个AdmissionReviewJSON对象,有一个response字段代替response:。
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "c46a6607-129d-425b-af2f-c6f87a0756da",
"allowed": true,
"patchType": "JSONPatch",
"patch": "W3sib3A ... Base64 patch data omitted"
}
}
Let’s dissect the response object’s fields:
让我们来剖析一下response 对象的字段。
- uid: the value of this field must match the corresponding field present in the incoming request field
- allowed: The outcome of the review action. true means that the API call processing may proceed to the next step
- patchType: Valid only for mutating admission controllers. Indicates the patch type returned by the AdmissionReview request
- patch: Patches to apply in the incoming object. Details on next section
3.2. Patch Data
3.2补丁数据
The patch field present in the response from a mutating admission controller tells the API server what needs to be changed before the request can proceed. Its value is a Base64-encoded JSONPatch object containing an array of instructions that the API server uses to modify the incoming API call’s body:
来自突变接纳控制器的响应中存在的patch字段告诉API服务器在请求继续进行之前需要更改的内容。它的值是一个Base64编码的JSONPatch对象,其中包含API服务器用来修改传入的API调用正文的指令数组。
[
{
"op": "add",
"path": "/spec/template/spec/volumes/-",
"value":{
"name": "migration-data",
"emptyDir": {}
}
}
]
In this example, we have a single instruction that appends a volume to the volumes array of the deployment specification. A common issue when dealing with patches is the fact that there’s no way to add an element to an existing array unless it already exists in the original object. This is particularly annoying when dealing with Kubernetes API objects, as the most common ones (e.g., deployments) include optional arrays.
在这个例子中,我们有一条指令,将一个卷添加到部署规范的volumes数组中。处理补丁时的一个常见问题是,没有办法将一个元素添加到现有的数组中,除非它已经存在于原始对象中。在处理Kubernetes API对象时,这一点尤其令人讨厌,因为最常见的对象(如部署)包括可选数组。
For instance, the previous example is valid only when the incoming deployment already has at least one volume. If this was not the case, we’d have to use a slightly different instruction:
例如,只有当传入的deployment已经有至少一个卷时,前面的例子才有效。如果不是这种情况,我们就必须使用一个稍微不同的指令。
[
{
"op": "add",
"path": "/spec/template/spec/volumes",
"value": [{
"name": "migration-data",
"emptyDir": {}
}]
}
]
Here, we’ve defined a new volumes field whose value is an array containing the volume definition. Previously, the value was an object since this is what we were appending to the existing array.
这里,我们定义了一个新的volumes字段,其值是一个包含卷定义的数组。以前,该值是一个对象,因为这是我们追加到现有数组的内容。
4. Sample Use Case: Wait-For-It
4.示例用例 等待–它
Now that we have a basic understanding of the expected behavior of an admission controller, let’s write a simple example. A common issue in Kubernetes when is managing runtime dependencies, especially when using a microservices architecture. For instance, if a particular microservice requires access to a database, there’s no point in starting if the former is offline.
现在我们对接纳控制器的预期行为有了基本了解,让我们写一个简单的例子。在Kubernetes中,一个常见的问题是管理运行时的依赖关系,特别是在使用微服务架构时。例如,如果一个特定的微服务需要访问数据库,如果前者是离线的,就没有必要启动。
To address issues like this, we can use an initContainer with our pods to do this check before starting the main container. An easy way to do that is using the popular wait-for-it shell script, also available as a docker image.
为了解决这样的问题,我们可以使用initContainer与我们的pods一起,在启动主容器之前做这个检查。一个简单的方法是使用流行的wait-for-it shell脚本,也可以作为docker图像提供。
The script takes a hostname and port parameters and tries to connect to it. If the test succeeds, the container exits with a successful status code, and the pod initialization proceeds. Otherwise, it will fail, and the associated controller will keep on retrying according to the defined policy. The cool thing about externalizing this pre-flight check is that any associated Kubernetes service will notice that the failure. Consequently, no requests will be sent to it, potentially improving overall resiliency.
该脚本需要一个hostname和port参数并尝试连接到它。如果测试成功,容器将以成功的状态代码退出,并继续进行pod初始化。否则,它将失败,相关的控制器将根据定义的策略继续重试。将这个飞行前检查外部化的好处是,任何相关的Kubernetes服务都会注意到这一失败。因此,不会有请求被发送到它那里,可能会提高整体的弹性。
4.1. The Case for Admission Controller
4.1.接纳控制器的案例
This is what a typical deployment with the wait-for-it init container added to it:
这就是一个典型的部署,其中加入了wait-for-it init容器。
apiVersion: apps/v1 kind: Deployment metadata: name: frontend labels: app: nginx spec: replicas: 1 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: initContainers: - name: wait-backend image: willwill/wait-for-it args: -
www.google.com:80
containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80
While not that complicated (at least in this simple example), adding the relevant code to every deployment has some drawbacks. In particular, we’re imposing on deployment authors the burden to specify exactly how a dependency check should be done. Instead, a better experience would require only defining what should be tested.
虽然没有那么复杂(至少在这个简单的例子中),但在每个部署中添加相关代码有一些缺点。尤其是,我们将指定依赖性检查的确切方式的负担强加给了部署作者。相反,更好的体验只需要定义what应该被测试。
Enter our admission controller. To address this use case, we’ll write a mutating admission controller that looks for the presence of a particular annotation in a resource and adds the initContainer to it if present. This is what an annotated deployment spec would look like:
为了解决这个用例,我们将编写一个突变的接纳控制器,寻找资源中是否存在特定的注解,如果存在,就将initContainer添加到资源中。这就是带注解的部署规范的样子。
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
labels:
app: nginx
annotations:
com.baeldung/wait-for-it: "www.google.com:80"
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
Here, we’re using the annotation com.baeldung/wait-for-it to indicate the host and port we must test. What’s important, though, is nothing is telling us how the test should be done. In theory, we could change the test in any way while keeping the deployment spec unchanged.
在这里,我们使用注解com.baeldung/wait-for-it来指示我们必须测试的主机和端口。重要的是,没有任何东西告诉我们如何进行测试。理论上,我们可以以任何方式改变测试,同时保持部署规范不变。
Now, let’s move on to the implementation.
现在,让我们继续讨论实施问题。
4.2. Project Structure
4.2.项目结构
As discussed before, the external admission controller is just a simple HTTP service. As such, we’ll create a Spring Boot project as our basic structure. For this example, this is all we need is the Spring Web Reactive starter but, for a real-world application, it might also be useful to add features like the Actuator and/or some Cloud Config dependencies.
如前所述,外部接纳控制器只是一个简单的HTTP服务。因此,我们将创建一个Spring Boot项目作为我们的基本结构。在这个例子中,我们只需要Spring Web Reactive启动器,但是,对于现实世界的应用程序来说,添加Actuator和/或一些Cloud Config依赖项等功能可能也是有用的。
4.3. Handling Requests
4.3 处理请求
The entry point for admission request is a simple Spring REST controller that delegates the processing of the incoming payload to a service:
接收请求的入口点是一个简单的Spring REST控制器,它将传入的有效载荷的处理委托给一个服务。
@RestController
@RequiredArgsConstructor
public class AdmissionReviewController {
private final AdmissionService admissionService;
@PostMapping(path = "/mutate")
public Mono<AdmissionReviewResponse> processAdmissionReviewRequest(@RequestBody Mono<ObjectNode> request) {
return request.map((body) -> admissionService.processAdmission(body));
}
}
Here, we’re using an ObjectNode as the input parameter. This means that we’ll try to process any well-formed JSON sent by the API Server. The reason for this lax approach is, as of this writing, there’s still no official schema published for this payload. Using a non-structured type, in this case, implies some extra work, but ensures our implementation deals a bit better with any extra fields that a particular Kubernetes implementation or version decides to throw at us.
这里,我们使用一个ObjectNode作为输入参数。这意味着我们将尝试处理API服务器发送的任何格式良好的JSON。这种宽松的方法的原因是,截至本文写作时,仍然没有为这种有效载荷发布官方模式。在这种情况下,使用非结构化类型意味着一些额外的工作,但确保我们的实现能够更好地处理特定的Kubernetes实现或版本决定扔给我们的任何额外字段。
Also, given that the request object can be any of the available resources in the Kubernetes API, adding too much structure here would not be that helpful.
另外,考虑到请求对象可以是Kubernetes API中的任何可用资源,在这里添加太多的结构不会有什么帮助。
4.4. Modifying Admission Requests
4.4.修改录取请求
The meat of the processing happens in the AdmissionService class. This is a @Component class injected into the controller with a single public method: processAdmission. This method processes the incoming review request and returns the appropriate response.
处理的主要部分发生在AdmissionService类中。这是一个@Component类,它被注入到控制器中,只有一个公共方法。processAdmission.该方法处理传入的审查请求并返回适当的响应。
The full code is available online and basically consists of a long sequence of JSON manipulations. Most of them are trivial, but some excerpts deserve some explanation:
完整的代码可以在网上找到,基本上由一长串的JSON操作组成。其中大部分是微不足道的,但有些节选值得解释一下。
if (admissionControllerProperties.isDisabled()) {
data = createSimpleAllowedReview(body);
} else if (annotations.isMissingNode()) {
data = createSimpleAllowedReview(body);
} else {
data = processAnnotations(body, annotations);
}
First, why add a “disabled” property? Well, it turns out that, in some highly controlled environments, it might be much easier to change a configuration parameter of an existing deployment than removing and/or updating it. Since we’re using the @ConfigurationProperties mechanism to populate this property, its actual value can come from a variety of sources.
首先,为什么要添加一个 “禁用 “属性?好吧,事实证明,在一些高度控制的环境中,改变现有部署的配置参数可能比删除和/或更新它要容易得多。由于我们使用@ConfigurationProperties 机制来填充该属性,其实际值可以来自各种来源。
Next, we test for missing annotations, which we’ll treat as a sign that we should leave the deployment unchanged. This approach ensures the “opt-in” behavior that we want in this case.
接下来,我们测试是否有缺失的注释,我们会将其视为我们应该保持部署不变的标志。这种方法确保了我们在这种情况下想要的 “选入 “行为。
Another interesting snippet comes from the JSONPatch generation logic in the injectInitContainer() method:
另一个有趣的片段来自injectInitContainer()方法中的JSONPatch生成逻辑。
JsonNode maybeInitContainers = originalSpec.path("initContainers");
ArrayNode initContainers =
maybeInitContainers.isMissingNode() ?
om.createArrayNode() : (ArrayNode) maybeInitContainers;
ArrayNode patchArray = om.createArrayNode();
ObjectNode addNode = patchArray.addObject();
addNode.put("op", "add");
addNode.put("path", "/spec/template/spec/initContainers");
ArrayNode values = addNode.putArray("values");
values.addAll(initContainers);
As there’s no guarantee that the incoming specification contains the initContainers field, we must handle two cases: they may be either missing or present. If it is missing, we use an ObjectMapper instance (om in the snippet above) to create a new ArrayNode. Otherwise, we just use the incoming array.
由于不能保证传入的规范包含initContainers字段,我们必须处理两种情况:它们可能缺失或存在。如果缺失,我们使用一个ObjectMapper实例(om在上面的片段中)来创建一个新的ArrayNode。否则,我们就使用传入的数组。
In doing so, we can use a single “add” patch instruction. Despite its name, its behavior is such that the field either will be created or replace an existing field with the same name. The value field is always an array, which includes the (possibly empty) original initContainers array. The last step adds the actual wait-for-it container:
在这样做的时候,我们可以使用一条 “添加 “补丁指令。尽管它的名字,它的行为是这样的:这个字段要么被创建,要么替换一个现有的同名字段。value字段总是一个数组,其中包括(可能是空的)原始initContainers数组。最后一步是添加实际的wait-for-it容器。
ObjectNode wfi = values.addObject();
wfi.put("name", "wait-for-it-" + UUID.randomUUID())
// ... additional container fields added (omitted)
As container names must be unique within a pod, we just add a random UUID to a fixed prefix. This avoids any name clash with existing containers.
由于容器名称在一个pod内必须是唯一的,我们只是在一个固定的前缀上添加一个随机的UUID。这就避免了与现有的容器发生任何名称冲突。
4.5. Deployment
4.5.部署
The final step to start using our admission controller is to deploy it to a target Kubernetes cluster. As expected, this requires writing some YAML or using a tool like Terraform. Either way, those are the resources we need to create:
开始使用我们的接纳控制器的最后一步是将其部署到目标Kubernetes集群上。正如预期的那样,这需要编写一些YAML或者使用像Terraform这样的工具。不管怎么说,这些都是我们需要创建的资源。
- A Deployment to run our admission controller. It’s a good idea to spin more than one replica of this service, as failures may block any new deployments to happen
- A Service to route requests from the API Server to an available pod running the admission controller
- A MutatingWebhookConfiguration resource that describes which API calls should be routed to our Service
For instance, let’s say that we’d like Kubernetes to use our admission controller every time a deployment is created or updated. In the MutatingWebhookConfiguration documents we’ll see a rule definition like this:
例如,假设我们希望Kubernetes在每次创建或更新部署时使用我们的接纳控制器。在MutatingWebhookConfiguration文件中,我们会看到一个rule定义,像这样。
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: "wait-for-it.baeldung.com"
webhooks:
- name: "wait-for-it.baeldung.com"
rules:
- apiGroups: ["*"]
apiVersions: ["*"]
operations: ["CREATE","UPDATE"]
resources: ["deployments"]
... other fields omitted
An important point about our server: Kubernetes requires HTTPS to communicate with external admission controllers. This means we need to provide our SpringBoot server with a proper certificate and private key. Please check the Terraform script used to deploy the sample admission controller to see one way to do this.
关于我们的服务器的一个重要观点。Kubernetes需要HTTPS来与外部接纳控制器通信。这意味着我们需要为我们的SpringBoot服务器提供一个适当的证书和私钥。请查看用于部署样本接纳控制器的Terraform脚本,看看有什么方法可以做到这一点。
Also, a quick tip: Although not mentioned anywhere in the documentation, some Kubernetes implementations (e.g. GCP) require the usage of port 443, so we need to change the SpringBoot HTTPS port from its default value (8443).
另外,一个简单的提示:虽然在文档中没有提到,但一些Kubernetes实现(如GCP)需要使用443端口,所以我们需要改变SpringBoot HTTPS端口的默认值(8443)。
4.6. Testing
4.6.测试
Once we have the deployment artifacts ready, it’s finally time to test our admission controller in an existing cluster. In our case, we’re using Terraform to perform the deployment so all we have to do is an apply:
一旦我们准备好了部署工件,现在终于可以在现有集群中测试我们的接纳控制器了。在我们的案例中,我们使用Terraform来执行部署,所以我们所要做的就是apply。
$ terraform apply -auto-approve
Once completed, we can check the deployment and admission controller status using kubectl:
一旦完成,我们可以使用kubectl检查部署和接纳控制器状态。
$ kubectl get mutatingwebhookconfigurations
NAME WEBHOOKS AGE
wait-for-it-admission-controller 1 58s
$ kubectl get deployments wait-for-it-admission-controller
NAME READY UP-TO-DATE AVAILABLE AGE
wait-for-it-admission-controller 1/1 1 1 10m
Now, let’s create a simple nginx deployment including our annotation:
现在,让我们创建一个简单的nginx部署,包括我们的注释。
$ kubectl apply -f nginx.yaml
deployment.apps/frontend created
We can check the associated logs to see that the wait-for-it init container was indeed injected:
我们可以检查相关的日志,看看wait-for-it init容器确实被注入了。
$ kubectl logs --since=1h --all-containers deployment/frontend
wait-for-it.sh: waiting 15 seconds for www.google.com:80
wait-for-it.sh: www.google.com:80 is available after 0 seconds
Just to be sure, let’s check the deployment’s YAML:
为确定起见,让我们检查一下部署的YAML。
$ kubectl get deployment/frontend -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
com.baeldung/wait-for-it: www.google.com:80
deployment.kubernetes.io/revision: "1"
... fields omitted
spec:
... fields omitted
template:
... metadata omitted
spec:
containers:
- image: nginx:1.14.2
name: nginx
... some fields omitted
initContainers:
- args:
- www.google.com:80
image: willwill/wait-for-it
imagePullPolicy: Always
name: wait-for-it-b86c1ced-71cf-4607-b22b-acb33a548bb2
... fields omitted
... fields omitted
status:
... status fields omitted
This output shows the initContainer that our admission controller added to the deployment.
这个输出显示了我们的接纳控制器添加到部署中的initContainer。
5. Conclusion
5.总结
In this article, we’ve covered how to create a Kubernetes admission controller in Java and deploy it to an existing cluster.
在这篇文章中,我们已经介绍了如何在Java中创建一个Kubernetes接纳控制器,并将其部署到现有集群中。
As usual, the full source code of the examples can be found over on GitHub.
像往常一样,这些例子的完整源代码可以在GitHub上找到。