istio icon indicating copy to clipboard operation
istio copied to clipboard

Documentation unclear about sidecar customization (could be bug in injector)

Open mikebell90 opened this issue 1 year ago • 4 comments
trafficstars

Is this the right place to submit this?

  • [X] This is not a security vulnerability or a crashing bug
  • [X] This is not a question about how to use Istio

Bug Description

  1. As you know there are annotations to control the pods memory and cpu requests and limits. We use them
  2. For the specific purpose of optionally setting up a prestop hook for the proxy, we also embed using the methodology discussed in https://istio.io/latest/docs/setup/additional-setup/sidecar-injection/#customizing-injection
{{- define "mesh-container" }}
{{- if .Values.mesh.enabled -}}
{{- if .Values.mesh.prestop.enabled -}}
- name: istio-proxy
  image: auto
  lifecycle:
    preStop:
      exec:
        command: [ "{{ join "\",\"" .Values.mesh.prestop.commands }}" ]
{{- end }}
{{- end }}
{{- end }}
  1. That works great
  2. Except then we found effectively that memory and cpu annotations are ignored. My guess is the template takes precedence and sees no clause so just uses default values?

Version

istio 1.20.2

Additional Information

i have two concerns

  • This isn't very clear in the docs. There's comments on the same page like: "Additionally, certain fields are configurable by annotations on the pod, although it is recommended to use the above approach to customizing settings. Additional care must be taken for certain annotations:

If sidecar.istio.io/proxyCPU is set, make sure to explicitly set sidecar.istio.io/proxyCPULimit. Otherwise the sidecar’s cpu limit will be set as unlimited. If sidecar.istio.io/proxyMemory is set, make sure to explicitly set sidecar.istio.io/proxyMemoryLimit. Otherwise the sidecar’s memory limit will be set as unlimited."

but it doesn't mention they won't even WORK in this case.

  • creating a complete alternative template would be pretty hard to do and preserve function. In fact arguably it would be a deployment nightmare.

mikebell90 avatar May 21 '24 21:05 mikebell90

Given the above, is this

  • A bug in istio injector not merging the two correctly (there's no conflict per se)
  • A bug in the docs, and the only option is to drop the prestop or try to copy everything into a new template, which is rarely feasible.

mikebell90 avatar May 21 '24 21:05 mikebell90

I'd be happy to try to submit clearer docs, but it's not obvious to me the current behavior is as designed.

mikebell90 avatar May 28 '24 15:05 mikebell90

hmm...

mikebell90 avatar Jul 12 '24 15:07 mikebell90

hello

mikebell90 avatar Sep 04 '24 15:09 mikebell90

Hey, sorry for the (very long) delay here. I don't really get what you tried.

Where is the define mesh-container specified? Where is mesh-container used?

How are referencing the custom template?

We have a unit test around this I tried to reproduce in, but I couldn't grok how to set it up:

diff --git a/pkg/kube/inject/testdata/inject/custom-template.iop.yaml b/pkg/kube/inject/testdata/inject/custom-template.iop.yaml
diff --git a/pkg/kube/inject/testdata/inject/custom-template.iop.yaml b/pkg/kube/inject/testdata/inject/custom-template.iop.yaml
index f68cf9accb..efb2586a3a 100644
--- a/pkg/kube/inject/testdata/inject/custom-template.iop.yaml
+++ b/pkg/kube/inject/testdata/inject/custom-template.iop.yaml
@@ -8,30 +8,15 @@ spec:
     sidecarInjectorWebhook:
       templates:
         custom: |
-          metadata:
-            annotations:
-              # Disable the built-in transformations. In the future we may want a template-level API
-              prometheus.istio.io/merge-metrics: "false"
-              sidecar.istio.io/rewriteAppHTTPProbers: "false"
-              foo: bar
-          spec:
-            containers:
-            {{- range $index, $container := .Spec.Containers }}
-            - name: {{ $container.Name }}
-              env:
-              - name: SOME_ENV
-                value: "true"
-              - name: SOME_FILE
-                value: /var/lib/data/foo.json
-              volumeMounts:
-              - mountPath: /var/lib/data/foo.json
-                subPath: foo.json
-                name: some-injected-file
-            {{- end}}
-            volumes:
-            - name: some-injected-file
-              downwardAPI:
-                items:
-                - path: foo.json
-                  fieldRef:
-                    fieldPath: "metadata.annotations['foo']"
+          {{- define "mesh-container" }}
+          {{- if .Values.mesh.enabled -}}
+          {{- if .Values.mesh.prestop.enabled -}}
+          - name: istio-proxy
+            image: auto
+            lifecycle:
+              preStop:
+                exec:
+                  command: [ "{{ join "\",\"" .Values.mesh.prestop.commands }}" ]
+            {{- end }}
+            {{- end }}
+            {{- end }}
\ No newline at end of file
diff --git a/pkg/kube/inject/testdata/inject/custom-template.yaml b/pkg/kube/inject/testdata/inject/custom-template.yaml
index 0d1914bacf..5bfbb54e3b 100644
--- a/pkg/kube/inject/testdata/inject/custom-template.yaml
+++ b/pkg/kube/inject/testdata/inject/custom-template.yaml
@@ -9,7 +9,8 @@ spec:
   template:
     metadata:
       annotations:
