Register-SelfDestructiveAzResource.ps1
<#PSScriptInfo .VERSION 1.0.0.8 .GUID ba60be59-a510-4ca2-a15d-34cbb83ed089 .AUTHOR Mahmood Abushaireh .COMPANYNAME .COPYRIGHT .TAGS Azure 'Azure Automation' .LICENSEURI .PROJECTURI http://mabushaireh.info/posts/about-register-selfdestructiveazresource .ICONURI .EXTERNALMODULEDEPENDENCIES Az .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES [1.0.0.6] 2020-06-24 Rename TicketNumber to RefNumber and make it [optional] 2020-06-24 Make subscription ID [optinoal] if not passed default subscription is assumed [1.0.0.7] 2020-06-29 Add help Message to the params 2020-06-29 Add validation for -DeleteAfterDays, should positive integer and a maximum of 30 days. 2020-06-29 Add validation for DeleteAfterTimeOfDay, should be positive and from 0 to 23 hours [1.0.0.8] 2020-07-02 Add -Force to Expand-Archive to save over existing runbook 2021-01-04 Add Resource group support #> <# .Synopsis Schedule a cleanup job for the provided Azure Resources. .DESCRIPTION Automatically delete Azure Resource at a given time date .EXAMPLE Register-SelfDestructiveResource --ResourceId "Resource Id" -RefNumber "Ref Number" #Note: Refnumber is not to link the resource to an internal record, could be a workitem id if you are working on a project .EXAMPLE Register-SelfDestructiveResource -ResourceName "Resource Name" -ResrourceGroup "Resource Group" -DeleteAfterDays 3 DeleteAfterTimeOfDay 19 .LINK http://mabushaireh.info/posts/about-register-selfdestructiveazresource #> #Requires -RunAsAdministrator [CmdletBinding( DefaultParameterSetName = 'ResourceID', HelpUri = 'http://mabushaireh.info/posts/about-register-selfdestructiveazresource' )] #TODO: add Support for resource group cleanup param ( [Parameter(Mandatory = $true, ParameterSetName = "ResourceID", HelpMessage = "Resource Id for the resource you want the tool to manage. Resource Id format is /subscriptions/{guid}/resourceGroups/{resource-group-name}/{resource-provider-namespace}/{resource-type}/{resource-name}." )] [string] $ResourceId, [Parameter(Mandatory = $true, ParameterSetName = "ResourceName", HelpMessage = "Resource Name")] [string] $ResourceName, [Parameter(Mandatory = $true, ParameterSetName = "ResourceName", HelpMessage = "Resource Group where the Resource provided in the [-ResourceName] param exists. ")] [Parameter(Mandatory = $true, ParameterSetName = "ResourceGroup", HelpMessage = "Resource Group required to be destroyed.")] [string] $ResourceGroup, [Parameter(Mandatory = $false, ParameterSetName = "ResourceID", HelpMessage = "[OPTIONAL] Referce numbed used for tracking resouces created for specific recored. Refernace Id could be workitem id or task id from your DevOps tool!")] [Parameter(ParameterSetName = "ResourceName")] [Parameter(ParameterSetName = "ResourceGroup")] [string] $RefNumber, [Parameter(Mandatory = $false, ParameterSetName = "ResourceID", HelpMessage = "[OPTIONAL] if you want to manage a resource in a subscription other than the default subsciption!")] [Parameter(ParameterSetName = "ResourceName")] [Parameter(ParameterSetName = "ResourceGroup")] [string] $SubscriptionId, [Parameter(Mandatory = $false, ParameterSetName = "ResourceID", HelpMessage = "Number of Days before the resource is deleted. Default value is 0")] [Parameter(ParameterSetName = "ResourceName")] [Parameter(ParameterSetName = "ResourceGroup")] [ValidateRange(0, 30)] [int] $DeleteAfterDays = 0, [Parameter(Mandatory = $false, ParameterSetName = "ResourceID", HelpMessage = "Local time in Days (24 Hours format) when the delete operaion starts. Default is 19:00 or 7pm.")] [Parameter(ParameterSetName = "ResourceName")] [Parameter(ParameterSetName = "ResourceGroup")] [ValidateRange(0, 23)] [int] $DeleteAfterTimeOfDay = 19, [Parameter(Mandatory = $false, ParameterSetName = "ResourceID", HelpMessage = "Resource Group where the resources required for the tool will be created, by default its SelfDistructiveTool-rg.")] [Parameter(ParameterSetName = "ResourceName")] [Parameter(ParameterSetName = "ResourceGroup")] [string] $ToolResourceGroup = "SelfDistructiveTool-rg" ) $ScriptVersion = '1.0.0.8' function New-RunAsAccount { Param ( [Parameter(Mandatory = $true)] [String] $ResourceGroup, [Parameter(Mandatory = $true)] [String] $AutomationAccountName, [Parameter(Mandatory = $true)] [String] $ApplicationDisplayName, [Parameter(Mandatory = $true)] [String] $SubscriptionId, [Parameter(Mandatory = $true)] [String] $SelfSignedCertPlainPassword, [Parameter(Mandatory = $false)] [string] $EnterpriseCertPathForRunAsAccount, [Parameter(Mandatory = $false)] [String] $EnterpriseCertPlainPasswordForRunAsAccount, [Parameter(Mandatory = $false)] [String] $EnterpriseCertPathForClassicRunAsAccount, [Parameter(Mandatory = $false)] [int] $SelfSignedCertNoOfMonthsUntilExpired = 12 ) function CreateSelfSignedCertificate([string] $certificateName, [string] $selfSignedCertPlainPassword, [string] $certPath, [string] $certPathCer, [string] $selfSignedCertNoOfMonthsUntilExpired ) { $Cert = New-SelfSignedCertificate -DnsName $certificateName -CertStoreLocation cert:\LocalMachine\My ` -KeyExportPolicy Exportable -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" ` -NotAfter (Get-Date).AddMonths($selfSignedCertNoOfMonthsUntilExpired) -HashAlgorithm SHA256 $CertPassword = ConvertTo-SecureString $selfSignedCertPlainPassword -AsPlainText -Force Export-PfxCertificate -Cert ("Cert:\localmachine\my\" + $Cert.Thumbprint) -FilePath $certPath -Password $CertPassword -Force | Write-Verbose Export-Certificate -Cert ("Cert:\localmachine\my\" + $Cert.Thumbprint) -FilePath $certPathCer -Type CERT | Write-Verbose } function CreateServicePrincipal([System.Security.Cryptography.X509Certificates.X509Certificate2] $PfxCert, [string] $applicationDisplayName) { $keyValue = [System.Convert]::ToBase64String($PfxCert.GetRawCertData()) $keyId = (New-Guid).Guid # Create an Azure AD application, AD App Credential, AD ServicePrincipal # Requires Application Developer Role, but works with Application administrator or GLOBAL ADMIN $Application = New-AzADApplication -DisplayName $ApplicationDisplayName -HomePage ("http://" + $applicationDisplayName) -IdentifierUris ("http://" + $keyId) # Requires Application administrator or GLOBAL ADMIN $ApplicationCredential = New-AzADAppCredential -ApplicationId $Application.ApplicationId -CertValue $keyValue -StartDate $PfxCert.NotBefore -EndDate $PfxCert.NotAfter # Requires Application administrator or GLOBAL ADMIN $ServicePrincipal = New-AzADServicePrincipal -ApplicationId $Application.ApplicationId $GetServicePrincipal = Get-AzADServicePrincipal -ObjectId $ServicePrincipal.Id # Sleep here for a few seconds to allow the service principal application to become active (ordinarily takes a few seconds) Start-Sleep -s 15 # Requires User Access Administrator or Owner. $NewRole = New-AzRoleAssignment -RoleDefinitionName Contributor -ServicePrincipalName $Application.ApplicationId -ErrorAction SilentlyContinue $Retries = 0; While ($null -eq $NewRole -and $Retries -le 6) { Start-Sleep -s 10 New-AzRoleAssignment -RoleDefinitionName Contributor -ServicePrincipalName $Application.ApplicationId | Write-Verbose -ErrorAction SilentlyContinue $NewRole = Get-AzRoleAssignment -ServicePrincipalName $Application.ApplicationId -ErrorAction SilentlyContinue $Retries++; } return $Application.ApplicationId.ToString(); } function CreateAutomationCertificateAsset ([string] $resourceGroup, [string] $automationAccountName, [string] $certifcateAssetName, [string] $certPath, [string] $certPlainPassword, [Boolean] $Exportable) { $CertPassword = ConvertTo-SecureString $certPlainPassword -AsPlainText -Force Remove-AzAutomationCertificate -ResourceGroupName $resourceGroup -AutomationAccountName $automationAccountName -Name $certifcateAssetName -ErrorAction SilentlyContinue New-AzAutomationCertificate -ResourceGroupName $resourceGroup -AutomationAccountName $automationAccountName -Path $certPath -Name $certifcateAssetName -Password $CertPassword -Exportable:$Exportable | write-verbose } function CreateAutomationConnectionAsset ([string] $resourceGroup, [string] $automationAccountName, [string] $connectionAssetName, [string] $connectionTypeName, [System.Collections.Hashtable] $connectionFieldValues ) { Remove-AzAutomationConnection -ResourceGroupName $resourceGroup -AutomationAccountName $automationAccountName -Name $connectionAssetName -Force -ErrorAction SilentlyContinue New-AzAutomationConnection -ResourceGroupName $ResourceGroup -AutomationAccountName $automationAccountName -Name $connectionAssetName -ConnectionTypeName $connectionTypeName -ConnectionFieldValues $connectionFieldValues } # To use the new Az modules to create your Run As accounts, please uncomment the following lines and ensure you comment out the previous 8 lines that import the AzureRM modules to avoid any issues. To learn about about using Az modules in your Automation account see https://docs.microsoft.com/azure/automation/az-modules. Import-Module Az.Automation Enable-AzureRmAlias $Subscription = Get-AzSubscription -SubscriptionId $SubscriptionId | Set-AzContext # Create a Run As account by using a service principal $CertifcateAssetName = "AzureRunAsCertificate" $ConnectionAssetName = "AzureRunAsConnection" $ConnectionTypeName = "AzureServicePrincipal" if ($EnterpriseCertPathForRunAsAccount -and $EnterpriseCertPlainPasswordForRunAsAccount) { $PfxCertPathForRunAsAccount = $EnterpriseCertPathForRunAsAccount $PfxCertPlainPasswordForRunAsAccount = $EnterpriseCertPlainPasswordForRunAsAccount } else { $CertificateName = $AutomationAccountName + $CertifcateAssetName $PfxCertPathForRunAsAccount = Join-Path $env:TEMP ($CertificateName + ".pfx") $PfxCertPlainPasswordForRunAsAccount = $SelfSignedCertPlainPassword $CerCertPathForRunAsAccount = Join-Path $env:TEMP ($CertificateName + ".cer") CreateSelfSignedCertificate $CertificateName $PfxCertPlainPasswordForRunAsAccount $PfxCertPathForRunAsAccount $CerCertPathForRunAsAccount $SelfSignedCertNoOfMonthsUntilExpired } # Create a service principal $PfxCert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($PfxCertPathForRunAsAccount, $PfxCertPlainPasswordForRunAsAccount) $ApplicationId = CreateServicePrincipal $PfxCert $ApplicationDisplayName # Create the Automation certificate asset CreateAutomationCertificateAsset $ResourceGroup $AutomationAccountName $CertifcateAssetName $PfxCertPathForRunAsAccount $PfxCertPlainPasswordForRunAsAccount $true # Populate the ConnectionFieldValues $SubscriptionInfo = Get-AzSubscription -SubscriptionId $SubscriptionId $TenantID = $SubscriptionInfo | Select-Object TenantId -First 1 $Thumbprint = $PfxCert.Thumbprint $ConnectionFieldValues = @{"ApplicationId" = $ApplicationId; "TenantId" = $TenantID.TenantId; "CertificateThumbprint" = $Thumbprint; "SubscriptionId" = $SubscriptionId } # Create an Automation connection asset named AzureRunAsConnection in the Automation account. This connection uses the service principal. CreateAutomationConnectionAsset $ResourceGroup $AutomationAccountName $ConnectionAssetName $ConnectionTypeName $ConnectionFieldValues } Import-Module Az # Write the script version Write-Host ("Script version: {0}" -f $ScriptVersion) # Write the command line that was used when the script was called Write-Host ("Command line: {0}" -f $MyInvocation.Line) # Get the current time in UTC Write-Host ("Current UTC time: {0}" -f [System.DateTime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss')) #### Constants DONT CHANGE $runbookName = "Delete-AzureResource" $automationAccountName = "SelfDistructiveTool-aa" $DeleteAfter = (Get-Date ($deleteAfterTimeOfDay.ToString() + ":00:00")).AddDays($DeleteAfterDays) if ($DeleteAfter.Subtract((Get-Date)).Hours -lt 2) { #Delete Next Day!!! $DeleteAfter = $DeleteAfter.AddDays(1) } # this is might be needed in the feature. #$moduleContentUrl = "https://www.powershellgallery.com/api/v2/package/AzureRM.HDInsight/1.0.4" # #TODO: Convert this to a param with default value $defaultLocation = "West Europe" $ToolTags = @{"CreatedWith" = "Register-SelfDestructiveAzResource.ps1"; "VersionInfo" = "$ScriptVersion"; } Write-Verbose "subscriptionId : $SubscriptionId" Write-Verbose "deleteAfterDays : $DeleteAfterDays" Write-Verbose "deleteAfterTimeOfDay : $deleteAfterTimeOfDay" Write-Verbose "DeleteAfter : $DeleteAfter" if ($PSBoundParameters.ContainsKey("SubscriptionId")) { if ($SubscriptionId -ne "" ) { Write-Information -Message "Selecting Subscription $subscriptionId" Get-AzSubscription -SubscriptionId $SubscriptionId | Set-AzContext } else { Write-Information "Using default subscription" } } else { Write-Information "Using default subscription" } Write-Verbose -Message 'Get Resource' if ($PSBoundParameters.ContainsKey("ResourceId")) { $resource = Get-AzResource -ResourceId $ResourceId } elseif ($PSBoundParameters.ContainsKey("ResourceName")) { $resource = Get-AzResource -ResourceGroupName $ResourceGroup -Name $ResourceName $ResourceId = $resource.ResourceId } else { # this is resource Group $rg = Get-AzResourceGroup -ResourceGroupName $ResourceGroup } if (!$resource -and !$rg) { if ($PSBoundParameters.ContainsKey("ResourceId")) { Throw "Resource with id [$ResourceId] doesnt exist!" } elseif ($PSBoundParameters.ContainsKey("ResourceName")){ Throw "Resource with name [$ResourceName] doesnt exist under resource group [$ResourceGroup]!" } else { Throw "Resource Group with name [$ResourceGroup] doesnt exist!" } exit 1 } Write-Verbose -Message 'Set resourcer TAGs' $resourceTags = @{"RefNumber" = " $RefNumber"; "DeleteAfter" = "$DeleteAfter"; "ManagedBy" = "Register-SelfDestructiveAzResource.ps1"; "VersionInfo" = "$ScriptVersion"; } if ($resource){ Set-AzResource -ResourceId $ResourceId -Tag $resourceTags -Force } else { Set-AzResourceGroup -Name $ResourceGroup -Tag $resourceTags } Write-Verbose "Check if Tool Resource Group Exists account exists" $rg = Get-AzResourceGroup -Name $ToolResourceGroup -ErrorAction SilentlyContinue if (!$rg) { Write-Verbose "Resource Group [$ToolResourceGroup] doent exist, creating it!" New-AzResourceGroup -Name $ToolResourceGroup -Location $defaultLocation -Tag $ToolTags } Write-Verbose "Check if automation account exists" $aa = Get-AzAutomationAccount -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName if (!$aa) { Write-Verbose "Automation Account required for cleanup doesnt not exist!" Write-Verbose "Creating automation account, this required for the cleanup" New-AzAutomationAccount -ResourceGroupName $ToolResourceGroup -Name $automationAccountName -Location $defaultLocation -Tag $ToolTags #TODO: take this outside to enable retries and check if RunAs Is created then skip the step Write-Verbose "Creating automation account, Run As Account" New-RunAsAccount -ResourceGroup $ToolResourceGroup -AutomationAccountName $automationAccountName -ApplicationDisplayName "$automationAccountName-runas" -SubscriptionId $subscriptionId -SelfSignedCertPlainPassword "P@ssw0rd1" #Write-Verbose "Import AzureRm.HdiInsight Module to Automation Account" #New-AzAutomationModule -Name AzureRM.HDInsight -ResourceGroupName $AutomationAccountResourceGroup -AutomationAccountName $automationAccountName -ContentLink $ModuleContentUrl } Write-Verbose "Check if runbook for cleanup exists" $runbook = Get-AzAutomationRunbook -Name $runbookName -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName if (!$runbook) { Write-Verbose "Create Runbook for $runbookName" New-AzAutomationRunbook -Name $runbookName -Type PowerShell -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName -Tags $ToolTags Write-Verbose "Import local script to automation account" #Downlaod Script Locally $url = "https://psg-prod-eastus.azureedge.net/packages/delete-azureresource.1.0.0.1.nupkg" $output = "$env:TEMP\Delete-AzureResource.zip" Invoke-WebRequest -Uri $url -OutFile $output Expand-Archive -LiteralPath $output -DestinationPath "$env:TEMP\Delete-AzureResource" -Force $runbookPath = "$env:TEMP\Delete-AzureResource\Delete-AzureResource.ps1" Import-AzAutomationRunbook -Name $runbookName -Path $runbookPath -Type PowerShell -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName -Force #TODO: Clean up temps when finish Write-Verbose "Publish Runbook" Publish-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $ToolResourceGroup -Name $runbookName } $postfix = "" if ($PSBoundParameters.ContainsKey("RefNumber")) { $postfix = "-" + $RefNumber.Substring($RefNumber.Length - 3) } if ($resource){ $scheduleName = "Cleanup-Resource-" + $resource.ResourceName + $postfix } else { $scheduleName = "Cleanup-ResourceGroup-" + $rg.ResourceGroupName + $postfix } Write-Verbose "Check if scheduleName [$scheduleName] exists" $schedule = Get-AzAutomationSchedule -AutomationAccountName $automationAccountName -ResourceGroupName $ToolResourceGroup -Name $scheduleName if (!$schedule) { Write-Verbose "Create Schedule" if ($resource){ $params = @{"ResourceId" = "$ResourceId"; } } else { $params = @{"ResourceGroup" = "$ResourceGroup"; } } $timezone = ([System.TimeZoneInfo]::local).Id Write-Verbose "Your Timezone: $timezone" New-AzAutomationSchedule -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName -Name $scheduleName -StartTime $DeleteAfter -OneTime -TimeZone $timezone Write-Verbose "Register Schedule" Register-AzAutomationScheduledRunbook -Name $runbookName -ResourceGroupName $ToolResourceGroup -AutomationAccountName $automationAccountName -ScheduleName $scheduleName -Parameters $params } |