Creating SPNs [Service Principal Names], Service Plans, Azure Web Apps

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…

VSTS Services Octopus Accounts

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.


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!]


###############################################
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