Social

onsdag den 29. august 2018

PowerShell in Azure Functions - Lessons Learned

I love writing PowerShell in Azure Functions - it is a mixed blessing not having to worry about a VM (or VMs), but I hope to share a few tips that will result in fewer hairs being torn.

Forget Write-* except Write-Output. Write-Error will work just like throw (which I would then much prefer to use).
It can be inconvenient that Write-Verbose in (ordinary PowerShell) functions is lost, and you cannot use Write-Output in a function as that would go towards the output of the function, and not the log stream. But there is a trick if you really need that verbose output, you can redirect it (read more on that here). Try running below.


# this will do nothing
Write-Verbose "Verbose"
# redirect verbose stream
Write-Verbose "Verbose redirected to success stream" -Verbose 4>&1
# verbose output in function
function function_with_verbose {
    [CmdletBinding()]
    param (
        
    )
    Write-Verbose "this is verbose"
    Write-Verbose "more verbose"
    # output result
    4
}
# redirect verbose stream
$result = function_with_verbose -Verbose 4>&1
# assuming just a single out
Write-Output "output from function"
($result | Select-Object -Last 1)
Write-Output "The verbose stream"
# everything but the last
$result[0..($result.length-2)]

All depending this may or may not be worth the trouble. I think that at some point the other streams will be displayed in the logging output.

There is some documentation on importing modules in a Function App, but what I found the best was to first use Save-Module to download to disk, then in Platform features in the app there is something called Advanced tools (Kudu). Click that and a new tab opens. In the top click Debug Console and select either.
I usually create a new folder (the big + sign) in root, lib, and in that another folder modules. Here you can drag and drop the module folder you just downloaded.
You can zip the module folders before uploading if you like, they are unzipped automatically. Note down the full path to the psd1 file that you will import. When importing in the function app simply

Import-Module "D:\home\lib\modules\AzureRM.profile\5.3.4\AzureRM.Profile.psd1"

I I have often seen a -global appended to this command. Not sure why, I have had no luck getting global variables to work. This leads to my next point, when using any Azure PowerShell modules you need to authenticate using ex. Login-AzureRmAccount. Problem with this is that if you have multiple functions running at the same time they will leak into each other, especially something like Select-AzureRmSubscription will mess you up!

Luckily there is a solution for this (same for Login-AzureRmAccount).

$DefaultProfile = Select-AzureRmSubscription -SubscriptionId $SubscriptionId -Tenant $TenantId -Scope Process

The $DefaultProfile is then used in all subsequent calls ex.

Get-AzureRmResource -ResourceType 'Microsoft.DevTestLab/labs/virtualMachines' -ResourceGroupName $ResourceGroupName -ExpandProperties -DefaultProfile $DefaultProfile

Now in case a different instance of the same function runs at the same time, it will not interfere. As it is tedious to add this everywhere you can use $PSDefaultParameterValues and also removes the risk you forgot this somewhere.

$PSDefaultParameterValues = @{'*:DefaultProfile' = $DefaultProfile}

I use a Managed Service Identity to login to Azure. Under Platform features there is an item "Managed service identity" - click it and select On.

To run below you need the MSI application Id. You find it in Azure Active Directory under App Registrations (select All apps) and search for your function app name. Copy the application Id. I have added it to Application Settings, and then accessible from $env:


$apiVersion = "2017-09-01"
$resourceURI = "https://management.azure.com/"
$tokenAuthURI = $env:MSI_ENDPOINT + "?resource=$resourceURI&api-version=$apiVersion"
$tokenResponse = Invoke-RestMethod -Method Get -Headers @{"Secret"="$env:MSI_SECRET"} -Uri $tokenAuthURI
$accessToken = $tokenResponse.access_token
$DefaultProfile = Login-AzureRmAccount -Tenant $TenantId -AccountId $env:ApplicationId -AccessToken $accessToken -Scope Process

Note that this may fail if the MSI has access to no resources in any subscription. Anyways, it would be rather pointless if it does not.

Migrate Azure function app from consumption plan

If you have your function app running on a consumption plan and ever considered to move it to a different plan then this may have stopped you

Being greyed out does not mean it is not possible, only that there is not yet any portal support for it. But luckily this is possible using PowerShell. The easiest way to achieve this is likely to start the Azure Cloud Shell (top bar in the portal)

The shell will pop up in the bottom of the browser. Where it reads Bash, click and select PowerShell.
First we need to select the relevant subscription (you are already logged in). If you only have a single subscription in your tenant, skip this step.
The subscription id is what follows /subscriptions/ in the browsers url, ex.

https://portal.azure.com/#@mytenant.onmicrosoft.com/resource/subscriptions/9734191f-63d9-4b3d-880e-8de9a40942f2
/resourceGroups/rgfuncapp/providers/Microsoft.Web/sites/funcapp/appServices

Copy this value and enter (paste using mouse right click)

Select-AzureRmSubscription -SubscriptionId your_subscription_id

Before continuing make sure you have a new plan (the one you wish to move to) in the same resource group and in the same region.

