Production-ready blockchain node infrastructure and deployment automation. Provisions AWS, Latitude, and OVH bare metal with Terraform, configures nodes with Ansible, and secures RPC access via HAProxy (Let's Encrypt TLS, API-key auth, Route 53 DNS). Includes multi-region HA with Route 53 latency-based failover. Supports Docker and binary deployments, with included block-lag monitoring and load-testing tools.
Currently supports Ethereum, Avalanche, Arbitrum, Optimism, and BNB Smart Chain - expanding to HyperEVM, Polygon, Base, Polkadot, IOTA, Celestia and more.
Ansible inventory is derived directly from Terraform state using the cloud.terraform.terraform_provider inventory plugin and Terraform ansible_host resources. The inventory plugin configuration file is auto-generated by Terraform.
┌─────────────────────────────── System Overview ────────────────────────────────┐
│ │
│ Your Machine │
│ ┌───────────────────────────────────────────────────────────────────────────┐ │
│ │ claude (CLI) ◀──▶ MCP Server (blockchain-infra, 11 tools) │ │
│ │ │ │ │
│ │ reads │ calls live RPC │ │
│ │ ┌────────────────┼─────────────────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ terraform/ ansible/ HAProxy HTTPS │ │
│ │ stacks group_vars (node endpoints) │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Cloud Infrastructure (AWS / Latitude.sh / OVH) │
│ ┌───────────────────────────────────────────────────────────────────────────┐ │
│ │ Terraform ──▶ Ansible ──▶ Docker / Binary │ │
│ │ │ │
│ │ Ethereum Avalanche Arbitrum Optimism │ │
│ │ Reth+Lighthouse AvalancheGo Nitro op-reth + op-node │ │
│ │ Geth+Prysm Classic BNB (bsc-reth) │ │
│ │ Erigon+Caplin │ │
│ │ │ │
│ │ HAProxy (TLS + API key auth + WebSocket + Route 53 DNS) │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
- Architecture
- Supported Configurations
- Repository Structure
- Quick Start
- Ethereum Node Configurations
- Avalanche Node Configurations
- Arbitrum Node Configurations
- Optimism Node Configurations
- BNB Smart Chain Node Configurations
- HAProxy (Reverse Proxy)
- HA Multi-Region (AWS)
- Documentation
- Common Commands
- Security
- AI - MCP Server
- Roadmap
- License
┌─────────────────────────────────────────────────────────────────────────┐
│ Deployment Flow │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
│ Terraform │────▶│ Ansible │────▶│ Docker / Binary │
│ (Provision) │ │ (Configure) │ │ (Runtime) │
└──────────────┘ └──────────────┘ └──────────────────────┘
│ │ │
▼ ▼ ▼
AWS: VPC, EC2, OS hardening, Docker: Reth+LH, Geth+Prysm,
EBS, EIP system setup AvalancheGo, Nitro/Classic,
Latitude: Import op-reth+op-node (Optimism)
pre-ordered BMs Binary: Erigon+Caplin, bsc-reth (BNB)
OVH: Import
pre-ordered BMs
HA Multi-Region: Route 53 latency routing + health checks
aws-ha/ stack ▶ HAProxy LBs (EIP) ▶ Backend nodes (VPC)
See: HA Multi-Region (AWS) section
| Chain | Network | Clients | Node Type | Platform | Deploy | Status |
|---|---|---|---|---|---|---|
| Ethereum | Mainnet | Reth + Lighthouse | Archive | AWS | Docker | ✅ Ready |
| Ethereum | Mainnet | Geth + Prysm | Full | AWS | Docker | ✅ Ready |
| Ethereum | Mainnet | Reth + Lighthouse | Archive | Latitude | Docker | ✅ Ready |
| Ethereum | Mainnet | Geth + Prysm | Full | Latitude | Docker | ✅ Ready |
| Ethereum | Mainnet | Erigon + Caplin | Full | Latitude | Binary | ✅ Ready |
| Ethereum | Mainnet | Erigon + Caplin | Full | AWS | Binary | ✅ Ready |
| Ethereum | Mainnet | Erigon + Caplin | Archive | Latitude | Binary | ✅ Ready |
| Ethereum | Mainnet | Erigon + Caplin | Archive | AWS | Binary | ✅ Ready |
| Ethereum | Sepolia | Erigon + Caplin | Full | Latitude | Binary | ✅ Ready |
| Ethereum | Sepolia | Erigon + Caplin | Full | AWS | Binary | ✅ Ready |
| Ethereum | Sepolia | Erigon + Caplin | Archive | Latitude | Binary | ✅ Ready |
| Ethereum | Sepolia | Erigon + Caplin | Archive | AWS | Binary | ✅ Ready |
| Ethereum | Sepolia | Geth + Prysm | Full | AWS | Docker | ✅ Ready |
| Ethereum | Sepolia | Geth + Prysm | Full | Latitude | Docker | ✅ Ready |
| Ethereum | Sepolia | Reth + Lighthouse | Archive | AWS | Docker | ✅ Ready |
| Ethereum | Sepolia | Reth + Lighthouse | Archive | Latitude | Docker | ✅ Ready |
| Avalanche | Mainnet | AvalancheGo | Full | AWS | Docker | ✅ Ready |
| Avalanche | Mainnet | AvalancheGo | Archive | AWS | Docker | ✅ Ready |
| Avalanche | Mainnet | AvalancheGo | Full | Latitude | Docker | ✅ Ready |
| Avalanche | Mainnet | AvalancheGo | Archive | Latitude | Docker | ✅ Ready |
| Avalanche | Fuji | AvalancheGo | Full | AWS | Docker | ✅ Ready |
| Avalanche | Fuji | AvalancheGo | Archive | AWS | Docker | ✅ Ready |
| Avalanche | Fuji | AvalancheGo | Full | Latitude | Docker | ✅ Ready |
| Avalanche | Fuji | AvalancheGo | Archive | Latitude | Docker | ✅ Ready |
| Arbitrum | Mainnet | arb-node (Classic) | Archive | AWS | Docker | ✅ Ready |
| Arbitrum | Mainnet | arb-node (Classic) | Archive | Latitude | Docker | ✅ Ready |
| Arbitrum | Mainnet | nitro-node | Archive | AWS | Docker | ✅ Ready |
| Arbitrum | Mainnet | nitro-node | Archive | Latitude | Docker | ✅ Ready |
| Arbitrum | Mainnet | nitro-node | Full | AWS | Docker | ✅ Ready |
| Arbitrum | Mainnet | nitro-node | Full | Latitude | Docker | ✅ Ready |
| Arbitrum | Sepolia | nitro-node | Archive | AWS | Docker | ✅ Ready |
| Arbitrum | Sepolia | nitro-node | Archive | Latitude | Docker | ✅ Ready |
| Arbitrum | Sepolia | nitro-node | Full | AWS | Docker | ✅ Ready |
| Arbitrum | Sepolia | nitro-node | Full | Latitude | Docker | ✅ Ready |
| Ethereum | Mainnet | Reth + Lighthouse | Archive | OVH | Docker | ✅ Ready |
| Ethereum | Mainnet | Geth + Prysm | Full | OVH | Docker | ✅ Ready |
| Ethereum | Mainnet | Erigon + Caplin | Full | OVH | Binary | ✅ Ready |
| Ethereum | Mainnet | Erigon + Caplin | Archive | OVH | Binary | ✅ Ready |
| Ethereum | Sepolia | Geth + Prysm | Full | OVH | Docker | ✅ Ready |
| Ethereum | Sepolia | Reth + Lighthouse | Archive | OVH | Docker | ✅ Ready |
| Ethereum | Sepolia | Erigon + Caplin | Full | OVH | Binary | ✅ Ready |
| Ethereum | Sepolia | Erigon + Caplin | Archive | OVH | Binary | ✅ Ready |
| Avalanche | Mainnet | AvalancheGo | Full | OVH | Docker | ✅ Ready |
| Avalanche | Mainnet | AvalancheGo | Archive | OVH | Docker | ✅ Ready |
| Avalanche | Fuji | AvalancheGo | Full | OVH | Docker | ✅ Ready |
| Avalanche | Fuji | AvalancheGo | Archive | OVH | Docker | ✅ Ready |
| Arbitrum | Mainnet | arb-node (Classic) | Archive | OVH | Docker | ✅ Ready |
| Arbitrum | Mainnet | nitro-node | Archive | OVH | Docker | ✅ Ready |
| Arbitrum | Mainnet | nitro-node | Full | OVH | Docker | ✅ Ready |
| Arbitrum | Sepolia | nitro-node | Archive | OVH | Docker | ✅ Ready |
| Arbitrum | Sepolia | nitro-node | Full | OVH | Docker | ✅ Ready |
| Optimism | Mainnet | op-reth + op-node | Full | AWS | Docker | ✅ Ready |
| Optimism | Mainnet | op-reth + op-node | Archive | AWS | Docker | ✅ Ready |
| BNB | Mainnet | bsc-reth | Archive | AWS | Binary | ✅ Ready |
HA Multi-Region (AWS):
| Chain | Network | Clients | Architecture | Platform | Status |
|---|---|---|---|---|---|
| Ethereum | Mainnet | Geth + Prysm | Multi-region active-active (Route 53 + HAProxy LB) | AWS HA | ✅ Ready |
| Ethereum | Mainnet | Reth + Lighthouse | Multi-region active-active (Route 53 + HAProxy LB) | AWS HA | ✅ Ready |
| Arbitrum | Mainnet | nitro-node (Full) | Multi-region active-active (Route 53 + HAProxy LB) | AWS HA | ✅ Ready |
| Arbitrum | Mainnet | nitro-node (Archive) | Multi-region active-active (Route 53 + HAProxy LB) | AWS HA | ✅ Ready |
| Avalanche | Mainnet | AvalancheGo (Full) | Multi-region active-active (Route 53 + HAProxy LB) | AWS HA | ✅ Ready |
| Avalanche | Mainnet | AvalancheGo (Archive) | Multi-region active-active (Route 53 + HAProxy LB) | AWS HA | ✅ Ready |
Single
terraform applydeploys both regions + DNS. See HA Multi-Region (AWS).
.
├── terraform/
│ ├── stacks/
│ │ ├── aws/ethereum/mainnet/ # AWS Ethereum Mainnet
│ │ ├── aws/ethereum/sepolia/ # AWS Ethereum Sepolia
│ │ ├── aws/avalanche/mainnet/ # AWS Avalanche Mainnet
│ │ ├── aws/avalanche/fuji/ # AWS Avalanche Fuji
│ │ ├── aws/arbitrum/mainnet/ # AWS Arbitrum Mainnet
│ │ ├── aws/arbitrum/sepolia/ # AWS Arbitrum Sepolia
│ │ ├── aws/optimism/mainnet/ # AWS Optimism Mainnet
│ │ ├── aws/bnb/mainnet/ # AWS BNB Smart Chain Mainnet
│ │ ├── latitude/ethereum/mainnet/ # Latitude BM Ethereum Mainnet
│ │ ├── latitude/ethereum/sepolia/ # Latitude BM Ethereum Sepolia
│ │ ├── latitude/avalanche/mainnet/ # Latitude BM Avalanche Mainnet
│ │ ├── latitude/avalanche/fuji/ # Latitude BM Avalanche Fuji
│ │ ├── latitude/arbitrum/mainnet/ # Latitude BM Arbitrum Mainnet
│ │ ├── latitude/arbitrum/sepolia/ # Latitude BM Arbitrum Sepolia
│ │ ├── ovh/ethereum/mainnet/ # OVH BM Ethereum Mainnet
│ │ ├── ovh/ethereum/sepolia/ # OVH BM Ethereum Sepolia
│ │ ├── ovh/avalanche/mainnet/ # OVH BM Avalanche Mainnet
│ │ ├── ovh/avalanche/fuji/ # OVH BM Avalanche Fuji
│ │ ├── ovh/arbitrum/mainnet/ # OVH BM Arbitrum Mainnet
│ │ ├── ovh/arbitrum/sepolia/ # OVH BM Arbitrum Sepolia
│ │ ├── aws-ha/ethereum/mainnet/ # HA Multi-Region Ethereum (Route 53 + HAProxy LB)
│ │ ├── aws-ha/arbitrum/mainnet/ # HA Multi-Region Arbitrum
│ │ └── aws-ha/avalanche/mainnet/ # HA Multi-Region Avalanche
│ └── modules/
│ ├── compute/ # AWS EC2 instances
│ ├── compute-haproxy/ # AWS EC2 for HAProxy LB (no data EBS)
│ ├── compute-latitude/ # Latitude bare metal servers
│ ├── compute-ovh/ # OVH dedicated servers
│ ├── vrack-ovh/ # OVH vRack private networking
│ ├── network/ # AWS VPC/subnets
│ ├── ha-region/ # HA per-region composition (Ethereum)
│ ├── ha-region-arb/ # HA per-region composition (Arbitrum)
│ ├── ha-region-avax/ # HA per-region composition (Avalanche)
│ └── ha-dns/ # Route 53 latency routing + health checks
├── ansible/
│ ├── playbooks/
│ │ ├── ethereum.yml # Ethereum deployment (Docker)
│ │ ├── erigon.yml # Erigon deployment (binary build)
│ │ ├── avalanche.yml # Avalanche deployment (Docker)
│ │ ├── arbitrum.yml # Arbitrum deployment (Docker: Nitro + Classic)
│ │ ├── optimism.yml # Optimism deployment (Docker: op-reth + op-node)
│ │ ├── bnb.yml # BNB Smart Chain deployment (binary: bsc-reth)
│ │ ├── raid.yml # RAID-0 setup (bare metal)
│ │ ├── haproxy-lb.yml # HAProxy LB deployment (HA multi-region)
│ │ └── site.yml # Entry point (imports all chain playbooks)
│ ├── inventory/
│ │ ├── ethereum_mainnet_aws_terraform_state.yml # Auto-generated (AWS)
│ │ ├── ethereum_mainnet_latitude_terraform_state.yml # Auto-generated (Latitude)
│ │ ├── ethereum_sepolia_*_terraform_state.yml # Auto-generated (Sepolia)
│ │ ├── avalanche_mainnet_aws_terraform_state.yml # Auto-generated (AWS)
│ │ ├── avalanche_mainnet_latitude_terraform_state.yml # Auto-generated (Latitude)
│ │ ├── avalanche_fuji_*_terraform_state.yml # Auto-generated (Fuji)
│ │ ├── arbitrum_mainnet_aws_terraform_state.yml # Auto-generated (AWS)
│ │ ├── arbitrum_mainnet_latitude_terraform_state.yml # Auto-generated (Latitude)
│ │ ├── arbitrum_sepolia_*_terraform_state.yml # Auto-generated (Sepolia)
│ │ ├── *_ovh_terraform_state.yml # Auto-generated (OVH)
│ │ ├── ethereum_mainnet_aws_ha_terraform_state.yml # Auto-generated (HA)
│ │ └── group_vars/
│ │ ├── all/ # Global defaults (certbot email, data mount)
│ │ │ └── main.yml
│ │ ├── ethereum/ # Docker node config (role=ethereum) + vault secrets
│ │ │ ├── main.yml # Versions, images, ports, vault refs
│ │ │ └── vault.yml # Encrypted (ansible-vault create)
│ │ ├── erigon/ # Erigon node config (role=erigon) + vault secrets
│ │ │ ├── main.yml # Version, RPC, Caplin, vault refs
│ │ │ └── vault.yml # Encrypted (ansible-vault create)
│ │ ├── avalanche/ # Avalanche node config (role=avalanche) + vault secrets
│ │ │ ├── main.yml # Version, image, ports, vault refs
│ │ │ └── vault.yml # Encrypted (ansible-vault create)
│ │ ├── arbitrum/ # Arbitrum node config (role=arbitrum) + vault secrets
│ │ │ ├── main.yml # Versions, images, ports, L1 RPC refs, vault refs
│ │ │ └── vault.yml # Encrypted (ansible-vault create)
│ │ ├── node_archive/ # Archive node overrides (prune.mode, commitment-history)
│ │ │ └── main.yml
│ │ ├── network_mainnet/ # Mainnet network overrides
│ │ │ └── main.yml
│ │ ├── network_sepolia/ # Sepolia network overrides (chain, RPC tuning)
│ │ │ └── main.yml
│ │ ├── network_fuji/ # Fuji testnet overrides (Avalanche)
│ │ │ └── main.yml
│ │ ├── platform_latitude/ # Latitude BM platform overrides
│ │ │ ├── main.yml # SSH user, firewall, vault refs
│ │ │ └── vault.yml # Encrypted AWS creds (BM only)
│ │ ├── platform_ovh/ # OVH BM platform overrides
│ │ │ ├── main.yml # SSH user, firewall, vault refs
│ │ │ └── vault.yml # Encrypted AWS creds (BM only)
│ │ └── haproxy_lb/ # HA HAProxy LB config
│ │ └── main.yml # No Docker, health endpoint, vault refs
│ ├── roles/
│ │ ├── common/ # Shared (system setup, disk, optional Docker)
│ │ ├── raid/ # RAID-0 setup (NVMe auto-discovery or explicit disks)
│ │ ├── ethereum/ # Ethereum Docker clients (Reth+LH, Geth+Prysm)
│ │ ├── erigon/ # Erigon binary build (built-in Caplin CL)
│ │ ├── avalanche/ # Avalanche Docker (AvalancheGo)
│ │ ├── arbitrum/ # Arbitrum Docker (Nitro + Classic)
│ │ ├── optimism/ # Optimism Docker (op-reth + op-node)
│ │ ├── bnb/ # BNB Smart Chain binary (bsc-reth, unified binary)
│ │ └── haproxy/ # Reverse proxy (TLS, API key auth, WebSocket)
├── examples/
│ ├── docker/ethereum/
│ │ ├── reth-lighthouse/ # Reth+Lighthouse (full & archive profiles)
│ │ └── geth-prysm/ # Geth+Prysm full node
│ └── systemd/
│ └── erigon/ # Erigon systemd service (binary, no Docker)
├── ai/
│ └── mcp-server/ # MCP server (Claude Code integration, 11 tools)
├── tools/
│ ├── block-lag-monitor/ # RPC block lag & latency monitoring (EVM + Avalanche)
│ └── load-test/ # JSON-RPC load testing with Paradigm's flood
├── docs/
│ ├── ansible.md # Ansible configuration guide
│ ├── ops.md # Operations guide
│ ├── terraform.md # Terraform infrastructure guide
│ └── static-inventory.md # Deploy without Terraform
├── images/ # Screenshots for documentation
└── .github/workflows/
examples/ contains standalone reference configs (Docker Compose and systemd) for testing and quick setups. Production deployments use Ansible.
tools/ contains operational utilities. See Block Lag Monitor for RPC block lag & latency monitoring and Load Test for JSON-RPC load testing with Paradigm's flood.
Naming Conventions:
- Terraform stacks:
terraform/stacks/{platform}/{chain}/{network}/- Inventory files:
ansible/inventory/{chain}_{network}_{platform}_terraform_state.yml
- HCP Terraform account (free tier available)
- Terraform >= 1.10
- Python >= 3.10
- Docker (for local testing)
- AWS: AWS CLI configured with credentials
- Latitude: Latitude.sh account + API token
- OVH: OVH account + API credentials
# Clone the repository
git clone https://github.com/danlix2000/blockchain-node-infra.git
cd blockchain-node-infra
# Create Python virtual environment
python3 -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
# Install Python dependencies (Ansible)
pip install -r requirements.txt
# Install Ansible Galaxy collections
ansible-galaxy collection install -r ansible/requirements.yml# HCP Terraform authentication (required for all platforms)
export TF_TOKEN_app_terraform_io="your-api-token"
# Or: terraform login
# AWS credentials (required for all platforms - AWS infra + Route 53 DNS)
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_REGION="us-east-1"
# Or: aws configure
# Latitude.sh credentials (for bare metal deployments)
# Note: AWS credentials above are also required for Route 53 DNS records and Certbot DNS-01 validation.
export LATITUDESH_AUTH_TOKEN="your-latitude-api-token"
# OVH credentials (for bare metal deployments)
# Note: AWS credentials above are also required for Route 53 DNS records and Certbot DNS-01 validation.
export OVH_ENDPOINT="ovh-us" # or ovh-eu, ovh-ca
export OVH_APPLICATION_KEY="your-application-key"
export OVH_APPLICATION_SECRET="your-application-secret"
export OVH_CONSUMER_KEY="your-consumer-key"
# Create credentials at: https://api.ovh.com/createToken/Note: This repo uses local execution mode - Terraform runs on your machine while TF Cloud stores state only. Set execution mode in TF Cloud: Workspace > Settings > General > Execution Mode > Local.
Each chain has its own stack directory. Pick the platform, chain, and network:
AWS:
cd terraform/stacks/aws/ethereum/mainnet # Ethereum Mainnet
cd terraform/stacks/aws/ethereum/sepolia # Ethereum Sepolia
cd terraform/stacks/aws/avalanche/mainnet # Avalanche Mainnet
cd terraform/stacks/aws/avalanche/fuji # Avalanche Fuji
cd terraform/stacks/aws/arbitrum/mainnet # Arbitrum Mainnet
cd terraform/stacks/aws/arbitrum/sepolia # Arbitrum Sepolia
cp cloud.tf.example cloud.tf
cp terraform.tfvars.example terraform.tfvars
cp nodes.tfvars.example nodes.tfvars
# Edit terraform.tfvars and nodes.tfvars for your deploymentLatitude.sh (Bare Metal):
# 1. Order servers on Latitude.sh dashboard (select plan, site, OS - no RAID)
# 2. Rename hostname in console (e.g., chi-avalanche-mainnet-full)
# 3. Copy server IDs (sv_xxx) from dashboard
cd terraform/stacks/latitude/ethereum/mainnet # Ethereum Mainnet
cd terraform/stacks/latitude/ethereum/sepolia # Ethereum Sepolia
cd terraform/stacks/latitude/avalanche/mainnet # Avalanche Mainnet
cd terraform/stacks/latitude/avalanche/fuji # Avalanche Fuji
cd terraform/stacks/latitude/arbitrum/mainnet # Arbitrum Mainnet
cd terraform/stacks/latitude/arbitrum/sepolia # Arbitrum Sepolia
cp cloud.tf.example cloud.tf
cp terraform.tfvars.example terraform.tfvars
cp nodes.tfvars.example nodes.tfvars
# Edit terraform.tfvars (set project_id, ssh_key_ids)
# Edit nodes.tfvars (set latitude_id for each pre-ordered server)OVH (Bare Metal):
# 1. Order dedicated servers on OVH console
# 2. Note service name (nsXXXXXXX.ip-XX-XX-XX.net) from dashboard
# 3. Set skip_reinstall = false for first deploy (OS install), then true
cd terraform/stacks/ovh/ethereum/mainnet # Ethereum Mainnet
cd terraform/stacks/ovh/ethereum/sepolia # Ethereum Sepolia
cd terraform/stacks/ovh/avalanche/mainnet # Avalanche Mainnet
cd terraform/stacks/ovh/avalanche/fuji # Avalanche Fuji
cd terraform/stacks/ovh/arbitrum/mainnet # Arbitrum Mainnet
cd terraform/stacks/ovh/arbitrum/sepolia # Arbitrum Sepolia
cp cloud.tf.example cloud.tf
cp terraform.tfvars.example terraform.tfvars
cp nodes.tfvars.example nodes.tfvars
# Edit terraform.tfvars (set ssh_public_key, optionally route53_zone_id for auto DNS)
# Edit nodes.tfvars (set service_name, server_model for each pre-ordered server)# Ethereum - AWS Mainnet
cd terraform/stacks/aws/ethereum/mainnet
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/ethereum_mainnet_aws_terraform_state.yml
# Ethereum - Latitude Mainnet (imports pre-ordered servers, separate workspace/state)
cd terraform/stacks/latitude/ethereum/mainnet
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/ethereum_mainnet_latitude_terraform_state.yml
# Avalanche - AWS Mainnet
cd terraform/stacks/aws/avalanche/mainnet
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/avalanche_mainnet_aws_terraform_state.yml
# Avalanche - AWS Fuji
cd terraform/stacks/aws/avalanche/fuji
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/avalanche_fuji_aws_terraform_state.yml
# Avalanche - Latitude Mainnet
cd terraform/stacks/latitude/avalanche/mainnet
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/avalanche_mainnet_latitude_terraform_state.yml
# Arbitrum - AWS Mainnet (Classic + Nitro nodes)
cd terraform/stacks/aws/arbitrum/mainnet
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/arbitrum_mainnet_aws_terraform_state.yml
# -> auto-wires arbitrum_classic_redirect_url on the nitro archive node
# Arbitrum - AWS Sepolia (Nitro only)
cd terraform/stacks/aws/arbitrum/sepolia
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/arbitrum_sepolia_aws_terraform_state.yml
# Ethereum - OVH Mainnet (installs OS on pre-ordered servers, separate workspace/state)
# First apply with skip_reinstall = false installs OS + sets hostname + SSH key
cd terraform/stacks/ovh/ethereum/mainnet
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/ethereum_mainnet_ovh_terraform_state.yml
# After successful install: set skip_reinstall = true, then terraform apply again
# Avalanche - OVH Mainnet
cd terraform/stacks/ovh/avalanche/mainnet
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/avalanche_mainnet_ovh_terraform_state.yml
# Arbitrum - OVH Mainnet
cd terraform/stacks/ovh/arbitrum/mainnet
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/arbitrum_mainnet_ovh_terraform_state.ymlSkip this step if you do not need HAProxy (TLS + API key auth). Nodes will still deploy and sync without HAProxy. You can add HAProxy later by completing this step and re-running the playbook with
--tags haproxy.
HAProxy requires encrypted vault files with API keys before deployment. Create one vault per role you deploy. For full details, see HAProxy Configuration or Ansible Vault Setup.
cd ansible
# Step 1: Create vault password file
echo "your-vault-password" > .vault_password
chmod 600 .vault_password
# Step 2: Create vault for Docker nodes (role = "ethereum")
uuidgen # Generate a UUID4 API key, copy the output
ansible-vault create inventory/group_vars/ethereum/vault.yml
# -> Editor opens. Add:
# ---
# vault_haproxy_api_key: "<paste-uuid4>"
# vault_haproxy_stats_password: "<choose-password>"
# Step 3: Create vault for Erigon nodes (role = "erigon")
uuidgen # Generate a DIFFERENT UUID4
ansible-vault create inventory/group_vars/erigon/vault.yml
# -> Editor opens. Add:
# ---
# vault_haproxy_api_key: "<paste-uuid4>"
# vault_haproxy_stats_password: "<choose-password>"
# Step 4: Create vault for Avalanche nodes (role = "avalanche")
uuidgen # Generate a DIFFERENT UUID4
ansible-vault create inventory/group_vars/avalanche/vault.yml
# -> Editor opens. Add:
# ---
# vault_haproxy_api_key: "<paste-uuid4>"
# vault_haproxy_stats_password: "<choose-password>"
# Step 5: Create vault for Arbitrum nodes (role = "arbitrum")
# Arbitrum is an L2 - requires L1 Ethereum RPC + beacon endpoints to sync.
uuidgen # Generate a DIFFERENT UUID4
ansible-vault create inventory/group_vars/arbitrum/vault.yml
# -> Editor opens. Add:
# ---
# vault_haproxy_api_key: "<paste-uuid4>"
# vault_haproxy_stats_password: "<choose-password>"
# vault_arbitrum_parent_chain_url: "https://your-eth-rpc-endpoint"
# vault_arbitrum_beacon_url: "http://your-lighthouse-node:5052"
# Step 6: Create vault for Optimism nodes (role = "optimism")
# Optimism is an L2 - requires L1 Ethereum execution RPC + beacon endpoints to sync.
# Without these the op-node will not start. Create this vault BEFORE running the playbook.
uuidgen # Generate a DIFFERENT UUID4
ansible-vault create inventory/group_vars/optimism/vault.yml
# -> Editor opens. Add:
# ---
# vault_haproxy_api_key: "<paste-uuid4>"
# vault_haproxy_stats_password: "<choose-password>"
# vault_optimism_l1_rpc_url: "https://your-eth-mainnet-rpc"
# vault_optimism_l1_beacon_url: "http://your-lighthouse-node:5052"
# Step 7: Create vault for BNB nodes (role = "bnb")
# BNB Smart Chain is L1 - no L1 RPC endpoints needed. HAProxy vault only.
uuidgen # Generate a DIFFERENT UUID4
ansible-vault create inventory/group_vars/bnb/vault.yml
# -> Editor opens. Add:
# ---
# vault_haproxy_api_key: "<paste-uuid4>"
# vault_haproxy_stats_password: "<choose-password>"
# Step 9 (Latitude/OVH BM only): Create vault for AWS credentials (Certbot Route 53)
# ansible-vault create inventory/group_vars/platform_latitude/vault.yml # Latitude BM
# ansible-vault create inventory/group_vars/platform_ovh/vault.yml # OVH BM
# -> Editor opens. Add:
# ---
# vault_haproxy_certbot_aws_access_key: "<your-iam-access-key>"
# vault_haproxy_certbot_aws_secret_key: "<your-iam-secret-key>"Use a different API key per role group.
.vault_passwordis in.gitignore- store the password securely.
Bare metal only (Latitude/OVH): Before deploying, set your trusted IPs in one file - all chains and networks use this shared list. This is required - without real IPs, UFW will block all access including SSH:
# Find your VPN/office exit IP
curl -4 ifconfig.me# ansible/inventory/group_vars/all/main.yml
firewall_allowed_ips:
- { ip: "YOUR_VPN_IP/32", comment: "VPN egress" } # Replace with real IP
- { ip: "YOUR_OFFICE_CIDR/24", comment: "Office network" } # Replace with real CIDRThe playbook validates IPs and rejects placeholders (
YOUR_*) and documentation IPs. See Bare Metal Firewall (UFW) for per-role overrides, port tables, and full details.
cd ansible
# Terraform Cloud token for inventory access
export TF_TOKEN_app_terraform_io="your-api-token"
export ANSIBLE_PRIVATE_KEY_FILE=~/.ssh/your-key.pemHost key checking is enabled. Ensure hosts are present in ~/.ssh/known_hosts before deployment.
Different node types use different Ansible playbooks. Match the playbook to the role in your nodes.tfvars:
role in nodes.tfvars |
Playbook | Clients |
|---|---|---|
ethereum |
playbooks/ethereum.yml |
Docker: Geth+Prysm, Reth+Lighthouse |
erigon |
playbooks/erigon.yml |
Binary: Erigon+Caplin |
avalanche |
playbooks/avalanche.yml |
Docker: AvalancheGo |
arbitrum |
playbooks/arbitrum.yml |
Docker: Nitro, Classic (FROZEN) |
optimism |
playbooks/optimism.yml |
Docker: op-reth + op-node |
bnb |
playbooks/bnb.yml |
Binary: bsc-reth (unified, no separate CL) |
Key rule: Run the correct playbook for each role. If your inventory has nodes with different roles, you need separate playbook runs for each role.
Every chain follows the same inventory naming convention, auto-generated by Terraform:
inventory/{chain}_{network}_{platform}_terraform_state.yml
The Ansible command is always the same shape regardless of chain:
ansible-playbook -i inventory/{chain}_{network}_{platform}_terraform_state.yml playbooks/{chain}.ymlWhen a new chain is added (HyperEVM, Base, BNB, etc.), the same pattern applies. The scenarios below show the key variations: platform differences, L2 vault prerequisites, mixed roles, and bare metal RAID.
All nodes in the inventory use the same role. Each chain has its own inventory file and playbook:
# Ethereum - Docker nodes (Geth+Prysm, Reth+Lighthouse)
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/ethereum.yml
# Ethereum - Erigon nodes (binary build)
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml
# Avalanche - AvalancheGo (single client)
ansible-playbook -i inventory/avalanche_mainnet_aws_terraform_state.yml playbooks/avalanche.yml
# Arbitrum - Nitro + Classic (Docker)
ansible-playbook -i inventory/arbitrum_mainnet_aws_terraform_state.yml playbooks/arbitrum.yml
# Optimism - op-reth + op-node (Docker)
ansible-playbook -i inventory/optimism_mainnet_aws_terraform_state.yml playbooks/optimism.yml
# BNB Smart Chain - bsc-reth (binary build, unified, no separate CL)
ansible-playbook -i inventory/bnb_mainnet_aws_terraform_state.yml playbooks/bnb.ymlThe inventory file naming convention is {chain}_{network}_{platform}_terraform_state.yml, auto-generated by Terraform.
When your nodes.tfvars has nodes with different roles (e.g., a Geth+Prysm full node and an Erigon archive node), run each playbook separately. Use --limit to target the right nodes:
# Step 1: Deploy the Ethereum full node (Geth+Prysm, Docker)
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/ethereum.yml --limit node_full
# Step 2: Deploy the Erigon archive node (binary build)
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --limit node_archiveWithout --limit, the playbook runs against all nodes in the inventory. The playbook itself targets its own role group, but using --limit is recommended to avoid unnecessary connection attempts to nodes that belong to a different role.
Deploy or reconfigure a single node without touching others:
# Terraform: provision only the erigon-arch node
terraform apply -var-file=nodes.tfvars -target='module.compute["erigon-arch"]'
# Ansible: configure only that node
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --limit node_archiveBare metal servers (Latitude, OVH) may have multiple data disks. Run RAID setup first, then the node playbook:
# Latitude example:
# Step 1: Verify connectivity
ansible -i inventory/ethereum_mainnet_latitude_terraform_state.yml all -m ping
# Step 2: Setup RAID-0 (creates /dev/md127)
ansible-playbook -i inventory/ethereum_mainnet_latitude_terraform_state.yml playbooks/raid.yml
# Step 3: Deploy nodes (choose the correct playbook for your role)
ansible-playbook -i inventory/ethereum_mainnet_latitude_terraform_state.yml playbooks/ethereum.yml # Docker nodes
ansible-playbook -i inventory/ethereum_mainnet_latitude_terraform_state.yml playbooks/erigon.yml # Erigon nodes
# OVH example:
# Step 1: Verify connectivity
ansible -i inventory/ethereum_mainnet_ovh_terraform_state.yml all -m ping
# Step 2: Setup RAID-0 (creates /dev/md127)
ansible-playbook -i inventory/ethereum_mainnet_ovh_terraform_state.yml playbooks/raid.yml
# Step 3: Deploy nodes (choose the correct playbook for your role)
ansible-playbook -i inventory/ethereum_mainnet_ovh_terraform_state.yml playbooks/ethereum.yml # Docker nodes
ansible-playbook -i inventory/ethereum_mainnet_ovh_terraform_state.yml playbooks/erigon.yml # Erigon nodesRAID disk selection (recommended workflow):
Bare metal servers often have mixed NVMe drives (e.g., 2x480GB for OS + 2x4TB for data). Auto-discovery excludes the OS disk, but may pick up wrong drives if the OS layout is non-standard. Always verify disks before running RAID:
# Step 1: After terraform apply, SSH into the server
ssh ubuntu@<server-ip>
# Step 2: Identify data disks (look for large unmounted NVMe drives)
lsblk -dpno NAME,TYPE,SIZE,MOUNTPOINT
# Example output:
# /dev/nvme0n1 disk 447.1G / <- OS (skip)
# /dev/nvme1n1 disk 447.1G <- OS mirror (skip)
# /dev/nvme2n1 disk 3.5T <- data disk (use)
# /dev/nvme3n1 disk 3.5T <- data disk (use)
# Step 3: Run RAID playbook with explicit disks for that host
# Use the hostname from terraform output (e.g., "ash-avalanche-mainnet-full")
ansible-playbook -i inventory/avalanche_mainnet_latitude_terraform_state.yml playbooks/raid.yml --limit ash-avalanche-mainnet-full \
-e '{"raid_disks":["/dev/nvme2n1","/dev/nvme3n1"]}'Auto-discovery: If
raid_disksis not passed, the role auto-discovers unmounted NVMe drives (excludes OS disk and SATA/SAS). This works for simple layouts (e.g., 2 identical data NVMe drives), but explicit disks via-eare recommended for production - especially when the server has mixed NVMe sizes.Single disk: If the server has only 1 data disk, skip
raid.yml- the common role handles single disks automatically.Why not group_vars? Each server may have different disk layouts. Passing
raid_disksvia-eextra-vars per host avoids applying a blanket disk list across all servers on a platform.
For mixed roles on bare metal, combine RAID + targeted playbooks:
# Latitude example:
ansible-playbook -i inventory/ethereum_mainnet_latitude_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/ethereum_mainnet_latitude_terraform_state.yml playbooks/ethereum.yml --limit node_full
ansible-playbook -i inventory/ethereum_mainnet_latitude_terraform_state.yml playbooks/erigon.yml --limit node_archive
# OVH example:
ansible-playbook -i inventory/ethereum_mainnet_ovh_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/ethereum_mainnet_ovh_terraform_state.yml playbooks/ethereum.yml --limit node_full
ansible-playbook -i inventory/ethereum_mainnet_ovh_terraform_state.yml playbooks/erigon.yml --limit node_archiveAvalanche uses a single client (AvalancheGo) - no separate execution + consensus layers:
# Avalanche Mainnet - AWS
ansible-playbook -i inventory/avalanche_mainnet_aws_terraform_state.yml playbooks/avalanche.yml
# Avalanche Mainnet - Latitude BM (RAID first if multiple disks)
ansible-playbook -i inventory/avalanche_mainnet_latitude_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/avalanche_mainnet_latitude_terraform_state.yml playbooks/avalanche.yml
# Avalanche Mainnet - OVH BM (RAID first if multiple disks)
ansible-playbook -i inventory/avalanche_mainnet_ovh_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/avalanche_mainnet_ovh_terraform_state.yml playbooks/avalanche.yml
# Avalanche Fuji testnet - AWS
ansible-playbook -i inventory/avalanche_fuji_aws_terraform_state.yml playbooks/avalanche.yml
# Specific node only
ansible-playbook -i inventory/avalanche_mainnet_aws_terraform_state.yml playbooks/avalanche.yml --limit node_archiveL2 chains require L1 Ethereum vault secrets before the Ansible playbook runs. The op-node and Nitro node will not start without a valid L1 RPC and beacon URL. Create the vault in Step 5 before running these commands.
Arbitrum - Mainnet requires Classic + Nitro archive nodes on separate hosts. Terraform auto-wires the classic redirect URL.
# Arbitrum Mainnet - AWS (Classic archive + Nitro archive + Nitro full)
ansible-playbook -i inventory/arbitrum_mainnet_aws_terraform_state.yml playbooks/arbitrum.yml
# Arbitrum Mainnet - Latitude BM (RAID first if multiple disks)
ansible-playbook -i inventory/arbitrum_mainnet_latitude_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/arbitrum_mainnet_latitude_terraform_state.yml playbooks/arbitrum.yml
# Arbitrum Mainnet - OVH BM (RAID first if multiple disks)
ansible-playbook -i inventory/arbitrum_mainnet_ovh_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/arbitrum_mainnet_ovh_terraform_state.yml playbooks/arbitrum.yml
# Arbitrum Sepolia - AWS (Nitro only)
ansible-playbook -i inventory/arbitrum_sepolia_aws_terraform_state.yml playbooks/arbitrum.yml
# Deploy a specific Arbitrum node only
ansible-playbook -i inventory/arbitrum_mainnet_aws_terraform_state.yml playbooks/arbitrum.yml --limit node_full
ansible-playbook -i inventory/arbitrum_mainnet_aws_terraform_state.yml playbooks/arbitrum.yml --limit node_archiveOptimism - op-reth + op-node. Snapshot required for initial sync (cannot sync from genesis on Mainnet).
# Optimism Mainnet - AWS (full node)
ansible-playbook -i inventory/optimism_mainnet_aws_terraform_state.yml playbooks/optimism.yml
# Deploy a specific Optimism node only
ansible-playbook -i inventory/optimism_mainnet_aws_terraform_state.yml playbooks/optimism.yml --limit node_full
ansible-playbook -i inventory/optimism_mainnet_aws_terraform_state.yml playbooks/optimism.yml --limit node_archiveBNB uses bsc-reth (binary build, no Docker). bsc-reth is a unified binary with no separate consensus client. No L1 vault needed. Build takes 30-60 minutes on first deploy.
Snapshots (MDBX format, download before running playbook): https://github.com/bnb-chain/bsc-snapshots
# BNB Mainnet - AWS (archive node, ~15 TB current)
cd terraform/stacks/aws/bnb/mainnet
cp cloud.tf.example cloud.tf
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your org/workspace/key
cp nodes.tfvars.example nodes.tfvars
# Uncomment and edit the archive block in nodes.tfvars
terraform init && terraform apply -var-file=nodes.tfvars
cd ../../../../../../ansible
ansible-vault create inventory/group_vars/bnb/vault.yml
ansible-playbook -i inventory/bnb_mainnet_aws_terraform_state.yml playbooks/bnb.yml
# Deploy a specific BNB node only
ansible-playbook -i inventory/bnb_mainnet_aws_terraform_state.yml playbooks/bnb.yml --limit node_archive
# BNB only - HAProxy
ansible-playbook -i inventory/bnb_mainnet_aws_terraform_state.yml playbooks/bnb.yml --tags haproxySame commands as mainnet - just use the correct inventory file for the network. The naming convention {chain}_{network}_{platform}_terraform_state.yml applies to all chains and networks. When a new chain or testnet is added, the same pattern works without any changes to this guide:
# Ethereum Sepolia - Docker nodes
ansible-playbook -i inventory/ethereum_sepolia_aws_terraform_state.yml playbooks/ethereum.yml
# Ethereum Sepolia - Erigon nodes
ansible-playbook -i inventory/ethereum_sepolia_aws_terraform_state.yml playbooks/erigon.yml
# Avalanche Fuji - AWS
ansible-playbook -i inventory/avalanche_fuji_aws_terraform_state.yml playbooks/avalanche.yml
# Arbitrum Sepolia - AWS
ansible-playbook -i inventory/arbitrum_sepolia_aws_terraform_state.yml playbooks/arbitrum.yml
# Latitude BM Sepolia - RAID + Erigon
ansible-playbook -i inventory/ethereum_sepolia_latitude_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/ethereum_sepolia_latitude_terraform_state.yml playbooks/erigon.yml
# Latitude BM Arbitrum Sepolia - RAID + Arbitrum
ansible-playbook -i inventory/arbitrum_sepolia_latitude_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/arbitrum_sepolia_latitude_terraform_state.yml playbooks/arbitrum.yml
# OVH BM Sepolia - RAID + Erigon
ansible-playbook -i inventory/ethereum_sepolia_ovh_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/ethereum_sepolia_ovh_terraform_state.yml playbooks/erigon.yml
# OVH BM Arbitrum Sepolia - RAID + Arbitrum
ansible-playbook -i inventory/arbitrum_sepolia_ovh_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/arbitrum_sepolia_ovh_terraform_state.yml playbooks/arbitrum.yml# Erigon nodes - HAProxy only
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --tags haproxy
# Docker nodes - HAProxy only
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/ethereum.yml --tags haproxy
# Avalanche nodes - HAProxy only
ansible-playbook -i inventory/avalanche_mainnet_aws_terraform_state.yml playbooks/avalanche.yml --tags haproxy
# Arbitrum nodes - HAProxy only
ansible-playbook -i inventory/arbitrum_mainnet_aws_terraform_state.yml playbooks/arbitrum.yml --tags haproxy
# Optimism nodes - HAProxy only
ansible-playbook -i inventory/optimism_mainnet_aws_terraform_state.yml playbooks/optimism.yml --tags haproxy
# BNB nodes - HAProxy only
ansible-playbook -i inventory/bnb_mainnet_aws_terraform_state.yml playbooks/bnb.yml --tags haproxy
# Deploy node without HAProxy
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --skip-tags haproxyFor manually provisioned servers without Terraform state (see Static Inventory Guide):
ansible-playbook -i inventory/ethereum_sepolia_hosts.yml playbooks/erigon.ymlDeploy multi-region active-active RPC with Route 53 latency-based failover. See the complete guides:
- Terraform HA Guide - Infrastructure setup, configuration, and deployment
- Ansible HA Guide - Node configuration, vault setup, and region targeting
Terraform - use -target to provision specific nodes:
terraform apply -var-file=nodes.tfvars -target='module.compute["full"]'
terraform apply -var-file=nodes.tfvars -target='module.compute["erigon-arch"]'Ansible - use --limit to target specific node groups:
# Single node group
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/ethereum.yml --limit node_full
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --limit node_archive
# Multiple node groups
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/ethereum.yml --limit "node_full,node_archive"| Variable | Options | Description |
|---|---|---|
role |
ethereum, erigon |
Deployment type (Docker or binary) |
execution_client |
reth, geth, erigon |
Execution layer client |
consensus_client |
lighthouse, prysm, caplin |
Consensus layer client |
node_type |
archive, full |
Node synchronization mode |
Deploys as a Lighthouse Beacon Archive with full blob retention (--prune-blobs=false, --supernode). Reth runs in default mode (no --full flag) which stores all historical state without pruning.
L2 Beacon Endpoint: Both mainnet and Sepolia Lighthouse archive nodes expose the Beacon API on port 5052 (--http-port=5052). This provides an L1 beacon endpoint for L2 chains (Arbitrum, Optimism, Base, etc.) that require historical blob data access via the /eth/v1/beacon/blob_sidecars/{block_id} API.
Checkpoint Sync: Lighthouse supports fast sync from a finalized checkpoint. This is optional but recommended as it is substantially faster than syncing from genesis. Endpoint availability varies by network:
- Mainnet:
https://mainnet.checkpoint.sigp.io(Sigma Prime, stable) - Sepolia:
http://unstable.sepolia.beacon-api.nimbus.team(Nimbus team, unstable/non-commercial). Standard Sepolia endpoints (beaconstate.info,ethpandaops.io,chainsafe.io) do not serve blobs, which Lighthouse v8.0.0+ requires for post-Deneb checkpoint sync.
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 8 vCPU | 16 vCPU (higher clock speed over core count) |
| Memory | 32 GB | 64 GB |
| Storage | 3 TB | 5+ TB (TLC NVMe recommended) |
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 4 vCPU | 8 vCPU |
| Memory | 16 GB | 32 GB |
| Storage | 1 TB | 2 TB |
Erigon v3 with built-in Caplin consensus client. Deployed as a native binary (no Docker). Single process handles both execution and consensus layers.
- Full (default): Erigon's native mode. No
--prune.modeflag needed. - Archive:
--prune.mode=archive+--experimental.commitment-history, applied viagroup_vars/node_archive/whennode_type = "archive".
| Resource | Full | Archive |
|---|---|---|
| CPU | 8+ vCPU | 16+ vCPU |
| Memory | 32+ GB | 64+ GB |
| Storage | 2+ TB | 3+ TB (RAID-0 recommended) |
| Configuration | Execution | Consensus | Total |
|---|---|---|---|
| Reth + Lighthouse (archive) | ~3 TB | ~500 GB | 5 TB |
| Geth + Prysm (full) | ~1 TB | ~200 GB | 2 TB |
| Erigon + Caplin (full) | ~1.5 TB | built-in | 2 TB |
| Erigon + Caplin (archive) | ~2.5 TB | built-in | 3 TB |
Docker nodes (Reth+Lighthouse, Geth+Prysm):
| Port | Protocol | Client | Purpose |
|---|---|---|---|
| 30303 | TCP/UDP | Reth/Geth | Execution P2P |
| 9000 | TCP/UDP | Lighthouse | Consensus P2P |
| 9001 | UDP | Lighthouse | Consensus QUIC |
| 13000 | TCP | Prysm | Consensus P2P TCP |
| 12000 | UDP | Prysm | Consensus P2P UDP |
| 8545 | TCP | All | HTTP RPC |
| 8546 | TCP | All | WebSocket RPC |
| 5052 | TCP | Lighthouse | Beacon HTTP API (L1 beacon for L2 chains) |
| 3500 | TCP | Prysm | Beacon HTTP API |
Erigon nodes (binary build, UFW firewall):
| Port | Protocol | Component | Purpose | Expose |
|---|---|---|---|---|
| 53 | TCP/UDP | System | DNS | Public |
| 30303 | TCP/UDP | Erigon | eth/68 P2P | Public |
| 30304 | TCP/UDP | Erigon | eth/69 P2P | Public |
| 42069 | TCP/UDP | Erigon | Torrent/snap sync | Public |
| 4000 | UDP | Caplin | CL discovery | Public |
| 4001 | TCP | Caplin | CL discovery | Public |
| 8545 | TCP | Erigon | HTTP RPC | Localhost |
| 8546 | TCP | Erigon | WebSocket RPC | Localhost |
| 8551 | TCP | Erigon | Engine API (JWT) | VPN only |
| 9090 | TCP | Erigon | gRPC server | VPN only |
UFW Firewall: Erigon bare metal nodes use UFW. Public ports are open to all. RPC ports (8545, 8546) bind to
127.0.0.1only - external access is via HAProxy (443). Engine API and gRPC are accessible from VPN IPs configured viafirewall_allowed_ipsingroup_vars/all/main.yml.
AvalancheGo is the official Go implementation of an Avalanche node. Unlike Ethereum, Avalanche uses a single client - no separate execution and consensus layers.
- Full (pruned): State-sync enabled, pruning enabled. Fast sync from network state.
- Archive: All pruning disabled, transaction indexing enabled. Full historical data.
Configuration is managed via JSON config files (node.json + chains/C/config.json), templated by Ansible based on node_type.
| Resource | Full | Archive |
|---|---|---|
| CPU | 8 vCPU | 16 vCPU |
| Memory | 32 GB | 64 GB |
| Storage | 1 TB | 16 TB (mainnet), 3 TB (Fuji) |
| Network | Node Type | Current Usage (Feb 2026) | Recommended |
|---|---|---|---|
| Mainnet | Full (pruned) | ~500 GB | 2 TB |
| Mainnet | Archive | ~13.5 TB | 16 TB |
| Fuji | Full (pruned) | ~500 GB | 1 TB |
| Fuji | Archive | ~2 TB | 3 TB |
| Port | Protocol | Purpose | Expose |
|---|---|---|---|
| 9650 | TCP | HTTP API / RPC | Localhost |
| 9651 | TCP/UDP | P2P staking | Public |
RPC port (9650) binds to localhost only - external access is via HAProxy (443).
Arbitrum is an L2 rollup that syncs from a sequencer feed rather than P2P peers. No inbound P2P ports are required.
Mainnet requires two nodes on separate hosts:
- Classic (FROZEN image): handles pre-Nitro history (blocks 1-~22.2M, ~700 GB). Uses
offchainlabs/arb-node:v1.4.5-e97c1a4- this image is frozen and must never be updated. - Nitro archive: handles post-Nitro history (~37 TB). Automatically redirects pre-Nitro RPC queries to the Classic node via
--execution.rpc.classic-redirect. Terraform auto-wires the IP when both nodes are in the same stack. If the classic node is external, setclassic_redirect_urlinnodes.tfvars(orarbitrum_classic_redirect_urlhost var in static inventory).
Sepolia uses Nitro only (no pre-Nitro history).
| Network | Node Key | Client | node_type | ~Disk (Feb 2026) |
|---|---|---|---|---|
| Mainnet | classic |
arb-node (FROZEN) | archive | ~700 GB |
| Mainnet | archive |
nitro-node | archive | ~37 TB |
| Mainnet | full |
nitro-node | full | ~2.1 TB |
| Sepolia | archive |
nitro-node | archive | ~20 TB |
| Sepolia | full |
nitro-node | full | ~1.7 TB |
| Node | CPU | Memory | Storage |
|---|---|---|---|
| Classic archive | 4 vCPU | 16 GB | 2 TB |
| Nitro archive (mainnet) | 32 vCPU | 128 GB | 50 TB |
| Nitro full (mainnet) | 8 vCPU | 32 GB | 5 TB |
| Nitro archive (Sepolia) | 16 vCPU | 64 GB | 25 TB |
| Nitro full (Sepolia) | 4 vCPU | 16 GB | 3 TB |
| Network | Node Type | Client | Current Usage (Feb 2026) | Recommended |
|---|---|---|---|---|
| Mainnet | Classic archive | arb-node | ~700 GB | 2 TB |
| Mainnet | Nitro archive | nitro-node | ~37 TB | 50 TB |
| Mainnet | Nitro full | nitro-node | ~2.1 TB | 5 TB |
| Sepolia | Nitro archive | nitro-node | ~20 TB | 25 TB |
| Sepolia | Nitro full | nitro-node | ~1.7 TB | 3 TB |
Snapshot init:
- Classic (pre-Nitro): Snapshots available at snapshot-explorer.arbitrum.io. Use these to speed up initial sync.
- Nitro archive (mainnet): Uses
--init.latest=archivebut archive snapshots are no longer updated since May 2024 (announcement). Only stale 2023 snapshots remain on snapshot-explorer. The node will fall back to syncing from genesis. To speed up, obtain a recent snapshot from a node operator and use--init.url=<snapshot-url>.- Nitro archive (Sepolia): Uses
--init.latest=archivebut no Sepolia archive snapshots exist. The node will sync from genesis.- Nitro full: Uses
--init.latest=prunedto auto-download a pruned snapshot fromsnapshot.arbitrum.io.
| Port | Protocol | Client | Purpose | Expose |
|---|---|---|---|---|
| 8547 | TCP | Nitro | HTTP RPC + WebSocket | Localhost |
| 8547 | TCP | Classic | HTTP RPC | Localhost |
| 8548 | TCP | Classic | WebSocket RPC | Localhost |
No inbound P2P ports needed - Arbitrum is L2 and syncs from the sequencer feed. RPC ports bind to localhost only - external access is via HAProxy (443).
Arbitrum requires two additional vault variables. Create inventory/group_vars/arbitrum/vault.yml:
ansible-vault create inventory/group_vars/arbitrum/vault.yml---
vault_haproxy_api_key: "<paste-uuid4-from-uuidgen>"
vault_haproxy_stats_password: "<choose-a-stats-password>"
vault_arbitrum_parent_chain_url: "https://your-eth-rpc-endpoint" # L1 ETH RPC
vault_arbitrum_beacon_url: "http://your-lighthouse-node:5052" # L1 beacon (port 5052)Use your own Ethereum node (Reth+Lighthouse, Geth+Prysm, or Erigon) as the parent chain and beacon URL. Sepolia Arbitrum nodes require Sepolia L1 endpoints. Mainnet nodes require mainnet endpoints.
Optimism is an L2 optimistic rollup. It uses op-reth (execution layer, fork of Reth) and op-node (consensus/derivation layer). op-node connects to an L1 Ethereum node to derive L2 state.
- Full: op-reth with
--fullflag (recent state only, faster sync). Syncs state from the sequencer in hours. - Archive: op-reth without
--fullflag (default mode, stores all historical state).
L1 Dependency: op-node requires a live L1 Ethereum RPC endpoint and Beacon API. Use your own Ethereum node (Reth+Lighthouse, Geth+Prysm, or Erigon) as the L1 source.
⚠️ Snapshot Required: Optimism cannot sync from genesis. Before running Ansible for the first time, download and restore a snapshot from https://datadirs.optimism.io/. Extract the snapshot into the op-reth data directory (/data/op-reth/) on the target host, then run the playbook. Skipping this step will leave the node stuck. op-reth requires bootstrapped Bedrock state to start deriving L2 blocks.
| Resource | Full | Archive |
|---|---|---|
| CPU | 8 vCPU | 16 vCPU |
| Memory | 32 GB | 64 GB |
| Storage | 1 TB | 3 TB |
| Network | Node Type | Recommended |
|---|---|---|
| Mainnet | Full | 1 TB |
| Mainnet | Archive | 3 TB |
| Port | Protocol | Client | Purpose | Expose |
|---|---|---|---|---|
| 30304 | TCP/UDP | op-reth | Execution P2P | Public |
| 9001 | TCP/UDP | op-node | Consensus P2P | Public |
| 8555 | TCP | op-reth | HTTP RPC | Localhost |
| 8556 | TCP | op-reth | WebSocket RPC | Localhost |
| 8557 | TCP | op-reth | Engine API (JWT) | Localhost |
| 8558 | TCP | op-node | op-node RPC | Localhost |
| 9106 | TCP | op-reth | Prometheus metrics | Localhost |
| 9206 | TCP | op-node | Prometheus metrics | Localhost |
RPC ports bind to localhost only. External access via HAProxy (443). op-reth uses port 30304 (not 30303) to avoid conflicts with co-located L1 nodes.
Optimism requires vault variables for L1 endpoints. Create inventory/group_vars/optimism/vault.yml:
ansible-vault create inventory/group_vars/optimism/vault.yml---
vault_haproxy_api_key: "<paste-uuid4-from-uuidgen>"
vault_haproxy_stats_password: "<choose-a-stats-password>"
vault_optimism_l1_rpc_url: "https://your-eth-mainnet-rpc" # L1 ETH execution RPC
vault_optimism_l1_beacon_url: "http://your-lighthouse:5052" # L1 Beacon APIDeploy:
# Optimism Mainnet - AWS
cd terraform/stacks/aws/optimism/mainnet
cp cloud.tf.example cloud.tf
cp terraform.tfvars.example terraform.tfvars
cp nodes.tfvars.example nodes.tfvars
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/optimism_mainnet_aws_terraform_state.yml
cd ansible
ansible-playbook -i inventory/optimism_mainnet_aws_terraform_state.yml playbooks/optimism.yml
# Full node only
ansible-playbook -i inventory/optimism_mainnet_aws_terraform_state.yml playbooks/optimism.yml --limit node_full
# Archive node only
ansible-playbook -i inventory/optimism_mainnet_aws_terraform_state.yml playbooks/optimism.yml --limit node_archive
# HAProxy only (update proxy without redeploying node)
ansible-playbook -i inventory/optimism_mainnet_aws_terraform_state.yml playbooks/optimism.yml --tags haproxyBNB Smart Chain (BSC) uses bsc-reth - a BSC-compatible fork of Reth. It is a unified binary with no separate consensus client needed. No L1 vault secrets needed (BNB is L1, not L2).
Build from source using make maxperf (Rust, 30-60 min). No Docker.
- Binary and releases: https://github.com/bnb-chain/reth-bsc/releases
- Snapshots: https://github.com/bnb-chain/bsc-snapshots
⚠️ Snapshot Strongly Recommended: Syncing BSC from genesis takes weeks. Download the latest MDBX snapshot from https://github.com/bnb-chain/bsc-snapshots before running Ansible, or after deployment by stopping the service and extracting the snapshot to/data/reth-db/. Follow the extraction instructions on the snapshot page.
| Resource | Archive |
|---|---|
| CPU | 32 vCPU |
| Memory | 128 GB |
| Storage | 20 TB (current ~15 TB, growing) |
| Network | Node Type | Recommended |
|---|---|---|
| Mainnet | Archive | 20 TB |
Use a single 20 TB EBS volume (gp3, 16000 IOPS, 1000 MB/s throughput). For bare metal with multiple disks, run
playbooks/raid.ymlfirst.
| Port | Protocol | Client | Purpose | Expose |
|---|---|---|---|---|
| 30303 | TCP/UDP | bsc-reth | P2P | Public |
| 30304 | TCP/UDP | bsc-reth | P2P discovery | Public |
| 8545 | TCP | bsc-reth | HTTP RPC | Localhost |
| 8546 | TCP | bsc-reth | WebSocket RPC | Localhost |
| 8551 | TCP | bsc-reth | Engine API | Localhost |
| 7061 | TCP | bsc-reth | Prometheus metrics | Localhost |
RPC ports bind to localhost only. External access via HAProxy (443).
Deploy:
# BNB Mainnet - AWS
cd terraform/stacks/aws/bnb/mainnet
cp cloud.tf.example cloud.tf
cp terraform.tfvars.example terraform.tfvars
cp nodes.tfvars.example nodes.tfvars
terraform init && terraform apply -var-file=nodes.tfvars
# -> generates ansible/inventory/bnb_mainnet_aws_terraform_state.yml
cd ../../../../../ansible
ansible-vault create inventory/group_vars/bnb/vault.yml
ansible-playbook -i inventory/bnb_mainnet_aws_terraform_state.yml playbooks/bnb.yml
# Archive node only
ansible-playbook -i inventory/bnb_mainnet_aws_terraform_state.yml playbooks/bnb.yml --limit node_archive
# HAProxy only (update proxy without redeploying node)
ansible-playbook -i inventory/bnb_mainnet_aws_terraform_state.yml playbooks/bnb.yml --tags haproxyHAProxy provides secure external RPC access with TLS termination, UUID4 API key authentication, and WebSocket support. Runs on the same node as the blockchain client, proxying to localhost backends.
Features:
- Let's Encrypt TLS via Certbot + Route 53 DNS-01 challenge (auto-renewal)
- API key authentication via
X-API-Keyheader or?apikey=URL parameter (file-based lookup) - WebSocket upgrade detection with extended timeouts (1h tunnel)
- HTTP to HTTPS redirect
- Optional HAProxy stats page and Prometheus metrics exporter
Prerequisites: Domain with AWS Route 53 hosted zone (required for Let's Encrypt DNS-01 challenge). Complete these steps before running the Ansible playbooks in Step 6. For the full vault reference, see Ansible Vault Setup.
Step 1: Create vault password file:
cd ansible
echo "your-vault-password" > .vault_password
chmod 600 .vault_password
.vault_passwordis in.gitignore. Store the password securely and share with team members out-of-band.
Step 2: Create per-role secrets vault (API key + stats password):
Each role group gets its own vault so different node types use different API keys. Create one vault per role you deploy:
uuidgen # Generate a UUID4 API key, copy the output
# For Docker nodes (Reth+Lighthouse, Geth+Prysm):
ansible-vault create inventory/group_vars/ethereum/vault.yml
# For Erigon nodes (Erigon+Caplin):
ansible-vault create inventory/group_vars/erigon/vault.yml
# For Avalanche nodes (AvalancheGo):
ansible-vault create inventory/group_vars/avalanche/vault.yml
# For Arbitrum nodes (Nitro + Classic):
ansible-vault create inventory/group_vars/arbitrum/vault.ymlYour editor opens for each file. Add the following variables, save, and close.
For ethereum, erigon, avalanche vaults:
---
vault_haproxy_api_key: "<paste-uuid4-from-uuidgen>"
vault_haproxy_stats_password: "<choose-a-stats-password>"For the arbitrum vault (requires two additional L1 connection secrets):
---
vault_haproxy_api_key: "<paste-uuid4-from-uuidgen>"
vault_haproxy_stats_password: "<choose-a-stats-password>"
vault_arbitrum_parent_chain_url: "https://your-eth-rpc-endpoint" # L1 ETH RPC
vault_arbitrum_beacon_url: "http://your-lighthouse-node:5052" # L1 beaconUse a different
uuidgenoutput for each role group. This isolates API keys so compromising one key does not expose other node types.
Step 3: Create bare metal vault (skip for AWS-only deployments):
Create an IAM user with Route 53 permissions (route53:GetChange, route53:ListHostedZones, route53:ListHostedZonesByName, route53:ChangeResourceRecordSets), then:
# Latitude BM
ansible-vault create inventory/group_vars/platform_latitude/vault.yml
# OVH BM
ansible-vault create inventory/group_vars/platform_ovh/vault.ymlYour editor opens. Add the following variables, save, and close:
---
vault_haproxy_certbot_aws_access_key: "<iam-access-key>"
vault_haproxy_certbot_aws_secret_key: "<iam-secret-key>"AWS EC2 nodes use IAM instance profiles for Certbot Route 53 - no AWS credentials needed. Only bare metal (Latitude, OVH) nodes require this vault file.
Step 4: Set deployment config (plaintext, not secrets):
Edit inventory/group_vars/all/main.yml and set the certbot email:
haproxy_certbot_email: "your-real-email@yourcompany.com"AWS:
haproxy_domainis set per-node via Terraform: adddomaininnodes.tfvarsandroute53_zone_idinterraform.tfvars. Terraform creates Route 53 A records and passeshaproxy_domainas a host variable automatically. See Terraform DNS.Bare metal (Latitude/OVH): Add
domaininnodes.tfvarsandroute53_zone_idinterraform.tfvars- Terraform creates Route 53 A records and passeshaproxy_domainto Ansible automatically (same as AWS). RequiresAWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEYenv vars. Without Route 53, skiproute53_zone_idand create the A record manually in your DNS provider.
Deploy (see Step 6: Configure and Deploy Services for full scenarios):
cd ansible
# Full deploy (node + HAProxy)
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml
# HAProxy only (skip node rebuild)
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --tags haproxy
# Node only (skip HAProxy)
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --skip-tags haproxyVerify:
API key can be passed via X-API-Key header or ?apikey= URL parameter:
# Method 1: API key via header
curl -s https://rpc.eth.example.com/ \
-H "X-API-Key: <your-uuid4-api-key>" \
-X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}'
# Method 2: API key via URL parameter
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' \
"https://rpc.eth.example.com/?apikey=<your-uuid4-api-key>"
# Test without API key (should return 403)
curl -s https://rpc.eth.example.com/
# Test WebSocket
wscat -c wss://rpc.eth.example.com/ -H "X-API-Key: <your-uuid4-api-key>"eth_syncing returns sync progress (currentBlock, highestBlock) or false when fully synced. For continuous monitoring, use the Block Lag Monitor to track block lag, latency, and estimate sync ETA across all your nodes.
For vault management commands (edit, view, rekey), see Ansible Vault Setup.
| Port | Protocol | Purpose | Expose |
|---|---|---|---|
| 80 | TCP | HTTP (redirects to HTTPS) | Public |
| 443 | TCP | HTTPS (TLS + API key auth) | Public |
| 8404 | TCP | Stats page (localhost only) | Local |
| 8405 | TCP | Prometheus metrics (optional) | Local |
All variables have sensible defaults in roles/haproxy/defaults/main.yml. Backend ports are auto-mapped from execution client ports via group_vars:
- Erigon (
group_vars/erigon/main.yml):haproxy_backend_http_port: "{{ erigon_http_port }}"(8545) - Docker (
group_vars/ethereum/main.yml):haproxy_backend_http_port: "{{ ports.execution_http }}"(8545) - Avalanche (
group_vars/avalanche/main.yml):haproxy_backend_http_port: "{{ ports.http_api }}"(9650) - Arbitrum (
group_vars/arbitrum/main.yml):haproxy_backend_http_port: "{{ ports.http_rpc }}"(8547),haproxy_backend_ws_enabled: true
Multi-region high-availability RPC setup with automatic failover. Deploys to multiple AWS regions with a single terraform apply. Existing single-node stacks (aws/, latitude/, ovh/) remain unchanged.
Client -> rpc.eth.example.com
|
Route 53 (latency-based routing + health checks)
|
+--------+--------+
| |
us-east-1 eu-west-1
| |
HAProxy-1 (EIP) HAProxy-1 (EIP) <- HTTPS :443 /health for Route 53
HAProxy-2 (EIP) HAProxy-2 (EIP) <- API key auth, TLS termination
| |
v v
Node-1 (AZ-a) Node-1 (AZ-a) <- Backend nodes (private IP RPC)
Node-2 (AZ-b) Node-2 (AZ-b) <- No per-node HAProxy
How it works:
- Route 53 latency-based routing - one A record per region with all HAProxy IPs. Returns the nearest healthy region
- Per-endpoint health checks (HTTPS
:443 /health, 30s interval) - HAProxy returns 200 if at least one backend is healthy - Per-region calculated health checks - healthy if ANY HAProxy in the region is up (OR logic)
- Automatic failover - if all HAProxies in a region fail, Route 53 routes traffic to the other region
- No NLB - Route 53 handles failover directly to HAProxy EIPs (simpler, lower cost)
- Public subnets for backend nodes (needed for P2P, avoids NAT gateway cost)
Key design decisions:
- Single Terraform state with provider aliases for multi-region (one
terraform apply) - Single Ansible inventory file for all HA nodes (use
--limitfor region targeting) - HAProxy discovers backend IPs via Ansible inventory groups (no hardcoded IPs)
- Backend nodes get chain-specific internal RPC SG (Ethereum: 8545/8546, Arbitrum: 8547, Avalanche: 9650 - all from VPC CIDR) - no per-node HAProxy
- HAProxy nodes are lightweight EC2 instances (no data EBS, always EIP)
Terraform modules:
| Module | Description |
|---|---|
compute-haproxy/ |
Lightweight EC2 for HAProxy LB (root volume only, always EIP) |
ha-region/ |
Per-region composition for Ethereum (EL+CL P2P SGs, internal RPC 8545/8546) |
ha-region-arb/ |
Per-region composition for Arbitrum (no P2P - L2, internal RPC 8547) |
ha-region-avax/ |
Per-region composition for Avalanche (P2P 9651 staking, internal RPC 9650) |
ha-dns/ |
Route 53 health checks + latency-based A records |
network/ (modified) |
internal_rpc SG gated by enable_internal_rpc (backward compatible) |
Prerequisites: Terraform Cloud account + workspace (aws-ha-ethereum-mainnet), terraform login (or TF_TOKEN_app_terraform_io env var), AWS credentials, key pairs per region.
1. Configure Terraform:
cd terraform/stacks/aws-ha/ethereum/mainnet
cp cloud.tf.example cloud.tf
# Edit cloud.tf: set your TFC organization name and workspace name
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your valuesKey variables to fill in terraform.tfvars:
domain = "rpc.eth.example.com" # Must be within your Route 53 hosted zone
route53_zone_id = "Z0123456789ABCDEF" # aws route53 list-hosted-zones
regions = {
"us-east-1" = {
vpc_cidr = "10.100.0.0/16"
subnet_cidrs = ["10.100.0.0/24", "10.100.1.0/24"]
azs = ["us-east-1a", "us-east-1b"]
key_name = "my-key-us-east" # EC2 key pair name in us-east-1
ssh_private_key_file = "~/.ssh/my-key-us-east.pem"
allowed_ssh_cidrs = ["YOUR_VPN_IP/32"] # Restrict SSH to your IPs
node_count = 2 # Backend nodes (auto round-robin across AZs)
node_type = "full" # "full" or "archive"
node_instance_type = "m7i.2xlarge"
node_data_volume_size_gb = 3000
haproxy_count = 2 # HAProxy LBs (auto round-robin across AZs)
haproxy_instance_type = "c6i.large"
}
"eu-west-1" = { ... } # Same structure, different CIDR/AZs/key
}2. Deploy all infrastructure (single command):
terraform init && terraform apply
# Creates per region: VPC, 2x backend nodes + EBS, 2x HAProxy + EIP, security groups, IAM profiles
# Creates shared: 4x Route 53 endpoint health checks, 2x calculated region checks, 2x latency A records
# Also generates: ansible/inventory/ethereum_mainnet_aws_ha_terraform_state.yml3. Setup vault secrets and certbot email:
cd ansible
# Set certbot email (used for Let's Encrypt TLS cert registration)
# Edit inventory/group_vars/all/main.yml:
# haproxy_certbot_email: "your-real-email@yourcompany.com"
# Vault for dedicated HAProxy LB nodes (real secrets)
ansible-vault create inventory/group_vars/haproxy_lb/vault.yml
# Add:
# vault_haproxy_api_key: "your-uuid4-api-key" # generate: uuidgen
# vault_haproxy_stats_password: "your-stats-password"
# Vault for backend Ethereum nodes (empty - HAProxy is on dedicated nodes, not co-located)
# Required: Ansible loads vault files for every group a host belongs to
ansible-vault create inventory/group_vars/ethereum/vault.yml
# Add:
# vault_haproxy_api_key: ""
# vault_haproxy_stats_password: ""4. Verify connectivity to all nodes:
export TF_TOKEN_app_terraform_io="your-api-token" # needed for inventory plugin to read TF state
ansible -i inventory/ethereum_mainnet_aws_ha_terraform_state.yml all -m ping
# All nodes in both regions should return pong before proceeding5. Configure backend blockchain nodes (all regions):
ansible-playbook -i inventory/ethereum_mainnet_aws_ha_terraform_state.yml playbooks/ethereum.yml6. Configure HAProxy load balancers (all regions):
ansible-playbook -i inventory/ethereum_mainnet_aws_ha_terraform_state.yml playbooks/haproxy-lb.yml
# Issues TLS cert via Let's Encrypt + Route 53 DNS-01. Route 53 health checks go green within ~2 min.Target a specific region (optional):
ansible-playbook -i inventory/ethereum_mainnet_aws_ha_terraform_state.yml playbooks/ethereum.yml --limit region_us_east_1
ansible-playbook -i inventory/ethereum_mainnet_aws_ha_terraform_state.yml playbooks/haproxy-lb.yml --limit region_eu_west_1Same flow as Ethereum HA. Key differences:
- No inbound P2P ports - Arbitrum L2 syncs from the sequencer feed only
- RPC port 8547 - HTTP and WS on the same port (Nitro, no 8545/8546 split)
- Single process -
nitro(no EL+CL split). Noexecution_client/consensus_clientvars - Instance type -
m7i.4xlargerecommended. 2.5 TB data volume for mainnet full (~37 TB archive) - Requires L1 secrets - parent chain ETH RPC + beacon endpoint with full blob history in vault
Prerequisites: TFC workspace aws-ha-arbitrum-mainnet, a running Ethereum L1 node with blob retention (Lighthouse archive --supernode --prune-blobs=false or Prysm 7.1.0+ --semi-supernode).
# 1. Terraform
cd terraform/stacks/aws-ha/arbitrum/mainnet
cp cloud.tf.example cloud.tf # Edit: TFC org + workspace "aws-ha-arbitrum-mainnet"
cp terraform.tfvars.example terraform.tfvars # Edit: domain, route53_zone_id, key_name, allowed_ssh_cidrs, regions
terraform init && terraform apply
# Also generates: ansible/inventory/arbitrum_mainnet_aws_ha_terraform_state.yml# 2. Vault secrets
cd ansible
# HAProxy LB vault - skip if already created for another chain (shared across chains)
ansible-vault create inventory/group_vars/haproxy_lb/vault.yml
# vault_haproxy_api_key: "your-uuid4" # generate: uuidgen
# vault_haproxy_stats_password: "your-stats-password"
# Arbitrum backend nodes vault (includes required L1 connection secrets)
ansible-vault create inventory/group_vars/arbitrum/vault.yml
# vault_haproxy_api_key: "" # empty - HAProxy on dedicated nodes
# vault_haproxy_stats_password: ""
# vault_arbitrum_parent_chain_url: "https://your-eth-mainnet-rpc"
# vault_arbitrum_beacon_url: "http://your-lighthouse:5052"# 3. Verify connectivity
export TF_TOKEN_app_terraform_io="your-api-token"
ansible -i inventory/arbitrum_mainnet_aws_ha_terraform_state.yml all -m ping
# 4. Deploy backend Arbitrum nodes (all regions)
ansible-playbook -i inventory/arbitrum_mainnet_aws_ha_terraform_state.yml playbooks/arbitrum.yml
# 5. Deploy HAProxy LBs (all regions)
ansible-playbook -i inventory/arbitrum_mainnet_aws_ha_terraform_state.yml playbooks/haproxy-lb.yml
# Region-specific targeting (optional)
ansible-playbook -i inventory/arbitrum_mainnet_aws_ha_terraform_state.yml playbooks/arbitrum.yml --limit region_us_east_1
ansible-playbook -i inventory/arbitrum_mainnet_aws_ha_terraform_state.yml playbooks/haproxy-lb.yml --limit region_eu_west_1Same flow as Ethereum HA. Key differences:
- P2P staking port 9651 - must be publicly reachable for validator/peer communication
- HTTP API port 9650 - C-chain JSON-RPC at
/ext/bc/C/rpc(not root/) - Single process -
avalanchego(no EL+CL split). Noexecution_client/consensus_clientvars - Health check URI - HAProxy uses
/ext/bc/C/rpcautomatically (set ingroup_vars/avalanche/main.yml) - Instance type -
m7i.2xlargerecommended. 2 TB data volume for mainnet (~1.5 TB and growing)
Prerequisites: TFC workspace aws-ha-avalanche-mainnet. No L1 node required.
# 1. Terraform
cd terraform/stacks/aws-ha/avalanche/mainnet
cp cloud.tf.example cloud.tf # Edit: TFC org + workspace "aws-ha-avalanche-mainnet"
cp terraform.tfvars.example terraform.tfvars # Edit: domain, route53_zone_id, key_name, allowed_ssh_cidrs, regions
terraform init && terraform apply
# Also generates: ansible/inventory/avalanche_mainnet_aws_ha_terraform_state.yml# 2. Vault secrets
cd ansible
# HAProxy LB vault - skip if already created for another chain (shared across chains)
ansible-vault create inventory/group_vars/haproxy_lb/vault.yml
# vault_haproxy_api_key: "your-uuid4" # generate: uuidgen
# vault_haproxy_stats_password: "your-stats-password"
# Avalanche backend nodes vault
ansible-vault create inventory/group_vars/avalanche/vault.yml
# vault_haproxy_api_key: "" # empty - HAProxy on dedicated nodes
# vault_haproxy_stats_password: ""# 3. Verify connectivity
export TF_TOKEN_app_terraform_io="your-api-token"
ansible -i inventory/avalanche_mainnet_aws_ha_terraform_state.yml all -m ping
# 4. Deploy backend Avalanche nodes (all regions)
ansible-playbook -i inventory/avalanche_mainnet_aws_ha_terraform_state.yml playbooks/avalanche.yml
# 5. Deploy HAProxy LBs (all regions)
ansible-playbook -i inventory/avalanche_mainnet_aws_ha_terraform_state.yml playbooks/haproxy-lb.yml
# Region-specific targeting (optional)
ansible-playbook -i inventory/avalanche_mainnet_aws_ha_terraform_state.yml playbooks/avalanche.yml --limit region_us_east_1
ansible-playbook -i inventory/avalanche_mainnet_aws_ha_terraform_state.yml playbooks/haproxy-lb.yml --limit region_eu_west_1All HA nodes are in a single inventory file. Use --limit with these groups:
| Group | Contains | Purpose |
|---|---|---|
haproxy_lb |
All HAProxy LB nodes | Target HAProxy LB playbook |
region_us_east_1 |
All nodes in us-east-1 | Region targeting |
region_eu_west_1 |
All nodes in eu-west-1 | Region targeting |
backend_us_east_1 |
Backend nodes in us-east-1 | HAProxy template backend discovery |
backend_eu_west_1 |
Backend nodes in eu-west-1 | HAProxy template backend discovery |
ethereum / arbitrum / avalanche |
All backend nodes | Chain role group (determines which playbook to use) |
HAProxy nodes have host var haproxy_backend_group set to their region's backend group (e.g., backend_us_east_1). The HAProxy template uses this to discover backend IPs from the Ansible inventory -- no hardcoded addresses.
Run these after Ansible completes to confirm the full stack is healthy. Steps are the same for all chains - swap the domain and RPC method where noted.
1. DNS - confirm Route 53 is returning HAProxy IPs:
# Returns the nearest region's HAProxy IPs (latency-based routing)
dig rpc.eth.yourdomain.com # Ethereum
dig rpc.arb.yourdomain.com # Arbitrum
dig rpc.avax.yourdomain.com # Avalanche
dig @8.8.8.8 rpc.eth.yourdomain.com # From Google DNS (remote client view)2. HAProxy health endpoint - confirm TLS and backend connectivity:
# Get HAProxy EIPs from Terraform output (swap chain dir as needed)
cd terraform/stacks/aws-ha/ethereum/mainnet # or arbitrum/mainnet, avalanche/mainnet
terraform output us_east_1_haproxy_instances
terraform output eu_west_1_haproxy_instances
# Hit /health directly on each HAProxy EIP (same for all chains)
# 200 = at least one backend is up; 503 = all backends down
curl -sk https://<haproxy-eip>/health3. Route 53 health check status:
# List all health checks and status
aws route53 list-health-checks \
--query 'HealthChecks[*].[Id,HealthCheckConfig.IPAddress,HealthCheckConfig.Type]' \
--output table
# Get status of a specific health check
aws route53 get-health-check-status --health-check-id <id> \
--query 'HealthCheckObservations[*].[Region,StatusReport.Status]' --output table4. RPC via HA domain - confirm API key auth and routing:
API_KEY="your-api-key"
# --- Ethereum / Arbitrum (JSON-RPC at /) ---
DOMAIN="https://rpc.eth.yourdomain.com" # or rpc.arb.yourdomain.com
# Should return 403 (API key required)
curl -s -o /dev/null -w "%{http_code}" -X POST $DOMAIN \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","id":1}'
# Should return block number (hex)
curl -s -X POST $DOMAIN \
-H "Content-Type: application/json" -H "X-API-Key: $API_KEY" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","id":1}'
# Sync status (false = fully synced)
curl -s -X POST $DOMAIN \
-H "Content-Type: application/json" -H "X-API-Key: $API_KEY" \
-d '{"jsonrpc":"2.0","method":"eth_syncing","id":1}'
# --- Avalanche (C-chain JSON-RPC at /ext/bc/C/rpc) ---
DOMAIN="https://rpc.avax.yourdomain.com"
curl -s -X POST $DOMAIN/ext/bc/C/rpc \
-H "Content-Type: application/json" -H "X-API-Key: $API_KEY" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","id":1}'5. Compare block numbers per region (checks both regions are in sync):
API_KEY="your-api-key"
DOMAIN="rpc.eth.yourdomain.com" # swap for arb/avax domain
# Force request to a specific region's HAProxy (bypasses DNS routing)
for IP in <us-east-haproxy-1-eip> <us-east-haproxy-2-eip> <eu-west-haproxy-1-eip> <eu-west-haproxy-2-eip>; do
BLOCK=$(curl -sk -X POST https://$IP \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
--resolve "$DOMAIN:443:$IP" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","id":1}' | jq -r '.result')
echo "$IP -> block $((16#${BLOCK#0x}))"
done- Terraform - Infrastructure documentation
- Ansible - Configuration management guide
- Operations - Operations guide
- Block Lag Monitor - RPC block lag & latency monitoring (any EVM chain + Avalanche)
- Load Test - JSON-RPC load testing with Paradigm's flood
- Static Inventory - Deploy without Terraform (manual servers)
Working directories: Terraform runs from its stack directory (
terraform/stacks/{platform}/{chain}/{network}/). Ansible runs from theansible/directory. See Deployment Scenarios for complete end-to-end guides.
# ── Terraform ─────────────────────────────────────────────────────────────────
# Each stack has its own directory - cd into it before running commands.
cd terraform/stacks/aws/ethereum/mainnet # AWS Ethereum Mainnet
cd terraform/stacks/aws/ethereum/sepolia # AWS Ethereum Sepolia
cd terraform/stacks/aws/avalanche/mainnet # AWS Avalanche Mainnet
cd terraform/stacks/aws/avalanche/fuji # AWS Avalanche Fuji
cd terraform/stacks/aws/arbitrum/mainnet # AWS Arbitrum Mainnet
cd terraform/stacks/aws/arbitrum/sepolia # AWS Arbitrum Sepolia
cd terraform/stacks/latitude/ethereum/mainnet # Latitude Ethereum Mainnet
cd terraform/stacks/latitude/avalanche/mainnet # Latitude Avalanche Mainnet
cd terraform/stacks/latitude/arbitrum/mainnet # Latitude Arbitrum Mainnet
cd terraform/stacks/ovh/ethereum/mainnet # OVH Ethereum Mainnet
cd terraform/stacks/ovh/avalanche/mainnet # OVH Avalanche Mainnet
cd terraform/stacks/ovh/arbitrum/mainnet # OVH Arbitrum Mainnet
cd terraform/stacks/aws-ha/ethereum/mainnet # HA Multi-Region Ethereum
cd terraform/stacks/aws-ha/arbitrum/mainnet # HA Multi-Region Arbitrum
cd terraform/stacks/aws-ha/avalanche/mainnet # HA Multi-Region Avalanche
terraform init && terraform apply -var-file=nodes.tfvars # All nodes
terraform apply -var-file=nodes.tfvars -target='module.compute["full"]' # Specific node
terraform plan -var-file=nodes.tfvars # Preview
# ── Ansible ───────────────────────────────────────────────────────────────────
# All ansible commands run from the ansible/ directory.
cd ansible
export TF_TOKEN_app_terraform_io="your-api-token"
export ANSIBLE_PRIVATE_KEY_FILE=~/.ssh/your-key.pem
# Match playbook to node role:
# role = "ethereum" -> playbooks/ethereum.yml (Docker: Geth+Prysm, Reth+Lighthouse)
# role = "erigon" -> playbooks/erigon.yml (Binary: Erigon+Caplin)
# role = "avalanche" -> playbooks/avalanche.yml (Docker: AvalancheGo)
# role = "arbitrum" -> playbooks/arbitrum.yml (Docker: Nitro + Classic)
# Deploy all nodes of a role
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/ethereum.yml
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml
ansible-playbook -i inventory/avalanche_mainnet_aws_terraform_state.yml playbooks/avalanche.yml
ansible-playbook -i inventory/arbitrum_mainnet_aws_terraform_state.yml playbooks/arbitrum.yml
# Deploy specific node group
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --limit node_full
ansible-playbook -i inventory/avalanche_mainnet_aws_terraform_state.yml playbooks/avalanche.yml --limit node_archive
ansible-playbook -i inventory/arbitrum_mainnet_aws_terraform_state.yml playbooks/arbitrum.yml --limit node_archive
# Latitude BM (RAID first, then node playbook)
ansible-playbook -i inventory/ethereum_mainnet_latitude_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/ethereum_mainnet_latitude_terraform_state.yml playbooks/erigon.yml
ansible-playbook -i inventory/avalanche_mainnet_latitude_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/avalanche_mainnet_latitude_terraform_state.yml playbooks/avalanche.yml
ansible-playbook -i inventory/arbitrum_mainnet_latitude_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/arbitrum_mainnet_latitude_terraform_state.yml playbooks/arbitrum.yml
# OVH BM (RAID first, then node playbook)
ansible-playbook -i inventory/ethereum_mainnet_ovh_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/ethereum_mainnet_ovh_terraform_state.yml playbooks/erigon.yml
ansible-playbook -i inventory/avalanche_mainnet_ovh_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/avalanche_mainnet_ovh_terraform_state.yml playbooks/avalanche.yml
ansible-playbook -i inventory/arbitrum_mainnet_ovh_terraform_state.yml playbooks/raid.yml
ansible-playbook -i inventory/arbitrum_mainnet_ovh_terraform_state.yml playbooks/arbitrum.yml
# HAProxy only (update proxy without redeploying node)
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --tags haproxy
ansible-playbook -i inventory/avalanche_mainnet_aws_terraform_state.yml playbooks/avalanche.yml --tags haproxy
ansible-playbook -i inventory/arbitrum_mainnet_aws_terraform_state.yml playbooks/arbitrum.yml --tags haproxy
# Deploy node without HAProxy
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --skip-tags haproxy
# Dry-run (preview changes without applying)
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --check --diff
# Static inventory (no Terraform)
ansible-playbook -i inventory/ethereum_sepolia_hosts.yml playbooks/erigon.yml
# ── HA Multi-Region ─────────────────────────────────────────────────────────
# Single terraform apply deploys both regions + DNS. Single inventory file.
cd terraform/stacks/aws-ha/ethereum/mainnet # Ethereum HA
cd terraform/stacks/aws-ha/arbitrum/mainnet # Arbitrum HA
cd terraform/stacks/aws-ha/avalanche/mainnet # Avalanche HA
terraform init && terraform apply # All regions + DNS
cd ansible
# Ethereum HA
ansible-playbook -i inventory/ethereum_mainnet_aws_ha_terraform_state.yml playbooks/ethereum.yml # All backends
ansible-playbook -i inventory/ethereum_mainnet_aws_ha_terraform_state.yml playbooks/haproxy-lb.yml # All HAProxy LBs
ansible-playbook -i inventory/ethereum_mainnet_aws_ha_terraform_state.yml playbooks/ethereum.yml --limit region_us_east_1 # Region-specific
ansible-playbook -i inventory/ethereum_mainnet_aws_ha_terraform_state.yml playbooks/haproxy-lb.yml --limit region_eu_west_1 # Region-specific
# Arbitrum HA
ansible-playbook -i inventory/arbitrum_mainnet_aws_ha_terraform_state.yml playbooks/arbitrum.yml # All backends
ansible-playbook -i inventory/arbitrum_mainnet_aws_ha_terraform_state.yml playbooks/haproxy-lb.yml # All HAProxy LBs
ansible-playbook -i inventory/arbitrum_mainnet_aws_ha_terraform_state.yml playbooks/arbitrum.yml --limit region_us_east_1 # Region-specific
# Avalanche HA
ansible-playbook -i inventory/avalanche_mainnet_aws_ha_terraform_state.yml playbooks/avalanche.yml # All backends
ansible-playbook -i inventory/avalanche_mainnet_aws_ha_terraform_state.yml playbooks/haproxy-lb.yml # All HAProxy LBs
ansible-playbook -i inventory/avalanche_mainnet_aws_ha_terraform_state.yml playbooks/avalanche.yml --limit region_us_east_1 # Region-specific
# ── HA Verification ──────────────────────────────────────────────────────────
dig rpc.eth.yourdomain.com # DNS - check Route 53 returns HAProxy IPs
curl -sk https://<haproxy-eip>/health # HAProxy health (200=ok, 503=all backends down)
curl -s -X POST https://rpc.eth.yourdomain.com \
-H "Content-Type: application/json" -H "X-API-Key: $API_KEY" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","id":1}' # Block number via HA domain
curl -s -X POST https://rpc.eth.yourdomain.com \
-H "Content-Type: application/json" -H "X-API-Key: $API_KEY" \
-d '{"jsonrpc":"2.0","method":"eth_syncing","id":1}' # Sync status (false=synced)
aws route53 get-health-check-status --health-check-id <id> \
--query 'HealthCheckObservations[*].[Region,StatusReport.Status]' --output table # Route 53 health- SSH access restricted to VPN/office IP ranges only
- Per-client P2P security groups: each node receives only the SGs for its clients (e.g.,
p2p-reth,p2p-lighthouse,p2p-avalanche) - IAM instance profile scoped to Route 53 only (Certbot DNS-01 for HAProxy TLS)
- SSH keys managed via AWS key pairs (never commit private keys)
- Optional Elastic IPs for stable endpoints
- IMDSv2 required (no IMDSv1)
UFW is enabled on all bare metal nodes (Latitude, OVH) via group_vars/platform_latitude/main.yml and group_vars/platform_ovh/main.yml. Disabled on AWS where Security Groups handle network access.
Trusted IP configuration: All chains share a single firewall_allowed_ips list in group_vars/all/main.yml. Replace YOUR_VPN_IP with real IPs before deploying:
# ansible/inventory/group_vars/all/main.yml
firewall_allowed_ips:
- { ip: "YOUR_VPN_IP/32", comment: "VPN egress" } # curl -4 ifconfig.me
- { ip: "YOUR_OFFICE_CIDR/24", comment: "Office network" }Per-role overrides are optional - uncomment erigon_firewall_allowed_ips, ethereum_firewall_allowed_ips, avalanche_firewall_allowed_ips, or arbitrum_firewall_allowed_ips in the respective group_vars if a chain needs different IPs.
Default policy: deny all incoming, allow all outgoing, then open:
| Role | Public Ports (open to all) | Restricted (trusted IPs only) |
|---|---|---|
| Ethereum | 30303/tcp+udp (EL P2P), 9000/tcp+udp (CL P2P), 9001/udp (QUIC) | RPC (8545), WS (8546), Engine (8551) |
| Erigon | 30303/tcp+udp (P2P), 42069/tcp+udp (Torrent), 4000/udp (Caplin), 4001/tcp (Caplin), 5353/udp (DNS) | RPC (8545), WS (8546), Engine (8551), gRPC (9090) |
| Avalanche | 9651/tcp (P2P staking) | HTTP API (9650) |
| Arbitrum | None (L2 - outbound sequencer feed only) | RPC (8547), WS (8548) |
| HAProxy | 80/tcp (HTTP redirect), 443/tcp (HTTPS) | Stats (8404) |
- SSH (22) restricted to
firewall_allowed_ipsonly - set your VPN/office IPs before deploying - All Docker containers use
network_mode: host- UFW is the only access control layer on bare metal
- Let's Encrypt TLS certificates via Certbot + Route 53 DNS-01 (auto-renewal every 90 days)
- UUID4 API key required for all RPC requests (validated via
X-API-Keyheader or?apikey=URL parameter) - API keys stored in
/etc/haproxy/api-keys/(file-based lookup, never inline in config) - RPC backends bind to
127.0.0.1(not accessible externally without HAProxy) - Stats page bound to localhost only by default (port 8404)
This repository includes a Model Context Protocol (MCP) server that exposes the entire infrastructure as tools Claude can call directly inside Claude Code.
Ask Claude questions like "What versions are running on Optimism?" or "Which of my nodes are lagging?" and Claude reads the actual Terraform configs, Ansible group_vars, and live RPC endpoints to answer with real data.
Config tools (read local files, no network required):
| Tool | What it does |
|---|---|
list_chains |
Scans terraform/stacks/ and returns all chains, networks, and platforms |
get_versions |
Returns all pinned client versions from ansible/inventory/group_vars |
get_chain_config |
Returns full Ansible config for a given chain |
check_drift |
Compares group_vars versions against role defaults. Flags any mismatches |
get_stack_info |
Returns Terraform variables and example config for a stack |
Live node tools (require a reachable RPC endpoint):
| Tool | What it does |
|---|---|
node_health |
Calls a live RPC endpoint and returns block height, sync status, and peer count |
get_block_lag |
Fetches the latest block and calculates lag in seconds against current time |
Block lag monitor tools (require tools/block-lag-monitor/endpoints.json):
| Tool | What it does |
|---|---|
list_endpoint_groups |
Lists all named endpoint groups from endpoints.json |
check_endpoint_group |
Checks block lag for every endpoint in a named group |
Load test config tools (require tools/load-test/nodes.json):
| Tool | What it does |
|---|---|
list_load_test_nodes |
Lists all node targets configured for load testing |
list_load_test_suites |
Lists available test suites and their RPC method coverage |
Works with both the Claude Code CLI (
claudein terminal) and the Claude Code VS Code extension.
# 1. Create a virtual environment and install dependencies
python3 -m venv .venv && source .venv/bin/activate
pip install -r ai/mcp-server/requirements.txt
# 2. Verify the tools work (no client needed)
python3 ai/mcp-server/test_tools.py
# 3. Register with Claude Code (detects your paths automatically)
bash ai/mcp-server/setup-mcp.sh
# 4a. CLI - open from the repo directory
claude
# Inside the session: /mcp -> blockchain-infra connected 11 tools. Press Esc then ask.
# 4b. VS Code extension - open this repo folder in VS Code.
# Check the Claude Code MCP panel - blockchain-infra shows Connected.Once connected in Claude Code:
What client versions are configured across all chains?
Are there any version mismatches between group_vars and role defaults?
Show me the full config for the Optimism chain.
Check the health of my Ethereum node at https://eth-full.example.com
Which of my nodes in the eth-mainnet group are lagging?
What RPC methods does the archive load test suite cover?
Full setup guide and project structure: ai/mcp-server/README.md
- Ethereum Mainnet archive node (Reth + Lighthouse) - Docker
- Ethereum Mainnet full node (Geth + Prysm) - Docker
- Ethereum Mainnet full node (Erigon + Caplin) - Binary build
- Ethereum Mainnet archive node (Erigon + Caplin) - Binary build
- Ethereum Sepolia full node (Erigon + Caplin) - Binary build
- Ethereum Sepolia archive node (Erigon + Caplin) - Binary build
- Ethereum Sepolia full node (Geth + Prysm) - Docker
- Ethereum Sepolia archive node (Reth + Lighthouse) - Docker
- HAProxy reverse proxy (TLS + API key auth + WebSocket)
- Avalanche Mainnet (AvalancheGo) - Full + Archive - Docker
- Avalanche Fuji testnet (AvalancheGo) - Full + Archive - Docker
- Arbitrum Mainnet (Nitro + Classic archive) - Full + Archive - Docker
- Arbitrum Sepolia (Nitro) - Full + Archive - Docker
- Optimism Mainnet (op-reth + op-node) - Full + Archive - Docker
- BNB Smart Chain Mainnet (bsc-reth) - Archive - Binary build
- Monitoring stack (Prometheus, Grafana)
- Additional chains (Base, IOTA, Polkadot, Xai, Polygon, HyperEVM and more)
- Multi-cloud support (GCP, Azure)
- High availability (multi-region Route 53 + HAProxy LB) - Ethereum - AWS
- High availability (multi-region Route 53 + HAProxy LB) - Arbitrum - AWS
- High availability (multi-region Route 53 + HAProxy LB) - Avalanche - AWS
MIT License - See LICENSE for details.