Skip to content

danlix2000/blockchain-node-infra

Repository files navigation

Blockchain Node Infrastructure

CI Terraform AWS Latitude OVH Ansible License: MIT

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, and Arbitrum - expanding to BNB, Optimism, 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.

Table of Contents

Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                              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                          Binary: Erigon+Caplin
   pre-ordered BMs
   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

Supported Configurations

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

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 apply deploys both regions + DNS. See HA Multi-Region (AWS).

Repository Structure

.
├── 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
│   │   ├── 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)
│   │   ├── 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)
│   │   └── 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)
├── 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

Quick Start

Prerequisites

  • 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

1. Clone and Setup Environment

# 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

2. Setup Credentials

# 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.

3. Configure Infrastructure

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 deployment

Latitude.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)

4. Provision Infrastructure

# 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.yml

5. Setup HAProxy Secrets (Vault)

Skip 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")
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 (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_password is 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 CIDR

The playbook validates IPs and rejects placeholders (YOUR_*) and documentation IPs. See Bare Metal Firewall (UFW) for per-role overrides, port tables, and full details.

6. Configure and Deploy Services

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.pem

Host 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)

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.

Deployment Scenarios

Scenario 1: Single role - all nodes same type

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

The inventory file naming convention is {chain}_{network}_{platform}_terraform_state.yml, auto-generated by Terraform.

Scenario 2: Mixed roles - Ethereum + Erigon on the same platform

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_archive

Without --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.

Scenario 3: Specific node only

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_archive

Scenario 4: Bare Metal (RAID-0 + node deployment)

Bare 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 nodes

RAID 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_disks is 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 -e are 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_disks via -e extra-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_archive

Scenario 5: Avalanche nodes

Avalanche 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_archive

Scenario 6: Arbitrum nodes

Arbitrum requires vault secrets for L1 RPC endpoints before deployment. Mainnet requires Classic + Nitro archive nodes on separate hosts; Terraform auto-wires the 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_archive

Scenario 7: Sepolia / Fuji / Arbitrum Sepolia testnets

Same commands, different inventory file:

# 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

Scenario 8: HAProxy only (update proxy without redeploying node)

# 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

# Deploy node without HAProxy
ansible-playbook -i inventory/ethereum_mainnet_aws_terraform_state.yml playbooks/erigon.yml --skip-tags haproxy

Scenario 9: Static inventory (no Terraform)

For manually provisioned servers without Terraform state (see Static Inventory Guide):

ansible-playbook -i inventory/ethereum_sepolia_hosts.yml playbooks/erigon.yml

Scenario 10: HA multi-region (AWS)

Deploy multi-region active-active RPC with Route 53 latency-based failover. See the complete guides:

Deploying Specific Nodes

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"

Ethereum Node Configurations

Client Selection

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

Reth + Lighthouse (Archive)

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)

Reth system requirements

Geth + Prysm (Full Node)

Resource Minimum Recommended
CPU 4 vCPU 8 vCPU
Memory 16 GB 32 GB
Storage 1 TB 2 TB

Geth hardware requirements

Erigon + Caplin (Binary Build)

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.mode flag needed.
  • Archive: --prune.mode=archive + --experimental.commitment-history, applied via group_vars/node_archive/ when node_type = "archive".
Resource Full Archive
CPU 8+ vCPU 16+ vCPU
Memory 32+ GB 64+ GB
Storage 2+ TB 3+ TB (RAID-0 recommended)

Erigon hardware requirements

Storage Sizing

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

Network Ports

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.1 only - external access is via HAProxy (443). Engine API and gRPC are accessible from VPN IPs configured via firewall_allowed_ips in group_vars/all/main.yml.

Avalanche Node Configurations

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.

AvalancheGo

Resource Full Archive
CPU 8 vCPU 16 vCPU
Memory 32 GB 64 GB
Storage 1 TB 16 TB (mainnet), 3 TB (Fuji)

Avalanche system requirements

Storage Sizing (Avalanche)

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

Network Ports (Avalanche)

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 Node Configurations

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, set classic_redirect_url in nodes.tfvars (or arbitrum_classic_redirect_url host 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

Hardware Requirements (Arbitrum)

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

Storage Sizing (Arbitrum)

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=archive but 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=archive but no Sepolia archive snapshots exist. The node will sync from genesis.
  • Nitro full: Uses --init.latest=pruned to auto-download a pruned snapshot from snapshot.arbitrum.io.

Network Ports (Arbitrum)

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).

Vault Setup (Arbitrum)

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.

HAProxy (Reverse Proxy)

HAProxy 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-Key header 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

HAProxy Configuration

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_password is 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.yml

Your 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 beacon

Use a different uuidgen output 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.yml

Your 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_domain is set per-node via Terraform: add domain in nodes.tfvars and route53_zone_id in terraform.tfvars. Terraform creates Route 53 A records and passes haproxy_domain as a host variable automatically. See Terraform DNS.

Bare metal (Latitude/OVH): Add domain in nodes.tfvars and route53_zone_id in terraform.tfvars - Terraform creates Route 53 A records and passes haproxy_domain to Ansible automatically (same as AWS). Requires AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY env vars. Without Route 53, skip route53_zone_id and 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 haproxy

Verify:

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.

HAProxy Ports

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

HAProxy Variables

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

HA Multi-Region (AWS)

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.

HA Architecture

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 --limit for 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)

HA Deployment Guide - Ethereum

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 values

Key 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.yml

3. 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 proceeding

5. Configure backend blockchain nodes (all regions):

ansible-playbook -i inventory/ethereum_mainnet_aws_ha_terraform_state.yml playbooks/ethereum.yml

6. 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_1

HA Deployment Guide - Arbitrum

Same 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); no execution_client/consensus_client vars
  • Instance type - m7i.4xlarge recommended; 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_1

HA Deployment Guide - Avalanche

Same 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); no execution_client/consensus_client vars
  • Health check URI - HAProxy uses /ext/bc/C/rpc automatically (set in group_vars/avalanche/main.yml)
  • Instance type - m7i.2xlarge recommended; 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_1

HA Ansible Groups

All 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.

HA Verification

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>/health

3. 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 table

4. 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

Documentation

Common Commands

Working directories: Terraform runs from its stack directory (terraform/stacks/{platform}/{chain}/{network}/). Ansible runs from the ansible/ 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

Security

AWS (Security Groups)

  • 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)

Bare Metal (UFW Firewall)

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_ips only - set your VPN/office IPs before deploying
  • All Docker containers use network_mode: host - UFW is the only access control layer on bare metal

HAProxy (TLS + API Key Auth)

  • 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-Key header 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)

Roadmap

  • 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
  • Monitoring stack (Prometheus, Grafana)
  • Additional chains (BNB, Optimism, Base, IOTA, Polkadot, Xai, Polygon 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

License

MIT License - See LICENSE for details.

About

Production-ready blockchain node infrastructure: Terraform + Ansible on AWS, OVH, and Latitude bare metal - Docker/binary deployments, HAProxy TLS, Route 53 DNS, and AWS multi-region HA.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors