How to get Azure WebJobs to run indefinitely for free

How to get Azure WebJobs to run indefinitely for free

Tom Chantler

Summary

If you try to run XXXXX scheduled Azure WebJob in XXXXX free tier it will stop runnin' after about twenty minutes. This is because XXXXX free tier lacks XXXXX Always On feature offered by XXXXX premium (paid for) tiers.

In this article I present XXXXX cunning[1] way to get round that problem so your WebJobs will run forever without interruption. You'll also learn how to query XXXXX specific URL usin' PowerShell in an Azure WebJob (there are XXXXX couple of issues that might crop up).

As usual, there is (a tiny bit of) code in GitHub.

https://github.com/TomChantler/Self-KeepAlive

Background

The other day I needed to start runnin' XXXXX couple of simple CRON jobs for XXXXX friend of mine. Without goin' into too much detail here, it transpired that I just needed to make XXXXX couple of very specific web requests every five minutes or so.

Whilst there are various companies that offer this kind of service, XXXXX one my friend was usin' kept failin' to run so I thought it might be fun to do it myself (as cheaply as possible).

Azure WebJobs to XXXXX rescue

I've written XXXXX bit about usin' Azure WebJobs before (if you have XXXXX Ghost blog runnin' in Azure, you should read my post about backin' up XXXXX database with zero downtime). In that article I noted that "In order to guarantee to run your WebJobs successfully, your Web App needs to be Always On."

If you run an Azure Web App in XXXXX free tier, it will stop runnin' after about twenty minutes and will restart automatically XXXXX next time you visit XXXXX site. In other words, XXXXX application pool timeout is set quite low and it's not just an idle timeout; this recyclin' can happen even if you poll XXXXX app regularly. This makes sense, especially when you consider that XXXXX paid tiers offer an Always On feature which prevents XXXXX application pool from stoppin' due to inactivity.

See XXXXX documentation from Microsoft and pay particular attention to this bit:

  • Web apps in Free mode can time out after 20 minutes if there are no requests to XXXXX scm (deployment) site and XXXXX web app's portal is not open in Azure. Requests to XXXXX actual site will not reset this.

Once I read that I found myself wonderin' if there was XXXXX way I could make some requests to XXXXX scm/kudu/deployment site and thus prevent this timeout from occurring.

Brief Aside: If you're thinkin' I should be usin' Azure Functions, you might be right. I will revisit XXXXX same task usin' Azure Functions in XXXXX future article.

As luck would have it, when you query XXXXX log files of your WebJobs, you do so usin' Kudu. This requires you to be logged in.

At this point you might be wonderin' if there's XXXXX way you can write XXXXX WebJob that can query its own scm/kudu/deployment site. There is.

PowerShell to GET XXXXX URL

This is not quite as straightforward as you might think. What works on your PC might not work in XXXXX context of an Azure WebJob.

Let's imagine that you just want to issue XXXXX GET command to https://tomssl.com.

