15

I'm trying to automate the creation of a server farm in PowerShell. Through manual creation I've got the following XML:

<webFarms>
    <webFarm name="alwaysup" enabled="true">
        <server address="alwaysup-blue" enabled="true">
            <applicationRequestRouting httpPort="8001" />
        </server>
        <server address="alwaysup-green" enabled="true">
            <applicationRequestRouting httpPort="8002" />
        </server>
        <applicationRequestRouting>
            <healthCheck url="http://alwaysup/up.html" interval="00:00:05" responseMatch="up" />
        </applicationRequestRouting>
    </webFarm>
    <applicationRequestRouting>
        <hostAffinityProviderList>
            <add name="Microsoft.Web.Arr.HostNameRoundRobin" />
        </hostAffinityProviderList>
    </applicationRequestRouting>
</webFarms>

Trying to do this via PS proves troublesome however: as far as I can tell there is no dedicated API to do this through (WebFarmSnapin is meant for an older version).

I have shifted my attention to IIS Administration Cmdlets but only got it half working.

The code that I have:

#####
# Overwriting the server farm
#####

Write-Host "Overwriting the server farm $($webFarmName)"

$webFarm = @{};
$webFarm["name"] = 'siteFarm'

Set-WebConfiguration "/webFarms" -Value $webFarm

#####
# Adding the servers
#####

Write-Host "Adding the servers"

$blueServer = @{}
$blueServer["address"] = 'site-blue'
$blueServer["applicationRequestRouting"] = @{}

$greenServer = @{}
$greenServer["address"] = 'site-green'
$greenServer["applicationRequestRouting"] = @{}

$servers = @($blueServer, $greenServer)

Add-WebConfiguration -Filter "/webFarms/webFarm[@name='siteFarm']" -Value $servers

#####
# Adding routing
#####

Write-Host "Adding the routing configurations"

$blueServerRouting = @{}
$blueServerRouting["httpPort"] = "8001"
Add-WebConfiguration -Filter "/webFarms/webFarm[@name='siteFarm']/server[@address='site-blue']" -Value $blueServerRouting

This generates

<webFarms>
    <webFarm name="siteFarm">
        <server address="site-blue" />
        <server address="site-green" />
    </webFarm>
    <applicationRequestRouting>
        <hostAffinityProviderList>
            <add name="Microsoft.Web.Arr.HostNameRoundRobin" />
        </hostAffinityProviderList>
    </applicationRequestRouting>
</webFarms>

As you can see it's missing the port related to the routing. And I haven't even started with trying to add the healthcheck at this point.

What am I doing wrong? Is there some Cmdlet that I haven't found which makes this easier?

Related but without much of a useful answer (the PowerShell tab with generated code stays empty).

Jeroen Vannevel
  • 43,651
  • 22
  • 107
  • 170
  • You could try and see if the IIS provider can do what you want https://technet.microsoft.com/en-us/library/ee909471(v=ws.10).aspx It is arguably a bit more nitty-gritty than using IIS cmdlets, but sometimes that's what it takes. – mtnielsen Nov 13 '17 at 13:30
  • Have you looked at this cmdlet to configure the port? `Set-WebBinding -Name 'Default Web Site' -BindingInformation "*:80:" -PropertyName "Port" -Value "1234"` I'm not familiar enough with webfarms to know if this will help you or not. – FoxDeploy Nov 14 '17 at 15:46
  • If you haven't already, I'd suggest looking into the [xWebAdministration DSC resource](https://github.com/PowerShell/xWebAdministration) to automate the creation/configuration/desired-state of the webfarm. The WebAdministration module leaves a lot to be desired (speaking from experience after automating some IIS setup earlier this year) – Maximilian Burszley Nov 14 '17 at 19:54
  • @TheIncorrigible1 I don't see anything related to webfarms in there. Am I looking past it? – Jeroen Vannevel Nov 15 '17 at 15:17
  • I am curious how this works! We did something with containers similar but not as a farm. –  Nov 17 '17 at 14:31

3 Answers3

7

It looks like you figured out how to do this by modifying the XML configuration file itself. Despite the feeling that this doesn't quite seem like the "right way", changing the config file directly is a perfectly valid solution. In essence, you created a template, and configuration templates provide a fast and readable approach to producing maintainable and repeatable configuration.

We can improve upon this approach by extracting the template text from the script into a separate text file. Then, scripts can read (or source) the template and swap out any placeholder values as needed. This is similar to how we separate concerns in a web application by decoupling our HTML templates from the code.

To more directly answer the question, let's take a look at how to do this using PowerShell and the IIS Administration API (you're correct—Web Farm Framework is no longer supported for recent versions if IIS and Windows). The original code in the question is a good start. We just need to distinguish between manipulating configuration collections (using *-WebConfiguration cmdlets) and configuration items (using *-WebConfigurationProperty cmdlets). Here's a script that will set the configuration values based on the example in the question:

$farmName = 'siteFarm'

Add-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' `
    -Filter 'webFarms' `
    -Name '.'  `
    -Value @{ name = $farmName; enabled = $true }

