Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Data Factory Custom Dataset Terraform Module

The generic dataset β€” any type, properties as raw JSON, and a name validator that almost never fires. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Caveat


🧩 Overview

  • πŸ“¦ Describes a dataset of any Data Factory type β€” the type is a string and its properties are raw JSON.
  • 🎁 The provider wraps your JSON in a typeProperties envelope, so supply the inner object only. A double-wrap is detected and reported.
  • πŸ” The JSON is validated for parseability only. Constraining a vendor-defined shape would reject legal payloads as they evolve β€” and block terraform destroy.
  • πŸ”΄ The name validator is effectively inert: its regex rejects only a name made entirely of its "forbidden" characters, so my/dataset:v1 passes.
  • πŸ”— The linked service is named by string, and nothing verifies it exists or suits the type.
  • ⏱️ Timeouts here are the usual thirty minutes β€” unlike this family's credential resources, whose four all default to five.

πŸ’‘ Why it matters: almost nothing about this resource is checked. What the module can do is report β€” which keys the JSON actually contains, whether the envelope was supplied twice, and whether the name carries characters Azure may refuse β€” so a check block can enforce intent where a validation {} would be wrong.


❀️ Support this project

If this module saves you time:


πŸ—ΊοΈ Where this fits in the family

flowchart TB
  ADF["azurerm_data_factory<br/>the parent - has tags"]
  THIS["THIS MODULE<br/>custom dataset<br/>generic, any type"]
  TYPED["azurerm_data_factory_dataset_*<br/>the typed alternatives"]
  LS["a linked service<br/>named by STRING, never verified"]
  CRED["credentials<br/>where authentication lives"]
  PIPE["pipelines<br/>reference this BY NAME<br/>nothing points back"]
  DATA["the underlying data<br/>described, never touched"]

  ADF -->|"data_factory_id"| THIS
  ADF -->|"data_factory_id"| TYPED
  THIS -->|"linked_service.name"| LS
  LS -->|"authenticates via"| CRED
  PIPE -.->|"uses by name"| THIS
  LS -.->|"reaches"| DATA

  classDef me fill:#0078D4,stroke:#004578,color:#ffffff
  classDef keystone fill:#004578,stroke:#002B44,color:#ffffff
  classDef other fill:#F0F3F7,stroke:#B8C4D0,color:#1B1B1B
  class THIS me
  class ADF keystone
  class TYPED,LS,CRED,PIPE,DATA other
Loading

Note the dotted edge from pipelines: they reference a dataset by name, and nothing points back. That is the fact to know before deleting one.


🧬 What this module builds

flowchart TB
  IN1["var.name<br/>validator is INERT"]
  IN2["var.data_factory_id"]
  IN3["var.type + var.type_properties_json<br/>COUPLED and UNCHECKED"]
  IN4["var.linked_service<br/>a NAME, never verified"]
  IN5["var.schema_json / parameters<br/>additional_properties / annotations"]
  R["azurerm_data_factory_custom_dataset.this"]
  O1["id / name"]
  O2["name_contains_characters<br/>_azure_may_reject"]
  O3["type_property_keys<br/>type_properties_looks_double_wrapped"]
  O4["the_json_shape_is_not<br/>_validated_by_anything"]
  O5["the_module_cannot_see<br/>_which_pipelines_use_this"]

  IN1 --> R
  IN2 --> R
  IN3 --> R
  IN4 --> R
  IN5 --> R
  R --> O1
  IN1 --> O2
  IN3 --> O3
  IN3 --> O4
  R --> O5

  classDef me fill:#0078D4,stroke:#004578,color:#ffffff
  classDef io fill:#F0F3F7,stroke:#B8C4D0,color:#1B1B1B
  class R me
  class IN1,IN2,IN3,IN4,IN5,O1,O2,O3,O4,O5 io
Loading

Resource inventory

Resource Count Note
azurerm_data_factory_custom_dataset 1 (this) One nested block, capped at a single entry.

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module β€” the caller configures the provider, its authentication, and its mandatory features {} block.

