Skip to content
Published on

The Complete Guide to Emissary-ingress (Ambassador): Everything About API Gateway-style Ingress

Authors

Introduction

When exposing services from Kubernetes to the outside world, Ingress is usually the first thing that comes to mind. But once you actually run things in production, a moment arrives when you need more than simple path-based routing. You want to attach authentication, apply rate limits, route only 5 percent of traffic to a new version via a canary release, branch routing based on headers, and handle gRPC and WebSocket at the same time.

As these requirements pile up, you eventually arrive at the question: "Isn't what I actually need an API Gateway rather than an Ingress?" Emissary-ingress (formerly Ambassador) is a project that starts exactly at that point. As a CNCF incubating project, it uses Envoy Proxy as its data plane and provides API Gateway features in a Kubernetes-native way.

As of 2026, the ingress ecosystem is going through a major inflection point. The networking Ingress API is effectively frozen, meaning no new features are being added to it. Gateway API has established itself as the successor standard, and ingress-nginx, once the most widely used controller, is trending toward something close to maintenance mode due to security issues and a shortage of maintainers. In this situation, understanding a modern Envoy-based controller is increasingly important.

In this article, we will dissect the architecture of Emissary-ingress, take a deep look at its core resource the Mapping CRD, and cover authentication, rate limiting, traffic management, canary releases, Gateway API support, hands-on deployment, operational tuning, and the common pitfalls people fall into, all in one place.

What Is Emissary-ingress

Emissary-ingress is an open-source API Gateway and Ingress controller for Kubernetes. Let us first lay out its key characteristics.

  • It uses Envoy Proxy as its data plane. In other words, the battle-tested Envoy handles the actual traffic.
  • It declares all configuration via Kubernetes CRDs (custom resources). Mapping, Host, Listener, and AuthService are representative examples.
  • Rather than annotation-based configuration, it defines routing through independent resources, so a decentralized configuration model in which each microservice team owns its own Mapping comes naturally.
  • It provides API Gateway features such as gRPC, WebSocket, HTTP/2, HTTP/3, TLS termination, authentication, and rate limiting out of the box.

Historically, this project started from Ambassador, built by Datawire (now Ambassador Labs). Later, when the open-source core was donated to the CNCF, it was renamed Emissary-ingress, while the commercial version, named Ambassador Edge Stack (AES), additionally provides a developer portal, advanced rate limiting, OAuth/OIDC filters, a web application firewall, and more.

Positioning at a Glance

AspectPlain Ingress controllerEmissary-ingress
Primary useL7 path routing, TLS terminationAPI Gateway, path routing, traffic management
Configuration styleIngress resource plus annotationsDedicated CRDs (Mapping, etc.)
Data planeVaries by controllerEnvoy Proxy
AuthenticationRequires separate setupBuilt in via AuthService
Rate limitingLimitedBuilt in via RateLimitService
CanaryDifficultBuilt in, weight based
Gateway APIVaries by controllerSupported

Architecture: Separating the Control Plane and the Data Plane

To truly understand Emissary-ingress, you first need to grasp the separation of the control plane and the data plane. Many plain Ingress controllers render a configuration file directly and restart the proxy, but Emissary leverages Envoy's xDS (dynamic discovery) protocol.

                    +-----------------------------+
                    |   Kubernetes API Server      |
                    |  (Mapping, Host, Listener...) |
                    +--------------+--------------+
                                   | watch (CRD)
                                   v
              +----------------------------------------+
              |        Emissary Pod                     |
              |  +----------------+   +--------------+   |
              |  |  controller    |   |   Envoy      |   |
              |  | (CRD -> snapshot|-->| (data plane) |  |
              |  |  -> Envoy conf) |xDS|              |  |
              |  +----------------+   +------+-------+   |
              +--------------------------------|--------+
                                               | L7 traffic
                  client ---- :8080/:8443 ------+----> upstream services

Breaking the flow into steps looks like this.

  1. The user creates CRDs such as Mapping and Host with kubectl apply.
  2. The Emissary controller watches the Kubernetes API server and detects changes.
  3. The controller gathers all resources into a single consistent snapshot and converts it into Envoy configuration.
  4. The converted configuration is delivered to the Envoy data plane via the xDS (or V3 ADS) protocol.
  5. Envoy applies the new configuration without downtime and handles the actual client traffic.

