Ambassador as API Gateway

Wed 13 March 2019

API gateway acts as a reverse proxy, routing API requests from clients to services. Usually it also performs authentication and rate limiting, so the services behind the gate don't have to. In this short tutorial we'll see how to achieve that with Ambassador.

The demo is based on a dummy Traveling project where we have services to rent a car and book a hotel.

Firstly, we shall install Ambassador on Minikube by following Ambassador user guide.

$ minikube start
Starting local Kubernetes v1.13.2 cluster...
$ kubectl apply -f https://www.getambassador.io/yaml/ambassador/ambassador-rbac.yaml

Next, create Kubernetes service for Ambassador deployment of type NodePort, so it's easily accessible outside of the cluster. It will be the entry point of our API gateway.

$ kubectl apply -f - <<<'
apiVersion: v1
kind: Service
metadata:
  name: ambassador
spec:
  selector:
    service: ambassador
  type: NodePort
  ports:
    - port: 80
  # Propagate the original source IP of the client.
  externalTrafficPolicy: Local
'
$ AMBASSADORURL=$(minikube service --url ambassador)

Note, there is a diagnostic web UI at $AMBASSADORURL/ambassador/v0/diag/. You should restrict access to it from your custom auth service.

Travel Apps

Let's deploy the travel apps on Kubernetes. For simplicity, you can use the images uploaded to Docker Hub. If you prefer to build Docker images yourself, make sure they are accessible from Minikube: configure Docker client to communicate with the Minikube Docker daemon. Note, you can restore your local Docker environment with eval $(minikube docker-env --unset) later on.

$ git clone https://github.com/marselester/apigate.git
$ cd ./apigate/
$ eval $(minikube docker-env)
$ docker build --tag=marselester/travel-hotel:v1.0.0 --file=docker/hotel.Dockerfile .
$ kubectl apply -f k8s-ambassador/hotel/deployment.yml
$ kubectl apply -f k8s-ambassador/hotel/service.yml

The hotel app should be running:

$ kubectl get pods -l app=hotel
hotel-api-8d7d59b69-bcsvh
$ kubectl port-forward hotel-api-8d7d59b69-bcsvh 8000
$ curl localhost:8000/v1/hotels/bookings/7b4fc183-ee67-494d-9715-3510c6d8f2ef
{
    "id": "7b4fc183-ee67-494d-9715-3510c6d8f2ef",
    "hotel_id": "046d471d-70c7-4595-80cc-266d3e6e07fa",
    "status": "confirmed"
}

Now, it's time to put the hotel Service behind the gateway by adding annotations to the service.

apiVersion: v1
kind: Service
metadata:
  name: hotel
  annotations:
    getambassador.io/config: |
      ---
      apiVersion: ambassador/v1
      kind: Mapping
      name: hotel_mapping
      prefix: /v1/hotels
      rewrite: ""
      service: hotel
spec:
  selector:
    app: hotel
  type: NodePort
  ports:
    - port: 80
      targetPort: 8000
$ kubectl apply -f k8s-ambassador/hotel/service-gate.yml

By default, Ambassador would rewrite /v1/hotels prefix to /. With rewrite: "" directive we configure Ambassador to not change the prefix as it forwards a request to the hotel service. Let's confirm that hotel booking requests are routed to the hotel app:

$ curl $AMBASSADORURL/v1/hotels/bookings/7b4fc183-ee67-494d-9715-3510c6d8f2ef
{
    "id": "7b4fc183-ee67-494d-9715-3510c6d8f2ef",
    "hotel_id": "046d471d-70c7-4595-80cc-266d3e6e07fa",
    "status": "confirmed"
}

The car rental app is deployed similarly.

apiVersion: v1
kind: Service
metadata:
  name: car
  annotations:
    getambassador.io/config: |
      ---
      apiVersion: ambassador/v1
      kind: Mapping
      name: car_mapping
      prefix: /v1/cars
      rewrite: ""
      service: car
spec:
  selector:
    app: car
  type: NodePort
  ports:
    - port: 80
      targetPort: 8000
$ docker build --tag=marselester/travel-car:v1.0.0 --file=docker/car.Dockerfile .
$ kubectl apply -f k8s-ambassador/car/deployment.yml
$ kubectl apply -f k8s-ambassador/car/service-gate.yml

You can see that the car booking is available at the gateway.

$ curl $AMBASSADORURL/v1/cars/bookings/9e0d65f5-9de2-4428-9bee-1f3967f05129
{
    "id": "9e0d65f5-9de2-4428-9bee-1f3967f05129",
    "car_id": "cfb6f7a5-4591-4f5c-8b17-9a1b10f98ada",
    "status": "confirmed"
}

Authentication Service

API requests must be authenticated before reaching the Travel apps. This work is delegated to travelauth Service that performs HTTP Basic authentication and returns a username in X-Travel-User header if credentials matched. We shall start from deploying it on the cluster.

$ docker build --tag=marselester/travel-auth:v1.0.0 --file=docker/auth.Dockerfile .
$ kubectl apply -f k8s-ambassador/auth/deployment.yml
$ kubectl apply -f k8s-ambassador/auth/service.yml

If the auth server is properly deployed, it will prompt for username/password.

$ kubectl get pods -l app=auth
auth-api-556685f658-h9qb4
$ kubectl port-forward auth-api-556685f658-h9qb4 8000
$ curl -i -u bob:bob localhost:8000/v1/hotels
HTTP/1.1 200 OK
X-Travel-User: bob

Let's tell Ambassador to forward all requests to the auth server and copy X-Travel-User response header from the auth server to the routed request.

