<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Azure · Loosely Decoupled</title>
    <link>https://looselydecoupled.com/topics/azure/</link>
    <description>Answers to PowerShell, Windows Server and OpenSSH questions the docs skip, with the reasoning behind them. Answer first, discussion after.</description>
    <language>en-us</language>
    
    <copyright>© 2026 Emmanuel Tsouris</copyright>
    <lastBuildDate>Mon, 02 Nov 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://looselydecoupled.com/topics/azure/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Schedule an Azure VM to Start Daily and Deallocate Itself When Idle</title>
      <link>https://looselydecoupled.com/azure-vm-daily-maintenance-window/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/azure-vm-daily-maintenance-window/</guid>
      <pubDate>Mon, 02 Nov 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Windows Server</category>
      <category>PowerShell</category>
      <category>Azure</category>
      <category>Automation</category>
      
      <description>Configure an Azure server VM to start itself daily, update Windows, and deallocate when idle, so it runs about an hour or two a day instead of 24/7.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>A <a href="https://learn.microsoft.com/en-us/azure/logic-apps/logic-apps-overview" rel="external noopener" target="_blank">Logic App<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> starts the Azure VM. Its Recurrence trigger POSTs to the VM&rsquo;s <code>start</code> endpoint as the Logic App&rsquo;s system-assigned identity.</p>
<p>The VM turns itself off. A scheduled task gets a token from the instance metadata service. Then it POSTs to the <code>deallocate</code> endpoint as the VM&rsquo;s own identity.</p>
<p>Both identities get one custom role with three actions: <code>virtualMachines/read</code>, <code>start/action</code> and <code>deallocate/action</code>. Assign it on that one VM.</p>
<p>Do not shut Windows down to turn the VM off. Azure leaves it Stopped, and a stopped VM still bills for compute. Only a deallocate stops the billing. The task deallocates only when the schedule started this boot. The game must also be stopped, or the server&rsquo;s <code>lp</code> command must report zero players. Any other answer leaves the VM on.</p>
<p>Give the trigger a <code>startTime</code> one minute in the past. Without one, the Logic App fires the moment you save it. Set it to the scheduled minute and it skips the first day. With <code>timeZone</code> set to <code>UTC</code>, the <code>startTime</code> must end in <code>Z</code>, or the service refuses it. On a subscription that has never had a Logic App, run <code>az provider register --namespace Microsoft.Logic --wait</code> first.</p>

</div>

<p>What do zombies and servers have in common? I play a game called <a href="https://store.steampowered.com/app/251570/7_Days_to_Die/" rel="external noopener" target="_blank">7 Days to Die<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>, and I&rsquo;ve run a private test server for it for years. It has lived in a few places. When I had recurring Azure credits, I moved it there. The problem was that I kept burning through all of them. I needed the server on when I wanted to play and when it needed updates, and off the rest of the time, so some credits were left for trying other Azure features.</p>
<p>On Halloween morning, my server&rsquo;s nightly update failed. SteamCMD printed <code>Error! App '294420' state is 0x606 after update job.</code> in my update script&rsquo;s log. The script did what I&rsquo;d built it to do: it left the game stopped rather than start it on a half-installed build. I didn&rsquo;t notice for two days. The game was dead the whole time, and the VM didn&rsquo;t know it. It kept running, and billing, until I found it and turned it off by hand.</p>
<p>That VM had no reason to be on most of the day. It ran around the clock so that a backup at 11:00 and an update at 11:30 could each take a few minutes. So I gave it a daily window instead: it starts itself, runs those tasks and the Windows updates, and turns itself off again when nobody is playing. A failed update can still happen, but now the VM goes off afterward instead of billing, and the next morning&rsquo;s boot starts the game again. Every piece is built here, so you don&rsquo;t need my module.</p>
<!-- style-check: procedure -->
<h2 id="1-confirm-the-prerequisites">1. Confirm the prerequisites<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#1-confirm-the-prerequisites"
     aria-label="Link to section: 1. Confirm the prerequisites" data-pagefind-ignore></a>
</h2>
<p>You need:</p>
<ul>
<li>An Azure VM running Windows with a 7 Days to Die server on it. I tested on Windows Server 2019.</li>
<li>The Azure CLI on your own machine, signed in as someone who can create role definitions and role assignments. Creating the role and its assignments takes Owner or User Access Administrator on the subscription. The other steps also change the VM and create a Logic App, which Owner covers.</li>
<li>Telnet enabled in the server&rsquo;s <code>serverconfig.xml</code> (<code>TelnetEnabled</code> set to <code>true</code>, with a <code>TelnetPort</code>). The player check in step 7 asks the server over telnet, and telnet is also how the script stops the server cleanly. <a href="https://looselydecoupled.com/start-7-days-to-die-server-at-boot-and-stop-it-cleanly/">Start a 7 Days to Die Dedicated Server at Boot on Windows and Stop It Cleanly</a> covers both.</li>
<li>A task that starts the server at boot, a daily backup task and a daily update task. The articles on <a href="https://looselydecoupled.com/backup-7-days-to-die-server-s3/">backing up the server to S3</a> and on <a href="https://looselydecoupled.com/update-7-days-to-die-server-only-on-new-build/">updating it only on a new build</a> build the last two. The script in step 11 waits for <code>7DaysToDieServerS3Backup</code> and <code>7DaysToDieServerAutoUpdate</code>, named in <code>$WaitFor</code>. If yours are named differently, change <code>$WaitFor</code> to match. A name that doesn&rsquo;t match is skipped, so the VM can deallocate under a running backup or update.</li>
</ul>
<h2 id="2-lay-out-the-window">2. Lay out the window<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#2-lay-out-the-window"
     aria-label="Link to section: 2. Lay out the window" data-pagefind-ignore></a>
</h2>
<p>Five things happen in the window, in this order. Each one starts only after the one before it has had time to finish.</p>
<div class="table-wrap" data-scrollable data-label="Table: UTC">
  <table>
    <thead>
      <tr><th scope="col">UTC</th><th scope="col">What runs</th></tr>
    </thead>
    <tbody>
      <tr><td>10:45</td><td>The Logic App starts the VM.</td></tr>
      <tr><td>After boot</td><td>The autostart task brings the game up.</td></tr>
      <tr><td>11:00</td><td>The backup task runs.</td></tr>
      <tr><td>11:30</td><td>The update task runs.</td></tr>
      <tr><td>12:00</td><td>The maintenance task installs Windows updates, then deallocates the VM.</td></tr>
    </tbody>
  </table>
</div>
<p>If you move one time, keep the same order. Keep the start before the backup by enough to cover the boot and the game&rsquo;s startup. Keep the maintenance task after the update task, so it never stops the game under an update. The maintenance task also waits for the backup and update tasks if they are still running, but it gives up after 60 minutes and leaves the VM on.</p>
<p>The window runs about an hour or two a day instead of 24, depending on the work. A game update to download or Windows updates to install make it longer.</p>
<p>When a log line ends &ldquo;VM left on&rdquo;, the VM stays on, and tomorrow&rsquo;s run doesn&rsquo;t turn it off either, because the schedule didn&rsquo;t start that boot. Read the log and deallocate it by hand.</p>
<h2 id="3-create-the-custom-role">3. Create the custom role<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#3-create-the-custom-role"
     aria-label="Link to section: 3. Create the custom role" data-pagefind-ignore></a>
</h2>
<p>The role lets an identity read a VM, start it and deallocate it, and nothing else. Virtual Machine Contributor would work, but it also lets an identity resize the VM, swap its disks and reset its password, which neither of these identities needs.</p>
<p>Steps 3 to 6 and 13 share variables. Run the commands meant for your own machine in one PowerShell session, starting with your subscription ID, which step 5 needs too:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$subscription</span> <span class="p">=</span> <span class="n">az</span> <span class="n">account</span> <span class="n">show</span> <span class="p">-</span><span class="n">-query</span> <span class="n">id</span> <span class="n">-o</span> <span class="n">tsv</span></span></span></code></pre></div></div>
  </div>
</div><p>Before you create the role, check whether it already exists with <code>az role definition list --name 'Game Server VM Power' --custom-role-only true</code>. If that returns the role, skip the create. Otherwise this makes the role definition in your subscription:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$definition</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Name</span>             <span class="p">=</span> <span class="s1">&#39;Game Server VM Power&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Description</span>      <span class="p">=</span> <span class="s1">&#39;Read, start and deallocate a virtual machine. Nothing else.&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Actions</span>          <span class="p">=</span> <span class="vm">@</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;Microsoft.Compute/virtualMachines/read&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;Microsoft.Compute/virtualMachines/start/action&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;Microsoft.Compute/virtualMachines/deallocate/action&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">AssignableScopes</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="s2">&#34;/subscriptions/</span><span class="nv">$subscription</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nv">$definition</span> <span class="p">|</span> <span class="nb">ConvertTo-Json</span> <span class="p">|</span> <span class="nb">Set-Content</span> <span class="n">role</span><span class="p">.</span><span class="py">json</span> <span class="n">-Encoding</span> <span class="n">ascii</span>
</span></span><span class="line"><span class="cl"><span class="n">az</span> <span class="n">role</span> <span class="n">definition</span> <span class="n">create</span> <span class="p">-</span><span class="n">-role-definition</span> <span class="s2">&#34;@role.json&#34;</span> <span class="n">-o</span> <span class="n">none</span></span></span></code></pre></div></div>
  </div>