Thanks to this structure, you can update routing rules when configuration changes without restarting the entire proxy process. The data plane (Envoy) focuses solely on traffic handling, while the control plane focuses on translating the declared intent into a form Envoy understands.

The Core Resource Hierarchy

In Emissary 3.x, the path that incoming traffic takes is composed by the following resources working together.

  • Listener: defines which port and protocol Envoy receives requests on (for example, 8080 HTTP, 8443 HTTPS).
  • Host: ties together a domain (hostname), TLS settings, and certificates. Automatic ACME issuance is handled here too.
  • Mapping: the actual routing rule. It defines which service a path prefix is sent to.
  • AuthService / RateLimitService: define the behavior of external filters.

Thanks to this separation, "which port do we listen on (Listener)," "which domain do we handle (Host)," and "which path goes where (Mapping)" are cleanly divided.

A Deep Look at the Mapping CRD

Mapping is the heart of Emissary. Let us start with its most basic form.

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: quote-backend
  namespace: default
spec:
  hostname: "*"
  prefix: /backend/
  service: quote

This Mapping sends every request starting with /backend/ to the quote service. The hostname: "*" means it applies to all domains. The service name points to a Kubernetes Service in the same namespace, and you can specify other namespaces with the quote.namespace form.

prefix and Regex Routing

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: regex-route
spec:
  hostname: "*"
  prefix: "/user/[0-9]+/profile"
  prefix_regex: true
  service: user-service

If you set prefix_regex: true, the prefix is interpreted as a regular expression. However, regex routing has a performance cost, so it is best used only when truly necessary.

Header and Query Parameter Based Routing

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: canary-by-header
spec:
  hostname: "*"
  prefix: /api/
  service: api-v2
  headers:
    x-api-version: "v2"

The Mapping above sends only requests carrying the x-api-version: v2 header to api-v2. Requests without the header flow to a different Mapping handling the same prefix. This kind of header-based branching is useful for gradual migrations or for separating internal test traffic.

Path Rewriting and Host Rewriting

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: rewrite-example
spec:
  hostname: "*"
  prefix: /legacy/
  rewrite: /v3/
  host_rewrite: internal.example.com
  service: modern-service

A /legacy/foo request is delivered to the upstream as /v3/foo, and the Host header is changed to internal.example.com. This is a common pattern when you keep the legacy path but swap out only the backend.

Timeouts, Retries, and Circuit Breakers

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: resilient-route
spec:
  hostname: "*"
  prefix: /orders/
  service: orders
  timeout_ms: 4000
  connect_timeout_ms: 1500
  retry_policy:
    retry_on: "5xx"
    num_retries: 3
  circuit_breakers:
    - max_connections: 2048
      max_pending_requests: 1024
      max_requests: 2048

Envoy's powerful resilience features are exposed directly as Mapping fields. retry_on defines under which conditions to retry (5xx, gateway-error, connect-failure, and so on), and circuit_breakers sets the thresholds that prevent upstream overload.

Authentication: AuthService

Emissary handles authentication through Envoy's ext_authz (external authorization) filter. When you create an AuthService resource, all (or specific) requests are first forwarded to the authentication service before they reach the upstream.

apiVersion: getambassador.io/v3alpha1
kind: AuthService
metadata:
  name: authentication
  namespace: default
spec:
  auth_service: "auth-service:3000"
  proto: http
  path_prefix: "/extauth"
  allowed_request_headers:
    - "x-request-id"
    - "authorization"
  allowed_authorization_headers:
    - "x-user-id"
    - "x-user-role"

The behavior is as follows.

  1. When a request arrives, Emissary sends an authentication request to auth-service:3000 before processing the body.
  2. If the authentication service returns 200 OK, the request passes through and the response headers (such as x-user-id) are forwarded to the upstream.
  3. If the authentication service returns 401/403, the request is blocked right there.

To make only a specific Mapping skip authentication, add bypass_auth: true to that Mapping. In the commercial Edge Stack, you can declaratively attach OAuth2/OIDC as a Filter resource, so you do not have to implement a separate authentication service yourself.

Rate Limiting: RateLimitService