apiVersion: v1
kind: Service
metadata:
  name: travelauth
  annotations:
    getambassador.io/config: |
      ---
      apiVersion: ambassador/v1
      kind: AuthService
      name: gate_auth
      # This is k8s service name; all API requests are sent there. For example,
      # API request /v1/hotels will be sent to http://travelauth:80/v1/hotels.
      auth_service: travelauth
      proto: http
      # The travelauth service adds a username into a header after successful authentication,
      # so all the other services know who the user is (ratelimit, hotel, car services).
      allowed_authorization_headers:
        - "X-Travel-User"
spec:
  selector:
    app: auth
  type: NodePort
  ports:
    - port: 80
      targetPort: 8000
$ kubectl apply -f k8s-ambassador/auth/service-auth.yml

With updated config Ambassador should enforce authentication on API gateway.

$ curl -i $AMBASSADORURL/v1/hotels/bookings/7b4fc183-ee67-494d-9715-3510c6d8f2ef
HTTP/1.1 401 Unauthorized
$ curl -i -u bob:bob $AMBASSADORURL/v1/hotels/bookings/7b4fc183-ee67-494d-9715-3510c6d8f2ef
HTTP/1.1 200 OK
{
    "id": "7b4fc183-ee67-494d-9715-3510c6d8f2ef",
    "hotel_id": "046d471d-70c7-4595-80cc-266d3e6e07fa",
    "status": "confirmed"
}

Rate Limiting Service

With Ambassador, individual requests can be annotated with metadata, called labels. These labels can then be passed to a third party rate limiting service through a gRPC interface which implements actual rate limit logic. Check out Node.js and Java based examples.

In order to build such service, it must support Envoy's ratelimit.proto interface. The protocol buffer definition of RateLimitService service is described in ./internal/pb/ratelimit.proto. You need to install protoc compiler and protoc plugin for Go (beware of ProtoPackageIsVersion3 issue) to generate gRPC service code.

$ brew install protobuf
$ go get -u github.com/golang/protobuf/protoc-gen-go
$ cd $GOPATH/src/github.com/golang/protobuf/protoc-gen-go
$ git checkout v1.2.0
$ go install

The arguments tell protoc to use ratelimit.proto definition, search for imports in ./internal/pb/ dir, generate Go code using gprc plugin, and place the result in ./internal/pb/ dir.

$ protoc ratelimit.proto -I internal/pb/ --go_out=plugins=grpc:internal/pb/

We now have a newly generated gRPC server and client code in ./internal/pb/ratelimit.pb.go. Our ratelimit server already implements RateLimitServiceServer interface.

// ShouldRateLimit must respond to the request with an OK or OVER_LIMIT code.
func (s *server) ShouldRateLimit(ctx context.Context, req *pb.RateLimitRequest) (*pb.RateLimitResponse, error) {
    // Descriptors is a list of labels on which the rate limit service can base
    // its decision to accept or reject the request.
    log.Printf("%s: %+v", req.GetDomain(), req.GetDescriptors())

    resp := pb.RateLimitResponse{
        OverallCode: pb.RateLimitResponse_OVER_LIMIT,
    }
    return &resp, nil
}

Let's deploy it on the cluster and send a few requests using grpcurl.

$ docker build --tag=marselester/travel-ratelimit:v1.0.0 --file=docker/ratelimit.Dockerfile .
$ kubectl apply -f k8s-ambassador/ratelimit/deployment.yml
$ kubectl apply -f k8s-ambassador/ratelimit/service.yml
$ kubectl get pods -l app=ratelimit
ratelimit-api-5554898589-4gmv5
$ kubectl port-forward ratelimit-api-5554898589-4gmv5 5000

As we can see the server exposes RateLimitService.

$ grpcurl -plaintext localhost:5000 list
grpc.reflection.v1alpha.ServerReflection
pb.lyft.ratelimit.RateLimitService

Its ShouldRateLimit method should return OK or OVER_LIMIT reply.

$ grpcurl -d '{"domain":"envoy"}' -plaintext localhost:5000 pb.lyft.ratelimit.RateLimitService/ShouldRateLimit
{
  "overallCode": "OVER_LIMIT"
}

Now we shall introduce the ratelimit service to Ambassador.

apiVersion: v1
kind: Service
metadata:
  name: travelratelimit
  annotations:
    getambassador.io/config: |
      ---
      apiVersion: ambassador/v1
      kind: RateLimitService
      name: gate_ratelimit
      service: travelratelimit
spec:
  selector:
    app: ratelimit
  type: NodePort
  ports:
    - port: 80
      targetPort: 5000
$ kubectl apply -f k8s-ambassador/ratelimit/service-rate.yml

Finally, I am going to add labels to attach rate limiting descriptors as shown in Rate Limiting tutorial.

apiVersion: v1
kind: Service
metadata:
  name: hotel
  annotations:
    getambassador.io/config: |
      ---
      apiVersion: ambassador/v1
      kind: Mapping
      name: hotel_mapping
      prefix: /v1/hotels
      rewrite: ""
      service: hotel
      labels:
        ambassador:
          - request_label_group:
            - x-ambassador-test-allow:
                header: "x-ambassador-test-allow"
                omit_if_not_present: true
spec:
  selector:
    app: hotel
  type: NodePort
  ports:
    - port: 80
      targetPort: 8000
$ kubectl apply -f k8s-ambassador/hotel/service-rate.yml

If a request made to the hotel API has header X-Ambassador-Test-Allow, it should be eligible for rate limiting.

$ curl -H "x-ambassador-test-allow: probably" -i -u bob:bob $AMBASSADORURL/v1/hotels
HTTP/1.1 200 OK

Unfortunately I have not been able to make rate limiting work yet. Here is the corresponding GitHub issue.

Category: Infrastructure Tagged: ambassador kubernetes api gateway auth rate limiting

Comments