-        inject.istio.io/templates: "custom"
+        inject.istio.io/templates: "sidecar,custom"
+        sidecar.istio.io/proxyCPU: "123m"
       labels:
         app: hello
     spec:

is what I had, if it helps

howardjohn avatar Dec 02 '24 20:12 howardjohn

The "sidecar.istio.io/proxyCPU is set, make sure to explicitly set sidecar.istio.io/proxyCPULimit" part makes sense and I agree could use some improved docs BTW

howardjohn avatar Dec 02 '24 20:12 howardjohn

  1. We have a chart to deploy our applications.
  2. It always specifies proxy cpu/memory requests and limits. Similar to
sidecar.istio.io/proxyMemory: {{ .memory | quote }}
sidecar.istio.io/proxyMemoryLimit: {{ .memory | quote }}
sidecar.istio.io/proxyCPU: {{ .cpu | quote }}
sidecar.istio.io/proxyCPULimit: {{ .cpu | quote }}
  1. In addition if mesh.prestop,enabled is set by the user, this part is injected
{{- define "mesh-container" }}
{{- if .Values.mesh.enabled -}}
{{- if .Values.mesh.prestop.enabled -}}
- name: istio-proxy
  image: auto
  lifecycle:
    preStop:
      exec:
        command: [ "{{ join "\",\"" .Values.mesh.prestop.commands }}" ]
{{- end }}
{{- end }}
{{- end }}

This follows the advice in https://istio.io/latest/docs/setup/additional-setup/sidecar-injection/#customizing-injection

