54
Improving Application Availability with Pod Readiness Gates
Making sure your application running in Kubernetes is available and ready to serve traffic can be very easy with Pod liveness and readiness probes. However, not all application are built to be able to use probes or in some cases require more complex readiness checks which these probes simply cannot perform. Is there however any other solution, if Pod probes just aren't good enough?
The answer is obviously, yes. It's possible to implement complex custom readiness checks for Kubernetes Pods with help of Readiness Gates.
Readiness gates allow us to create custom status condition types similar to PodScheduled
or Initialized
. Those conditions can then be used to evaluate Pod readiness.
Normally, Pod readiness is determined only by readiness of all the containers in a Pod, meaning that if all containers are Ready
, then whole is Ready
too. If readiness gate is added to a Pod, then readiness of a Pod gets determined by readiness of all containers and status of all readiness gate conditions.
Let's look at an example, to get a better idea of how this work:
kind: Pod
...
spec:
readinessGates:
- conditionType: "www.example.com/some-gate-1"
...
status:
conditions:
- type: Ready
status: "False"
lastProbeTime: null
lastTransitionTime: 2021-11-01T00:00:00Z
- type: ContainersReady
status: "True"
lastProbeTime: null
lastTransitionTime: 2021-11-01T00:00:00Z
- type: "www.example.com/some-gate-1"
status: "False"
lastProbeTime: null
lastTransitionTime: 2021-11-01T00:00:00Z
The above manifest shows a Pod with single readiness gate named www.example.com/some-gate-1
. Looking at the conditions in status stanza, we can see that the ContainersReady
condition is True
, meaning that all containers are ready, but the custom readiness gate condition is False
and therefore also Pod's Ready
condition must be False
.
If you use kubectl describe pod ...
on such a pod you would also see the following in the Conditions section:
...
Conditions:
Type Status
www.example.com/gate-1 False
Initialized True
Ready False
ContainersReady True
PodScheduled True
We now know that it's possible to implement these additional readiness conditions, but are they really necessary though? Shouldn't it be enough to just leverage health checks using probes?
In most cases probes should be sufficient, there are however situations where more complex readiness checks are necessary. Probably the most common use-case for readiness gates is to sync-up with external system such as cloud provider's load balancer. Example of that would be AWS LoadBalancer or Container-native load balancing in GKE. In these cases readiness gates allow us to make the workloads network aware.
Another reason to use readiness gates is if you have external system that can perform more thorough health checks on your workloads using - for example - application metrics. This can help integrate your system into Kubernetes workload lifecycle without requiring changes to kubelet. It also allows the external system to subscribe to Pod condition changes and act upon on it, possibly applying changes to remediate any availability issues.
Finally, readiness gates can be a lifesaver if you have legacy application deployed to Kubernetes which is not compatible with liveness or readiness probes, yet its readiness can be checked in a different way.
For complete rationale for this feature, check out the original KEP in GitHub.
Enough talking, let's create our first readiness gate. All we need to do is add readinessGates
stanza in Pod spec
with the name of our desired condition:
# kubectl run nginx \
# --image=nginx \
# --overrides='{"spec": {"readinessGates": [{"conditionType": "www.example.com/gate-1"}]}}'
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
readinessGates:
- conditionType: www.example.com/gate-1
containers:
- name: nginx
image: nginx:latest
Adding the gate is easy, but updating is little more complicated. kubectl
subcommands don't support patching of object status, therefore we cannot use kubectl patch
set the condition to True
/False
. Instead, we have to use PATCH
HTTP request sent directly to API server.
The simplest way to access cluster API server is using kubectl proxy
, which allows us to reach the server on localhost
:
kubectl proxy --port 12345 &
curl -s http://localhost:12345/
curl -k -H 'Accept: application/json' http://localhost:12345/api/v1/namespaces/default/pods/nginx/status
In addition to starting the proxy in the background, we also used curl
to check if the server is reachable and queried the server for manifest/status of the pod we will be updating.
Now that we have a way to reach the API server, let's try updating the Pod status. Every readiness gate status condition defaults to False
, but let's start by explicitly setting it:
# Explicitly set status to "False"
curl -k \
-H "Content-Type: application/json-patch+json" \
-X PATCH http://localhost:12345/api/v1/namespaces/default/pods/nginx/status \
--data '[ { "op": "add", "path": "/status/conditions/-", "value": { "lastProbeTime": null, "lastTransitionTime": "2020-03-05T15:50:51Z", "status": "False", "type": "www.example.com/gate-1" }}]'
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 25s 10.244.0.6 kind-control-plane <none> 0/1
kubectl describe pod nginx
...
Readiness Gates:
Type Status
www.example.com/gate-1 False
Conditions:
Type Status
www.example.com/gate-1 False
Initialized True
Ready False
ContainersReady True
PodScheduled True
In this snippet we first used PATCH
request against API proxy server to apply JSON patch to status.condition
fields of the Pod. In this case we used add
operation because the status was not set yet. Additionally, you can also see that when we list the pods with -o wide
, the READINESS GATES
column shows 0/1
indicating that the gate is set to False
. Same can be also seen in output of kubectl describe
.
Next, let's see how we can toggle the value to True
:
curl -k \
-H "Content-Type: application/json-patch+json" \
-X PATCH http://localhost:12345/api/v1/namespaces/default/pods/nginx/status \
--data '[{ "op": "replace", "path": "/status/conditions/0", "value": { "type": "www.example.com/gate-1", "status": "True" }}]'
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 58s 10.244.0.6 kind-control-plane <none> 1/1
Similarly to previous code snippet, we again use PATCH
request to update the condition, this time however we used replace
operation, specifically on the first condition in the list as specified by /status/conditions/0
. Be aware though, that the custom condition doesn't necessarily have to be first in the list so if you will be using some script to update conditions, then you should first check which condition you should be updating.
Updating condition with curl
like we saw above works for simple scripts or quick manual updates, but generally you will probably need more robust solution. Considering that kubectl
is not an option here, your best bet will be one of the Kubernetes client libraries. For demonstration purposes let's see how it can be done in Python:
# pip install kubernetes
import time
from kubernetes import client, config
config.load_kube_config()
pod_manifest = {
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "nginx"
},
"spec": {
"readinessGates": [
{"conditionType": "www.example.com/gate-1"}
],
"containers": [{
"image": "nginx",
"name": "nginx",
}]
}
}
v1 = client.CoreV1Api()
response = v1.create_namespaced_pod(body=pod_manifest, namespace="default")
while True:
response = v1.read_namespaced_pod(name="nginx", namespace="default")
if response.status.phase != "Pending":
break
time.sleep(1)
print("Pod is 'Running'...")
First thing we need to do is authenticate to the cluster and create the Pod. The authentication part is in this case done using config.load_kube_config()
which loads your credentials from ~/.kube/config
, in general though it's better to use service accounts and tokens to authenticate to the cluster, sample for that can be found in docs.
As for the second part - Pod creation - that's pretty straightforward, we just apply the pod manifest and then wait until it's status phase changes from Pending
.
With the Pod running, we can continue by setting its status to the initial False
value:
response = v1.patch_namespaced_pod_status(name="nginx", namespace="default", body=[{
"op": "add", "path": "/status/conditions/-",
"value": {
"lastProbeTime": None,
"lastTransitionTime": "2020-03-05T15:50:51Z",
"status": "False",
"type": "www.example.com/gate-1"
}}])
pod = v1.read_namespaced_pod_status(name="nginx", namespace="default")
for i, condition in enumerate(pod.status.conditions):
if condition.type == "Ready":
gate, index = condition, i
print(f"ReadinessGate '{gate.type}' has readiness status: {gate.status}, Reason: {gate.message}.")
# ReadinessGate 'Ready' has readiness status: False, Reason: the status of pod readiness gate "www.example.com/gate-1" is not "True", but False.
In addition to setting the status, we also queried the cluster for the current Pod status after the update. We looked up the section that corresponds to the Ready
condition and printed its status.
And finally, we can flip the value to True
with the following code:
pod = v1.read_namespaced_pod_status(name="nginx", namespace="default")
for i, condition in enumerate(pod.status.conditions):
if condition.type == "www.example.com/gate-1":
gate, index = condition, i
print(f"ReadinessGate '{gate.type}' has readiness status: {gate.status}, Reason: {gate.message}.")
# ReadinessGate 'www.example.com/gate-1' has readiness status: False, Reason: None.
response = v1.patch_namespaced_pod_status(name="nginx", namespace="default", body=[{
"op": "replace", "path": f"/status/conditions/{index}",
"value": {
"lastProbeTime": None,
"lastTransitionTime": "2020-03-05T15:50:51Z",
"status": "True",
"type": "www.example.com/gate-1"
}}])
The code here is very similar to the example earlier, this time however we look for condition of type www.example.com/gate-1
, we verify its current state with print
and then apply the change using replace
operation to the condition listed at index
.
Both the shell scripts and Python code above demonstrate how you can go about implementing readiness gates and their updates. In real world application you will probably need a more robust solution though.
The ideal solution for this would be custom controller which would watch pods, that have readinessGate
stanza set to relevant conditionType
s. The controller would be then able to update the condition(s) based on the observed state of the pod, whether it's based on custom pod metrics, external network state or whatever else.
If you're thinking about implementing such controller, then you can get some inspiration from existing solutions such as the AWS and GKE load balancers mentioned earlier or the (now archived) kube-conditioner
.
54