Every time I deploy a webapp, via VSTS or Octopus, service principal creation process and web app deployment is assumed to be manual and requires to be configured in advance of deployment process. There is always a drop down without allowing you put new service principal name, and a web app name. [Hope this will change soon, and this post will be unnecessary :)] The meetup demos I attend, the same as well as the msdn documentations don’t mind showing how to add these manually.
However our Azure governance model is Functional Pattern, requires one subscription per environment, and one SPN per resource group, I should be able to create SPN per each environment automatically from scratch to automate our pipeline, plus I don’t like doing things manually…
Part I: Creating SPNs
So, what is an SPN? Think of service accounts. For each application [essentially an identifier Uri], you create a service principal, with a password [or a certificate], and a homepage url.
Azure has RBAC, so you can set any level of permission to any object/user. In basic, it has “Reader”, “Contributor” and “Admin” permissions. For simplicity I will use Contributor role within a specific ResourceGroup. You may want to set a granular permission on the resource group, such as creating app service plans, website by a different service principal, or you may say “Sky is the limit” you can have your role definitions too! Check out the msdn site for role definitions.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function New-AzureSpn{ | |
param([string]$Subscriptionid , | |
[string]$environmentName, | |
[string]$ApplicationName, | |
[string]$resourceGroupName, | |
[string]$location, | |
[string]$password | |
) | |
#Login-AzureRmAccount | |
$DisplayName =$ApplicationName+ $environmentName + "SPN" | |
$HomePage = "http://$applicationName.clouddev.com" | |
$IdentifierUri = "http://$applicationName.clouddev.com" | |
#refer the first script | |
########################################################################### | |
#Step1: Check there is no existing AzureAD Application with the same Uri: | |
#Names are not unique, but the IdentifierUri should be unique | |
########################################################################### | |
$clientApplication=Get-AzureRmADApplication -IdentifierUri $identifierUri | |
If ($clientApplication) { | |
Write-Output "There is already an AD Application for this URI: $identifierUri" | |
#Either remove, stop the process and rename the Uri, or get the ADApplication, which is the default behavior here: | |
#Remove-AzureRmADApplication -ObjectId $clientApplication.ObjectId -Force | |
} | |
else | |
{ | |
$clientApplication = New-AzureRmADApplication -DisplayName $displayName -HomePage $homePage -IdentifierUris $identifierUri -Password $password -Verbose | |
Write-Output "A new AzureAD Application is created for this URI: $identifierUri" | |
} | |
################################################ | |
#Step2: Create Application and SPN: | |
################################################ | |
$clientId = $clientApplication.ApplicationId | |
Write-Output "Azure AAD Application creation completed successfully (Application Id: $clientId)" -Verbose | |
if((Get-AzureRmADServicePrincipal -ServicePrincipalName $clientId -ErrorAction SilentlyContinue) -eq $null){ | |
$spn = New-AzureRmADServicePrincipal -ApplicationId $clientId | |
} | |
else { | |
$spn = Get-AzureRmADServicePrincipal -ServicePrincipalName $clientId | |
} | |
# This will allow it to regenerate keys of all the Storage Accounts in the subscription | |
$spnRole = "Contributor" | |
$resourceGroup=Get-AzureRmResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue | |
if ( $null -eq $resourceGroup) | |
{ | |
New-AzureRmResourceGroup -Name $resourceGroupName -Location $location | |
} | |
New-AzureRmRoleAssignment -RoleDefinitionName $spnRole -ServicePrincipalName $clientId -Verbose -ResourceGroupName $resourceGroupName | |
################################################ | |
#Step3: Login to verify the SPN | |
################################################ | |
#If you have not yet,now we can login and verify our new spn: | |
#Login-AzureRmAccount -Credential $creds -ServicePrincipal -TenantId $tenantId | |
#Use the subscriptionId parameter from loginazuresubscription script. | |
$tenantId= (Get-AzureRmSubscription -SubscriptionId $subscriptionid).TenantId | |
$objectId=$spn.Id | |
#Cleanup actions enable below if you wanted to clean up the azure ad application | |
#$clientApplication | Remove-AzureRmADApplication -ObjectId $_.ObjectId -Force | |
################################################ | |
#Step4: Get the packer/VSTS/Octopus info | |
################################################ | |
Write-Host 'subscription_Id :' $subscriptionid.tostring() | |
Write-Host 'tenant_Id : ' $tenantId.ToString() | |
Write-Host 'object_id :' $objectId.ToString() | |
write-Host 'client_id/Username/SPN Name :' $clientId.ToString() | |
write-Host 'client_secret/Password : ' $password.ToString() | |
Write-Host 'spn_displayname: ' $DisplayName.Tostring() | |
} | |
$params= @{ | |
Password=New-Guid | |
ResourceGroupName='resourcegroup' | |
Subscriptionid='xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' | |
Location='location' | |
ApplicationName='app' | |
EnvironmentName='dev' | |
} | |
Login-AzureRmAccount | |
New-AzureSpn @params |
Part II: Creating Service Plan and WebApps
For each web application, we need a service plan, like a hosting plan to define:
– Region (West US, East US, etc.),
– Scale count (one, two, three instances, etc.)
– Instance size (Small, Medium, Large)
– SKU (Free, Shared, Basic, Standard, Premium)
And we will deploy our webapp on a service plan with the service principal we have created. The nice thing is Get-AzureRmWebAppPublishingProfile
gives you all the deployment account details it has just created [if you are thinking of other deployment methods].
And, one thing we found useful was to set ‘AppServiceUse32BitWorkerProcess’ to true. [Shanselman has a great post about it!]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
############################################### | |
function Add-Account { | |
param( | |
[string]$AzureTenantId, | |
[string]$AzureServicePrincipalName, | |
[string]$AzureSPNPassword | |
) | |
############################################### | |
##Step1: Get Variables | |
$SPNNamingStandard='^[–z]{5,40}$' | |
############################################### | |
##Step2: Validate Variables: | |
if (!($AzureTenantId -match $SPNNamingStandard)) | |
{ | |
Write-Output "SPN is not in the right format" | |
} | |
############################################### | |
##Step3: Create Account | |
Write-Output "Creating Account" | |
$SecurePassword = ConvertTo-SecureString -asplaintext -force $AzureSPNPassword | |
$SecureCredential = New-Object System.Management.Automation.PSCredential ($AzureServicePrincipalName, $SecurePassword) | |
write-output '###############################################' | |
write-output '##Step4: Login to the SPN Account' | |
try{ | |
write-output "Adding AzureRM Account" | |
Add-AzureRmAccount -ServicePrincipal -Tenant $AzureTenantId -Credential $SecureCredential | |
} | |
catch { | |
Write-Output $_ | |
throw "Cannot add account $AzureServicePrincipalName" | |
} | |
} | |
############################################### | |
############################################### | |
## Check and Create Service Plan | |
function New-AzureAppServicePlan{ | |
param([string]$ResourceGroupName, | |
[string]$AppServicePlanName, | |
[string]$Location, | |
[string]$AppServicePlanNumberofWorkers, | |
[string]$AppServicePlanWorkerSize, | |
[string]$AppServicePlanTier | |
) | |
try{ | |
$ServicePlan= Get-AzureRmAppServicePlan -ResourceGroupName $ResourceGroupName -Name $AppServicePlanName -ErrorAction SilentlyContinue | |
if ($null -eq $ServicePlan) | |
{ | |
$ServicePlan=New-AzureRmAppServicePlan -Name $AppServicePlanName -Location $Location -ResourceGroupName $ResourceGroupName -Tier $AppServicePlanTier -WorkerSize $AppServicePlanWorkerSize -NumberofWorkers $AppServicePlanNumberofWorkers | |
} | |
} | |
catch{ | |
Write-Output "Cannot add serviceplan : $AppServicePlanName " | |
Write-Output $_ | |
Throw "Something went wrong" | |
} | |
return $ServicePlan | |
} | |
############################################### | |
## Check and Create Web App | |
function New-AzureWebApp { | |
param( | |
[bool]$AppServiceUse32BitWorkerProcess, | |
[string]$Location, | |
[string]$PublishProfilePath, | |
[string]$ResourceGroupName, | |
[string]$WebAppName | |
) | |
try{ | |
$WebApp = Get-AzureRmWebApp -ResourceGroupName $ResourceGroupName -Name $WebAppName -ErrorAction SilentlyContinue | |
if($null -eq $WebApp) | |
{ | |
$WebApp = New-AzureRmWebApp -Name $WebAppName -AppServicePlan $AppServicePlanName -ResourceGroupName $ResourceGroupName -Location $Location | |
} | |
Set-AzureRmWebApp -ResourceGroupName $ResourceGroupName -Name $WebAppName -Use32BitWorkerProcess $AppServiceUse32BitWorkerProcess | |
if (!(Test-Path -Path (split-path $PublishProfilePath -Parent))){ | |
throw [System.IO.FileNotFoundException] "$PublishProfilePath not found." | |
} | |
$profile = Get-AzureRmWebAppPublishingProfile -OutputFile $PublishProfilePath -ResourceGroupName $ResourceGroupName -Name $WebAppName -Format WebDeploy -Verbose | |
if ($profile){ | |
([xml] $profile).publishData.publishProfile | select publishMethod, publishurl, username, userPWD | |
} | |
else{ | |
throw "There was a problem with your publishprofile, check your webapp" | |
} | |
} | |
catch{ | |
Write-Output "Cannot add webapp : $WebAppName" | |
Write-Output $_ | |
} | |
return $WebApp | |
} | |
############################################### | |
##Step1: Define the Variables | |
$ServicePlanParams= @{ | |
ResourceGroupName = "resourcegroup" | |
Location = "NorthEurope" | |
AppServicePlanName = "AppServicePlanName" | |
AppServicePlanTier = "Basic" | |
AppServicePlanWorkerSize = "Small" | |
AppServicePlanNumberofWorkers =3 | |
} | |
$WebAppPlanParams= @{ | |
WebAppName = "wineAppDemo20170809" | |
ResourceGroupName = "resourcegroup" | |
Location = "NorthEurope" | |
TimeStamp = Get-Date -Format ddMMyyyy_hhmmss | |
PublishProfilePath = Join-Path -Path $ENV:Temp -ChildPath "publishprofile$TimeStamp.xml" | |
AppServiceUse32BitWorkerProcess=$true | |
} | |
$AzureAccountParams= @{ | |
AzureTenantId=$AzureTenantId | |
AzureServicePrincipalName=$AzureServicePrincipalName | |
AzureSPNPassword=$AzureSPNPassword | |
} | |
Add-Account @AzureAccountParams | |
New-AzureAppServicePlan @ServicePlanParams | |
New-AzureWebApp @WebAppPlanParams | |