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


API based on Flask

Mon 09 December 2013

Here I want to consider implementation of API best practices which usually don't follow Fielding's REST strictly. Example Flask project is on GitHub.

API Versioning

Interfaces are changed hence versioning is mandatory in order to not annoy your users. You might need to add new resource or field to particular …

Category: Python Tagged: python flask api

comments

Read More

Preparation to Python Interview

Fri 02 November 2012

I decided to collect a little more information and experience during preparation to Python developer interview. These are some information and links which seemed important to me. Maybe it will be helpful.

How does it usually go?

What kind of projects did you participate in?

What did you do at …

Category: Python Tagged: python interview

comments

Read More

Django TODO: тестирование во время конструирования

Fri 29 June 2012

Тестирование, выполняемое разработчиками -- один из важнейших элементов полной стратегии тестирования.

Тестирование может указать только на отдельные дефектные области программы -- оно не сделает программу удобнее в использовании, более быстрой, компактной, удобочитаемой или расширяемой.

Цель тестирования противоположна целям других этапов разработки. Его целью является нахождение ошибок. Успешным считается тест, нарушающий работу ПО …

Category: Python Tagged: python django django-todo testing

comments

Read More

Django TODO: конструирование системы

Fri 29 June 2012

При работе над проектом конструирование включает другие процессы, в том числе проектирование. Формальная архитектура дает ответы только на вопросы системного уровня, при этом значительная часть проектирования может быть намеренно оставлена на этап конструирования. Проектирование -- это "постепенный" процесс. Проекты приложений не возникают в умах разработчиков сразу в готовом виде. Они развиваются …

Category: Python Tagged: python django django-todo construction

comments

Read More

Django TODO: проектирование архитектуры системы

Fri 29 June 2012

Следующим этапом разработки системы является проектирование архитектуры.

Архитектура должна быть продуманным концептуальным целым. Главный тезис самой популярной книги по разработке ПО "Мифический человеко-месяц" гласит, что основной проблемой, характерной для крупных систем, является поддержание их концептуальной целостности. Хорошая архитектура должна соответствовать проблеме [1].

Разделение системы на подсистемы на уровне архитектуры, позволяет …

Category: Python Tagged: python django django-todo architecture

comments

Read More

Django TODO: выработка требований к системе

Fri 29 June 2012

После прочтения Макконелла захотелось спроецировать его советы на Django. Для этого я взял за основу разработку системы Django TODO. Итак, первый этап -- выработка требований к системе.

Требования подробно описывают, что должна делать система. Внимание к требованиям помогает свести к минимуму изменения системы после начала разработки. Явные требования помогают гарантировать, что …

Category: Python Tagged: python django django-todo requirements

comments

Read More

Соглашения по разработке на Python/Django

Fri 29 June 2012

Во время разработки я часто сверяюсь с известными мне соглашениями, стараюсь следовать рекомендациям. Цитировать их не имеет смысла -- лучше приведу ссылки.

PEP 8 -- Style Guide for Python Code.

Code Like a Pythonista: Idiomatic Python. В нем я нашел ответы на вопросы форматирования длинных строк:

expended_time = (self.finish_date() - self.start_date
                 + datetime …

Category: Python Tagged: python django best practices

comments

Read More

Разделение настроек в Django

Fri 29 June 2012

В Django wiki собраны различные способы разделения настроек. Мне нравится вариант, описанный в блоге Senko Rašić:

settings/
├── __init__.py
├── base.py
├── development.py
├── local.py
└── production.py

base.py содержит общие настройки для development.py и production.py, например:

ADMINS = ()
MANAGERS = ADMINS

TIME_ZONE = 'Asia/Yekaterinburg'
# ...

production.py содержит настройки для …

Category: Python Tagged: python django settings

comments

Read More

Краткий обзор инфраструктуры для разработки reusable Django приложений

Wed 13 June 2012

Начиная впервые разрабатывать веб-приложения на новом фреймворке программист зачастую сталкивается с некоторыми трудностями. При разработке отчуждаемых веб-приложений на Django к этим проблемам необходимо отнести организацию файлов в проекте, обнаружение тестов, вопросы пакетирования приложений и организации автоматизированного тестирования. В данной статье приведены пути решения этих проблем.

Важно знать различия между двумя …

Category: Python Tagged: python django infrastructure

comments

Read More

Вычислительные методы одномерной оптимизации

Wed 06 October 2010

На третьем курсе по предмету методы оптимизации делали лабораторную работу на тему «Вычислительные методы одномерной оптимизации». Задача заключалась в поиске безусловного минимума функции f(x) = pow(x, 3) – x + pow(e, -x) на начальном интервале [0, 1] с точностью 0.00001.

Вычисления производились через:

  • пассивный метод;
  • равномерные блочные методы;
  • метод …

Category: Misc Tagged: php mathematical optimization

comments

Read More

Определение нажатия комбинации клавиш средствами BIOS на ассемблере

Thu 03 December 2009

По учебе понадобилось написать программу на ассемблере, которая должна распознать нажатие «горячей» комбинации клавиш LeftCtrl+RightShift+F3 и реагировать на него звуковым сигналом. Информации/примеров по этой теме маловато, по этому решил опубликовать свою программку.

masm
.model small
.stack 256
.data
    Msg_about db 'Распознать нажатие «горячей» комбинации клавиш', 0Ah, 0Dh …

Category: Misc Tagged: assembler

comments

Read More

Моделирование одноканальной СМО с отказами

Sat 30 May 2009

Дана одноканальная система массового обслуживания с отказами. В нее поступают заявки через промежуток времени n, где n – случайная величина, подчиненная равномерному закону распределения. Время обслуживания заявки системой m также является случайной величиной с показательным законом распределения. Если к моменту прихода заявки канал занят, заявка покидает систему необслуженной.

Изначально код был …

Category: Misc Tagged: python modeling single-channel queue

comments

Read More