<?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/">
  <channel>
    <title>Cloudbase-Init on The Nested Lab</title>
    <link>https://thenestedlab.com/tags/cloudbase-init/</link>
    <description>Recent content in Cloudbase-Init on The Nested Lab</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <lastBuildDate>Thu, 17 Sep 2026 06:20:00 +0100</lastBuildDate>
    <atom:link href="https://thenestedlab.com/tags/cloudbase-init/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Windows Server 2025 via Aria Automation, part 2: a state machine that survives four reboots</title>
      <link>https://thenestedlab.com/posts/windows-2025-aria-part-2/</link>
      <pubDate>Thu, 17 Sep 2026 06:20:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/windows-2025-aria-part-2/</guid>
      <description>05-build-master.ps1 runs at every startup until the build is done. Flag files make each step run once; a persisted state file keeps true timings across reboots; a kill-switch flag makes a stray run a no-op. Then it cleans up, validates, publishes — and deletes itself.</description>
      <content:encoded><![CDATA[<p><a href="/posts/windows-2025-aria-part-1/">Part 1</a> ended with a startup scheduled
task registered and <code>05-build-master.ps1</code> staged locally. From this point
the machine will reboot at least twice more — Windows Updates insists, and
the build ends with a clean reboot — and every one of those boots runs the
same script. The script has to <em>converge</em> on a finished build no matter how
many times it&rsquo;s invoked.</p>
<p>That&rsquo;s a state machine, and it&rsquo;s built from three very boring mechanisms.</p>
<h2 id="the-three-mechanisms">The three mechanisms</h2>
<p><strong>1. Master kill switch.</strong> If <code>100_build_complete.flag</code> exists, exit
immediately. A stray task run after completion does nothing.</p>
<p><strong>2. Per-step flags.</strong> Every install step writes
<code>NN_&lt;Step&gt;_installed.flag</code> on success and is skipped on subsequent runs.</p>
<p><strong>3. Persisted state.</strong> <code>data-state.json</code> holds the provisioning start
time and per-step timings, re-loaded on every boot. So the final report
shows the <em>true</em> duration of every step across the whole build, not just
the last boot — and a step re-observed as SKIPPED after a reboot never
overwrites its real recorded duration.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">every</span> <span class="n">boot</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="mi">100</span><span class="n">_build_complete</span><span class="o">.</span><span class="n">flag</span> <span class="err">→</span> <span class="n">exit</span>
</span></span><span class="line"><span class="cl">  <span class="nb">load</span> <span class="n">data</span><span class="o">-</span><span class="n">state</span><span class="o">.</span><span class="n">json</span>
</span></span><span class="line"><span class="cl">  <span class="k">for</span> <span class="n">step</span> <span class="ow">in</span> <span class="o">$</span><span class="n">softwarePayload</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">NN_step_installed</span><span class="o">.</span><span class="n">flag</span> <span class="err">→</span> <span class="n">SKIP</span> <span class="p">(</span><span class="n">keep</span> <span class="n">recorded</span> <span class="n">timing</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">run</span> <span class="n">installer</span> <span class="n">from</span> <span class="n">local</span> <span class="n">Staging</span> <span class="p">(</span><span class="n">timeout</span> <span class="n">ceiling</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">exit</span> <span class="mi">0</span> <span class="ow">or</span> <span class="mi">3010</span> <span class="err">→</span> <span class="n">write</span> <span class="n">flag</span><span class="p">,</span> <span class="n">record</span> <span class="n">timing</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">step</span><span class="o">.</span><span class="n">RebootAfter</span> <span class="err">→</span> <span class="n">Restart</span><span class="o">-</span><span class="n">Computer</span><span class="p">;</span> <span class="n">exit</span>      <span class="err">←</span> <span class="n">next</span> <span class="n">boot</span> <span class="n">resumes</span> <span class="n">here</span>
</span></span><span class="line"><span class="cl">  <span class="n">cleanup</span> <span class="err">→</span> <span class="n">validate</span> <span class="err">→</span> <span class="n">publish</span> <span class="err">→</span> <span class="bp">self</span><span class="o">-</span><span class="n">destruct</span> <span class="err">→</span> <span class="n">final</span> <span class="n">reboot</span>
</span></span></code></pre></div><h2 id="phase-1-software-declaratively">Phase 1: software, declaratively</h2>
<p>The stack is a single array — the designed extension point:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$softwarePayload</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span><span class="p">=</span><span class="s1">&#39;MonitoringAgent&#39;</span><span class="p">;</span> <span class="n">Folder</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">monPath</span><span class="p">;</span>  <span class="n">File</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">monInstallFile</span><span class="p">;</span>  <span class="n">Timeout</span><span class="p">=</span><span class="mf">20</span><span class="p">;</span> <span class="n">Reboot</span><span class="p">=</span><span class="vm">$false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">     <span class="n">Service</span><span class="p">=</span><span class="s1">&#39;HealthService&#39;</span><span class="p">;</span>       <span class="n">Path</span><span class="p">=</span><span class="s1">&#39;...\Monitoring Agent\HealthService.exe&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span><span class="p">=</span><span class="s1">&#39;InventoryAgent&#39;</span><span class="p">;</span>  <span class="n">Folder</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">invPath</span><span class="p">;</span>  <span class="n">File</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">invInstallFile</span><span class="p">;</span>  <span class="n">Timeout</span><span class="p">=</span><span class="mf">15</span><span class="p">;</span> <span class="n">Reboot</span><span class="p">=</span><span class="vm">$false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">     <span class="n">Service</span><span class="p">=</span><span class="s1">&#39;InventoryAgent&#39;</span><span class="p">;</span>      <span class="n">Path</span><span class="p">=</span><span class="s1">&#39;...\Inventory\agent.exe&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span><span class="p">=</span><span class="s1">&#39;EndpointSecurity&#39;</span><span class="p">;</span><span class="n">Folder</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">epsPath</span><span class="p">;</span>  <span class="n">File</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">epsInstallFile</span><span class="p">;</span>  <span class="n">Timeout</span><span class="p">=</span><span class="mf">30</span><span class="p">;</span> <span class="n">Reboot</span><span class="p">=</span><span class="vm">$false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">     <span class="n">Service</span><span class="p">=</span><span class="s1">&#39;masvc&#39;</span><span class="p">;</span>               <span class="n">Path</span><span class="p">=</span><span class="s1">&#39;...\Agent\masvc.exe&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span><span class="p">=</span><span class="s1">&#39;LogAgent&#39;</span><span class="p">;</span>        <span class="n">Folder</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">logPath</span><span class="p">;</span>  <span class="n">File</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">logInstallFile</span><span class="p">;</span>  <span class="n">Timeout</span><span class="p">=</span><span class="mf">15</span><span class="p">;</span> <span class="n">Reboot</span><span class="p">=</span><span class="vm">$false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">     <span class="n">Service</span><span class="p">=</span><span class="s1">&#39;LogAgentService&#39;</span><span class="p">;</span>     <span class="n">Path</span><span class="p">=</span><span class="s1">&#39;...\Log Agent\liwinsvc.exe&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span><span class="p">=</span><span class="s1">&#39;WindowsUpdates&#39;</span><span class="p">;</span>  <span class="n">Folder</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">updPath</span><span class="p">;</span>  <span class="n">File</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">updInstallFile</span><span class="p">;</span>  <span class="n">Timeout</span><span class="p">=</span><span class="mf">45</span><span class="p">;</span> <span class="n">Reboot</span><span class="p">=</span><span class="vm">$true</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><p>Adding a product is adding an element. Each installer runs from the local
staging copy with a hung-installer timeout; exit codes 0 and <strong>3010</strong>
(success, reboot required) count as success. When a <code>Reboot=$true</code> step
succeeds, the script restarts the machine and exits; on the next boot the
task re-runs it, completed steps skip via their flags, execution resumes at
the next step.</p>
<p>Post-install health isn&rsquo;t &ldquo;installer said OK&rdquo; — it&rsquo;s <strong>flag present AND
service running AND install path exists</strong>, with a 120-second wait for
delayed-start services. Windows Updates is flag-only; there&rsquo;s no service
to check.</p>
<h2 id="phase-2-cleanup--leave-nothing-behind">Phase 2: cleanup — leave nothing behind</h2>
<ul>
<li><strong>Eject the config-drive ISO.</strong> In Session 0 there&rsquo;s no Explorer, so
the usual shell ejection doesn&rsquo;t work. A P/Invoke to <code>winmm</code>&rsquo;s
<code>mciSendString(&quot;set cdaudio door open&quot;)</code> does.</li>
<li><strong>Restore UAC.</strong> <code>EnableLUA</code> and <code>FilterAdministratorToken</code> were relaxed
in the base image so the build could run unattended; re-enable both.</li>
<li><strong>Uninstall cloudbase-init.</strong> Stop and delete the service, kill its
processes, run the uninstaller silently, remove the directory. Guarded
by <code>99_cleanup_complete.flag</code>.</li>
</ul>
<p>The startup task is deleted <em>before</em> validation, so the health check can
truthfully report &ldquo;no automation task remains&rdquo;.</p>
<h2 id="phase-3-collect-the-evidence">Phase 3: collect the evidence</h2>
<p>The script assembles <code>data-validation.json</code> — the single input to the
report in <a href="/posts/windows-2025-aria-part-3/">part 3</a>:</p>
<table>
	<thead>
			<tr>
					<th>Collector</th>
					<th>Captures</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Network</td>
					<td>per active adapter: alias, IPv4, prefix, gateway, MAC, DNS</td>
			</tr>
			<tr>
					<td>Infrastructure</td>
					<td><code>guestinfo.vra.infrastructure</code> via <code>vmtoolsd</code>, <strong>15 attempts × 10 s</strong> (the vRO subscription can land late)</td>
			</tr>
			<tr>
					<td>OS / hardware</td>
					<td>domain, CPUs, RAM, caption, uptime, time zone, pending reboot, latest hotfix</td>
			</tr>
			<tr>
					<td>AD / security</td>
					<td>local Administrators, secure-channel test, machine OU</td>
			</tr>
			<tr>
					<td>Disks</td>
					<td>per volume: letter, label, size, free</td>
			</tr>
			<tr>
					<td>Post-build health</td>
					<td>WinRM, RDP, UAC enabled, startup task gone, domain DNS resolves, NTP source</td>
			</tr>
			<tr>
					<td>Software</td>
					<td>per app: flag + service + path</td>
			</tr>
			<tr>
					<td>Installed apps</td>
					<td>both uninstall hives (64-bit and WOW6432)</td>
			</tr>
	</tbody>
</table>
<p>The verdict is strict: <strong><code>GuestStatus = Success</code> only if every software
item is healthy <em>and</em> the machine is domain-joined.</strong> Anything else renders
the report banner red.</p>
<h2 id="phases-4-and-5-publish-then-self-destruct">Phases 4 and 5: publish, then self-destruct</h2>
<p>Remap the share with the <strong>write</strong> account (5 × 10 s retries — a
different account from the read-only one used for pulls; a compromised
build guest can&rsquo;t tamper with the engine). Render the HTML report locally.
Write <code>100_build_complete.flag</code> — kill switch armed. Copy the report plus
<code>Data\</code>, <code>Flags\</code>, <code>Logs\</code> to <code>\\share\Builds\&lt;image&gt;\&lt;HOSTNAME&gt;\</code>. Stop the
transcript <em>before</em> copying logs so the final log is complete and unlocked.</p>
<p>Then: delete the startup task; overwrite both share passwords in the
on-disk payload with <code>*** SCRUBBED ***</code>; delete <code>Data\</code> and <code>Staging\</code>
(and <code>Flags\</code>/<code>Logs\</code> if the share copy succeeded); delete the sibling
scripts and itself; remove <code>Scripts\</code>; reboot one final time.</p>
<p>The delivered server boots clean, domain-joined, agents running, and
carries <strong>no credentials, payloads or tooling</strong>.</p>
<h2 id="the-reboot-sequence-of-a-nominal-build">The reboot sequence of a nominal build</h2>
<ol>
<li>After static networking (<code>00</code>, exit 1003)</li>
<li>After engine pull (<code>03</code>, exit 1003) — ends the cloudbase-init phase</li>
<li>After Windows Updates (<code>05</code>, <code>Reboot=$true</code>)</li>
<li>Final, after publish and self-destruct</li>
</ol>
<p>The report&rsquo;s timeline chart finds the update reboot automatically: any gap
over 30 seconds between steps is shaded and labelled REBOOT.</p>
<p>At completion the <code>Flags\</code> folder is the whole history of the build in
file names:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">Flags</span>  <span class="mi">00</span><span class="n">_config</span><span class="o">-</span><span class="n">network</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">01</span><span class="n">_config</span><span class="o">-</span><span class="n">disks</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">02</span><span class="n">_join</span><span class="o">-</span><span class="n">domain</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">03</span><span class="n">_init</span><span class="o">-</span><span class="n">puller</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">04</span><span class="n">_stage</span><span class="o">-</span><span class="n">payloads</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">01</span><span class="n">_MonitoringAgent_installed</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">02</span><span class="n">_InventoryAgent_installed</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">03</span><span class="n">_EndpointSecurity_installed</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">04</span><span class="n">_LogAgent_installed</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">05</span><span class="n">_WindowsUpdates_installed</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">99</span><span class="n">_cleanup_complete</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">100</span><span class="n">_build_complete</span><span class="o">.</span><span class="n">flag</span>        <span class="o">&lt;-</span> <span class="n">kill</span> <span class="k">switch</span>
</span></span></code></pre></div><p>And the persisted timings turn into this, in the report from <a href="/posts/windows-2025-aria-part-3/">part
3</a>:</p>
<p><img alt="Provisioning timeline from the build report: five install steps, a shaded four-minute REBOOT gap after Windows Updates, then cleanup, collection and write steps" loading="lazy" src="/images/ui/a4-aria-report-timeline.jpg">
<em>Every bar sits at its true wall-clock position across the reboots because the state file carried the timings. The amber band is the reboot after Windows Updates, found automatically from the gap.</em></p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>What customers get from a reboot-safe build is predictability: every
server takes the same steps in the same order, survives the reboots
Windows insists on, and finishes clean — with no tooling, no credentials
and no leftover tasks on the delivered machine. That last part matters to
security reviewers as much as the first part matters to operations. And
because every step&rsquo;s timing is recorded, &ldquo;why did this build take twice as
long?&rdquo; has an answer instead of a guess.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>A reboot-safe build is <strong>kill switch + per-step flags + persisted
timings</strong>. Nothing cleverer is needed.</li>
<li>Treat exit <strong>3010</strong> as success. Let the <em>step</em> declare whether to
reboot; the loop handles it.</li>
<li>Health = flag <strong>and</strong> service <strong>and</strong> path. Installer exit codes lie.</li>
<li>Read platform-injected metadata with <strong>retries</strong> — the injecting
workflow may land after the guest starts looking.</li>
<li>Two share accounts: read-only for pulls, write-only for publishing.</li>
<li>Delete the task before validating, so &ldquo;no task remains&rdquo; is checkable.</li>
<li>Stop the transcript before you copy the logs.</li>
<li>Scrub, delete yourself, reboot. The customer gets a server, not a
build environment.</li>
</ul>
<p><em>Part 3: <a href="/posts/windows-2025-aria-part-3/">validation as a product, and the details that hurt</a>.</em></p>
<hr>
<p><em>Lab write-up of a production pattern; customer specifics removed. Opinions
my own.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Windows Server 2025 via Aria Automation, part 1: the pipeline</title>
      <link>https://thenestedlab.com/posts/windows-2025-aria-part-1/</link>
      <pubDate>Thu, 17 Sep 2026 06:10:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/windows-2025-aria-part-1/</guid>
      <description>One catalog request, one fully-built domain-joined Windows Server 2025 with a standard agent stack, a validation report, and no trace of the tooling that built it. The three-layer design — Aria/vRO control plane, cloudbase-init first boot, a pulled build engine — and the reasoning behind each seam.</description>
      <content:encoded><![CDATA[<p>A user fills in a form: environment, size, disks, networks, tags. Some time
later there&rsquo;s a Windows Server 2025 VM that has a sequential hostname
allocated from Active Directory, static IPs on up to four NICs, its data
disks laid out and lettered, domain membership, the standard agent stack
installed and healthy, a pixel-perfect HTML build report on a file share —
and <strong>none of the automation tooling left on the box</strong>.</p>
<p>This three-part series is how that works. It&rsquo;s a VM Apps build (classic
Aria Automation cloud template plus vRO), and it&rsquo;s a good example of the
pattern: the platform does the allocation and the metadata; the guest does
the guest. Part 1 is the architecture. <a href="/posts/windows-2025-aria-part-2/">Part 2</a>
is the state machine that survives four reboots. <a href="/posts/windows-2025-aria-part-3/">Part 3</a>
is the validation report and the details that hurt.</p>
<blockquote>
<p>Customer-specific names, products and policies are generalised throughout.
The patterns are the point.</p>
</blockquote>
<h2 id="three-layers-three-reasons">Three layers, three reasons</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="err">┌──────────────────────────────────────────────────────────────────────┐</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span> <span class="mf">1.</span> <span class="n">Aria</span> <span class="n">Automation</span> <span class="o">+</span> <span class="n">Orchestrator</span>         <span class="p">(</span><span class="n">control</span> <span class="n">plane</span><span class="p">)</span>            <span class="err">│</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>    <span class="n">cloud</span> <span class="n">template</span> <span class="err">·</span> <span class="n">Event</span> <span class="n">Broker</span> <span class="err">→</span> <span class="n">vRO</span> <span class="n">at</span> <span class="n">Allocation</span> <span class="o">/</span> <span class="n">Post</span><span class="o">-</span><span class="n">Provision</span> <span class="err">│</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>    <span class="n">hostname</span> <span class="n">from</span> <span class="n">AD</span> <span class="err">·</span> <span class="n">placement</span> <span class="n">metadata</span> <span class="err">→</span> <span class="n">guestinfo</span> <span class="err">·</span> <span class="n">notifications</span>  <span class="err">│</span>
</span></span><span class="line"><span class="cl"><span class="err">├──────────────────────────────────────────────────────────────────────┤</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span> <span class="mf">2.</span> <span class="n">cloudbase</span><span class="o">-</span><span class="n">init</span>                          <span class="p">(</span><span class="n">first</span> <span class="n">boot</span><span class="p">,</span> <span class="n">template</span><span class="o">-</span><span class="n">owned</span><span class="p">)</span><span class="err">│</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>    <span class="n">multipart</span> <span class="n">userdata</span><span class="p">:</span> <span class="n">network</span> <span class="err">·</span> <span class="n">disks</span> <span class="err">·</span> <span class="n">domain</span> <span class="n">join</span> <span class="err">·</span> <span class="n">pull</span> <span class="n">engine</span>    <span class="err">│</span>
</span></span><span class="line"><span class="cl"><span class="err">├──────────────────────────────────────────────────────────────────────┤</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span> <span class="mf">3.</span> <span class="ne">File</span><span class="o">-</span><span class="n">share</span> <span class="n">build</span> <span class="n">engine</span>                 <span class="p">(</span><span class="n">guest</span> <span class="n">state</span> <span class="n">machine</span><span class="p">)</span>      <span class="err">│</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>    <span class="n">stage</span> <span class="n">payloads</span> <span class="err">·</span> <span class="n">install</span> <span class="n">across</span> <span class="n">reboots</span> <span class="err">·</span> <span class="n">validate</span> <span class="err">·</span> <span class="n">report</span> <span class="err">·</span> <span class="bp">self</span><span class="o">-</span><span class="err">│</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>    <span class="n">destruct</span>                                                           <span class="err">│</span>
</span></span><span class="line"><span class="cl"><span class="err">└──────────────────────────────────────────────────────────────────────┘</span>
</span></span></code></pre></div><p>Each seam exists for a reason you can state in one sentence:</p>
<ul>
<li><strong>Aria/vRO owns what needs platform credentials</strong> — querying AD for the
next hostname, reading vCenter for where the VM landed. The guest never
holds a vCenter or AD-admin credential.</li>
<li><strong>cloudbase-init owns what must happen before anything else</strong> — the
network has to work before the share can be reached; the disk layout
has to exist before installers land on it; and the domain join changes
the security context everything after it runs in, which is the whole
reason the next layer exists (more on that below).</li>
<li><strong>The engine is pulled, not embedded</strong> — because installers change,
agents get new versions, and re-releasing a cloud template for every
payload update is how automation dies. Update a script on the share;
every subsequent build gets it.</li>
</ul>
<p>vCenter guest customization is <strong>disabled</strong> (<code>customizeGuestOs: false</code>).
One code path owns hostname, networking, disks and join; two would fight.</p>
<h2 id="the-control-plane-two-vro-hooks-that-matter">The control plane: two vRO hooks that matter</h2>
<p><strong>Compute Allocation → sequential hostname.</strong> The template composes a
prefix from location/classification/environment/application inputs. A vRO
workflow queries AD computer objects with that prefix and allocates
<em>highest existing suffix + 1</em>. Gaps are never reused: with <code>-001</code>, <code>-002</code>,
<code>-003</code> and <code>-005</code> present, the next is <code>-006</code>. A gap usually means a deleted
machine whose name may still live in DNS, a backup catalogue or the CMDB;
reusing it is how you restore the wrong server.</p>
<p><strong>Compute Post Provision → placement metadata into the guest.</strong> A second
workflow asks vCenter where the VM actually landed — vCenter, datacenter,
cluster, host, folder, datastores, NIC→portgroup map — and writes it as
JSON into the VM&rsquo;s <code>extraConfig</code> as <code>guestinfo.vra.infrastructure</code>. Later,
inside the guest, <code>vmtoolsd.exe --cmd &quot;info-get guestinfo.vra.infrastructure&quot;</code>
reads it back. That&rsquo;s the bridge that lets the build report say &ldquo;this VM is
on host X, datastore Y&rdquo; with <strong>zero vCenter credentials in the guest</strong>.</p>
<p>Three more Post Provision workflows email the backup team, the SOC and the
requester. (One observation for part 3: the requester&rsquo;s &ldquo;your deployment
has completed&rdquo; fires here — before the in-guest build has even started.)</p>
<h2 id="first-boot-cloudbase-init-four-scripts-two-reboots">First boot: cloudbase-init, four scripts, two reboots</h2>
<p>The template&rsquo;s <code>cloudConfig</code> is a MIME multipart: one <code>cloud-config</code> part
(hostname + write the tags to disk) and four <code>#ps1_sysnative</code> scripts,
executed in order. Three conventions make them idempotent:</p>
<ul>
<li><strong>Flag files</strong> under <code>Flags\</code> — a script exits immediately if its flag
exists, so re-runs after a reboot are no-ops.</li>
<li><strong>Transcripts</strong> under <code>Logs\</code>, one per script.</li>
<li><strong>Exit 1003</strong> = &ldquo;reboot me and run this part again on next boot&rdquo; (the
flag then short-circuits it). Exit 0 = done. Exit 1 = failure.</li>
</ul>
<table>
	<thead>
			<tr>
					<th>Script</th>
					<th>Does</th>
					<th>Exit</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>00-config-network</code></td>
					<td>pairs adapters (by ifIndex) with <code>to_json(self.networks)</code> (by deviceIndex); static IP/gateway/DNS per NIC</td>
					<td><strong>1003</strong> — reboot 1</td>
			</tr>
			<tr>
					<td><code>01-config-disks</code></td>
					<td>extends C:, onlines/initialises/partitions/formats each extra disk from the request array</td>
					<td>0</td>
			</tr>
			<tr>
					<td><code>02-join-domain</code></td>
					<td>runs the join script via a one-shot scheduled task as local admin; verifies <code>CsDomain ≠ WORKGROUP</code></td>
					<td>0</td>
			</tr>
			<tr>
					<td><code>03-init-puller</code></td>
					<td>writes <code>data-payload.json</code>, encrypts the share secrets, pulls 04/05/06 from the share, launches staging</td>
					<td><strong>1003</strong> — reboot 2</td>
			</tr>
	</tbody>
</table>
<p>The network script is the one with an assumption worth writing on the
wall: it pairs OS adapters with the request&rsquo;s NICs <strong>positionally</strong>, which
is valid for freshly cloned VMs where NICs were added in PCI order. If
anyone ever customises NIC order post-clone, revisit it.</p>
<h2 id="why-not-just-cloudbase-init-or-guest-customization-all-the-way-down">Why not just cloudbase-init (or guest customization) all the way down?</h2>
<p>The obvious design is the one we started with: let the platform do the
guest. vCenter guest customization for hostname, IP and domain join, then
one cloudbase-init userdata that installs the agents, patches, and
reports back. No scheduled tasks, no pulled engine, no state machine. It
works on a workgroup machine. It stops working the moment the machine
joins a real domain, and it stops in a way that is easy to misread.</p>
<p><strong>The domain join changes the rules mid-build.</strong> Everything up to the
join runs as a fresh, local, un-managed Windows install: local
Administrator, default execution policy, no central policy. At the join
the machine lands in its target OU, and on the next policy refresh (which
the reboot guarantees) <strong>Group Policy applies</strong>. In this environment the
policy set for member servers includes the usual security baseline:
script-execution controls, restrictions on what may run from where and
under which accounts, and hardening of the local administrator context.
None of that is negotiable, and none of it should be — it is the same
policy every production server gets.</p>
<p><strong>What that does to a first-boot pipeline.</strong> cloudbase-init runs its
plugins as a service, as LocalSystem, executing scripts from its own
directory. Before the join that context can do anything. After the join
it is exactly the kind of context the baseline is designed to constrain,
so the parts of the build that come <em>after</em> the join — the installer
pulls, the agent installs, the reboots, the validation — either fail
outright or, worse, quietly do nothing: cloudbase-init logs the plugin as
executed, the script never ran anything, and the build &ldquo;completes&rdquo; with
an unpatched server carrying no agents. The first few builds looked
exactly like that.</p>
<p><strong>Two ways out, one of them wrong.</strong> You can relax policy for the build
(a staging OU with a weaker baseline, a GPO exemption for the
cloudbase-init path, a delayed join) — which means the server is built
under one set of rules and delivered under another, and the join
becomes a late step that nothing after it exercises. Or you can accept
the policy as the environment it is, and run the post-join work in a
context the policy <em>permits</em>.</p>
<p><strong>The permitted context is a scheduled task under an explicit identity.</strong>
A task registered to run as a named account — a domain-joined local admin
for the pull and the join verification, SYSTEM for the build master — at
highest run level, with <code>-ExecutionPolicy Bypass</code> per invocation, from a
staging path the policy allows, is an ordinary, auditable pattern that
the baseline was written to accommodate. That is why the OS commands
aren&rsquo;t run <em>by</em> cloudbase-init but delegated to tasks: cloudbase-init&rsquo;s
job shrinks to &ldquo;get the network up, lay out disks, join, hand off&rdquo;. The
design also picks up three things it now depends on:</p>
<ul>
<li>a <strong>per-task execution ceiling</strong> (1 h for the pull, 2 h for the build)
that contains a hung installer instead of leaving a half-built VM;</li>
<li>an <strong>at-startup trigger</strong>, which is what makes a multi-reboot state
machine possible after cloudbase-init has been uninstalled;</li>
<li>a clean <strong>security story</strong>: the identities that do the work are the
ones the domain already governs, and they stop existing on the box when
the build is done.</li>
</ul>
<p><strong>Why not Aria&rsquo;s own in-guest mechanisms?</strong> Guest customization only
covers hostname, IP and join, and having it <em>and</em> cloudbase-init own the
same settings means two code paths fighting (hence
<code>customizeGuestOs: false</code>). Driving the guest from outside — vRO calling
into the VM for two hours — needs guest credentials held centrally and
keeps a management path open for the whole build; ABX can&rsquo;t reach inside
the guest at all. The platform&rsquo;s job is what needs platform credentials:
the hostname from AD, the placement facts from vCenter. The guest does
the guest, under the domain&rsquo;s rules, from the first reboot after the
join.</p>
<p>Even if the GPO were relaxed tomorrow, don&rsquo;t simplify the wrappers away:
the ceiling and the startup trigger are worth having on their own.</p>
<h2 id="the-hand-off-03-init-puller">The hand-off: <code>03-init-puller</code></h2>
<p>This is where template-embedded code stops and the centrally managed
engine starts:</p>
<ol>
<li>Create <code>Flags\ Logs\ Data\ Scripts\</code> under the log folder.</li>
<li>Write <strong><code>data-payload.json</code></strong> — the one document everything after this
reads: share path and accounts, image, project/deployment/requester,
the installer folder/file pairs, the tags, the placement facts.</li>
<li><strong>Encrypt the two share passwords</strong> with AES, key derived from the
machine&rsquo;s BIOS UUID. The payload is useless copied off-box.</li>
<li>Generate a runner, execute it via a one-shot task as local admin: map
the share read-only, copy <code>04-stage-payloads</code>, <code>05-build-master</code>,
<code>06-validate-build</code> locally, unmap, launch staging.</li>
<li>Write the flag, exit 1003. Reboot 2. cloudbase-init&rsquo;s job is done.</li>
</ol>
<p><code>04-stage-payloads</code> copies every installer to local disk (installers never
run across SMB — immune to network blips and file locks mid-install) and
registers the <strong><code>Build-Master</code> startup task</strong>. From here on, every boot
runs <code>05-build-master.ps1</code> until the build is complete.</p>
<p>What the requester actually sees is a short form. Stripped of the
site-specific enum values, the inputs are:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">location</span><span class="p">:</span><span class="w">        </span><span class="c"># site code -&gt; hostname prefix</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">classification</span><span class="p">:</span><span class="w">  </span><span class="c"># security zone -&gt; hostname prefix, OU</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">environment</span><span class="p">:</span><span class="w">     </span><span class="c"># prod / pre-prod / test -&gt; hostname prefix, tags</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">application</span><span class="p">:</span><span class="w">     </span><span class="c"># application code -&gt; hostname prefix, folder</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w">           </span><span class="c"># Windows2025 (the template is image-versioned)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">flavor</span><span class="p">:</span><span class="w">          </span><span class="c"># Small / Medium / Large -&gt; vCPU + RAM</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">count</span><span class="p">:</span><span class="w">           </span><span class="c"># number of identical machines</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">bootDiskSizeGB</span><span class="p">:</span><span class="w">  </span><span class="c"># C: (extended in-guest by 01-config-disks)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">primaryNetwork</span><span class="p">:</span><span class="w">  </span><span class="c"># required</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">network2..4</span><span class="p">:</span><span class="w">     </span><span class="c"># optional; each becomes a static NIC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">additionalDisks</span><span class="p">:</span><span class="w"> </span><span class="c"># [{number, name, letter, sizeGB}] -&gt; D:, L:, ...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tags</span><span class="p">:</span><span class="w">            </span><span class="c"># free-form key/value -&gt; written to disk, shown in the report</span><span class="w">
</span></span></span></code></pre></div><p>Every one of those either shapes the hostname, lands in <code>guestinfo</code>, or
is written to disk for the build engine to read. Nothing is entered twice.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>For an organisation, this pipeline turns a Windows server from something a
person builds into something the platform <em>delivers</em>: a request in a
catalog, a domain-joined and agent-loaded server out, with the security
team&rsquo;s controls (naming, join, hardening, monitoring agents) applied every
time because they&rsquo;re in the pipeline, not in a checklist. The separation of
concerns is what makes it maintainable — the platform holds the
credentials, the guest does the work, and a change to the software stack
is a script update on a share rather than a template re-release.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Split by <strong>who holds the credential</strong>: platform queries AD/vCenter,
guest never does. <code>guestinfo</code> is the one-way bridge.</li>
<li><strong>Disable vCenter customization</strong> when cloudbase-init owns the guest.
One owner.</li>
<li><strong>Pull the engine</strong> from a share; embed only what must run before the
network exists.</li>
<li>Flag files + transcripts + exit 1003 = idempotent, reboot-safe
first-boot scripts.</li>
<li>Anything after the domain join runs in a <strong>scheduled task under an
explicit identity</strong> — for policy, for the execution ceiling, and for the
startup trigger.</li>
<li>Never reuse hostname gaps.</li>
</ul>
<p><em>Part 2: <a href="/posts/windows-2025-aria-part-2/">the state machine that survives four reboots</a>.</em></p>
<hr>
<p><em>Lab write-up of a production pattern; customer specifics removed. Opinions
my own.</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