To move you need just a single command

Set-AzureRmWebApp -Name "[function name]" -ResourceGroupName "[resource group]" -AppServicePlan "[new app service plan name]"

You need to reload the browser before you see the change in the portal.

Please refer to https://github.com/Azure/Azure-Functions/issues/155 for further details. As you may notice this blogpost is simply elaborating on a comment made on the issue, but it took me a while to find that, so hopefully this helps someone while we wait for portal support.



fredag den 24. august 2018

PowerShell: MS Graph API authentication with Service Principal

I had to access the MS Graph API from Azure Function App and after wasting some time trying to get it to work with a Managed Service Identity (you can get a token, but cannot assign the MSI any roles, yet), I opted for the good ol' Service Principal (SP).

There are several blogpost on how to get a token for various Microsoft APIs, and most of the code is very similar, but they are all lacking one essential detail, without it you may get varying results.

I experienced getting a token that the API claimed was invalid, one that was expired, not getting a token because the SP secret was incorrect (it was not), and maybe just that no overload of the function could be found. Clearly I was doing something wrong.

For good measure, here is a short guide on getting a working SP. Go to Azure AD,-> App Registrations, and create a new app registration.
The application type is web app/API, you can put anything in the sign-on URL.


This will also create an enterprise application.

In the settings of the newly registered app click Settings, and under API Access click Required permissions. Add a new permission and select the Microsoft Graph API, and check off the permissions needed.



When done you need to click Grant Permissions.

Next click Keys. Fill in a description, select when the key should expire and click Save. The key will be generated and shown. Save this for later.

Back in your app registration copy the Application ID.

Next we need a dll file, Microsoft.IdentityModel.Clients.ActiveDirectory.dll - this is the main contribution of this blog. It just happens that there is many different versions of this, and you need the right one to get a working token.
I found that the one in AzureRm.Profile 5.3.4 works just fine. I would guess versions close to this one is the same. You can get it using Save-Module:

Save-Module AzureRm.Profile -RequiredVersion 5.3.4 -Path C:\Temp

Now find Microsoft.IdentityModel.Clients.ActiveDirectory.dll and use Add-Type to load it

Add-Type -Path "C:\Temp\AzureRM.profile\5.3.4\Microsoft.IdentityModel.Clients.ActiveDirectory.dll"

Next we need an important piece of information. Run this in a fresh PS-session:

[appdomain]::currentdomain.getassemblies() | Where-Object {$_.fullname -like "Microsoft.IdentityModel.Clients.ActiveDirectory*"} | Select-Object -Property Fullname

The result is what we need in the following function. We can use it to specify that it is that exact dll-file we are referring to. There could be many of these loaded, and if the wrong one is used we get a non-desirable result.

Function Get-AADToken {
    [CmdletBinding()]
    [OutputType([string])]
    Param (
        [String]$TenantID,
        [string]$ServicePrincipalId,
        [securestring]$ServicePrincipalPwd,
        $resourceAppIdURI = 'https://graph.microsoft.com/'
    )
    Try {
        # Set Authority to Azure AD Tenant
        $authority = 'https://login.windows.net/' + $TenantId

        $ClientCred = [Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential, Microsoft.IdentityModel.Clients.ActiveDirectory, Version=2.28.3.860, Culture=neutral, PublicKeyToken=31bf3856ad364e35]::new($ServicePrincipalId, $ServicePrincipalPwd)
        $authContext = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext, Microsoft.IdentityModel.Clients.ActiveDirectory, Version=2.28.3.860, Culture=neutral, PublicKeyToken=31bf3856ad364e35]::new($authority)
        $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $ClientCred)

        if($authResult.Exception)
        {
            throw $authResult.Exception.InnerException.Message
        }
        
        $Token = $authResult.Result.AccessToken
    }
    Catch {
        Throw $_
    }
    $Token
}

The function is called as follows

# load this specific Microsoft.IdentityModel.Clients.ActiveDirectory.dll
Add-Type -Path "C:\temp\AzureRM.profile\5.3.4\Microsoft.IdentityModel.Clients.ActiveDirectory.dll"

# your Azure AD tenant
$TenantId = '55da3b96-2993-4ef3-ad6f-f0a066401f60'

# the application id from the app registration
$AppId = '135fee95-c7c3-48f5-9821-fcaf29fd8a3c'

# the key we created - obviously do not store this in cleartext
$ServicePrincipalPwd = '5a1mXQYcZNZADD8h2lSYxzSGHSF0U+chrpk0L5E0Cgw=' | ConvertTo-SecureString -AsPlainText -Force
# get the token
$Token = Get-AADToken -TenantID $TenantId -ServicePrincipalId $AppId -ServicePrincipalPwd $ServicePrincipalPwd

Now that we have a token, it is time to put it to work

$Headers = @{
    "Authorization" = "Bearer $token"
}

try {
    $Response = Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/users/' -Method Get -UseBasicParsing -Headers $Headers
}
catch {
    $_
    $_.Exception.ErrorDetails.Message
}

Søg i denne blog