+1 (415) 649-9454

Neo4j Aura as Code: The Aura API, the Terraform Provider, and Environments You Can Rebuild

Every Neo4j team eventually hits the same wall: three Aura instances (dev, staging, prod) created by three different people, in two different regions, on two different tiers, with nobody quite sure which one the nightly job writes to. Clicking instances into existence in the Aura Console does not scale past the first month of a project.

This tutorial puts Aura under version control. You will register an Aura API credential, drive the API by hand to understand what it does, then manage the same instances declaratively with the official Terraform provider — including the parts that trip people up: tier changes that resize in place, pausing non-production instances on a schedule, and getting the connection URI into your application without pasting a password into a ticket.

What you need

  • An Aura Professional, Business Critical, or Virtual Dedicated Cloud account. The Aura API is not available on Aura Free, so you cannot follow the Terraform half of this on a free tier instance.
  • Terraform 1.5+ (or OpenTofu — the provider works with both).
  • curl and jq for the API warm-up.

Step 1 — create an API credential

In the Aura Console, open your account menu and go to API Keys (account-level, not instance-level). Create a key and copy the client ID and client secret; the secret is shown once.

The API uses OAuth2 client credentials, so every session starts with a token exchange:

export AURA_CLIENT_ID=...
export AURA_CLIENT_SECRET=...

TOKEN=$(curl -s -X POST https://api.neo4j.io/oauth/token \
  -u "$AURA_CLIENT_ID:$AURA_CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" | jq -r .access_token)

Tokens are short-lived (an hour), which is the first reason you want tooling rather than a shell history full of bearer tokens.

List what you already own:

curl -s https://api.neo4j.io/v1/tenants \
  -H "Authorization: Bearer $TOKEN" | jq .

TENANT=<tenant-id-from-above>

curl -s "https://api.neo4j.io/v1/instances?tenantId=$TENANT" \
  -H "Authorization: Bearer $TOKEN" \
  | jq -r '.data[] | [.id, .name, .cloud_provider, .region, .memory, .type] | @tsv'

That one command is usually worth the whole exercise. The inventory it prints is the answer to "what are we actually paying for?" — and in most first engagements it contains at least one instance nobody has connected to in six months.

Step 2 — the API lifecycle, by hand

Create an instance:

curl -s -X POST https://api.neo4j.io/v1/instances \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "'"$TENANT"'",
    "name": "graphguru-dev",
    "version": "5",
    "region": "europe-west1",
    "cloud_provider": "gcp",
    "memory": "2GB",
    "type": "professional-db"
  }' | jq .

The response contains data.username, data.password, and data.connection_url. That password is returned exactly once and is never retrievable again. Pipe it straight into your secret manager in the same command — do not let it land in a terminal scrollback.

The instance comes back with status: creating. Poll it:

curl -s "https://api.neo4j.io/v1/instances/$ID" \
  -H "Authorization: Bearer $TOKEN" | jq -r .data.status

Other endpoints you will use: POST /v1/instances/{id}/pause, POST /v1/instances/{id}/resume, PATCH /v1/instances/{id} (rename, resize memory, change tier), GET/POST /v1/instances/{id}/snapshots, and DELETE /v1/instances/{id}. Pause is the cost lever: a paused instance keeps its data and bills at a much lower rate, and dev instances spend most of the week paused anyway.

Step 3 — the same thing in Terraform

The provider is neo4j/neo4j-aura in the public registry.

terraform {
  required_version = ">= 1.5"
  required_providers {
    neo4j = {
      source  = "neo4j/neo4j-aura"
      version = "~> 1.0"
    }
  }
}

provider "neo4j" {
  client_id     = var.aura_client_id      # or env AURA_API_CLIENT_ID
  client_secret = var.aura_client_secret  # or env AURA_API_CLIENT_SECRET
}

variable "aura_client_id"     { type = string, sensitive = true }
variable "aura_client_secret" { type = string, sensitive = true }
variable "tenant_id"          { type = string }

Now describe the environments as data rather than as three hand-made instances:

locals {
  environments = {
    dev = {
      memory = "2GB"
      type   = "professional-db"
    }
    staging = {
      memory = "4GB"
      type   = "professional-db"
    }
    prod = {
      memory = "16GB"
      type   = "enterprise-db"
    }
  }
}

resource "neo4j_aura_instance" "env" {
  for_each = local.environments

  tenant_id      = var.tenant_id
  name           = "graphguru-${each.key}"
  cloud_provider = "gcp"
  region         = "europe-west1"
  version        = "5"
  memory         = each.value.memory
  type           = each.value.type
}

output "connection_urls" {
  value = { for k, v in neo4j_aura_instance.env : k => v.connection_url }
}
terraform init
terraform plan   # read it; see Step 4 before you apply to anything real
terraform apply

