Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

Если вы DevOps-инженер и впервые сталкиваетесь с блокчейном, разворачивание Ethereum ноды может показаться непростой и непривычной задачей. В отличие от стандартных веб-приложений, здесь два независимых клиента, аутентификация через JWT, огромные объёмы данных и долгая синхронизация. Но если разобраться в особенностях, то деплой через Kubernetes не сложнее, чем настройка обычного stateful-приложения.

В этой статье мы разберем из каких слоёв состоит современный Ethereum (после Merge), как взаимодействуют execution и consensus клиенты, где в этой цепочке используется JWT. Я покажу практический процесс разворачивания и его отладки Ethereum full node в k8s кластере с использованием написанного для этого Helm chart. В качестве основной среды для инфраструктуры я буду использовать свой MacBook Pro c macOS 26.3, на котором развернем kubernetes кластер через k3d. Далее, мы задеплоим ноду, проверим её работоспособность, разберём типичные проблемы и их решения. В качестве клиентов будем использовать Geth (execution layer) и Nimbus (consensus layer).

Содержание:

Архитектура Ethereum: “два мозга” одной ноды

С сентября 2022 года (после события The Merge) Ethereum работает на Proof-of-Stake консенсусе и разделён на два слоя. Представьте, что Ethereum нода — это организм с двумя полушариями мозга: одно отвечает за «что делать» (выполнение транзакций), другое — за «правильно ли это» (консенсус). Оба работают независимо, но без связи между ними нода мертва.

Execution Layer (EL) - «левое полушарие». Это рациональная часть ноды, «исполнитель» (runtime). Слой, который:

Это рациональная часть ноды, «исполнитель». Слой, который:

  • Обрабатывает транзакции и выполняет смарт-контракты в EVM
  • Поддерживает mempool
  • Хранит состояние блокчейна (accounts, contracts, balances)
  • Предоставляет JSON-RPC API для dApps и кошельков (по умолчанию порт 8545)

Популярные клиенты: Geth, Nethermind, Erigon, Besu, Reth

Consensus Layer (CL) — «правое полушарие». А это «контролёр», который следит за порядком в сети. Слой, который:

  • Управляет консенсусом и валидацией блоков
  • Контролирует стейкинг и работу валидаторов
  • Финализирует блоки через механизм Casper FFG
  • Предоставляет Beacon API (по умолчанию порт 5052)

Популярные клиенты: Lighthouse, Prysm, Teku, Nimbus, Lodestar

Как они взаимодействуют?

Как и полушария мозга связанные между собой, EL и CL обмениваются данными через Engine API — защищённый JSON-RPC интерфейс поверх HTTP. Для взаимной аутентификации используется JWT token — своего рода общий секрет, подтверждающий что оба клиента «свои». Без него клиенты не смогут обмениваться данными и нода не будет работать.

┌─────────────┐ ┌─────────────┐ │ Geth │◄───────►│ Nimbus │ │ (EL) │ Engine │ (CL) │ │ :8551 │ API │ :9000 │ └──────┬──────┘ (JWT) └──────┬──────┘ │ │ │ JSON-RPC Beacon API │ :8545 │ ▼ ▼ External External Apps Apps

Системные требования

Если верить сайту, то для EL (geth) понадобятся следующие ресурсы:

  • Fast CPU with 4+ cores
  • 16 GB+ RAM
  • Fast SSD with 2+TB
  • 25+ MBit/s bandwidth

Но я бы рекомендовал для production закладывать больше CPU и RAM, так как нагрузка на сеть ethereum постоянно меняется. Для архивной ноды, которая хранит полные исторический данные понадобится больше места (~12TB).

В процессе деплоя ноды и тестирования работоспособности helm-чарта такие ресурcы не понадобятся.

Для CL (Nimbus Beacon Chain), согласно руководству на сайте, потребуется:

  • 4GB RAM
  • 200GB Storage space

Предварительная настройка окружения

Итак, для деплоя ноды на macos нам понадобятся - helm, kubernetes (k3d) и клиент для взаимодействия kubectl. Если у вас уже поднят свой k8s кластер, то шаг с установкой и настройкой k3d можно пропустить.