Schema notes that bite

  • πŸ”΄ The provider wraps type_properties_json in a typeProperties envelope. Supply the inner object only; a payload already containing that key is double-nested and nothing reports it.
  • πŸ”΄ The name validator is effectively inert. Its regex ^[-.+?/<>*%&:\\]+$ is anchored over the whole string, so it rejects only a name made entirely of those characters β€” my/dataset:v1 passes despite the message listing / and : as not allowed.
  • πŸ”΄ Nothing validates the JSON shape or its agreement with type β€” not the provider, not this module.
  • ⚠️ Both JSON fields use semantic comparison, so reordering or reformatting produces no diff.
  • ⚠️ type has no enum β€” any non-empty string, because the type set is Microsoft's and grows.
  • ⚠️ linked_service is capped at one block, a limit absent from the published schema, and its name is never verified to exist.
  • ⚠️ additional_properties writes arbitrary top-level keys and can collide with managed fields silently.
  • ⚠️ Only name, data_factory_id and type are force-new β€” everything describing what the dataset reads updates in place.
  • ⚠️ Timeouts are the usual 30m/5m/30m/30m, unlike this family's credential resources.

πŸ”‘ Required Azure RBAC Roles / Permissions

Permission Scope Why
Microsoft.DataFactory/factories/datasets/write the Data Factory Create and update.
Microsoft.DataFactory/factories/datasets/read the Data Factory Refresh and plan.
Microsoft.DataFactory/factories/datasets/delete the Data Factory Destroy β€” which can break a pipeline still using it.
Data Factory Contributor the Data Factory The built-in role containing the above.

πŸ”’ No permission on the data source is required or used. A dataset describes where data lives; it does not read it. Whoever can write datasets can describe any storage account or database β€” the access itself belongs to the linked service and the credential behind it. Delete is the operation to review, not create: removing a dataset breaks every pipeline referencing it by name, and nothing can see those references.


Azure Prerequisites

  • An existing Data Factory.
  • A linked service already configured in that factory, whose name is the only handle this dataset has.
  • Knowledge of the correct type string and the property shape it expects β€” both Microsoft's, both unvalidated here and in the provider.
  • A name satisfying Microsoft's naming rules for Data Factory objects, which the provider does not meaningfully enforce.

πŸ“ Module Structure

terraform-azurerm-data-factory-custom-dataset/
β”œβ”€β”€ providers.tf    # required_version + the pinned azurerm; no provider block
β”œβ”€β”€ variables.tf    # twelve inputs, eleven validations
β”œβ”€β”€ main.tf         # locals decoding the JSON and reporting what it contains
β”œβ”€β”€ outputs.tf      # 46 outputs, led by the two provider behaviours that surprise
β”œβ”€β”€ README.md       # this file
β”œβ”€β”€ SCOPE.md        # the cross-module contract
β”œβ”€β”€ LICENSE         # MIT
└── .gitignore

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "dataset" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-custom-dataset.git?ref=v1.0.0"

  name            = "ds_landing"
  data_factory_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data-eastus/providers/Microsoft.DataFactory/factories/adf-corp"
  type            = "AzureBlob"

  linked_service = {
    name = "ls-blob"
  }

  # The INNER object only -- the provider adds the typeProperties envelope.
  type_properties_json = jsonencode({
    folderPath = "raw"
    container  = "landing"
  })
}

πŸ’‘ jsonencode() is the readable way to write these: it produces valid JSON from HCL, and the provider compares the field semantically so formatting never causes a diff.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source
data_factory_id string terraform-azurerm-data-factory (id)
linked_service object caller β€” a linked service name
type, type_properties_json string caller β€” coupled, neither checks the other
schema_json, parameters, additional_properties, annotations, description, folder, timeouts β€” caller

Emits

Output Description
id The dataset's Resource ID.
name How a pipeline refers to it.
type_property_keys What the JSON actually contains.
type_properties_looks_double_wrapped Whether the envelope was supplied twice.
name_contains_characters_azure_may_reject The suspect-character report.
uses_additional_properties Whether the escape hatch is in use.

πŸ“š Example Library

