Deploying custom Istio gateways in Helm
Customize gateways by editing the resource that defines the ingress and egress gateways for Istio-managed app traffic.
With the move to Helm for the Istio add-on version 1.24 and later, the IstioOperator custom resource is no longer used.
Setting up Helm
Before you begin deploying and managing custom gateways, set up Helm 3.18.4 or earlier.
-
Install Helm 3.18.4 or earlier.
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 chmod 700 get_helm.sh helm_version_pin="v3.18.4" DESIRED_VERSION="${helm_version_pin}" ./get_helm.sh which helm helm version rm get_helm.sh -
Add Istio's Helm repo.
helm repo add istio https://istio-release.storage.googleapis.com/charts -
Run the
helm repo updatecommand.helm repo update
Modifying existing default gateways
The add-on deploys one customizable istio-ingressgateway and one customizable istio-egressgateway. To customize the gateway ConfigMaps for the Helm charts, instead of adding a key-value pair like you do for the control
plane, edit the multiline string in the value.yaml key.
These value.yaml files are found as multiline strings in the managed-istio-ingressgateway-values and managed-istio-egressgateway-values ConfigMaps in the ibm-operators namespace.
apiVersion: v1
kind: ConfigMap
metadata:
labels:
addonmanager.kubernetes.io/mode: EnsureExists
name: managed-istio-egressgateway-values
namespace: ibm-operators
data:
values.yaml: |
...
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 2000m
memory: 1024Mi
To edit the istio-ingressgateway and istio-egressgateway value.yaml content:
-
Create a cluster.
-
Install the managed Istio add-on 1.24 or later.
ibmcloud ks cluster addon enable istio -c $CLUSTERID --version 1.24 -
Get the
kubeconfigof the cluster.ibmcloud ks cluster config -c $CLUSTERID -
Locate the two Istio gateway ConfigMaps that hold the
value.yamlcontent for the ingress and egress gateways.kubectl get cm -n ibm-operatorsOutput:
NAME DATA AGE istio-ca-root-cert 1 12m kube-root-ca.crt 1 24h managed-istio-base-control-plane-values 2 13m managed-istio-custom 1 13m managed-istio-egressgateway-values 2 13m managed-istio-ingressgateway-values 2 13m managed-istio-istiod-control-plane-values 2 13m -
Output the
values.yamlof the gateways to a file.kubectl get cm -n ibm-operators managed-istio-ingressgateway-values -o json | jq -r .data.\"values.yaml\" > gateway-values.yaml; open gateway-values.yamlOutput:
# "_internal_defaults_do_not_set" is a workaround for Helm limitations. Users should NOT set "._internal_defaults_do_not_set" explicitly, but rather directly set the fields internally. # For instance, instead of `--set _internal_defaults_do_not_set.foo=bar``, just set `--set foo=bar`. _internal_defaults_do_not_set: # Name allows overriding the release name. Generally this should not be set name: "" serviceAccount: # If set, a service account will be created. Otherwise, the default is used create: true # Annotations to add to the service account annotations: {} # The name of the service account to use. # If not set, the release name is used name: "istio-ingressgateway-service-account" podAnnotations: prometheus.io/port: "15020" prometheus.io/scrape: "true" prometheus.io/path: "/stats/prometheus" inject.istio.io/templates: "gateway" sidecar.istio.io/inject: "true" service: # Egress gateways do not need an external LoadBalancer IP so they set "service.type: ClusterIP". # Type of service. Set to "None" to disable the service entirely type: LoadBalancer ports: - name: http2 port: 80 protocol: TCP targetPort: 8080 - name: https port: 443 protocol: TCP targetPort: 8443 loadBalancerIP: "" loadBalancerSourceRanges: [] externalTrafficPolicy: "" externalIPs: [] ipFamilyPolicy: "" ipFamilies: [] ## Whether to automatically allocate NodePorts (only for LoadBalancers). # allocateLoadBalancerNodePorts: false resources: requests: cpu: 100m memory: 128Mi limits: cpu: 2000m memory: 1024Mi autoscaling: enabled: true minReplicas: 2 maxReplicas: 5 targetCPUUtilizationPercentage: 80 targetMemoryUtilizationPercentage: {} autoscaleBehavior: {} tolerations: - key: dedicated value: edge topologySpreadConstraints: [] affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - istio-ingressgateway topologyKey: kubernetes.io/hostname weight: 100 nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - preference: matchExpressions: - key: dedicated operator: In values: - edge weight: 100 -
You can manage the data plane, including these gateways. Start with the default configuration, which includes automatic patch updates, a preferred pod anti-affinity, and, toleration and preference for edge nodes. You are responsible for the customizations you make. Unlike the control plane
value.yamlfiles, you can edit thevalue.yamlfiles in these ConfigMaps.These are the resources created by the istio/gateway chart by using the default Ingress configuration. The same naming conventions are true for egress.
PodDisruptionBudget,Service,Deployment, andHorizontalPodAutoscalerare named inistio-ingressgatewayin theistio-systemnamespace. These names are set by thenamefields in thevalues.yaml.ServiceAccount,Role, andRolebindingare named inistio-ingressgateway-service-accountin theistio-systemnamespace. These names are set by theserviceAccount.namefield in thevalues.yaml.
-
Test the changes you want to make by first making them in the saved
gateway-values.yaml. Then use a dry run of Helm to see the manifest changes.Example:
Shown below are example changes. Only the changes are being shown; the rest of the
values.yamlcontent is unchanged. These example changes include:-
Changing the names of the resources
-
Adjusting resource requests/limits
-
Increasing autoscaling
-
Adding a node affinity
- If you are considering using node affinity to create zone affinities, you might also use
topologySpreadConstraintsinstead.
- If you are considering using node affinity to create zone affinities, you might also use
a. Revise the
values.yamlcontent as necessary.name: "custom-gateway" serviceAccount: name: "custom-ingressgateway-service-account" resources: requests: cpu: 100m memory: 128Mi limits: cpu: 2500m memory: 1024Mi autoscaling: enabled: true minReplicas: 3 maxReplicas: 7 targetCPUUtilizationPercentage: 80 targetMemoryUtilizationPercentage: {} autoscaleBehavior: {} affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: ibm-cloud.kubernetes.io/zone operator: In values: - "dal10"b. Use Helm with the
--dry-runoption to output a manifest so that you can confirm the syntax and that the configuration matches your intent.When you are targeting one of the default gateways, which are
istio-ingressgatewayoristio-egressgateway, only run this command with the--dry-runoption. Do not run this command without the--dry-runoption.helm upgrade istio-ingressgateway istio/gateway --version 1.24.0 --install -n istio-system -f gateway-values.yaml --dry-run -
-
If you are satisfied with the changes, then use
kubectl editto edit the gateway'svalues.yamlinside the ConfigMap it came from.a. Open
gateway-values.yamland indent the copy of thevalues.yamlfile by 4 spaces.b. Run the
kubectl editcommand.kubectl edit cm -n ibm-operators managed-istio-ingressgateway-valuesc. Delete the lines of the previous
values.yaml.d. Start the
values.yamlkey with a multiline string. Example:|e. Copy your 4 space indented
values.yamlfile into the lines below thevalues.yamlkey.Example:
data: values.yaml: | <Copy values.yaml here.> values.yaml.helm.result: | <Don't remove these previous Helm logs.> -
After about 10 minutes, check for an updated Helm log in the
values.yaml.helm.resultfield of that ConfigMap and debug as necessary.kubectl get cm -n ibm-operators managed-istio-ingressgateway-values -o json | jq -r .data.\"values.yaml.helm.result\"Example output:
GMT HELM_SUCCESS: Release "istio-ingressgateway" does not exist. Installing it now. NAME: istio-ingressgateway LAST DEPLOYED: Fri Sep 5 16:46:30 2025 NAMESPACE: istio-system STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: "istio-ingressgateway" successfully installed! To learn more about the release, try: $ helm status istio-ingressgateway -n istio-system $ helm get all istio-ingressgateway -n istio-system Next steps: * Deploy an HTTP Gateway: https://istio.io/latest/docs/tasks/traffic-management/ingress/ingress-control/ * Deploy an HTTPS Gateway: https://istio.io/latest/docs/tasks/traffic-management/ingress/secure-ingress/ -
View the configuration options.
a. Show the values.
helm show values istio/gateway --version 1.24.6b. Review the possible keys that could display.
name: # The gateway deployment's and service's name serviceAccount: name: # The service account, role, and rolebinding name resources: # Resource requests and limits autoscaling: # Min and Max gateway pods tolerations: # Tolerate your taints topologySpreadConstraints: # An alternative to node affinities affinity: # Where you can specify node affinities
Creating additional gateways
After customizing the default gateway that has one gateway deployment, you might want to configure additional gateways. Generate the resource manifest with Helm, then either apply it with Helm or with a CI/CD pipeline for YAML resources.
-
Run the
helm show valuescommand.helm show values "istio/gateway" --version "1.24.0" -
Create a
values.yamlfile for the gateway. The following example is a minimalistvalues.yamlfor an Istioingressgatewaybased on the options that are available in Istio 1.24.6.rbac: # If enabled, roles will be created to enable accessing certificates from Gateways. This is not needed # when using http://gateway-api.org/. enabled: true serviceAccount: # If set, a service account will be created. Otherwise, the default is used create: true # Define the security context for the pod. # If unset, this will be automatically set to the minimum privileges required to bind to port 80 and 443. # On Kubernetes 1.22+, this only requires the `net.ipv4.ip_unprivileged_port_start` sysctl. securityContext: runAsGroup: 1337 runAsNonRoot: true runAsUser: 1337 seccompProfile: type: RuntimeDefault service: # Egress gateways do not need an external LoadBalancer IP so they set "service.type: ClusterIP". # Type of service. Set to "None" to disable the service entirely type: LoadBalancer ports: - name: http2 port: 80 protocol: TCP targetPort: 8080 - name: https port: 443 protocol: TCP targetPort: 8443 autoscaling: enabled: true minReplicas: 2 maxReplicas: 5 # Deployment Update strategy strategy: rollingUpdate: maxSurge: 100% maxUnavailable: 25% tolerations: - key: dedicated value: edge affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - istio-ingressgateway topologyKey: kubernetes.io/hostname weight: 100 nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - preference: matchExpressions: - key: dedicated operator: In values: - edge weight: 100 podDisruptionBudget: minAvailable: 1 # Sets the per-pod terminationGracePeriodSeconds setting. terminationGracePeriodSeconds: 30 # Configure this to a higher priority class in order to make sure your Istio gateway pods # will not be killed because of low priority class. # Refer to https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass # for more detail. priorityClassName: ibm-app-cluster-criticalAlternatively you can use the default gateway's
values.yamlas a starting point.kubectl get cm -n ibm-operators managed-istio-ingressgateway-values -o json | jq -r .data.\"values.yaml\" -
When choosing a Helm release name and namespace, consider the following conditions.
- The Helm release name and namespace should match the gateway's deployment name and namespace.
- Avoid
istio-base,istiod,istio-ingressgateway, andistio-egressgatewaybecause the Istio managed add-on uses those release names. - Avoid using the release name of another of the additional gateways.
-
Do a dry run to see the manifest of the YAML resources for the gateway. For Istio 1.25.4 and earlier, you must use Helm v3.18.4.
helm upgrade --dry-run RELEASE_NAME istio/gateway --version ISTIO_VERSION --install -n NAMESPACE -f values.yaml -
Apply those resources with one of the following methods:
- Use the Helm
upgradecommand without the--dry-runoption. - Take the manifest of the YAML resources and apply it as you would other Istio data plane YAML, depending on your cluster's CI/CD use case.
- Use the Helm
Example customizations
Egress gateway
Egress gateways must have a service type of ClusterIP since they don't need a LoadBalancer IP.
service:
type: ClusterIP
Resource requests and limits
If a field is not specified, the Istio default values are used.
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 2000m
memory: 1024Mi
Autoscaling
If autoscaling.enabled=true is set, then you can set the minimum and maximum replicas for the horizontal pod autoscaler.
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
Graceful termination
Graceful termination gives the gateway extra time to handle existing connections while it is terminating. This feature replaces specifying the TERMINATION_DRAIN_DURATION environment variable. You can increase the value for this
setting, if needed.
# Sets the per-pod terminationGracePeriodSeconds setting.
terminationGracePeriodSeconds: 30
Zone affinity
Topology spread constraints can be set with the topologySpreadConstraints field. Depending on the use case, this solution can be
a better alternative to the previous zone affinity solution.
topologySpreadConstraints: []
Zone affinities can be specified by adding a service annotation and a node affinity.
service:
annotations:
service.kubernetes.io/ibm-load-balancer-cloud-provider-zone: "dal10"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: ibm-cloud.kubernetes.io/zone
operator: In
values:
- "dal10"
You can specify the loadBalancerIP.
Service.spec.loadBalancerIP was deprecated by Kubernetes in version 1.24. This option stops working when Kubernetes finishes removing the field. If you specify an IP that is already in use elsewhere on the cluster, then the service will have its external IP stay in pending.
service:
type: LoadBalancer
loadBalancerIP: ""
Pinning the Istio version
The Istio gateways have image: auto so that they will pick up the expected sidecar proxyv2 image on pod creation. This setting can be overridden by a pod annotation. If you use this override to pin the image tag,
you are responsible for updating that pin on each Istio patch and minor update.
podAnnotations:
"sidecar.istio.io/proxyImage": "icr.io/ext/istio/proxyv2:1.24.0"
Disabling the gateway
You can disable the service by changing its type to None. You can also scale down the gateway deployment. In Istio 1.24 and 1.25, there is an issue where replicaCount has a minimum of 1. In Istio 1.26.0
and later, you can set the replicaCount to 0. When the service type for ingressgateway is changed from LoadBalancer to None, its LoadBalancer IP is eventually relinquished.
If the service type was changed back to LoadBalancer, a new IP is assigned.
replicaCount: 0
service:
type: None
autoscaling:
enabled: false
Removing gateway deployments
If you enabled istio-ingressgateway-public-2, istio-ingressgateway-public-3, or have any other custom gateway that you want to remove, locate and delete these resources.
-
Locate the gateways. If the gateway was installed with Helm, you can use
helm get all RELEASE_NAME -n NAMESPACEas a shortcut.kubectl get PodDisruptionBudget -n NAMESPACE GATEWAY_NAME --ignore-not-found kubectl get Service -n NAMESPACE GATEWAY_NAME --ignore-not-found kubectl get Deployment -n NAMESPACE GATEWAY_NAME --ignore-not-found kubectl get HorizontalPodAutoscaler -n NAMESPACE GATEWAY_NAME --ignore-not-found kubectl get ServiceAccount -n NAMESPACE --ignore-not-found | grep GATEWAY_NAME kubectl get Role -n NAMESPACE --ignore-not-found | grep GATEWAY_NAME kubectl get RoleBinding -n NAMESPACE --ignore-not-found | grep GATEWAY_NAME -
Remove the gateways. If the gateway was installed with Helm, you can use
helm uninstall RELEASE_NAME -n NAMESPACEas a shortcut.Example for removing
istio-ingressgateway-public-2in theistio-systemnamespace:kubectl delete PodDisruptionBudget -n istio-system istio-ingressgateway-public-2 --ignore-not-found kubectl delete Service -n istio-system istio-ingressgateway-public-2 --ignore-not-found kubectl delete Deployment -n istio-system istio-ingressgateway-public-2 --ignore-not-found kubectl delete HorizontalPodAutoscaler -n istio-system istio-ingressgateway-public-2 --ignore-not-found kubectl delete ServiceAccount -n istio-system istio-ingressgateway-public-2-service-account --ignore-not-found kubectl delete Role -n istio-system istio-ingressgateway-public-2-sds --ignore-not-found kubectl delete RoleBinding -n istio-system istio-ingressgateway-public-2-sds --ignore-not-foundExample output:
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE istio-ingressgateway-public-2 N/A N/A 0 2m33s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE istio-ingressgateway-public-2 LoadBalancer 172.21.227.3 169.46.62.156 80:32705/TCP,443:31154/TCP 2m32s NAME READY UP-TO-DATE AVAILABLE AGE istio-ingressgateway-public-2 2/2 2 2 2m33s NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE istio-ingressgateway-public-2 Deployment/istio-ingressgateway-public-2 cpu: <unknown>/80% 2 5 2 2m34s istio-ingressgateway-public-2-service-account 0 2m34s istio-ingressgateway-public-2-sds 2025-09-09T17:20:46Z istio-ingressgateway-public-2-sds Role/istio-ingressgateway-public-2-sds 2m34s