После нескольких месяцев тестов мы наконец перенесли приложение Ruby on Rails в продакшен с кластером Kubernetes.
В этой статье я расскажу, как настроить маршрутизацию на основе Path для приложения Ruby on Rails в Kubernetes с контроллером HAProxy Ingress.
Предполагается, что вы примерно представляете себе, что такое поды, деплои, сервисы, карта конфигурации и Ingress в Kubernetes
Обычно в Rails-приложении есть такие сервисы, как unicorn/puma, sidekiq/delayed-job/resque, веб-сокеты и несколько специальных API сервисов. У нас был один веб-сервис, открытый наружу через балансировщик, и все работало нормально. Но трафик рос, и нужно было маршрутизировать его по URL или Path.
В Kubernetes нет готового решения для балансировки нагрузки такого типа. Под нее уже разрабатывается alb-ingress-controller, но он пока на стадии альфы и для продакшена не подходит.
Для маршрутизации на основе Path лучше всего было использовать Ingress-контроллер.
Мы изучили вопрос и узнали, что в k8s есть разные решения для Ingress.
Мы поэкспериментировали с nginx-ingress и HAProxy и остановились на HAProxy — он лучше подходит для веб-сокетов Rails, которые мы использовали в проекте.
Я расскажу пошагово, как прикрутить HAProxy Ingress к Rails-приложению.
Настройка Rails-приложения с контроллером HAProxy Ingress
Вот что мы будем делать:
- Создадим Rails-приложение с разными сервисами и деплоями.
- Создадим секрет TLS для SSL.
- Создадим карту конфигурации HAProxy Ingress.
- Создадим контроллер HAProxy Ingress.
- Откроем доступ к Ingress через сервис типа LoadBalancer.
- Настроим DNS приложения для сервиса Ingress.
- Создадим разные правила Ingress для маршрутизации на основе Path.
- Протестируем маршрутизацию на основе Path.
Давайте создадим манифест развертывания Rails-приложения для разных сервисов — веб (unicorn), фоновые задачи (sidekiq), веб-сокет (ruby thin), API (выделенный unicorn).
Вот наш деплой веб-приложения и шаблон сервиса.
---
apiVersion: v1
kind: Deployment
metadata:
name: test-production-web
labels:
app: test-production-web
namespace: test
spec:
template:
metadata:
labels:
app: test-production-web
spec:
containers:
- image: <your-repo>/<your-image-name>:latest
name: test-production
imagePullPolicy: Always
env:
- name: POSTGRES_HOST
value: test-production-postgres
- name: REDIS_HOST
value: test-production-redis
- name: APP_ENV
value: production
- name: APP_TYPE
value: web
- name: CLIENT
value: test
ports:
- containerPort: 80
imagePullSecrets:
- name: registrykey
---
apiVersion: v1
kind: Service
metadata:
name: test-production-web
labels:
app: test-production-web
namespace: test
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: test-production-web
Вот деплой фонового приложения и шаблон сервиса.
---
apiVersion: v1
kind: Deployment
metadata:
name: test-production-background
labels:
app: test-production-background
namespace: test
spec:
template:
metadata:
labels:
app: test-production-background
spec:
containers:
- image: <your-repo>/<your-image-name>:latest
name: test-production
imagePullPolicy: Always
env:
- name: POSTGRES_HOST
value: test-production-postgres
- name: REDIS_HOST
value: test-production-redis
- name: APP_ENV
value: production
- name: APP_TYPE
value: background
- name: CLIENT
value: test
ports:
- containerPort: 80
imagePullSecrets:
- name: registrykey
---
apiVersion: v1
kind: Service
metadata:
name: test-production-background
labels:
app: test-production-background
namespace: test
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: test-production-background
Вот деплой веб-сокет приложения и шаблон сервиса.
---
apiVersion: v1
kind: Deployment
metadata:
name: test-production-websocket
labels:
app: test-production-websocket
namespace: test
spec:
template:
metadata:
labels:
app: test-production-websocket
spec:
containers:
- image: <your-repo>/<your-image-name>:latest
name: test-production
imagePullPolicy: Always
env:
- name: POSTGRES_HOST
value: test-production-postgres
- name: REDIS_HOST
value: test-production-redis
- name: APP_ENV
value: production
- name: APP_TYPE
value: websocket
- name: CLIENT
value: test
ports:
- containerPort: 80
imagePullSecrets:
- name: registrykey
---
apiVersion: v1
kind: Service
metadata:
name: test-production-websocket
labels:
app: test-production-websocket
namespace: test
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: test-production-websocket
Вот деплой API приложения и сведения о сервисе.
---
`apiVersion: v1
kind: Deployment
metadata:
name: test-production-api
labels:
app: test-production-api
namespace: test
spec:
template:
metadata:
labels:
app: test-production-api
spec:
containers:
- image: <your-repo>/<your-image-name>:latest
name: test-production
imagePullPolicy: Always
env:
- name: POSTGRES_HOST
value: test-production-postgres
- name: REDIS_HOST
value: test-production-redis
- name: APP_ENV
value: production
- name: APP_TYPE
value: api
- name: CLIENT
value: test
ports:
- containerPort: 80
imagePullSecrets:
- name: registrykey
---
apiVersion: v1
kind: Service
metadata:
name: test-production-api
labels:
app: test-production-api
namespace: test
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: test-production-api
Давайте запустим манифест командой kubectl apply
.
$ kubectl apply -f test-web.yml -f test-background.yml -f test-websocket.yml -f test-api.yml
deployment "test-production-web" created
service "test-production-web" created
deployment "test-production-background" created
service "test-production-background" created
deployment "test-production-websocket" created
service "test-production-websocket" created
deployment "test-production-api" created
service "test-production-api" created
Как только приложение будет развернуто и запущено, нужно будет создать HAProxy Ingress. Но сначала давайте создадим секрет TLS с ключом и сертификатом SSL.
Он же будет разрешать HTTPS для URL приложения и терминировать его на L7.
$ kubectl create secret tls tls-certificate --key server.key --cert server.pem
server.key
здесь — это наш ключ SSL, а server.pem
— наш сертификат SSL в формате pem.
Теперь создадим ресурсы контроллера HAProxy.
Карта конфигурации HAProxy
Все доступные параметры конфигурации для HAProxy смотрите здесь.
apiVersion: v1
data:
dynamic-scaling: "true"
backend-server-slots-increment: "4"
kind: ConfigMap
metadata:
name: haproxy-configmap
namespace: test
Развертывание контроллера HAProxy Ingress
Шаблон развертывания для Ingress-контроллера минимум с двумя репликами для управления последовательным деплоем.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
run: haproxy-ingress
name: haproxy-ingress
namespace: test
spec:
replicas: 2
selector:
matchLabels:
run: haproxy-ingress
template:
metadata:
labels:
run: haproxy-ingress
spec:
containers:
- name: haproxy-ingress
image: quay.io/jcmoraisjr/haproxy-ingress:v0.5-beta.1
args:
- --default-backend-service=$(POD_NAMESPACE)/test-production-web
- --default-ssl-certificate=$(POD_NAMESPACE)/tls-certificate
- --configmap=$(POD_NAMESPACE)/haproxy-configmap
- --ingress-class=haproxy
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
- name: stat
containerPort: 1936
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
В этом манифесте нас особенно интересуют аргументы, передаваемые контроллеру.
--default-backend-service
— это сервис, который приложение будет использовать, если запросу не соответствуют никакие правила.
У нас это сервис test-production-web
, но это может быть кастомная страница 404 или что-нибудь подобное — решать вам.
--default-ssl-certificate
— это секрет SSL, который мы только что создали. Он будет терминировать SSL на L7, и приложение будет доступно извне по HTTPS.
Сервис HAProxy Ingress
Это тип сервиса LoadBalancer
, который разрешает клиентскому трафику доступ к нашему Ingress-контроллеру.
У LoadBalancer есть доступ к публичной сети и внутренней сети Kubernetes, а на L7 он маршрутизирует трафик для Ingress-контроллера.
apiVersion: v1
kind: Service
metadata:
labels:
run: haproxy-ingress
name: haproxy-ingress
namespace: test
spec:
type: LoadBalancer
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
- name: https
port: 443
protocol: TCP
targetPort: 443
- name: stat
port: 1936
protocol: TCP
targetPort: 1936
selector:
run: haproxy-ingress
Давайте применим все манифесты HAProxy.
$ kubectl apply -f haproxy-configmap.yml -f haproxy-deployment.yml -f haproxy-service.yml
configmap "haproxy-configmap" created
deployment "haproxy-ingress" created
service "haproxy-ingress" created
Когда все ресурсы будут запущены, укажите конечную точку LoadBalancer.
$ kubectl -n test get svc haproxy-ingress -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
haproxy-ingress LoadBalancer 100.67.194.186 a694abcdefghi11e8bc3b0af2eb5c5d8-806901662.us-east-1.elb.amazonaws.com 80:31788/TCP,443:32274/TCP,1936:32157/TCP 2m run=ingress
Сопоставление DNS с URL приложения
Как только мы укажем конечную точку ELB для сервиса Ingress, нужно будет сопоставить между собой DNS сервиса и URL запроса (например test-rails-app.com
).
Реализация Ingress
Самое сложное позади, пора настроить Ingress и правила на основе Path.
Нам нужны нужны следующие правила.
Запросы к https://test-rails-app.com будут обрабатываться сервисом test-production-web
.
Запросы к https://test-rails-app.com/websocket будут обрабатываться сервисом test-production-websocket
.
Запросы к https://test-rails-app.com/api будут обрабатываться сервисом test-production-api
.
Давайте создадим манифест Ingress со всеми этими правилами.
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress
namespace: test
spec:
tls:
- hosts:
- test-rails-app.com
secretName: tls-certificate
rules:
- host: test-rails-app.com
http:
paths:
- path: /
backend:
serviceName: test-production-web
servicePort: 80
- path: /api
backend:
serviceName: test-production-api
servicePort: 80
- path: /websocket
backend:
serviceName: test-production-websocket
servicePort: 80
На случай изменений конфигурации у нас есть аннотации для Ingress ресурсов.
Как и ожидалось, по умолчанию наш трафик на /
маршрутизируется в сервис test-production-web
, /api
— в test-production-api
, а /websocket
— в test-production-websocket
.
Нам нужна была маршрутизация на основе Path и терминация SSL на L7 в Kubernetes, и реализация Ingress решила эту задачу.