Установка инструментов

brew install k3d kubectl helm jq

Проверка версий

k3d version kubectl version helm version
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

Шаг 1: Создание локального k8s кластера

Создаём k3s кластер через k3d с нужными параметрами:

k3d cluster create eth-local \ --servers 1 \ --agents 1 \ --k3s-arg "--disable=traefik@server:0" \ -p "80:80@loadbalancer" \ -p "443:443@loadbalancer"

Параметры:

  • --servers 1 — один control plane узел (для локального теста достаточно)
  • --agents 1 — один worker узел (опционально, можно без него)
  • --disable=traefik — по умолчанию k3d использует traefik в качестве ingress-контроллера, для большей универсальности я буду использовать nginx. (опционально, можно оставить traefik)
  • -p "80:80@loadbalancer" -p "443:443@loadbalancer" — пробрасываем порты с хоста в k3d loadbalancer (без этого ingress не будет доступен на localhost)
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

Проверяем, что кластер создан

kubectl cluster-info
<i>Адреса основных компонентов кластера</i>
Адреса основных компонентов кластера

Выведем список k8s нод:

kubectl get nodes
<i>Ноды кластера - мастер и воркер</i>
Ноды кластера - мастер и воркер

Если видите статус `Ready` у всех воркеров значит кластер готов к работе.

Шаг 2: Установка Ingress Controller

Для тестирования доступа к сервису изнутри кластера по внешнему url в helm chart созданы ingress. Чтобы эта связка работала, установим ingress-nginx-controller:

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm install ingress-nginx ingress-nginx/ingress-nginx \ --set controller.ingressClassResource.name=nginx-dev \ --set controller.ingressClassResource.controllerValue=k8s.io/ingress-nginx-dev \ --set controller.ingressClass=nginx-dev

При установке, мы задаем имя ingressClass=nginx-dev, чтобы не путаться в будущем. Контроллер создаст Service типа LoadBalancer, k3d свяжет его с проброшенными портами 80/443, и сервис станет доступен на локальном хосте.

kubectl get po kubectl get svc ingress-nginx-controller
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

В колонке EXTERNAL-IP будут внутренние Docker IP нод k3d (например, как у нас 172.19.0.3,172.19.0.4) —это нормально. Трафик с localhost:80/443 всё равно попадает в ingress благодаря -p флагам при создании кластера (macOS → k3d loadbalancer → ingress-nginx). В целом, для локального теста ingress не обязателен (можно обойтись port-forward), но мы хотим проверить как отрабатывают ingress-сущности нашего чарта — поэтому он нужен.

Шаг 3: Подготовка конфигурации helm chart

Сгенерировать JWT secret

JWT — это по сути 32-байтовый hex-секрет, который должен быть одинаковым у Geth и Nimbus:

openssl rand -hex 32

Создание values.local.yaml

Текущий values.yaml ориентирован на production (крупные ресурсы и диски), для обычного MacBook это будет тяжело. Сделаем локальный override-файл в папке с чартом helm/values.local.yaml и вставим туда токен, который мы генерировали выше:

network: mainnet jwt: secret: "<вывод команды выше>" geth: resources: requests: cpu: "300m" memory: 1Gi limits: cpu: "1" memory: 2Gi # Для локального теста удобнее hostPath (без больших PVC) persistence: type: hostPath hostPath: /var/lib/rancher/k3s/storage/geth-local config: maxpeers: 30 extraArgs: - "--syncmode=snap" - "--cache=512" beacon: enabled: true resources: requests: cpu: "200m" memory: 1Gi limits: cpu: "1" memory: 2Gi persistence: type: hostPath hostPath: /var/lib/rancher/k3s/storage/nimbus-local config: extraArgs: - "--max-peers=20" # Ingress: включаем и настраиваем на тестовый домен ingress: enabled: true className: nginx-dev hosts: - host: eth-node.test.com paths: - path: / service: geth port: 8545 pathType: Prefix

