Stop and Start Azure VMs using an Office 365 Calendar

Recently I was challenged to provide a solution to delegate VM stop and start control to a group of developers who needed the VMs on demand. The developers are not trained in using the Azure Portal.

At first, I suggested to use the available Azure Marketplace solution based on Automation and schedules and explained to the customer how the developers could leverage this to stop and start VMs.

  • The conclusion was that this method was way too advanced to explain and use effectively and a simpler solution was requested.
  • Requirements for the solution:
  • Developers need to stop and start VMs to make them available when needed.
  • The solution must be easily accessible for the developers.
  • Extra education or learning-curve must be kept to a minimum.
  • Developers’ rights in the Azure Subscription and its resources must be kept to a minimum.
  • The solution must be secure.
  • The solution must be manageable by the developers themselves.
  • If money can be saved, please do so.

And then it hit me:

What if we could create a (shared) calendar of some sorts and control Stop and Start behavior based on calendar appointments for VMs? Everyone knows how to use Outlook and Calendars.
Could we create a script or workflow that somehow reads a calendar with appointments that have VM names in their subjects and only have VMs running during the appointed time? PowerShell?
Will this solution meet all the requirements?

Let’s find out!

To be able to build this solution, you will need an Azure Active Directory, an Office 365 Subscription, and an Azure Subscription.

Let’s start with the basics:
Create a shared mailbox so we have a license free Calendar which we can share with the developers (if money can be saved, please do so).
Log in to the Office 365 Exchange Admin panel and browse to Recipients and select ‘Shared’. Click the ‘+’ icon to create a new shared mailbox.

When it’s created, double-click and make a note of the email address. You will need it later.

While you’re here, also click “mailbox delegation” and select anyone who will need to use this shared calendar.

Note: This is a quick and dirty setup for a shared mailbox. You could also just share the calendar. Doing it this way however will automatically make the mailbox appear in Arjan Mensch’s Outlook but on the flip side will also grant him full blown mailbox rights: he can send and read mail for instance.
Configure following your company’s guidelines on Shared Mailboxes and you’ll be fine.

We’re done on the Office 365 side of things.