1 Β· The minimum call
module "dataset" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-custom-dataset.git?ref=v1.0.0"

  name            = "ds_landing"
  data_factory_id = var.data_factory_id
  type            = "AzureBlob"
  linked_service  = { name = "ls-blob" }

  type_properties_json = jsonencode({
    folderPath = "raw"
    container  = "landing"
  })
}

ℹ️ Nothing here is verified: not the type, not the property names, not the linked service.

2 Β· The envelope trap
# WRONG -- the provider adds the envelope itself, so this nests twice:
#
#   type_properties_json = jsonencode({
#     typeProperties = { folderPath = "raw" }
#   })

check "no_double_wrap" {
  assert {
    condition     = !module.dataset.type_properties_looks_double_wrapped
    error_message = "type_properties_json already contains a typeProperties key. The provider wraps this value itself, so the payload will be nested twice and Azure will not interpret it as intended."
  }
}

⚠️ Reported rather than refused, because a dataset type could in principle have a property of that name. The check block is where the judgement belongs.

3 Β· Asserting the JSON contains what it should
check "blob_properties_present" {
  assert {
    condition = alltrue([
      for k in ["folderPath", "container"] : contains(module.dataset.type_property_keys, k)
    ])
    error_message = "The AzureBlob dataset is missing folderPath or container. Nothing in the provider or the module checks the JSON shape, so assert on the keys here."
  }
}

πŸ’‘ This is the nearest thing to shape validation that cannot reject legal input β€” and it lives in the caller, where the expected keys can change without a module release.

4 Β· The name validator almost never fires
module "awkward_name" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-custom-dataset.git?ref=v1.0.0"

  name            = "my/dataset:v1" # ACCEPTED -- by the provider and by this module
  data_factory_id = var.data_factory_id
  type            = "AzureBlob"
  linked_service  = { name = "ls-blob" }
  type_properties_json = jsonencode({ folderPath = "raw" })
}

output "the_defect" {
  value = module.awkward_name.the_provider_name_validator_is_effectively_inert
}

πŸ”΄ The provider's message lists twelve characters as not allowed; its regex rejects only a name composed entirely of them. The module mirrors that exactly rather than enforcing the message, because enforcing it would reject names the provider and Azure may accept β€” and a failed validation blocks terraform destroy.

5 Β· Using the name report without drowning in it
output "name_check" {
  value = module.dataset.name_contains_characters_azure_may_reject
}

⚠️ A hyphen or a period alone makes this true, and both appear in perfectly working names. Treat it as a prompt to check Microsoft's naming rules rather than as a verdict β€” which is why the module reports it instead of refusing.

6 Β· A schema, and why it is usually omitted
module "typed_dataset" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-custom-dataset.git?ref=v1.0.0"

  name            = "ds_customers"
  data_factory_id = var.data_factory_id
  type            = "DelimitedText"
  linked_service  = { name = "ls-blob" }

  type_properties_json = jsonencode({
    location = { type = "AzureBlobStorageLocation", container = "curated", fileName = "customers.csv" }
    columnDelimiter  = ","
    firstRowAsHeader = true
  })

  schema_json = jsonencode([
    { name = "id", type = "String" },
    { name = "created_at", type = "DateTime" },
  ])
}

ℹ️ Unlike the type properties, schema_json is sent as supplied with no envelope added. Data Factory can read most sources without one, so omitting it is normal.

7 Β· Parameterised datasets
module "parameterised" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-custom-dataset.git?ref=v1.0.0"

  name            = "ds_by_date"
  data_factory_id = var.data_factory_id
  type            = "AzureBlob"

  linked_service = {
    name       = "ls-blob"
    parameters = { environment = "prod" }
  }

  parameters = {
    partitionDate = "2026-01-01"
  }

  type_properties_json = jsonencode({
    folderPath = "@dataset().partitionDate"
    container  = "landing"
  })
}

πŸ’‘ Note the two parameter maps: parameters belongs to the dataset, linked_service.parameters is passed through to the linked service. They are not interchangeable.

8 Β· The escape hatch, flagged
module "with_extras" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-custom-dataset.git?ref=v1.0.0"

  name            = "ds_landing"
  data_factory_id = var.data_factory_id
  type            = "AzureBlob"
  linked_service  = { name = "ls-blob" }
  type_properties_json = jsonencode({ folderPath = "raw" })

  additional_properties = {
    someUnmodelledField = "value"
  }
}

check "escape_hatch_is_deliberate" {
  assert {
    condition     = !module.with_extras.uses_additional_properties
    error_message = "This dataset uses additional_properties, which writes arbitrary top-level keys into the definition. Confirm it is intended -- a key colliding with a managed field is a conflict nothing reports."
  }
}

⚠️ The check above is written to fail on this configuration deliberately, as a review gate. Remove it once the use is agreed.

9 Β· Reformatting the JSON is free
output "no_diff_on_reformat" {
  value = module.dataset.json_key_ordering_produces_no_diff
}

πŸ’‘ The provider compares both JSON fields semantically and suppresses key-ordering differences. Switching from a hand-written string to jsonencode(), or reordering keys, produces no plan diff at all.

10 Β· Several datasets over one linked service
locals {
  containers = {
    landing = "raw"
    curated = "curated"
  }
}

module "datasets" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-custom-dataset.git?ref=v1.0.0"
  for_each = local.containers

  name            = "ds_${each.key}"
  data_factory_id = var.data_factory_id
  type            = "AzureBlob"
  linked_service  = { name = "ls-blob" }

  type_properties_json = jsonencode({
    folderPath = each.value
    container  = each.key
  })
}

output "property_counts" {
  value = { for k, m in module.datasets : k => m.type_property_count }
}

πŸ’‘ The provider takes no lock on the factory, so these are created concurrently. Key by a stable label.

11 Β· What the module cannot see
output "unverifiable" {
  value = {
    linked_service = module.dataset.the_linked_service_is_not_verified_to_exist
    json_shape     = module.dataset.the_json_shape_is_not_validated_by_anything
    type_pairing   = module.dataset.the_type_and_the_json_are_coupled_and_unchecked
    consumers      = module.dataset.the_module_cannot_see_which_pipelines_use_this
  }
}

πŸ’‘ The last is the one to read before a destroy: pipelines reference a dataset by name, and nothing points back from the dataset to them.

12 Β· Re-pointing a dataset is an in-place update
output "change_surface" {
  value = {
    force_new = module.dataset.force_new_fields                      # name, factory, type
    in_place  = module.dataset.fields_that_can_change_after_creation # everything else
  }
}

⚠️ Changing where a dataset points β€” its linked service, its folder, its container β€” is an in-place update. Only its identity and its type force a replacement, so a review scanning for destroys will not surface a dataset being redirected at different data.

13 Β· Importing an existing dataset
import {
  to = module.dataset.azurerm_data_factory_custom_dataset.this
  id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data-eastus/providers/Microsoft.DataFactory/factories/adf-corp/datasets/ds_landing"
}

⚠️ The datasets path segment is shared with every typed dataset resource in the provider, so the ID alone does not tell you whether a dataset should be imported here or into a typed resource. Check its type in the portal first.

14 Β· πŸ—οΈ End-to-end composition
provider "azurerm" {
  features {}
}

# 1. The factory -- the parent, and the only taggable resource here.
module "data_factory" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory.git?ref=v1.0.0"

  name                = "adf-corp"
  resource_group_name = "rg-data-eastus"
  location            = "eastus"

  tags = { owner = "data-platform" }
}

# 2. A credential the linked service will authenticate with. No secret passes
#    through Terraform -- it names a Key Vault secret.
module "credential" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-credential-user-managed-identity.git?ref=v1.0.0"

  name            = "cred-managed"
  data_factory_id = module.data_factory.id
  identity_id     = var.user_assigned_identity_id

  depends_on = [module.data_factory]
}

# 3. THIS MODULE -- a description of where data lives. It reads nothing.
module "dataset" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-factory-custom-dataset.git?ref=v1.0.0"

  name            = "ds_landing"
  data_factory_id = module.data_factory.id
  type            = "AzureBlob"

  # The linked service is named by STRING and is not created here -- it is
  # configured in the factory and referenced by name.
  linked_service = { name = "ls-blob" }

  type_properties_json = jsonencode({
    folderPath = "raw"
    container  = "landing"
  })

  description = "Landing-zone blobs written by the ingest pipeline."
  folder      = "raw"
  annotations = ["owner:data-platform"]
}

