Use a WIM to Deploy Large Apps via ConfigMgr App Model

Inspiration for this post comes from Aaron Y (@_aarony). Follow him for more ConfigMgr tips.

What's this all about?

If you've only ever deployed MSIs or self-extracting EXEs that require little customization, then you've likely never experienced the unique challenge presented by enterprise installers that generate enormous install packages (multiple gigs, tens of thousands of files). In my particular case, Autodesk installers are a perfect example.

When you utilize the app model for software distribution in ConfigMgr, every single file in the source folder gets hashed so that the CM client can validate each file during the download process. So if your installer package contains 30,000 files like my Autodesk one does, that can take a 15 minute download and turn it into an hours-long (or days-long) process that monopolizes CPU resources for a never-ending stream of hash checks.

The common workaround for this is to manually compress the final install package into a single archive file. This way, you save on download bandwidth and the PC only has to hash check the one archive file. This works fine, but there's a tradeoff... disk space. As a first step in your install script, you have to extract the archive to a local temp folder before you can run the actual install. In the case of my Civil 3D deployment, the total disk space needed was close to 30GB. (Roughly 6GB for the initial download including the ZIP archive, then 11GB to extract the ZIP archive, then around 12GB to install everything.) Once the install is complete, I can release 17GB back to the user by purging the client cache, but if that disk space doesn't exist for my temporary use, we've got a problem.

And a WIM is better?

You bet it is, and here's why. We get all the same hash-check and bandwidth optimization benefits of the ZIP file approach, but we add one more: Windows can mount a WIM directly, without the need to fully extract before reading the sources for the install. In my case, that means I don't need the 11GB for ZIP extraction - an approximate 37% reduction in required disk space for a successful deployment. That's huge. As long as you're comfortable using powershell wrapper scripts for your deployments, using this approach is incredibly simple. At a high level, it looks like this:

  • Stage your installer sources in a temp folder and create the WIM.
  • In your PowerShell install script, mount the WIM to an empty folder.
  • Call your installers normally from the mounted location.
  • Ensure your PowerShell script properly unmounts the WIM before exiting.

Step 1: Create the WIM

For this example, I'll use my actual Civil 3D 2020 sources which include various installers for C3D, Raster and Vehicle Tracking, as well as hundreds of MBs of custom template/resource files specific to my org. As I would for the traditional archive method, I have staged all my install sources in a temp folder.

WIM-01

Instead of compressing to a ZIP or other archive format, we can leverage PowerShell's built-in DISM module to capture the folder as a windows image (WIM) file using the New-WindowsImage cmdlet. In my case:

New-WindowsImage -ImagePath "f:\CADWIM\Files.wim" -CapturePath "f:\CADWIM\Files" -Name "InstallerSources"

In general, it's worth it to use the -CompressionType Max parameter. It takes a little longer to create the WIM, but you'll save on download time and bandwidth on the other end. (Edit: Some smart people have suggested that Fast mode is better so as not to cause issues with deduplication. I like to listen to smart people.) For reference, here's the resulting file sizes when using the Fast vs. Max options, as well as the resulting size of a 7zip archive using Maximum compression option. As you can see 7zip certainly achieves better compression, but the WIM still does an acceptable job. The source folder was just over 11GB.

WIM-02

We can now incoporate this WIM into our MEMCM app model install script.

Step 2: Mounting the WIM

As mentioned, we don't need to extract this WIM, we can simply mount it and allow Windows to read from it directly. In your install script, prior to calling any of the installers, mount the WIM using Mount-WindowsImage. Note that you must create an empty folder to use as the mount target. I prefer to mount this inside the ccmcache folder, though that's not required.

$mountPath = "$PSScriptRoot\mount"
New-Item -Path $mountPath -ItemType Directory
Mount-WindowsImage -ImagePath "[path\to\Files.wim]" -Index 1 -Path $mountPath

As opposed to waiting 3-5 minutes for a ZIP decompression, you should see that it only takes around 10 seconds for that WIM to mount. Now your install can proceed as normal by calling the installer sources from the $mountPath location.

Step 3: Unmounting the WIM

Once your install is complete, it's important to properly unmount the WIM before exiting your install wrapper script using Dismount-WindowsImage. Significantly, this should happen whether your install succeeds or not. One way of ensuring you always unmount would be to use a try-catch-finally block. Also, there's no reason to save any changes to the mounted WIM, so we'll specify the option to Discard.

Updated 7/20/2020 per Aaron Y to include additional logic that sets a scheduled task to perform cleanup later if the first unmount attempt fails.

try {
    ## mount the WIM here
}
catch {
    ## failed to mount the WIM, can't proceed
    exit 1
}

try {
    ## perform your install here
    
    ## set exit return code to success value
    $returnCode = 0
}
catch {
    ## handle/log the errors, etc.
    
    ## set exit return code to error value
    $returnCode = 1
}
finally {
    ## dismount the WIM whether we succeeded or failed
    try {
        Dismount-WindowsImage -Path $mountPath -Discard
    }
    catch {
        ## failed to cleanly dismount, so set a task to cleanup after reboot
        $STAction = New-ScheduledTaskAction `
            -Execute 'Powershell.exe' `
            -Argument '-NoProfile -WindowStyle Hidden -command "& {Get-WindowsImage -Mounted | Where-Object {$_.MountStatus -eq ''Invalid''} | ForEach-Object {$_ | Dismount-WindowsImage -Discard -ErrorVariable wimerr; if ([bool]$wimerr) {$errflag = $true}}; If (-not $errflag) {Clear-WindowsCorruptMountPoint; Unregister-ScheduledTask -TaskName ''CleanupWIM'' -Confirm:$false}}"'
            
        $STTrigger = New-ScheduledTaskTrigger -AtStartup
        
        Register-ScheduledTask `
            -Action $STAction `
            -Trigger $STTrigger `
            -TaskName "CleanupWIM" `
            -Description "Clean up WIM Mount points that failed to dismount properly" `
            -User "NT AUTHORITY\SYSTEM" `
            -RunLevel Highest `
            -Force
    }
    
    ## return exit code
    exit $returnCode
}

If any processes still possess a read lock on content in the WIM file, the dismount will fail. So be sure that all processes referencing the WIM content have fully exited before unmounting.

In Conclusion

That's pretty much it. The WIM is a little bigger than some other archive formats. But in my testing, the additional download time was offset by the time saved by not needing to extract the archive. And the 37% reduction in required disk space makes it all worth it either way. Thanks again to Aaron for the tip!


I hope this was helpful. If you have any comments or questions, or if you have an idea about how to further improve this approach, you can connect with me via the comments below or via Twitter.

Show Comments