- Reading Azure VM name, IP address, and hostname with PowerShell - Fri, Jul 28 2017
- Automating WSUS with PowerShell - Thu, Jul 13 2017
- Disable SSL and TLS 1.0/1.1 on IIS with PowerShell - Tue, Jun 27 2017
The same thought came to our web team at some point, and they asked me for help. I didn’t know much about scripting for IIS at that time, but fortunately it turned out not to be very complicated. I found out that there is a PowerShell module called WebAdministration that allows us to do almost everything with IIS using PowerShell.
However, I couldn’t put everything into just one script, so I wrote two. The first script is for creating IIS web sites, their NTFS folders, default application pools, and bindings, and the second script is for creating application pools for specific web applications, web applications themselves, and NTFS folders for them and setting up some configuration parameters for application pools.
Today I’ll talk about only first of the scripts to create the IIS websites. You can see the script below with line-by-line explanations following.
<# .SYNOPSIS . .DESCRIPTION Creates a number of websites as well as folder structure and default application pools under them. Also sets up bindings for the newly created websites. .PARAMETER WebSiteName This is the site name you are going to create. It will be complemented by the site number if you are creating more than one site. .PARAMETER WebSitesNumber Specifies the number of websites that will be created. .PARAMETER RootFSFolder Specifies a root folder where all the websites will be created. .PARAMETER Hostheaders Specifies host headers that will be added to each website. Use quotes for this parameter to avoid errors. .PARAMETER EnviromentName Specifies an environment name for the site name. If you omit this, you will create a site without an environment name. .PARAMETER DefaultAppPoolName Specifies default name for application pool. If omitted, the WebSiteName will be used as the default app pool name. .PARAMETER Help Displays the help screen. .EXAMPLE C:\PS>.\createIISSites.ps1 -WebSiteName app -WebSitesNumber 3 -RootFSFolder c:\webs -EnviromentName lab -DefaultAppPoolName platformsc -Hostheaders ".contentexpress.lan, .enservio.com" Creates 3 IIS sites with the names app1lab, app2lab, app3lab, root folder c:\web and subfolders for each site like c:\webs\app1lab, c:\webs\app2lab etc., default app pools for those sites with the names platformsc1, platformsc2 etc., and hostheaders for each site with the names app1.contentexpress.lan, app2.contentexpress.lan, app2.enservio.com etc. .NOTES Author: Alex Chaika Date: May 16, 2016 test commment #> [CmdletBinding()] Param( [Parameter(Mandatory=$True)] [string]$WebSiteName, [Parameter(Mandatory=$True)] [int]$WebSitesNumber, [Parameter(Mandatory=$True)] [string]$RootFSFolder, [Parameter(Mandatory=$False)] [string]$Hostheaders, [Parameter(Mandatory=$False)] [string]$EnviromentName, [Parameter(Mandatory=$False)] [string]$DefaultAppPoolName, ) if(-not $DefaultAppPoolName){ $DefaultAppPoolName = $WebSiteName} import-module WebAdministration Function CreateAppPool { Param([string] $appPoolName) if(Test-Path ("IIS:\AppPools\" + $appPoolName)) { Write-Host "The App Pool $appPoolName already exists" -ForegroundColor Yellow return } $appPool = New-WebAppPool -Name $appPoolName } function CreatePhysicalPath { Param([string] $fpath) if(Test-path $fpath) { Write-Host "The folder $fpath already exists" -ForegroundColor Yellow return } else{ New-Item -ItemType directory -Path $fpath -Force } } Function SetupBindings { Param([string] $hostheaders) $charCount = ($hostheaders.ToCharArray() | Where-Object {$_ -eq ','} | Measure-Object).Count + 1 $option = [System.StringSplitOptions]::RemoveEmptyEntries $hhs=$hostheaders.Split(',',$charCount,$option) get-Website | ? {$_.Name -ne "Default Web Site"} | % { foreach ($h in $hhs){ $header=$_.Name + $h.Trim() New-WebBinding -Name $_.Name -HostHeader $header -IP "*" -Port 80 -Protocol http } } Get-WebBinding | ?{($_.bindingInformation).Length -eq 5} | Remove-WebBinding } for ($i=1;$i -le $WebSitesNumber;$i++) { $fpath = $RootFSFolder + "\" + $WebSiteName + $i + $EnviromentName + "\" + $DefaultAppPoolName + $i + $EnviromentName CreatePhysicalPath $fpath $appPoolName = $DefaultAppPoolName + $i + $EnviromentName $GenWebSiteName = $WebSiteName +$i + $EnviromentName CreateAppPool $appPoolName If(!(Test-Path "IIS:\Sites\$GenWebSiteName")){ New-Website -Name $GenWebSiteName -PhysicalPath $fpath -ApplicationPool $appPoolName } else { Write-Host "The IIS site $GenWebSiteName already exists" -ForegroundColor Yellow exit } } SetupBindings $Hostheaders
Now let’s go through it:
This large commented area at the beginning of the script (synopsis) creates a help section for the script. With the help of PowerShell keywords (.DESCRIPTION, .PARAMETER, .EXAMPLE), I gave my script the ability to use the standard Get-Help command to display a help screen.
[CmdletBinding()] Param( [Parameter(Mandatory=$True)] [string]$WebSiteName, [Parameter(Mandatory=$True)] [int]$WebSitesNumber, [Parameter(Mandatory=$True)] [string]$RootFSFolder, [Parameter(Mandatory=$False)] [string]$Hostheaders, [Parameter(Mandatory=$False)] [string]$EnviromentName, [Parameter(Mandatory=$False)] [string]$DefaultAppPoolName, )
CmdletBinding() is a PowerShell keyword that tells the interpreter that this script should behave as a compiled C# application.
Param establishes the parameters section. As you can see, my script accepts five different parameters. Some of them are mandatory, and some are not.
$WebSiteName is the common name for the websites that are going to be created.
$WebSitesNumber indicates the number of websites I’m going to create.
$RootFSFolder is the folder where the all IIS sites subfolders are going to reside.
$Hostheaders is the headers that are going to be assigned to IIS sites.
$EnviromentName adds the environment name to each IIS site created by the script. Almost every company I’ve seen has several environments, such as development, staging, and production.
$DefaultAppPoolName specifies the default application pool name for the web site. This parameter is not mandatory because later I can assign the application pool name if it is not provided explicitly.
if(-not $DefaultAppPoolName){ $DefaultAppPoolName = $WebSiteName}
As mentioned above, if $DefaultAppPoolName is empty, it uses the $WebSiteName value.
import-module WebAdministration
This is command imports the WebAdministration module. I know that PowerShell now imports modules on the fly, but I still prefer do this explicitly to ensure that the module is really available.
Function CreateAppPool { Param([string] $appPoolName) if(Test-Path ("IIS:\AppPools\" + $appPoolName)) { Write-Host "The App Pool $appPoolName already exists" -ForegroundColor Yellow return } $appPool = New-WebAppPool -Name $appPoolName }
The function above, as you can gather from its name, creates the application pool. It uses the $appPoolName parameter, tests to determine if a pool with the same name already exists (using Test-Path), and if not, creates the new application pool with the name from the $appPoolName parameter. If the pool name already exists, the function throws an error message and exits.
function CreatePhysicalPath { Param([string] $fpath) if(Test-path $fpath) { Write-Host "The folder $fpath already exists" -ForegroundColor Yellow return } else{ New-Item -ItemType directory -Path $fpath -Force } }
The CreatePhysicalPath function does almost the same thing for the NTFS folders. It gets the folder name as a parameter, checks if the folder already exists, spits the error message if it does, and creates a new one if it doesn’t.
Function SetupBindings { Param([string] $hostheaders) $charCount = ($hostheaders.ToCharArray() | Where-Object {$_ -eq ','} | Measure-Object).Count + 1 $option = [System.StringSplitOptions]::RemoveEmptyEntries $hhs=$hostheaders.Split(',',$charCount,$option) get-Website | ? {$_.Name -ne "Default Web Site"} | % { foreach ($h in $hhs){ $header=$_.Name + $h.Trim() New-WebBinding -Name $_.Name -HostHeader $header -IP "*" -Port 80 -Protocol http } } Get-WebBinding | ?{($_.bindingInformation).Length -eq 5} | Remove-WebBinding }
This function is a little bit more sophisticated. As you can see, it accepts the $hostheaders string as a parameter, and there can be several headers separated by commas.
$charCount = ($hostheaders.ToCharArray() | Where-Object {$_ -eq ','} | Measure-Object).Count + 1
First, I need to count how many values there are. To do so, I count the commas in the string. But since I have commas only between values, I need to increment this number. The result of the whole operation is stored in the $charCount variable.
$option = [System.StringSplitOptions]::RemoveEmptyEntries
Here I’m setting up the $option variable. This variable will later be used for the split method to remove empty entries.
$hhs=$hostheaders.Split(',',$charCount,$option)
Then I’m splitting the values from the $hostheaders into different strings and saving the result in the $hhs variable.
As I now have an array of strings and each of them represents one header, I can actually start setting up headers for the websites.
get-Website | ? {$_.Name -ne "Default Web Site"} | %
With this command, I’m getting all websites which exist on this particular machine without the default one (Default Web Site).
foreach ($h in $hhs){ $header=$_.Name + $h.Trim()
Then I’m looping through my headers array and assigning the $header variable value, which consists of the current site name and the current header name.
New-WebBinding -Name $_.Name -HostHeader $header -IP "*" -Port 80 -Protocol http
Now I create a new binding using the New-WebBinding cmdlet site name and header.
Get-WebBinding | ?{($_.bindingInformation).Length -eq 5} | Remove-WebBinding
When all bindings are set, I remove the default one because I don’t need it.
for ($i=1;$i -le $WebSitesNumber;$i++) { $fpath = $RootFSFolder + "\" + $WebSiteName + $i + $EnviromentName + "\" + $DefaultAppPoolName + $i + $EnviromentName CreatePhysicalPath $fpath $appPoolName = $DefaultAppPoolName + $i + $EnviromentName $GenWebSiteName = $WebSiteName +$i + $EnviromentName CreateAppPool $appPoolName If(!(Test-Path "IIS:\Sites\$GenWebSiteName")){ New-Website -Name $GenWebSiteName -PhysicalPath $fpath -ApplicationPool $appPoolName } else { Write-Host "The IIS site $GenWebSiteName already exists" -ForegroundColor Yellow exit } } SetupBindings $Hostheaders
I’m now done with the functions, and it's time to take a look at the main script code.
for ($i=1;$i -le $WebSitesNumber;$i++)
This command loops through the site numbers.
$fpath = $RootFSFolder + "\" + $WebSiteName + $i + $EnviromentName + "\" + $DefaultAppPoolName + $i + $EnviromentName
This long line sets the path to the NTFS folders used and stores the IIS site and application pool files.
CreatePhysicalPath $fpath
Now I’m using the CreatePhysicalPath function to create the folders.
$GenWebSiteName = $WebSiteName +$i + $EnviromentName CreateAppPool $appPoolName
This line create an application pool with the corresponding name.
If(!(Test-Path "IIS:\Sites\$GenWebSiteName")){ New-Website -Name $GenWebSiteName -PhysicalPath $fpath -ApplicationPool $appPoolName } else { Write-Host "The IIS site $GenWebSiteName already exists" -ForegroundColor Yellow exit
As the folders and applications pools now exist, I can create the websites: First I check if the folder exists and if not it errors.
SetupBindings $Hostheaders
Because everything is in place now, I am setting up the bindings for the websites. The screenshots below show the results when running the script against my sandbox machine.
Hi Alex,
Thanks for the Article.
I am facing a few issues here. If there are no websites in My IIS, it is creating sites and bindings perfectly. But if there are already some websites exists in IIS and when creating some more websites using this script, it is also creating additional bindings in the existing websites.
And on more issue is i am unable to start more than one website. when attempt it is throwing error “This website cannot be started. Another Website may be using the same port.
Kindly check this.
Changing the code in setupbindings will make this actually useful, amazed Alex has survived 15 years as an MCSE with code like this
Update the get-website portion to look like this: get-Website | ? {$_.Name -eq $WebSiteName} and it will only update bindings on the site you want it to, instead of every site in IIS