We’re going to use Graph to access the calendar’s items, so go ahead and check out Graph if you haven’t:
I really like the Graph Explorer ( to test out Graph URIs and rights. Here’s a screenshot showing the shared mailbox’s calendar events:

This works because I signed in using the Arjan Mensch identity and apparently, I have enough rights to read ‘’ Calendar Events.

The object is to create a PowerShell script that will read the events so we’ll need an identity or an account that can be used by the script and that has access to the mailbox as well.

In comes Azure Active Directory.

Let’s first create an identity that has the necessary rights to read calendar events.
Login to the Azure Portal with a user that has enough rights to manage the Azure Active Directory, or click the “Azure Active Directory” button in the Office 365 Admin portal:

Navigate to “Azure Active Directory”, click “App Registrations”, and click “New registration”.

Name the Application, leave the default for “Support account types”, leave “Redirect URI” blank because a Redirect URI is not needed, and press “Register”.

Copy the Application (client) ID, you will need it later.

Still in the application’s blade, click “Certificates & secrets” and then click “New client secret”.

I named the secret “Powershell” because I am going to use PowerShell to use this identity, but it doesn’t matter, this name is for reference only. I chose to never expire the secret, but you might want to consult your security policy on that.
What’s important is that you copy the Client Secret. This is the only time you will see it and you won’t be able to retrieve it. If you lose it, you will need to create a new client secret.

Now click “API permissions”.

Lo and behold! Graph permissions! How convenient. By default, the app will request “User.Read” permissions when a user accesses the application for the first time. We are going to change that. Users will never use this app, it will be used by a background process, and we want to make sure the appropriate rights are already on the application.

Click “Add a permission”.

Click “Application permissions”, browse to Calendars and select “Calendars.Read”. Click “Update permissions”.

Notice the notice when you click the Update button? We’ll get to that.

First, delete the “User.Read” permission.

Click “User.Read”, and then click “Remove permission”. Click “Yes, remove” because we’re sure.

Remember that notice? Click “Grant admin consent for …” and click “Yes” after that.

Our application now has permissions to read calendar events for accounts in Azure Active Directory.


We have a calendar that is shared. We have an identity in Azure Active Directory that has access to that calendar.

Now we need a PowerShell script to use the identity to read the calendar events and tell Azure to de-allocate and start VMs according to the events.
How can we run the PowerShell script with the required permissions to do just that? Well, as it happens, we can use Azure services to do that.

If you don’t have an Automation account in your subscription yet, now is the time to create one.

In the Azure Portal click “Create a resource”, search for “automation”, click “Automation”, and click “Create”.

Give the account a name. Select an existing Resource Group or create a new one.
Make sure “Create Azure Run As account” is enabled, as this will be the identity we will be using to de-allocate or start VMs.
Remember this Automation Account name and the Resource Group name you used here.
Click “Create” to create the automation account and the Run As accounts. Yes “accounts”, as is multiple accounts. It will create a Legacy Run As account as well, which can be used to control Classic Resources. Since we will not need it, we’ll go ahead and delete it.

Go to the newly created Automation Account.

Scroll down to “Run as accounts” and click it. Select the “Azure Classic Run As account” and click “Delete”.
Note: The remaining Run As account has a validity equal to the lifespan of the certificate that was created with it. It’s probably a good idea to note this date somewhere and make sure you renew the certificate before it expires, or this solution will stop working.

If you need to renew the certificate simply click on the Run As account and click “Renew certificate”.

Before we continue building the solution, let’s make sure we are ready for testing the solution.
For this you need to have a VM in this Azure subscription, and of course, you need all the previous steps in place as well.
I am going to use tags to be able to limit a calendar’s control span.

I will use the VM named “VMT001” for this. I added a Tag named “AutomatedStopStart” and gave it a value matching the email address of the calendar I shared in the previous steps.
This will be the method used to identify calendar events to a VM: the event will have the VM’s name as a subject, and the calendar will only be able to control VMs that have an “AutomatedStopStart” tag with a value matching the calendar’s identity, and no other VMs at all. You don’t want your developers to be able to stop Domain Controllers or any other VMs that are assigned to them.
The VM’s status should be “Stopped (deallocated)”.

Now we need to create a PowerShell runbook in the Automation Account and schedule it.

Download the PowerShell script here:

Breaking down the PowerShell script:

The script needs 3 parameters to run.
One or more calendars to read, the ApplictionId and the ApplicationSecret needed to access and use the identity with the necessary Graph permissions.
To get a sense of runtime for the script we output the starting time (and end with outputting the ending time).
Variable declaration is in the beginning of the script as well. When the script’s magic is done, the startVMs array will hold all the VMs that need to be running, and the stopVMs array will hold all the VMs that need to be stopped.

This piece of code connects to the Run As account for the Automation Account and creates an array containing all the VMs in the subscription that can be controlled.
To be able to control a VM the VM needs to have a tag attached to it named “AutomatedStopStart”. VMs without this tag present will not be processed.

To be able to execute Graph queries we will need an access Token. These lines acquire a token for the application identity that has the Graph permissions to read Calendars.

This is where the actual processing of the calendar events happens. Using a foreach loop we process al calendars.
During processing the Graph query is executed to find events in the calendar for this moment.
An event found means we need to have a VM in a running state. A VM for which the name matches the subject of the event found. The subject is added to the processVMs array.
When all events are processed, we add the VMs that need to start to the startVMs array, and the VMs that need to be de-allocated to the stopVMs array.

When all calendars are processed the startVMs and stopVMs arrays need to be processed.

Note that I am not a PowerShell guru, but this script gets the job done nicely. I welcome suggestions for improving or simplifying the code.
Also note that there’s no error handling in the code at all. If anything fails, it simply tries again next run.

With that explanation it’s time to add this script as a PowerShell Runbook.

Find your Automation Account in the Azure Portal and click on “Runbooks”.
It seems creating an Automation Account also creates several tutorial runbooks. I don’t need them and it’s safe to delete them.

Whether you delete them or not is up to you. Either way, click “Create a runbook”.

Name your runbook and make sure the type is set to “PowerShell”.

And this is how a new runbook looks like. There are no Jobs, Schedules or even Code yet.
Click “Edit”.

Now copy the code from the PowerShell script and paste it in the editor pane.

Hit the “Save” button, and then click the “Test pane” button. It’s time to test the workings of the solution.

Fill in the Calendars array (comma separated if you have more than one), and the ApplicationId and ApplicationSecret for the application you created in Azure Active Directory.
If you look at the screenshot, there’s a bunch of errors. That’s not good. For some reason, when you create a new Automation Account, it doesn’t have the required (version of) PowerShell modules.
There’s a fix for that. Note that you only need to run the fix if you these errors.

Fix for PowerShell module errors:
Go back to your Automation Account.

Scroll down to “Modules”, click it and then click “Update Azure Modules”.
Next challenge awaits! So apparently that convenient little button is depreciated. It does tell you the solution though: create a new PowerShell runbook, copy the Update code from GitHub and run that runbook.

So, copy the code from

Still in your Automation Account, scroll to “Runbooks”, and click “Create a runbook”.

Name the runbook and make sure it’s a “PowerShell” type runbook.

Paste the code from the GitHub link, click “Save”, and click “Test pane”.

Enter the Resource Group name and the Automation Account name for your Automation Account and Click “Start”. This runbook will run for about 5 minutes in which it updates all PowerShell modules.
When this is done, we can go back to testing our runbook.

Go to the runbook, click “Edit”, and then click “Test pane” again. Fill in the parameter fields again, and click “Start”.

Now your output should resemble what is shown in the screenshot, which tells us we successfully used the Run As account to enumerate the tagged VMs, we successfully authenticated to and queried Graph, but no VM state changes were required.
To actually test the functionality of the VM state change code, go ahead and create an appointment in the shared calendar using the VM’s name as a subject, and make sure the appointment is now:

Run the test again.

The script detects the event and says it’s started the corresponding VM.

The Azure Portal agrees. The VM is now running.

Delete the appointment from the calendar and test the script again.

The script now de-allocates the machine since the VM’s state was “Running” without having an appointment in the shared calendar.

Again, the Azure Portal agrees.

With the script tested and approved, close the Test pane, or click the “Edit PowerShell Runbook” link in the breadcrumbs returning you to the Edit page.

Click the “Publish” button this time. This will save the Runbook with the published state to your Automation Account so we will be able to schedule it.

Notice the status for the Runbook is now “Published”. Click “Link to schedule”.

Since this is a new Automation Account I don’t have any schedules yet.
Click “Link a schedule to your runbook”, click “Create a new schedule”.
Name the schedule, select a time to run (note: this must be at least 5 minutes into the future to be valid), select a timezone that matches the shared calendar’s time zone, and set the schedule to be recurring.
Since schedules cannot run more often than once an hour, we’ll need to create multiple schedules to run once an hour and link those to the Runbook as well.
Click “Create” to create and link the schedule.

Click “Parameters and run settings” and enter the parameter values just as you did when running the script in the “Test pane”. Note that you can tell the script to process multiple calendars by entering them comma separated in the Calendars parameter.
Click “OK” to accept the parameters and click “OK” again to assign the schedule to the runbook.

Repeat the schedule linking and creation steps if you need the Runbook to process the shared calendars more often than once an hour.
Here I created four schedules effectively running the Runbook every 15 minutes.

If you want to see how your schedules are doing, you can go back to the Runbook anytime and see the associated jobs.

Clicking on a job entry will show you details for that job. You can see what parameters are used (Application Secret exposed!), the job’s output as we’ve seen it in the Test pane, and any errors or warnings.

And that’s it. Let’s review the requirements for this solution:

  • Developers need to stop and start VMs to make them available when needed.
    Stopping and starting VMs happens based on calendar events in a calendar to which the developers have appropriate rights. Requirement met.
  • The solution must be easily accessible for the developers.
    Since the developers work with Outlook and can manage the events in the calendar using a familiar application, this requirement is met.
  • Extra education or learning-curve must be kept to a minimum.
    Outlook is a familiar application. The developers do not need to use the Azure Portal or any other extra tooling to use the solution. Requirement met.
  • Developers’ rights in the Azure Subscription and its resources must be kept to a minimum.
    With the solution the developers do not need any roles or rights in the Azure Subscription at all. Requirement met.
  • The solution must be secure.
    We’re leveraging Azure Active directory and Azure services. They’re secure, ergo this solution is secure. By tagging VMs with the corresponding shared calendars and assigning appropriate rights to the calendar, using the solution is secure as well. Requirement met.
  • The solution must be manageable by the developers themselves.
    Delegate control of the shared calendar equals delegating control to the developers. The calendar events determine the VM’s state. The developers can create, edit, and delete events. Requirement met.
  • If money can be saved, please do so.
    This solution requires no extra software, and no extra Office 365 licenses. Since we’re using Azure Automation to host and run the PowerShell script, we don’t need an extra VM or on-premises resources to run the script. Requirement met.

All requirements are met. Great solution. Fun build.

Things to consider when using this solution:

  • The Run As account that was created automatically granted the “Contributor” Role on the subscription level. Consider creating a new role that only has permissions to change a VM’s state and assigning that role to the Run As account opposed to the “Contributor” Role.
  • There’s no error handling whatsoever in the PowerShell script. Consider handling errors and maybe even notify someone or something that the script errored out.
  • Sometimes a runbook can end up being queued for several minutes before running. Effectively this could entail that a VM starts or stops several minutes later than the event in the shared calendar would have you believe.
  • This solution works with Shared Mailbox Calendars, Equipment Mailbox Calendars, and User Mailbox Calendars. It will not work (as of writing) with Group Calendars and Teams Calendars. The reason being that you cannot assign the Graph permissions for reading events in those types of calendars to the registered application.




25+ years experience in Microsoft powered environments. Enjoy automating stuff using powershell. In my free time (hah! as if there is any) I used to hunt achievements and gamerscore on anything Xbox Live enabled (Windows Mobile, Windows 8, Windows 10, Xbox 360 and Xbox One). Recently I picked up my Lego addiction again.

Tagged with: , , , ,
Posted in Automation, Azure, Powershell, Step-by-Step guide
8 comments on “Stop and Start Azure VMs using an Office 365 Calendar
  1. Ramsy Lowes says:

    Great effort and work Arjan. Very nice. Thank you!!!

  2. […] Stop and Start Azure VMs using an Office 365 Calendar Check out this awesome solution for Arjan to help you save time and money by using an Office 365 Calendar to control when your VMs should power on and off. […]

  3. tachytelic says:

    A brilliant and imaginative solution – well done.

  4. RC says:

    Is it possible to use this solution to start and stop vms (on domain B) with users, office 365 account and credentials of (domain A)

    • Arjan Mensch says:

      Hi RC,
      If domain A invites a guest account from B into the azure ad and a role is granted to the guest account for stopping and starting vms, I don’t see why not.

      • sac01 says:

        could you provide details how to achieve this ?
        i’ve also tried without any success by selecting the option “accounts in any organizational directory” when registering the application.

      • sac01 says:

        Hi Arjan,
        to clarify, i am able to startup & shutdown the VMs if i hard code the script.
        i get an access denied when reading the calendar information(# Run Graph API query section)
        i’ve also tried without any success by selecting the option “accounts in any organizational directory” when registering the application.

        the Office 365 mailboxes are in a separate subscription then the ones azure is running in.

      • Arjan Mensch says:

        Hi Rc,
        In that situation, I don’t think the script is going to run. The runas account needs access to the tenant with the mailboxes. You can’t invite the runas account into that tenant.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog Authors

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 434 other followers

Blog Stats
  • 3,276,552 hits
  • An error has occurred; the feed is probably down. Try again later.
  • An error has occurred; the feed is probably down. Try again later.
%d bloggers like this: