Showing posts with label module. Show all posts
Showing posts with label module. Show all posts

Friday, 12 February 2016

Automating IIS App Pool Compliance


Background

With many servers comes the need to simply configuration management. I find application pools to be a constant source of headaches when it comes to keeping things consistent. They have loads of settings, which often get changed when trying to troubleshoot a problem (and not changed back). There’s also many other situations where they need to be configured; moving an application to another server, operating system upgrades, migrations, rationalisation, enforcing consistency - to name a few.

This has led me to automate the process of app pool configuration.

There are two scenarios to be covered – an ‘Online’ and an ‘Offline’ version. The Online version uses PowerShell remoting to connect to another IIS server that contains all the app pools and their required configurations. It then takes those settings and applies them to the target server.

The Offline version uses an existing IIS server and exports the app pool settings to a csv file, and then uses that to apply the settings to a target server.

Note that this process does not cover how to create app pools. This can easily be done by using MS Web Deploy if migrating a site, or New-WebAppPool if setting up a new site.

The process only works for IIS versions 7 and above. It is possible to migrate app pool settings from IIS v6 to IIS v7 onwards but it is more complicated than the process described here. The app pool settings are not the same between these versions, plus standard PowerShell commands and the IIS: drive are not available due to the limitations of PowerShell v2 (which is the highest version that can be used on an IIS 6.0 server).


Preparation

The first step is to decide which settings need to be configured. I like to set my app pool defaults first, and then explicitly set anything which hasn’t been caught by the default settings.

Setting the app pool defaults will change any app pool setting that isn’t the same as the new default setting or hasn’t been explicitly set, so use with caution. Setting app pool defaults is usually a one off setting when the server is new, or for a new global setting where there is a requirement to overwrite all existing settings.

To keep things simple I won’t go into the details of how to set app pool defaults, what I use for defaults, and how to revert an explicit app pool setting to inherit the default, I’ll try and cover that in a future blog post.

For now, what is important is that I want to set some app pool settings to a value that is different to the default.

The following settings need to be assessed as they may need to be different to the defaults:

.NET CLR Version
Enable 32 bit Applications
Managed Pipeline Mode
Identity

I also want to change the Recycling > Specific Times setting. My default is to recycle at 02:00am every day. I want to change this so that each app pool recycles at a random time between 02:00 – 02:59. I already have a separate script for this, see here for a blog post.
 

Offline Mode – export

This method uses an existing IIS server and exports the app pool settings to a csv file, and then uses that to apply the settings to a target server.

The following script is used to obtain and export these settings to csv file.

Import-Module webadministration

$AppPoolProperties = @()

