9 min read

How to Use Terraform with IPAM for Network Subnets

Connect Terraform to IPAM with this hands on guide. Use Obelinf to find an approved subnet, read its CIDR safely, and plan an AWS network from it.

ByAndré Ribeiro· Founder, Obelinf
How to Use Terraform with IPAM for Network Subnets
How to Use Terraform with IPAM for Network Subnets · September 30, 2026
On this page

Terraform makes infrastructure changes repeatable, but a plan is only as sound as the network facts it starts with. If a subnet lives in a spreadsheet while a cloud network lives in code, someone must keep those two records aligned. A typo or a stale copy can turn an otherwise valid plan into an address conflict. A second problem appears as a team grows: Terraform state knows about the resources in its own configuration, while operators still need to find the network context for equipment, cloud accounts, sites, and allocations managed elsewhere.

This tutorial uses Obelinf to hold approved network inventory and Terraform to consume a subnet record while planning an AWS subnet. You will set ownership rules, create or identify the Obelinf record, inspect it through the API, query it during a Terraform plan, and connect the approved CIDR to a real resource. The example is deliberately read focused: Terraform will not allocate or reserve the subnet. That distinction is central to avoiding duplicate assignments.

Approved subnet data flows from Obelinf into a Terraform planNetwork ownerapproves the rangeObelinf IPAMTerraform planCloud resourceapproved CIDRreviewable inputprovisioned networkThe record supplies an approved value. A separate allocation workflow must prevent duplicate assignments.

Decide what owns each fact

Treat the source of truth as an operating agreement, not as a special file format. Decide which system owns each value before you connect APIs. For this example, the network team owns the approved CIDR, purpose, site, gateway, and VLAN association in Obelinf. Terraform owns the lifecycle of cloud resources declared in its configuration. The cloud provider reports what was actually created. Those records connect, but they have different jobs.

Write down an ownership table for your own network before automating it. One workable starting point is:

Fact Owner Example
Approved subnet and purpose Network team in Obelinf 10.42.10.0/24 for production applications
Cloud resource lifecycle Terraform state and code AWS subnet and route table association
Observed deployed configuration Cloud API or network discovery Actual subnet ID and route state
Exception approval Named network reviewer Ticket or pull request linked to a change

This prevents a circular update where Terraform reads an Obelinf value, the cloud changes, and another job blindly overwrites the record with whatever it discovered. Discovery is useful evidence, but it does not tell you whether an unexpected change was approved.

Create or confirm a subnet record such as prod-apps with CIDR 10.42.10.0/24 in Obelinf. Include the real site, gateway, VLAN or VRF if you use them, and a description that says which environment and purpose the range serves. Avoid sample documentation values for actual networks. Use the subnet’s stable record ID in Terraform rather than searching by display name at every run. A renamed record retains its ID, while a name search can become ambiguous or select a different entry.

Before you automate, agree what happens when someone needs a new range. A person or allocation service should reserve and record it first, then Terraform can consume the approved assignment. If record creation must happen from a pipeline, make that a separate, retry safe write workflow with locking or another concurrency guard. A GET request cannot reserve a CIDR, and two simultaneous plans can read the same record.

Prepare the working environment

You need Terraform 1.2 or later for the postcondition used below, Python 3, curl, jq, the AWS CLI, AWS credentials, an AWS region, and an existing target VPC. The example uses the hashicorp/external provider as a small read adapter. It lets a local Python script make the authenticated request while keeping the API key out of Terraform arguments and state.

The external data source is an escape hatch for a small integration, not a replacement for a full Obelinf provider. It runs a local program during planning, so every runner must have the script and Python available. If your organization disallows external programs in Terraform, run the API lookup as a CI step and pass the validated CIDR into the plan as an ordinary input instead.

Create a scoped API key

In your Obelinf organization, open API & MCP, select Create Key, and choose read only access. This workflow only retrieves inventory, so it does not need permission to create or update records. Copy the key once into your CI secret manager or a protected local environment. Do not commit it to Git, put it in a .tf file, or pass it as a Terraform variable.

Set the required values in your shell. Replace the example organization slug and subnet name with your own values:

export OBELINF_API_KEY="obf_replace_me"
export OBELINF_ORG="acme-netops"
export OBELINF_SUBNET_NAME="prod-apps"

Keep shell tracing disabled while the key is in your environment. In CI, map the secret into OBELINF_API_KEY only for the Terraform job, and restrict who can edit the workflow that receives it. To find a record by name, query the list endpoint. Obelinf returns a subnets array and a total count. The q parameter matches the beginning of a name or CIDR, so use a distinctive name prefix:

curl --fail --silent --show-error \
  -H "Authorization: Bearer ${OBELINF_API_KEY}" \
  "https://api.obelinf.com/${OBELINF_ORG}/subnets?q=${OBELINF_SUBNET_NAME}&limit=100" \
  | jq -r '.subnets[] | [.id, .name, .cidr, .site_name, .vlan_name] | @tsv'

Copy the ID for the intended result, then read that exact record. The detail endpoint returns a subnet object with fields such as id, name, cidr, site_id, gateway, vlan_id, and description:

export OBELINF_SUBNET_ID="paste_the_record_id_here"

curl --fail --silent --show-error \
  -H "Authorization: Bearer ${OBELINF_API_KEY}" \
  "https://api.obelinf.com/${OBELINF_ORG}/subnets/${OBELINF_SUBNET_ID}" \
  | jq '.subnet | {id, name, cidr, site_id, gateway, vlan_id, vrf_id, description}'

Check that the returned name, range, site, and purpose match the change you intend to make. If the key is denied, the organization slug is wrong, or the record is missing, stop here and fix access or the ID before you run a plan. The list endpoint is paginated, so increase offset if the intended record is not in the first 100 results.

Read the subnet in Terraform

Create a small working directory with main.tf and read_subnet.py. Terraform’s external data source passes the organization slug and subnet ID to the script over standard input. The script sends an authenticated GET request and returns only the stable ID, name, and CIDR. The API key arrives through the process environment, so it is not a Terraform argument that gets recorded with the data source.

The external provider is a small integration bridge. It runs a local program during planning, so every runner needs Python and the checked in script. If your organization prohibits local programs in Terraform, run this same lookup as a CI step and pass the validated CIDR to Terraform as an ordinary input instead.

Add this to main.tf:

terraform {
  required_providers {
    external = {
      source  = "hashicorp/external"
      version = "~> 2.3"
    }
    aws = {
      source = "hashicorp/aws"
    }
  }
}

variable "obelinf_org" {
  type        = string
  description = "Obelinf organization slug that owns the subnet record."
}

variable "obelinf_subnet_id" {
  type        = string
  description = "Stable Obelinf ID of the approved subnet."
}

data "external" "approved_subnet" {
  program = ["python3", "${path.module}/read_subnet.py"]

  query = {
    org_slug  = var.obelinf_org
    subnet_id = var.obelinf_subnet_id
  }

  lifecycle {
    postcondition {
      condition     = self.result.id == var.obelinf_subnet_id && can(cidrhost(self.result.cidr, 0))
      error_message = "Obelinf returned an unexpected record or an invalid IPv4 CIDR. Check the ID and subnet record."
    }
  }
}

locals {
  approved_subnet = data.external.approved_subnet.result
}

output "approved_subnet_cidr" {
  value       = local.approved_subnet.cidr
  description = "Approved application subnet recorded in Obelinf."
}

Create read_subnet.py beside main.tf:

import json
import os
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

query = json.load(sys.stdin)
api_key = os.environ.get("OBELINF_API_KEY")
if not api_key:
    sys.exit("OBELINF_API_KEY is not set")