В целом, если его не указывать, то chart сгенерирует случайный секрет автоматически. Но при следующем обновлении чарта (helm upgrade) он будет обновляться каждый раз, что приведёт к рестарту подов и перезапуску ноды, это создаст время простоя (downtime). Для стабильного локального тестирования лучше его зафиксировать.

Для ingress свяжем тестовый домен с нашим локальным хостом, добавив в /etc/hosts следующие строки:

echo "127.0.0.1 eth-node.test.com" | sudo tee -a /etc/hosts

Далее сделаю несколько замечаний по поводу конфигурации helm chart.

Почему StatefulSet, а не Deployment?

Ethereum нода — это типичный stateful workload, как база данных. Geth хранит сотни гигабайт состояния блокчейна, Nimbus — десятки гигабайт данных beacon chain. Потеря этих данных означает пересинхронизацию с нуля, которая может занять дни. Поэтому в Helm chart используется StatefulSet, а не Deployment.

StatefulSet даёт нам то, чего Deployment не может: стабильные имена подов (eth-node-geth-0 вместо случайного хеша), что упрощает мониторинг и отладку; гарантированную привязку к хранилищу — при рестарте или перепланировании под получит обратно тот же PersistentVolume с данными блокчейна; упорядоченное обновление — поды обновляются последовательно, и мы можем убедиться, что один клиент успешно поднялся, прежде чем обновлять следующий.

По сути, правило простое: если приложение хранит данные, которые нельзя потерять — используйте StatefulSet.

Про хранилище: hostPath vs PVC

В нашем values.local.yaml мы используем persistence.type=hostPath — данные блокчейна пишутся напрямую в директорию на ноде кластера. Для локального тестирования через k3d это удобно: не нужно настраивать StorageClass и выделять PVC.

Однако у hostPath есть серьёзные ограничения. Данные привязаны к конкретной ноде — если под переедет на другую ноду кластера, он потеряет всю БД блокчейна. Отсутствует контроль доступа — любой под на той же ноде может читать и писать в эту директорию. Нет встроенных механизмов бэкапа и расширения диска.

Для production-окружений рекомендуется использовать PersistentVolumeClaim (PVC) с подходящим StorageClass. В облаке это, как правило, SSD-диски провайдера (gp3 в AWS, pd-ssd в GCP, managed-csi в Azure). Но есть сценарий, когда hostPath в production оправдан — bare-metal кластеры с локальными NVMe-дисками. Если ваш Kubernetes развёрнут на физических серверах, каждый из которых оснащён быстрым NVMe-накопителем, то hostPath (или его более безопасная альтернатива — local PersistentVolume) даёт максимальную производительность без сетевых задержек. Именно так работают многие production-стейкинг операторы: выделенный сервер с NVMe, нода привязана к конкретной машине через nodeAffinity, а данные лежат на локальном диске. В этом случае привязка к ноде — не баг, а фича, потому что Ethereum нода и так stateful и не предполагает миграцию между серверами «на лету».

Шаг 4: Проверка helm chart перед установкой

Склонируем чарт из репо и запустим проверку:

git clone https://github.com/MindPhaser34/ethereum.git cd ethereum helm lint helm/ -f helm/values.local.yaml helm template eth-node helm/ \ -f helm/values.local.yaml > ./eth-node-rendered.yaml
<i>Линт прошел без ошибок</i>
Линт прошел без ошибок

Так же можно запустить тестовый деплой чарта с параметром --dry-run

helm upgrade --install eth-node helm/ \ -f helm/values.local.yaml --dry-run

Шаг 5: Установка helm chart в k8s кластер

helm upgrade --install eth-node helm/ \ -f helm/values.local.yaml
Деплой прошел успешно
Деплой прошел успешно

Проверим список подов:

kubectl get pods -o wide -w
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

Статусы Pending/ContainerCreating/PodInitializing — это нормально на старте, k8s стягивает docker образы клиентов, создаёт volumes и подготавливает поды. Переход в Running может занять 30-60 секунд

На скриншоте видно, что поды залетели на разные ноды кубернетиса: geth на k3d-eth-local-agent-0, а nimbus на k3d-eth-local-server-0.

Проверяем логи Geth:

