Gateway installation guide for EKS
This guide exposes the Agent Router data plane in an Amazon Elastic Kubernetes Service (EKS) cluster using the tare command-line utility. The install drives a Network Load Balancer through the egress EnvoyProxy, so the endpoint answers on a customer-facing hostname without an AWS Load Balancer Controller and without the aws CLI.
By the end of this guide the cluster has:
- The egress EnvoyProxy (
tars-egress-proxyintars-system) patched to expose its service as a LoadBalancer, annotated for a Network Load Balancer. - The
egressService intars-dataplanereporting an NLB hostname. - A customer-facing DNS
CNAMErecord pointing at that NLB hostname.
Plan for 10 to 20 minutes end-to-end. The NLB itself usually publishes a hostname within a minute of the patch.
Architecture
┌───────────────────────────────────────────┐
│ proxy.<your-domain> ← customer DNS │
│ │ (CNAME) │
│ ▼ │
│ ┌────────────────┐ │
│ │ Network Load │ ← provisioned by the │
│ │ Balancer │ in-tree EKS cloud │
│ └────────┬───────┘ provider │
│ │ │
│ ┌────────▼───────┐ │
│ │ egress Service │ ← reconciled by │
│ │ (tars-dataplane)│ Envoy Gateway from │
│ └────────┬───────┘ the EnvoyProxy │
│ │ │
│ ┌────────▼───────┐ │
│ │ Agent Router │ │
│ │ Dataplane │ ← installed earlier │
│ └────────────────┘ via tare CLI │
└───────────────────────────────────────────┘
This is a layer 4 path. The tare utility patches one resource: the egress EnvoyProxy. It sets spec.provider.kubernetes.envoyService.type to LoadBalancer and adds service.beta.kubernetes.io/aws-load-balancer-type: nlb. Envoy Gateway then reconciles the egress Service, and the in-tree EKS cloud provider provisions the NLB from the annotation. The operator provides one customer-facing DNS record at the top.
Driving the load balancer through the EnvoyProxy is what makes the exposure durable. The egress Service is owned by Envoy Gateway, so a direct Service patch reverts to ClusterIP on the next reconcile; the EnvoyProxy setting is re-asserted across pod restarts, Envoy Gateway restarts, and tare upgrade.
Choose an exposure path:
- NLB via
tare(this guide's default). Use when an EKS cluster already has the data plane installed, a layer 4 endpoint is enough, and TLS terminates upstream of the load balancer or at the client. Smoke-tests use plain HTTP on port10080. This path is not sufficient for membership-probe DNS failover, which requires HTTPS on port 443. - NLB with ACM TLS on the load balancer. Keep the NLB shape and terminate TLS with an ACM certificate via the AWS Load Balancer Controller — see Optional: terminate TLS on the NLB. Use this when you need HTTPS at the edge on layer 4, including any project fronted by health-checked DNS failover (Gateway sets and DNS-level failover).
- ALB with ACM TLS. Use the AWS data plane installation guide when layer 7 ingress is required, or when you prefer an ALB Ingress with ACM (steps 10 and 11 install the AWS Load Balancer Controller). Also use that guide when the data plane is not yet installed — gateway install needs the egress EnvoyProxy to exist before it can patch it.
These are alternative exposure shapes, not a required sequence. For high-availability gateway sets, prefer option 2 (or an equivalent HTTPS-on-443 front for each member).
Prerequisites
tareCLIv0.1.0-beta.2or later. Install or upgrade withcurl -fsSL https://tare.tetrate.ai/tools/install.sh | bash. The binary lands at~/.tare/bin/tare; add it toPATH.kubectlpointed at the EKS cluster (aws eks update-kubeconfig --region $REGION --name $CLUSTER); confirm withkubectl config current-context.- Data plane already installed, so the egress EnvoyProxy exists:
kubectl get envoyproxy tars-egress-proxy -n tars-systemreturns a resource. identity.json(the data plane credential and service-account file). The same file used fortare install.- Kubernetes RBAC allowing
patchonenvoyproxies.gateway.envoyproxy.iointars-system. The pre-flight runskubectl auth can-iand fails with a pointer to the required permission when it is denied.
No aws CLI, no AWS Load Balancer Controller, and no IAM policy work are required for this path. The --apply-prereqs and --ack-prereqs flags belong to the GCP and Azure paths and do not apply here.
Credential-less upstream authentication to Bedrock is a separate concern from ingress. See Bedrock workload identity for IRSA and EKS Pod Identity.
Conventions
The commands below assume the following environment variables are exported in the current shell:
export SERVE_DOMAIN="proxy.acme.example.com"
export REGION="<eks-region>"
export CLUSTER="<eks-cluster-name>"
Each step assumes these variables remain set; export them again after opening a new shell.
Step 1: preview the patch
tare gateway install identity.json --type aws --plan-only
The Plan view lists the EnvoyProxy that will be patched, the annotations that will be applied, and the Service that Envoy Gateway will reconcile into an NLB. Neither --plan-only nor --print-resources touches the cluster, so both are safe to run first.
To see the exact merge patch instead of the summary:
tare gateway install identity.json --type aws --print-resources
Unlike the GCP and Azure paths, --type aws takes no config file. It is configured entirely by flags.
Step 2: expose the egress as an NLB
tare gateway install identity.json --type aws --wait
--wait polls the egress Service until the cloud provider publishes a hostname; NLBs surface a hostname rather than an IP address. Add --yes to skip the Proceed? prompt, which is required in non-TTY contexts such as CI. The wait timeout defaults to 10m and is set with --timeout.
For an internal NLB that is reachable only from inside the VPC:
tare gateway install identity.json --type aws --aws-internal --wait
This adds service.beta.kubernetes.io/aws-load-balancer-internal: true to the same patch.
Install sequence
The install proceeds through the following stages:
- Plan and confirm - the plan prints before any cluster access, then the CLI prompts unless
--yesis passed. - Pre-flight
kubectland cluster - verifieskubectlis available and the cluster is reachable. - Pre-flight EnvoyProxy - confirms
tars-egress-proxyexists intars-systemand that the current context can patch it. A missing resource fails with an "is the data plane installed?" hint. - Patch the EnvoyProxy - a JSON merge patch sets
envoyService.type=LoadBalancerand the NLB annotations, preserving the rest ofprovider.kubernetes. --wait- pollsstatus.loadBalancer.ingress[0].hostnameon theegressService until the NLB hostname appears.- Completion output - prints the DNS CNAME action and a smoke-test command.
Flags for the AWS path
| Flag | Default | Description |
|---|---|---|
--aws-internal | false | Provision an internal NLB (adds the aws-load-balancer-internal annotation) |
--aws-service | egress | Egress Service that Envoy Gateway exposes as an NLB |
--aws-envoyproxy | tars-egress-proxy | Egress EnvoyProxy resource to drive the NLB through |
Step 3: wire DNS and verify
Read the NLB hostname from the Service:
kubectl get service egress -n tars-dataplane \
-o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
# k8s-tarsdata-egress-xxxxxxxxxx-yyyyyyyyy.elb.us-east-1.amazonaws.com
Point the serve hostname at that value with a CNAME record at your DNS provider:
proxy.acme.example.com CNAME k8s-tarsdata-egress-xxxxxxxxxx-yyyyyyyyy.elb.us-east-1.amazonaws.com
Smoke-test the endpoint on the egress port:
curl -sk -o /dev/null -w 'HTTP %{http_code}\n' \
http://<nlb-hostname>:10080/v1/chat/completions
# expected: HTTP 401
401 is the success signal: the request reached the tars-proxy filter, which is asking for an API key. 200 means the filter is not loaded, and 502 means the load balancer health check is failing.
Optional: terminate TLS on the NLB
The layer 4 path above leaves TLS to whatever sits in front of the load balancer, and the ALB path terminates it at a layer 7 ingress. There is a third option: keep the NLB and have it terminate TLS with an ACM certificate.
This suits deployments that want a layer 4 endpoint but still need HTTPS at the edge — including any project fronted by health-checked DNS failover, since the membership probe those health checks target is an HTTPS-on-443 contract.
It requires the AWS Load Balancer Controller, which reads these annotations; the in-tree provider the tare path uses does not claim Services annotated external. Set them on the egress EnvoyProxy in Helm values, never on the Service directly — Envoy Gateway owns the Service and reverts direct edits:
envoyProxy:
service:
type: LoadBalancer
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: external
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: <CERT_ARN>
service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "<service-port>"
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp
The annotations map is passed through verbatim, so any annotation the controller supports can be set the same way.
ssl-ports must name a port the Service exposesEnvoy Gateway derives the Service's ports from the Gateway's listeners, and the egress Gateway ships exactly one — HTTP on 10080. So out of the box the Service exposes only 10080, and ssl-ports: "443" matches nothing: the load balancer comes up with no TLS listener at all.
Check what the Service actually exposes before setting the annotation:
kubectl get service egress -n tars-dataplane \
-o jsonpath='{range .spec.ports[*]}{.name} {.port}->{.targetPort}{"\n"}{end}'
Publishing on 443 — which the membership probe requires, since its Host match is portless — therefore needs the Gateway to expose a 443 listener so the Service carries that port. That is a change to the egress Gateway's listener set, and its shape depends on your release: confirm it with your Tetrate field engineer rather than deriving it.
Certificates are regional, and a project served by several gateways needs each member's certificate to cover both the member's own hostname and the shared client-facing name. Plan that before the second gateway exists — see Gateway sets and DNS-level failover.
Troubleshooting
EnvoyProxy "tars-egress-proxy" not found
The data plane is not installed in the current context, or the EnvoyProxy is named differently. Run tare install first, or pass the actual name:
tare gateway install identity.json --type aws --aws-envoyproxy <name> --wait
Egress service stays ClusterIP after patching
Confirm the EnvoyProxy carries the setting:
kubectl get envoyproxy tars-egress-proxy -n tars-system \
-o jsonpath='{.spec.provider.kubernetes.envoyService.type}'
# expected: LoadBalancer
When the EnvoyProxy reads LoadBalancer but the Service does not, Envoy Gateway has not reconciled it. Check the envoy-gateway controller logs.
NLB hostname never appears
Inspect the Service events for provisioning errors:
kubectl describe service egress -n tars-dataplane
The in-tree cloud provider discovers subnets by tag. Missing or incorrect kubernetes.io/role/elb subnet tags are the usual cause.
Kubernetes RBAC denies the patch
The pre-flight reports that the current context cannot patch envoyproxies.gateway.envoyproxy.io. Grant a role with patch on that resource in tars-system and rerun.
Notes
- The exposure is durable. Because Envoy Gateway owns the
egressService through the EnvoyProxy, the NLB is re-asserted across pod restarts, Envoy Gateway restarts, andtare upgrade. - The install is idempotent. Re-running applies the same merge patch, so it is safe to repeat after changing
--aws-internal. - This path handles ingress only. Upstream provider credentials, including credential-less Bedrock access, are configured separately.
Where to go next