Rate limiting also leverages Envoy's ext filter mechanism. First, you register an external rate limit service with a RateLimitService.

apiVersion: getambassador.io/v3alpha1
kind: RateLimitService
metadata:
  name: ratelimit
  namespace: default
spec:
  service: "ratelimit-service:8081"
  protocol_version: v3
  domain: emissary

Then, in the Mapping, you specify with labels which dimension (descriptor) the rate limit is applied to.

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: rate-limited-api
spec:
  hostname: "*"
  prefix: /api/
  service: api
  labels:
    emissary:
      - request_label_group:
          - remote_address:
              key: remote_address

Here, the group under labels is converted into an Envoy rate limit descriptor and forwarded to the external rate limit service. The rate limit service manages a counter per descriptor and is configured to return 429 once the limit is exceeded. Open-source Emissary uses the Envoy ratelimit service (with a Redis backend) as is, while Edge Stack provides an integrated rate limiting in which limits are declared as CRDs.

Traffic Management and Canary Releases

The real value of an API Gateway-style Ingress shows up in traffic management. If you assign weight to two Mappings handling the same prefix, you get weight-based traffic splitting.

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: app-stable
spec:
  hostname: "*"
  prefix: /app/
  service: app-v1
  weight: 90
---
apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: app-canary
spec:
  hostname: "*"
  prefix: /app/
  service: app-v2
  weight: 10

This configuration sends 90 percent of /app/ traffic to v1 and 10 percent to v2. This is the simplest form of a canary release. If your monitoring metrics look healthy, you gradually raise the weight from 10 to 25, 50, and 100.

   incoming /app/ requests
            |
            v
   +-----------------+
   | Emissary route  |
   | weight split    |
   +--+-----------+--+
      | 90%       | 10%
      v           v
  app-v1       app-v2  (canary)

Combined with GitOps, you can control a gradual rollout simply by adjusting this weight value through a PR. When integrated with a tool like Argo Rollouts, metric-based automatic promotion is also possible. Combining a header-based canary (only specific internal users get the new version) with a weight-based canary (a random ratio) is a common pattern as well.

Traffic Shadowing

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: shadow-traffic
spec:
  hostname: "*"
  prefix: /app/
  service: app-v2-shadow
  shadow: true

shadow: true sends a copy of the real traffic to the new version but discards the response. In other words, you can observe how the new version handles real traffic without affecting users at all. This is useful for a dark launch.

API Gateway vs Plain Ingress: When to Use Which

Let us pause here to clarify. Not every service needs an API Gateway.

A plain Ingress is sufficient in the following cases.

  • When exposing an internal tool or a small service owned by a single team
  • When you only need path-based routing and TLS termination
  • When authentication is already handled at the application or service-mesh level

By contrast, an API Gateway-style Ingress like Emissary shines in these cases.

  • When multiple teams each own their microservices and you want to manage routing configuration in a decentralized way
  • When you want to apply authentication, rate limiting, and transformation uniformly at the gateway level
  • When you need gradual deployments such as canary, traffic shadowing, and header-based routing
  • When you need to handle diverse protocols such as gRPC, WebSocket, and HTTP/3 from a single entry point
RequirementRecommendation
Simple path routing plus TLSBasic Ingress controller
Decentralized routing ownershipEmissary Mapping
Gateway-level authenticationEmissary AuthService
Gradual canaryEmissary weight or Argo Rollouts
Standardized, future-proof APIGateway API (supported by Emissary)

The Developer Portal

The commercial Ambassador Edge Stack includes a developer portal (Dev Portal). This is a feature that automatically generates an API catalog page once you attach an OpenAPI spec to a Mapping.

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: catalog-api
  labels:
    docs.getambassador.io/source: "true"
spec:
  hostname: "*"
  prefix: /catalog/
  service: catalog
  docs:
    path: "/openapi.json"

If you specify the path to the OpenAPI (Swagger) document in docs.path, the portal collects it and automatically generates documentation that external and internal developers can browse. As the number of microservices grows, automatically aggregating "which API lives where and how to call it" is a significant operational advantage. Open-source Emissary alone does not provide the portal UI, so if you need this feature you should consider Edge Stack.

Gateway API: The Flow Toward the Future Standard