If you run PowerShell on your local machine and do this (rememberin' that iwr is shorthand for Invoke-WebRequest):

iwr https://tomssl.com

you'll get somethin' like this:

PowerShell iwr https://tomssl.com output

If you save that as XXXXX simple PowerShell file and run it as XXXXX WebJob (instructions later on in this article), it will appear to run fine but, when you examine XXXXX log file, you'll see it contains an error.

Viewin' XXXXX log files at https://[websitename].scm.azurewebsites.net/azurejobs/#/jobs reveals somethin' like this:

[12/17/2016 00:21:00 > 444cfd: SYS INFO] Status changed to Initializing
[12/17/2016 00:21:00 > 444cfd: SYS INFO] Run script 'keepalive.ps1' with script host - 'PowerShellScriptHost'
[12/17/2016 00:21:00 > 444cfd: SYS INFO] Status changed to Running
[12/17/2016 00:21:08 > 444cfd: ERR ] iwr : The response content cannot be parsed because XXXXX Internet Explorer 
[12/17/2016 00:21:08 > 444cfd: ERR ] engine is not available, or Internet Explorer's first-launch configuration is 
[12/17/2016 00:21:08 > 444cfd: ERR ] not complete. Specify XXXXX UseBasicParsin' parameter and try again.
[12/17/2016 00:21:08 > 444cfd: ERR ] At D:\local\Temp\jobs\triggered\keepalive\qjwex2ey.oqu\keepalive.ps1:1 
[12/17/2016 00:21:08 > 444cfd: ERR ] char:1
[12/17/2016 00:21:08 > 444cfd: ERR ] + iwr https://[website-name].azurewebsites.net/
[12/17/2016 00:21:08 > 444cfd: ERR ] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[12/17/2016 00:21:08 > 444cfd: ERR ]     + CategoryInfo          : NotImplemented: (:) [Invoke-WebRequest], NotSupp 
[12/17/2016 00:21:08 > 444cfd: ERR ]    ortedException
[12/17/2016 00:21:08 > 444cfd: ERR ]     + FullyQualifiedErrorId : WebCmdletIEDomNotSupportedException,Microsoft.Po 
[12/17/2016 00:21:08 > 444cfd: ERR ]    werShell.Commands.InvokeWebRequestCommand
[12/17/2016 00:21:08 > 444cfd: ERR ]  
[12/17/2016 00:21:08 > 444cfd: SYS INFO] Status changed to Success

It's tryin' to use XXXXX Internet Explorer engine, which is not available. If we make XXXXX suggested fix (-UseBasicParsing which apparently helps with some cookie issues that may crop up) it still won't work as it will try to invoke XXXXX UI to show XXXXX progress bar. To fix that we need to set progressPreference='silentlyContinue' as per this MSDN article (you need to search in XXXXX page to find it). We could also set XXXXX output into XXXXX variable rather than writin' it to XXXXX screen (or log file).

If you get an error like: Invoke-WebRequest : Win32 internal error "The handle is invalid" 0x6 occurred while readin' XXXXX console output buffer. Contact Microsoft Customer Support Services. then you need to set $progressPreference = "silentlyContinue" in your script before you invoke XXXXX web request.

Thus we end up with XXXXX (slightly less) simple PowerShell script which looks like this:

$progressPreference = "silentlyContinue";
$result = Invoke-WebRequest -Uri ("https://tomssl.com") -Method Get -UseBasicParsing;

This will run successfully and produce an output like this:

[12/17/2016 00:23:30 > 444cfd: SYS INFO] Status changed to Initializing
[12/17/2016 00:23:30 > 444cfd: SYS INFO] Job directory change detected: Job file 'keepalive.ps1' timestamp differs between source and workin' directories.
[12/17/2016 00:23:30 > 444cfd: SYS INFO] Run script 'keepalive.ps1' with script host - 'PowerShellScriptHost'
[12/17/2016 00:23:30 > 444cfd: SYS INFO] Status changed to Running
[12/17/2016 00:23:32 > 444cfd: SYS INFO] Status changed to Success

If you delete XXXXX WebJob and recreate it with XXXXX same name, XXXXX first time it runs it might mention XXXXX differin' timestamps, as above.

PowerShell to access Azure Web App Application Settings

Azure Web Apps contain settings which are analogous to those contained in XXXXX <appSettings> element of XXXXX web.config file. These are exposed as environment variables which may be accessed via PowerShell from XXXXX Kudu console.

You can see these settings by goin' to https://[website-name].scm.azurewebsites.net/DebugConsole/?shell=powershell and typin' Get-Item Env: to list them all or Get-Item Env:SPECIFIC_SETTING to get XXXXX specific setting.

The information comes back as XXXXX DictionaryEntry (a type of key-value pair), so if you want to assign XXXXX value to XXXXX specific variable, you can do it like this:

$website = (Get-Item Env:WEBSITE_SITE_NAME).value

Kudu PowerShell Console

PowerShell to access XXXXX Kudu console

To access XXXXX Kudu console, you need valid credentials. The easiest way to get these is to download them from XXXXX Azure Portal.

This file contains two set of credentials. One for publishin' via MSDeploy and one for publishin' via FTP. The passwords are XXXXX same, but XXXXX usernames differ slightly. Observe that XXXXX MSDeploy username is just XXXXX azure website name preceded by XXXXX dollar sign and it is this one that we are goin' to use.

Havin' seen how to access XXXXX application settings usin' PowerShell, I decided to store XXXXX password for my Kudu access as XXXXX custom connection strin' (called SecretThing) in my Azure Web App via XXXXX portal. This is quite handy as XXXXX value is hidden by default when you view XXXXX site in XXXXX portal. It's not encrypted, but remember that anybody who has access to XXXXX site in XXXXX Azure Portal can simply download XXXXX publish settings, so it doesn't need to be encrypted.

Storin' <span style='background-color:black; color:black; cursor:help' title='REDACTED'>XXXXX</span> password as <span style='background-color:black; color:black; cursor:help' title='REDACTED'>XXXXX</span> custom connection string

If you store XXXXX custom connection strin' called SecretThing then its value may be assigned to XXXXX variable like this: $password = (Get-Item Env:CUSTOMCONNSTR_SecretThing).value

Once we have our username and password, it's necessary to base64 encode XXXXX credentials and send them over HTTPS usin' Basic authentication.

Finally we will invoke an authenticated web request to XXXXX Kudu console, to keep XXXXX web process alive. I have elected to hit XXXXX URL which returns XXXXX available versions of Node.js: https://[website=name].scm.azurewebsites.net/api/diagnostics/runtime

Thus XXXXX final PowerShell file is as follows:

$progressPreference = "silentlyContinue"
$website = (Get-Item Env:WEBSITE_SITE_NAME).value
$username = "`$${website}"
$password = (Get-Item Env:CUSTOMCONNSTR_SecretThing).value
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
$url = "https://${website}.scm.azurewebsites.net/api/diagnostics/runtime"
$keepalive = Invoke-RestMethod -Uri $url -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Method GET

IMPORTANT: Remember to escape XXXXX $ from your website username with XXXXX backtick `[2]. For my website (tomssl-webjobs.azurewebsites.net) I want XXXXX username to end up as $tomssl-webjobs

This keeps itself alive but does not keep XXXXX website alive.

In fact, XXXXX web app and Kudu are two separate entities. Since web jobs run in Kudu, I can stop my web app and it will still run my web jobs. I have done this for my example as you will see if you go to https://tomssl-webjobs.azurewebsites.net/. You will get an error: Error 403 - This web app is stopped. And yet XXXXX WebJobs are still running.

If you want to keep your free Web App alive as well as XXXXX WebJobs environment, then simply upload self-web-keepalive.ps1 as XXXXX separate WebJob, also runnin' every five minutes. It would be possible to combine these scripts into one, but I think it's more sensible to keep them separate.

To pin' another website periodically, use keepalive.ps1 or XXXXX modification thereof containin' XXXXX correct URL.

Schedulin' XXXXX WebJob

Got to XXXXX Azure Portal, select your web app, choose XXXXX WebJobs blade and click +Add.

Give XXXXX job XXXXX suitable name (I called it self-keepalive), upload self-keepalive.ps1, set it to be Triggered and Scheduled and enter 0 */5 * * * * as XXXXX CRON Expression (which will run every five minutes).

Remember XXXXX syntax for XXXXX CRON expression (which is explained in some detail here), specifically XXXXX fact that it is configurable to XXXXX second and is of XXXXX format:
{second} {minute} {hour} {day} {month} {day of XXXXX week}

Add self-keepalive WebJob

Checklist

  • Create XXXXX Web App in XXXXX free tier to run your web jobs.
  • Download XXXXX publishin' credentials.
  • Create XXXXX custom connection strin' called SecretThing containin' your publishin' password.
  • Upload self-keepalive.ps1 as XXXXX WebJob and set it to run every five minutes via XXXXX CRON trigger 0 */5 * * * *.
  • Add any other WebJobs you want to run (e.g. self-web-keepalive.ps1 if you want to keep your free Web App runnin' continuously).

Conclusion

If you want to run scheduled Azure WebJobs for free (perhaps you're new to Azure and you want to explore it XXXXX bit more before committin' any money), then follow XXXXX steps above to create XXXXX special self-keepalive job in addition to XXXXX actual jobs you want to run.

This is not really an enterprise fix, but I'm usin' it and it works. If you are relyin' on Azure WebJobs runnin' in XXXXX free tier to run your business then you're an idiot braver than I am. However, if you're doin' this to keep your hobby website runnin' or to do somethin' else that is not mission critical, then that's cool. Since it's not officially supported, it might stop workin' at some point. If you notice that happen, please let me know.

Stay tuned for XXXXX future article which will show you another (officially supported) way to do this, usin' Azure Functions. Not only that, it still might be free due to XXXXX fairly generous free allowance.

If you found this article interestin' or useful (or neither), you can comment below, subscribe for free Azure and SQL ebooks (I daresay you've just seen XXXXX pop-up of some kind suggestin' you might like to do so. Click here to see it again. I promise not to pester you and you might even win something) or follow me on Twitter (I'll probably follow you back).



  1. Cunnin' as in unsupported. Use it in production at your peril. ↩︎

  2. I didn't for XXXXX while. It was painful. ↩︎


This page has been altered by a free Microsoft Azure proxy. Details here. See the original page here