kubectl logs -f statefulset/eth-node-ethereum-node-geth
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

Geth поднялся и ждет когда синхронизируется beacon chain - Nimbus. После синхронизации напишет "Block synchronisation started" и пойдет дальше синхронизироваться сам.

Проверяем логи Nimbus:

kubectl logs -f statefulset/eth-node-ethereum-node-nimbus
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

В последней строчке он показывает какой слот (slot=...) и эпоха (epoch=...) сейчас актуальные и сколько по времени осталось синкаться (sync="..d..h..m (0.00%). Всё хорошо, процесс пошел.

Шаг 6: Smoke-тесты (RPC/Beacon)

Пробросим порт Geth RPC на локальный хост и в другой вкладке проверим что он отвечает:

kubectl port-forward svc/eth-node-ethereum-node-geth 8545:8545 &

Проверяем версию и процесс синка:

curl -s http://127.0.0.1:8545 \ -H 'content-type: application/json' \ --data '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}' | jq curl -s http://127.0.0.1:8545 \ -H 'content-type: application/json' \ --data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' | jq
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

Отлично, geth отвечает.

Теперь сделаем тоже самое с nimbus:

kubectl port-forward svc/eth-node-ethereum-node-nimbus 5052:5052 & curl -i http://127.0.0.1:5052/eth/v1/node/health curl -i http://127.0.0.1:5052/eth/v1/node/version
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

Nimbus корректно возвращает версию

Ожидаемые ответы: HTTP 200 (нода синхронизирована) или HTTP 206 (нода синкается). Оба варианта означают, что Nimbus работает корректно.

Services в helm chart настроены корректно и проксируют запрос к нужному контейнеру. Давайте проверим ingress, кинем RPC-запросы через внешний url:

curl -s http://eth-node.test.com \ -H 'content-type: application/json' \ --data '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}' | jq
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps
curl -i http://eth-node.test.com/beacon/eth/v1/node/health curl -s http://eth-node.test.com/beacon/eth/v1/node/version
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

Отлично. По внешнему url тоже всё работает корректно.

Обновление чарта

Давайте попробуем обновить версию Nimbus. Проверяем актуальную версию на официальном GitHub репо(на момент написания статьи v26.2.1), находим актуальные тег для образа docker, и добавляем изменения в values.local.yaml:

... beacon: ... image: repository: statusim/nimbus-eth2 tag: multiarch-v26.2.1 pullPolicy: IfNotPresent ...

После чего применяем изменения

helm upgrade eth-node helm/ -f helm/values.local.yaml

Проверяем

kubectl get pods -o wide -w
Видно этапы обновления пода с nimbus - сначала он состоянии PodInitializing, потом поменялся на Running cо статусом контейнеров Ready 0/1 и затем Ready 1/1
Видно этапы обновления пода с nimbus - сначала он состоянии PodInitializing, потом поменялся на Running cо статусом контейнеров Ready 0/1 и затем Ready 1/1

Проверяем версию:

curl -s http://eth-node.test.com/beacon/eth/v1/node/version​
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

Отлично, версия обновилась.

Бонус: Включаем Checkpoint Sync для Nimbus

Если вы обратили внимание на логи Nimbus, синхронизация с нуля занимает значительное время — от нескольких дней до недели и более. Nimbus скачивает и верифицирует каждый слот начиная с генезис-блока, что надёжно, но очень медленно.

Существует альтернативный подход — checkpoint sync (он же trusted node sync). Суть в том, что вместо синхронизации с нуля, Nimbus скачивает недавнее финализированное состояние цепочки от доверенного узла и начинает работу уже от этой точки. Это сокращает время синхронизации CL с дней до нескольких минут.

Как это работает:

Nimbus запрашивает последний финализированный checkpoint (состояние + блок) через Beacon API доверенного узла, записывает их в свою базу данных и стартует с этой точки. Исторические блоки до checkpoint'а дозагружаются параллельно (backfill) — это не блокирует работу ноды.

Как использовать:

Для Nimbus checkpoint sync добавим в конфиг дополнительные аргументы запуска:

beacon: ... config: extraArgs: - "--max-peers=20" - "--external-beacon-api-url=https://beaconstate.info" - "--trusted-block-root=0x_LATEST_BLOCK_ROOT"

Флаг --external-beacon-api-url указывает публичный endpoint для загрузки checkpoint state, а --trusted-block-root — корень блока для верификации. Чтобы получить актуальный trusted-block-root можно выполнить команду:

curl -s --compressed https://lodestar-mainnet.chainsafe.io/eth/v1/beacon/headers/finalized | jq -r '.data.root'

Либо скопировать на сайте beaconstate.info

Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

При успешном выполнении в логах Nimbus вы увидите:

Starting light client topics="lightcl" trusted_block_root=some(...) Sync overseer starting topics="overseer" wall_slot=13722356 (...) Scheduling first slot action topics="beacnde" startTime= (...) Slot start (...)
Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

А в логах клиента Geth что-то вроде этого:

Как задеплоить Ethereum Full Node в Kubernetes: практический гайд для DevOps

Публичные endpoint'ы для checkpoint sync:

Сообщество поддерживает список проверенных публичных endpoint'ов на eth-clients.github.io/checkpoint-sync-endpoints. Популярные для mainnet:

— https://beaconstate.info — https://mainnet-checkpoint-sync.attestant.io — https://mainnet-checkpoint-sync.stakely.io

Альтернативный вариант — initContainer с trustedNodeSync:

Можно обойтись без поиска trusted-block-root, создав следующий init-container с trustedNodeSync:

initContainers: - name: checkpoint-sync image: statusim/nimbus-eth2:multiarch-v26.2.1 command: - /bin/sh - -c - | if [ -d "/data/db" ]; then echo "Database already exists, skipping checkpoint sync" else echo "No database found, starting checkpoint sync..." /home/user/nimbus_beacon_node trustedNodeSync \ --network=mainnet \ --data-dir=/data \ --trusted-node-url=https://beaconstate.info \ --backfill=false fi volumeMounts: - name: nimbus-data mountPath: /data

оба варианта работают только при первом запуске на пустой базе данных. Если Nimbus уже начал синхронизацию с генезиса, эти флаги будут проигнорированы. Чтобы переключиться на checkpoint sync, нужно сначала удалить директорию /data/db в контейнере Nimbus.

Заключение

Мы прошли полный путь от создания локального k8s кластера до работающей Ethereum full node с проверенными RPC и Beacon API эндпоинтами. Давайте зафиксируем ключевые моменты.

Два клиента обязательны — после The Merge Ethereum не работает без связки execution (Geth) и consensus (Nimbus) клиентов. Они общаются через Engine API и без общего JWT секрета нода не заведётся.

StatefulSet, а не Deployment — блокчейн нода хранит сотни гигабайт данных, потеря которых означает дни на пересинхронизацию. StatefulSet гарантирует стабильные имена подов и привязку к хранилищу.

Checkpoint sync экономит дни — вместо синхронизации consensus layer с генезиса (неделя+), checkpoint sync позволяет начать работу за минуты. Главное помнить, что он работает только на пустой БД.

Терпение при синхронизации — даже с checkpoint sync для Nimbus, Geth всё равно потребуется значительное время на snap sync execution layer. Для mainnet это часы или дни в зависимости от железа и канала. Ускорить процесс можно через snapshots.

Мониторинг обязателен — без Prometheus/Grafana вы не поймёте, на каком этапе синхронизация, сколько пиров подключено и есть ли проблемы с ресурсами . Настройка мониторинга для Ethereum ноды — тема для отдельной статьи.

Helm chart делает процесс воспроизводимым: развернуть ноду в другом кластере — вопрос нового values файла. Весь код чарта доступен в репозитории (ссылка).

Если вам интересны практические DevOps гайды по блокчейну и AI, разборы инфраструктуры, Docker/Kubernetes конфигурации и troubleshooting реальных проблем — подписывайтесь на телеграм-канал Art of Chain. Никаких спекуляций и трейдинга — только техническая сторона блокчейн-инфраструктуры.

1
Начать дискуссию