url = (
    "https://api.obelinf.com/"
    f"{query['org_slug']}/subnets/{query['subnet_id']}"
)
request = Request(url, headers={
    "Authorization": f"Bearer {api_key}",
    "Accept": "application/json",
})

try:
    with urlopen(request, timeout=15) as response:
        subnet = json.load(response)["subnet"]
except (HTTPError, URLError, KeyError, json.JSONDecodeError) as error:
    sys.exit(f"Could not read the Obelinf subnet: {error}")

print(json.dumps({
    "id": str(subnet["id"]),
    "name": str(subnet["name"]),
    "cidr": str(subnet["cidr"]),
}))

Terraform data sources only read external data. If the request fails or the expected JSON is missing, the script exits unsuccessfully and the plan stops. The postcondition confirms that the returned ID matches the configured ID and that the example’s IPv4 CIDR is parseable before Terraform uses it. If your inventory contains IPv6 networks, use an IPv6 aware validation rule and downstream resource instead of applying this IPv4 check unchanged.

The script keeps the API key out of Terraform arguments and state, but the CIDR and returned record values become Terraform data source state. Avoid replacing this helper with an HTTP data source that sends the key in request_headers without checking its schema and state behavior. The HTTP provider treats that header map as ordinary input, so the authorization value can be stored with the data source even when the source variable is marked sensitive. Restrict state and saved plan access, use a protected remote backend with encryption and locking, and keep state and plan files out of Git. Marking a Terraform variable sensitive only hides it from normal CLI display, it does not guarantee the value is absent from state. Avoid logging environment variables or printing request headers.

Initialize and review the plan

Pass the nonsecret record identifiers as Terraform input variables, then initialize and validate the configuration. The API key remains an environment variable that the Python program reads when Terraform invokes it:

export TF_VAR_obelinf_org="$OBELINF_ORG"
export TF_VAR_obelinf_subnet_id="$OBELINF_SUBNET_ID"

terraform init
terraform validate
terraform plan

The first plan should only read the record and show the approved_subnet_cidr output. If Terraform cannot run the script, check that Python is on PATH, OBELINF_API_KEY is set in the same environment, and the organization and subnet ID are correct. This is a useful checkpoint before the plan is allowed to create anything.

In the AWS stack, add an AWS provider configuration and connect the approved record to the subnet resource. This assumes the VPC already exists. The resource uses the CIDR from Obelinf, and the name from that same record becomes its tag:

variable "aws_region" { type = string }
variable "vpc_id" { type = string }
variable "availability_zone" { type = string }

provider "aws" {
  region = var.aws_region
}

resource "aws_subnet" "app" {
  vpc_id            = var.vpc_id
  cidr_block        = local.approved_subnet.cidr
  availability_zone = var.availability_zone

  tags = {
    Name = local.approved_subnet.name
  }
}

Set the AWS inputs and check that the approved CIDR belongs to the target VPC and does not overlap a subnet already in use. The AWS CLI commands below print the VPC ranges and its current subnet ranges for comparison:

export AWS_PROFILE="your-profile"
export TF_VAR_aws_region="us-east-1"
export TF_VAR_vpc_id="vpc-0123456789abcdef0"
export TF_VAR_availability_zone="us-east-1a"

aws ec2 describe-vpcs \\
  --vpc-ids "$TF_VAR_vpc_id" \\
  --query 'Vpcs[0].CidrBlockAssociationSet[].CidrBlock' --output text
aws ec2 describe-subnets \\
  --filters "Name=vpc-id,Values=$TF_VAR_vpc_id" \\
  --query 'Subnets[].CidrBlock' --output text

Keep the subnet ID and expected network purpose in configuration or module inputs so a reviewer can see why the range is being used. Run a fresh plan after adding the AWS resource:

terraform plan

Review the VPC, availability zone, CIDR, and resource tags in the plan. Terraform should propose one subnet with the approved CIDR, provided the range is valid and unused in that VPC.

For the example record, the plan should show Terraform creating one AWS subnet with 10.42.10.0/24. After you have reviewed and approved that plan, apply it:

