Запускаємо новий сервіс для проекту, і цьому сервісу треба підняти Redis та MongoDB.
Сама “технічна задача” виглядає приблизно так:
- Redis: task queue + some other stuff (e.g. we stream the agent response from LLM to Redis, and if the user is connected on a websocket – then we stream data from the Redis to user)
- MongoDB: similar purpose as the PostgreSQL for our Backend API – main storage
Що з неї зрозуміло:
- для Redis нам не дуже важливий data persistence – він більше грає роль проксі/кеша
- а для MongoDB як раз дані важливі, а значить треба мати і бекапи
Довго думав як їх запускати – з AWS managed сервісами типу ElastiCache або MemoryDB замість Redis, та DocumentDB чи Atlas для MongoDB, але врешті-решт вирішили запускати в Kubernetes:
- менша latency: колись робив порівняння часу відповіді від Redis в EKS vs AWS ElastiCache – різниця була дуже відчутною (хоча це було ще у 2020)
- не хочеться тягнути зайвий Terraform код: менеджити з Helm все ж простіше
- моніторинг: “всередині” Kubernetes простіше збирати метрики (не треба тягнути з CloudWacth Metrics/Logs, ще і платити за це), є метрики і вже налаштовані алерти по контейнерам, Pods, EC2
- але головне – новий сервіс ще в експерементальній стадії, а тому просто нема сенсу піднімати щось прям “навєка”, ще і платити за це додаткові гроші AWS
Починати будемо з Redis, в цьому пості про нього, а вже десь далі – про MongoDB.
Note: я тут буду Valkey і Redis використовувати як синоніми, да пробачать мені розробники Valkey.
Зміст
Redis vs Valkey
Дуже давно не мав справу з Redis, і коли спитав людей в чатику RTFM що нині актуально для Redis in Kubernetes – то майже всі сказали Valkey.
На своєму сайт Redis, звісно, описує різницю як “Redis вміє все, а Valkey – обрізана подєлка” (див. Fast data you can trust: Redis vs. Valkey), але це більше про Redis Enterprise vs Valkey. А от різниця між саме Redis OSS та Valkey вже не така відчутна, див. ще документацію від AWS – Compare Redis OSS and Valkey.
Головна відмінність – ліцензія, бо Valkey повністю open-source з BSD-ліцензією. Але особисто для мене питання ліцензії не надто важливі, бо міняють, як перчатки (привіт, Hashicorp, MongoDB, Elasticsearch, ну і сам Redis).
Ще є відмінності в технічній реалізації, як-от I/O та механізму кешування, і, скоріш за все, ще багато чого – але нічого принципового чи такого, що кардинально змінювало б роботу з ним.
План архітектури
Працювати все буде в AWS Elastic Kubernetes Service, для нового сервісу маємо окремий NodePool в Karpenter.
Що треба буде робити:
- fault-tolerance: новий сервіс хоч і в експерименті, але відразу треба закласти роботу на кількох Pods
- network:
- доступ тільки в межах EKS/VPC, без Ingress та Load Balancer
- додамо статичний Kubernetes Service з типом ExternalName – аби потім простіше було переключитись на щось AWS Managed
- моніторинг: збираємо метрики, потім створимо алерти і дашборду в Grafana
- аутентифікація: поки чисто мінімальний PoC-конфіг ACL
Деплой в Kubernetes
У Valkey є власні Helm charts – їх і візьмемо, але робити будемо через власний чарт та Helm dependencies – бо треба буде створювати власні ресурси.
Є і Valkey Operator, але він зараз “This operator is in active development and not ready for production use“, да і у нас не та система, щоб його тягнути – тому обійдемось без нього.
Deployment Modes:
- є standalone, чисто 1 Pod
- є masters-slave replication, як і у звичайного Redis, його і будемо робити – 1 master та 2 replicas (див. Redis: репликация, часть 1 — обзор. Replication vs Sharding. Sentinel vs Cluster. Топология Redis, 2019 рік – але концептуально нічого не змінилось)
Valkey, як і Redis, вміє і в cluster mode, див. Cluster tutorial – але і чарт цього не підтримує, і в моєму випадку це зайве.
Аутентифікація буде через AWS Secrets Store або Param Store для збереження credentials, а потім з External Secrets Operator (ESO) передаємо їх до Valkey Pods.
Створення власного Helm chart
Додаємо собі локально репозиторій Valkey:
$ helm repo add valkey https://valkey.io/valkey-helm/ "valkey" has been added to your repositories $ helm repo update
Шукаємо актуальну версію чарту valkey/valkey, на момент написання цього поста версія 0.10.0:
$ helm search repo valkey NAME CHART VERSION APP VERSION DESCRIPTION valkey/valkey 0.10.0 9.1.0 A Helm chart for Kubernetes valkey/valkey-operator 0.3.2 v0.3.0 A Helm chart for the Valkey Operator valkey/valkey-resources 0.1.0 v0.3.0 Helm chart for operator-managed Valkey resource...
Створюємо свій файл Chart.yaml, в dependencies задаємо Valkey, через alias задаємо власну назву:
apiVersion: v2
name: agents-mainframe-redis
description: >-
Helm chart for the Redis master-slave setup on the shared hOS EKS cluster.
type: application
version: 0.10.0
appVersion: "0.10.0"
dependencies:
- name: valkey
repository: https://valkey.io/valkey-helm/
version: 0.10.0
alias: valkey-redis
Пишемо собі Makefile:
SHELL := /usr/bin/env bash
.PHONY: helm-dependencies-upgrade \
helm-dependencies-build \
helm-lint-test-1-33 \
helm-template-test-1-33 \
helm-diff-test-1-33 \
helm-install-test-1-33 \
helm-lint-dev-1-33 \
helm-template-dev-1-33 \
helm-diff-dev-1-33 \
helm-install-dev-1-33 \
helm-lint-staging-1-33 \
helm-template-staging-1-33 \
helm-diff-staging-1-33 \
helm-install-staging-1-33 \
helm-lint-prod-1-33 \
helm-template-prod-1-33 \
helm-diff-prod-1-33 \
helm-install-prod-1-33
helm-dependencies-upgrade:
helm dependency update .
helm-dependencies-build:
helm dependency build .
helm-lint-test-1-33: helm-dependencies-build
helm lint . \
-f values/common-values.yaml \
-f values/test/test-1-33-values.yaml
helm-template-test-1-33: helm-dependencies-build
helm template agents-mainframe-redis . \
--namespace test-valkey-redis-ns \
-f values/common-values.yaml \
-f values/test/test-1-33-values.yaml
helm-diff-test-1-33: helm-dependencies-build
helm diff upgrade agents-mainframe-redis . \
--namespace test-valkey-redis-ns \
--allow-unreleased \
-f values/common-values.yaml \
-f values/test/test-1-33-values.yaml
helm-install-test-1-33: helm-dependencies-build
helm upgrade --install agents-mainframe-redis . \
--namespace test-valkey-redis-ns \
--create-namespace \
--wait \
--timeout 10m \
--debug \
-f values/common-values.yaml \
-f values/test/test-1-33-values.yaml
Створюємо Kubernetes Namespace (хоча в helm upgrade --install вже є --create-namespace):
$ kk create ns test-valkey-redis-ns
Корисні Values у Valkey Helm chart
Тепер нам треба додати власні values, глянемо, що цікавого в самому чарті (див. всі в values.yaml):
podAnnotations: тут треба додати анотацію для Reloader (див. Kubernetes: ConfigMap и Secrets – auto-reload данных в подах)commonLabels: треба додати власну лейблуcomponent(це конкретно для мого проекту)service: лишаємо дефолтні значення – ClusterIP нам зараз підходитьresources: додамо якісь мінімальні значення, потім подивимось під час роботи і підтюнимоextraValkeySecrets: можна підключити додаткові Kubernetes Secret (але паролі з ESO будуть не тут)valkeyConfig: можна додати власні параметри дляvalkey.conf(akaredis.conf)authіusersExistingSecret: а оце якраз для ESO та паролів юзерівreplica: включаємо master-slavereplicas: вкажемо 2 (плюс буде 1 master)replicationUser: поки лишимо дефолт, потім можна буде зробити окремогоdisklessSync: налаштування реплікації – “традиційний” RDB (писав у RDB Persistence), або напряму через master mem bufer => network socket => replica mem buferminReplicasToWriteтаminReplicasMaxLag: якщо в “кластері” у мастера нема healthy replicas – то нові writes будуть заблоковані, див. Write Safety Configuration (тут лінк на Cluster Mode, але то просто баг в README – неправильний зробили header для Safety Configuration)persistence: додаємо – треба для реплікації- ще окремо треба буде погратись з
valkeyConfigіappendonlyтаappendfsyncв ньому, але це вже іншим разом – треба згадувати, як воно працює
- ще окремо треба буде погратись з
persistentVolumeClaimRetentionPolicy: можна вказати явно, але в мене є окремий StorageClass зreclaimPolicy: Retain
tolerations,nodeAffinity,podAntiAffinityтаtopologySpreadConstraints: треба для повноцінного fault-tolerance, але поки тут використаємо тількиtolerationsтаnodeAffinity– аби Pods запускались тільки на потрібних WorkerNodes (див. Kubernetes: Pods та WorkerNodes – контроль розміщення подів на нодах)podDisruptionBudget: включаємо, must havevalkeyLogLevel: додамо в наш values відразуmetrics: включаємо метрики, використовує стандартний oliver006/redis_exporterserviceMonitor: включимо, щоб VictoriaMetrics створилаVMServiceScrape
Створення Values для власного Helm chart
Створюємо каталоги – в кожному будуть values для конкретного environment:
$ mkdir -p helm/redis/values/{dev,staging,prod,test}
Тепер вся структура виглядає так:
$ tree helm/redis/
helm/redis/
├── Chart.yaml
├── Makefile
└── values
├── dev
├── prod
├── staging
└── test
В test-1-33-values.yaml будуть env-specific параметри для Testing Env на EKS-кластері v1.33.
Власне, пишемо values.
Робимо загальний файл common-values.yaml – бо поки що всі параметри для всіх environments однакові:
### ALL VALUES:
# https://github.com/valkey-io/valkey-helm/blob/main/valkey/values.yaml
valkey-redis:
podAnnotations:
reloader.stakater.com/auto: "true"
commonLabels:
component: mainframe
# TODO
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
# TODO
#extraValkeySecrets: []
# TODO
#extraValkeyConfigs: []
# TODO
# Content for valkey.conf (will be mounted via ConfigMap)
#valkeyConfig: ""
# TODO
auth:
enabled: false
replica:
enabled: true
# Number of replica instances (total pods = replicas + 1 master)
replicas: 2
replicationUser: "default"
minReplicasToWrite: 1
minReplicasMaxLag: 10
# EBS volume configuration
persistence:
size: 10Gi
storageClass: gp3-retain
tolerations:
- key: AgentsMainfraimOnly
operator: Exists
effect: NoSchedule
# TODO: enable together with podAntiAffinity when enough EC2 nodes are available.
#podLabels:
# app.kubernetes.io/component: valkey
# TODO
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: component
operator: In
values:
- mainframe
#podAntiAffinity:
# requiredDuringSchedulingIgnoredDuringExecution:
# - labelSelector:
# matchLabels:
# app.kubernetes.io/component: valkey
# topologyKey: kubernetes.io/hostname
#topologySpreadConstraints: []
podDisruptionBudget:
enabled: true
# Minimum number of pods that must be available during a disruption
#minAvailable: 1
# Maximum number of pods that can be unavailable during a disruption
maxUnavailable: 1
valkeyLogLevel: notice
metrics:
enabled: true
serviceMonitor:
enabled: true
Деплоїмо:
$ make helm-install-test-1-33
Перевіряємо Pods:
$ kk get pod NAME READY STATUS RESTARTS AGE agents-mainframe-redis-valkey-redis-0 2/2 Running 0 5m34s agents-mainframe-redis-valkey-redis-1 2/2 Running 0 5m13s agents-mainframe-redis-valkey-redis-2 2/2 Running 0 4m7s
Перевірка Redis
І швидка перевірка, що все працює.
Підключаємось в master Pod:
$ kk exec -ti agents-mainframe-redis-valkey-redis-0 -- redis-cli Defaulted container "agents-mainframe-redis-valkey-redis" out of: agents-mainframe-redis-valkey-redis, metrics, agents-mainframe-redis-valkey-redis-init (init) 127.0.0.1:6379> keys * (empty array) 127.0.0.1:6379>
Перевіряємо статус реплікації:
127.0.0.1:6379> info replication # Replication role:master connected_slaves:2 min_slaves_good_slaves:2 slave0:ip=agents-mainframe-redis-valkey-redis-1.agents-mainframe-redis-valkey-redis-headless.test-valkey-redis-ns.svc.cluster.local,port=6379,state=online,offset=630,lag=0,type=replica slave1:ip=agents-mainframe-redis-valkey-redis-2.agents-mainframe-redis-valkey-redis-headless.test-valkey-redis-ns.svc.cluster.local,port=6379,state=online,offset=630,lag=0,type=replica replicas_waiting_psync:0 master_failover_state:no-failover master_replid:69b0ef28b85bca2a0e331fc00ed5254f6b7dd7a5 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:644 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:10485760 repl_backlog_first_byte_offset:1 repl_backlog_histlen:644
Monitoring – метрики Redis та VictoriaMetrics
Спершу глянемо, що сам Valkey віддає метрики – знаходимо Service та порт:
$ kk get svc | grep metr agents-mainframe-redis-valkey-redis-metrics ClusterIP 172.20.189.28 <none> 9121/TCP 10m
Відкриваємо собі доступ:
$ kk port-forward svc/agents-mainframe-redis-valkey-redis-metrics 9121
Перевіряємо ендпоінт /metrics:
$ curl -s http://localhost:9121/metrics | grep redis | head # HELP redis_acl_access_denied_auth_total acl_access_denied_auth_total metric # TYPE redis_acl_access_denied_auth_total counter redis_acl_access_denied_auth_total 0 # HELP redis_acl_access_denied_channel_total acl_access_denied_channel_total metric # TYPE redis_acl_access_denied_channel_total counter redis_acl_access_denied_channel_total 0 # HELP redis_acl_access_denied_cmd_total acl_access_denied_cmd_total metric # TYPE redis_acl_access_denied_cmd_total counter redis_acl_access_denied_cmd_total 0 # HELP redis_acl_access_denied_key_total acl_access_denied_key_total metric
Тут все є – перевіряємо в VictoriaMetrics.
В values нашого чарту вказано створення ServiceMonitor, перевіряємо його:
$ kk get servicemonitor NAME AGE agents-mainframe-redis-valkey-redis 4m11s
В VictoriaMetrics маємо задану опцію operator.disable_prometheus_converter="false", а тому VictoriaMetrics створила відповідний VMservicescrape (див. VMServiceScrape з ServiceMonitor та VictoriaMetrics Prometheus Converter):
$ kk get vmservicescrape -A | grep valkey test-valkey-redis-ns agents-mainframe-redis-valkey-redis 5m23s operational
Перевіряємо Targets на vmagent:
І метрики в самій VictoriaMetrics:
Алерти і Grafana будемо додавати пізніше.
Persistent DNS name ExternalName
Так як ми в майбутньому можливо будемо переїжджати до AWS managed сервісів – можна відразу створити Kubernetes Service з типом ExternalName – і використовувати його в конфігах нашого сервісу, а потім його переключити на AWS endpoint (див. Kubernetes: ClusterIP vs NodePort vs LoadBalancer, Services и Ingress – обзор, примеры).
Що у нас є з сервісів від самого Valkey Helm chart:
$ kk get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE agents-mainframe-redis-valkey-redis ClusterIP 172.20.243.142 <none> 6379/TCP 10m agents-mainframe-redis-valkey-redis-headless ClusterIP None <none> 6379/TCP 10m agents-mainframe-redis-valkey-redis-metrics ClusterIP 172.20.189.28 <none> 9121/TCP 10m agents-mainframe-redis-valkey-redis-read ClusterIP 172.20.42.50 <none> 6379/TCP 10m
Створюємо файл templates/service-externalname.yaml, в ньому з {{ .Release.Namespace }} динамічно задаємо ім’я Kubernetes Namespace – бо dev/staging/prod будуть у власних Namespaces.
Описуємо два Service – для master та replica:
apiVersion: v1
kind: Service
metadata:
name: agents-mainframe-redis
labels:
app.kubernetes.io/name: agents-mainframe-redis
app.kubernetes.io/component: primary
spec:
type: ExternalName
externalName: agents-mainframe-redis-valkey-redis.{{ .Release.Namespace }}.svc.cluster.local
ports:
- name: tcp
port: 6379
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: agents-mainframe-redis-read
labels:
app.kubernetes.io/name: agents-mainframe-redis
app.kubernetes.io/component: read
spec:
type: ExternalName
externalName: agents-mainframe-redis-valkey-redis-read.{{ .Release.Namespace }}.svc.cluster.local
ports:
- name: tcp
port: 6379
protocol: TCP
Власне – тут все готово.
Тепер маємо Valkey/Redis з master-slave реплікацією, є метрики і логи.
Єдине, що залишилось зараз – це аутентифікація.
Authentication та Redis/Valkey Access Control List
Документація – Valkey ACL.
Нам для нашого сервісу треба створити трьох нових юзерів:
- admin: “нормальний” амдін для якихось ручних задач
- agents-mainframe: юзер для самого сервісу, який роблять девелопери
- replication: для master-slave реплікації самого Redis
- і “технічний” default – бо він повинен бути в чарті
В Redis/Valkey юзери та права доступу задаються через Access Control List, який визначає який юзер що може виконувати – аналогічно до того, як ми описуємо RBAC в Kubernetes. Тільки в Redis ми всі доступи описуємо прямо в конфігу – а в Kubernetes маємо Roles та RoleBindings.
Але є дуже суттєва відмінність: в Kubernetes RBAC простий і чіткий синтаксис, який легко читається, а Redis ACL – це якийсь окремий вид збочення. Може, діло звички – але ну дуже неявно все описується і треба витрачати час, щоб зрозуміти логіку.
Є генератори ACL, наприклад – redis-acl-builder.
Перше – включаємо аутентифікацію, бо в нашому values вище вона відключена:
valkey-redis:
auth:
enabled: true
Загальний синтаксис ACL:
~: шаблон ключів:- приклад:
~cache:*
- приклад:
@:категорія команд, використовується разом із+або-:- приклад:
+@read,-@dangerous
- приклад:
&: шаблон Pub/Sub-каналів:- приклад:
&events:*
- приклад:
Отже, якщо ми хочемо створити ACL для root-юзера з повними правами – то використовуємо:
+@all: всі команди~*: по всім ключам~*&*: в усіх каналах
Додавання ACL
Для тесту – робимо Kubernetes Secret вручну, потім це буде робити ESO (тут описувати не буду, бо поки не роблю, але писав більш детально у LiteLLM: AI Gateway в Kubernetes та метрики до VictoriaMetrics):
$ kk create secret generic agents-mainframe-valkey-users \ --namespace test-valkey-redis-ns \ --from-literal=admin-password='test-admin-password' \ --from-literal=default-password='test-default-password' \ --dry-run=client -o yaml | kk apply -f -
Далі додаємо usersExistingSecret з іменем цього Secret та ACL з двома юзерами:
valkey-redis:
auth:
enabled: true
usersExistingSecret: agents-mainframe-valkey-users
aclUsers:
default:
permissions: "+@all ~* &*"
passwordKey: default-password
admin:
permissions: "+@all ~* &*"
passwordKey: admin-password
Деплоїмо, підключаємось в Pod, перевіряємо:
127.0.0.1:6379> ACL GETUSER default (error) NOAUTH Authentication required.
Не пускає – чудово, аутентифікація працює.
Логінимось:
127.0.0.1:6379> AUTH admin test-admin-password OK
І перевіряємо права:
127.0.0.1:6379> ACL GETUSER default
1) "flags"
2) 1) "on"
2) "sanitize-payload"
3) "passwords"
4) 1) "7ef4d6abeee6999e26357a968bf5e429a6a050e2c84f08bce69ff5a48dc17755"
5) "commands"
6) "+@all"
7) "keys"
8) "~*"
9) "channels"
10) "&*"
11) "databases"
12) "alldbs"
13) "selectors"
14) (empty array)
127.0.0.1:6379> ACL GETUSER admin
1) "flags"
2) 1) "on"
2) "sanitize-payload"
3) "passwords"
4) 1) "f7a03f48c0e2aa2d5e55ca186c20032ddbf53b7f5f93fce387d65c3f83433e8d"
5) "commands"
6) "+@all"
7) "keys"
8) "~*"
9) "channels"
10) "&*"
11) "databases"
12) "alldbs"
13) "selectors"
14) (empty array)
ACL для юзера самого нашого сервісу може виглядати так:
...
agents-mainframe-test:
permissions: "+@read +@write +@keyspace +@transaction +@scripting +ping ~agents-mainframe:*"
passwordKey: application-password
Тут:
+@readта+@write: дозволяємо команди читання/запису+@keyspace: командиTTL,EXPIRE,DEL, перевірка існування ключів тощо+@transaction: виконанняMULTI,EXEC,WATCH+@scripting: виконання Lua/EVAL, часто використовується бібліотеками (але треба уточнювати у девелоперів)+ping: health check~agents-mainframe:*: доступ тільки для ключів з цим префіксом- Pub/Sub не дозволений взагалі
І окремо – replica юзер: доступ до application keys (~*) і Pub/Sub (&*) йому не потрібен, обрізаємо права тільки до кількох команд:
...
replication-test:
permissions: "+psync +replconf +ping"
passwordKey: replication-password
І вказуємо ім’я юзера в replica.replicationUser:
...
replica:
replicationUser: replication-test
Оновлюємо Secret – додаємо ключ replication-password:
$ kk create secret generic agents-mainframe-valkey-users \ --namespace test-valkey-redis-ns \ --from-literal=admin-password='test-admin-password' \ --from-literal=default-password='test-default-password' \ --from-literal=application-password='test-application-password' \ --from-literal=replication-password='test-replication-password' \ --dry-run=client -o yaml | kk apply -f -
Деплоїмо, перевіряємо, що все працює.
І все разом тепер:
valkey-redis:
auth:
enabled: true
usersExistingSecret: agents-mainframe-valkey-users
aclUsers:
default:
# after implementing on the app side, disable all for the 'default':
# "-@all resetkeys resetchannels"
permissions: "+@all ~* &*"
passwordKey: default-password
admin:
permissions: "+@all ~* &*"
passwordKey: admin-password
agents-mainframe-test:
# best to limit by:
# "+@read +@write +@keyspace +@transaction +@scripting +ping ~agents-mainframe:*"
# or if don't know KEYS yet:
# "+@read +@write +@keyspace +@transaction +@scripting +ping ~*"
permissions: "+@all ~* &*"
passwordKey: application-password
replication-test:
permissions: "+psync +replconf +ping"
passwordKey: replication-password
replica:
replicationUser: replication-test
Ну і поки на цьому все.
Далі вже додати моніторинг Redis і деплоїти MongoDB.
![]()
