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
dscCLI on your path (dsc --versionshould respond) - An elevated shell —
AppEnvironmentwrites 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.mofexists and was produced by your own compile step - [ ]
Test-DscConfigurationreportsTrue - [ ]
BINDERY_API_URLresolves tohttps://api.bindery.exampleat machine scope - [ ] Your inventory shows
BinderyOps/AppEnvironmentas 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