check "json_has_the_expected_keys" {
  assert {
    condition = alltrue([
      for k in ["folderPath", "container"] : contains(module.dataset.type_property_keys, k)
    ])
    error_message = "The AzureBlob dataset is missing folderPath or container."
  }
}

check "no_double_wrap" {
  assert {
    condition     = !module.dataset.type_properties_looks_double_wrapped
    error_message = "type_properties_json already contains a typeProperties key; the provider adds that envelope itself."
  }
}

output "dataset_status" {
  description = "What is described, and what Terraform deliberately cannot confirm."
  value = {
    dataset         = module.dataset.id
    used_by_name    = module.dataset.name
    properties      = module.dataset.type_property_keys
    linked_service  = module.dataset.linked_service_name
    credential      = module.credential.name
    reads_anything  = false # A dataset is a description; pipelines do the work.
  }
}

⚠️ The literal false is deliberate and is not a module output. Creating a dataset moves no data, incurs no cost, and touches no storage account β€” a pipeline must use it before anything happens.


πŸ“₯ Inputs

Input Type Required Note
name string βœ… Force-new. Validator mirrored; suspect characters reported.
data_factory_id string βœ… Force-new. Anchored at both ends.
type string βœ… Force-new. No enum.
linked_service object βœ… A name, plus optional parameters.
type_properties_json string βœ… The inner object; parseability checked only.
schema_json string β€” Parseability checked only.
parameters map(string) β€” The dataset's own parameters.
additional_properties map(string) β€” An escape hatch; reported.
annotations list(string) β€” Not tags.
description, folder string β€” Optional; empty refused.
timeouts object β€” Four keys, 30m/5m/30m/30m.
Full schemas
variable "name" {
  type = string
  # Non-empty, and refuses a name made ENTIRELY of - . + ? / < > * % & : \
  # -- which is the only thing the provider actually rejects. A name merely
  # CONTAINING them is accepted, and reported.
}

variable "data_factory_id" {
  type = string
  # /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.DataFactory
  #   /factories/<factory>
}

variable "type" {
  type = string
  # Non-empty only. NO enum -- the dataset type set is Microsoft's and grows.
  # Decides what type_properties_json must contain; the pairing is unchecked.
}

variable "linked_service" {
  type = object({
    name       = string                 # a NAME in this factory, not an ID
    parameters = optional(map(string))
  })
  # A single object: the provider permits one block, a limit absent from its
  # published schema.
}

variable "type_properties_json" {
  type = string
  # Checked for PARSEABILITY and that it decodes to an OBJECT. Nothing else.
  # SUPPLY THE INNER OBJECT -- the provider adds a typeProperties envelope.
}

variable "schema_json" {
  type    = string
  default = null
  # Parseability only. Sent as supplied, with no envelope.
}

variable "parameters" {
  type    = map(string)
  default = {}
}

variable "additional_properties" {
  type    = map(string)
  default = {}
  # An escape hatch: arbitrary top-level keys, able to collide with managed
  # fields without either Terraform or the provider reporting it.
}

variable "annotations" {
  type    = list(string)
  default = []
  # Data Factory annotations -- a LIST, unrelated to Azure resource tags.
}

variable "description" {
  type    = string
  default = null
}

variable "folder" {
  type    = string
  default = null
  # Omit for the root level. Purely an authoring convenience.
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default = null
  # Provider defaults: create 30m, update 30m, read 5m, delete 30m.
}

🧾 Outputs

