Skip to content

microsoftexpert/tf-mod-powerplatform-environment

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🔌 Power Platform Environment Terraform Module

Provisions a Power Platform environment — the keystone resource of this module library — including its optionally embedded Dataverse database. Secure-by-default Sandbox environment type, a required (never silently open) Dataverse security group, and a single primary resource named this. Built for microsoft/power-platform ~> 4.1.

Terraform provider module type resources


🧩 Overview

  • 🏗️ 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_id is a required field of this module's dataverse object whenever dataverse is supplied at all.
  • 🔗 Is the keystone every other module in this library either creates a child of, or consumes environment_id from.

💡 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, location treated as force-new — sets the security posture for every module built on top of it.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

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!


🗺️ Where this fits

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;
Loading

🧬 What this builds

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;
Loading

Resource inventory: one resource, powerplatform_environment.this. No for_each children — Dataverse is a nested attribute of the same resource, not an independent child.


✅ Provider / Versions

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:

  • location is force-new — changing it destroys and recreates the environment, including any embedded Dataverse database.
  • azure_region is also force-new, for the same reason.
  • environment_type transitions are one-directional in practice: moving a Production environment back to Sandbox is not a supported in-place update.
  • Dataverse currency_code and language_code are 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_id is set from the environment side of the relationship — powerplatform_environment_group does 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.tf for this case.

🔑 Required Entra App Registration Permissions & Power Platform Admin Role

  • 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 Prerequisites

  • 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 location is available to the calling Entra tenant (query powerplatform_locations).

📁 Module Structure

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

⚙️ Quick Start

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 powerplatform provider block (Azure CLI, Service Principal + secret/certificate, OIDC, or Managed Identity) — this module never references authentication.


🔌 Cross-Module Contract

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

📚 Example Library

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_type defaults 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_region when co-location with specific Azure/Fabric resources is required — both location and azure_region are 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 Environment apply as 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_id and environment_group_id accept 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 joins environment_group_id at creation, the settings applied by managed_environment above will not take effect until confirmed against actual group membership — see "Architecture Notes" below.


📥 Inputs

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 = {}
}

🧾 Outputs

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

🧠 Architecture Notes

  • location and azure_region are both force-new. A change to either destroys and recreates the environment, including any embedded Dataverse database. Consider a prevent_destroy lifecycle 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.
  • dataverse is an embedded nested attribute, not a child resource. There is no powerplatform_dataverse resource type — a Dataverse database only exists as part of an environment, and this module's main.tf assigns dataverse = var.dataverse directly as a single object-typed attribute (this provider's Plugin-Framework schema uses attribute-nested objects, not classic HCL blocks, for dataverse and timeouts).
  • Group membership is set from the environment, not the group. environment_group_id is an attribute of powerplatform_environment itself — tf-mod-powerplatform-environment-group's own resource does not enumerate members. Any composition wiring an environment into a group should set environment_group_id here, 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 subsequent tf-mod-powerplatform-managed-environment apply 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_type is effectively one-directional. Once Production, moving back to Sandbox is not a supported in-place update.

🧱 Design Principles

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

🚀 Runbook

cd tf-mod-powerplatform-environment
terraform init -backend=false
terraform validate
terraform fmt -check

Pin 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.


🧪 Testing

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.


💬 Example Output

$ 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 = []

🔍 Troubleshooting

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

🔗 Related Docs

  • 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."

Packages

 
 
 

Contributors

Languages