Add-WebConfiguration -PSPath 'MACHINE/WEBROOT/APPHOST' `
    -Filter "webFarms/webFarm[@name='$farmName']" `
    -Value @(
        @{ address = 'site-blue'; enabled = $true },
        @{ address = 'site-green'; enabled = $true }
    )

Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' `
    -Filter "webFarms/webFarm[@name='$farmName']/server[@address='site-blue']" `
    -Name 'applicationRequestRouting' `
    -Value @{ httpPort = 8001 }

Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' `
    -Filter "webFarms/webFarm[@name='$farmName']/server[@address='site-green']" `
    -Name 'applicationRequestRouting' `
    -Value @{ httpPort = 8002 }

Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' `
    -Filter "webFarms/webFarm[@name='$farmName']/applicationRequestRouting" `
    -Name 'healthCheck' `
    -Value @{
        url = 'http://mySite/up.html'
        interval = '00:00:05'
        responseMatch = 'up'
    }

Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' `
    -Filter "webFarms/webFarm[@name='$farmName']/applicationRequestRouting" `
    -Name 'protocol' `
    -Value @{ reverseRewriteHostInResponseHeaders = $true }

Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' `
    -Filter "webFarms/webFarm[@name='$farmName']/applicationRequestRouting/protocol" `
    -Name 'cache' `
    -Value @{ enabled = $false; queryStringHandling = 'NoCaching' }

This may be a bit more verbose than necessary, but I wanted to clearly illustrate the intention in each step. This implementation uses XPath queries for the -Filter arguments to select the appropriate XML nodes. We could, perhaps, refactor the script to to reduce repetitive tasks, such as by defining an Add-FarmServer function that takes a server name and port and then adds the appropriate directives. We may also need Remove-WebConfigurationLock if we encounter locked configuration issues.

Whether we choose to use a template or a programmatic approach depends on the project and team preference. The API becomes much more attractive when we have many similar items to configure, such as if we have hundreds of servers to add to a web farm. On the other hand, templates are simple to understand don't require that other team members learn a new (and maybe somewhat confusing) API.

Cy Rossignol
  • 16,216
  • 4
  • 57
  • 83
  • 1
    Thank you for the insightful answer! I didn't quite like my approach since it's just adding a blorb of XML so I like yours as it at least uses the XML APIs to manipulate it. – Jeroen Vannevel Nov 19 '17 at 10:15
3

I've been working on this and I discovered that you can use Microsoft.Web.Administration in Powershell to accomplish this.

Add-Type -AssemblyName "Microsoft.Web.Administration, Version=7.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"

$iisManager = New-Object Microsoft.Web.Administration.ServerManager
$applicationHostConfig = $IISManager.GetApplicationHostConfiguration()

# Get the WebFarms section. Section names are case-sensitive.
$webFarmSection = $ApplicationHostConfig.GetSection("webFarms")
$webFarms = $WebFarmSection.GetCollection()

# Create a new webfarm and assign a name.
$webFarm = $WebFarms.CreateElement("webFarm")
$webFarm.SetAttributeValue("name", "MyNewWebFarmName")
# Add it to the parent element
$webFarms.Add($webFarm)
# get Webfarm server collection
$servers = $webFarm.GetCollection()
# add server
$serverBlue = $servers.CreateElement("server")
$routingBlue = $serverBlue.GetChildElement("applicationRequestRouting")
$routingBlue.SetAttributeValue("httpPort", "8001")
$serverBlue.SetAttributeValue("address", "MyNewWebFarmName-blue")
$servers.Add($serverBlue)
# Save changes
$iisManager.CommitChanges()

There are two things that are important to mention:

  1. Once you call $iisManager.CommitChanges() then the object goes into read-only mode. You need to re-instantiate all objects before making any new changes.

  2. If you decide to create a new server in your web farm, you'll run into a problem if you assign the server a name and then try to access its port settings. What you need to do is assign the port first and then assign the name and whether it's enabled/disabled. Otherwise, it'll throw a System.AccessViolationException: Attempted to read or write protected memory. error.

Let me know how it goes!

agua from mars
  • 16,428
  • 4
  • 61
  • 70
AMoghrabi
  • 343
  • 2
  • 18
  • You put me in the right direction, however it took me a while to figure out I have to call `$servers = $webFarm.GetCollection()` to be able to create servers – agua from mars Mar 28 '18 at 13:39
  • So, I udate your answer with a sample to create server – agua from mars Mar 28 '18 at 13:46
  • I know this is old, but great post, it was exactly what I needed, coupled with the documentation from MS: https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.administration.configurationsection?view=iis-dotnet – Chris Nov 20 '20 at 13:43
1

Seeing as nobody has come up with a solution: I've taken the very simple workaround of just editing the XML directly instead of going through an API.

The actual webfarm functionality I haven't got to work yet but it seems like all the parts are there at least.

An example of manipulating the XML can be this:

$configPath = "C:\Windows\System32\inetsrv\config\applicationHost.config"
$configXml = [xml] (type $configPath)

[xml] $webFarmXml = @"
<webFarms>
    <webFarm name="siteFarm" enabled="true">
        <server address="site-blue" enabled="true">
            <applicationRequestRouting httpPort="$bluePort" />
        </server>
        <server address="site-green" enabled="true">
            <applicationRequestRouting httpPort="$greenPort" />
        </server>
        <applicationRequestRouting>
            <healthCheck url="http://mySite/up.html" interval="00:00:05" responseMatch="up" />
            <protocol reverseRewriteHostInResponseHeaders="true">
                <cache enabled="false" queryStringHandling="NoCaching" />
            </protocol>
        </applicationRequestRouting>
    </webFarm>
    <applicationRequestRouting>
        <hostAffinityProviderList>
            <add name="Microsoft.Web.Arr.HostNameRoundRobin" />
        </hostAffinityProviderList>
    </applicationRequestRouting>
</webFarms>
"@

$configXml.configuration.AppendChild($configXml.ImportNode($webFarmXml.webFarms, $true))
$configXml.Save($configPath)
Jeroen Vannevel
  • 43,651
  • 22
  • 107
  • 170