Output Description Note
id The dataset's Resource ID. first
name How a pipeline refers to it.
data_factory_id / data_factory_name The parent factory.
resource_group_name / subscription_id Where the factory lives.
name_contains_characters_azure_may_reject The suspect-character report. derived
the_provider_name_validator_is_effectively_inert The provider defect. constant
microsoft_naming_rules_are_the_real_authority Who actually decides. constant
type The dataset type.
type_property_keys / type_property_count What the JSON contains. derived
type_properties_looks_double_wrapped The envelope supplied twice. derived
the_provider_wraps_the_type_properties_json The easiest mistake. constant
the_json_shape_is_not_validated_by_anything The design decision. constant
the_type_and_the_json_are_coupled_and_unchecked Two arguments, no cross-check. constant
json_key_ordering_produces_no_diff Reformatting is free. constant
has_schema_json Whether a schema was supplied. derived
linked_service_name / linked_service_parameter_count The linked service.
the_linked_service_is_not_verified_to_exist Named, never checked. constant
only_one_linked_service_block_is_permitted Why the input is an object. constant
parameters / parameter_count The dataset's parameters.
uses_additional_properties / additional_property_count The escape hatch, flagged. derived
annotations / annotation_count / has_annotations A list, not tags. derived
description / has_description / folder / has_folder Metadata.
force_new_fields name, factory, type. list
fields_that_can_change_after_creation Everything describing what it reads. list
fields_azure_returns_on_read What a refresh repopulates. list
this_dataset_moves_no_data_by_itself It is a description. constant
the_module_cannot_see_which_pipelines_use_this Read before deleting. constant
timeouts_here_are_the_usual_thirty_minutes Unlike the credential siblings. constant
destroying_this_does_not_touch_the_underlying_data No blobs are harmed. constant
destroying_the_factory_destroys_this_dataset_too Ownership. constant
lifecycle_prevent_destroy_is_not_available_to_a_module_caller Use a management lock. constant
this_is_a_real_azure_resource_not_a_composite Real ID, real delete. constant
the_provider_takes_no_lock_on_the_data_factory Concurrent datasets are safe. constant
this_resource_supports_no_azure_resource_tags Tag the factory. constant
no_secret_is_accepted_or_emitted_by_this_module With one caveat β€” see below. constant

No output is sensitive. Note, though, that type_properties_json is free-form: a caller could write something sensitive into it, nothing would stop them, and it would sit in plain text in state.


🧠 Architecture Notes

Almost nothing about this resource is validated, and the module's job is to report rather than to invent rules. The type is any string, its properties are raw JSON, and the linked service is a name β€” so the module checks that the JSON parses and decodes to an object, and stops there. Encoding Microsoft's per-type property sets into a Terraform module would put them somewhere they go stale independently of the provider, and because a failed validation {} blocks terraform destroy as well as apply, a wrong constraint would strand anyone already using a payload it rejected. Instead the module emits type_property_keys, so a check block in the caller can assert that expected properties are present β€” shape enforcement placed where it can be maintained without a module release.

The provider wraps your JSON, and the double-wrap is the easiest mistake here. It sends the supplied value as {"typeProperties": <your json>}, so the caller provides the inner object. A payload that already carries a typeProperties key gets nested twice, produces no error, and simply does not do what was intended. type_properties_looks_double_wrapped detects it β€” reported rather than refused, since a dataset type could in principle have a property of that name.

The name validator is a defect worth knowing about. Its message lists twelve characters as not allowed in a dataset name; its regex is anchored over the whole string with +, so it matches only a name made entirely of them. "---" is rejected and "my/dataset:v1" is accepted. The module mirrors the provider exactly β€” never rejecting what the provider accepts β€” and reports the suspect characters instead, with the report's own description warning that a hyphen or period alone makes it true. Without that warning the flag would fire on most real names and be ignored.

The interesting change is an in-place update. Only name, data_factory_id and type are force-new; the linked service, the type properties and the schema all update in place. So re-pointing a dataset at a different container, folder or file is an attribute change rather than a replacement, and a review that scans plans for destroys will not surface it.

And nothing points back from a dataset to its consumers. Pipelines reference a dataset by name. Destroying one breaks every pipeline that uses it, and neither Terraform nor this module can see those references β€” which is why the fact is emitted rather than left for a reader to discover.


🧱 Design Principles

