Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure Oracle Resource Anchor Terraform Module

Links an Azure resource group to an Oracle Cloud Infrastructure compartment, so Oracle Database@Azure resources have somewhere to land on the OCI side (azurerm_oracle_resource_anchor). Targets hashicorp/azurerm ~> 4.0.

Terraform Provider Module Type Resources Provider namespace

🧩 Overview

  • 🔗 Creates the Azure-side object that links a resource group to an Oracle Cloud Infrastructure compartment, and reports that compartment's OCID.
  • ⚠️ This family is not a Microsoft resource provider. Everything here lives under the third-party Oracle.Database provider, and what it creates exists in OCI as well as in Azure.
  • 🔑 RBAC is written against Oracle.Database/*. A role scoped to Microsoft.* grants nothing here.
  • 📋 Three inputs, total: name, resource_group_name, tags. There is no location — the region is computed from the resource group.
  • 🕳️ The family's one invisible dependency. No sibling module consumes this anchor's id, so Terraform cannot infer that the anchor must exist first. Use depends_on.
  • 💥 Destroying the anchor severs the compartment link that sibling resources depend on, and no plan says so.

💡 Why it matters: This is the smallest module in the family and the right one to read first, because it is where the family's defining fact becomes visible. Almost everything that goes wrong with Oracle Database@Azure in Terraform is a provider-registration, tenant-onboarding, or RBAC-namespace problem rather than a configuration error — and all three surface here, at the cheapest possible resource to fail on.

❤️ Support this project

If this module saves you time, please consider supporting its continued development:


🗺️ Where this fits in the family

flowchart LR
  rg["terraform-azurerm-resource-group"]
  vnet["terraform-azurerm-virtual-network: the subnet must be DELEGATED for Oracle Database@Azure"]
  kv["terraform-azurerm-key-vault: holds the admin_password, read at plan time"]
  nsg["terraform-azurerm-network-security-group: the only thing guarding the SCAN listener ports"]
  anchor["terraform-azurerm-oracle-resource-anchor: links the resource group to an OCI COMPARTMENT. No location input, it is computed."]
  exa["terraform-azurerm-oracle-exadata-infrastructure: the RACK. Every argument force-new except tags."]
  vault["terraform-azurerm-oracle-exascale-database-storage-vault: sized ONCE, there is NO grow operation"]
  cluster["terraform-azurerm-oracle-cloud-vm-cluster: the GI cluster. Use hostname_actual, not hostname, for connections."]
  dbsrv["azurerm_oracle_db_servers DATA SOURCE: OCI OCIDs that do not exist until the rack does"]
  adb["terraform-azurerm-oracle-autonomous-database: keystone plus for_each BACKUPS. Network posture is a ONE-WAY DOOR."]
  cfb["terraform-azurerm-oracle-autonomous-database-clone-from-backup: a POINT-IN-TIME restore into a NEW database"]
  cfd["terraform-azurerm-oracle-autonomous-database-clone-from-database: a clone of CURRENT state, optionally refreshable"]
  note["PEERS, NOT CHILDREN: both clones import to the SAME Oracle.Database/autonomousDatabases path the keystone uses, so each IS an autonomous database"]

  rg -->|"resource_group_name"| anchor
  rg -->|"resource_group_name, location"| exa
  rg -->|"resource_group_name, location"| vault
  rg -->|"resource_group_name, location"| cluster
  rg -->|"resource_group_name, location"| adb
  anchor -->|"REQUIRED FIRST, but NO module consumes its id, so use depends_on"| exa
  anchor -->|"same invisible dependency"| adb
  exa -->|"id as cloud_exadata_infrastructure_id"| cluster
  exa -->|"read AFTER it exists"| dbsrv
  dbsrv -->|"OCIDs as db_servers"| cluster
  vault -->|"NO Terraform link exists. Aligned by REGION and ZONE only."| cluster
  vnet -->|"id plus a delegated subnet id"| cluster
  vnet -->|"id plus subnet id, the PRIVATE posture"| adb
  nsg -->|"restricts the unencrypted SCAN port 1521"| cluster
  kv -->|"secret read as admin_password"| adb
  kv -->|"secret read as admin_password"| cfb
  kv -->|"secret read as admin_password"| cfd
  adb -->|"id as source_autonomous_database_id"| cfb
  adb -->|"id as source_autonomous_database_id"| cfd
  note -->|"why they are separate modules"| cfb
  note -->|"why they are separate modules"| cfd

  classDef me fill:#0078D4,stroke:#004578,color:#fff;
  classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
  classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
  class exa keystone;
  class anchor,vault,cluster,adb,cfb,cfd me;
  class rg,vnet,kv,nsg,dbsrv,note sib;
Loading

🧬 What this module builds

flowchart TB
  third["READ FIRST: this family is NOT a Microsoft resource provider. Everything lives under the third-party Oracle.Database provider."]
  dual["so what these modules create exists in ORACLE CLOUD INFRASTRUCTURE as well as in Azure. Two clouds, one Terraform apply."]
  reg["consequence one: Oracle.Database must be REGISTERED explicitly. It is not a Microsoft namespace and is not registered by default."]
  rbac["consequence two: least-privilege roles are written against Oracle.Database/* . A role scoped to Microsoft.* grants NOTHING here."]
  onboard["consequence three: the TENANT needs Oracle Database@Azure onboarding, the marketplace subscription and the Azure-to-Oracle account link. Without it nothing under Oracle.Database can be created at all."]
  job["THE ANCHOR'S JOB: link an Azure resource group to an OCI COMPARTMENT, giving the family's resources somewhere to land on the OCI side"]
  tiny["the input surface is THREE fields: name, resource_group_name, tags. That is all."]
  noloc["there is NO location argument. The region is COMPUTED from the resource group, which is unusual in this library."]
  compart["linked_compartment_id is COMPUTED and is the module's real product: the OCID someone with OCI console access needs, and the form Oracle support asks for"]
  invisible["THE FAMILY'S ONE INVISIBLE DEPENDENCY: no sibling module consumes the anchor's id, so Terraform CANNOT infer that the anchor must exist first"]
  fix["so a composition that needs the ordering states it with depends_on, or relies on the resource group they share"]
  destroy["and destroying the anchor SEVERS the OCI compartment link that sibling resources depend on. Destroy those first. No plan says so."]
  notags["tags apply on the AZURE side only. They are not propagated to the linked OCI compartment."]
  this["terraform-azurerm-oracle-resource-anchor"]
  keystone["azurerm_oracle_resource_anchor.this"]

  third -->|"read this first"| dual
  dual -->|"so"| reg
  reg -->|"and"| rbac
  rbac -->|"and"| onboard
  onboard -->|"prerequisites"| this
  job -->|"the job"| tiny
  tiny -->|"note"| noloc
  noloc -->|"computed"| compart
  compart -->|"the real product"| this
  invisible -->|"remedy"| fix
  fix -->|"ordering"| destroy
  destroy -->|"hazard"| this
  notags -->|"boundary"| this
  this -->|"creates"| keystone

  classDef me fill:#0078D4,stroke:#004578,color:#fff;
  classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
  classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
  class this me;
  class keystone keystone;
  class third,dual,reg,rbac,onboard,job,tiny,noloc,compart,invisible,fix,destroy,notags sib;
Loading

Resource inventory

Resource Count Notes
azurerm_oracle_resource_anchor.this 1 The keystone. name and resource_group_name force-new; location and linked_compartment_id computed.

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Azure resource provider Oracle.Database — a third-party namespace, API 2025-09-01
Provider block None in this module. The caller configures provider "azurerm", including the mandatory features {} block, and supplies authentication.

Schema notes that bite — confirmed against the live provider schema and its documentation:

  • Oracle.Database is a third-party resource provider. It must be registered explicitly on the subscription, and RBAC for it is written against Oracle.Database/*.
  • name and resource_group_name are force-new.
  • There is no location argument, and the region does not follow the resource group. The provider sends Location: "global" — hard-coded and unconditional — so the location output always reads global regardless of where the resource group lives. Unusual in this library, where regional resources take a location value.
  • linked_compartment_id is computed and is the module's real product.
  • tags are the only mutable field, and they are not propagated to the linked OCI compartment.
  • No sibling module consumes this anchor's id, so the ordering it establishes is invisible to Terraform's dependency graph.

🔑 Required Azure RBAC Roles / Permissions

Scope Role / permission Why
The resource group Contributor, or a custom role with Oracle.Database/resourceAnchors/* Creating the anchor is a write against the Oracle.Database provider.

⚠️ Note the namespace. Least-privilege custom roles for this family are written against Oracle.Database/*, not Microsoft.*. A carefully-scoped role covering only Microsoft providers grants nothing here, and the resulting failure is an authorization error that looks like a bug rather than a missing grant.

Azure Prerequisites

  • The Oracle.Database resource provider registered on the subscription. This is the prerequisite most likely to be missing, because it is not a Microsoft namespace and is not registered by default.
  • Oracle Database@Azure onboarding completed for the tenant — the marketplace subscription and the link between the Azure tenant and an Oracle Cloud account. Without it, nothing under Oracle.Database can be created, and the failure is a tenant-level one this module cannot report.
  • An existing resource group in a region where Oracle Database@Azure is offered. The supported set is smaller than Azure's overall footprint.

📁 Module Structure

terraform-azurerm-oracle-resource-anchor/
├── providers.tf   # required_version + the pinned azurerm provider. No provider block.
├── variables.tf   # name, resource_group_name, tags, timeouts
├── main.tf        # the keystone anchor
├── outputs.tf     # id, name, resource_group_name, computed location, linked_compartment_id
├── README.md      # this document
├── SCOPE.md       # the cross-module contract
├── LICENSE        # MIT
└── .gitignore

⚙️ Quick Start

provider "azurerm" {
  features {}
}

module "oracle_anchor" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-oracle-resource-anchor.git?ref=v1.0.0"

  name                = "anchor-oracle-prod"
  resource_group_name = module.rg.name

  tags = { workload = "oracle-database" }
}

ℹ️ The caller configures the provider, its authentication, and the mandatory features {} block. This module declares none of them.

💡 No location — the provider hard-codes it to global. See example 3.

🔌 Cross-Module Contract

Consumes

Input Type Source module
resource_group_name string terraform-azurerm-resource-group → name
name string caller decision
tags map(string) caller decision

Emits

Output Description Consumed by
id The anchor's Resource ID. imports, diagnostics, RBAC
name The anchor's name. Force-new. review
resource_group_name The group the anchor belongs to. Force-new. review
location The Azure region. Computed. review
linked_compartment_id The linked OCI compartment's OCID. Computed. OCI-side investigation, support cases

📚 Example Library

1 · ⚠️ Register the provider first — this is not a Microsoft namespace
# The prerequisite most likely to be missing.
az provider register --namespace Oracle.Database
az provider show --namespace Oracle.Database --query registrationState

⚠️ Oracle.Database is not registered by default in most subscriptions, because it is not a Microsoft namespace. Every resource in this family fails without it.

💡 Registering it is a subscription-level, one-time operation and deliberately not something this module does. Provider registration is a subscription concern, and a module that registered namespaces as a side effect would be reaching well outside its scope.

ℹ️ Registration is also not the whole story — see example 2.

2 · ⚠️ The tenant needs Oracle Database@Azure onboarding
Beyond provider registration, the TENANT needs:
  - an Oracle Database@Azure marketplace subscription, and
  - the link between the Azure tenant and an Oracle Cloud account.

⚠️ Without onboarding, nothing under Oracle.Database can be created at all — including this anchor. The failure is a tenant-level one, so it cannot be fixed from a resource group and this module cannot report it usefully.

💡 If a first apply of this module fails and the provider is registered, onboarding is the next thing to check. That ordering saves time, because the two failures look similar from a Terraform error message.

ℹ️ This is why the anchor is worth applying on its own first: it is the cheapest resource in the family, so it is the cheapest place to discover a tenant-level problem. Discovering it on an Exadata rack apply is considerably more expensive.

3 · There is no `location` — it is computed
module "oracle_anchor" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-oracle-resource-anchor.git?ref=v1.0.0"

  name                = "anchor-oracle-prod"
  resource_group_name = module.rg.name
  # no location — the anchor takes its region from the resource group
}

output "where_it_landed" {
  value = module.oracle_anchor.location
}

ℹ️ Unusual in this library, where a regional resource normally takes a location value. The provider marks location computed on this resource type, so the module has no such input — adding one for consistency would mean inventing an argument the provider does not accept.

💡 The practical consequence: to place the anchor in a particular region, place the resource group there.

4 · `linked_compartment_id` is the module's real product
output "oci_compartment" {
  value = module.oracle_anchor.linked_compartment_id
}
# => "ocid1.compartment.oc1..aaaaaaaa..."

💡 This is the bridge between the Azure view and the OCI view. It is the identifier someone with OCI console access needs in order to find the compartment these Azure resources actually live in, and the form Oracle support will ask for when you open a case.

ℹ️ Computed, so it appears after apply. Worth surfacing in a composition's outputs rather than leaving buried in state — the moment someone needs it is usually the moment they are debugging under time pressure and do not want to be reading state files.

⚠️ The OCID identifies a compartment in Oracle Cloud. Its contents, its policies and its OCI-side IAM are not Azure resources and are not manageable from Terraform's azurerm provider.

5 · ⚠️ The family's invisible dependency
module "oracle_anchor" {
  source              = "git::.../terraform-azurerm-oracle-resource-anchor.git?ref=v1.0.0"
  name                = "anchor-oracle-prod"
  resource_group_name = module.rg.name
}

# FRAGMENT: shown to make the point about the missing dependency edge. The rack's own required
# inputs are in the end-to-end example below.
module "exadata" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-oracle-exadata-infrastructure.git?ref=v1.0.0"

  zones               = local.oracle_zone
  resource_group_name = module.rg.name
  # ⚠️ nothing here references module.oracle_anchor — Terraform sees NO dependency

  depends_on = [module.oracle_anchor] # ...so state it explicitly
}

⚠️ No sibling module consumes the anchor's id, because no sibling resource takes it as an argument. Terraform therefore cannot infer that the anchor must exist before the Exadata infrastructure, the VM cluster or the databases.

💡 depends_on is the fix. Without it, Terraform is free to create an Oracle resource before the compartment link exists, and the failure is an apply-time error that names neither module.

ℹ️ In practice both modules usually depend on the same resource group, which gives some ordering — but not enough, because the resource group can exist long before the anchor does.

6 · Destroying the anchor is an ordering hazard
# module.oracle_anchor.azurerm_oracle_resource_anchor.this will be destroyed

⚠️ This severs the OCI compartment link that sibling resources depend on. Destroy the Oracle databases, clusters and infrastructure first.

⚠️ Nothing in the plan says so, and that follows directly from example 5: because no sibling references the anchor, Terraform has no reason to order the destroys correctly either. A plan that removes the anchor while Oracle databases still exist looks perfectly ordinary.

💡 Removing this module is cheap to type and expensive to mean. Where a composition manages the whole family, the same depends_on that fixes creation order also fixes destroy order — Terraform reverses it.

7 · RBAC is written against `Oracle.Database/*`
# ✅ A custom role that actually works for this family.
module "oracle_operator_role" {
  source = "git::.../terraform-azurerm-role-definition.git?ref=v1.0.0"

  scope                        = "scope"

  name        = "Oracle Database at Azure Operator"
  permissions = {
    actions = [
      "Oracle.Database/resourceAnchors/*",
      "Oracle.Database/cloudExadataInfrastructures/*",
      "Oracle.Database/cloudVmClusters/*",
      "Oracle.Database/autonomousDatabases/*",
    ]
  }
}
# ❌ A carefully-scoped role that grants nothing here.
actions = ["Microsoft.Resources/*", "Microsoft.Network/*"]

⚠️ A role scoped to Microsoft providers grants nothing in this family. The resulting failure is an authorization error, which reads like a bug rather than a missing grant.

💡 Note the VM cluster module needs both namespaces — Oracle.Database/* for the cluster and Microsoft.Network/virtualNetworks/subnets/join/action for the subnet join. A role covering only one of the two fails partway through an apply.

8 · Tags stop at the Azure boundary
tags = {
  workload    = "oracle-database"
  cost-centre = "platform"
}

ℹ️ Tags apply on the Azure side only. They are not propagated to the linked OCI compartment or to anything inside it.

⚠️ So an Azure cost-attribution tag does not attribute OCI-side consumption, and an OCI-side tagging requirement is a separate exercise carried out with Oracle's own tooling. Worth knowing before someone builds a chargeback report on the assumption that these tags cover everything.

💡 tags is the only mutable field on this resource, which makes it the only thing here that can be corrected without a replacement.

9 · One anchor per resource group
locals {
  oracle_groups = {
    prod = "rg-oracle-prod"
    test = "rg-oracle-test"
  }
}

module "oracle_anchors" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-oracle-resource-anchor.git?ref=v1.0.0"
  for_each = local.oracle_groups

  name                = "anchor-oracle-${each.key}"
  resource_group_name = each.value

  tags = { environment = each.key }
}

💡 Keyed by environment, so adding a third never re-indexes the first two — which matters because name and resource_group_name are both force-new.

ℹ️ Each anchor links its own resource group to its own OCI compartment, which is the natural way to keep production and test data separated on the OCI side as well as the Azure side.

⚠️ Read back linked_compartment_id per environment and record it. When someone later asks "which OCI compartment is test in?", the answer should not require a state file.

10 · A destroy-and-recreate is not cosmetic
  # module.oracle_anchor.azurerm_oracle_resource_anchor.this must be replaced
-/+ resource "azurerm_oracle_resource_anchor" "this" {
      ~ name = "anchor-oracle-prod" -> "anchor-oracle-production" # forces replacement
    }

⚠️ A rename replaces the anchor, which means severing and re-establishing the OCI compartment link that sibling resources are provisioned against. This is not a name change; it is example 6 with an extra step.

💡 Both name and resource_group_name are force-new, and tags is the only field that is not. Get the name right the first time.

ℹ️ linked_compartment_id is computed, so a replacement may well produce a different compartment OCID. Anything recorded against the old value becomes stale.

11 · Adopting an anchor that already exists
terraform import 'module.oracle_anchor.azurerm_oracle_resource_anchor.this' \
  '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-oracle-prod/providers/Oracle.Database/resourceAnchors/anchor-oracle-prod'

💡 A common real situation: the anchor was created through the portal during onboarding, and Terraform is being introduced afterwards. Importing is the right move — recreating it would sever and re-establish the compartment link (example 6).

ℹ️ Note the Oracle.Database segment in the Resource ID. Copying the ID shape from a Microsoft.* resource and substituting names will not produce a valid one.

⚠️ After importing, run a plan and expect it to be empty. If it proposes a replacement, the name or resource_group_name in your configuration does not match the imported resource — fix the configuration rather than accepting the plan, because that replacement is a compartment relink.

💡 linked_compartment_id is populated by the read, so an import is also the quickest way to discover the OCID of a compartment somebody else set up.

12 · 🏗️ End-to-end composition
provider "azurerm" {
  features {}
}

module "rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-oracle-prod"
  location = "eastus" # the anchor's region follows from here
}

# 1 · The anchor. Apply this first, on its own, the very first time —
#     it is the cheapest place to discover a provider-registration or onboarding problem.
module "oracle_anchor" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-oracle-resource-anchor.git?ref=v1.0.0"

  name                = "anchor-oracle-prod"
  resource_group_name = module.rg.name

  tags = { workload = "oracle-database" }
}

# 2 · The rack. Note the depends_on — nothing here references the anchor.
module "exadata" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-oracle-exadata-infrastructure.git?ref=v1.0.0"

  name                = "exa-oracle-prod"
  resource_group_name = module.rg.name
  location            = module.rg.location

  display_name  = "Oracle Production Exadata"
  shape         = "Exadata.X9M"
  zones         = ["2"]
  compute_count = 2
  storage_count = 3

  customer_contacts = ["oracle-platform@example.com"] # a team alias — force-new

  depends_on = [module.oracle_anchor] # the ordering Terraform cannot infer
}

output "oracle_landing_zone" {
  value = {
    anchor_id       = module.oracle_anchor.id
    region          = module.oracle_anchor.location          # computed
    oci_compartment = module.oracle_anchor.linked_compartment_id # record this
  }
}

🔒 What the composition gets right: the anchor is applied before anything depends on it, the ordering is stated rather than assumed, the OCI compartment OCID is surfaced as an output instead of left in state, and the Exadata notification address is a team alias — which matters because that field is force-new.

⚠️ What no plan will tell you: whether Oracle.Database is registered, whether the tenant is onboarded, and whether the identity running Terraform holds Oracle.Database/* rather than only Microsoft.*. All three produce apply-time failures (examples 1, 2, 7).

💡 On a genuinely first deployment, apply step 1 alone before adding step 2. An Exadata apply takes hours; finding out at the end of it that a namespace was not registered is an expensive way to learn.

📥 Inputs

Input Type Default Notes
name string — Required. Force-new.
resource_group_name string — Required. Force-new. The anchor's region follows from it.
tags map(string) {} The only mutable field. Not propagated to OCI.
timeouts object(...) null create / read / update / delete.
Full schemas
variable "resource_group_name" {
  type = string
  # ℹ️ The anchor's `location` follows from this group rather than being supplied — which is why this
  #    module has no `location` variable, unlike every other module in this family.
  # ⚠️ Immutable: changing it forces a new anchor to be created.
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default = null
  # ⚠️ Deleting the anchor is not a local operation. It severs the link to an OCI compartment that
  #    sibling Oracle Database@Azure resources depend on. Destroy those first.
}

🧾 Outputs

Output Description Kind
id Passthrough
name The anchor's name Passthrough
resource_group_name The resource group the anchor belongs to Passthrough
tags Tags assigned to the anchor on the Azure side Passthrough
linked_compartment_id Passthrough
has_linked_compartment Passthrough
location Passthrough
created_with_global_location Constant
resource_provider_namespace Derived
requires_resource_provider_registration Constant
azure_api_provider_version Derived
arm_properties_object_is_empty Constant
ordering_requires_explicit_depends_on Constant
consumed_by_arm_type Derived
destroy_severs_oci_compartment_link Constant
replacement_triggering_arguments Derived
only_mutable_argument Constant Derived
tags_apply_to_azure_side_only Constant

🔒 Nothing sensitive is accepted or emitted. The compartment OCID is an identifier, not a credential.

🧠 Architecture Notes

  • The third-party provider namespace leads the documentation deliberately. Almost everything that goes wrong with Oracle Database@Azure in Terraform is a provider-registration, tenant-onboarding or RBAC-namespace problem rather than a configuration error, and all three fail here first. A reader who learns that from the smallest module in the family saves themselves discovering it on a multi-hour Exadata apply.

  • The module has no location variable because the resource has no location argument. The provider marks it computed, following the resource group. Adding an input for consistency with the rest of the library would mean inventing something the provider does not accept.

  • linked_compartment_id is treated as the module's product, not as an incidental attribute. An anchor whose OCID nobody recorded is an anchor that will be hunted for in a state file during an incident. The output description says what it is for rather than what it is.

  • The invisible dependency is documented rather than solved with a synthetic input. This module could accept an unused handle purely to create a graph edge, but a variable that exists only to influence ordering misrepresents what the resource does. Naming the gap and pointing at depends_on is more honest and equally actionable.

  • The destroy hazard follows from the same fact and is stated separately anyway. Because no sibling references the anchor, Terraform cannot order the destroys correctly either — so the warning belongs in its own place rather than as a footnote to the creation-ordering note.

  • Provider registration is deliberately out of scope. It is a subscription-level concern, and a module that registered namespaces as a side effect of creating a resource would be reaching well outside what its name promises.

  • Tags are documented as stopping at the Azure boundary, because the natural assumption is that a tag on an Azure resource covers the cost it generates. Here a substantial part of that cost is incurred on the OCI side, where these tags do not reach.

  • tags is the only mutable field, which makes this module unusually unforgiving for its size: two of its three inputs force replacement, and a replacement is an OCI compartment relink.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Provider namespace stated up front, in the badge row and the RBAC table —
Least privilege RBAC documented against Oracle.Database/*, per resource type —
Tenant prerequisites onboarding and registration named as prerequisites, not assumed —
Ordering the invisible dependency documented, with depends_on named —
Destroy safety the compartment-relink consequence stated plainly —
Scope discipline provider registration left to the subscription owner —
Cross-cloud boundary tags documented as Azure-only —
Secrets none accepted, none emitted —
  • Before the first apply: register Oracle.Database and confirm tenant onboarding.
  • Before the first apply: confirm the identity holds Oracle.Database/*, not only Microsoft.*.
  • Before adding siblings: add depends_on pointing at this module.
  • Before planning a destroy: destroy the Oracle resources that depend on the compartment link first.
  • After the first apply: record linked_compartment_id somewhere a human can find it.

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the source to a tag — ?ref=v1.0.0 — never a branch.
  • Plan-only from here. A human applies from CI.
  • ⚠️ On a first deployment, apply this module alone before anything else in the family. It is the cheapest place to discover a registration or onboarding failure.
  • ⚠️ Read any plan that replaces or destroys this anchor as a compartment-relink plan, not a rename.
  • ℹ️ Only tags updates in place.
  • ℹ️ Record linked_compartment_id after apply — it is what Oracle support will ask for.

🧪 Testing

terraform validate and terraform fmt -check are the offline gate. They confirm:

  • name and resource_group_name are present and typed correctly;
  • the timeouts value is an object of Go duration strings - but note that a key the type does not declare is SILENTLY DISCARDED rather than refused, so a misspelling here produces no error anywhere and the provider default quietly applies;
  • the module declares no provider block.

What only plan and apply exercise:

  • whether the resource group exists;
  • whether the anchor name is already taken.

What no Terraform command checks at any stage, and which is where this module actually fails:

  • whether the Oracle.Database resource provider is registered on the subscription;
  • whether the tenant is onboarded to Oracle Database@Azure;
  • whether the identity holds Oracle.Database/* permissions rather than only Microsoft.*;
  • whether the resource group's region offers Oracle Database@Azure at all.

💬 Example Output

Outputs:

id                    = "/subscriptions/00000000-.../resourceGroups/rg-oracle-prod/providers/Oracle.Database/resourceAnchors/anchor-oracle-prod"
linked_compartment_id = "ocid1.compartment.oc1..aaaaaaaaexampleexampleexample"
location              = "eastus"
name                  = "anchor-oracle-prod"
resource_group_name   = "rg-oracle-prod"

ℹ️ Note the Oracle.Database segment in the Resource ID rather than a Microsoft.* one — the clearest single indication that this family is not what most azurerm resources are.

💡 linked_compartment_id is the value worth copying somewhere durable.

🔍 Troubleshooting

Symptom Cause Fix
Apply fails with a provider-not-registered error Oracle.Database is not registered — it is not a Microsoft namespace. az provider register --namespace Oracle.Database (example 1).
Apply fails and the provider is registered The tenant is not onboarded to Oracle Database@Azure. Complete the marketplace subscription and Oracle account link (example 2).
Authorization error despite a carefully-scoped role The role covers Microsoft.* but not Oracle.Database/*. Add the Oracle namespace actions (example 7).
Wanted the anchor in a specific region and there is no input location is computed from the resource group. Place the resource group in that region (example 3).
A sibling Oracle resource was created before the anchor No sibling references the anchor, so Terraform saw no dependency. Add depends_on (example 5).
A destroy removed the anchor while databases still existed Same cause — no dependency edge to order the destroys. Destroy dependants first (example 6).
A rename produced a different OCI compartment OCID name is force-new, and the OCID is computed. Expected; record the new value (example 10).
Azure cost tags do not account for OCI-side spend Tags stop at the Azure boundary. Use Oracle's own tooling for OCI attribution (example 8).
Oracle support asked for an identifier and the Resource ID was not it They want the OCI OCID. Use linked_compartment_id (example 4).
The region was accepted but Oracle resources fail there Oracle Database@Azure is not offered in every Azure region. Choose a supported region.

🔗 Related Docs

💙 "Infrastructure as Code should be standardized, consistent, and secure."