Ran into an edge case in one of my automation projects yesterday. I have a Logic App that takes takes input from a Form and invokes an Azure Function (via Azure AD authenticated HTTP) which in turn invokes an Automation Account Runbook to create an account on-prem.
The Runbook does the actual work, but you can only talk to it via the Azure Function and that enforces Azure AD authentication. I can invoke the Azure Function directly via HTTP (which I also wrap within a PowerShell module for ease of use). Everything works well except when calling via the Logic App I hit an edge case that if the Azure Function takes more than 2 minutes to respond the Logic App times out. In general the Azure Function responds within 1.7 minutes or so, but occassionally it take longer. I am not sure why – it comes down to the Runbook taking longer to run its checks and create the account – but obviously I can’t leave things to chance like this. I need some way of making the HTTP trigger not time out.
Turns out that for Consumption plan Logic Apps the 2 minutes timeout is hard coded. If I had gone with a Standard plan the limit is 3.9 minutes, but there’s no way to move from Consumption plan to Standard without recreating it from scratch (eugh!). Plus, do I really want to do that? All I need is some way of the HTTP trigger sticking around for longer.
Thanks to Google I came across this blog post that details 3 ways of achieving it.
Option 1: The Logic App puts the request in a Queue of some sort, and the Azure Function takes the task from the Queue and returns the result to another Queue. This in turn triggers another Logic App that continues the work. You need a second Logic App as it needs to be triggered by inputs to the Queue. (And even though I say Queue for the results, it could be a SharePoint Online list or similar too I suppose…)
Option 3: Durable Functions. I don’t know much about this but I had encountered the name in the past. Didn’t sound like something I might have use for, but turns out Async HTTP APIs (which is what you call what I am trying to achieve here) is one of the things you can use it for. Plus it supports PowerShell too, so that’s good (my code is in PowerShell).
I didn’t go for this option, however, as I didn’t want to rework the Azure Function now. I have that coded and tested well, and changing that core piece to Durable Functions for this Logic App use case seemed unnecessary. Still, something to keep in mind for future projects.
And that leaves me with…
Option 2: Switch the HTTP action on the Logic App side to a HTTP Webhook action.
You can read more about this in the official docs. But the idea is that when you call an HTTP endpoint using the HTTP Webhook action the latter creates a “callback Url”and 1) it waits (for up to 2 minutes) for the HTTP end point to acknowledge the respose (so it waits for a 200 OK status code I think) and 2) additionally it waits for something to be sent to the callback Url it created. So what this means practically is that the Logic App can call an Azure Function HTTP endpoint, the Function replies with a 200 OK so the Logic App goes into waiting mode (not sure for how long, but you can set a timeout if needed), and the Function launches a new Function (before it returns with 200 OK of course) which does the actual work and responds to the callback Url with the results. The Logic App gets the results sent to the callback Url and continues with business as usual.
Clearly, this was the option best suited for me as all I had to do was create a new wrapper Function which the Logic App can call, and it will in turn invoke the existing Function which can do the real work and respond to the callback Url (if it was sent one as input). The existing Function can continue to be invoked from my PowerShell modules and other automation; only the Logic App (and any other use cases in future that have a similar issue) need invoke the wrapper Function.
Authentication
The first issue I hit upon with HTTP Webhook is how to do Azure AD authentication? I use Managed Identities, and with the HTTP action I can simply select that from a drop down (as detailed in a previous post).
There’s no similar option with an HTTP Webhook. All you get is a blank box. :)
(Notice how HTTP Webhook gives you a “Callback url” you can now put in the body. In the screenshot above that’s the only thing in the body but typically there’d be other items too. Also, the name “callbackUrl” can be anything, as long the HTTP endpoint knows that’s the property name of the callback Url).
Googling on what to do about authentication didn’t help. People were putting bearer tokens and putting it as headers. Some were doing Azure AD authentication but they were calling a Graph endpoint to get the token and then putting that. How do I do the same with a Managed Identity though?!
Then I happened to look at the raw inputs of the HTTP action that was already there and noticed it had a section like this:
1 2 3 4 |
"authentication": { "audience": "<guid of the app registration>", "type": "ManagedServiceIdentity" }, |
Hmm, maybe I put that in the blank box? No harm trying. :)
Funnily enough, that worked! That’s all you need to do a Managed Identity authentication from the Logic App using the HTTP Webhook action.
Content Type
Another oddity. On the HTTP action I was previously sending a “ContentType
” header of value “application/json”. This is so the Azure Function knows the inputs its getting is JSON (and it helpfully converts it to a hash table without you having to do a ConvertTo-Json
).
Turns out the HTTP Webhook action does not like that. I have to specify the header as “Content-Type
“. This is the correct header, so I think the HTTP action was just converting ContentType
to Content-Type
behind the scenes perhaps. Anyways, something to keep in mind else the Azure Function behaves weirdly with the input (as it doesn’t convert it to a hash table).
Authentication on the Function side
When called via the HTTP action using Managed Identity authentication the Azure Function sets two headers:
- ‘
x-ms-workflow-name
‘ (the name of the Logic App) and - ‘
x-ms-client-principal-id
‘ (the id of the Logic App)
When called via the HTTP Webhook action only the second header is set – so you don’t get the Logic App name basically. No biggie, but good to know if you are using that to log somewhere.
The wrapper Function calling the real Function with Authentication
Now onto the wrapper Function.
The wrapper Function has to call the real Function. But in my case the entire Function App is behind Azure AD authentication so I have to call it with authentication.
For this: 1) ensure you have Managed Identity turned on for the Azure Function app (pretty sure it’s turned on already if you Managed Identities to authenticate with Key Vaults, Storage Accounts etc. – best practices), and 2) uncomment the following lines in profile.ps1
of the Function app:
1 2 3 4 |
if ($env:MSI_SECRET) { Disable-AzContextAutosave -Scope Process | Out-Null Connect-AzAccount -Identity } |
Then, in the actual code get a bearer token using Get-AzAccessToken
:
1 2 3 4 5 |
$azToken = (Get-AzAccessToken -ResourceUri '<guid of app registration>').token $headers = @{ "Authorization" = "Bearer $azToken" } |
The ResourceUri above (which is the GUID of the App Registration associated with the Azure Function for Azure AD authentication) is what you need a bearer token for. This gets set in the audience claim. (You can get this GUID from the authentication section of the Function App too – in the allowed token audiences section).
This token can be set as a bearer token header and the real Function ($functionUrl
below) can be invoked.
I put this under a try/catch
block thus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
try { # Invoke $functionUrl and send an OK back. # If we ran into any error invoking $functionUrl this try/ catch will fail. # We don't wait for the output of $functionUrl as it will reply to the callback Url directly. Invoke-WebRequest -Uri $functionUrl -Headers $headers -Body ($RequestObj | ConvertTo-Json -Depth 5) -ContentType $contentType -UseBasicParsing -ErrorAction Stop Write-Host "Successfully invoked $functionUrl" # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = "Successfully invoked $functionUrl" }) } catch { $statusCode = $_.Exception.Response.StatusCode.value__ $errorReason = $_.Exception.Response.ReasonPhrase Write-Host "Error invoking $functionUrl - $errorReason" # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::BadRequest Body = "Error invoking $functionUrl - $statusCode - $errorReason" }) } |
The wrapper Function doesn’t need to wait for any results from the real Function. All it does is invoke and return with a 200 OK. But if the invocation fails for any reason immediately, the catch
block kicks in and the wrapper Function return an error – this way the Logic App knows something went wrong. It’s very simple that way.
Getting the Function Url
It makes sense to have the same code for all my wrapper Functions. All they do is call the real Function after all. The only thing that varies is the name of the real Function ($functionUrl
above).
What I do is name the wrapper Functions as <NameOfTheRealFunction>_Wrapper
. Then I can get the name of the real Function to call this:
1 |
$functionUrl = $Request.Url -replace '_Wrapper' |
Turns out the $Request
parameter has a Url
property which contains the Url of the Function as it was called.
Invoke-WebRequest vs Invoke-RestMethod
The wrapper Function can invoke the real Function via Invoke-WebRequest
or Invoke-RestMethod
.
1 2 3 |
Invoke-WebRequest -Uri $functionUrl -Headers $headers -Body ($Request.Body | ConvertTo-Json -Depth 5) -ContentType $contentType -UseBasicParsing -ErrorAction Stop Invoke-RestMethod -Uri $functionUrl -Headers $headers -ContentType $contentType -Body $Request.Body -ErrorAction Stop |
Not sure why, but when I invoke it via Invoke-RestMethod
the body I send is set under the Query
property rather than the Body
property in the receiving Function. Something to be aware of.
Update (16th Nov 2022): See this post for something extra.
Update (23rd Nov 2022): See this post too.