Concern This module's position Opt-out
Raw JSON Validated for parseability and object-ness only; the shape is reported, never constrained. β€”
Provider defects The inert name validator is mirrored exactly and reported, not corrected. β€”
Rejecting legal input Nothing the provider accepts is refused here β€” not a wrong-shaped payload, not an unusual name, not a misspelled type. β€”
Escape hatches additional_properties is emitted as a flag so its use is visible in review. β€”
Invisible constraints The one-block linked_service cap, absent from the published schema, is mirrored by the object type. β€”
Unverifiable facts The linked service, the type pairing and the consuming pipelines are all stated as uncheckable. β€”
Secrets None accepted or emitted β€” with the caveat that free-form JSON could carry one into state. β€”

There is no security-relevant boolean or enum here to default, so the empty call does not exist: every consequential argument is required.


πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the source at ?ref=v1.0.0, never a branch. This module is plan-only; a human applies from CI.


πŸ§ͺ Testing

terraform validate and terraform fmt -check cover the module's structure and all eleven validation {} blocks: the two name rules, the anchored factory ID, the non-empty type, the two linked-service rules, both JSON parseability checks, the decodes-to-an-object check, and the description and folder rules.

terraform console against a root module is the harness that actually exercises them, because validate on a calling configuration does not fire them. Terraform also skips a validation whose referenced variable has already failed, so a short error list is not evidence a check is missing.

The accepting direction is the real test here. A name containing / and : must be accepted β€” if it is refused, the module has become stricter than the provider and would reject legal input. So must valid JSON of the wrong shape for the declared type, and a payload that already carries the typeProperties envelope: both are reported, not refused, and a fixture proving that is worth more than any of the rejections.

Also worth exercising in both directions: type_properties_looks_double_wrapped, uses_additional_properties, has_schema_json, and the name-character report β€” including a name with no suspect characters at all, which is the only case that returns false.

What no offline check can reach: whether the factory or the linked service exists, whether the type string is real, whether the JSON matches that type, and which pipelines depend on the dataset.


πŸ’¬ Example Output

id                    = "/subscriptions/00000000-.../factories/adf-corp/datasets/ds_landing"
name                  = "ds_landing"
data_factory_name     = "adf-corp"
type                  = "AzureBlob"
type_property_keys    = ["container", "folderPath"]
type_property_count   = 2
type_properties_looks_double_wrapped = false
name_contains_characters_azure_may_reject = false
linked_service_name   = "ls-blob"
has_schema_json       = false
uses_additional_properties = false
annotation_count      = 1
force_new_fields      = ["name", "data_factory_id", "type"]
this_dataset_moves_no_data_by_itself = true

πŸ” Troubleshooting

Symptom Cause Fix
The pipeline fails saying the dataset is misconfigured The JSON does not match the declared type. Nothing checks the pairing. Compare type_property_keys against Microsoft's properties for that dataset type.
The dataset applies but behaves as if empty The typeProperties envelope was supplied twice, producing a doubly-nested payload. Check type_properties_looks_double_wrapped and supply the inner object only.
A name with / or : was accepted and Azure rejected it The provider's validator only refuses names made entirely of those characters. Check name_contains_characters_azure_may_reject and Microsoft's naming rules.
name_contains_characters_azure_may_reject is true for an ordinary name A hyphen or period alone makes it true. Treat it as a prompt, not a verdict.
The dataset cannot reach the data The linked service name is wrong, or the linked service lacks a working credential. The name is never verified β€” confirm it in the factory, and check the credential behind it.
Reformatting the JSON produced no diff The provider compares both JSON fields semantically. Expected, and safe.
A pipeline broke after removing a dataset Pipelines reference datasets by name and nothing points back. Search the factory's pipelines before deleting a dataset.
Re-pointing a dataset showed no replacement The linked service and type properties update in place. Expected. Assert on the derived outputs rather than reviewing for destroys.
An additional_properties key had no effect, or an unexpected one It writes arbitrary top-level keys and can collide with managed fields. Check uses_additional_properties; prefer the modelled arguments.
A create timed out sooner than expected These timeouts are 30m β€” but the credential resources in this family are 5m. Check which resource is actually timing out.

πŸ”— Related Docs


πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."