--- # ============================================================================= # Envoy Gateway — Install via Helm + Gateway resource # ============================================================================= - name: "envoy_gateway | Check for existing Helm release" kubernetes.core.helm_info: name: envoy-gateway release_namespace: "{{ envoy_gateway_namespace }}" register: _eg_release failed_when: false - name: "envoy_gateway | Already installed — skipping" ansible.builtin.debug: msg: "Envoy Gateway release already deployed in {{ envoy_gateway_namespace }} — skipping install." when: _eg_release.status is defined and _eg_release.status is not none - name: "envoy_gateway | Install" when: _eg_release.status is not defined or _eg_release.status is none block: - name: "envoy_gateway | Install Envoy Gateway via Helm (OCI) — standalone" kubernetes.core.helm: name: envoy-gateway chart_ref: "{{ envoy_gateway_helm_repo }}" release_namespace: "{{ envoy_gateway_namespace }}" create_namespace: true chart_version: "v{{ envoy_gateway_version }}" wait: "{{ helm_wait }}" timeout: "{{ helm_timeout }}" values: deployment: envoyGateway: affinity: "{{ _platform_affinity | default({}) }}" - name: "envoy_gateway | Wait for controller deployment" kubernetes.core.k8s_info: api_version: apps/v1 kind: Deployment name: envoy-gateway namespace: "{{ envoy_gateway_namespace }}" register: _eg_deploy until: - _eg_deploy.resources | length > 0 - _eg_deploy.resources[0].status.conditions | default([]) | selectattr('type','eq','Available') | selectattr('status','eq','True') | list | length > 0 retries: 60 delay: 5 # ── Gateway namespaces ──────────────────────────────────────────────────────── - name: "envoy_gateway | Label envoy-gateway-system namespace — Istio" kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Namespace metadata: name: "{{ envoy_gateway_namespace }}" labels: istio.io/dataplane-mode: "ambient" when: istio_enabled | default(false) | bool # The eg-gateway proxy is the north-south mesh edge: external clients terminate TLS # here with no mesh identity, so it must accept plaintext inbound. Selector-scoped so # the internal ai-gateway proxy (same namespace, reached in-mesh) stays STRICT. - name: "envoy_gateway | Allow external ingress at eg-gateway edge (mTLS PERMISSIVE)" kubernetes.core.k8s: state: present definition: apiVersion: security.istio.io/v1 kind: PeerAuthentication metadata: name: eg-gateway-edge namespace: "{{ envoy_gateway_namespace }}" spec: selector: matchLabels: gateway.envoyproxy.io/owning-gateway-name: "{{ gateway_name }}" mtls: mode: PERMISSIVE when: istio_enabled | default(false) | bool # Host-network apiserver (no mesh identity) can't reach the envoy-gateway controller's # admission webhook under STRICT; open only its port, rest of the pod stays STRICT. - name: "envoy_gateway | Allow apiserver to reach controller webhook (port-level mTLS)" kubernetes.core.k8s: state: present definition: apiVersion: security.istio.io/v1 kind: PeerAuthentication metadata: name: allow-webhook-ctrl namespace: "{{ envoy_gateway_namespace }}" spec: selector: matchLabels: control-plane: envoy-gateway mtls: mode: STRICT portLevelMtls: "{{ {'9443': {'mode': 'PERMISSIVE'}} }}" when: istio_enabled | default(false) | bool - name: "envoy_gateway | Label envoy-gateway-system namespace — PSS" kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Namespace metadata: name: "{{ envoy_gateway_namespace }}" labels: pod-security.kubernetes.io/enforce: "privileged" pod-security.kubernetes.io/audit: "privileged" pod-security.kubernetes.io/warn: "privileged" when: enforce_pss | default(false) | bool - name: "envoy_gateway | Create gateway namespace (if different)" kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Namespace metadata: name: "{{ gateway_namespace }}" when: gateway_create and gateway_namespace != envoy_gateway_namespace - name: "envoy_gateway | Label gateway namespace — Istio" kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Namespace metadata: name: "{{ gateway_namespace }}" labels: istio.io/dataplane-mode: "ambient" when: - gateway_create - gateway_namespace != envoy_gateway_namespace - istio_enabled | default(false) | bool - name: "envoy_gateway | Label gateway namespace — PSS" kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Namespace metadata: name: "{{ gateway_namespace }}" labels: pod-security.kubernetes.io/enforce: "privileged" pod-security.kubernetes.io/audit: "privileged" pod-security.kubernetes.io/warn: "privileged" when: - gateway_create - gateway_namespace != envoy_gateway_namespace - enforce_pss | default(false) | bool # ── TLS serving certificate ─────────────────────────────────────────────────── - name: "envoy_gateway | Provision TLS serving certificate" ansible.builtin.include_tasks: tls.yaml # ── Gateway ─────────────────────────────────────────────────────────────────── # envoy_gateway | Data-plane workload: DaemonSet across all control-plane nodes # ────────────────────────────────────────────────────────────────────────── # MetalLB's L2 speaker announces the VIP from a single elected leader node # (metallb auto-pool uses a control-plane IP). For LB packets to reach an # envoy pod we need an envoy on that leader. A DaemonSet scoped to CP nodes # is the simplest way to guarantee that without caring *which* CP node # MetalLB elects at any given moment: every CP node always has a local # envoy. Survives CP failover, no hostname pinning, no cp[0] dance. # # nodeSelector keys on the standard control-plane label (empty string is # how kubespray / kubeadm stamp it) rather than the taint, because # operators can remove the taint; the label is the stable Kubernetes # control-plane marker. Toleration is kept for the case where the taint # is still in place. - name: "envoy_gateway | Create EnvoyProxy config (DaemonSet on control-plane nodes)" kubernetes.core.k8s: state: present definition: apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyProxy metadata: name: eg-proxy-config namespace: "{{ envoy_gateway_namespace }}" spec: provider: type: Kubernetes kubernetes: envoyDaemonSet: pod: nodeSelector: node-role.kubernetes.io/control-plane: "" tolerations: - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule - name: "envoy_gateway | Create GatewayClass" kubernetes.core.k8s: state: present definition: apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass metadata: name: eg spec: controllerName: gateway.envoyproxy.io/gatewayclass-controller parametersRef: group: gateway.envoyproxy.io kind: EnvoyProxy name: eg-proxy-config namespace: "{{ envoy_gateway_namespace }}" - name: "envoy_gateway | Apply Gateway resource" kubernetes.core.k8s: state: present definition: "{{ lookup('ansible.builtin.template', 'gateway.yaml.j2') | from_yaml_all | list }}" when: gateway_create - name: "envoy_gateway | Wait for Gateway to be accepted" kubernetes.core.k8s_info: api_version: gateway.networking.k8s.io/v1 kind: Gateway name: "{{ gateway_name }}" namespace: "{{ gateway_namespace }}" register: _gw_status until: - _gw_status.resources | length > 0 - _gw_status.resources[0].status.conditions | default([]) | selectattr('type','eq','Accepted') | selectattr('status','eq','True') | list | length > 0 retries: 60 delay: 5 when: gateway_create # ── BackendTrafficPolicy — default request timeout ─────────────────────────── - name: "envoy_gateway | Apply gateway-default BackendTrafficPolicy (timeout)" kubernetes.core.k8s: state: present definition: "{{ lookup('ansible.builtin.template', 'backend-traffic-policy.yaml.j2') | from_yaml }}" when: gateway_create # ── SecurityPolicy (auth) ───────────────────────────────────────────────────── - name: "envoy_gateway | Apply SecurityPolicy ({{ auth_provider }})" kubernetes.core.k8s: state: present definition: "{{ lookup('ansible.builtin.template', 'security-policy.yaml.j2') | from_yaml_all | list }}" when: auth_provider in ['keycloak', 'litellm'] and gateway_create - name: "envoy_gateway | Apply no-auth SecurityPolicy overrides" kubernetes.core.k8s: state: present definition: "{{ lookup('ansible.builtin.template', 'security-policy-no-auth.yaml.j2') | from_yaml_all | list }}" when: auth_provider in ['keycloak', 'litellm'] and gateway_no_auth_routes | length > 0 and gateway_create # ServiceMonitors require Prometheus Operator CRDs which are installed by the # observability role (runs after envoy_gateway). Skip silently if not yet present. - name: "envoy_gateway | Check if Prometheus Operator CRDs are available" kubernetes.core.k8s_info: api_version: apiextensions.k8s.io/v1 kind: CustomResourceDefinition name: servicemonitors.monitoring.coreos.com register: _eg_sm_crd failed_when: false - name: "envoy_gateway | Create ServiceMonitor for Envoy Gateway metrics" when: (_eg_sm_crd.resources | default([])) | length > 0 kubernetes.core.k8s: state: present definition: apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: envoy-gateway namespace: "{{ envoy_gateway_namespace }}" labels: app: envoy-gateway spec: selector: matchLabels: app.kubernetes.io/name: gateway-helm namespaceSelector: matchNames: - "{{ envoy_gateway_namespace }}" endpoints: - port: metrics path: /metrics interval: 30s - name: "envoy_gateway | ServiceMonitor skipped (Prometheus Operator not yet installed)" when: (_eg_sm_crd.resources | default([])) | length == 0 ansible.builtin.debug: msg: "Prometheus Operator CRDs not found — Envoy Gateway ServiceMonitor will be created by the observability role." - name: "envoy_gateway | Install complete" ansible.builtin.debug: msg: >- Envoy Gateway v{{ envoy_gateway_version }} installed in {{ envoy_gateway_namespace }}. Gateway '{{ gateway_name }}' created with TLS ({{ gateway_tls_mode }}){{ ' + auth (' + auth_provider + ')' if auth_provider != 'none' else '' }}.