</div><p>The role is assignable anywhere in the subscription, so one definition serves every VM you add later.</p>
<h2 id="4-enable-the-vms-identity-and-assign-the-role">4. Enable the VM&rsquo;s identity and assign the role<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#4-enable-the-vms-identity-and-assign-the-role"
     aria-label="Link to section: 4. Enable the VM’s identity and assign the role" data-pagefind-ignore></a>
</h2>
<p>Your VM needs a system-assigned managed identity: an Azure identity that belongs to the VM itself, with credentials Azure keeps for you. With it, the VM can ask Azure to deallocate it. This turns the identity on and grants it the role on that one VM:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$rg</span> <span class="p">=</span> <span class="s1">&#39;&lt;resource-group&gt;&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$vmName</span> <span class="p">=</span> <span class="s1">&#39;&lt;vm-name&gt;&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$vm</span> <span class="p">=</span> <span class="n">az</span> <span class="n">vm</span> <span class="n">show</span> <span class="n">-g</span> <span class="nv">$rg</span> <span class="n">-n</span> <span class="nv">$vmName</span> <span class="n">-o</span> <span class="n">json</span> <span class="p">|</span> <span class="nb">ConvertFrom-Json</span>
</span></span><span class="line"><span class="cl"><span class="nv">$vmPrincipal</span> <span class="p">=</span> <span class="p">(</span><span class="n">az</span> <span class="n">vm</span> <span class="n">identity</span> <span class="n">assign</span> <span class="n">-g</span> <span class="nv">$rg</span> <span class="n">-n</span> <span class="nv">$vmName</span> <span class="n">-o</span> <span class="n">json</span> <span class="p">|</span> <span class="nb">ConvertFrom-Json</span><span class="p">).</span><span class="py">systemAssignedIdentity</span>
</span></span><span class="line"><span class="cl"><span class="n">az</span> <span class="n">role</span> <span class="n">assignment</span> <span class="n">create</span> <span class="p">-</span><span class="n">-assignee-object-id</span> <span class="nv">$vmPrincipal</span> <span class="p">-</span><span class="n">-assignee-principal-type</span> <span class="n">ServicePrincipal</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="p">-</span><span class="n">-role</span> <span class="s1">&#39;Game Server VM Power&#39;</span> <span class="p">-</span><span class="n">-scope</span> <span class="nv">$vm</span><span class="p">.</span><span class="py">id</span> <span class="n">-o</span> <span class="n">none</span></span></span></code></pre></div></div>
  </div>
</div><p><code>--assignee-object-id</code> with <code>--assignee-principal-type</code> skips the directory lookup that <code>--assignee</code> does in Microsoft Graph. That lookup fails for a few minutes after you create an identity. See <a href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#cannot-find-user-or-service-principal-in-graph-database">Cannot find user or service principal in graph database</a>.</p>
<h2 id="5-create-the-logic-app-that-starts-the-vm">5. Create the Logic App that starts the VM<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#5-create-the-logic-app-that-starts-the-vm"
     aria-label="Link to section: 5. Create the Logic App that starts the VM" data-pagefind-ignore></a>
</h2>
<p>The start half is a Logic App, Azure&rsquo;s hosted workflow service, on the pay-per-run Consumption plan. It has a Recurrence trigger and one HTTP action. The action POSTs to the VM&rsquo;s <code>start</code> endpoint and authenticates as the Logic App&rsquo;s own system-assigned identity. Use a Logic App rather than Azure Automation, which needs a script (a runbook) that you keep up to date. A Logic App is only a timer and one web request.</p>
<p>Other clouds have their own timer for the start half. On AWS, an <a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-targets-universal.html" rel="external noopener" target="_blank">EventBridge Scheduler universal target<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> can call the EC2 <code>StartInstances</code> API directly. On Google Cloud, a Compute Engine <a href="https://docs.cloud.google.com/compute/docs/instances/schedule-instance-start-stop" rel="external noopener" target="_blank">instance schedule<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> starts and stops a VM at set times.</p>
<p>First check that your subscription is registered for the Logic App resource provider. If it has never had a Logic App, it isn&rsquo;t, and Azure refuses the create with <code>MissingSubscriptionRegistration</code>:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">az</span> <span class="n">provider</span> <span class="n">show</span> <span class="p">-</span><span class="n">-namespace</span> <span class="n">Microsoft</span><span class="p">.</span><span class="py">Logic</span> <span class="p">-</span><span class="n">-query</span> <span class="n">registrationState</span> <span class="n">-o</span> <span class="n">tsv</span></span></span></code></pre></div></div>
  </div>
</div><p>If that prints anything other than <code>Registered</code>, register it. This took under two minutes for me:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">az</span> <span class="n">provider</span> <span class="n">register</span> <span class="p">-</span><span class="n">-namespace</span> <span class="n">Microsoft</span><span class="p">.</span><span class="py">Logic</span> <span class="p">-</span><span class="n">-wait</span></span></span></code></pre></div></div>
  </div>
