# Gateway installation guide for EKS

> Expose the Agent Router data plane on Amazon EKS as a Network Load Balancer with the tare CLI, without the AWS Load Balancer Controller.

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 <code>aws</code> CLI.

By the end of this guide the cluster has:

- The egress EnvoyProxy (`tars-egress-proxy` in `tars-system`) patched to expose its service as a LoadBalancer, annotated for a Network Load Balancer.
- The `egress` Service in `tars-dataplane` reporting an NLB hostname.
- A customer-facing DNS `CNAME` record 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

```text
┌───────────────────────────────────────────┐
│  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`.

:::info
Choose an exposure path:

1. **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 port `10080`. This path is **not** sufficient for membership-probe DNS failover, which requires HTTPS on port 443.
1. **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](#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](/agent-router-enterprise/guides/operate-and-govern/project-and-data-plane-management/gateway-sets-and-dns-failover)).
1. **ALB with ACM TLS.** Use the [AWS data plane installation guide](/agent-router-enterprise/self-hosted-data-plane/data-plane-installation/installation-guide-aws) 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

- `tare` CLI `v0.1.0-beta.2` or later. Install or upgrade with `curl -fsSL https://tare.tetrate.ai/tools/install.sh | bash`. The binary lands at `~/.tare/bin/tare`; add it to `PATH`.
- `kubectl` pointed at the EKS cluster (`aws eks update-kubeconfig --region $REGION --name $CLUSTER`); confirm with `kubectl config current-context`.
- Data plane already installed, so the egress EnvoyProxy exists: `kubectl get envoyproxy tars-egress-proxy -n tars-system` returns a resource.
- `identity.json` (the data plane credential and service-account file). The same file used for `tare install`.
- Kubernetes RBAC allowing `patch` on `envoyproxies.gateway.envoyproxy.io` in `tars-system`. The pre-flight runs `kubectl auth can-i` and 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.

:::note
Credential-less upstream authentication to Bedrock is a separate concern from ingress. See [Bedrock workload identity](/agent-router-enterprise/guides/operate-and-govern/provision-models-and-providers/bedrock-workload-identity) for IRSA and EKS Pod Identity.
:::

## Conventions

The commands below assume the following environment variables are exported in the current shell:

```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

```shell
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:

```shell
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

```shell
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:

```shell
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:

1. **Plan and confirm** - the plan prints before any cluster access, then the CLI prompts unless `--yes` is passed.
1. **Pre-flight `kubectl` and cluster** - verifies `kubectl` is available and the cluster is reachable.
1. **Pre-flight EnvoyProxy** - confirms `tars-egress-proxy` exists in `tars-system` and that the current context can patch it. A missing resource fails with an "is the data plane installed?" hint.
1. **Patch the EnvoyProxy** - a JSON merge patch sets `envoyService.type=LoadBalancer` and the NLB annotations, preserving the rest of `provider.kubernetes`.
1. **`--wait`** - polls `status.loadBalancer.ingress[0].hostname` on the `egress` Service until the NLB hostname appears.
1. **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:

```shell
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:

```text
proxy.acme.example.com  CNAME  k8s-tarsdata-egress-xxxxxxxxxx-yyyyyyyyy.elb.us-east-1.amazonaws.com
```

Smoke-test the endpoint on the egress port:

```shell
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](/agent-router-enterprise/self-hosted-data-plane/data-plane-installation/installation-guide-aws#appendix-a-tls-via-acm--https-listener) 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:

```yaml
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.

:::warning `ssl-ports` must name a port the Service exposes
Envoy 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:

```shell
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](/agent-router-enterprise/guides/operate-and-govern/project-and-data-plane-management/gateway-sets-and-dns-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:

```shell
tare gateway install identity.json --type aws --aws-envoyproxy <name> --wait
```

### Egress service stays `ClusterIP` after patching

Confirm the EnvoyProxy carries the setting:

```shell
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:

```shell
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 `egress` Service through the EnvoyProxy, the NLB is re-asserted across pod restarts, Envoy Gateway restarts, and `tare 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

  <Link to="/agent-router-enterprise/guides/operate-and-govern/provision-models-and-providers/bedrock-workload-identity" className="tare-nav-card">
    Bedrock workload identity
    Give the data plane credential-less access to Bedrock with IRSA or EKS Pod Identity.
  </Link>
  <Link to="/agent-router-service/quickstarts/quickstart-console" className="tare-nav-card">
    Console quickstart
    Issue an API key and confirm a first routed request through the new endpoint.
  </Link>