Three facts about this resource that save real incidents:

  1. region and cloud_provider are immutable. Changing them is a destroy-and-recreate, and Terraform will say so in the plan with # forces replacement. Read every plan for that string before applying to production.
  2. memory is updated in place — the provider calls the resize endpoint. The instance stays available, but plan for a short performance dip while it moves.
  3. The initial password is in state. Terraform state for Aura contains credentials, so it belongs in an encrypted remote backend with restricted access, not in a repository and not on a laptop.

Step 4 — import the instances you already have

Nobody starts green-field. Bring existing instances under management rather than recreating them:

import {
  to = neo4j_aura_instance.env["prod"]
  id = "a1b2c3d4"   # the Aura instance id
}
terraform plan -generate-config-out=imported.hcl

Then reconcile imported.hcl against your intended config until terraform plan reports no changes. That "no changes" state is the goal of the whole exercise: from then on, every difference between your repo and your Aura account is a deliberate, reviewed change.

Step 5 — pause dev and staging on a schedule

The pause/resume endpoints are a better cost control than downsizing, and they are trivial to automate. A GitHub Actions workflow that pauses non-production instances every evening:

name: pause-aura-nonprod
on:
  schedule:
    - cron: "0 19 * * 1-5"   # 19:00 UTC, weekdays
  workflow_dispatch:

jobs:
  pause:
    runs-on: ubuntu-latest
    steps:
      - name: Pause dev and staging
        env:
          AURA_CLIENT_ID: ${{ secrets.AURA_CLIENT_ID }}
          AURA_CLIENT_SECRET: ${{ secrets.AURA_CLIENT_SECRET }}
          TENANT: ${{ secrets.AURA_TENANT_ID }}
        run: |
          TOKEN=$(curl -s -X POST https://api.neo4j.io/oauth/token \
            -u "$AURA_CLIENT_ID:$AURA_CLIENT_SECRET" \
            -d grant_type=client_credentials | jq -r .access_token)
          curl -s "https://api.neo4j.io/v1/instances?tenantId=$TENANT" \
            -H "Authorization: Bearer $TOKEN" \
          | jq -r '.data[] | select(.name | test("-(dev|staging)$")) | .id' \
          | while read -r id; do
              echo "pausing $id"
              curl -s -X POST "https://api.neo4j.io/v1/instances/$id/pause" \
                -H "Authorization: Bearer $TOKEN" | jq -r .data.status
            done

Add a mirrored resume workflow at 06:00 and warn your team: a resumed instance needs its page cache warmed before the first query feels normal. Resume ahead of the working day, not at the start of it.

Step 6 — connect the application without copying passwords

Terraform created the instance; it should also hand the credentials to whatever runs your application. Write them into your secret store from the same apply:

resource "google_secret_manager_secret_version" "neo4j_prod" {
  secret = google_secret_manager_secret.neo4j_prod.id
  secret_data = jsonencode({
    uri      = neo4j_aura_instance.env["prod"].connection_url
    username = neo4j_aura_instance.env["prod"].username
    password = neo4j_aura_instance.env["prod"].password
  })
}

Substitute AWS Secrets Manager, Vault, or Kubernetes ExternalSecret as appropriate. The rule is the same one we apply in hardening Neo4j for production: a human should never read a database password in order to deploy an application.

Step 7 — put schema in the pipeline too

Infrastructure as code gets you an empty database in a known shape. Constraints and indexes are a separate concern and belong in a versioned migration tool, not in Terraform — see our walkthrough of versioned schema migrations and Testcontainers. The clean division of labour:

LayerOwned byChanges when
Instance, region, tier, sizeTerraformCapacity or environment changes
Users, roles, privilegesMigration scripts (GRANT/DENY)Access model changes
Constraints, indexesMigration scriptsData model changes
DataLoaders / CDCContinuously

Keep those in separate repositories or at least separate pipelines. Mixing a tier resize and a constraint change into one deploy is how you end up unable to roll back either.

A checklist before you call it done

  • terraform plan on a clean checkout reports no changes for every environment.
  • State lives in an encrypted remote backend with access limited to the deploy role.
  • No # forces replacement can reach production without a second reviewer.
  • Non-production instances pause outside working hours, verified by looking at a bill.
  • Snapshots are scheduled, and someone has actually restored one into a scratch instance this quarter — the restore drill from our backup and PITR tutorial applies to Aura too.
  • Application credentials come from a secret store, and rotating them does not require a Terraform apply.

Where this usually goes next

Once the instances are declarative, the interesting questions become architectural: is prod sized for its page cache working set, or for a number someone guessed? Should analytics run on a separate instance or on Aura Graph Analytics sessions? Do you need Virtual Dedicated Cloud for the network isolation your security review is about to ask for?

Those are the questions we work through with clients on our Neo4j Aura and managed cloud engagements. If you are standing up Aura environments and want the sizing and isolation decisions reviewed before they calcify, get in touch.