Provisions a Power Platform environment — the keystone resource of this module library — including its optionally embedded Dataverse database. Secure-by-default
Sandboxenvironment type, a required (never silently open) Dataverse security group, and a single primary resource namedthis. Built for microsoft/power-platform ~> 4.1.
- 🏗️ Provisions one
powerplatform_environment— a Power Platform space with its own security boundary, apps, flows, chatbots, and (optionally) Dataverse database. - 🔒 Defaults to
environment_type = "Sandbox"— the caller must type"Production"explicitly to provision a production environment. - 🗄️ Embeds Dataverse configuration as a nested attribute on the same resource (not a separate child resource) — currency and language are set once and are immutable thereafter.
- 🚫 Never silently opens Dataverse membership:
dataverse.security_group_idis a required field of this module'sdataverseobject wheneverdataverseis supplied at all. - 🔗 Is the keystone every other module in this library either creates a child of, or consumes
environment_idfrom.
💡 Why it matters: almost every Power Platform governance, connectivity, identity, and application-lifecycle capability is scoped to an environment, not the Entra tenant. Getting this module's defaults right — Sandbox by default, an explicit security group choice,
locationtreated as force-new — sets the security posture for every module built on top of it.
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!
graph TD
TENANT["Entra / Power Platform Tenant"]:::neutral
BILL["tf-mod-powerplatform-billing-policy"]:::neutral
ENV["tf-mod-powerplatform-environment (this module)"]:::this
ME["tf-mod-powerplatform-managed-environment"]:::neutral
EG["tf-mod-powerplatform-environment-group"]:::neutral
ES["tf-mod-powerplatform-environment-settings"]:::neutral
DLP["tf-mod-powerplatform-data-loss-prevention-policy"]:::neutral
CONN["tf-mod-powerplatform-connection"]:::neutral
USR["tf-mod-powerplatform-user"]:::neutral
EAA["tf-mod-powerplatform-environment-application-admin"]:::neutral
SOL["tf-mod-powerplatform-solution"]:::neutral
EP["tf-mod-powerplatform-enterprise-policy"]:::neutral
EW["tf-mod-powerplatform-environment-wave"]:::neutral
CSAI["tf-mod-powerplatform-copilot-studio-application-insights"]:::neutral
TENANT -->|"scopes"| ENV
BILL -->|"billing_policy_id"| ENV
ENV -->|"environment_id"| ME
ENV -->|"environment_group_id (set FROM the environment)"| EG
ENV -->|"environment_id"| ES
ENV -->|"environment_id scope"| DLP
ENV -->|"environment_id"| CONN
ENV -->|"environment_id"| USR
ENV -->|"environment_id"| EAA
ENV -->|"environment_id + dataverse present"| SOL
ENV -->|"environment_id"| EP
ENV -->|"environment_id"| EW
ENV -->|"environment_id"| CSAI
classDef this fill:#742774,color:#fff,stroke:#742774;
classDef neutral fill:#E8E8E8,color:#222,stroke:#999;
graph TD
D1["var.display_name"]:::input
D2["var.location"]:::input
D3["var.environment_type (default Sandbox)"]:::input
D4["var.dataverse (optional, security_group_id required within)"]:::input
D5["var.billing_policy_id / environment_group_id / owner_id / cadence /..."]:::input
R["powerplatform_environment.this"]:::this
O1["output: id"]:::output
O2["output: display_name"]:::output
O3["output: environment_type"]:::output
O4["output: dataverse (organization_id, url, version,...)"]:::output
O5["output: enterprise_policies"]:::output
D1 -->|"display_name"| R
D2 -->|"location (force-new)"| R
D3 -->|"environment_type"| R
D4 -->|"dataverse (nested attribute)"| R
D5 -->|"optional attributes"| R
R -->|"id"| O1
R -->|"display_name"| O2
R -->|"environment_type"| O3
R -->|"dataverse.*"| O4
R -->|"enterprise_policies"| O5
classDef input fill:#E8E8E8,color:#222,stroke:#999;
classDef this fill:#742774,color:#fff,stroke:#742774;
classDef output fill:#E8E8E8,color:#222,stroke:#999;
Resource inventory: one resource, powerplatform_environment.this. No for_each children —
Dataverse is a nested attribute of the same resource, not an independent child.
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
microsoft/power-platform |
~> 4.1 |
| Provider block | None — the caller's root module configures authentication |
Schema notes that bite:
locationis force-new — changing it destroys and recreates the environment, including any embedded Dataverse database.azure_regionis also force-new, for the same reason.environment_typetransitions are one-directional in practice: moving aProductionenvironment back toSandboxis not a supported in-place update.- Dataverse
currency_codeandlanguage_codeare immutable once the environment is provisioned — there is no update path. - Creating a
Developer-type environment is currently a preview capability of this provider and is not supported under Service Principal authentication — only Azure CLI / interactive user auth. environment_group_idis set from the environment side of the relationship —powerplatform_environment_groupdoes not itself enumerate members.- Managed Environment settings silently do not apply while this environment is a member of a group — the provider emits a warning, not an error.
- A 404 on read/delete is treated as "already deleted" by the current provider version — no
defensive workaround is needed in
main.tffor this case.
- Power Platform Administrator, or Dynamics 365 Administrator, role on the authenticating principal.
- Confirm current Power Platform API app-registration permission scope names against the provider's Authentication guide at authoring/deployment time — these scope names have been renamed across provider versions.
- Power Platform Administrator or Dynamics 365 Administrator role.
- Service principals cannot create
Developer-type environments. - If provisioning Dataverse, the tenant must have available database capacity, and the caller must know the target security group GUID in advance.
- Confirm the Power Platform geo passed via
locationis available to the calling Entra tenant (querypowerplatform_locations).
tf-mod-powerplatform-environment/
├── providers.tf # required_providers + required_version — no provider {} block
├── variables.tf # display_name, location, environment_type, dataverse, timeouts,...
├── main.tf # single resource "powerplatform_environment" "this"
├── outputs.tf # id, display_name, environment_type, dataverse, enterprise_policies
├── README.md # this file
├── SCOPE.md # lightweight standalone scope
└── examples/ # runnable example configurations
module "dev_sandbox" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Loan Origination — Dev"
location = "unitedstates"
dataverse = {
currency_code = "USD"
language_code = 1033
security_group_id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}ℹ️ The caller's root module configures the
powerplatformprovider block (Azure CLI, Service Principal + secret/certificate, OIDC, or Managed Identity) — this module never references authentication.
Consumes
| Input | Type | Source module |
|---|---|---|
billing_policy_id |
string (optional) |
tf-mod-powerplatform-billing-policy (.id) |
environment_group_id |
string (optional) |
tf-mod-powerplatform-environment-group (.id) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Environment GUID — primary cross-module reference | Nearly every sibling module |
display_name |
Environment display name | Reporting |
environment_type |
Sandbox / Production / Trial / Developer | Reporting, caller gating logic |
dataverse |
Read-only Dataverse details, or null |
tf-mod-powerplatform-solution, -user, -environment-application-admin |
enterprise_policies |
Read-only linked enterprise policies | Informational / audit |
1 · Minimal Sandbox environment, no Dataverse
module "scratch" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Scratch — No Database"
location = "unitedstates"
}💡
environment_typedefaults to"Sandbox"— no need to set it for the common case.
2 · Sandbox environment with Dataverse
module "dev_with_dataverse" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Claims Processing — Dev"
location = "unitedstates"
dataverse = {
currency_code = "USD"
language_code = 1033
security_group_id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}3 · Explicit acknowledgement of open Dataverse membership
module "open_membership_dev_only" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Sandbox — Open Membership (dev-only)"
location = "unitedstates"
dataverse = {
currency_code = "USD"
language_code = 1033
# Explicit sentinel acknowledging open/ungoverned Dataverse membership. Never do this for a
# Production environment.
security_group_id = "00000000-0000-0000-0000-000000000000"
}
}
⚠️ This module will never default to open membership on its own — the caller must type this sentinel value explicitly.
4 · Production environment (explicit opt-in)
module "prod" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Claims Processing — Production"
location = "unitedstates"
environment_type = "Production"
dataverse = {
currency_code = "USD"
language_code = 1033
security_group_id = "8e6499a0-4b1b-4c2e-9f2b-6a9d3e2b7c11"
}
}🔒
environment_type = "Production"must be typed explicitly — this module never defaults to it.
5 · Trial environment for a time-boxed evaluation
module "trial" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "CoE Starter Kit — Trial"
location = "unitedstates"
environment_type = "Trial"
}6 · Developer environment (interactive/Azure-CLI auth only)
module "developer_sandbox" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Personal Dev — jsmith"
location = "unitedstates"
environment_type = "Developer"
owner_id = "b6f1c2a4-9d3e-4f7a-8b2c-1e5d4a6f9c30"
}
⚠️ Developer-type environment creation is a preview provider capability and is not supported under Service Principal authentication — plan/apply this example only under Azure CLI or interactive user auth.
7 · Pinning an Azure region within the Power Platform geo
module "regional_pin" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Fabric Co-located — Dev"
location = "unitedstates"
azure_region = "eastus2"
dataverse = {
currency_code = "USD"
language_code = 1033
security_group_id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}ℹ️ Only set
azure_regionwhen co-location with specific Azure/Fabric resources is required — bothlocationandazure_regionare force-new.
8 · Non-English Dataverse language
module "eu_environment" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "EU Claims — Dev"
location = "europe"
dataverse = {
currency_code = "EUR"
language_code = 1031 # German
security_group_id = "9c1d4e2b-6a5f-4b3c-8d7e-2f1a9c6b4d20"
}
}9 · Frequent refresh cadence for an actively-developed environment
module "fast_cadence" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Active Dev — Fast Refresh"
location = "unitedstates"
cadence = "Frequent"
}10 · Linked to a pay-as-you-go billing policy
module "papg_linked" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Analytics Workspace — PAYG"
location = "unitedstates"
billing_policy_id = module.billing_policy.id
dataverse = {
currency_code = "USD"
language_code = 1033
security_group_id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}11 · Joining an environment group at creation time
module "grouped" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Regulated Workload — Dev"
location = "unitedstates"
environment_group_id = module.environment_group.id
}
⚠️ Managed Environment settings applied elsewhere will silently not take effect while this environment is a group member — verify group membership before treating a Managed Environmentapplyas confirmation.
12 · Custom per-operation timeouts
module "slow_region" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Slow-Provisioning Region — Dev"
location = "asia"
dataverse = {
currency_code = "USD"
language_code = 1033
security_group_id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
timeouts = {
create = "45m"
delete = "30m"
}
}13 · Removing an environment from its billing policy / group (sentinel values)
module "detach" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Analytics Workspace — PAYG"
location = "unitedstates"
billing_policy_id = "00000000-0000-0000-0000-000000000000"
environment_group_id = "00000000-0000-0000-0000-000000000000"
}ℹ️ Both
billing_policy_idandenvironment_group_idaccept the explicit all-zeros sentinel to mean "remove from the current billing policy / group."
14 · Restricting data movement and disabling Bing search (locked-down posture)
module "locked_down" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Regulated Data — Dev"
location = "unitedstates"
allow_bing_search = false
allow_moving_data_across_regions = false
dataverse = {
currency_code = "USD"
language_code = 1033
security_group_id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}🔒 Recommended posture for any environment handling PII/regulated data.
15 · 🏗️ End-to-end composition — environment → managed environment → DLP policy → environment group
module "environment" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment.git?ref=v1.0.0"
display_name = "Claims Processing — Production"
location = "unitedstates"
environment_type = "Production"
environment_group_id = module.environment_group.id
dataverse = {
currency_code = "USD"
language_code = 1033
security_group_id = "8e6499a0-4b1b-4c2e-9f2b-6a9d3e2b7c11"
}
}
module "environment_group" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-environment-group.git?ref=v1.0.0"
display_name = "Regulated Workloads"
}
module "managed_environment" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-managed-environment.git?ref=v1.0.0"
environment_id = module.environment.id
}
module "dlp_policy" {
source = "git::https://github.com/microsoftexpert/tf-mod-powerplatform-data-loss-prevention-policy.git?ref=v1.0.0"
display_name = "Claims Processing DLP"
environments = [module.environment.id]
}
⚠️ Because this environment joinsenvironment_group_idat creation, the settings applied bymanaged_environmentabove will not take effect until confirmed against actual group membership — see "Architecture Notes" below.
| Group | Variables |
|---|---|
| Primary identity | display_name, location |
| Core config | environment_type (default "Sandbox"), azure_region, description, cadence, release_cycle |
| Cross-module references | billing_policy_id, environment_group_id |
| Identity (Developer type only) | owner_id |
| Posture toggles | allow_bing_search, allow_moving_data_across_regions |
| Dataverse | dataverse (object; security_group_id required within) |
| Universal tail | timeouts |
Full variables.tf schema
variable "display_name" {
type = string
}
variable "location" {
type = string # Power Platform geo — force-new
}
variable "environment_type" {
type = string
default = "Sandbox" # Sandbox | Production | Trial | Developer
}
variable "azure_region" {
type = string
default = null # force-new
}
variable "description" {
type = string
default = null
}
variable "cadence" {
type = string
default = null # Frequent | Moderate
}
variable "release_cycle" {
type = string
default = null
}
variable "billing_policy_id" {
type = string
default = null
}
variable "environment_group_id" {
type = string
default = null
}
variable "owner_id" {
type = string
default = null # Developer-type environments only
}
variable "allow_bing_search" {
type = bool
default = null
}
variable "allow_moving_data_across_regions" {
type = bool
default = null
}
variable "dataverse" {
type = object({
currency_code = string
language_code = number
security_group_id = string # required here — secure by default
administration_mode_enabled = optional(bool)
background_operation_enabled = optional(bool)
domain = optional(string)
template_metadata = optional(string)
templates = optional(list(string))
})
default = null
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = {}
}| Output | Description | Sensitive/Conditional |
|---|---|---|
id |
Environment GUID (primary output) | — |
display_name |
Environment display name | — |
environment_type |
Sandbox / Production / Trial / Developer | — |
dataverse |
Read-only Dataverse details | Conditional — null if no Dataverse database was provisioned |
enterprise_policies |
Read-only linked enterprise policies | — |
locationandazure_regionare both force-new. A change to either destroys and recreates the environment, including any embedded Dataverse database. Consider aprevent_destroylifecycle guard in the caller's root module for production environments — this module intentionally does not hardcode one, since a module should not dictate the caller's lifecycle policy.dataverseis an embedded nested attribute, not a child resource. There is nopowerplatform_dataverseresource type — a Dataverse database only exists as part of an environment, and this module'smain.tfassignsdataverse = var.dataversedirectly as a single object-typed attribute (this provider's Plugin-Framework schema uses attribute-nested objects, not classic HCL blocks, fordataverseandtimeouts).- Group membership is set from the environment, not the group.
environment_group_idis an attribute ofpowerplatform_environmentitself —tf-mod-powerplatform-environment-group's own resource does not enumerate members. Any composition wiring an environment into a group should setenvironment_group_idhere, not expect a membership resource on the group side. - Managed Environment settings silently do not apply inside a group. If this environment sets
environment_group_id, a subsequenttf-mod-powerplatform-managed-environmentapply may report success while its settings do not actually take effect — this is a documented provider warning, not a Terraform error, so it is easy to miss in CI output. environment_typeis effectively one-directional. OnceProduction, moving back toSandboxis not a supported in-place update.
| Default | Opt-out |
|---|---|
environment_type = "Sandbox" |
Caller sets environment_type = "Production" explicitly |
dataverse.security_group_id has no permissive default — required whenever dataverse is set |
Caller passes the explicit 00000000-0000-0000-0000-000000000000 sentinel to acknowledge open membership |
No tags variable (this provider has no tagging concept) |
N/A — not applicable to this provider |
No provider {} block, no credential-shaped variable |
Caller's root module configures auth |
cd tf-mod-powerplatform-environment
terraform init -backend=false
terraform validate
terraform fmt -checkPin consumption at ?ref=v1.0.0, never a branch. This is a plan-only authoring process — a human
runs terraform apply from CI after review, against a sub-production environment first.
terraform validate and fmt -check confirm the configuration is internally consistent and
correctly typed — they do not confirm the Power Platform API will accept a given location,
security_group_id, or billing_policy_id for your specific tenant. Only a live terraform plan
against a real tenant exercises those constraints; this module's authoring process is plan-only and
does not perform that exercise.
$ terraform output
id = "5c1e9a2b-6f3d-4e7a-9b1c-8a2d5f6e3c40"
display_name = "Claims Processing — Dev"
environment_type = "Sandbox"
dataverse = {
"organization_id" = "org12345"
"unique_name" = "org12345claimsdev"
"url" = "https://org12345claimsdev.crm.dynamics.com"
"version" = "9.2.23092.00206"
"linked_app_id" = null
"linked_app_type" = null
"linked_app_url" = null
}
enterprise_policies = []
| Symptom | Cause | Fix |
|---|---|---|
terraform plan wants to destroy/recreate the environment after a config change |
location or azure_region was changed — both are force-new |
Confirm this is intentional; use prevent_destroy on production environments to catch this before it applies |
Dataverse currency_code change is rejected at plan/apply time |
currency_code is immutable once provisioned |
Provision a new environment instead of changing currency on an existing one |
| Managed Environment settings applied elsewhere don't seem to take effect | This environment is a member of an environment group | Check environment_group_id; the provider warns (not errors) when settings don't apply inside a group |
| Developer-type environment creation fails under CI/CD | Service principals cannot create Developer-type environments | Create Developer environments only under Azure CLI / interactive user auth |
security_group_id validation error at plan time |
dataverse was supplied without security_group_id |
This module requires it explicitly — pass a real GUID or the all-zeros sentinel |
- microsoft/power-platform provider docs
tf-mod-powerplatform-managed-environment,tf-mod-powerplatform-environment-group,tf-mod-powerplatform-data-loss-prevention-policy(common downstream siblings)- This module's
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."