ClientNodetonetDeviceTarget TERRAFORM nodetonet.com
Integration Terraform IaC REST API

Nodetonet with Terraform — manage proxies as code via http or null_resource

Declare, create and destroy Nodetonet mobile proxies and HTTP tunnels from a Terraform plan using hashicorp/http for reads and null_resource local-exec for writes.

N Nodetonet Team
May 15, 2026 9 min read

The right way to manage long-lived infrastructure is declaratively. If your environment has staging URLs, webhook receivers, and per-tenant proxies, you don't want to curl POST /api/v1/proxies from a runbook every time the team adds a new service. You want it in a main.tf next to the rest of your cloud resources, with terraform plan showing the diff and terraform apply shipping the change.

TL;DR: Nodetonet's REST API is fully scriptable. Two stock Terraform primitives — hashicorp/http for reads and null_resource with local-exec for writes — let you manage mobile proxies, rotating proxy pools, and HTTP tunnels as code today, without waiting for a custom provider.

Why manage proxies as code?

Infrastructure-as-code isn't just a DevOps trend — for proxy-heavy workloads it solves real operational problems:

For context on how Nodetonet routes traffic behind the scenes, see how Nodetonet routes traffic. For the REST API basics before you dig into Terraform, read your first REST API call.

1 · The minimum providers block

You need http, the built-in null, and (optionally) random for unique subdomain suffixes. No private registry, no plugin install dance:

terraform {
  required_version = ">= 1.6"
  required_providers {
    http   = { source = "hashicorp/http",   version = "~> 3.4" }
    null   = { source = "hashicorp/null",   version = "~> 3.2" }
    random = { source = "hashicorp/random", version = "~> 3.6" }
  }
}

variable "nodetonet_token" {
  type      = string
  sensitive = true
}

locals {
  api_base = "https://nodetonet.com/api/v1"
  headers = {
    Authorization = "Bearer ${var.nodetonet_token}"
    Content-Type  = "application/json"
  }
}

Pass the token with TF_VAR_nodetonet_token=$NTN_TOKEN terraform apply, or look it up from your existing secret backend (AWS Secrets Manager, Vault, Doppler) via a data source. You can generate an API token from your account under the API Keys section — treat it like a password: sensitive in Terraform, stored in your vault, never in version control.

2 · Creating a proxy

The "create" half is a null_resource with two provisioners: local-exec on create calls POST /api/v1/proxies, local-exec on destroy calls DELETE. The trick is capturing the returned id so destroy knows what to delete — we use triggers and a small state file:

resource "random_string" "sub_suffix" {
  length  = 6
  upper   = false
  special = false
}

resource "null_resource" "tunnel_api" {
  triggers = {
    subdomain = "api-${random_string.sub_suffix.result}"
    port      = 8080
    state_dir = "${path.module}/.terraform-nodetonet"
  }

  provisioner "local-exec" {
    command = <<-EOT
      mkdir -p ${self.triggers.state_dir}
      RESP=$(curl -fsS -X POST ${local.api_base}/proxies \
        -H "Authorization: Bearer ${var.nodetonet_token}" \
        -H "Content-Type: application/json" \
        -d '{
          "subdomain": "${self.triggers.subdomain}",
          "target_host": "127.0.0.1",
          "target_port": ${self.triggers.port}
        }')
      echo "$RESP" > ${self.triggers.state_dir}/${self.triggers.subdomain}.json
    EOT
  }

  provisioner "local-exec" {
    when    = destroy
    command = <<-EOT
      ID=$(jq -r .id ${self.triggers.state_dir}/${self.triggers.subdomain}.json)
      curl -fsS -X DELETE \
        -H "Authorization: Bearer ${var.nodetonet_token}" \
        https://nodetonet.com/api/v1/proxies/$ID
      rm -f ${self.triggers.state_dir}/${self.triggers.subdomain}.json
    EOT
  }
}

