Valkey: запуск в Kubernetes – Helm, monitoring, ACL
0 (0)

Автор |  22/07/2026
Click to rate this post!
[Total: 0 Average: 0]

Запускаємо новий сервіс для проекту, і цьому сервісу треба підняти 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:

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 (aka redis.conf)
  • auth і usersExistingSecret: а оце якраз для ESO та паролів юзерів
  • replica: включаємо master-slave
    • replicas: вкажемо 2 (плюс буде 1 master)
    • replicationUser: поки лишимо дефолт, потім можна буде зробити окремого
    • disklessSync: налаштування реплікації – “традиційний” RDB (писав у  RDB Persistence), або напряму через master mem bufer => network socket => replica mem bufer
    • minReplicasToWrite та 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 have
  • valkeyLogLevel: додамо в наш values відразу
  • metrics: включаємо метрики, використовує стандартний oliver006/redis_exporter
    • serviceMonitor: включимо, щоб 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:

Redis: запуск Valkey в Kubernetes

І метрики в самій VictoriaMetrics:

Redis: запуск Valkey в KubernetesАлерти і 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.

Loading