Exercise: Build and inventory the Bindery estate

In this exercise you'll create the legacy estate that the rest of the course migrates. Building it yourself takes ten minutes and pays for itself three times over: you'll recognize every construct when you translate it, and you'll have a safe target to break.

[!NOTE]
The Bindery estate is the same running example used in The Microsoft DSC Handbook, Chapter 11. You don't need the book: every file you need is written out in full in the steps below.

Prerequisites

  • Windows with Windows PowerShell 5.1 and PowerShell 7+
  • The dsc CLI on your path (dsc --version should respond)
  • An elevated shell — AppEnvironment writes machine-level environment variables

Create a working folder for the lab and use it for the rest of the course. Everything you create here is reused in Modules 2 and 3.

New-Item -Path C:\BinderyLab -ItemType Directory -Force
Set-Location C:\BinderyLab

[!WARNING]
Do this on a lab machine or a VM, not on a workstation you care about. You'll compile MOF files, register a module in a PowerShell module path, and set machine-level environment variables.

Step 1: Create the script-based resource

Create the module folder structure:

$module = 'C:\Program Files\WindowsPowerShell\Modules\BinderyOps'
New-Item -Path "$module\DSCResources\AppEnvironment" -ItemType Directory -Force

Create AppEnvironment.psm1 in $module\DSCResources\AppEnvironment. This is the pattern you'll recognize from every script resource ever written — three functions, and the same parameters declared in all three:

function Get-TargetResource {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory)]
        [string]$Name
    )

    $value = [Environment]::GetEnvironmentVariable($Name, 'Machine')

    return @{
        Name   = $Name
        Value  = $value
        Ensure = if ($null -ne $value) { 'Present' } else { 'Absent' }
    }
}

function Test-TargetResource {
    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory)]
        [string]$Name,

        [string]$Value,

        [ValidateSet('Present', 'Absent')]
        [string]$Ensure = 'Present'
    )

    $current = Get-TargetResource -Name $Name

    if ($Ensure -eq 'Absent') {
        return ($current.Ensure -eq 'Absent')
    }

    return ($current.Ensure -eq 'Present' -and $current.Value -eq $Value)
}

function Set-TargetResource {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Name,

        [string]$Value,

        [ValidateSet('Present', 'Absent')]
        [string]$Ensure = 'Present'
    )

    if ($Ensure -eq 'Present') {
        [Environment]::SetEnvironmentVariable($Name, $Value, 'Machine')
    } else {
        [Environment]::SetEnvironmentVariable($Name, $null, 'Machine')
    }
}

Export-ModuleMember -Function Get-TargetResource, Test-TargetResource, Set-TargetResource

A script-based resource is nothing without its schema. Create AppEnvironment.schema.mof next to the .psm1:

[ClassVersion("1.0.0"), FriendlyName("AppEnvironment")]
class AppEnvironment : OMI_BaseResource
{
    [Key, Description("The name of the environment variable.")] String Name;
    [Write, Description("The value of the environment variable.")] String Value;
    [Write, Description("Whether the variable should exist."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure;
};

[!NOTE]
The schema MOF is what makes the resource discoverable and what declares which property is the key. When you rewrite this resource as a class in Module 2, this file disappears because its information moves into [DscProperty()] attributes.

Finally, the module manifest at the root of $module, as BinderyOps.psd1:

@{
    RootModule           = ''
    ModuleVersion        = '1.0.0'
    GUID                 = 'b3d4f7a1-6c2e-4e8a-9f31-8d5a2c7e4b90'
    Author               = 'Bindery Operations'
    Description          = 'Bindery operational resources.'
    PowerShellVersion    = '5.1'
}

Replace the GUID value with one of your own. Every module needs a unique identifier, and New-Guid generates one:

(New-Guid).Guid

Keep a note of it. When you write the version 2.0.0 manifest in Module 2, you reuse the same GUID, because both versions are the same module.

Step 2: Write the legacy configuration

In your working folder, create LegacyWebServer.ps1:

# LegacyWebServer.ps1 - the "before" state: classic PowerShell DSC
Configuration LegacyWebServer {
    param (
        [Parameter(Mandatory)]
        [string]$AppRoot
    )

    Import-DscResource -ModuleName PSDesiredStateConfiguration
    Import-DscResource -ModuleName BinderyOps

    Node 'localhost' {
        File AppDirectory {
            DestinationPath = $AppRoot
            Type            = 'Directory'
            Ensure          = 'Present'
        }

        AppEnvironment ApiUrl {
            Name      = 'BINDERY_API_URL'
            Value     = 'https://api.bindery.example'
            Ensure    = 'Present'
            DependsOn = '[File]AppDirectory'
        }
    }
}

LegacyWebServer -AppRoot 'C:\BinderyApp' -OutputPath .\output

Keep this picture in your head, because each migration path transforms a different part of it: the Configuration keyword, the Node block, Import-DscResource, Ensure strings, and a compile step that produces output\localhost.mof.

Step 3: Compile and apply

In Windows PowerShell 5.1 (not PowerShell 7):

powershell.exe -NoProfile -Command ".\LegacyWebServer.ps1"

You should now have output\localhost.mof. Apply it the classic way:

Start-DscConfiguration -Path .\output -Wait -Verbose -Force

Confirm the machine reached the desired state:

Test-DscConfiguration -Detailed
[Environment]::GetEnvironmentVariable('BINDERY_API_URL', 'Machine')

You now have a machine in a known-good state under the LCM. That state is your baseline. Every migration path in this course has to reproduce it exactly.

Step 4: Build the inventory

Save the inventory script from unit 3 as Get-DscMigrationInventory.ps1 and run it from Windows PowerShell 5.1:

.\Get-DscMigrationInventory.ps1 | Format-Table -AutoSize

Look for the BinderyOps/AppEnvironment row and check three columns:

Column Expected value Why
Implementation ScriptBased It's a Get/Set/Test-TargetResource trio with a schema MOF
VisibleTo51 True The Windows PowerShell adapter discovers script-based resources
VisibleToPs7 False The PowerShell 7+ adapter supports class-based resources only
SuggestedPath Adapt (5.1), consider class-based rewrite Derived from the two visibility columns

Step 5: Confirm the adapter views by hand

Don't take the script's word for it. Ask each adapter directly:

dsc resource list --adapter Microsoft.Adapter/WindowsPowerShell | Select-String 'BinderyOps'
dsc resource list --adapter Microsoft.Adapter/PowerShell | Select-String 'BinderyOps'

The first returns a row. The second returns nothing. That asymmetry is the whole reason Path C exists.

Troubleshooting

Symptom Cause Fix
Get-DscResource doesn't list AppEnvironment The module isn't on 5.1's $env:PSModulePath Print $env:PSModulePath in Windows PowerShell and confirm the parent folder is listed
dsc resource list returns nothing for either adapter The adapters aren't installed, or dsc can't find them Run dsc resource list --adapter Microsoft.Adapter/WindowsPowerShell -l debug and read the discovery trace
The schema MOF is rejected at compile time File name or class name mismatch The file must be <ResourceName>.schema.mof and the class name must match the folder name
Start-DscConfiguration fails with an access error Not elevated Machine-level environment variables require administrator rights

Check your work

You're ready for the next unit when all four statements are true:

  • [ ] output\localhost.mof exists and was produced by your own compile step
  • [ ] Test-DscConfiguration reports True
  • [ ] BINDERY_API_URL resolves to https://api.bindery.example at machine scope
  • [ ] Your inventory shows BinderyOps/AppEnvironment as visible to the 5.1 adapter only

[!TIP]
Snapshot the VM here if you can. Every path in Module 2 starts from this exact state, and re-running the paths from a clean baseline is the fastest way to compare their results honestly.

> EOF