Hello!

In the world of Azure, resources move fast. And sometimes too fast for my liking: no sooner had I automated a runbook to start some VM's that were Self-Hosted Integration Runtimes and invoke an ADF Pipeline did I discover that Microsoft announced in December that Azure Scheduler will retire on 30th of September 2019. Of course this sort of announcement was completely missed as I borrowed heavily from someone elses piece of work, because why reinvent the wheel. And so everything was good until someone who is far more dilligent than I told me about the impdending retirement of Azure Scheduler**. The apparent solution is to use Logic Apps, but seeing as I had an automation account deployed with the runbook and the runas account it made sense to change my ARM template to create some Job Schedules on the automation account instead and reference the runbook and scheule that were already created. Quite why logic apps are the touted replacement rather than automation account is a mystery to me, unless Microsoft have cancelled automation accounts as well. Perhaps I should check………. it's OK, looks like they haven't.

Right, so there's a couple of things to point out here: I don't really want my schedules running with the same frequency across my environments, mainly because ADF's can get expensive, as can running VM's, even for a little while. So with an unknown quantity of schedules and job schedules created for each environment, I should set the parameters to be an array and use the copy function in ARM to deploy the jobs and job schedules$$. So the hierarchy (not taking the runbook or permissions for the automation account) is Automation Account–>Schedule–>Job Schedule which we can sort out by using DependsOn on these resources. Simples. But if it really was that simple, this would not be much of a blog post. Below are my defaults for the schedules -

"schedules": {
    "type": "object",
    "defaultValue": {
        "schedule": [
            {
                "name": "Sunday4AM",
                "starttime": "2019-07-09T04:00:00+01:00",
                "expiryTime": "9999-12-31T23:59:00+00:00",
                "interval": 1,
                "frequency": "Week",
                "timeZone": "Europe/London",
                "isEnabled": false,
                "jobschedulename": "6e14c3e4-12f8-42a2-9fac-c4d58866368d",
                "runbookname": "runbook"
            },
            {
                "name": "Daily0005",
                "starttime": "2019-07-09T08:05:00+01:00",
                "expiryTime": "9999-12-31T23:59:00+00:00",
                "interval": 1,
                "frequency": "Day",
                "timeZone": "Europe/London",
                "isEnabled": false,
                "jobschedulename": "1dc000e3-a466-4bbe-819b-6474b07465ed",
                "runbookname": "runbook"
            },
            {
                "name": "Daily0805",
                "starttime": "2019-07-09T00:05:00+01:00",
                "expiryTime": "9999-12-31T23:59:00+00:00",
                "interval": 1,
                "frequency": "Day",
                "timeZone": "Europe/London",
                "isEnabled": false,
                "jobschedulename": "140f0180-fbce-4e3a-a966-38d52559a21d",
                "runbookname": "runbook"
            }
        ]
    }
}

…and much further down the ARM template is the deployment of the schedules and job schedules -

{
    "type": "Microsoft.Automation/automationAccounts/schedules",
    "apiVersion": "2015-10-31",
    "name": "[concat(parameters('automationAccountName'), '/', parameters('schedules').schedule[copyIndex('ScheduleCopy')].name)]",
    "dependsOn": [
        "[resourceId('Microsoft.Automation/automationAccounts/', parameters('automationAccountName'))]"
    ],
    "properties": {
        "startTime": "[parameters('schedules').schedule[copyIndex('schedulecopy')].starttime]",
        "expirytime": "[parameters('schedules').schedule[copyIndex('schedulecopy')].expirytime]",
        "interval": "[parameters('schedules').schedule[copyIndex('schedulecopy')].interval]",
        "frequency": "[parameters('schedules').schedule[copyIndex('schedulecopy')].frequency]",
        "timeZone": "[parameters('schedules').schedule[copyIndex('schedulecopy')].timeZone]",
        "isEnabled": "[parameters('schedules').schedule[copyIndex('schedulecopy')].isEnabled]"
    },
    "copy": {
        "name": "ScheduleCopy",
        "count": "[length(parameters('schedules').schedule)]"
    }
},
{
    "type": "Microsoft.Automation/automationAccounts/jobSchedules",
    "apiVersion": "2015-10-31",
    "name": "[concat(parameters('automationAccountName'), '/', parameters('schedules').schedule[copyIndex('jobschedulecopy')].jobschedulename)]",
    "dependsOn": [
        "[resourceId('Microsoft.Automation/automationAccounts/', parameters('automationAccountName'))]",
        "[concat('Microsoft.Automation/automationAccounts/', parameters('automationAccountName'), '/schedules/', parameters('schedules').schedule[copyIndex('jobschedulecopy')].name)]"
    ],
    "properties": {
        "runbook": {
            "name": "[parameters('schedules').schedule[copyIndex('jobschedulecopy')].runbookname]"
        },
        "schedule": {
            "name": "[parameters('schedules').schedule[copyIndex('jobschedulecopy')].name]"
        }
    },
    "copy": {
        "name": "jobschedulecopy",
        "count": "[length(parameters('schedules').schedule)]"
    }
}