foreach ( $pool in (get-item IIS:\AppPools\*) ) {

$Properties = New-Object System.Object

$Properties | Add-Member -type NoteProperty -name AppPoolName `
    -Value $($pool.name)

$Properties | Add-Member -type NoteProperty -name Enable32bit `
    -Value $($pool.enable32BitAppOnWin64)

$Properties | Add-Member -type NoteProperty -name Runtime `
    -Value $($pool.managedRuntimeVersion)

$Properties | Add-Member -type NoteProperty -name Pipeline `
    -Value $($pool.managedPipelineMode)

$Properties | Add-Member -type NoteProperty -name ProcessModel `
    -Value $($pool | Get-ItemProperty -name processModel.identityType)

$AppPoolProperties += $Properties
}

$AppPoolProperties | Export-Csv D:\AppPools.csv -NoTypeInformation

 
Breakdown

Import-Module WebAdministration
This loads the WebAdministration module which is required to make the IIS: drive available.

    $AppPoolProperties = @()
Creates an empty variable called AppPoolProperties and declares it as an array

Foreach loop

    foreach ( $pool in (get-item IIS:\AppPools\*) )

$pool is a temporary variable that only exists in the loop. This variable represents each member of the second command in the brackets –

    (get-item IIS:\AppPools\*)

This returns a list of app pool objects, so $pool will represent each app pool object in turn.

    $Properties = New-Object System.Object

Creates a new temporary object, see this link for an excellent explanation of custom objects:


The next section creates a variable for each of the app pool settings I want. This is generally straightforward and can be done by using $pool.property

    $pool.enable32BitAppOnWin64

To see all the app pool properties available use

    get-item IIS:\AppPools\* | Select-Object –first 1| Format-List –property *

Some of the properties cannot be obtained by using $pool.property

ProcessModel is an object type Microsoft.IIs.PowerShell.Framework.ConfigurationElement

To get its value I have to use Get-ItemProperty. This command will return the Identity Type for Process Model

    $pool | Get-ItemProperty -name processModel.identityType

Next I need to create on object that has each app pool value as a property. I use Add-Member to assign each of the app pool properties to the $properties object. Piping the $properties object created earlier to Add-Member assigns a name and value for each app pool property.

Viewing the $properties object after adding the properties for a single app pool looks like this

PS D:\scripts\modules> $Properties
AppPoolName  : .NET v2.0 Classic
Enable32bit  : False
Runtime      : v2.0
Pipeline     : Classic
ProcessModel : ApplicationPoolIdentity

    $AppPoolProperties += $Properties

Finally the $properties object is added to the $AppPoolProperties variable, and each time the foreach loop runs, another set of app pool properties are added to this variable. Using += adds the current object to the variable rather than overwriting the existing contents of the variable.

Once the loop is complete the final command uses Export-CSV to write $AppPoolProperties to CSV

    $AppPoolProperties | Export-Csv -Path D:\AppPools.csv –NoTypeInformation

Copy the CSV file to your target server, then run the Import script.
 
 
Offline Mode - import

The following script uses the exported CSV file to set each app pool property. It takes the App Pool name, and looks for a matching app pool, then sets the properties according to the setting in the CSV file. Once the app pool settings are in the CSV file, and the CSV file is stored on the server, this script can be run as a one off, or repeatedly to enforce the settings that are stored in the CSV file.
 

Import-Module webadministration

$AppPools = (Import-Csv D:\AppPools.csv)

foreach ( $pool in ( Get-Item IIS:\AppPools\* )) {

Set-ItemProperty IIS:\AppPools\$($pool.name) -Name enable32BitAppOnWin64 -Value `
    "$($AppPools | where { $_.AppPoolName -eq $pool.name } | Select-Object -ExpandProperty Enable32bit)"

Set-ItemProperty IIS:\AppPools\$($pool.name) -Name managedRuntimeVersion -Value `
    "$($AppPools | where { $_.AppPoolName -eq $pool.name } | Select-Object -ExpandProperty Runtime)"

Set-ItemProperty IIS:\AppPools\$($pool.name) -Name managedPipelineMode -Value `
    "$($AppPools | where { $_.AppPoolName -eq $pool.name } | Select-Object -ExpandProperty Pipeline)"

Set-ItemProperty IIS:\AppPools\$($pool.name) -Name processModel.identityType -Value `
    "$($AppPools | where { $_.AppPoolName -eq $pool.name } | Select-Object -ExpandProperty ProcessModel)"

}


Breakdown

    Import-Module WebAdministration

This loads the WebAdministration module which is required to make the IIS: drive available.

  $AppPools = (Import-Csv D:\AppPools.csv)

Creates a variable from the contents of the CSV file

Foreach loop

This is the same as the previous script. The purpose is to cycle through each local app pool, then change the settings accordingly.

    foreach ( $pool in (get-item IIS:\AppPools\*) )

The Set-ItemProperty command is quite long, so I will break it down.

    Set-ItemProperty IIS:\AppPools\$($pool.name)

The item property to be set is defined as the app pool in the current $pool variable, and is selected using the name property

    -Name enable32BitAppOnWin64

The name parameter is the name of the property being changed

    -Value

For the Value parameter I obtain the app pool value stored in the CSV file for the –name property

    "$($AppPools | where { $_.AppPoolName -eq $pool.name } | Select-Object -ExpandProperty Runtime)"

    $AppPools
Starts with the CSV contents in the variable

    where { $_.AppPoolName -eq $pool.name }
Filter with Where-Object to select only the row in the CSV file that matches the current app pool ($pool object) using the name property.

    Select-Object -ExpandProperty Runtime
Select the Property value with Select-Object, in this case the Runtime value, which is the managedRuntimeVersion

The whole command is then wrapped in $(), this returns the contents of the value object rather than the object itself.


Online mode - tbc ....
 

Wednesday, 19 August 2015

Import PowerShell Module from a UNC path share

Recently I have put all my most commonly used PowerShell scripts into a Module. By bundling them together into a module, I remove the need for individual script files to be maintained and updated on all servers in the network.

Doing this has caused a few problems of its own, so here is the process I used to get it working.

The first thing I did was copy the module to a shared location that can be accessed easily by everything on the network.

Next, anything that needs to use my module needs to be made aware of it. There are 2 steps to this process.

1. Edit the environment variable “PSModulePath” to include the path to the module
2. Import the module


Both steps can be accomplished by editing the PowerShell profile. There are several PowerShell profiles; I have chosen to use the profile for All Users, All Hosts. This ensures the module will be available no matter how PowerShell is used. This is important for me because as well as running commands locally I also call commands using external applications like Nagios.
More information on PowerShell profiles can be found here:

The All Users, All Hosts profile is located here:
C:\windows\system32\WindowsPowerShell\v1.0\profile.ps1


The profile.ps1 file will not exist by default, so it is usually necessary to create it (new text file called profile.ps1)

1. Edit the environment variable “PSModulePath” to include the path to the module This done by adding the following line to the PowerShell profile:


$env:psmodulepath = $env:psmodulepath +";\\ServerShare\Folder\modules"

2. Import the module
This done by adding the following line to the PowerShell profile:

Import-Module ModuleName

Launching PowerShell will give you an error message at this point. This is because PowerShell sees the module as coming from an external source and doesn't trust it. PowerShell uses Internet Explorer's Zone Policy to see if it trusts the file, script or module you are attempting to run.

This can be manually edited through:

Internet Options >> Security tab >> Local Intranet >> Sites
Here we can add the external source as a trusted site, \\ServerShare

Doing this will only take effect for the currently logged on user, and will still cause an error when an external program attempts to run something using PowerShell - eg. Nagios

To get around this we must add the trusted site to the local machine's trusted zone policy. This can be done by creating two new registry keys.

HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\EscDomains\ServerShare

Name: file
Type: DWORD
Value: 1

and

HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\ServerShare

Name: file
Type: DWORD
Value: 1

I have a lot of servers that require this change, so I wrote some PowerShell that will do it for me, here is the script:

$registryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\ServerShare"
$registryPath1 = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\EscDomains\ServerShare"
$name = "file"
$value = "1"

If (!(test-path $registryPath )) {
New-Item -Path $registryPath -Force | Out-Null
New-ItemProperty -Path $registryPath -Name $name -Value $value -PropertyType DWORD -Force | Out-Null
}
If (!(test-path $registryPath1 )) {
New-Item -Path $registryPath1 -Force | Out-Null
New-ItemProperty -Path $registryPath1 -Name $name -Value $value -PropertyType DWORD -Force | Out-Null
}

Using these settings let me use all the commands that are exposed by my module and I can leave the execution policy set at Remote Signed.

Below are some good articles that helped me out with this process:

Running PowerShell Scripts From An UNC Path (Share)
Signing PowerShell Scripts
Can't import a module from a UNC path
Update or Add Registry Key Value with PowerShell


UPDATE - 19-10-2015

I've been using this process quite a bit, and found that it doesn't work in all circumstances. For newer versions of Internet Explorer, there is an extra step to editing the Local Intranet zones policy:
Internet Options >> Security tab >> Local Intranet >> Sites >> Advanced

When using these later versions of IE, the registry changes don't work, and the module on a UNC path still causes an error when the profile tries to import it. I can stop the error from being displayed by changing the Execution Policy to Bypass, however, I wasn't keen on implementing that as a solution across our estate. Instead I'm now using a different method.

Copy-Item \\ServerShare\Folder\modules\ModuleName -Destination D:\scripts\modules -Recurse -Force

$env:psmodulepath = $env:psmodulepath +";D:\scripts\modules"

Import-module ModuleName


This takes a copy of the module and puts it into a local folder. The module is then imported from this local folder, which removes the need for PowerShell to treat it as a remote script.