</div><p>This creates the Logic App and turns on its identity. It starts the VM at 10:45 UTC. Change <code>hours</code> and <code>minutes</code> for another time:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$appName</span> <span class="p">=</span> <span class="s2">&#34;</span><span class="nv">$vmName</span><span class="s2">-daily-start&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c"># One minute in the past, with the Z. See Common failures for why each part matters.</span>
</span></span><span class="line"><span class="cl"><span class="nv">$first</span> <span class="p">=</span> <span class="p">[</span><span class="no">datetime</span><span class="p">]::</span><span class="n">UtcNow</span><span class="p">.</span><span class="py">AddMinutes</span><span class="p">(</span><span class="mf">-1</span><span class="p">).</span><span class="py">ToString</span><span class="p">(</span><span class="s2">&#34;yyyy-MM-dd&#39;T&#39;HH:mm:ss&#39;Z&#39;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">$uri</span> <span class="p">=</span> <span class="s2">&#34;https://management.azure.com/subscriptions/</span><span class="nv">$subscription</span><span class="s2">/resourceGroups/</span><span class="nv">$rg</span><span class="s2">/providers/Microsoft.Logic/workflows/</span><span class="p">$(</span><span class="nv">$appName</span><span class="p">)</span><span class="s2">?api-version=2019-05-01&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$workflow</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">location</span>   <span class="p">=</span> <span class="nv">$vm</span><span class="p">.</span><span class="py">location</span>
</span></span><span class="line"><span class="cl">    <span class="n">identity</span>   <span class="p">=</span> <span class="vm">@</span><span class="p">{</span> <span class="nb">type </span><span class="p">=</span> <span class="s1">&#39;SystemAssigned&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">properties</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">state</span>      <span class="p">=</span> <span class="s1">&#39;Enabled&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="n">definition</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;$schema&#39;</span>      <span class="p">=</span> <span class="s1">&#39;https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#&#39;</span>
</span></span><span class="line"><span class="cl">            <span class="n">contentVersion</span> <span class="p">=</span> <span class="s1">&#39;1.0.0.0&#39;</span>
</span></span><span class="line"><span class="cl">            <span class="n">triggers</span>       <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="n">Daily</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nb">type </span>      <span class="p">=</span> <span class="s1">&#39;Recurrence&#39;</span>
</span></span><span class="line"><span class="cl">                    <span class="n">recurrence</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">                        <span class="n">frequency</span> <span class="p">=</span> <span class="s1">&#39;Day&#39;</span>
</span></span><span class="line"><span class="cl">                        <span class="n">interval</span>  <span class="p">=</span> <span class="mf">1</span>
</span></span><span class="line"><span class="cl">                        <span class="n">timeZone</span>  <span class="p">=</span> <span class="s1">&#39;UTC&#39;</span>
</span></span><span class="line"><span class="cl">                        <span class="n">startTime</span> <span class="p">=</span> <span class="nv">$first</span>
</span></span><span class="line"><span class="cl">                        <span class="n">schedule</span>  <span class="p">=</span> <span class="vm">@</span><span class="p">{</span> <span class="n">hours</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="mf">10</span><span class="p">);</span> <span class="n">minutes</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="mf">45</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">                    <span class="p">}</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="n">actions</span>        <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="n">Start_VM</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nb">type </span>  <span class="p">=</span> <span class="s1">&#39;Http&#39;</span>
</span></span><span class="line"><span class="cl">                    <span class="n">inputs</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">                        <span class="n">method</span>         <span class="p">=</span> <span class="s1">&#39;POST&#39;</span>
</span></span><span class="line"><span class="cl">                        <span class="n">uri</span>            <span class="p">=</span> <span class="s2">&#34;https://management.azure.com</span><span class="p">$(</span><span class="nv">$vm</span><span class="p">.</span><span class="n">id</span><span class="p">)</span><span class="s2">/start?api-version=2024-07-01&#34;</span>
</span></span><span class="line"><span class="cl">                        <span class="n">authentication</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span> <span class="nb">type </span><span class="p">=</span> <span class="s1">&#39;ManagedServiceIdentity&#39;</span><span class="p">;</span> <span class="n">audience</span> <span class="p">=</span> <span class="s1">&#39;https://management.azure.com/&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">                    <span class="p">}</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nv">$workflow</span> <span class="p">|</span> <span class="nb">ConvertTo-Json</span> <span class="n">-Depth</span> <span class="mf">20</span> <span class="p">|</span> <span class="nb">Set-Content</span> <span class="n">workflow</span><span class="p">.</span><span class="py">json</span> <span class="n">-Encoding</span> <span class="n">ascii</span>
</span></span><span class="line"><span class="cl"><span class="nv">$app</span> <span class="p">=</span> <span class="n">az</span> <span class="n">rest</span> <span class="p">-</span><span class="n">-method</span> <span class="n">put</span> <span class="p">-</span><span class="n">-uri</span> <span class="nv">$uri</span> <span class="p">-</span><span class="n">-body</span> <span class="s2">&#34;@workflow.json&#34;</span> <span class="n">-o</span> <span class="n">json</span> <span class="p">|</span> <span class="nb">ConvertFrom-Json</span></span></span></code></pre></div></div>
  </div>
</div><p>Because <code>startTime</code> is in the past, nothing runs when you save it. Without a <code>startTime</code>, it runs at once and fails with <code>Forbidden</code>, because its identity holds no role until the next step.</p>
<h2 id="6-assign-the-role-to-the-logic-apps-identity">6. Assign the role to the Logic App&rsquo;s identity<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#6-assign-the-role-to-the-logic-apps-identity"
     aria-label="Link to section: 6. Assign the role to the Logic App’s identity" data-pagefind-ignore></a>
</h2>
<p>The Logic App can&rsquo;t start the VM until its own identity holds the role. List the assignments on the VM by scope and role and filter on <code>principalId</code>. That form works on an identity that is seconds old; <code>--assignee</code> doesn&rsquo;t.</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$appPrincipal</span> <span class="p">=</span> <span class="nv">$app</span><span class="p">.</span><span class="py">identity</span><span class="p">.</span><span class="py">principalId</span>
</span></span><span class="line"><span class="cl"><span class="nv">$held</span> <span class="p">=</span> <span class="n">az</span> <span class="n">role</span> <span class="n">assignment</span> <span class="n">list</span> <span class="p">-</span><span class="n">-scope</span> <span class="nv">$vm</span><span class="p">.</span><span class="py">id</span> <span class="p">-</span><span class="n">-role</span> <span class="s1">&#39;Game Server VM Power&#39;</span> <span class="n">-o</span> <span class="n">json</span> <span class="p">|</span> <span class="nb">ConvertFrom-Json</span> <span class="p">|</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Where-Object</span> <span class="p">{</span> <span class="nv">$_</span><span class="p">.</span><span class="py">principalId</span> <span class="o">-eq</span> <span class="nv">$appPrincipal</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">-not</span> <span class="nv">$held</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">az</span> <span class="n">role</span> <span class="n">assignment</span> <span class="n">create</span> <span class="p">-</span><span class="n">-assignee-object-id</span> <span class="nv">$appPrincipal</span> <span class="p">-</span><span class="n">-assignee-principal-type</span> <span class="n">ServicePrincipal</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">        <span class="p">-</span><span class="n">-role</span> <span class="s1">&#39;Game Server VM Power&#39;</span> <span class="p">-</span><span class="n">-scope</span> <span class="nv">$vm</span><span class="p">.</span><span class="py">id</span> <span class="n">-o</span> <span class="n">none</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><h2 id="7-check-the-player-count-over-telnet">7. Check the player count over telnet<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#7-check-the-player-count-over-telnet"
     aria-label="Link to section: 7. Check the player count over telnet" data-pagefind-ignore></a>
</h2>
<p>From here you work on the VM. Steps 7 to 11 build one script, <code>maintenance.ps1</code>, which runs there in Windows PowerShell 5.1. Step 11 gives the order you assemble it in.</p>
<p>While the game is running, the decision to turn the machine off rests on what the server reports, not on the time of day. The server&rsquo;s <code>lp</code> command lists connected players and ends with a line such as <code>Total of 1 in the game</code>. The function returns that number. For anything else it returns <code>$null</code>: telnet off, no connection, a wrong password, or no <code>Total of</code> line. Your script treats <code>$null</code> as &ldquo;someone might be on&rdquo;.</p>
<p>The function takes the last <code>Total of</code> line in the reply, because the console can echo an earlier listing that was still in its buffer. It connects to <code>127.0.0.1</code> and sends <code>exit</code>, which closes only the telnet session. It never sends <code>shutdown</code>.</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">ConvertFrom-PlayerListing</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span><span class="p">([</span><span class="n">AllowEmptyString</span><span class="p">()][</span><span class="no">string</span><span class="p">]</span><span class="nv">$Text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">-not</span> <span class="nv">$Text</span> <span class="o">-or</span> <span class="nv">$Text</span> <span class="o">-match</span> <span class="s1">&#39;Password incorrect&#39;</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="vm">$null</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$found</span> <span class="p">=</span> <span class="p">[</span><span class="no">regex</span><span class="p">]::</span><span class="n">Matches</span><span class="p">(</span><span class="nv">$Text</span><span class="p">,</span> <span class="s1">&#39;Total of (\d+) in the game&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$found</span><span class="p">.</span><span class="py">Count</span> <span class="o">-eq</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="vm">$null</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="no">int</span><span class="p">]</span><span class="nv">$found</span><span class="p">[</span><span class="nv">$found</span><span class="p">.</span><span class="py">Count</span> <span class="p">-</span> <span class="mf">1</span><span class="p">].</span><span class="n">Groups</span><span class="p">[</span><span class="mf">1</span><span class="p">].</span><span class="py">Value</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Get-ServerPlayerCount</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="no">int</span><span class="p">]</span><span class="nv">$Port</span> <span class="p">=</span> <span class="mf">8081</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="no">string</span><span class="p">]</span><span class="nv">$Password</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="no">int</span><span class="p">]</span><span class="nv">$TimeoutSeconds</span> <span class="p">=</span> <span class="mf">15</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$client</span> <span class="p">=</span> <span class="p">[</span><span class="no">Net.Sockets.TcpClient</span><span class="p">]::</span><span class="n">new</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="o">-not</span> <span class="nv">$client</span><span class="p">.</span><span class="py">ConnectAsync</span><span class="p">(</span><span class="s1">&#39;127.0.0.1&#39;</span><span class="p">,</span> <span class="nv">$Port</span><span class="p">).</span><span class="py">Wait</span><span class="p">(</span><span class="nv">$TimeoutSeconds</span> <span class="p">*</span> <span class="mf">1000</span><span class="p">))</span> <span class="p">{</span> <span class="k">return</span> <span class="vm">$null</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$stream</span> <span class="p">=</span> <span class="nv">$client</span><span class="p">.</span><span class="py">GetStream</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$writer</span> <span class="p">=</span> <span class="p">[</span><span class="no">IO.StreamWriter</span><span class="p">]::</span><span class="n">new</span><span class="p">(</span><span class="nv">$stream</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$writer</span><span class="p">.</span><span class="py">AutoFlush</span> <span class="p">=</span> <span class="vm">$true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="nb">Start-Sleep</span> <span class="n">-Milliseconds</span> <span class="mf">500</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nv">$Password</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$writer</span><span class="p">.</span><span class="py">WriteLine</span><span class="p">(</span><span class="nv">$Password</span><span class="p">);</span> <span class="nb">Start-Sleep</span> <span class="n">-Milliseconds</span> <span class="mf">500</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$writer</span><span class="p">.</span><span class="py">WriteLine</span><span class="p">(</span><span class="s1">&#39;lp&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="nv">$buffer</span>   <span class="p">=</span> <span class="nb">New-Object</span> <span class="n">byte</span><span class="p">[]</span> <span class="mf">4096</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$text</span>     <span class="p">=</span> <span class="s1">&#39;&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$count</span>    <span class="p">=</span> <span class="vm">$null</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$deadline</span> <span class="p">=</span> <span class="p">(</span><span class="nb">Get-Date</span><span class="p">).</span><span class="py">AddSeconds</span><span class="p">(</span><span class="nv">$TimeoutSeconds</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">while</span> <span class="p">((</span><span class="nb">Get-Date</span><span class="p">)</span> <span class="o">-lt</span> <span class="nv">$deadline</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">while</span> <span class="p">(</span><span class="nv">$stream</span><span class="p">.</span><span class="n">DataAvailable</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$read</span>  <span class="p">=</span> <span class="nv">$stream</span><span class="p">.</span><span class="py">Read</span><span class="p">(</span><span class="nv">$buffer</span><span class="p">,</span> <span class="mf">0</span><span class="p">,</span> <span class="nv">$buffer</span><span class="p">.</span><span class="n">Length</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$text</span> <span class="p">+=</span> <span class="p">[</span><span class="no">Text.Encoding</span><span class="p">]::</span><span class="n">UTF8</span><span class="p">.</span><span class="py">GetString</span><span class="p">(</span><span class="nv">$buffer</span><span class="p">,</span> <span class="mf">0</span><span class="p">,</span> <span class="nv">$read</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nv">$text</span> <span class="o">-match</span> <span class="s1">&#39;Password incorrect&#39;</span><span class="p">)</span> <span class="p">{</span> <span class="k">break</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="nv">$count</span> <span class="p">=</span> <span class="nb">ConvertFrom-PlayerListing</span> <span class="n">-Text</span> <span class="nv">$text</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="vm">$null</span> <span class="o">-ne</span> <span class="nv">$count</span><span class="p">)</span> <span class="p">{</span> <span class="k">break</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="nb">Start-Sleep</span> <span class="n">-Milliseconds</span> <span class="mf">250</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span> <span class="p">{</span> <span class="nv">$writer</span><span class="p">.</span><span class="py">WriteLine</span><span class="p">(</span><span class="s1">&#39;exit&#39;</span><span class="p">)</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$count</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="vm">$null</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$client</span><span class="p">.</span><span class="py">Dispose</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>On my server with nobody connected, my module&rsquo;s version of this function returned <code>0</code>.</p>
<h2 id="8-test-whether-the-schedule-started-this-boot">8. Test whether the schedule started this boot<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#8-test-whether-the-schedule-started-this-boot"
     aria-label="Link to section: 8. Test whether the schedule started this boot" data-pagefind-ignore></a>
</h2>
<p>The task turns the VM off only when the schedule started this boot. A VM you start by hand to play stays on. The test checks whether the boot falls in a window that opens at the scheduled start time. The script in step 11 sets that window to 30 minutes.</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Test-ScheduledBoot</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)][</span><span class="no">datetime</span><span class="p">]</span><span class="nv">$BootTimeUtc</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)][</span><span class="no">string</span><span class="p">]</span><span class="nv">$StartAtUtc</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)][</span><span class="no">int</span><span class="p">]</span><span class="nv">$WindowMinutes</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$time</span>  <span class="p">=</span> <span class="p">[</span><span class="no">datetime</span><span class="p">]::</span><span class="n">ParseExact</span><span class="p">(</span><span class="nv">$StartAtUtc</span><span class="p">,</span> <span class="s1">&#39;HH:mm&#39;</span><span class="p">,</span> <span class="p">[</span><span class="no">Globalization.CultureInfo</span><span class="p">]::</span><span class="n">InvariantCulture</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$start</span> <span class="p">=</span> <span class="nv">$BootTimeUtc</span><span class="p">.</span><span class="py">Date</span><span class="p">.</span><span class="py">Add</span><span class="p">(</span><span class="nv">$time</span><span class="p">.</span><span class="n">TimeOfDay</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$BootTimeUtc</span> <span class="o">-ge</span> <span class="nv">$start</span> <span class="o">-and</span> <span class="nv">$BootTimeUtc</span> <span class="o">-lt</span> <span class="nv">$start</span><span class="p">.</span><span class="py">AddMinutes</span><span class="p">(</span><span class="nv">$WindowMinutes</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>With a start of <code>10:45</code> and a window of 30, a boot at 10:47 counts as scheduled. Boots at 10:44, 11:15 and 19:30 don&rsquo;t. A boot you start by hand at 11:00 counts as scheduled, because the function sees only the time. Keep your own play sessions outside that window.</p>
<h2 id="9-search-for-and-install-windows-updates">9. Search for and install Windows updates<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#9-search-for-and-install-windows-updates"
     aria-label="Link to section: 9. Search for and install Windows updates" data-pagefind-ignore></a>
</h2>
<p>A VM that is deallocated most of the day never reaches the operating system&rsquo;s own update schedule, so the window installs them itself. PowerShell can call the Windows Update Agent, the update component built into Windows, directly. The task runs as SYSTEM, and you install no extra module.</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Test-PendingReboot</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$keys</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$key</span> <span class="k">in</span> <span class="nv">$keys</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nb">Test-Path</span> <span class="nv">$key</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="vm">$true</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="vm">$false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Install-PendingWindowsUpdate</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span><span class="p">([</span><span class="no">switch</span><span class="p">]</span><span class="nv">$SearchOnly</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$session</span> <span class="p">=</span> <span class="nb">New-Object</span> <span class="n">-ComObject</span> <span class="n">Microsoft</span><span class="p">.</span><span class="py">Update</span><span class="p">.</span><span class="py">Session</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$search</span>  <span class="p">=</span> <span class="nv">$session</span><span class="p">.</span><span class="py">CreateUpdateSearcher</span><span class="p">().</span><span class="py">Search</span><span class="p">(</span><span class="s2">&#34;IsInstalled=0 and IsHidden=0 and Type=&#39;Software&#39;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$titles</span> <span class="p">=</span> <span class="vm">@</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="nv">$i</span> <span class="p">=</span> <span class="mf">0</span><span class="p">;</span> <span class="nv">$i</span> <span class="o">-lt</span> <span class="nv">$search</span><span class="p">.</span><span class="py">Updates</span><span class="p">.</span><span class="n">Count</span><span class="p">;</span> <span class="nv">$i</span><span class="p">++)</span> <span class="p">{</span> <span class="nv">$titles</span> <span class="p">+=</span> <span class="nv">$search</span><span class="p">.</span><span class="py">Updates</span><span class="p">.</span><span class="py">Item</span><span class="p">(</span><span class="nv">$i</span><span class="p">).</span><span class="py">Title</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$result</span> <span class="p">=</span> <span class="p">[</span><span class="no">pscustomobject</span><span class="p">]</span><span class="vm">@</span><span class="p">{</span> <span class="n">Found</span> <span class="p">=</span> <span class="nv">$titles</span><span class="p">;</span> <span class="n">Installed</span> <span class="p">=</span> <span class="vm">@</span><span class="p">();</span> <span class="n">Failed</span> <span class="p">=</span> <span class="vm">@</span><span class="p">();</span> <span class="n">RebootRequired</span> <span class="p">=</span> <span class="vm">$false</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$titles</span><span class="p">.</span><span class="py">Count</span> <span class="o">-eq</span> <span class="mf">0</span> <span class="o">-or</span> <span class="nv">$SearchOnly</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$result</span><span class="p">.</span><span class="py">RebootRequired</span> <span class="p">=</span> <span class="nb">Test-PendingReboot</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nv">$result</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c"># Untested from here to the end of the function: no update was offered.</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$collection</span> <span class="p">=</span> <span class="nb">New-Object</span> <span class="n">-ComObject</span> <span class="n">Microsoft</span><span class="p">.</span><span class="py">Update</span><span class="p">.</span><span class="py">UpdateColl</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="nv">$i</span> <span class="p">=</span> <span class="mf">0</span><span class="p">;</span> <span class="nv">$i</span> <span class="o">-lt</span> <span class="nv">$search</span><span class="p">.</span><span class="py">Updates</span><span class="p">.</span><span class="n">Count</span><span class="p">;</span> <span class="nv">$i</span><span class="p">++)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$update</span> <span class="p">=</span> <span class="nv">$search</span><span class="p">.</span><span class="py">Updates</span><span class="p">.</span><span class="py">Item</span><span class="p">(</span><span class="nv">$i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="o">-not</span> <span class="nv">$update</span><span class="p">.</span><span class="n">EulaAccepted</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$update</span><span class="p">.</span><span class="py">AcceptEula</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="no">void</span><span class="p">]</span><span class="nv">$collection</span><span class="p">.</span><span class="py">Add</span><span class="p">(</span><span class="nv">$update</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$downloader</span> <span class="p">=</span> <span class="nv">$session</span><span class="p">.</span><span class="py">CreateUpdateDownloader</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$downloader</span><span class="p">.</span><span class="py">Updates</span> <span class="p">=</span> <span class="nv">$collection</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="no">void</span><span class="p">]</span><span class="nv">$downloader</span><span class="p">.</span><span class="py">Download</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$installer</span> <span class="p">=</span> <span class="nv">$session</span><span class="p">.</span><span class="py">CreateUpdateInstaller</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$installer</span><span class="p">.</span><span class="py">Updates</span> <span class="p">=</span> <span class="nv">$collection</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$outcome</span> <span class="p">=</span> <span class="nv">$installer</span><span class="p">.</span><span class="py">Install</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c"># ResultCode 2 is succeeded and 3 is succeeded with errors; 3 counts as installed.</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="nv">$i</span> <span class="p">=</span> <span class="mf">0</span><span class="p">;</span> <span class="nv">$i</span> <span class="o">-lt</span> <span class="nv">$collection</span><span class="p">.</span><span class="n">Count</span><span class="p">;</span> <span class="nv">$i</span><span class="p">++)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$code</span> <span class="p">=</span> <span class="nv">$outcome</span><span class="p">.</span><span class="py">GetUpdateResult</span><span class="p">(</span><span class="nv">$i</span><span class="p">).</span><span class="py">ResultCode</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nv">$code</span> <span class="n">-in</span> <span class="mf">2</span><span class="p">,</span> <span class="mf">3</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$result</span><span class="p">.</span><span class="py">Installed</span> <span class="p">+=</span> <span class="nv">$collection</span><span class="p">.</span><span class="py">Item</span><span class="p">(</span><span class="nv">$i</span><span class="p">).</span><span class="py">Title</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span> <span class="p">{</span> <span class="nv">$result</span><span class="p">.</span><span class="py">Failed</span> <span class="p">+=</span> <span class="s2">&#34;</span><span class="p">$(</span><span class="nv">$collection</span><span class="p">.</span><span class="py">Item</span><span class="p">(</span><span class="nv">$i</span><span class="p">).</span><span class="n">Title</span><span class="p">)</span><span class="s2"> (result </span><span class="nv">$code</span><span class="s2">)&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$result</span><span class="p">.</span><span class="py">RebootRequired</span> <span class="p">=</span> <span class="nv">$outcome</span><span class="p">.</span><span class="py">RebootRequired</span> <span class="o">-or</span> <span class="p">(</span><span class="nb">Test-PendingReboot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$result</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>The function never restarts the machine.</p>
<h2 id="10-deallocate-the-vm-from-inside">10. Deallocate the VM from inside<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#10-deallocate-the-vm-from-inside"
     aria-label="Link to section: 10. Deallocate the VM from inside" data-pagefind-ignore></a>
</h2>
<p>Shutting Windows down from inside leaves an Azure VM in the Stopped state, which still bills for compute. Only Azure can deallocate it, releasing its hardware so compute billing stops. So the VM asks Azure, using its managed identity. The first two calls go to the instance metadata service at <code>169.254.169.254</code>, a local endpoint that tells a VM about itself and answers only from inside it. The last goes to Azure itself:</p>
<ol>
<li>Read the VM&rsquo;s resource ID from the compute metadata.</li>
<li>Get a token for <code>management.azure.com</code>.</li>
<li>POST to the resource&rsquo;s <code>deallocate</code> endpoint with that token.</li>
</ol>
<p>Windows PowerShell 5.1 sends a POST with no body and no <code>Content-Length</code> header, so the deallocate call always sends a body, even an empty <code>{}</code>.</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Get-AzureResourceId</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="nb">Invoke-RestMethod</span> <span class="n">-Headers</span> <span class="vm">@</span><span class="p">{</span> <span class="n">Metadata</span> <span class="p">=</span> <span class="s1">&#39;true&#39;</span> <span class="p">}</span> <span class="n">-TimeoutSec</span> <span class="mf">5</span> <span class="n">-ErrorAction</span> <span class="n">Stop</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">            <span class="n">-Uri</span> <span class="s1">&#39;http://169.254.169.254/metadata/instance/compute?api-version=2021-02-01&#39;</span><span class="p">).</span><span class="py">resourceId</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="vm">$null</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Invoke-AzureSelfDeallocate</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span><span class="p">([</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)][</span><span class="no">string</span><span class="p">]</span><span class="nv">$ResourceId</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="no">Net.ServicePointManager</span><span class="p">]::</span><span class="n">SecurityProtocol</span> <span class="p">=</span> <span class="p">[</span><span class="no">Net.ServicePointManager</span><span class="p">]::</span><span class="n">SecurityProtocol</span> <span class="o">-bor</span> <span class="p">[</span><span class="no">Net.SecurityProtocolType</span><span class="p">]::</span><span class="n">Tls12</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$token</span> <span class="p">=</span> <span class="nb">Invoke-RestMethod</span> <span class="n">-Headers</span> <span class="vm">@</span><span class="p">{</span> <span class="n">Metadata</span> <span class="p">=</span> <span class="s1">&#39;true&#39;</span> <span class="p">}</span> <span class="n">-TimeoutSec</span> <span class="mf">10</span> <span class="n">-ErrorAction</span> <span class="n">Stop</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">        <span class="n">-Uri</span> <span class="s1">&#39;http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&amp;resource=https%3A%2F%2Fmanagement.azure.com%2F&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$response</span> <span class="p">=</span> <span class="nb">Invoke-WebRequest</span> <span class="n">-UseBasicParsing</span> <span class="n">-Method</span> <span class="n">Post</span> <span class="n">-ErrorAction</span> <span class="n">Stop</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">        <span class="n">-Uri</span> <span class="s2">&#34;https://management.azure.com</span><span class="nv">$ResourceId</span><span class="s2">/deallocate?api-version=2024-07-01&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">        <span class="n">-Headers</span> <span class="vm">@</span><span class="p">{</span> <span class="n">Authorization</span> <span class="p">=</span> <span class="s2">&#34;Bearer </span><span class="p">$(</span><span class="nv">$token</span><span class="p">.</span><span class="n">access_token</span><span class="p">)</span><span class="s2">&#34;</span> <span class="p">}</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">        <span class="n">-ContentType</span> <span class="s1">&#39;application/json&#39;</span> <span class="n">-Body</span> <span class="s1">&#39;{}&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="no">int</span><span class="p">]</span><span class="nv">$response</span><span class="p">.</span><span class="py">StatusCode</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>Azure answers <code>202</code> when it accepts the request. Azure shuts the operating system down a few seconds later, so write anything you still need before you call it.</p>
<h2 id="11-chain-the-checks-in-one-script">11. Chain the checks in one script<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#11-chain-the-checks-in-one-script"
     aria-label="Link to section: 11. Chain the checks in one script" data-pagefind-ignore></a>
</h2>
<p>The order of the file is what makes the pieces safe. Build one file, saved as <code>C:\7DaysToDieServer\maintenance.ps1</code> or wherever your tasks live, in this order:</p>
<ol>
<li>The <code>param([switch]$DryRun)</code> line from the top of the script below. It must be the first line of the file: PowerShell reads <code>param</code> as the script&rsquo;s parameters only when nothing but comments comes before it. Anywhere else, PowerShell ignores <code>-DryRun</code> and your rehearsal runs for real.</li>
<li>The functions from steps 7 to 10.</li>
<li>The <code>Stop-GameServer</code> function from step 4 of <a href="https://looselydecoupled.com/start-7-days-to-die-server-at-boot-and-stop-it-cleanly/">Start a 7 Days to Die Dedicated Server at Boot on Windows and Stop It Cleanly</a>. The task starts a fresh <code>powershell.exe</code> as SYSTEM, so a function defined only in your own session isn&rsquo;t there when the task runs.</li>
<li>The rest of the script below, starting at <code>$ErrorActionPreference</code>.</li>
</ol>
<p>Set the values at the top of the script to match your server and your tasks. When it runs, the script does this, in order:</p>
<ul>
<li>Exit if the machine booted under 15 minutes ago. That is the startup trigger&rsquo;s run in step 12.</li>
<li>Wait for the backup and update tasks.</li>
<li>Check the players.</li>
<li>Search for and install Windows updates.</li>
<li>Stop if the schedule didn&rsquo;t start this boot.</li>
<li>Check the players again, because someone could have joined during the updates.</li>
<li>Stop the server over telnet, so the world is saved.</li>
<li>Deallocate.</li>
</ul>
<p>This script leaves out one branch that my own module has: restarting the machine to finish updates that need it, and resuming the run afterward. When Windows reports a pending restart, this script logs that and carries on. The startup trigger exists for that branch, so without it the trigger does nothing.</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="k">param</span><span class="p">([</span><span class="no">switch</span><span class="p">]</span><span class="nv">$DryRun</span><span class="p">)</span>   <span class="c"># the first line of the file, above the functions</span>
</span></span><span class="line"><span class="cl"><span class="nv">$ErrorActionPreference</span> <span class="p">=</span> <span class="s1">&#39;Stop&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$StartAtUtc</span>     <span class="p">=</span> <span class="s1">&#39;10:45&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$WindowMinutes</span>  <span class="p">=</span> <span class="mf">30</span>
</span></span><span class="line"><span class="cl"><span class="nv">$TelnetPort</span>     <span class="p">=</span> <span class="mf">8081</span>
</span></span><span class="line"><span class="cl"><span class="nv">$TelnetPassword</span> <span class="p">=</span> <span class="s1">&#39;&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$GameTask</span>       <span class="p">=</span> <span class="s1">&#39;7DaysToDieServer&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$WaitFor</span>        <span class="p">=</span> <span class="s1">&#39;7DaysToDieServerS3Backup&#39;</span><span class="p">,</span> <span class="s1">&#39;7DaysToDieServerAutoUpdate&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$LogFile</span>        <span class="p">=</span> <span class="s1">&#39;C:\ProgramData\MaintenanceWindow\maintenance.log&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">New-Item</span> <span class="n">-ItemType</span> <span class="n">Directory</span> <span class="n">-Force</span> <span class="p">(</span><span class="nb">Split-Path</span> <span class="nv">$LogFile</span><span class="p">)</span> <span class="p">|</span> <span class="nb">Out-Null</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Write-Log([string]$Message</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Add-Content</span> <span class="n">-Path</span> <span class="nv">$LogFile</span> <span class="n">-Value</span> <span class="s2">&#34;</span><span class="p">$(</span><span class="nb">Get-Date</span> <span class="n">-Format</span> <span class="s1">&#39;yyyy-MM-dd HH:mm:ss&#39;</span><span class="p">)</span><span class="s2"> </span><span class="nv">$Message</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$os</span> <span class="p">=</span> <span class="nb">Get-CimInstance</span> <span class="n">Win32_OperatingSystem</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(((</span><span class="nb">Get-Date</span><span class="p">)</span> <span class="p">-</span> <span class="nv">$os</span><span class="p">.</span><span class="n">LastBootUpTime</span><span class="p">).</span><span class="py">TotalMinutes</span> <span class="o">-lt</span> <span class="mf">15</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nb">Write-Log</span> <span class="s1">&#39;Under 15 minutes since boot: the startup run. Nothing to do.&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$bootUtc</span>   <span class="p">=</span> <span class="nv">$os</span><span class="p">.</span><span class="py">LastBootUpTime</span><span class="p">.</span><span class="py">ToUniversalTime</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$scheduled</span> <span class="p">=</span> <span class="nb">Test-ScheduledBoot</span> <span class="n">-BootTimeUtc</span> <span class="nv">$bootUtc</span> <span class="n">-StartAtUtc</span> <span class="nv">$StartAtUtc</span> <span class="n">-WindowMinutes</span> <span class="nv">$WindowMinutes</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Write-Log</span> <span class="s2">&#34;Booted </span><span class="p">$(</span><span class="nv">$bootUtc</span><span class="p">.</span><span class="py">ToString</span><span class="p">(</span><span class="s1">&#39;yyyy-MM-dd HH:mm&#39;</span><span class="p">))</span><span class="s2"> UTC. Started by: </span><span class="p">$(</span><span class="k">if</span> <span class="p">(</span><span class="nv">$scheduled</span><span class="p">)</span> <span class="p">{</span> <span class="s1">&#39;the schedule&#39;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="s1">&#39;hand&#39;</span> <span class="p">})</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c"># The backup must finish before the machine goes away.</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$waitEnd</span> <span class="p">=</span> <span class="p">(</span><span class="nb">Get-Date</span><span class="p">).</span><span class="py">AddMinutes</span><span class="p">(</span><span class="mf">60</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="vm">$true</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$busy</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="nv">$WaitFor</span> <span class="p">|</span> <span class="nb">Where-Object</span> <span class="p">{</span> <span class="p">(</span><span class="nb">Get-ScheduledTask</span> <span class="n">-TaskName</span> <span class="nv">$_</span> <span class="n">-ErrorAction</span> <span class="n">SilentlyContinue</span><span class="p">).</span><span class="py">State</span> <span class="o">-eq</span> <span class="s1">&#39;Running&#39;</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="o">-not</span> <span class="nv">$busy</span><span class="p">)</span> <span class="p">{</span> <span class="k">break</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">((</span><span class="nb">Get-Date</span><span class="p">)</span> <span class="o">-gt</span> <span class="nv">$waitEnd</span><span class="p">)</span> <span class="p">{</span> <span class="nb">Write-Log</span> <span class="s2">&#34;Still running after 60 minutes: </span><span class="p">$(</span><span class="nv">$busy</span> <span class="n">-join</span> <span class="s1">&#39;, &#39;</span><span class="p">)</span><span class="s2">. VM left on.&#34;</span><span class="p">;</span> <span class="k">return</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nb">Write-Log</span> <span class="s2">&#34;Waiting for </span><span class="p">$(</span><span class="nv">$busy</span> <span class="n">-join</span> <span class="s1">&#39;, &#39;</span><span class="p">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="nb">Start-Sleep</span> <span class="n">-Seconds</span> <span class="mf">30</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c"># Anything but a clean 0 counts as occupied.</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$running</span> <span class="p">=</span> <span class="p">[</span><span class="no">bool</span><span class="p">](</span><span class="nb">Get-Process</span> <span class="n">-Name</span> <span class="s1">&#39;7DaysToDieServer&#39;</span> <span class="n">-ErrorAction</span> <span class="n">SilentlyContinue</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$players</span> <span class="p">=</span> <span class="mf">0</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$running</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$players</span> <span class="p">=</span> <span class="nb">Get-ServerPlayerCount</span> <span class="n">-Port</span> <span class="nv">$TelnetPort</span> <span class="n">-Password</span> <span class="nv">$TelnetPassword</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Write-Log</span> <span class="s2">&#34;Players online: </span><span class="p">$(</span><span class="k">if</span> <span class="p">(</span><span class="vm">$null</span> <span class="o">-eq</span> <span class="nv">$players</span><span class="p">)</span> <span class="p">{</span> <span class="s1">&#39;unknown&#39;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="nv">$players</span> <span class="p">})</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$players</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="nb">Write-Log</span> <span class="s1">&#39;Someone is or may be playing. VM left on.&#39;</span><span class="p">;</span> <span class="k">return</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">try</span> <span class="p">{</span> <span class="nv">$updates</span> <span class="p">=</span> <span class="nb">Install-PendingWindowsUpdate</span> <span class="n">-SearchOnly:</span><span class="nv">$DryRun</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">catch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nb">Write-Log</span> <span class="s2">&#34;WARNING: Windows Update did not run: </span><span class="p">$(</span><span class="nv">$_</span><span class="p">.</span><span class="py">Exception</span><span class="p">.</span><span class="n">Message</span><span class="p">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$updates</span> <span class="p">=</span> <span class="p">[</span><span class="no">pscustomobject</span><span class="p">]</span><span class="vm">@</span><span class="p">{</span> <span class="n">Found</span> <span class="p">=</span> <span class="vm">@</span><span class="p">();</span> <span class="n">Installed</span> <span class="p">=</span> <span class="vm">@</span><span class="p">();</span> <span class="n">Failed</span> <span class="p">=</span> <span class="vm">@</span><span class="p">();</span> <span class="n">RebootRequired</span> <span class="p">=</span> <span class="p">(</span><span class="nb">Test-PendingReboot</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Write-Log</span> <span class="s2">&#34;Windows Update offered: </span><span class="p">$(</span><span class="nv">$updates</span><span class="p">.</span><span class="py">Found</span><span class="p">.</span><span class="n">Count</span><span class="p">)</span><span class="s2">. Restart required: </span><span class="p">$(</span><span class="nv">$updates</span><span class="p">.</span><span class="n">RebootRequired</span><span class="p">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$title</span> <span class="k">in</span> <span class="nv">$updates</span><span class="p">.</span><span class="n">Installed</span><span class="p">)</span> <span class="p">{</span> <span class="nb">Write-Log</span> <span class="s2">&#34;  installed: </span><span class="nv">$title</span><span class="s2">&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$title</span> <span class="k">in</span> <span class="nv">$updates</span><span class="p">.</span><span class="n">Failed</span><span class="p">)</span>    <span class="p">{</span> <span class="nb">Write-Log</span> <span class="s2">&#34;  FAILED: </span><span class="nv">$title</span><span class="s2">&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">-not</span> <span class="nv">$scheduled</span><span class="p">)</span> <span class="p">{</span> <span class="nb">Write-Log</span> <span class="s1">&#39;Started by hand, so it stays on.&#39;</span><span class="p">;</span> <span class="k">return</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$resourceId</span> <span class="p">=</span> <span class="nb">Get-AzureResourceId</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">-not</span> <span class="nv">$resourceId</span><span class="p">)</span> <span class="p">{</span> <span class="nb">Write-Log</span> <span class="s1">&#39;No Azure instance metadata. Nothing to deallocate.&#39;</span><span class="p">;</span> <span class="k">return</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$running</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$late</span> <span class="p">=</span> <span class="nb">Get-ServerPlayerCount</span> <span class="n">-Port</span> <span class="nv">$TelnetPort</span> <span class="n">-Password</span> <span class="nv">$TelnetPassword</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nv">$late</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="nb">Write-Log</span> <span class="s1">&#39;A player may have joined. VM left on.&#39;</span><span class="p">;</span> <span class="k">return</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$DryRun</span><span class="p">)</span> <span class="p">{</span> <span class="nb">Write-Log</span> <span class="s2">&#34;Dry run: would stop the server and deallocate </span><span class="nv">$resourceId</span><span class="s2">.&#34;</span><span class="p">;</span> <span class="k">return</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c"># Stop-GameServer, pasted into this file from the start-at-boot article,</span>
</span></span><span class="line"><span class="cl">    <span class="c"># throws unless exactly one server process is running. A game that is</span>
</span></span><span class="line"><span class="cl">    <span class="c"># already down has nothing to save, so go straight to the deallocate.</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nb">Get-Process</span> <span class="n">-Name</span> <span class="s1">&#39;7DaysToDieServer&#39;</span> <span class="n">-ErrorAction</span> <span class="n">SilentlyContinue</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$secure</span> <span class="p">=</span> <span class="vm">$null</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nv">$TelnetPassword</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$secure</span> <span class="p">=</span> <span class="nb">ConvertTo-SecureString</span> <span class="nv">$TelnetPassword</span> <span class="n">-AsPlainText</span> <span class="n">-Force</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nb">Write-Log</span> <span class="p">(</span><span class="nb">Stop-GameServer</span> <span class="n">-Port</span> <span class="nv">$TelnetPort</span> <span class="n">-Password</span> <span class="nv">$secure</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Write-Log</span> <span class="s2">&#34;Deallocating </span><span class="nv">$resourceId</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$status</span> <span class="p">=</span> <span class="nb">Invoke-AzureSelfDeallocate</span> <span class="n">-ResourceId</span> <span class="nv">$resourceId</span>
</span></span><span class="line"><span class="cl">        <span class="nb">Write-Log</span> <span class="s2">&#34;Azure accepted the deallocate (HTTP </span><span class="nv">$status</span><span class="s2">).&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c"># Bring the game back rather than leave a running VM with nothing on it.</span>
</span></span><span class="line"><span class="cl">        <span class="nb">Write-Log</span> <span class="s2">&#34;ERROR: deallocate refused: </span><span class="p">$(</span><span class="nv">$_</span><span class="p">.</span><span class="py">Exception</span><span class="p">.</span><span class="n">Message</span><span class="p">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="nb">Start-ScheduledTask</span> <span class="n">-TaskName</span> <span class="nv">$GameTask</span>
</span></span><span class="line"><span class="cl">        <span class="k">throw</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Write-Log</span> <span class="s2">&#34;ERROR: </span><span class="p">$(</span><span class="nv">$_</span><span class="p">.</span><span class="py">Exception</span><span class="p">.</span><span class="n">Message</span><span class="p">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">exit</span> <span class="mf">1</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">exit</span> <span class="mf">0</span></span></span></code></pre></div></div>
  </div>
</div><h2 id="12-register-the-maintenance-task">12. Register the maintenance task<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#12-register-the-maintenance-task"
     aria-label="Link to section: 12. Register the maintenance task" data-pagefind-ignore></a>
</h2>
<p>This registers the task on the VM. It runs daily at 12:00 and again 5 minutes after startup, as SYSTEM, with a three-hour limit. <code>MultipleInstances IgnoreNew</code> stops a second copy starting while one runs. It leaves out <code>StartWhenAvailable</code> on purpose. If the VM is off at 12:00, you don&rsquo;t want that missed run to fire when you boot it at 20:00 to play.</p>
<p>The machine clock on my VM was UTC. If yours isn&rsquo;t, <code>-At '12:00'</code> means 12:00 on its clock, so convert the time.</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$script</span> <span class="p">=</span> <span class="s1">&#39;C:\7DaysToDieServer\maintenance.ps1&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$daily</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskTrigger</span> <span class="n">-Daily</span> <span class="n">-At</span> <span class="s1">&#39;12:00&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$startup</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskTrigger</span> <span class="n">-AtStartup</span>
</span></span><span class="line"><span class="cl"><span class="nv">$startup</span><span class="p">.</span><span class="py">Delay</span> <span class="p">=</span> <span class="s1">&#39;PT5M&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$action</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskAction</span> <span class="n">-Execute</span> <span class="s1">&#39;powershell.exe&#39;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="n">-Argument</span> <span class="s2">&#34;-NoProfile -ExecutionPolicy RemoteSigned -File </span><span class="se">`&#34;</span><span class="nv">$script</span><span class="se">`&#34;</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$settings</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskSettingsSet</span> <span class="n">-ExecutionTimeLimit</span> <span class="p">(</span><span class="nb">New-TimeSpan</span> <span class="n">-Hours</span> <span class="mf">3</span><span class="p">)</span> <span class="n">-MultipleInstances</span> <span class="n">IgnoreNew</span>
</span></span><span class="line"><span class="cl"><span class="nb">Register-ScheduledTask</span> <span class="n">-TaskName</span> <span class="s1">&#39;7DaysToDieServerMaintenance&#39;</span> <span class="n">-Action</span> <span class="nv">$action</span> <span class="n">-Trigger</span> <span class="vm">@</span><span class="p">(</span><span class="nv">$daily</span><span class="p">,</span> <span class="nv">$startup</span><span class="p">)</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="n">-Settings</span> <span class="nv">$settings</span> <span class="n">-User</span> <span class="s1">&#39;SYSTEM&#39;</span> <span class="n">-RunLevel</span> <span class="n">Highest</span></span></span></code></pre></div></div>
  </div>
</div><p>Both triggers run the same action, so the script can&rsquo;t tell which one fired. It uses uptime instead: under 15 minutes since boot means the startup run, and the script exits. It skips a daily run that lands within 15 minutes of a boot the same way, such as when you start the VM by hand at 11:58.</p>
<h2 id="13-confirm-the-window-works">13. Confirm the window works<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#13-confirm-the-window-works"
     aria-label="Link to section: 13. Confirm the window works" data-pagefind-ignore></a>
</h2>
<p>Run these in order. Each proves one claim.</p>
<p><strong>A hand-started VM stays on.</strong> Start the VM yourself outside the 30-minute window, wait more than 15 minutes, then run the task on the VM and read the log:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Start-ScheduledTask</span> <span class="n">-TaskName</span> <span class="s1">&#39;7DaysToDieServerMaintenance&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">Get-Content</span> <span class="s1">&#39;C:\ProgramData\MaintenanceWindow\maintenance.log&#39;</span> <span class="n">-Tail</span> <span class="mf">5</span></span></span></code></pre></div></div>
  </div>
</div><p>The log ends with <code>Started by hand, so it stays on.</code> and the VM keeps running. To rehearse a run without installing or deallocating anything, run <code>maintenance.ps1 -DryRun</code> from an elevated session instead.</p>
<p><strong>A scheduled boot deallocates.</strong> Use a VM that has been on and idle for more than 15 minutes. Set <code>$StartAtUtc</code> in <code>maintenance.ps1</code> to two minutes before the VM&rsquo;s boot time in UTC, so the boot counts as scheduled, and run the task. Read the boot time on the VM with <code>(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime()</code>. Then poll from your own machine:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">az</span> <span class="n">vm</span> <span class="nb">get-instance</span><span class="n">-view</span> <span class="n">-g</span> <span class="nv">$rg</span> <span class="n">-n</span> <span class="nv">$vmName</span> <span class="p">-</span><span class="n">-query</span> <span class="s2">&#34;instanceView.statuses[1].displayStatus&#34;</span> <span class="n">-o</span> <span class="n">tsv</span></span></span></code></pre></div></div>
  </div>
</div><p>The log shows <code>Azure accepted the deallocate (HTTP 202).</code> and the status reads <code>VM deallocated</code>. For me that took under a minute from the start of the run. Put <code>$StartAtUtc</code> back afterward.</p>
<p><strong>The Logic App starts the VM.</strong> With the VM deallocated, fire the trigger by hand instead of waiting for 10:45. This boots the VM outside the window, so the maintenance task leaves it on. Deallocate it by hand with <code>az vm deallocate -g $rg -n $vmName</code> when the test is done.</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">az</span> <span class="n">rest</span> <span class="p">-</span><span class="n">-method</span> <span class="n">post</span> <span class="p">-</span><span class="n">-uri</span> <span class="s2">&#34;https://management.azure.com</span><span class="p">$(</span><span class="nv">$app</span><span class="p">.</span><span class="n">id</span><span class="p">)</span><span class="s2">/triggers/Daily/run?api-version=2019-05-01&#34;</span></span></span></code></pre></div></div>
  </div>
</div><p>In the Logic App&rsquo;s run history in the Azure portal, the <code>Start_VM</code> action shows Running, then Accepted, and the VM boots.</p>
<p><strong>The right identities did it.</strong> Open the VM&rsquo;s activity log in the Azure portal. The deallocate shows the VM&rsquo;s own identity as its caller and the start shows the Logic App&rsquo;s.</p>
<p><strong>The schedule runs on its own.</strong> The next morning, read the logs instead of firing anything. My first scheduled window went the way the table in step 2 lays out. The Logic App started the VM a minute after 10:45, the backup uploaded, and the update found the server already on the published build. At 12:00 my module&rsquo;s maintenance log read <code>Started by: the schedule (10:45 UTC)</code>, <code>Players online: 0</code>, <code>Offered: 0</code>, then <code>Azure accepted the deallocate (HTTP 202).</code>, and the VM deallocated. Those lines come from my module&rsquo;s log; the script in step 11 words the update line as <code>Windows Update offered: 0</code>.</p>
<!-- style-check: end procedure -->
<p>Now the VM rises at 10:45, does its chores, and is back in the ground by lunch.</p>
<h2 id="tested-on">Tested on<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#tested-on"
     aria-label="Link to section: Tested on" data-pagefind-ignore></a>
</h2>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>VM size</dt> <dd>Standard_B4ls_v2</dd></div>
    <div class="tested__row"><dt>OS</dt> <dd>Windows Server 2019 Datacenter, image 17763.5206.231202</dd></div>
    <div class="tested__row"><dt>PowerShell on the VM</dt> <dd>Windows PowerShell 5.1</dd></div>
    <div class="tested__row"><dt>Machine clock</dt> <dd>UTC</dd></div>
    <div class="tested__row"><dt>Azure CLI</dt> <dd>on the admin machine</dd></div>
    <div class="tested__row"><dt>Game</dt> <dd>7 Days to Die dedicated server (294420)</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>November 2026</dd></div>
  </dl>
</div>
<h2 id="what-i-didnt-test">What I didn&rsquo;t test<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#what-i-didnt-test"
     aria-label="Link to section: What I didn’t test" data-pagefind-ignore></a>
</h2>
<p>My own server runs a module version of this logic, not the single script in step 11, so run the confirmation steps in step 13 before you trust your copy with the schedule.</p>
<p>Three things matter if your server differs from mine:</p>
<ul>
<li><strong>Installing updates.</strong> No update was waiting on the mornings I ran this, so only the search has run. The first morning one is offered, read the maintenance log afterward.</li>
<li><strong>A player online.</strong> I only ever saw a count of 0. If people play during your window, test that the VM stays on before you rely on it.</li>
<li><strong>A telnet password.</strong> I didn&rsquo;t run it with one set. If your server has one, check that the player count and the stop both still work.</li>
</ul>
<h2 id="common-failures">Common failures<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#common-failures"
     aria-label="Link to section: Common failures" data-pagefind-ignore></a>
</h2>
<h3 id="logic-app-run-at-creation-failed-with-forbidden">Logic App run at creation failed with <code>Forbidden</code><a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#logic-app-run-at-creation-failed-with-forbidden"
     aria-label="Link to section: Logic App run at creation failed with Forbidden" data-pagefind-ignore></a>
</h3>
<p>The run history shows a run at the moment you created the Logic App, with status Failed and the <code>Start_VM</code> action returning <code>Forbidden</code>. You created the Logic App without a <code>startTime</code>, so it ran once on save, before its identity held a role. Assign the role in step 6 and set <code>startTime</code> as in step 5. The scheduled runs after that aren&rsquo;t affected.</p>
<h3 id="invalidworkflowtriggerrecurrence-invalid-start-time">InvalidWorkflowTriggerRecurrence: invalid start time<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#invalidworkflowtriggerrecurrence-invalid-start-time"
     aria-label="Link to section: InvalidWorkflowTriggerRecurrence: invalid start time" data-pagefind-ignore></a>
</h3>
<div class="console console--error"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Error</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">InvalidWorkflowTriggerRecurrence</span> <span class="console__token">...</span> <span class="console__token">invalid</span> <span class="console__token">start</span> <span class="console__token">time</span> <span class="console__token">&#39;2026-11-02T10:45:00.0000000&#39;</span> <span class="console__token">zone</span> <span class="console__token">designator</span> <span class="console__token">for</span> <span class="console__token">time</span> <span class="console__token">zone</span> <span class="console__token">&#39;UTC&#39;.</span> <span class="console__token">Expected</span> <span class="console__token">start</span> <span class="console__token">time</span> <span class="console__token">format</span> <span class="console__token">with</span> <span class="console__token">zone</span> <span class="console__token">designator</span> <span class="console__token">is</span> <span class="console__token">&#39;yyyy-MM-ddTHH:mm:ssZ&#39;.</span></span></code></pre>
    </div>
  </div></div><p>With <code>timeZone</code> set to <code>UTC</code> the service requires the trailing <code>Z</code>. Format the time like this:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="p">[</span><span class="no">datetime</span><span class="p">]::</span><span class="n">UtcNow</span><span class="p">.</span><span class="py">AddMinutes</span><span class="p">(</span><span class="mf">-1</span><span class="p">).</span><span class="py">ToString</span><span class="p">(</span><span class="s2">&#34;yyyy-MM-dd&#39;T&#39;HH:mm:ss&#39;Z&#39;&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
  </div>
</div><h3 id="logic-app-skipped-the-first-day">Logic App skipped the first day<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#logic-app-skipped-the-first-day"
     aria-label="Link to section: Logic App skipped the first day" data-pagefind-ignore></a>
</h3>
<p>There is no error. You set <code>startTime</code> to the scheduled minute, and <code>nextExecutionTime</code> comes back a day late. The service reads that occurrence as already taken. Set <code>startTime</code> to one minute in the past, as in step 5.</p>
<h3 id="missingsubscriptionregistration-for-microsoftlogic">MissingSubscriptionRegistration for Microsoft.Logic<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#missingsubscriptionregistration-for-microsoftlogic"
     aria-label="Link to section: MissingSubscriptionRegistration for Microsoft.Logic" data-pagefind-ignore></a>
</h3>
<div class="console console--error"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Error</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">MissingSubscriptionRegistration</span> <span class="console__token">...</span> <span class="console__token">The</span> <span class="console__token">subscription</span> <span class="console__token">is</span> <span class="console__token">not</span> <span class="console__token">registered</span> <span class="console__token">to</span> <span class="console__token">use</span> <span class="console__token">namespace</span> <span class="console__token">&#39;Microsoft.Logic&#39;</span></span></code></pre>
    </div>
  </div></div><p>The subscription has never had a Logic App. Register the provider and run the create again:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">az</span> <span class="n">provider</span> <span class="n">register</span> <span class="p">-</span><span class="n">-namespace</span> <span class="n">Microsoft</span><span class="p">.</span><span class="py">Logic</span> <span class="p">-</span><span class="n">-wait</span></span></span></code></pre></div></div>
  </div>
</div><h3 id="cannot-find-user-or-service-principal-in-graph-database">Cannot find user or service principal in graph database<a class="heading-anchor" href="https://looselydecoupled.com/azure-vm-daily-maintenance-window/#cannot-find-user-or-service-principal-in-graph-database"
     aria-label="Link to section: Cannot find user or service principal in graph database" data-pagefind-ignore></a>
</h3>
<div class="console console--error"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Error</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">Cannot</span> <span class="console__token">find</span> <span class="console__token">user</span> <span class="console__token">or</span> <span class="console__token">service</span> <span class="console__token">principal</span> <span class="console__token">in</span> <span class="console__token">graph</span> <span class="console__token">database</span></span></code></pre>
    </div>
  </div></div><p><code>az role assignment list --assignee &lt;principal-id&gt;</code> looks the identity up in Microsoft Graph, and a new identity isn&rsquo;t there yet. List by scope and role and filter on <code>principalId</code>, and create the assignment with <code>--assignee-object-id</code> and <code>--assignee-principal-type ServicePrincipal</code>, as in steps 4 and 6.</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
