AZSBTools.psm1
function New-SBAZServicePrincipal { <# .SYNOPSIS Function to create Azure AD Service Principal .DESCRIPTION Function to create Azure AD Service Principal The use case intended for this function is to use the Service Principal to run PowerShell scripts against an Azure subscription .PARAMETER ServicePrincipalName One or more Service Principal Names .PARAMETER Environment Name of the Azure cloud. This parameter default to Azure Commercial cloud. As of 15 March 2018 that list is: AzureGermanCloud AzureCloud AzureUSGovernment AzureChinaCloud To see an updated list, use: (Get-AzureRMEnvironment).Name .PARAMETER Role This parameter is used to assign Role/Permissions for te Service Principal in the current subscription. The default value is 'Owner' role. As of 16 March 2018 the following default roles are defined: API Management Service Contributor Application Insights Component Contributor Automation Operator BizTalk Contributor Classic Network Contributor Classic Storage Account Contributor Classic Storage Account Key Operator Service Role Classic Virtual Machine Contributor ClearDB MySQL DB Contributor Contributor Cosmos DB Account Reader Role Data Factory Contributor Data Lake Analytics Developer DevTest Labs User DNS Zone Contributor DocumentDB Account Contributor Intelligent Systems Account Contributor Log Analytics Contributor Log Analytics Reader Network Contributor New Relic APM Account Contributor Owner Reader Redis Cache Contributor Scheduler Job Collections Contributor Search Service Contributor Security Manager SQL DB Contributor SQL Security Manager SQL Server Contributor Storage Account Contributor Storage Account Key Operator Service Role Traffic Manager Contributor User Access Administrator Virtual Machine Contributor Web Plan Contributor Website Contributor For more details on roles, type in: Get-AzureRmRoleDefinition | select name,description,actions | Out-GridView .EXAMPLE $SPList = New-SBAZServicePrincipal -ServicePrincipalName samtest1,sam1demo .EXAMPLE $SPN = New-SBAZServicePrincipal -ServicePrincipalName PowerShell05 -Environment AzureUSGovernment # The above line creates the SPN and gives it 'Owner' permission/role in the current subscription $SPN | Export-Csv .\PowerShell05-SPN.csv -NoTypeInformation # This line saves the $SPN to CSV (not the password) # To use the SPN in future automations: # $SPN = Import-Csv .\PowerShell05-SPN.csv # Login-AzureRmAccount -Credential (Get-SBCredential $SPN.ServicePrincipalName) -ServicePrincipal -TenantId $SPN.TenantID -Environment $SPN.Environment .OUTPUTS The function returns a PS Object for each input Service Principal Name containing the following properties: ServicePrincipalName TenantId Environment Role .LINK https://superwidgets.wordpress.com/2018/03/15/new-sbazserviceprincipal-cmdlet-to-create-new-azure-ad-service-principal-added-to-azsbtools-powershell-module/ .NOTES Function by Sam Boutros v0.1 - 14 March 2018 v0.2 - 15 March 2018 - Added 'Environment' parameter v0.3 - 16 March 2018 - Added 'Role' parameter, changed output to a custom PS Object #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String[]]$ServicePrincipalName, [Parameter(Mandatory=$false)][ValidateSet('AzureCloud','AzureUSGovernment','AzureGermanCloud','AzureChinaCloud')][String]$Environment = 'AzureCloud', [Parameter(Mandatory=$false)][String]$Role = 'Owner' ) Begin { $Subscription = Connect-AzureRmAccount -Environment $Environment } Process { if ($Subscription.Context.Subscription.Name) { Write-Log 'Identified',$Subscription.Context.Subscription.Name,'subscription in the',$Subscription.Context.Environment.Name,'cloud' Green,Cyan,Green,Cyan,Green $SPList = foreach ($AppName in $ServicePrincipalName) { $AppCred = Get-SBCredential -UserName $AppName #region Create/Validate Azure AD App Remove-Variable App -EA 0 if ($App = Get-AzureRmADApplication -DisplayName $AppName) { Write-Log 'Validated app:',$App.Displayname Green,Cyan } else { $App = New-AzureRmADApplication -DisplayName $AppName -IdentifierUris $AppName Write-Log 'Created app:',$App.Displayname Green,Cyan } #endregion #region Create/Validate Azure AD Service Principal Remove-Variable ServicePrincipal -EA 0 if ($ServicePrincipal = Get-AzureRmADServicePrincipal | where { $PSItem.ApplicationId -eq $App.ApplicationId.Guid }) { Write-Log 'Validated Service Principal:',($ServicePrincipal.SerVicePrincipalNames -join ', ') Green,Cyan } else { $ServicePrincipal = New-AzureRmADServicePrincipal -ApplicationId $App.ApplicationId.Guid -Password $AppCred.Password Write-Log 'Created Service Principal:',($ServicePrincipal.SerVicePrincipalNames -join ', ') Green,Cyan } #endregion #region Assign Role (Permissions) Write-Log 'Assigning role',$Role Green,Cyan -NoNewLine $Result = try { New-AzureRmRoleAssignment -ObjectId $ServicePrincipal.Id -RoleDefinitionName $Role -Scope "/subscriptions/$($Subscription.Context.Subscription.Id)" -EA 1 Write-Log 'done' Green } catch { Write-Log $PSItem.Exception.Message Yellow } #endregion [PSCustomObject][Ordered]@{ ServicePrincipalName = $AppName TenantId = (Get-AzureRmTenant).Id Environment = $Environment Role = $Role } } } else { Write-Log 'No subscriptions found for account',$Subscription.Context.Account.Id,'in the',$Subscription.Context.Environment.Name,'cloud' Magenta,Yellow,Magenta,Yellow,Magenta } } End { $SPList } } function Deploy-AzureARMVM { <# .SYNOPSIS Function to automate provisioning of Azure ARM VM(s) .DESCRIPTION Function to automate provisioning of Azure ARM VM(s) .PARAMETER SubscriptionName Name of existing Azure subscription .PARAMETER Location Name of Azure Data center/Location Example: 'eastus' To see location list use: Get-AzureRmLocation | sort Location | Select Location .PARAMETER ResourceGroup Name of Resource Group. Example: 'VMGroup17' The script will create it if it does not exist .PARAMETER AvailabilitySetName Example: 'Availability17' The script will create it if it does not exist .PARAMETER ConfirmShutdown This switch accepts $true or $False, and defaaults to $False If adding existing VMs to Availaibility set, the script must shut down the VMs .PARAMETER StorageAccountPrefix Only lower case letters and numbers, must be Azure (globally) unique .PARAMETER AdminName Example: 'myAdmin17' This will be the new VM local administrator .PARAMETER VMName Example: ('vm01','vm02') Name(s) of VM(s) to be created. Each is 15 characters maximum. If VMs exist, they will be added to Availability Set .PARAMETER VMSize Example: 'Standard_A1_v2' To see available sizes in this Azure location use: (Get-AzureRoleSize).RoleSizeLabel .PARAMETER WinOSImage This defaults to '2012-R2-Datacenter' Available options: '2008-R2-SP1','2012-Datacenter','2012-R2-Datacenter','2016-Datacenter','2016-Datacenter-Server-Core','2016-Datacenter-with-Containers','2016-Nano-Server' To see current options in a given Azure Location use: (Get-AzureRMVMImageSku -Location usgovvirginia -Publisher MicrosoftWindowsServer -Offer WindowsServer).Skus For more information see https://docs.microsoft.com/en-us/azure/virtual-machines/windows/cli-ps-findimage .PARAMETER vNetName Example: 'Seventeen' This will be the name of the virtual network to be created/updated if exist .PARAMETER vNetPrefix Example: '10.17.0.0/16' To be created/updated .PARAMETER SubnetName Example: 'vmSubnet' This will be the name of the subnet to be created/updated .PARAMETER SubnetPrefix Example: '10.17.0.0/24' Must be subset of vNetPrefix above - to be created/updated .PARAMETER LogFile' Path to log file where this scrit will log its commands and output Default is ".\Logs\Deploy-AzureARMVM-$($VMName -join '_')-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" .EXAMPLE Connect-AzureRmAccount -Environment AzureUSGovernment $myParamters = @{ SubscriptionName = 'Azure Government T1' Location = 'usgovvirginia' ResourceGroup = 'EncryptionTest01' AvailabilitySetName = 'AvailabilityTest01' ConfirmShutdown = $false StorageAccountPrefix = 'sam150318a' AdminName = 'myAdmin150318a' VMName = @('vm01','vm02','vm03') VMSize = 'Standard_A0' WinOSImage = '2016-Datacenter' vNetName = 'EncryptionTest01VNet' vNetPrefix = '10.3.0.0/16' SubnetName = 'vmSubnet' SubnetPrefix = '10.3.15.0/24' } Deploy-AzureARMVM @myParamters .LINK http://www.exigent.net/blog/microsoft-azure/provisioning-and-tearing-down-azure-virtual-machines/ .NOTES Function by Sam Boutros 3 January 2017 - v0.1 - Initial release 19 January 2017 - v0.2 Updated parameters - set to mandatory Updated Storage Account creation region, create a separate storage account for each VM Updated Initialize region; removing subscription login, adding input echo, adding error handling Added functionality to configure VMs in availability set 5 March 2018 - v0.3 Cosmetic updates #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$SubscriptionName , # Example: 'Sam Test 1' # Name of existing Azure subscription [Parameter(Mandatory=$true)][String]$Location , # Example: 'eastus' # Get-AzureRmLocation | sort Location | Select Location [Parameter(Mandatory=$true)][String]$ResourceGroup , # Example: 'VMGroup17' # To be created if not exist [Parameter(Mandatory=$false)][String]$AvailabilitySetName , # Example: 'Availability17' # To be created if not exist [Parameter(Mandatory=$false)][Switch]$ConfirmShutdown = $false, # If adding existing VMs to Availaibility set, the script must shut down the VMs [Parameter(Mandatory=$false)][String]$StorageAccountPrefix , # To be created if not exist, only lower case letters and numbers, must be Azure unique [Parameter(Mandatory=$true)][String]$AdminName , # Example: 'myAdmin17' # This will be the new VM local administrator [Parameter(Mandatory=$true)][String[]]$VMName , # Example: ('vm01','vm02') # Name(s) of VM(s) to be created. Each is 15 characters maximum. If VMs exist, they will be added to Availability Set [Parameter(Mandatory=$true)][String]$VMSize , # Example: 'Standard_A1_v2' # (Get-AzureRoleSize).RoleSizeLabel to see available sizes in this Azure location [Parameter(Mandatory=$false)][ValidateSet('2008-R2-SP1','2012-Datacenter','2012-R2-Datacenter','2016-Datacenter','2016-Datacenter-Server-Core','2016-Datacenter-with-Containers','2016-Nano-Server')] [String]$WinOSImage = '2012-R2-Datacenter' , # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/cli-ps-findimage [Parameter(Mandatory=$true)][String]$vNetName , # Example: 'Seventeen' # This will be the name of the virtual network to be created/updated if exist [Parameter(Mandatory=$true)][String]$vNetPrefix , # Example: '10.17.0.0/16' # To be created/updated [Parameter(Mandatory=$true)][String]$SubnetName , # Example: 'vmSubnet' # This will be the name of the subnet to be created/updated [Parameter(Mandatory=$true)][String]$SubnetPrefix , # Example: '10.17.0.0/24' # Must be subset of vNetPrefix above - to be created/updated [Parameter(Mandatory=$false)][String]$LogFile = ".\Logs\Deploy-AzureARMVM-$($VMName -join '_')-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { #region Initialize if (!(Test-Path (Split-Path $LogFile))) { New-Item -Path (Split-Path $LogFile) -ItemType directory -Force | Out-Null } Write-Log 'Input received:' Green $LogFile write-log " SubscriptionName: $SubscriptionName" Cyan $LogFile write-log " Location: $Location" Cyan $LogFile write-log " ResourceGroup: $ResourceGroup" Cyan $LogFile write-log " AvailabilitySetName: $AvailabilitySetName" Cyan $LogFile write-log " ConfirmShutdown: $ConfirmShutdown" Cyan $LogFile write-log " StorageAccountPrefix: $StorageAccountPrefix" Cyan $LogFile write-log " AdminName: $AdminName" Cyan $LogFile write-log " VMName(s): $($VMName -join ', ')" Cyan $LogFile write-log " VMSize: $VMSize" Cyan $LogFile write-log " vNetName: $vNetName" Cyan $LogFile write-log " vNetPrefix: $vNetPrefix" Cyan $LogFile write-log " SubnetName: $SubnetName" Cyan $LogFile write-log " SubnetPrefix: $SubnetPrefix" Cyan $LogFile $Cred = Get-SBCredential -UserName $AdminName #endregion #region Connect to Azure subscription Write-Log 'Connecting to Azure subscription',$SubscriptionName Green,Cyan $LogFile -NoNewLine try { $Result = Get-AzureRmSubscription –SubscriptionName $SubscriptionName -ErrorAction Stop | Select-AzureRmSubscription Write-Log 'done' Green $LogFile Write-Log ($Result | Out-String).Trim() Cyan $LogFile } catch { throw "unable to get Azure Subscription '$SubscriptionName'" } #endregion #region Create/Update Resource group Write-Log 'Create/Update Resource group',$ResourceGroup Green,Cyan $LogFile -NoNewLine try { $Result = New-AzureRmResourceGroup -Name $ResourceGroup -Location $Location -Force -ErrorAction Stop Write-Log 'done' Green $LogFile Write-Log ($Result | Out-String).Trim() Cyan $LogFile } catch { throw "Failed to create Resource Group '$ResourceGroup'" } #endregion #region Create/Update Subnet and vNet Write-Log 'Creating/updating vNet',$vNetName,$vNetPrefix,'and subnet',$SubnetName,$SubnetPrefix Cyan,Green,DarkYellow,Cyan,Green,DarkYellow $LogFile -NoNewLine $Subnet = New-AzureRmVirtualNetworkSubnetConfig -Name $SubnetName -AddressPrefix $SubnetPrefix $vNet = New-AzureRmVirtualNetwork -Name $vNetName -ResourceGroupName $ResourceGroup -Location $Location -AddressPrefix $vNetPrefix -Subnet $Subnet -Force Write-Log 'done' Green #endregion } Process { foreach ($Name in $VMName) { # Provision Azure VM(s) #region Create Storage Account if it does not exist $StorageAccountName = "stor$($StorageAccountPrefix.ToLower())$($Name.ToLower())" if ($StorageAccountName.Length -gt 20) { Write-Log 'Storage account name',$StorageAccountName,'is too long, using first 20 characters only..' Green,Yellow,Green $LogFile $StorageAccountName = $StorageAccountName.Substring(0,19) } Write-Log 'Creating Storage Account',$StorageAccountName Green,Cyan $LogFile try { $StorageAccount = Get-AzureRmStorageAccount -Name $StorageAccountName -ResourceGroupName $ResourceGroup -ErrorAction Stop Write-Log 'Using existing storage account',$StorageAccountName Green,Cyan $LogFile } catch { $i=0 $DesiredStorageAccountName = $StorageAccountName while (!(Get-AzureRmStorageAccountNameAvailability $StorageAccountName).NameAvailable) { $i++ $StorageAccountName = "$StorageAccountName$i" } if ($DesiredStorageAccountName -ne $StorageAccountName ) { Write-Log 'Storage account',$DesiredStorageAccountName,'is taken, using',$StorageAccountName,'instead (available)' Greem,Yellow,Green,Cyan,Green $LogFile } try { $Splatt = @{ ResourceGroupName = $ResourceGroup Name = $StorageAccountName SkuName = 'Standard_LRS' Kind = 'Storage' Location = $Location ErrorAction = 'Stop' } $StorageAccount = New-AzureRmStorageAccount @Splatt Write-Log 'Created storage account',$StorageAccountName Green,Cyan $LogFile } catch { Write-Log 'Failed to create storage account',$StorageAccountName Magenta,Yellow $LogFile throw $PSItem.exception.message } } #endregion #region Create/validate Availability Set if ($AvailabilitySetName) { Write-Log 'Creating/verifying Availability Set',$AvailabilitySetName Green,Cyan $LogFile try { $AvailabilitySet = Get-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName -ErrorAction Stop Write-Log 'Availability Set',$AvailabilitySetName,'already exists' Green,Yellow,Green $LogFile Write-Log ($AvailabilitySet | Out-String).Trim() Cyan $LogFile } catch { try { $AvailabilitySet = New-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName -Location $Location -ErrorAction Stop Write-Log 'Created Availability Set',$AvailabilitySetName Green,Cyan $LogFile } catch { Write-Log 'Failed to create Availability Set',$AvailabilitySetName Magenta,Yellow $LogFile throw $PSItem.exception.message } } if ($AvailabilitySet.Location -ne $Location) { Write-Log 'Unable to proceed, Availability set must be in the same location',$AvailabilitySet.Location,'as the desired VM location',$Location Magenta,Yellow,Magenta,Yellow $LogFile break } } #endregion try { $ExistingVM = Get-AzureRmVM -ResourceGroupName $ResourceGroup -Name $Name -ErrorAction Stop Write-Log 'VM',$ExistingVM.Name,'already exists' Green,Yellow,Gree $LogFile if ($AvailabilitySetName) { if ($ConfirmShutdown) { Write-Log 'Shutting down VM',$Name,'to add it to Availability set',$AvailabilitySetName Green,Cayn,Green,Cyan $LogFile Stop-AzureRmVM -Name $Name -Force -StayProvisioned -ResourceGroupName $ResourceGroup -Confirm:$false # Remove current VM Remove-AzureRmVM -ResourceGroupName $ResourceGroup -Name $Name -Force -Confirm:$false # Prepare to recreate VM $VM = New-AzureRmVMConfig -VMName $ExistingVM.Name -VMSize $ExistingVM.HardwareProfile.VmSize -AvailabilitySetId $AvailabilitySet.Id Set-AzureRmVMOSDisk -VM $VM -VhdUri $ExistingVM.StorageProfile.OsDisk.Vhd.Uri -Name $ExistingVM.Name -CreateOption Attach -Windows #Add Data Disks foreach ($Disk in $ExistingVM.StorageProfile.DataDisks) { Add-AzureRmVMDataDisk -VM $VM -Name $Disk.Name -VhdUri $Disk.Vhd.Uri -Caching $Disk.Caching -Lun $Disk.Lun -CreateOption Attach -DiskSizeInGB $Disk.DiskSizeGB } #Add NIC(s) foreach ($NIC in $ExistingVM.NetworkInterfaceIDs) { Add-AzureRmVMNetworkInterface -VM $VM -Id $NIC } # Recreate the VM as part of the Availability Set New-AzureRmVM -ResourceGroupName $ResourceGroup -Location $ExistingVM.Location -VM $VM -DisableBginfoExtension } else { Write-Log 'To add existing VM(s) to availability set, the VM(s) must be shut down. Use the','-ConfirmShutdown:$true','switch' Yellow,Cyan,Yellow $LogFile break } } } catch { Write-Log 'Preparing to create new VM',$Name Green,Cyan $LogFile Write-Log 'Requesting/updating public IP address assignment',"$Name-PublicIP" Green,Cyan $LogFile $PublicIp = New-AzureRmPublicIpAddress -Name "$Name-PublicIP" -ResourceGroupName $ResourceGroup -Location $Location -AllocationMethod Dynamic -Force Write-Log 'Provisining/updating vNIC',"$Name-vNIC" Green,Cyan $LogFile $vNIC = New-AzureRmNetworkInterface -Name "$Name-vNIC" -ResourceGroupName $ResourceGroup -Location $Location -SubnetId $vNet.Subnets[0].Id -PublicIpAddressId $PublicIp.Id -Force Write-Log 'Provisioning VM configuration object for VM',$Name Green,Cyan $LogFile if ($AvailabilitySetName) { $VM = New-AzureRmVMConfig -VMName $Name -VMSize $VMSize -AvailabilitySetId $AvailabilitySet.Id } else { $VM = New-AzureRmVMConfig -VMName $Name -VMSize $VMSize } Write-Log 'Configuring VM OS (Windows),',$Cred.UserName,'local admin' Green,Cyan,Green $LogFile $VM = Set-AzureRmVMOperatingSystem -VM $VM -Windows -ComputerName $Name -Credential $Cred -ProvisionVMAgent -EnableAutoUpdate Write-Log 'Selecting VM image - Latest',$WinOSImage Green,Cyan $LogFile $VM = Set-AzureRmVMSourceImage -VM $VM -PublisherName "MicrosoftWindowsServer" -Offer "WindowsServer" -Skus $WinOSImage -Version "latest" Write-Log 'Adding vNIC' Green $LogFile $VM = Add-AzureRmVMNetworkInterface -VM $VM -Id $vNIC.Id $VhdUri = "$($StorageAccount.PrimaryEndpoints.Blob.ToString())vhds/$($Name)-OsDisk1.vhd" Write-Log 'Configuring OS Disk',$VhdUri Green,Cyan $LogFile $VM = Set-AzureRmVMOSDisk -VM $VM -Name 'OSDisk' -VhdUri $VhdUri -CreateOption FromImage Write-Log 'Creating VM..' Green -NoNewLine New-AzureRmVM -ResourceGroupName $ResourceGroup -Location $Location -VM $VM Write-Log 'done' Green $LogFile $DoneVM = Get-AzureRmVM | where { $_.Name -eq $Name } | FT -a Write-Log ($DoneVM | Out-String).Trim() cyan $LogFile } } } End { if ($AvailabilitySetName) { $AvailabilitySet = Get-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName $VMDomains = $AvailabilitySet.VirtualMachinesReferences | foreach { $VM = Get-AzureRMVM -Name (Get-AzureRmResource -Id $_.id).Name -ResourceGroup $ResourceGroup -Status [PSCustomObject][Ordered]@{ Name = $VM.Name FaultDomain = $VM.PlatformFaultDomain UpdateDomain = $VM.PlatformUpdateDomain } } Write-Log ($VMDomains | sort Name | FT -a | Out-String).Trim() Cyan $LogFile } } } function Expand-JSON { <# .SYNOPSIS Function to expand a custom PowerShell object in a more readable format .DESCRIPTION Function to expand a custom PowerShell object in a more readable format The ConvertFrom-Json cmdlet of the Microsoft.PowerShell.Utility module outputs a PS Custom Object that often contains sub objects and so on. This function expands all objects and displays the key/value pairs in a more humanly readable format - see the example .PARAMETER JSON PS Custom Object, typically the output of ConvertFrom-Json cmdlet - see the example .PARAMETER Parent This is optional parameter used to show sub-objects when using the function recursively .EXAMPLE Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json | Expand-JSON where the contents of Storage1.json file are: { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "storageAccountType": { "type": "string", "defaultValue": "Standard_LRS", "allowedValues": [ "Standard_LRS", "Standard_GRS", "Standard_ZRS", "Premium_LRS" ], "metadata": { "description": "Storage Account type" } } }, "variables": { "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'standardsa')]" }, "resources": [ { "type": "Microsoft.Storage/storageAccounts", "name": "[variables('storageAccountName')]", "apiVersion": "2016-01-01", "location": "[resourceGroup().location]", "sku": { "name": "[parameters('storageAccountType')]" }, "kind": "Storage", "properties": { } } ], "outputs": { "storageAccountName": { "type": "string", "value": "[variables('storageAccountName')]" } } } The output of Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json would look like: $schema : https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json# contentVersion : 1.0.0.0 parameters : @{storageAccountType=} variables : @{storageAccountName=[concat(uniquestring(resourceGroup().id), 'standardsa')]} resources : {@{type=Microsoft.Storage/storageAccounts; name=[variables('storageAccountName')]; apiVersion=2016-01-01; location=[resourceGroup().location]; sku=; kind=Storage; properties=}} outputs : @{storageAccountName=} which does not show sub-objects such as parameters.storageAccountType.allowedValues, parameters.storageAccountType.defaultValue, ... However, the output of Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json | Expand-JSON shows all objects, sub-objects, and their key/pair values: $schema: https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json# contentVersion: 1.0.0.0 outputs.storageAccountName.type: string outputs.storageAccountName.value: [variables('storageAccountName')] parameters.storageAccountType.allowedValues: Standard_LRS, Standard_GRS, Standard_ZRS, Premium_LRS parameters.storageAccountType.defaultValue: Standard_LRS parameters.storageAccountType.metadata.description: Storage Account type parameters.storageAccountType.type: string resources.apiVersion: 2016-01-01 resources.kind: Storage resources.location: [resourceGroup().location] resources.name: [variables('storageAccountName')] resources.sku.name: [parameters('storageAccountType')] resources.type: Microsoft.Storage/storageAccounts variables.storageAccountName: [concat(uniquestring(resourceGroup().id), 'standardsa')] .LINK https://superwidgets.wordpress.com/ .NOTES Function by Sam Boutros v0.1 - 28 March 2018 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true,ValueFromPipeLine=$true,ValueFromPipeLineByPropertyName=$true)][PSCustomObject]$JSON, [Parameter(Mandatory=$false)][String[]]$Parent ) Begin { Write-Verbose "JSON: $($JSON | Out-String)" Write-Verbose "Parent: $($Parent -join '.')" } Process { foreach ($NoteProperty in ($JSON | Get-Member -MemberType NoteProperty)) { if ($NoteProperty.Definition -match 'PSCustomObject') { Expand-JSON -JSON $JSON.($NoteProperty.Name) -Parent ($Parent + $NoteProperty.Name) } else { if (($JSON.($NoteProperty.Name) -join '').Trim()) { Write-Log "$(($Parent + $NoteProperty.Name) -join '.'):",($JSON.($NoteProperty.Name) -join ', ') Green,Cyan } else { Expand-JSON -JSON $JSON.($NoteProperty.Name) -Parent ($Parent + $NoteProperty.Name) -EA 0 } } } } End { } } Export-ModuleMember -Function * -Variable * |