PowerShell Setup: Control 2.2 — Environment Groups and Tier Classification
Read the FSI PowerShell baseline first
Before running any command in this playbook, read the PowerShell Authoring Baseline for FSI Implementations. It is the canonical source for module version pinning, sovereign-cloud (GCC / GCC High / DoD) endpoints, mutation safety (-WhatIf / SupportsShouldProcess), Dataverse compatibility, and SHA-256 evidence emission.
Last Updated: April 2026
Modules required: Microsoft.PowerApps.Administration.PowerShell (≥ 2.0.190); the Power Platform CLI (pac) for any rule-publishing automation.
Scope and limitations
| Operation | Supported via PowerShell? | Notes |
|---|---|---|
| List environment groups | ✅ Get-AdminPowerAppEnvironmentGroup |
GA |
| List environments by group | ✅ Get-AdminPowerAppEnvironment filter |
GA |
| Add environment to group | ✅ Set-AdminPowerAppEnvironment -EnvironmentGroupId |
GA |
| Remove environment from group | ✅ same cmdlet with -EnvironmentGroupId $null |
GA |
| Create environment group | ⚠️ Portal-first | Programmatic creation is currently inconsistent across module versions; standard practice is portal creation, then PowerShell for membership and audit. |
| Read / set group rules | ❌ | Rule configuration is not exposed by the admin module. Use the portal walkthrough; verify enforcement via per-environment setting reads. |
Prerequisites
# Pin the module version per the FSI baseline
$module = 'Microsoft.PowerApps.Administration.PowerShell'
$min = '2.0.190'
if (-not (Get-Module -ListAvailable -Name $module |
Where-Object { $_.Version -ge [version]$min })) {
Install-Module -Name $module -MinimumVersion $min -Scope CurrentUser -Force -AllowClobber
}
Import-Module $module
# Interactive auth (operator workstation)
Add-PowerAppsAccount
# Service-principal auth (automation runner) — store secret in a vault, never inline
# $appId = $env:FSI_PPSP_APPID
# $secret = $env:FSI_PPSP_SECRET
# $tenantId = $env:FSI_PPSP_TENANT
# Add-PowerAppsAccount -ApplicationId $appId -ClientSecret $secret -TenantID $tenantId
Sovereign clouds (GCC / GCC High / DoD): add
-Endpoint usgov,-Endpoint usgovhigh, or-Endpoint dodtoAdd-PowerAppsAccount. See the PowerShell baseline for the full table.
Read scripts (read-only — safe to run any time)
List all environment groups
Get-AdminPowerAppEnvironmentGroup |
Select-Object EnvironmentGroupId, DisplayName, Description, CreatedTime |
Sort-Object DisplayName |
Format-Table -AutoSize
List environments in each group (with FSI naming check)
$groups = Get-AdminPowerAppEnvironmentGroup
$envs = Get-AdminPowerAppEnvironment
foreach ($g in $groups) {
$members = $envs | Where-Object { $_.EnvironmentGroupId -eq $g.EnvironmentGroupId }
$expectedZone = if ($g.DisplayName -match 'FSI-Z(\d)') { $Matches[1] } else { '?' }
Write-Host "`nGroup: $($g.DisplayName) (Zone $expectedZone)" -ForegroundColor Cyan
Write-Host " Group ID : $($g.EnvironmentGroupId)"
Write-Host " Members : $($members.Count)"
$members |
Select-Object DisplayName,
EnvironmentName,
EnvironmentType,
Location,
@{ Name = 'IsManaged'; Expression = {
$_.Properties.governanceConfiguration.protectionLevel -ne 'Standard'
}} |
Format-Table -AutoSize
}
Mutation scripts (require -WhatIf first per FSI baseline)
Add a Managed Environment to a group
function Add-FsiEnvironmentToGroup {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory)] [string] $EnvironmentName,
[Parameter(Mandatory)] [string] $EnvironmentGroupId
)
$env = Get-AdminPowerAppEnvironment -EnvironmentName $EnvironmentName
if (-not $env) { throw "Environment '$EnvironmentName' not found." }
$isManaged = $env.Properties.governanceConfiguration.protectionLevel -ne 'Standard'
if (-not $isManaged) {
throw "Environment '$($env.DisplayName)' is not a Managed Environment. Enable Managed Environment first (Control 2.1)."
}
if ($PSCmdlet.ShouldProcess(
"Environment '$($env.DisplayName)'",
"Assign to environment group '$EnvironmentGroupId'")) {
Set-AdminPowerAppEnvironment `
-EnvironmentName $EnvironmentName `
-EnvironmentGroupId $EnvironmentGroupId | Out-Null
Write-Host "[OK] Assigned $($env.DisplayName) -> $EnvironmentGroupId" -ForegroundColor Green
}
}
# Dry-run first
Add-FsiEnvironmentToGroup -EnvironmentName '<env-id>' -EnvironmentGroupId '<group-id>' -WhatIf
# Then commit
# Add-FsiEnvironmentToGroup -EnvironmentName '<env-id>' -EnvironmentGroupId '<group-id>'
Audit-evidence export
Generates the CSV bundle expected by Verification & Testing and emits a SHA-256 manifest per the FSI baseline.
<#
.SYNOPSIS
Exports Control 2.2 audit evidence: environment groups, members, and a SHA-256 manifest.
.NOTES
Last Updated : April 2026
Related Control: Control 2.2 - Environment Groups and Tier Classification
#>
[CmdletBinding()]
param(
[string] $OutputRoot = 'C:\FSI-Evidence\Control-2.2'
)
$ErrorActionPreference = 'Stop'
$timestamp = Get-Date -Format 'yyyyMMdd_HHmm'
$exportPath = Join-Path $OutputRoot $timestamp
New-Item -ItemType Directory -Path $exportPath -Force | Out-Null
Write-Host "=== Control 2.2 evidence export ===" -ForegroundColor Cyan
Write-Host "Output: $exportPath"
# 1. Environment groups
$groups = Get-AdminPowerAppEnvironmentGroup
$groups |
Select-Object EnvironmentGroupId, DisplayName, Description, CreatedTime |
Export-Csv -Path (Join-Path $exportPath 'EnvironmentGroups.csv') -NoTypeInformation -Encoding UTF8
# 2. Environment-to-group mapping
$envs = Get-AdminPowerAppEnvironment
$envs |
Select-Object DisplayName,
EnvironmentName,
EnvironmentGroupId,
EnvironmentType,
Location,
@{ Name = 'IsManaged'; Expression = {
$_.Properties.governanceConfiguration.protectionLevel -ne 'Standard'
}},
@{ Name = 'CapturedUtc'; Expression = { (Get-Date).ToUniversalTime().ToString('o') }} |
Export-Csv -Path (Join-Path $exportPath 'EnvironmentMembership.csv') -NoTypeInformation -Encoding UTF8
# 3. Group summary
$envs |
Group-Object EnvironmentGroupId |
ForEach-Object {
$name = ($groups | Where-Object EnvironmentGroupId -eq $_.Name).DisplayName
[pscustomobject]@{
GroupId = $_.Name
GroupDisplayName = if ($name) { $name } else { '(Ungrouped)' }
EnvironmentCount = $_.Count
}
} |
Export-Csv -Path (Join-Path $exportPath 'GroupSummary.csv') -NoTypeInformation -Encoding UTF8
# 4. SHA-256 manifest (FSI baseline requirement)
Get-ChildItem $exportPath -Filter *.csv |
ForEach-Object {
[pscustomobject]@{
File = $_.Name
SHA256 = (Get-FileHash -Algorithm SHA256 -Path $_.FullName).Hash
Bytes = $_.Length
}
} |
Export-Csv -Path (Join-Path $exportPath 'manifest.sha256.csv') -NoTypeInformation -Encoding UTF8
Write-Host "[OK] Evidence pack ready: $exportPath" -ForegroundColor Green
Validation script — Validate-Control-2.2.ps1
Read-only. Fails fast on the conditions documented in the Verification & Testing playbook.
<#
.SYNOPSIS
Validates Control 2.2 — Environment Groups and Tier Classification.
.PARAMETER FailOnWarning
Treat warnings as failures (recommended for CI).
.OUTPUTS
Exit code 0 = pass, 1 = fail.
#>
[CmdletBinding()]
param(
[switch] $FailOnWarning
)
$ErrorActionPreference = 'Stop'
$failures = @()
$warnings = @()
Write-Host "=== Control 2.2 validation ===" -ForegroundColor Cyan
$groups = Get-AdminPowerAppEnvironmentGroup
$envs = Get-AdminPowerAppEnvironment
# Check 1 — at least one FSI-Z{n} group exists per zone
foreach ($zone in 1..3) {
$zoneGroups = $groups | Where-Object { $_.DisplayName -match "FSI-Z$zone(-|$)" }
if (-not $zoneGroups) {
$failures += "No environment group named FSI-Z$zone-* found."
} else {
Write-Host "[PASS] Zone $zone groups: $(@($zoneGroups).Count)" -ForegroundColor Green
}
}
# Check 2 — every production environment is in a group
$prodEnvs = $envs | Where-Object EnvironmentType -eq 'Production'
$ungroupedProd = $prodEnvs | Where-Object { -not $_.EnvironmentGroupId }
if ($ungroupedProd) {
$failures += "Ungrouped production environments: $(($ungroupedProd.DisplayName) -join ', ')"
} else {
Write-Host "[PASS] All production environments are grouped." -ForegroundColor Green
}
# Check 3 — every grouped environment is Managed
$groupedEnvs = $envs | Where-Object { $_.EnvironmentGroupId }
$unmanagedInGrp = $groupedEnvs | Where-Object {
$_.Properties.governanceConfiguration.protectionLevel -eq 'Standard'
}
if ($unmanagedInGrp) {
$failures += "Unmanaged environments inside groups: $(($unmanagedInGrp.DisplayName) -join ', ')"
} else {
Write-Host "[PASS] All grouped environments are Managed Environments." -ForegroundColor Green
}
# Check 4 — group descriptions reference governance zone
$noZoneInDesc = $groups | Where-Object {
[string]::IsNullOrWhiteSpace($_.Description) -or
$_.Description -notmatch 'Zone\s*[123]'
}
if ($noZoneInDesc) {
$warnings += "Groups missing 'Zone {1|2|3}' in description: $(($noZoneInDesc.DisplayName) -join ', ')"
}
# Summary
Write-Host "`n=== Summary ===" -ForegroundColor Cyan
Write-Host "Groups : $(@($groups).Count)"
Write-Host "Environments : $(@($envs).Count)"
Write-Host "Grouped : $(@($groupedEnvs).Count)"
Write-Host "Failures : $(@($failures).Count)" -ForegroundColor (if ($failures) { 'Red' } else { 'Green' })
Write-Host "Warnings : $(@($warnings).Count)" -ForegroundColor (if ($warnings) { 'Yellow' } else { 'Green' })
$failures | ForEach-Object { Write-Host "[FAIL] $_" -ForegroundColor Red }
$warnings | ForEach-Object { Write-Host "[WARN] $_" -ForegroundColor Yellow }
if ($failures -or ($FailOnWarning -and $warnings)) { exit 1 }
exit 0
Notes
- Group creation and rule configuration remain portal-first per the portal walkthrough.
- For automated environment provisioning that auto-assigns to the correct zone group, see the Environment Lifecycle Management advanced implementation.
Updated: April 2026 | Version: v1.4.0
Back to Control 2.2 | Portal Walkthrough | Verification & Testing | Troubleshooting