Getting a Fast-Syncing Go Ethereum Node Up and Running on Azure in Less Than 10 Minutes
As Ethereum’s mainnet state continue to grow, it also lengthens the time it takes to fast-sync a node, specifically, a Go Ethereum node…
As Ethereum’s mainnet state continue to grow, it also lengthens the time it takes to fast-sync a node, specifically, a Go Ethereum node (`geth` for short), which we will be deploying in this article. Although common scenarios for running a node as part of a system would do well with running a light sync, as it allows transmitting transactions to the network, but some scenarios, like reading the transactions pool of the network, require a fast (or full) sync.
The Problem
Now, although it’s called a “fast” sync, if hardware isn’t strong enough, more than an average workstation, it would be anything but fast. and one of the main reasons for that, is its demand for high input/output operations per second (IOPS), due to the large amount of data (state tries) that needs to be written to disk.
The Solution
To solve that problem, we will be deploying a `geth` node on a storage optimized virtual machine (Lsv2 series), which in addition to its OS drive, it has directly mapped NVMe storage, targeted specifically for workloads requiring low latency and high throughput. By using a L8s v2 virtual machine instance, we should be able to sync in less than 24 hours, instead of weeks (like when trying to use a container with allocated 4 CPU/16GB/Mounted SSD).
Prerequisites
As we will be deploying on Azure by using a Terraform script, the following will need to be installed on the workstation:
- Azure CLI: installation guide is available here.
- Terraform: installation guide is here.
- An SSH client of choice.
The Example
If you really want to get you get your node up and running in less than 10 minutes, you can just clone the example GitHub repository and run the Terraform script:
GitHub - ItayPodhajcer/terraform-geth-azure-vm
Contribute to ItayPodhajcer/terraform-geth-azure-vm development by creating an account on GitHub.
The script will:
- Create an L8s v2 VM (with all the required dependencies).
- Create an SSH key (and save it to the local disk).
- Run a script that installs and starts running
gethas a service.
The script
For brevity, I’ll be only going over the geth specific areas of the script, as the rest is straight forward virtual machine deployment.
The first geth specific configuration is the ports we are going to open using a network security group, which are:
- Port 22/TCP — for SSH connections.
- Port 30303/TCP — Ethereum standard listening port.
- Port 30303/UDP — Ethereum standard discovery port.
- Port 8545/TCP — Ethereum standard HTTP port.
- Port 8546/TCP — Ethereum standard Web Sockets port.
resource "azurerm_network_security_group" "this" { name = "nsg-${local.deployment_name}" location = azurerm_resource_group.this.location resource_group_name = azurerm_resource_group.this.name
security_rule { name = "SSH" priority = 1001 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "22" source_address_prefix = "*" destination_address_prefix = "*" }
security_rule { name = "ETH_Listen" priority = 1002 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "30303" source_address_prefix = "*" destination_address_prefix = "*" }
security_rule { name = "ETH_Discovery" priority = 1003 direction = "Inbound" access = "Allow" protocol = "Udp" source_port_range = "*" destination_port_range = "30303" source_address_prefix = "*" destination_address_prefix = "*" }
security_rule { name = "ETH_JSON_RPC_HTTP" priority = 1004 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "8545" source_address_prefix = "*" destination_address_prefix = "*" }
security_rule { name = "ETH_JSON_RPC_WS" priority = 1005 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "8546" source_address_prefix = "*" destination_address_prefix = "*" }}Next, we will create a template systemd service file to install geth as service that starts automatically after system restarts (so we don’t need to connect to the VM run the geth command manually each time):
[Unit]Description=Go Ethereum
[Service]Type=simpleExecStart=/usr/bin/geth --syncmode "fast" --http --ws --http.addr 0.0.0.0 --ws.addr 0.0.0.0 --http.corsdomain '*' --ws.origins '*' --http.api eth,net,web3,personal --ws.api eth,net,web3,personal
User=${vm_user}Group=${vm_user}
[Install]WantedBy=default.targetNote that the geth command enables both HTTP and Web Sockets endpoints, and permits the ETH, NET, WEB3, and PERSONAL APIs on both endpoints, from any host (wildcard CORS).
Now that we have the service file, we need a template bash script that will:
- Create the data folder.
- Create a file system on the NVMe drive (we will be using
XFS). - Mount the NVMe drive to the data folder.
- Install
geth. - Install the
systemdservice file. - Enable the service.
- And lastly, start the service.
#!/bin/bashblkid --match-token TYPE=xfs ${nvme_device_name} || mkfs --type xfs -f ${nvme_device_name}mkdir /home/${vm_user}/.ethereummount ${nvme_device_name} /home/${vm_user}/.ethereumchown ${vm_user}:${vm_user} /home/${vm_user}/.ethereumadd-apt-repository -y ppa:ethereum/ethereumapt-get updateapt-get install ethereum -yecho "${nvme_device_name} ~/.ethereum xfs defaults,nofail 0 2" >> /etc/fstabecho "${geth_service}" > /etc/systemd/system/geth.servicesystemctl daemon-reloadsystemctl enable geth.servicesystemctl start geth.serviceThe service template will be passed to the entry point script template as a Terraform template variable, along with additional values such as the virtual machine’s username and the NVMe device name, and later base64 enncoded:
locals { deployment_name = "gethvm" location = "eastus" admin_username = "${local.deployment_name}user" geth_service = templatefile("${path.module}/geth.tpl", { vm_user = local.admin_username }) entrypoint_script = templatefile("${path.module}/entrypoint.tpl", { nvme_device_name = "/dev/nvme0n1" vm_user = local.admin_username geth_service = local.geth_service }) entrypoint_base64 = base64encode(local.entrypoint_script)}And use the base64 encoded script in the custom_data field of the Terraform `azurerm_linux_virtual_machine` resource:
resource "azurerm_linux_virtual_machine" "this" { name = "vm-${local.deployment_name}" location = azurerm_resource_group.this.location resource_group_name = azurerm_resource_group.this.name network_interface_ids = [azurerm_network_interface.this.id] size = "Standard_L8s_v2" custom_data = local.entrypoint_base64
os_disk { name = "disk-${local.deployment_name}" caching = "ReadWrite" storage_account_type = "Premium_LRS" }
source_image_reference { publisher = "Canonical" offer = "UbuntuServer" sku = "18.04-LTS" version = "latest" }
computer_name = "vm-${local.deployment_name}" admin_username = local.admin_username disable_password_authentication = true
admin_ssh_key { username = "${local.deployment_name}user" public_key = tls_private_key.this.public_key_openssh }}Now we are ready to azure login and then terraform apply our script.
Testing The Node
To test the node, and make sure it is up and running (and syncing!), you can SSH to the virtual machine, and run the systemctl status geth to see the logs that were printed.
Conclusion
Although the deployment discussed in this article is using production grade hardware, some of its configuration should be hardened in a real production deployment, for example limiting SSH access, closing HTTP/Web Sockets ports if they are not used, and limiting CORS hosts to name a few.