As mentioned earlier, as of 2026 the networking Ingress API is frozen and Gateway API has established itself as the successor standard. The core of Gateway API is its role-oriented design.

  • GatewayClass: the kind of gateway implementation defined by the infrastructure provider (for example, Emissary)
  • Gateway: the actual entry point (listeners, ports) created by the cluster operator
  • HTTPRoute: the routing rule created by the application developer

This separation is philosophically very similar to the Listener/Host/Mapping separation that Emissary originally pursued. That is why Emissary supports Gateway API alongside its own CRDs.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: emissary-gateway
spec:
  gatewayClassName: emissary
  listeners:
    - name: http
      protocol: HTTP
      port: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
spec:
  parentRefs:
    - name: emissary-gateway
  hostnames:
    - "app.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /app/
      backendRefs:
        - name: app
          port: 80

Looking at the flow, GatewayClass points to Emissary, Gateway opens a listener on port 8080, and HTTPRoute sends the /app/ path to the app service. Gateway API's backendRefs supports multiple backends and weights, so scenarios like canary can also be expressed in a standard way.

  GatewayClass(emissary)
        |
        v
     Gateway  ---- listener :8080
        ^
        | parentRefs
   HTTPRoute  ---- /app/ -> Service(app)

For a new project, it is well worth actively considering starting with Gateway API instead of a vendor CRD like Mapping. Because it is a standard, even if you switch controllers later, there is a good chance you can reuse the routing definitions as is. That said, advanced features that Gateway API does not yet cover (some Envoy details) may need to be supplemented with vendor CRDs or extension fields.

Hands-on Deployment

Let us follow the most common flow for installing Emissary with Helm.

# Install the CRDs first
kubectl apply -f https://app.getambassador.io/yaml/emissary/3.10.0/emissary-crds.yaml
kubectl wait --timeout=90s --for=condition=available deployment emissary-apiext -n emissary-system

# Add the Helm repo and install
helm repo add datawire https://app.getambassador.io
helm repo update
helm install emissary-ingress datawire/emissary-ingress \
  --namespace emissary \
  --create-namespace
kubectl rollout status deployment/emissary-ingress -n emissary

Once installation is complete, you define a basic Listener.

apiVersion: getambassador.io/v3alpha1
kind: Listener
metadata:
  name: http-listener
  namespace: emissary
spec:
  port: 8080
  protocol: HTTP
  securityModel: XFP
  hostBinding:
    namespace:
      from: ALL
---
apiVersion: getambassador.io/v3alpha1
kind: Listener
metadata:
  name: https-listener
  namespace: emissary
spec:
  port: 8443
  protocol: HTTPS
  securityModel: XFP
  hostBinding:
    namespace:
      from: ALL

Next, you create a Host that ties together a domain and TLS. You can automatically issue certificates with cert-manager or Emissary's built-in ACME.

apiVersion: getambassador.io/v3alpha1
kind: Host
metadata:
  name: example-host
  namespace: emissary
spec:
  hostname: app.example.com
  acmeProvider:
    authority: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
  tlsSecret:
    name: app-example-com-tls

Finally, you apply a routing Mapping and verify the behavior.

kubectl apply -f mapping.yaml

# Check the external IP
kubectl get service emissary-ingress -n emissary

