Manages a single
dynatrace_documentresource — the Platform (Grail-native) container for dashboards-as-documents, notebooks, and launchpads — targeting providerdynatrace-oss/dynatrace ~> 1.98(confirmed against the live schema for v1.100.0).
- 🎯 Manages one
dynatrace_document.thiskeystone resource per module instance — a Platform, Grail-native document object (dashboard-as-document, launchpad, or notebook), addressed via Dynatrace's Document Service API rather than classic Settings 2.0 or Config API. - 🧷 No nested
block_typesexist in the live schema — this is a flat, single-resource module with no owned children and nofor_eachcollection anywhere inmain.tf. - 🔒 Defaults
private = true— an house override of the provider's own undocumented zero-value default (see § Schema notes that bite). A caller must explicitly setprivate = falseto make a document readable by everybody in the environment. - 🧬
contentis an opaque, provider-untyped JSON string whose internal shape depends ontype(dashboard/launchpad/notebook) — this module does not attempt to model or validate that internal shape, matching the provider's own schema. - 🚫 Dynatrace SaaS only. This resource's own documentation states it is not available on Dynatrace Managed.
- 🔗 Is distinct from both
dynatrace_dashboard(typed classic/Settings 2.0 dashboard) anddynatrace_json_dashboard(raw-JSON classic Config API dashboard) — see § Where this fits. Itsidoutput is consumed by the siblingtf-mod-dynatrace-direct-sharesmodule.
💡 Why it matters:
dynatrace_documentis the newer, Platform-native successor container for dashboards, notebooks, and launchpads — and it authenticates differently than almost every other Settings 2.0 resource in this catalog. Its own documentation states plainly that only OAuth client credentials are accepted; there is no legacy-API-token fallback. A caller who provisions only a classic API token for this module will get an opaque authentication failure atapplytime that has nothing to do with the permissions granted to that token — the token type itself is wrong. Get the auth mechanism right first (§ Required OAuth Scopes / API Token Permissions), then reason about content and sharing.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
Validated via the Mermaid Chart MCP (valid: true) before embedding.
graph LR
DOC["tf-mod-dynatrace-document"]:::thisModule
KEYSTONE["dynatrace_document.this"]:::keystone
DASH["dynatrace_dashboard (classic Settings 2.0 -- distinct resource)"]:::excluded
JSONDASH["dynatrace_json_dashboard (classic Config API -- distinct resource)"]:::excluded
DS["tf-mod-dynatrace-direct-shares"]:::sibling
DOC -->|"renders"| KEYSTONE
DOC -->|"id output as document_id"| DS
DASH -.->|"sibling dashboard resource -- out of scope"| KEYSTONE
JSONDASH -.->|"sibling dashboard resource -- out of scope"| KEYSTONE
classDef thisModule fill:#1496FF,color:#FFFFFF,stroke:#0A2540,stroke-width:2px;
classDef keystone fill:#0A2540,color:#FFFFFF,stroke:#0A2540,stroke-width:2px;
classDef sibling fill:#E8EAED,color:#1A1A1A,stroke:#B0B4BA,stroke-width:1px;
classDef excluded fill:#F5F5F5,color:#8A8F98,stroke:#B0B4BA,stroke-width:1px,stroke-dasharray: 3 3;
class DOC thisModule;
class KEYSTONE keystone;
class DS sibling;
class DASH,JSONDASH excluded;
This module is a standalone primitive with one confirmed downstream consumer:
tf-mod-dynatrace-direct-shares, which wires this module's id output into its own document_id
input to grant read/read-write access to the document produced here. The two dashed edges are
intentional — dynatrace_dashboard and dynatrace_json_dashboard are structurally and
authentication-wise distinct resources, each its own sibling module, and neither shares any schema
or state with dynatrace_document. Do not attempt to migrate a dynatrace_document between those
resource types or vice versa; they are separate provider resources with separate lifecycles.
Validated via the Mermaid Chart MCP (valid: true) before embedding.
graph TB
NAME["var.name -- required string"]
TYPE["var.type -- required string, closed 3-value enum"]
CONTENT["var.content -- required string, JSON-encoded"]
CUSTOMID["var.custom_id -- optional string, default null"]
PRIVATE["var.private -- optional bool, default true (secure-default override)"]
subgraph KEYSTONE["dynatrace_document.this"]
direction TB
ATTRS["name, type, content, custom_id, private"]
COMPUTED["owner, version -- computed-only, not caller inputs"]
ATTRS --- COMPUTED
end
OID["output: id"]
ONAME["output: name"]
NAME --> KEYSTONE
TYPE --> KEYSTONE
CONTENT --> KEYSTONE
CUSTOMID --> KEYSTONE
PRIVATE --> KEYSTONE
KEYSTONE --> OID
KEYSTONE --> ONAME
classDef keystoneStyle fill:#0A2540,color:#FFFFFF,stroke:#1496FF,stroke-width:2px;
classDef ioStyle fill:#E8EAED,color:#111111,stroke:#8A8F98,stroke-width:1px;
class KEYSTONE keystoneStyle
class NAME,TYPE,CONTENT,CUSTOMID,PRIVATE,OID,ONAME ioStyle
Resource inventory: one resource, dynatrace_document.this. There is no separate child resource
and no repeatable nested block — every attribute the live schema exposes is a plain top-level field.
owner and version are computed-only (read-only) attributes; they are deliberately excluded from
both variables.tf (they cannot be caller inputs) and outputs.tf (no sibling module currently
consumes either, so this module does not invent an output for them — see § Architecture Notes).
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| Provider | dynatrace-oss/dynatrace, ~> 1.98 |
provider {} block |
None — the caller configures auth (OAuth client credentials only for this resource — see below) and dt_env_url once, outside this module |
| Schema confirmed against | dynatrace-oss/dynatrace v1.100.0, confirmed against the live provider schema (provider doc id 12717016) |
Schema notes that bite (confirmed against the live schema, not assumed):
ownerandversionare computed-only — never caller inputs. Both areRead-Onlyattributes in the live schema (owner: "The ID of the owner of this document";version: "The version of the document"). Neither appears invariables.tf; supplying either as a caller input would not even be possible against this resource's schema, and this module does not attempt to.custom_idis optional-and-computed, not merely optional. Provider description: "If provided, this will be the id of the document. If not provided, a system-generated id is used." Both paths — caller-pinned and system-generated — are legitimate, provider-supported outcomes;custom_iddefaults tonullin this module so the system-generated path is what an empty call produces.typeis a closed 3-value enum the provider itself only types as a barestring. Legal values aredashboard,launchpad, andnotebook. Terraform's type system cannot express a closed enum natively, sovariables.tfenforces it via avalidation {}block rather than the type declaration alone — an out-of-range value fails atterraform validate/plan, before any API call.privatecarries no provider-documented default — this module overrides the unsafe zero-value with an explicittrue. The live schema does not declare aDefaultkey forprivate, and the field is notcomputed; left unset, Terraform/the provider's SDKv2 layer would resolve an omitted value to the bool zero value,false(readable by everybody). That is the less-safe outcome for a resource whose content may include dashboards, notebooks, or launchpads surfacing operational or business data. Per this module suite's secure-by-default convention,variables.tfsetsdefault = trueinstead — a caller must explicitly passprivate = falseto opt into environment-wide readability. This is an house override, not the provider's own documented default (the provider documents no default at all for this field).contentis an opaque JSON string, not a typed nested object. The provider's own schema declarescontentas a plainstringregardless oftype— the internal shape (dashboard tiles, notebook cells, launchpad layout) is a Document Service payload convention, not something the Terraform schema itself enforces. Malformed JSON, or JSON that does not match the shape implied bytype, surfaces as an API-level error atapplytime, not a Terraform-level type error.- Dynatrace SaaS only. The resource's own doc page states this explicitly; it is not documented or supported against a Dynatrace Managed environment.
- Excluded by default from the provider's export utility. The resource must be explicitly named to the export tool to retrieve existing documents; Dynatrace's own ready-made example/template documents are excluded entirely and cannot be managed by Terraform regardless.
- Per-field force-new/immutability beyond the
Required/Optional/Read-Onlytop-level shape confirmed above was not independently enumerated during Discovery for this module — verify against the live schema for your pinned provider version before assuming any field supports an in-place update.
(Sourced verbatim from this module's SCOPE.md; the two files are kept in agreement.)
Confirmed directly against dynatrace_document's own live provider documentation
(provider dynatrace-oss/dynatrace v1.100.0, provider doc id 12717016) —
not assumed by analogy with sibling Settings 2.0 modules in this catalog.
Stated verbatim on the resource's own doc page:
"-> Dynatrace SaaS only
-> To utilize this resource, please define the environment variables
DT_CLIENT_ID,DT_CLIENT_SECRET,DT_ACCOUNT_IDwith an OAuth client including the following permissions: Create and edit documents (document:documents:write), View documents (document:documents:read), Delete documents (document:documents:delete), and Delete documents from trash (document:trash.documents:delete)."
- OAuth client credentials — the only mechanism this resource's own documentation states. Grant
the OAuth client's IAM policy
document:documents:read,document:documents:write,document:documents:delete, anddocument:trash.documents:delete. The two delete-scoped permissions are needed for a cleanterraform destroy/replace lifecycle, not justapply. - Legacy API token: not accepted. This resource's doc page documents no
dt_api_token/DYNATRACE_API_TOKENalternative anywhere, unlike most Settings 2.0-backed resources elsewhere in this catalog (comparetf-mod-dynatrace-management-zone, which accepts either mechanism). Treat this resource as OAuth-only, consistent with this catalog's convention for Platform/Automation-family resources requiring OAuth specifically, with no API-token fallback. - Platform token: not documented for this resource specifically. If your environment's platform
token is bound to the same
document:*scopes above, it should work per the general platform-token model (scopes granted the same way as OAuth) — this is not a statement confirmed on the resource's own doc page, so verify against your own environment before relying on it for a production rollout. - Not an IAM/Account-Management binding itself (no
account-idm-*scopes apply) — theDT_ACCOUNT_IDrequirement reflects that Document Service authentication is issued at the account level, not that this module manages IAM/Account-Management objects.
(Sourced verbatim from this module's SCOPE.md.)
- Dynatrace SaaS only — this resource is not available on Dynatrace Managed, per the resource's own doc page.
- An OAuth client provisioned with
document:documents:read,document:documents:write,document:documents:delete, anddocument:trash.documents:delete, configured viaDT_CLIENT_ID/DT_CLIENT_SECRET/DT_ACCOUNT_IDat the provider level (never as a module variable). - No ActiveGate, ingest-source, or synthetic-location prerequisites apply to this resource.
- This resource is excluded by default from the provider's export utility; it must be explicitly named to export existing documents. Dynatrace's own ready-made example/template documents are excluded and cannot be managed by Terraform.
| File | Role |
|---|---|
providers.tf |
required_version, required_providers pin — no provider {} block |
variables.tf |
name, type (with validation {} for the 3-value enum), content, custom_id (optional, default null), private (optional bool, default true) |
main.tf |
Thin, total renderer: single dynatrace_document.this keystone mapping every variable directly, try(x, null) on custom_id |
outputs.tf |
id, name |
README.md |
This file |
SCOPE.md |
The lightweight cross-module contract for this standalone module |
examples/ |
Runnable example snippets referenced from § Example Library |
module "document" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Platform Reliability Overview"
type = "notebook"
content = jsonencode({
# Shape depends on `type` — see § Schema notes that bite.
})
}The caller configures the dynatrace provider with OAuth client credentials specifically
(DT_CLIENT_ID / DT_CLIENT_SECRET / DT_ACCOUNT_ID) once, outside this module — see
§ Required OAuth Scopes / API Token Permissions above. This
call alone leaves private at its secure default (true) and custom_id unset (system-generated
id).
A runnable copy of this example lives at examples/quickstart/main.tf.
(Tables below match SCOPE.md exactly.)
Consumes
| Input | Type | Source module |
|---|---|---|
| (none) | This is a foundational, standalone keystone module; it takes no cross-module inputs. |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
The Dynatrace-assigned (or custom_id-pinned) ID of this document. |
tf-mod-dynatrace-direct-shares (its document_id input) |
name |
The document's name, as configured. |
Reference only |
1 · Minimal private notebook document (secure default)
The smallest legal call. private is left at its secure default (true) and custom_id is left
unset (system-generated id).
module "reliability_notebook" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Platform Reliability Notebook"
type = "notebook"
content = jsonencode({
sections = []
})
}🔒
privatedefaults totrue— this document is visible only to its owner until a caller explicitly opts intoprivate = false. This is an house override of the provider's own undocumented zero-value default (see § Schema notes that bite).
2 · Publicly-readable document (explicit opt-out)
module "team_status_notebook" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Team Status Notebook"
type = "notebook"
private = false
content = jsonencode({
sections = []
})
}
⚠️ private = falsemakes this document readable by everybody in the environment. Set this explicitly and deliberately — never as a copy-pasted default — since the module's own default is the narrower, private choice.
3 · Dashboard-type document
Mirrors the shape of the provider's own documented example usage for dynatrace_document, adapted
to this module's variables.
module "host_overview_dashboard" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Host Overview Dashboard"
type = "dashboard"
content = jsonencode({
version = 13
variables = []
tiles = {
"0" = {
type = "markdown"
title = ""
content = "## Host Overview"
}
"1" = {
type = "data"
title = ""
query = "timeseries avg(dt.host.cpu.user)"
visualization = "lineChart"
queryConfig = {
datatype = "metrics"
metricKey = "dt.host.cpu.user"
aggregation = "avg"
by = []
}
}
}
layouts = {
"0" = { x = 0, y = 0, w = 24, h = 4 }
"1" = { x = 0, y = 4, w = 12, h = 8 }
}
})
}ℹ️
type = "dashboard"produces a Grail-native document rendered as a dashboard — distinct fromdynatrace_dashboard(a separate, typed Settings 2.0 resource) anddynatrace_json_dashboard(a separate, raw-JSON classic Config API resource). These three do not interoperate; a document created here cannot be referenced by, or migrated into, either sibling resource's state.
4 · Launchpad-type document
module "sre_team_launchpad" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "SRE Team Launchpad"
type = "launchpad"
content = jsonencode({
tiles = [
{
type = "link"
title = "Incident Runbooks"
url = "https://internal.example.com/runbooks"
},
{
type = "link"
title = "On-call Schedule"
url = "https://internal.example.com/oncall"
}
]
})
}💡 A launchpad document's
contentshape is a tile/link layout, not a dashboard tile grid or a notebook section list — this module does not enforce that shape; an internally inconsistentcontentfor the chosentypesurfaces as an API-level error, not a Terraform-level one.
5 · Custom, caller-pinned document id
module "pinned_reference_dashboard" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Payments Gateway Reference Dashboard"
type = "dashboard"
custom_id = "payments-gateway-reference-dashboard"
content = jsonencode({
version = 13
variables = []
tiles = {}
layouts = {}
})
}💡
custom_idpins a stable, caller-known document id — useful when a downstream reference (e.g. atf-mod-dynatrace-direct-sharesgrant, or an external bookmark/link) must survive a destroy/recreate cycle without changing. Omitcustom_id(the default) whenever a stable id is not genuinely required; a system-generated id is Dynatrace's own out-of-the-box behavior.
6 · Notebook with structured content sections
module "incident_postmortem_notebook" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Incident Postmortem — 2026-07"
type = "notebook"
content = jsonencode({
sections = [
{
type = "markdown"
content = "# Incident Summary\n\nRoot cause and timeline."
},
{
type = "query"
query = "fetch logs | filter matchesValue(status, \"error\")"
}
]
})
}7 · Private financial-reporting dashboard (sensitive content, secure default emphasized)
module "quarterly_financials_dashboard" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Quarterly Financial Systems Health"
type = "dashboard"
# private intentionally omitted -- stays at the secure default (true)
content = jsonencode({
version = 13
variables = []
tiles = {}
layouts = {}
})
}🔒 Omitting
privatehere is deliberate, not an oversight — a dashboard surfacing financial-systems health data should remain owner-only unless a caller has a specific, reviewed reason to widen visibility (see Example 2 for the explicit opt-out pattern).
8 · Public team-wide launchpad (shared homepage)
module "engineering_homepage_launchpad" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Engineering Homepage"
type = "launchpad"
private = false
content = jsonencode({
tiles = [
{ type = "link", title = "Dashboards", url = "https://internal.example.com/dashboards" },
{ type = "link", title = "Documentation", url = "https://internal.example.com/docs" }
]
})
}
⚠️ A team-wide launchpad is one of the more defensible cases forprivate = false— still an explicit, reviewed choice, not the module's default.
9 · Multiple documents at scale via caller-side `for_each`
This module owns no children and has no internal for_each — every attribute is a flat, single
document. A caller fans the same shape out across several named documents by keying a for_each
over module instances, matching this catalog's house convention for standalone modules.
locals {
team_notebooks = {
payments = "Payments Team Runbook"
checkout = "Checkout Team Runbook"
ledger = "Ledger Team Runbook"
}
}
module "team_runbooks" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
for_each = local.team_notebooks
name = each.value
type = "notebook"
content = jsonencode({
sections = [
{ type = "markdown", content = "# ${each.value}" }
]
})
}
⚠️ Key thefor_eachby a stable, human-meaningful map key (payments,checkout,ledger), never a list index — adding or removing one team's runbook later must not perturb the Terraform addresses of the others.
10 · Environment-tiered naming convention (dev / stage / prod)
module "tier_status_dashboards" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
for_each = toset(["dev", "stage", "prod"])
name = "Status Dashboard — ${upper(each.key)}"
type = "dashboard"
custom_id = "status-dashboard-${each.key}"
private = each.key == "prod" ? true : false
content = jsonencode({
version = 13
variables = []
tiles = {}
layouts = {}
})
}💡 Only
prodkeeps the secure default (private = true) in this pattern;dev/stageopt intoprivate = falsefor broader internal visibility. Each tier'scustom_idis pinned separately so downstream references remain stable per tier.
11 · Side-by-side private vs. public comparison
module "internal_only_notebook" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Internal-Only Capacity Notes"
type = "notebook"
# private omitted -- secure default (true)
content = jsonencode({ sections = [] })
}
module "shared_capacity_notebook" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Shared Capacity Notes"
type = "notebook"
private = false
content = jsonencode({ sections = [] })
}ℹ️ Two module instances, identical shape, differing only in
private— a direct illustration of the secure-by-default posture in § Design Principles.
12 · Notebook with an embedded DQL query section
module "error_rate_investigation_notebook" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Error Rate Investigation"
type = "notebook"
content = jsonencode({
sections = [
{
type = "query"
query = "timeseries avg(dt.service.request.failure_count), by: { dt.entity.service }"
}
]
})
}ℹ️
contentaccepts any DQL/query payload the Document Service supports for a notebook query section — this module passes the string through unmodified; a malformed query surfaces as an API-level error atapplytime, not aplan-time one (see § Schema notes that bite).
13 · Custom-id dashboard paired with a system-generated-id notebook, same call
module "pinned_dashboard" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Ledger Reconciliation Dashboard"
type = "dashboard"
custom_id = "ledger-reconciliation-dashboard"
content = jsonencode({
version = 13
variables = []
tiles = {}
layouts = {}
})
}
module "generated_id_notebook" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Ledger Reconciliation Working Notes"
type = "notebook"
# custom_id omitted -- system-generated id (the default path)
content = jsonencode({ sections = [] })
}💡 Both
custom_idpaths are legitimate, provider-supported outcomes (§ Schema notes that bite) — pin an id only where a stable, caller-known reference is genuinely needed; otherwise leave it at its defaultnull.
14 · 🏗️ End-to-end composition — document → direct share
The mandatory full composition. Wires this module's id output into
tf-mod-dynatrace-direct-shares's document_id input, granting a finance group read-only access to
a document that is otherwise private by default.
module "quarterly_financials_dashboard" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-document.git?ref=v1.0.0"
name = "Quarterly Financial Systems Health"
type = "dashboard"
# private intentionally omitted -- stays at the secure default (true)
content = jsonencode({
version = 13
variables = []
tiles = {}
layouts = {}
})
}
module "quarterly_financials_share" {
source = "git::https://github.com/microsoftexpert/tf-mod-dynatrace-direct-shares.git?ref=v1.0.0"
# Implicit reference -- never depends_on. This module's own variables.tf documents that
# document_id expects this module's `id` output specifically.
document_id = module.quarterly_financials_dashboard.id
access = "read"
recipients = {
finance_group = { id = "finance-reporting-group-id", type = "group" }
}
}
output "shared_document_id" {
value = module.quarterly_financials_dashboard.id
}🔒 The document stays
privateby default; access is granted narrowly and explicitly through the direct-share module rather than by flippingprivate = false— the direct share limits visibility to exactly the recipients listed (here, one group), rather than everybody in the environment.
⚠️ Both modules in this composition require OAuth client credentials specifically —tf-mod-dynatrace-direct-shares' ownSCOPE.mdflags this as unverified-but-suspected; confirm its exact scopes against that module's own live provider schema before relying on this composition in a production rollout.
Grouped summary:
- Identity/content:
name(required string),type(required string, closed 3-value enum:dashboard/launchpad/notebook),content(required string, JSON-encoded). - Identity override:
custom_id(optional string, defaultnull) — pins the document's id; omit for a system-generated id. - Visibility:
private(optional bool, defaulttrue) — secure-default override; setfalseto make the document readable by everybody in the environment.
Full variables.tf schema
name = string # required, no default
type = string # required, no default -- closed enum, enforced via validation {}
# Legal values: "dashboard", "launchpad", "notebook"
content = string # required, no default -- JSON-encoded string; shape depends on `type`,
# not enforced by this module's type system (provider treats it as opaque)
custom_id = optional(string) # default: null -- system-generated id if omitted;
# caller-pinned id if supplied. Both are legitimate,
# provider-supported outcomes.
private = optional(bool) # default: true (house override of the provider's
# undocumented, unsafe `false` zero-value default)| Output | Description | Sensitive |
|---|---|---|
id |
The Dynatrace-assigned (or custom_id-pinned) ID of this document (dynatrace_document.this.id). Consumed by tf-mod-dynatrace-direct-shares as document_id. |
No |
name |
The document's name, as configured (dynatrace_document.this.name). |
No |
There is no owner or version output — both are computed-only attributes on the live schema, and
no sibling module in this catalog currently consumes either, so this module does not invent an
output for them (see § Architecture Notes).
- Single keystone, no owned child resource, no
for_eachanywhere in this module.main.tfis a thin, total renderer around exactly onedynatrace_document.thisresource, mapping every variable directly withtry(var.custom_id, null)on the one optional-and-computed field. There is no nestedblock_typesin the live schema, so there is nothing else to render. owner/versionare intentionally excluded from both inputs and outputs. Both areRead-Onlyin the live schema. They cannot be caller inputs, and — unlikeid/name— no sibling module in this catalog currently needs either as a typed cross-module reference, sooutputs.tfdoes not expose them speculatively. If a future sibling module needsowner(e.g. to cross-check document ownership before a direct share), add it then, with a clear "provider-computed reference-only" note in its description.private's secure-default override is a variable-level decision, not a provider one. The live schema declares no default forprivateat all; this module'svariables.tfsetsdefault = trueexplicitly, which is an house policy layered on top of the provider's schema, not something the live provider schema itself asserts. Anyone reviewing a diff against a future provider version should re-confirm this field still carries no provider-side default before assuming this override remains necessary.contentopacity is a real constraint, not a modeling shortcut. The provider's own schema typescontentas a plain string for everytypevalue — there is no schema-level way to catch a notebook-shaped payload accidentally paired withtype = "dashboard"atplantime. This is identical in spirit totf-mod-dynatrace-json-dashboard's raw-JSON handling, but distinct in authentication and API surface (see § Where this fits).- No secret-bearing fields. Nothing in this resource's schema is
sensitive— there is no credential-handling concern for this module itself, though the OAuth client credentials used to authenticate the provider (never a module variable) are a secrets-manager concern for the caller. - Platform-scoped authentication, environment-facing resource.
dynatrace_documentrequiresDT_CLIENT_ID/DT_CLIENT_SECRET/DT_ACCOUNT_ID— an account-level OAuth client — even though the document itself is created and visible within a specific Dynatrace SaaS environment. This is a narrower version of the same account-vs-environment distinction this catalog's IAM/Account Management guidance describes, worth keeping in mind when a single provider configuration must serve both this module and an environment-scoped Settings 2.0 module elsewhere in the same root module.
| Concern | Safe default | Opt-out (caller must set explicitly) |
|---|---|---|
| Document visibility | private defaults to true — visible only to the document's owner |
Explicit private = false to make the document readable by everybody in the environment |
| Document id assignment | custom_id defaults to null — Dynatrace generates the id server-side |
Caller supplies an explicit custom_id string to pin a stable, caller-known id |
| Content shape validation | Not applicable — the provider itself types content as an opaque string for every type; this module does not invent structure the API does not enforce |
N/A — a mismatched content/type pairing is an apply-time API error regardless |
| Type selection | No default — type is required and closed to dashboard / launchpad / notebook via validation {} |
Caller must choose one of the three legal values explicitly |
| Credential handling | Not applicable — dynatrace_document carries no secret-bearing fields; the OAuth client credentials that authenticate the provider are a caller/secrets-manager concern, never a module variable |
N/A |
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module source to an explicit tag — ?ref=v1.0.0 — never a branch. This library is
plan-only: a human runs terraform apply from CI after reviewing the plan; no step in this
authoring/runbook process applies changes to a live Dynatrace environment.
terraform validate and terraform fmt -check prove the module is internally consistent — correct
HCL syntax, a satisfiable type schema, and the closed-enum validation {} block catching an
out-of-range type value at plan time, before any API call.
What validate/fmt cannot prove, because it requires a real environment and terraform plan
against a configured provider:
- Whether a given
contentJSON payload actually matches the internal shape the Document Service expects for the chosentype(dashboard tiles vs. notebook sections vs. launchpad tiles). - Whether the configured OAuth client credentials are actually sufficient — an under-scoped or
wrong-mechanism credential (e.g. a legacy API token, which this resource does not accept at all)
fails at
apply, notplan. - Whether a
custom_idvalue collides with an existing document id in the target environment. - Whether the target environment is Dynatrace SaaS (required) rather than Managed (unsupported for this resource).
$ terraform output
id = "d4f9a1b2-3c5e-4a7d-9b1f-2e6c8a0d5f13"
name = "Platform Reliability Overview"
(Illustrative — actual Dynatrace-assigned document IDs vary by environment.)
| Symptom | Cause | Fix |
|---|---|---|
apply fails with a 401/403 even though a legacy API token is configured and looks valid |
This resource requires OAuth client credentials specifically — a legacy API token is not accepted at all, unlike most Settings 2.0 resources elsewhere in this catalog | Configure the provider with DT_CLIENT_ID / DT_CLIENT_SECRET / DT_ACCOUNT_ID for an OAuth client granted document:documents:read/write/delete and document:trash.documents:delete — see § Required OAuth Scopes / API Token Permissions |
apply fails against a Dynatrace Managed environment |
This resource is Dynatrace SaaS only, per its own documentation | Target a Dynatrace SaaS environment, or use a different resource/module for a Managed environment's dashboard needs |
terraform validate rejects var.type |
An out-of-range value was supplied for the closed 3-value enum | Use exactly one of dashboard, launchpad, notebook |
| A document applies successfully but its content doesn't render as expected in the Dynatrace UI | content is an opaque string to this module and the provider — an internally inconsistent JSON payload for the chosen type is not caught at plan time |
Compare the content payload against Dynatrace's own Document Service API examples for the chosen type, and inspect the document in the UI directly |
| A newly-applied document is visible to more users than expected | private was explicitly set to false (or a prior version of the module call did so) |
Confirm the intended visibility and set private = true (or omit the field entirely to take the secure default) |
A custom_id value conflicts with an existing document |
Two documents (or a document and a stale un-imported resource) share the same caller-pinned id | Choose a distinct custom_id, or omit it entirely to let Dynatrace generate one server-side |
A tf-mod-dynatrace-direct-shares grant referencing this module's id fails to plan/apply |
The direct-shares module itself also requires OAuth client credentials (unverified but suspected per its own SCOPE.md) — a mismatched auth mechanism on that module's provider configuration would fail independently of this module | Confirm the auth mechanism for tf-mod-dynatrace-direct-shares against that module's own live provider schema before relying on the composition in Example 14 |
dynatrace_documentprovider resource- This module's
SCOPE.md - Sibling modules:
tf-mod-dynatrace-direct-shares,tf-mod-dynatrace-dashboard,tf-mod-dynatrace-json-dashboard
💙 "Infrastructure as Code should be standardized, consistent, and secure."