(I'm open to other methods, the desired outcome is if some triggers exist in the helm values deploy file the proxy container adds a pre-stop hook.)

  1. This works perfectly!
  2. Except now the proxy cpu and memory annotations are ignored. You basically set proxy's cpu and memory to unbounded.
  3. I believe this is an order of merge operations from the default injection template combined with this one provided in the deployment pod template spec

mikebell90 avatar Dec 02 '24 20:12 mikebell90

Part of the confusion in this ticket @howardjohn is we have two completely different templates. We have the go-template injection by the injection controller and we have the helm template that we as application deployed use. I hope that corrects the confusion.

mikebell90 avatar Dec 02 '24 20:12 mikebell90

To further clarify. Here's fhe first part of our template pod template spec. I've edited it down

  template:
    metadata:
      annotations:
        app.kubernetes.io/version: {{ required ".Values.image.tag is required " .Values.image.tag | quote }}
        deployed: {{ date "20060102150405" now   | quote }}
      {{- range $name, $value := .Values.annotations }}
        {{ $name }}: "{{ $value }}"
      {{- end }}
      {{- include "mesh-annotations" . | nindent 8 }}
      labels:
      {{- range $name, $value := .Values.labels }}
        {{ $name }}: "{{ $value }}"
      {{- end }}
        identity: {{ include "identity.service-account" . | quote }}
        version: {{ include "identity.version" . | quote }}
        app: {{ .Release.Name }}
      {{- end}}
        {{- include "mesh-labels" . | nindent 8 }}

This isn't too interesting by itself, but it includes "mesh-labels" and "mesh-annotations:

"mesh-labels" determines sidecar injection

{{- define "mesh-labels" }}
{{- if .Values.mesh.enabled }}
sidecar.istio.io/inject: "{{ .Values.mesh.enabled }}"
{{- end }}
{{- end }}

while "mesh-annotations" is the resource stuff (I'll redact most of it)

sidecar.istio.io/proxyMemory: {{ .memory | quote }}
sidecar.istio.io/proxyMemoryLimit: {{ .memory | quote }}
sidecar.istio.io/proxyCPU: {{ .cpu | quote }}
sidecar.istio.io/proxyCPULimit: {{ .cpu | quote }}

Finally mesh sidecar container definition. This is ONLY injected if mesh.prestop.enabled is on, otherwise we let the injection controller handle this.

{{- define "mesh-container" }}
{{- if .Values.mesh.enabled -}}
{{- if .Values.mesh.prestop.enabled -}}
- name: istio-proxy
  image: auto
  lifecycle:
    preStop:
      exec:
        command: [ "{{ join "\",\"" .Values.mesh.prestop.commands }}" ]
{{- end }}
{{- end }}
{{- end }}

mikebell90 avatar Dec 02 '24 20:12 mikebell90

So IIUC the first part is the Deployment spec, so the fact its templated is invisible to Istio.

Here is what I think the end result you have:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: shell
spec:
  selector:
    matchLabels:
      app: shell
  template:
    metadata:
      annotations:
        sidecar.istio.io/proxyCPU: 123m
        sidecar.istio.io/proxyCPULimit: 200m
        sidecar.istio.io/proxyMemory: 123Mi
        sidecar.istio.io/proxyMemoryLimit: 250Mi
      labels:
        app: shell
    spec:
      containers:
      - image: auto
        lifecycle:
          preStop:
            exec:
              command:
              - sleep
              - "12"
        name: istio-proxy
      - args:
        - /bin/sleep
        - infinity
        image: howardjohn/shell
        name: shell

something like that?

In my testing that works fine. Is there a difference for yours?

howardjohn avatar Dec 02 '24 21:12 howardjohn

Archive.zip Yes. Attached are rendered deployments and the final injected pods. Note memory and cpu use the "default" with prestop, and use the annotations without

mikebell90 avatar Dec 03 '24 17:12 mikebell90

Do you have some controller mutating Deployment configs to add resourcces? Or maybe even to pods, running before Istio's injector? While the deployment object has no resources, the pod gets:

proxy.istio.io/overrides:  |
{
  "containers": [
    {
      "name": "istio-proxy",
      "resources": {
        "limits": {
          "cpu": "100m",
          "memory": "256Mi"
        },
        "requests": {
          "cpu": "100m",
          "memory": "256Mi"
        }
      },
      "volumeMounts": [
        {
          "name": "kube-api-access-g9qnt",
          "readOnly": true,
          "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
        }
      ],
      "lifecycle": {
        "preStop": {
          "exec": {
            "command": [
              "sleep",
              "12"
            ]
          }
        }
      },
      "terminationMessagePath": "/dev/termination-log",
      "terminationMessagePolicy": "File",
      "imagePullPolicy": "Always"
    }
  ]
}

So this is what the Istio webhook actually sees as the container spec by the time it reaches the webhook. This suggests something is actually adding these defaults.

I see kubernetes.io/limit-ranger: 'LimitRanger plugin set: memory request for container, which suggests its the limit ranger adding these.

So the order of operations is:

Base pod < templates (where annotation logic lives) < container overrides

The problem is something mutates the 'container overrides' before the Istio webhook, making it logically:

Base pod < templates (where annotation logic lives) < container overrides < limit-ranger mutation

howardjohn avatar Dec 03 '24 18:12 howardjohn

That makes sense! But why does it only kick in when the skeleton option exists? I'm guessing because the yaml is wholly absen in that case

So without our "template"

deployment -> Limit Ranger - doesn't see "bad" container" so no-op -> istio-injector creates pod yaml -> pod...

with our template

deployment -> Limit Ranger - sees the template skeleton with no request/liimit and mutates-> istio-injector creates pod yaml and sees these take precendence so ignores annotations -> pod...

This makes sense but short of removing limitrange (because it's a lame crd and you can't omit minimums IIRC) what can we do? Is there another way to achieve the prestop desired?

mikebell90 avatar Dec 03 '24 18:12 mikebell90

Yep, exactly.

There is probably a few approaches:

  • Don't use limit ranger (understandably unacceptable)
  • Add the resources into the istio-proxy container spec
  • Instead of in-lining the config into the pod (like https://istio.io/latest/docs/setup/additional-setup/sidecar-injection/#customizing-injection), make a new template and reference that. See https://github.com/istio/istio/blob/f2f6f265d9c3ffab144fd49df4ec9a287a7fe121/pkg/kube/inject/testdata/inject/custom-template.yaml#L12 (you want custom,sidecar so they both apply -- else you will have to fully replace it which you don't want). Then define the custom template like so https://github.com/istio/istio/blob/f2f6f265d9c3ffab144fd49df4ec9a287a7fe121/pkg/kube/inject/testdata/inject/custom-template.iop.yaml#L10. Note you will need to read some values from the pod, so you would probably have your deployment templating insert something like an annotation, etc to program it

howardjohn avatar Dec 03 '24 18:12 howardjohn

Those are all good ways. But seem inflexible. I'm not complaining per se - the design issues is fundamental to order of operations of admissions controllers, but very much affects this. Lets go through this list

  1. Don't use limit ranger (understandably unacceptable)

Yeah we need this.

  1. Create your own limit range controller as a replacement

It might be feasible to fork our own controller or use the CEL based as a replacement, but ultimately the issue is if you use istios "pod skeleton" feature, some mutator is is likely to have issues

  1. Repeat the literal cpu/memory settings in the pod skeleton, instead of just as annotation.

You allude to this and its actually the easiest thing - controlled in the same chart. The catch is if someone else changes something via annotation that mutating controller goes astray with. So I think this is better than the first option but has some future proofing.

  1. Instead of in-lining the config into the pod (like https://istio.io/latest/docs/setup/additional-setup/sidecar-injection/#customizing-injection), make a new template and reference that.

This is injected as permanent template in the injector CM I assume from what I read? That seems like that's not terribly flexible since you'd need to have 1 million templates (exaggeration) to cover every permutation of memory and cpu ;)

mikebell90 avatar Dec 03 '24 19:12 mikebell90

This is injected as permanent template in the injector CM I assume from what I read? That seems like that's not terribly flexible since you'd need to have 1 million templates (exaggeration) to cover every permutation of memory and cpu ;)

No, your template would look like

containers:
- name: istio-proxy
  lifecycle:
    preStop:
      exec:
        command: [ "sleep", "{{index .ObjectMeta.Annotations `some-annotation/sleep-duration`}}" ]

then when you want to use it, annotate on your pod inject.istio.io/templates: "custom,sidecar" and some-annotation/sleep-duration: 12s

howardjohn avatar Dec 03 '24 19:12 howardjohn

Ok, that clarifies things and I've tested it and it works. Small correction below

      prestop: |
        {{ if (isset .ObjectMeta.Annotations `mesh.foo.com/sleep-duration`) -}}
        spec:  
          containers:
          - name: istio-proxy
            lifecycle:
              preStop:
                exec:
                  command: [ "sleep", "{{index .ObjectMeta.Annotations `mesh.foo.com/sleep-duration`}}" ]
        {{- end}}

Anyway now it's just finding the time to redeploy everywhere. Thanks @howardjohn

mikebell90 avatar Dec 03 '24 20:12 mikebell90