# Verify routing
EMISSARY_IP=$(kubectl get svc emissary-ingress -n emissary -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -i http://$EMISSARY_IP/backend/

# Check diagnostics (port forwarding)
kubectl port-forward -n emissary deploy/emissary-ingress 8877
curl http://localhost:8877/ambassador/v0/diag/?json=true | jq .

The /ambassador/v0/diag/ diagnostics endpoint shows all the Mappings that Emissary currently recognizes and the cluster state, so it is the first place to look when checking whether your configuration has been applied correctly.

Comparison with Another Envoy-based Controller: Contour

Besides Emissary, Contour is a representative controller that also uses Envoy as its data plane. Both are CNCF projects and Envoy based, but they aim at different things.

ItemEmissary-ingressContour
Main stewardAmbassador Labs (core is CNCF)VMware/CNCF
Core CRDMapping, Host, ListenerHTTPProxy
AimFull API Gateway setLightweight Ingress plus L7 routing
Auth / rate limitBuilt in (AuthService, etc.)External ext_authz integration
Delegation modelPer namespace/resourceHTTPProxy include delegation
Gateway APISupportedActively supported
Commercial versionEdge StackNone (pure open source)

A rough rule of thumb for choosing is this. If you want a full set of features such as authentication, rate limiting, and a developer portal at the gateway level, Emissary is advantageous. Conversely, if you need a lightweight, simple Envoy-based L7 router and handle authentication externally, Contour is cleaner. Contour's HTTPProxy has a clear delegation model via include, which gives it an edge in preventing path conflicts in multi-tenant environments.

Operations and Tuning

To run Emissary stably in production, it is good to take care of the following.

Resources and Scaling

Envoy's memory usage grows in proportion to the scale of the configuration (the number of routes and clusters). As Mappings grow into the thousands, the cost of the controller recomputing the snapshot also increases, so set appropriate CPU/memory requests and limits and configure horizontal scaling with an HPA.

# Helm values example (partial)
replicaCount: 3
resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: "1"
    memory: 1Gi
autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70

Observability

Emissary exposes Envoy's statistics in Prometheus format. Monitoring request counts, latency (p50/p90/p99), upstream 5xx ratio, active connections, and so on in a dashboard gives you the basis for canary promotion decisions. For distributed tracing, you can integrate with a Zipkin/Jaeger/OpenTelemetry collector via the TracingService resource.

apiVersion: getambassador.io/v3alpha1
kind: TracingService
metadata:
  name: tracing
  namespace: emissary
spec:
  service: "otel-collector:9411"
  driver: zipkin
  sampling:
    overall: 10

Zero-downtime Redeployment

When upgrading Emissary itself, place a PodDisruptionBudget, appropriate readiness/liveness probes, and enough replicas so that traffic is not interrupted even during a rolling update. If you use externalTrafficPolicy: Local on the LoadBalancer service, the client IP is preserved, but you have to be careful about node distribution.

Common Pitfalls and Troubleshooting

Here we summarize the problems you frequently encounter in real operations.

  • You get a 404 even though the Mapping exists: the most common cause is that there is no Listener, or the Host's hostname does not match the request's Host header. First check on the /ambassador/v0/diag/ diagnostics page whether that Mapping is actually loaded.
  • Configuration is not being applied: Emissary only applies all CRDs when a consistent snapshot is formed. If even one resource fails validation, the entire snapshot may be rejected, so check the controller logs for validation errors.
  • TLS certificate is not applied: check whether the Host's tlsSecret is in the same namespace and whether ACME issuance has completed, by looking at the Host's state. ACME requires that the HTTP-01 challenge over port 80 be reachable.
  • gRPC does not work: people often forget to set grpc: true in the Mapping. Check the HTTP/2 prior knowledge setting and protocol matching together.
  • The canary ratio feels inaccurate: weight is a statistical split, so the ratio can fluctuate when the request count is low. Also, if the sum of weights of the Mappings handling the same prefix is not 100, it is normalized differently than you intended.
  • 503 upstream connect error: check whether the upstream service's endpoints are ready and whether the port and protocol (especially whether TLS origination is used) match. You can see the cluster state on the diagnostics page.

The golden rule of problem diagnosis is "diagnostics endpoint first, controller logs next, Envoy statistics last." Looking in this order quickly narrows down most routing problems.

Conclusion

Emissary-ingress is not a mere Ingress controller, but an API Gateway that unfolds the power of Envoy through Kubernetes-native CRDs. From the clear separation of control plane and data plane, decentralized routing ownership through Mapping, gateway-level authentication and rate limiting, weight-based canary and traffic shadowing, all the way to Gateway API support, it packs in almost every element you need for modern traffic management.

Viewed in the context of 2026, the direction is clear. The Ingress API is frozen, Gateway API is taking over the position of the standard, and existing controllers are facing maintenance and security burdens. In this flow, the strategy of understanding an Envoy-based controller and standardizing new routing on Gateway API significantly reduces the cost of future migrations.

If you only need simple exposure, a basic Ingress is enough. But when the moment arrives that you need multiple teams, multiple microservices, gradual deployments, and gateway-level policies, an API Gateway-style Ingress like Emissary becomes the answer. Start small, put a diagnostics endpoint and observability in place, and expand gradually with weight and Gateway API.

References