terraform apply

Terraform manages the AWS subnet after the apply. Obelinf continues to hold the approved network record and context that Terraform does not manage, such as site, VLAN, VRF, gateway, and operational purpose. If Terraform created the AWS subnet before you introduced this configuration, do not apply and create another. Add the matching resource block, then import the existing AWS ID to that resource address before applying:

terraform import aws_subnet.app subnet-0123456789abcdef0
terraform plan

Review the plan to ensure Terraform proposes no unintended replacement. Import connects an existing object to Terraform state. It does not change the actual subnet or automatically fill in the right Obelinf record, so verify both sides before continuing.

Do not make two independent plans select the same supposedly free range. This configuration reads a known, approved subnet. It does not find an available range, lock it, or claim it. If your workflow needs dynamic allocation, add an allocation step with concurrency control and write the successful assignment back to the system that owns allocation before Terraform consumes it. If the process spans several Terraform workspaces, keep the allocation decision in a shared service or controlled change workflow. A local lock on one Terraform state file cannot coordinate independent states.

Add drift checks and safe updates

Reconcile the documented record against cloud state on a schedule or during a change review. A matching CIDR does not prove that routing, ACLs, gateways, or device configuration match, so compare the fields that matter to your service and report exceptions to an owner. Avoid automatically copying every discovered value back into the approved record because an emergency change could be unintended.

Choose an exception policy before the first mismatch. For example, a change to a resource tag may be corrected by Terraform, while a CIDR mismatch may pause deployment and create a review ticket. For higher risk fields such as routes and security rules, require an approver and a post change check. Record the last successful lookup time and surface failures in CI, because stale data that looks like a successful run can be more dangerous than an explicit failure.

Keep mutations separate from this read path. If a later workflow needs to create or update Obelinf records, give that job a separate write key, make it safe to retry, and log the source run and result. Use a stable external identifier to match an existing record before creating anything, so rerunning a job does not produce duplicates. Test it against a nonproduction organization before letting a pipeline change shared inventory. Do not use a Terraform provisioner to hide an API write after apply: Terraform would not have a proper resource lifecycle for the Obelinf record, and a failed side effect can leave cloud and inventory out of sync.

Keep the source useful

Start with one site and one subnet class. Agree on field ownership, assign a person to review exceptions, and make successful reconciliation visible in CI. Pin and review provider versions, keep the helper script in version control, and have the job fail if the script exits unsuccessfully or returns the wrong subnet. Add more resources only when the records have stable IDs and the team knows which system is authoritative for each value.

A source of truth is useful when people trust its ownership rules and automation consumes it in a controlled way. Use Obelinf to maintain the network records your team shares, and let Terraform plan resources from those approved facts. Start with one subnet, verify the API result against the planned resource, and only then extend the pattern to more environments. For more on modeling address space and assignments, see Obelinf IP address management.

Frequently Asked Questions

Can Terraform read a subnet from Obelinf?
Yes. Terraform can call Obelinf's subnet API through a small external data source script and use the returned CIDR in a plan. A data source reads a value, it does not reserve that range.
Does Terraform replace an IPAM source of truth?
Terraform state tracks resources managed by a Terraform configuration. A network source of truth also needs to represent infrastructure and allocations outside that state, with owners and relationships teams can review.
How do I prevent two Terraform runs from using the same subnet?
Record and approve each allocation before plans consume it, and serialize allocation changes or use an allocator with concurrency control. A read of an available range is not a reservation.
What should an Obelinf API key for Terraform be allowed to do?
For this tutorial, a read only key is sufficient because Terraform only fetches a subnet. The helper reads it from the process environment rather than passing it as a Terraform argument. Store the key in your CI secret manager and restrict access to the runner, state, and plan files.

Stop reaching for a spreadsheet

Obelinf keeps every subnet, device, circuit, and rack in one live source of truth, with audit logs and a topology view. Free for personal use.

Related Articles