Now I assumed that I would be able to reference the scheule like I have in the second line of the DependsOn for Job Schedules. But come to find out that this would not work. The error I got was “scheduler not found”.

New-AzureRmResourceGroupDeployment : 11:44:23 AM - Resource Microsoft.Automation/automationAccounts/jobSchedules 'bzzztautomationaccount/6e14c3e4-12f8-42a2-9fac-c4d58866368d' failed with message '{
"code": "NotFound",
"message": "Schedule not found."
}'

This was because the template is validated prior to executing against the resource providers. So although the schedule will be deployed by the template, the validation does not take the individual objects being created by the ScheduleCopy into account when validating the rest of the template. This is fair enough. But how to fix? Because the copy for both resources are using the same schedule parameter, just different properties, I can change the dependsOn for the job schedules to wait for the scheduleCopy to be completed. This is far more simplistic as really I don't need the template to create a schedule, then the corresponding job schedule, then the seconds schedule, then the corresponding job schedule etc. I just want all schedules to be completed before any job schedules need to be deployed..

"dependsOn": [
"[resourceId('Microsoft.Automation/automationAccounts/', parameters('automationAccountName'))]",
"ScheduleCopy"
],

Why Job Schedules Suck in ARM Templates

Alors, home and dry n'es pas? Malheusement, pas des tout. When deploying a job schedule, the name, which is not exposed in the UI, but is if you find the resource at resources.azure.com is a GUID. In the schedule parameter above you can see I've hardcoded a default GUID for jobschedulename property into the, um, defaults. And if you re-run a deployment you'll find out that not all resources are idempotent, because you'll get a failed deployment stating that “A job schedule for the specified runbook and schedule already exists”. No doubt Bullet, I deployyed it from this template! There is a suggestion that can be upvoted to get rid of this major annoyance. The only way around this is to delete each job schedule using Azure RM prior to the ARM depoyment, or excluding them from the tempalte altogether, running a complete ARM Deployment as opposed to incrmental, and adding them afterwards using Azure RM, the idea being that they are dropped prior to be re-added. The second option is far more risky though.

Having to manage the job schedules yourself rather than relying on the resource providers is but a small part of the general faff that is creating a scheduled task to be executed by a runbook. Maybe a post for another day.

…And Why The Documentation Sucks for Schedules

Oh! And if you look at the properties I am setting for the schedule, you'll see that the schedule is being created as disabled. This option is not documented, but if you use resources.azure.com to find a schedule on an automation account, you'll see that such a property exists and can be configured via ARM Templates. Useful, but also slightly annoying that it takes deploying a resource and then using another method to validate properties for it that either aren't documented or presented in the template that can be generated from the portal.

** As a sidebar, one of the things I like about terraform is that if a feature has been deprecated, when an apply is run it will print out that that feature has been deprecated, with a link to the GitHub issue and what to use instead. If such a feature was available in ARM then no doubt I would've spotted this immediately after I started to use the scheduler.

$$ I should perhaps clarify: you create a schedule on an automation account, and publish a runbook to the account, and then you create a job schedule that uses the schedule to, erm, schedule the execution of a given runbook.