triggers hashes the inputs into Terraform state. If you change port in the next plan, Terraform detects the drift and re-runs the create provisioner — which calls our API with the new port. That's not a real "update" (it tears down and re-creates) but for ephemeral tunnels it's perfectly fine.

3 · Reading state — the http data source

For reads (anything that shouldn't mutate state), http is cleaner. List proxies, find the URL for one you created out-of-band, or feed it as input to another resource:

data "http" "proxies" {
  url = "${local.api_base}/proxies"
  request_headers = local.headers
}

locals {
  proxies = jsondecode(data.http.proxies.response_body)
}

output "all_urls" {
  value = [for p in local.proxies : p.url]
}

Pair this with the null_resource above to make outputs that include the URL of the proxy you just created:

locals {
  created = jsondecode(file(
    "${null_resource.tunnel_api.triggers.state_dir}/${null_resource.tunnel_api.triggers.subdomain}.json"
  ))
}

output "tunnel_url" {
  value = local.created.url
}

output "tunnel_id" {
  value = local.created.id
}

4 · Putting it together — a full main.tf

A complete file you can copy/paste, run terraform init && terraform apply, and end up with a tunnel pointed at a service you'll define elsewhere:

terraform {
  required_version = ">= 1.6"
  required_providers {
    null   = { source = "hashicorp/null",   version = "~> 3.2" }
    random = { source = "hashicorp/random", version = "~> 3.6" }
  }
}

variable "nodetonet_token" { type = string, sensitive = true }
variable "service_port"    { type = number, default = 3000 }
variable "service_name"    { type = string }

resource "random_string" "suffix" {
  length = 6, upper = false, special = false
}

resource "null_resource" "tunnel" {
  triggers = {
    name = "${var.service_name}-${random_string.suffix.result}"
    port = var.service_port
  }

  provisioner "local-exec" {
    command = "curl -fsS -X POST https://nodetonet.com/api/v1/proxies -H 'Authorization: Bearer ${var.nodetonet_token}' -H 'Content-Type: application/json' -d '{"subdomain":"${self.triggers.name}","target_port":${self.triggers.port}}' | tee ${path.module}/.tunnel-${self.triggers.name}.json"
  }

  provisioner "local-exec" {
    when    = destroy
    command = "curl -fsS -X DELETE -H 'Authorization: Bearer ${var.nodetonet_token}' https://nodetonet.com/api/v1/proxies/$(jq -r .id ${path.module}/.tunnel-${self.triggers.name}.json) && rm -f ${path.module}/.tunnel-${self.triggers.name}.json"
  }
}

output "url" {
  value = jsondecode(file("${path.module}/.tunnel-${null_resource.tunnel.triggers.name}.json")).url
}

Run it with terraform apply -var="service_name=stripe-test". Destroy with terraform destroy — the when = destroy provisioner runs the DELETE.

5 · Managing rotating proxy pools with Terraform

Beyond single tunnels, Nodetonet's rotating proxy pools (called token groups) can also be managed through the API. A token group is a named pool of mobile devices that your clients connect to via a single endpoint — Nodetonet picks a device using round-robin or least-connection selection, with automatic failover if a device goes offline.

The same null_resource pattern applies. Create a group, store the returned group id, reference it when creating per-client credentials. The API returns the proxy endpoint, username and password you hand to your downstream consumers. This is how teams that manage proxies for multiple clients — for example via white-label reseller accounts — can version-control the entire customer topology.

For a conceptual overview of how token groups and pools work before writing Terraform, see token groups: creating pools.

6 · DIY vs official provider — comparison

To be honest: the null_resource approach works, but it isn't beautiful. Here is where it falls short of a real provider, and where it holds up fine:

Capabilitynull_resource + curl (today)Official provider (roadmap)
Create / delete resourcesYes, via local-execYes, native resource lifecycle
Read existing stateYes, via http data sourceYes, typed data sources
Drift detectionPartial — triggers hash changes, not panel editsFull — refresh detects out-of-band changes
In-place updates (PATCH)No — destroy + recreateYes, per-field granularity
Human-readable plan diffsMinimal ("will be replaced")Full field-by-field diff
Typed resource schemaNo — raw JSON stringsYes — validated inputs
Works with Terraform CloudYes, with remote state workaroundYes, natively
Required external toolscurl, jq on runnerNone

If you're managing 1–5 long-lived tunnels, the DIY approach is perfectly adequate. If you're managing 50+ resources across multiple tenants, wait for the real provider — or reach out to support@nodetonet.com and tell us your use case; enterprise timelines are flexible.

7 · An official Terraform provider is on the roadmap

Terraform provider · Q4 2026. When it lands the equivalent of the entire main.tf above becomes:

provider "nodetonet" {
  token = var.nodetonet_token
}

resource "nodetonet_proxy" "api" {
  subdomain   = "stripe-test"
  target_host = "127.0.0.1"
  target_port = 3000
}

output "url" {
  value = nodetonet_proxy.api.url
}

Same REST API underneath; the provider just wraps it with proper drift detection, in-place PATCH, and typed schema. Existing main.tf files that use the null_resource pattern will continue to work — you can migrate one resource at a time.

8 · CI/CD integration tips

A few practical details that trip teams up when running the curl approach in automation:

Get started

Paste the main.tf above, set your token, and run terraform apply. Your first declarative proxy is two minutes away. Questions? Visit contact, join discord.gg/nodetonet, or browse the blog for deeper guides on specific features. If you want to script proxy creation in Python instead, see programmatic tunnel creation with Python.

Frequently asked questions

Should I commit the .tunnel-*.json state files?
No — add .tunnel-*.json to .gitignore. They are local state, not source of truth. The Terraform state file (terraform.tfstate) tracks the null_resource existence; the JSON files are just where we cache the returned id for the destroy provisioner. Keep them per-workspace via the path.module trick.
Can I use Terraform Cloud or Atlantis with the curl approach?
Yes — both run plans in a sandbox where curl and jq are available. Set TF_VAR_nodetonet_token as a sensitive workspace variable. The only caveat is the local cache files (.tunnel-*.json) will not persist across runs, so a destroy in a fresh runner may fail to find the id. Use a remote KV store or output the id into Terraform state as a local_file resource to harden this.
Will the official Terraform provider be open source?
Yes. The provider will be published under the Mozilla Public License — the same as most Terraform providers. You will be able to vendor a fork, audit the code, or contribute upstream. The REST API it wraps is public, so any third party could write a provider too; ours will be the canonical one.
Can I manage rotating proxy pools (token groups) through Terraform?
Yes. Nodetonet's token groups are exposed through the REST API, so you can create, configure and tear down pools using the same null_resource pattern. Store the returned group id in a local file and reference it when provisioning per-client credentials. See our guide on creating token group pools for the full API shape.
How do I handle Nodetonet API authentication in Terraform?
Declare a sensitive Terraform variable (variable "nodetonet_token" { sensitive = true }) and pass it via TF_VAR_nodetonet_token or from a secrets manager data source. Never hard-code tokens in .tf files. When the official provider ships, authentication will be configured in a provider "nodetonet" block with the same token.
Is there a Python alternative for programmatic proxy management?
Yes. If Terraform is heavier than your use case demands, you can drive the same REST API from Python with the requests library. See our guide on programmatic tunnel creation with Python for a full working example.
What happens to my Terraform-managed proxies if the Nodetonet panel changes the resource?
With the current null_resource approach, Terraform will not detect the panel change on the next plan — it only notices if the triggers map changes. The official provider will fix this with proper drift detection: a terraform refresh or plan -refresh-only will pull live state and show the divergence.

Manage Nodetonet proxies as code

Paste the main.tf above and run terraform apply. Your first declarative proxy is two minutes away.