Home
About
Contact
Monday, February 15, 2010

In part 1 I explained why I needed a load balancer that was capable of doing smart balancing decisions using more than just IPs and why I decided to use IIS7 and Application Request Routing (ARR). In this part I will give a detailed explanation of how to setup ARR on Windows Server 2008 R2 Web Server Core.

Server Core provide the following benefits over traditional installations according to Microsoft:

  • Reduced maintenance
  • Reduced attack surface
  • Reduced management
  • Less disk space required

I concluded that the above points is quite important for a load balancer, as you don’t want it to spend its resources on anything else than load balancing.

In this guide I will walk you through the steps needed to install Windows Server 2008 Web Server Core. Secondly I will show you how to create the necessary scripts to automate about 99% of the installation and configuration of ARR.

Before we dig deep, here’s a short description of the setup I used for my ARR environment:

Two Web Servers with:

  • 3 websites running both SSL and normal HTTP
  • Each website needed its own IP
  • Each website needed its own SSL certificate

ARR Server:

  • In order to avoid the “clients NAT’ed behind one IP” problem, I needed to configure ARR Server Affinity to UseCookie.
  • As for the Load Balancing Algorithm I used Weighted Round Robin (you should look into the other alternatives that suite your needs)
  • The ARR server is responsible for distributing traffic to two web servers mentioned above

All of the above can of course be changed in the scripts that I’ll be showing later to suite your configuration needs. Below is an illustration of the infrastructure:

image

Installing Windows Server 2008 R2 Web Server Core:

  1. Install Windows Server 2008 R2 Web - Server Core
  2. Set password for admin (when prompted)
  3. Rename server:
    NETDOM renamecomputer %computername% /newname:[nameOfServer]
  4. Restart:
    shutdown /r /t 0
  5. Set static IP:
    netsh interface ipv4 set address "Local Area Connection" static [IP] [Subnet] [Gateway]
  6. Set Primary DNS server:
    netsh interface ipv4 add dnsserver "Local Area Connection" [IP]
  7. Set Secondary DNS server:
    netsh interface ipv4 add dnsserver "Local Area Connection" [IP] index=2
  8. If you want the server to join a domain:
    netdom join %computername% /domain:[domainname] /userd:[username] /passwordd:*
  9. Restart:
    shutdown /r /t 0
  10. If you want to enable Remote Desktop:

    For Vista/Win7/2008 clients:
    cscript %windir%\system32\SCRegEdit.wsf /ar 0

    For earlier clients:
    cscript %windir%\system32\SCRegEdit.wsf /cs 0

    For both new and earlier clients run both of the above
  11. If you want to allow remote MMC management (e.g. to simplify managing users etc):
    netsh advfirewall firewall set rule group="Remote Administration" new enable=yes
  12. Activate Windows:
    start /w slmgr.vbs –ipk [key]
    start /w slmgr.vbs –ato

Now the server is ready for ARR installation and configuration, but since the OS is a server core, creating some scripts to automate this procedure is very helpful. Next I will show you the scripts I created and used, which hopefully will make this task easier for you than for me :-) I’ve made all the files and scripts available in this zip file.

Creating script for installing SSL certificates (do this on some client computer, not the ARR Server):

Note: You can skip this section (except step 1 and 2) if you’re not using SSL.

  1. Create a folder called ARRInstall
  2. In the ARRInstall folder, create a new folder called Utils
  3. Download capicom_dc_sdk.msi and place MSI in the Utils folder
  4. Download winhttpcertcfg.msi and install with default options on a client computer
  5. Browse to C:\Program Files (x86)\Windows Resource Kits\Tools\ and copy winhttpcertcfg.exe into your Utils folder
  6. Create a folder called Certificates in your ARRInstall folder
  7. The above folder must contain all your certificates (both with and without private keys). For my setup this looks like this:

    ARRInstall 
      |-- Certificates 
            |-- site1.torresdal.net.cer 
            |-- site1.torresdal.net.pfx 
            |-- site2.torresdal.net.cer 
            |-- site2.torresdal.net.pfx 
            |-- site3.torresdal.net.cer 
            |-- site3.torresdal.net.pfx
  8. Copy the script below, read comments in script and modify accordingly. Then save the script as _SetupCertificates.cmd in the ARRInstall folder:
    @echo off
    
    echo Installing CAPICOM...
    
    echo.
    
    msiexec /qn /i "%~dp0Utils\capicom_dc_sdk.msi"
    
    echo Removing existing certificate from store...
    
    REM Example:
    
    REM certutil -delstore Root "blog.torresdal.net"
    
    certutil -delstore Root "[certificate name]"
    
    echo Removing SSL certificate from IIS...
    
    netsh http delete sslcert ipport=0.0.0.0:443
    
    echo Installing certificates...
    
    echo.
    
    REM Example:
    
    REM certutil -f -addstore Root "%~dp0Certificates\blog.torresdal.net.cer"
    
    certutil -f -addstore Root "%~dp0Certificates\[certificate name].cer"
    
    IF EXIST "%PROGRAMFILES%\Microsoft CAPICOM 2.1.0.2 SDK" (
    
        SET capicompath="%PROGRAMFILES%\Microsoft CAPICOM 2.1.0.2 SDK\Samples\vbs\cstore.vbs"
    
        SET cscript=%windir%\system32\cscript.exe
    
    )
    
    IF EXIST "%PROGRAMFILES(x86)%\Microsoft CAPICOM 2.1.0.2 SDK" (
    
        SET capicompath="%PROGRAMFILES(x86)%\Microsoft CAPICOM 2.1.0.2 SDK\Samples\vbs\cstore.vbs"
    
        SET cscript=%windir%\syswow64\cscript.exe
    
        ECHO Setting up CAPICOM for 64 bits environment...
    
        copy /y "%PROGRAMFILES(x86)%\Microsoft CAPICOM 2.1.0.2 SDK\Lib\X86\capicom.dll" %windir%\syswow64
    
        %windir%\syswow64\regsvr32.exe /s %windir%\syswow64\capicom.dll
    
    )
    
    REM Example:
    
    REM %cscript% /nologo %capicompath% delete -l LM -subject "blog.torresdal.net" -noprompt
    
    %cscript% /nologo %capicompath% delete -l LM -subject "[certificate name]" -noprompt
    
    REM Example:
    
    REM %cscript% /nologo %capicompath% import -l LM "%~dp0Certificates\blog.torresdal.net.pfx" "somePassword"
    
    %cscript% /nologo %capicompath% import -l LM "%~dp0Certificates\[certificate name].pfx" "[password]"
    
    REM Example:
    
    REM "%~dp0Utils\winhttpcertcfg.exe" -g -c LOCAL_MACHINE\My -s blog.torresdal.net -a "IIS_IUSRS"
    
    "%~dp0Utils\winhttpcertcfg.exe" -g -c LOCAL_MACHINE\My -s [certificate name] -a "IIS_IUSRS"
    
    echo.
    
    echo ============================
    
    echo Certificates Setup finished!
    
    echo ============================
    
    REM pause
    
    :end
  9. Your folder structure should now look like this:
    ARRInstall 
      |-- Certificates 
            |-- site1.torresdal.net.cer 
            |-- site1.torresdal.net.pfx 
            |-- site2.torresdal.net.cer 
            |-- site2.torresdal.net.pfx 
            |-- site3.torresdal.net.cer 
            |-- site3.torresdal.net.pfx
      |-- Utils
            |-- capicom_dc_sdk.msi
            |-- winhttpcertcfg.exe
      |-- _SetupCertificates.cmd

Create script for installing and configuring ARR:

  1. Download the x64 installer for ARR from the ARR website: http://www.iis.net/expand/ApplicationRequestRouting
  2. Copy the downloaded file (ARRv2_setup_x64.EXE) into the Utils folder
  3. Download IIS7Util.exe which I’ve made available for you on my site. (The source code can be found at the foaf-ssl-dotnet project. Just browse code and search for “iis7util”, to make it clear where I got it from :-))
  4. Copy the IIS7Util.exe into the Utils folder
  5. Copy the script below, read comments in script and modify accordingly. Then save the script as InstallARR.cmd in the ARRInstall folder. You should then have this file structure:

    ARRInstall 
      |-- Certificates 
            |-- site1.torresdal.net.cer 
            |-- site1.torresdal.net.pfx 
            |-- site2.torresdal.net.cer 
            |-- site2.torresdal.net.pfx 
            |-- site3.torresdal.net.cer 
            |-- site3.torresdal.net.pfx
      |—Utils
            |-- ARRv2_setup_x64.EXE
            |-- capicom_dc_sdk.msi
            |-- IIS7Util.exe
            |-- winhttpcertcfg.exe
      |-- _SetupCertificates.cmd
      |-- InstallARR.cmd

    Note: If you’re not using SSL, comment out this line in your script:
    call %~dp0_SetupCertificates.cmd

    @echo off
    
    REM =====================
    
    REM 	CONFIGURATION
    
    REM =====================
    
    REM =============================================
    
    REM IP's for the different sites on the LB Server
    
    REM =============================================
    
    SET Site1IPNLB=10.0.0.51
    
    SET Site2IPNLB=10.0.0.52
    
    SET Site3IPNLB=10.0.0.53
    
    REM =============================
    
    REM IP's for back end web servers
    
    REM =============================
    
    SET Site1IPWeb01=10.137.13.61
    
    SET Site2IPWeb01=10.137.13.62
    
    SET Site3IPWeb01=10.137.13.63
    
    SET Site1IPWeb02=10.137.13.71
    
    SET Site2IPWeb02=10.137.13.72
    
    SET Site3IPWeb02=10.137.13.73
    
    REM ==============================
    
    REM Host names for different sites
    
    REM ==============================
    
    SET Site1HostName=site1.torresdal.net
    
    SET Site2HostName=site2.torresdal.net
    
    SET Site3HostName=site3.torresdal.net
    
    REM ==================================
    
    REM Name of Server Farms for each site
    
    REM ==================================
    
    SET ARRServerFarm1=Site1
    
    SET ARRServerFarm2=Site2
    
    SET ARRServerFarm3=Site3
    
    REM ============================
    
    REM File locations for web sites
    
    REM ============================
    
    SET Site1FileLoc=C:\%ARRServerFarm1%
    
    SET Site2FileLoc=C:\%ARRServerFarm2%
    
    SET Site3FileLoc=C:\%ARRServerFarm3%
    
    REM ==========
    
    REM AppCmd.exe
    
    REM ==========
    
    SET AppCMD=%windir%\system32\inetsrv\appcmd.exe
    
    REM =========================
    
    REM	INSTALLATION
    
    REM =========================
    
    Echo Installing IIS...
    
    start /w pkgmgr /l:log.etw /iu:IIS-WebServerRole;WAS-WindowsActivationService;WAS-ProcessModel;WAS-NetFxEnvironment;WAS-ConfigurationAPI;NetFx2-ServerCore;NetFx2-ServerCore-WOW64;IIS-ManagementService
    
    Echo Enabling remote management of IIS...
    
    reg add HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WebManagement\Server /v EnableRemoteManagement /t REG_DWORD /d 1 /f
    
    sc config WMSVC start= auto
    
    Echo Installing Application Request Routing (ARR)...
    
    net stop was /y
    
    net stop wmsvc /y
    
    "%~dp0utils\ARRv2_setup_x64.EXE" /Q
    
    net start was
    
    net start wmsvc
    
    Echo Disable Idle Timeout on Application Pool...
    
    %AppCMD% set apppool "DefaultAppPool" -processModel.idleTimeout:"00:00:00" /commit:apphost
    
    %AppCMD% set config -section:system.applicationHost/applicationPools /[name='DefaultAppPool'].recycling.periodicRestart.time:"00:00:00" /commit:apphost
    
    Echo Installing certificates...
    
    call %~dp0_SetupCertificates.cmd
    
    Echo Adding aditional IPs for web sites...
    
    netsh interface ipv4 add address "Local Area Connection" %Site1IPNLB% 255.255.255.0
    
    netsh interface ipv4 add address "Local Area Connection" %Site2IPNLB% 255.255.255.0
    
    netsh interface ipv4 add address "Local Area Connection" %Site3IPNLB% 255.255.255.0
    
    Echo Creating folders for web sites...
    
    md %Site1FileLoc%
    
    md %Site2FileLoc%
    
    md %Site3FileLoc%
    
    Echo Creating web sites in IIS...
    
    %AppCMD% add site /name:%site1HostName% /bindings:"http/%Site1IPNLB%:80:,https/%Site1IPNLB%:443:" /physicalPath:"%Site1FileLoc%"
    
    %AppCMD% add site /name:%site2HostName% /bindings:"http/%Site2IPNLB%:80:,https/%Site2IPNLB%:443:" /physicalPath:"%Site2FileLoc%"
    
    %AppCMD% add site /name:%site3HostName% /bindings:"http/%Site3IPNLB%:80:,https/%Site3IPNLB%:443:" /physicalPath:"%Site3FileLoc%"
    
    Echo Adding farms and servers to ARR...
    
    %AppCMD% set config  -section:webFarms /+"[name='%ARRServerFarm1%']" /commit:apphost
    
    %AppCMD% set config  -section:webFarms /+"[name='%ARRServerFarm1%'].[address='%Site1IPWeb01%']" /commit:apphost
    
    %AppCMD% set config  -section:webFarms /+"[name='%ARRServerFarm1%'].[address='%Site1IPWeb02%']" /commit:apphost
    
    %AppCMD% set config  -section:webFarms /+"[name='%ARRServerFarm2%']" /commit:apphost
    
    %AppCMD% set config  -section:webFarms /+"[name='%ARRServerFarm2%'].[address='%Site2IPWeb01%']" /commit:apphost
    
    %AppCMD% set config  -section:webFarms /+"[name='%ARRServerFarm2%'].[address='%Site2IPWeb02%']" /commit:apphost
    
    %AppCMD% set config  -section:webFarms /+"[name='%ARRServerFarm3%']" /commit:apphost
    
    %AppCMD% set config  -section:webFarms /+"[name='%ARRServerFarm3%'].[address='%Site3IPWeb01%']" /commit:apphost
    
    %AppCMD% set config  -section:webFarms /+"[name='%ARRServerFarm3%'].[address='%Site3IPWeb02%']" /commit:apphost
    
    REM ==============================================================================
    
    REM ALERT!: REMEMBER THAT SSL RULES FOR EACH SITE MUST BE BEFORE THE HTTP RULES!!!
    
    REM 	    E.g. 1) Rules for Site1 SSL
    
    REM              2) Rules for Site1 HTTP
    
    REM		 3) Rules for Site2 SSL
    
    REM		 4) Rules for Site2 HTTP
    
    REM		 5) etc.
    
    REM
    
    REM	    If we had switched 1 and 2, it would match on HTTP first, and would
    
    REM	    have redirected to a non SSL site, resulting in SSL offloading.
    
    REM
    
    REM	    If SSL offloading is the wanted behaviour, only the HTTP rule is 
    
    REM	    nessesary.
    
    REM ==============================================================================
    
    echo Adding rewriting rules to ARR...
    
    %AppCMD% clear config -section:system.webServer/rewrite/globalRules
    
    REM Site1 SSL
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm1%_SSL', patternSyntax='Wildcard',stopProcessing='True']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm1%_SSL',patternSyntax='Wildcard',stopProcessing='True'].match.url:"*"  /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm1%_SSL',patternSyntax='Wildcard',stopProcessing='True'].conditions.[input='{HTTPS}',pattern='on']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm1%_SSL',patternSyntax='Wildcard',stopProcessing='True'].conditions.[input='{HTTP_HOST}',pattern='*%Site1HostName%*']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm1%_SSL',patternSyntax='Wildcard',stopProcessing='True'].action.type:"Rewrite" /[name='%ARRServerFarm1%_SSL',patternSyntax='Wildcard',stopProcessing='True'].action.url:"https://%ARRServerFarm1%/{R:0}" /commit:apphost
    
    REM Site1 HTTP
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm1%', patternSyntax='Wildcard',stopProcessing='True']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm1%',patternSyntax='Wildcard',stopProcessing='True'].match.url:"*"  /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm1%',patternSyntax='Wildcard',stopProcessing='True'].conditions.[input='{HTTP_HOST}',pattern='*%Site1HostName%*']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm1%',patternSyntax='Wildcard',stopProcessing='True'].action.type:"Rewrite" /[name='%ARRServerFarm1%',patternSyntax='Wildcard',stopProcessing='True'].action.url:"http://%ARRServerFarm1%/{R:0}" /commit:apphost
    
    REM Site2 SSL
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm2%_SSL', patternSyntax='Wildcard',stopProcessing='True']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm2%_SSL',patternSyntax='Wildcard',stopProcessing='True'].match.url:"*"  /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm2%_SSL',patternSyntax='Wildcard',stopProcessing='True'].conditions.[input='{HTTPS}',pattern='on']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm2%_SSL',patternSyntax='Wildcard',stopProcessing='True'].conditions.[input='{HTTP_HOST}',pattern='*%Site2HostName%*']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm2%_SSL',patternSyntax='Wildcard',stopProcessing='True'].action.type:"Rewrite" /[name='%ARRServerFarm2%_SSL',patternSyntax='Wildcard',stopProcessing='True'].action.url:"https://%ARRServerFarm2%/{R:0}" /commit:apphost
    
    REM Site2 HTTP
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm2%', patternSyntax='Wildcard',stopProcessing='True']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm2%',patternSyntax='Wildcard',stopProcessing='True'].match.url:"*"  /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm2%',patternSyntax='Wildcard',stopProcessing='True'].conditions.[input='{HTTP_HOST}',pattern='*%Site2HostName%*']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm2%',patternSyntax='Wildcard',stopProcessing='True'].action.type:"Rewrite" /[name='%ARRServerFarm2%',patternSyntax='Wildcard',stopProcessing='True'].action.url:"http://%ARRServerFarm2%/{R:0}" /commit:apphost
    
    REM Site3 SSL
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm3%_SSL', patternSyntax='Wildcard',stopProcessing='True']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm3%_SSL',patternSyntax='Wildcard',stopProcessing='True'].match.url:"*"  /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm3%_SSL',patternSyntax='Wildcard',stopProcessing='True'].conditions.[input='{HTTPS}',pattern='on']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm3%_SSL',patternSyntax='Wildcard',stopProcessing='True'].conditions.[input='{HTTP_HOST}',pattern='*%Site3HostName%*']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm3%_SSL',patternSyntax='Wildcard',stopProcessing='True'].action.type:"Rewrite" /[name='%ARRServerFarm3%_SSL',patternSyntax='Wildcard',stopProcessing='True'].action.url:"https://%ARRServerFarm3%/{R:0}" /commit:apphost
    
    REM Site3 HTTP
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm3%', patternSyntax='Wildcard',stopProcessing='True']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm3%',patternSyntax='Wildcard',stopProcessing='True'].match.url:"*"  /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /+"[name='%ARRServerFarm3%',patternSyntax='Wildcard',stopProcessing='True'].conditions.[input='{HTTP_HOST}',pattern='*%Site3HostName%*']" /commit:apphost
    
    %AppCMD% set config  -section:system.webServer/rewrite/globalRules /[name='%ARRServerFarm3%',patternSyntax='Wildcard',stopProcessing='True'].action.type:"Rewrite" /[name='%ARRServerFarm3%',patternSyntax='Wildcard',stopProcessing='True'].action.url:"http://%ARRServerFarm3%/{R:0}" /commit:apphost
    
    REM ===============
    
    REM Server Affinity
    
    REM ===============
    
    Echo Adding Server Affinity...
    
    %AppCMD% set config  -section:webFarms /[name='%ARRServerFarm1%'].applicationRequestRouting.affinity.useCookie:"True"  /commit:apphost
    
    %AppCMD% set config  -section:webFarms /[name='%ARRServerFarm2%'].applicationRequestRouting.affinity.useCookie:"True"  /commit:apphost
    
    %AppCMD% set config  -section:webFarms /[name='%ARRServerFarm3%'].applicationRequestRouting.affinity.useCookie:"True"  /commit:apphost
    
    REM ========================
    
    REM Load Balancing Algorithm
    
    REM ========================
    
    Echo Adding Load Balancing Algorithm...
    
    %AppCMD% set config  -section:webFarms /[name='%ARRServerFarm1%'].applicationRequestRouting.loadBalancing.algorithm:"WeightedRoundRobin" /commit:apphost
    
    %AppCMD% set config  -section:webFarms /[name='%ARRServerFarm2%'].applicationRequestRouting.loadBalancing.algorithm:"WeightedRoundRobin" /commit:apphost
    
    %AppCMD% set config  -section:webFarms /[name='%ARRServerFarm3%'].applicationRequestRouting.loadBalancing.algorithm:"WeightedRoundRobin" /commit:apphost

Installing Application Request Routing:

  1. From the ARR server connect to your client containing the script files:
    net use [some drive letter]: \\clientComputerName\[drive on client]$ /user:domain\user
    Example:
    net use r: \\myClient\c$ /user:torresdal\jon
  2. Make a directory on the server to copy the files to:
    E.g.: md c:\temp\arr
  3. Copy the ARRInstall folder from your client to C:\temp\arr on your server
  4. Run InstallARR.bat, watch for errors and wait for it to finish
  5. Install Remote IIS Manger on a PC that will administrate IIS and connect to the ARR server: http://www.iis.net/expand/IISManager
  6. In Internet Information Services Manager connected to ARR do:
    1. Select Sites->Site1
    2. Click Bindings (upper right corner)
    3. Click https –> Edit
    4. Select proper certificate
    5. Repeat for all other sites
  7. Under Sites:
    1. For each site that require SSL:
      1. Click SSL Settings
      2. Check Require SSL
  8. Start IIS on ARR server:
    iisreset /start
  9. Using Internet Information Services Manager:
    Stop “Default Web Site”
  10. You’re done!

I hope this helped in making the installation and configuration of the ARR server easier, and that my scenario is applicable to others except me :-) Let me know if I can help with questions or if you find any errors.

Next time I will talk about how to avoid the ARR server being the Single Point of Failure.

Monday, February 15, 2010 8:00:00 AM (W. Europe Standard Time, UTC+01:00)
Wednesday, February 10, 2010

image A few weeks back I had to solve a performance problem at work. For some of our applications most of our users are NAT’ed behind the same IP, making the default load balancer (NLB) in Windows direct the majority of all traffic to one server. NLB in Windows work on layer 3 (network layer) in the OSI model (that thing you learned at uni), meaning we can only take advantage of IP stuff. So usually you use what’s often called Sticky Sessions (Single Affinity) to keep track of which IP’s got redirected to which server, and keep doing that from that point on for that IP. This was the setup we had.

You can work around that problem by disabling Sticky Sessions, but it might influence performance and could give you unwanted consequences. As an example, if you use ASP.NET Web Forms you will probably run into an issue with ViewState and possibly session issues. For SSL however (which was the case for us), Sticky Session is not recommended.

Other “smarter” load balancers work on layer 7 (application layer), providing us with a much bigger toolbox to inspect traffic and make smarter choices for where to redirect traffic. So I started to look for a load balancer on layer 7. The obvious first choice was hardware LB. This is often the quickest solution, but also the most expensive. I looked at Cisco’s Application Control Engine Module (ACE) and F5 BigIP, but the price was and issue as well as the lack of people with know-how inside our organization of how to configure them.

My next option was setting up a Linux server running Apache and LB, but then I came across a Microsoft module on IIS7: Application Request Routing (ARR). I found some really good documentation and it looked quite easy to set up. Since I (and others in my organization) are used to working with IIS, this seamed to be worth investigating.

In my next post I’ll go trough all the details of setting up ARR on Windows 2008 R2 Server Core. Everything from installing and configuring the OS to automate installation of ARR by scripts.

Wednesday, February 10, 2010 7:00:00 AM (W. Europe Standard Time, UTC+01:00)
Friday, October 24, 2008

This is my fourth post in my WiX and DTF series. Here are some others I’ve written:

Intro

In this article I’ll cover how to allow a user to choose which web site to install an application to by listing available web sites in a list box in the MSI installation wizard. As mention in my previous articles I'm applying best practices (at the best of my knowledge) to have the installer pass the Vista certification, so hopefully that will be the case if you use any of this in production :-)

When doing lookups in IIS using custom actions in the InstallUISequence (as we need to do in order to get available web sites), elevated privileges is required (at least in Vista SP1). This means we need the installation to run as admin. This is also an issue in the WiX extension for IIS as described in this bug report. See my previous article of a workaround for both the UI Sequence part and the bug: Using a bootstrapper to force elevated privileges in Vista

If you did not quite understand what I just said about the sequence stuff, elevated privileges etc., relax and keep reading, you will know by the end of this post and/or by clicking the link above :-)

Overview

This article assume you’ve read my previous article about Using WiX to author MSI installations or that you are somewhat familiar with WiX. If you haven’t done so already and want to follow this article step-by-step, download the WiX project from my previous article here: SimpleWebApp.zip

There is another possibility of course :-) If you’re lazy and just want the complete source for this article, you can download everything from here: SimpleWebApp_WSSelect.zip

Since this article became rather long I’ve structured it into 3 main sections with related sub sections, which hopefully will help you later if you need to look up something:

Now let’s dig into it!

Custom actions

To add a CA to your solution, do this:

  1. Add a new class library project to your solution and name it IISCustomAction
  2. Add a reference to Microsoft.Deployment.WindowsInstaller found in the WiX SDK
  3. Add a reference to System.DirectoryServices
  4. Rename the class created by VS to CustomAction.cs
  5. Add the following using’s to CustomAction.cs:
    1. using System.DirectoryServices;
    2. using Microsoft.Deployment.WindowsInstaller;

When building your project a couple of things happens:

  • The managed dll (IISCustomAction.dll) is created as expected by a class library
  • MakeSfxCA.exe (which is automatically called when building in VS) is creating a new dll based on the managed dll named IISCustomAction.CA.dll (this is the one you need to use in your WiX project)

It’s MakeSfxCA.exe that does all the magic here. The output dll that MakeSfxCA has created is actually a Win32 DLL. Here’s what Christopher Painter says about this:

At runtime, MSI thinks it’s calling a Win32 DLL in it’s own sandbox but in reality the CLR is being fired up out of process and communicated with through a named pipe.

Read the complete article here.

A typical CA looks something like this:

[CustomAction]
public static ActionResult MyCustomAction(Session session)
{
    try
    {
        ...
    }
    catch (Exception ex)
    {
        session.Log("CustomActionException: " + ex.ToString());
        return ActionResult.Failure;
    }
    return ActionResult.Success;
}

The [CustomAction] attribute is needed to tag this method as a CA. The ActionResult returns either (in my case) Failure or Success allowing the installer to respond accordingly. The parameter for this method is a Session object. This object gives me access to the MSI database and the inner workings of Windows Installer allowing me to query the database, access MSI properties etc.

Now you have what you need to start writing CA’s for the Windows Installer, so let’s get cranking.

Custom action – Get web sites
Copy and paste the code below into the CustomAction.cs class in the project you created above:

[CustomAction]
public static ActionResult GetWebSites(Session session)
{
    try
    {
        View listBoxView = session.Database.OpenView("select * from ListBox");
        View availableWSView = session.Database.OpenView("select * from AvailableWebSites");
        DirectoryEntry iisRoot = new DirectoryEntry("IIS://localhost/W3SVC");
        int order = 1;
        foreach (DirectoryEntry webSite in iisRoot.Children)
        {
            if (webSite.SchemaClassName.ToLower() == "iiswebserver" && 
                webSite.Name.ToLower() != "administration web site")
            {
                StoreWebSiteDataInListBoxTable(webSite, order, listBoxView);
                StoreWebSiteDataInAvailableWebSitesTable(webSite, availableWSView);
                order++;
            }
        }
    }
    catch (Exception ex)
    {
        session.Log("CustomActionException: " + ex.ToString());
        return ActionResult.Failure;
    }
    return ActionResult.Success;
}

Here’s what I’ve done in the code above:

  • Open two views to the msi database. One for the ListBox table and one for a custom table which I will cover later called AvailableWebSites.
  • Get the root entry in IIS by using an LDAP query.
  • Iterate every child of the web site to get and store the information I need.

Here’s the code for the two helper methods I use to store the IIS data to the Windows Installer database:

private static void StoreWebSiteDataInListBoxTable(DirectoryEntry webSite, int order, View listBoxView)
{
    Record newListBoxRecord = new Record(4);
    newListBoxRecord[1] = "WEBSITE";
    newListBoxRecord[2] = order;
    newListBoxRecord[3] = webSite.Name;
    newListBoxRecord[4] = webSite.Properties["ServerComment"].Value;
    listBoxView.Modify(ViewModifyMode.InsertTemporary, newListBoxRecord);
}
private static void StoreWebSiteDataInAvailableWebSitesTable(DirectoryEntry webSite, View availableWSView)
{
    //Get Ip, Port and Header from server bindings
    string[] serverBindings = ((string)webSite.Properties["ServerBindings"].Value).Split(':');
    string ip = serverBindings[0];
    string port = serverBindings[1];
    string header = serverBindings[2];
    Record newFoundWebSiteRecord = new Record(5);
    newFoundWebSiteRecord[1] = webSite.Name;
    newFoundWebSiteRecord[2] = webSite.Properties["ServerComment"].Value;
    newFoundWebSiteRecord[3] = port;
    newFoundWebSiteRecord[4] = ip;
    newFoundWebSiteRecord[5] = header;
    availableWSView.Modify(ViewModifyMode.InsertTemporary, newFoundWebSiteRecord);
}

The first method stores data in the ListBox table and the second in my custom AvailableWebSites table.

My custom table (which I’ll explain in more detail later) contains the following columns:

  • WebSiteNo
  • WebSiteDescription
  • WebSitePort
  • WebSiteIP
  • WebSiteHeader

The ListBox table has these four columns:

  • Property
  • Order
  • Value
  • Text

For the ListBox table the Property is used for identifying one specific list box and will allow you to get the Value of the selected item later. The Property must be the same for every record that you want displayed in one single UI list box. Order defines in which order the item are displayed in the list. Value is what’s being returned to you when accessing the property later on. The Text field is what is shown in the UI list box that the end user sees in the MSI wizard.

In my code I set the Property for all records to WEBSITE. The Order is set to 1 the first time and then incremented by 1 every iteration. The Value is set to the web site id and theText is set to the web site name.

After the user have selected the web site we can find out which one by using the WEBSITE property, which will give us the id of the web site (the Value field).

In my custom AvailableWebSites table I store some more details about every web site. But before I can do that I need to get the server bindings from IIS which is formatted like this: ip:port:header. I split these up and add them to to the database. I do the same with WebSiteNo and WebSiteDescription.

In both cases I store the values to the record and update the database (using modify). Note that you have to use InsertTemporary because you’re not allowed to write permanent data to the MSI database during installation.

Custom action – Add selected web site info to properties
After the user have selected a web site in the list box, I call a CA where I store the details about the web site to some public properties. This will make more sense later when we stitch everything together in the WiX product file, but for now here’s the code:

[CustomAction]
public static ActionResult UpdatePropsWithSelectedWebSite(Session session)
{
    try
    {
        string selectedWebSiteId = session["WEBSITE"];
        session.Log("CA: Found web site id: " + selectedWebSiteId);
        View availableWebSitesView = session.Database.OpenView("Select * from AvailableWebSites where WebSiteNo=" + selectedWebSiteId);
        availableWebSitesView.Execute();
        Record record = availableWebSitesView.Fetch();
        if ((record[1].ToString()) == selectedWebSiteId)
        {
            session["WEBSITE_DESCRIPTION"] = (string)record[2];
            session["WEBSITE_PORT"] = (string)record[3];
            session["WEBSITE_IP"] = (string)record[4];
            session["WEBSITE_HEADER"] = (string)record[5];
        }
    }
    catch(Exception ex)
    {
        session.Log("CustomActionException: " + ex.ToString());
        return ActionResult.Failure;
    }
    return ActionResult.Success;
}

Remember the id for the web site that the user selected is stored in the WEBSITE property in the ListBox table? I can get this value by calling session[“WEBSITE”]. I then query my custom table that I populated earlier for details about this web site using Select with the id of the web site in the where clause. I then store these values to the properties: WEBSITE_DESCRIPTION, WEBSITE_PORT, WEBSITE_IP and WEBSITE_HEADER. These properties and where they came from will be explained later :-)

Create a new dialog

To have the user see and select from available web sites, we need to create a dialog displaying a list box. Add a new wxs file to your project and name it SelectWebSiteDlg.wxs. Then replace any generated xml with this xml:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  <Fragment>
    <CustomAction Id="UpdatePropsWithSelectedWebSite" BinaryKey="WebSiteCA" DllEntry="UpdatePropsWithSelectedWebSite" Execute="immediate" Return="check" />
    <Binary Id="WebSiteCA" SourceFile="$(var.SolutionDir)\IISCustomAction\bin\Debug\IISCustomAction.CA.dll" />
  </Fragment>
  <Fragment>
    <UI>
      <Dialog Id="SelectWebSiteDlg" Width="370" Height="270" Title="Select Web Site">
        <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)" />
        <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" />
        <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
          <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
        </Control>
        <Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes" Text="Please select which web site you want to install to." />
        <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="Select Web Site" />
        <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.InstallDirDlgBannerBitmap)" />
        <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
        <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
        <Control Id="SelectWebSiteLabel" Type="Text" X="20" Y="60" Width="290" Height="14" NoPrefix="yes" Text="Select web site:" />
        <Control Id="SelectWebSiteCombo" Type="ListBox" X="20" Y="75" Width="200" Height="150" Property="WEBSITE" Sorted="yes" />
      </Dialog>
    </UI>
  </Fragment>
</Wix>

This xml file is separated into two fragments; one for the CA and one for the actual UI. The CA entry define in which assembly the CA is located and defines an id we can use later when calling the CA. This CA is the one I created previously for updating some properties with IIS data. The reason I’m defining it here is that I can, and it makes sense to group it with the dialog that are using it. Because, as you will see later, we’re going to call this CA after the user have clicked next on this dialog.

The UI section is where the layout of the actual UI components is defined. Most elements are default for any dialog, except SelectWebSiteLabel and SelectWebSiteCombo which is the two controls specific to the web site dialog.

I also need to update MyUI.wxs which controls which and in which order the dialogs are displayed in the wizard. I’ve added the SelectWebSiteDlg between InstallDirDlg and VerifyReadyDlg as shown here:

<Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="SelectWebSiteDlg" Order="6"><![CDATA[WIXUI_INSTALLDIR_VALID="1"]]></Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
<Publish Dialog="SelectWebSiteDlg" Control="Next" Event="DoAction" Value="UpdatePropsWithSelectedWebSite" Order="1">1</Publish>
<Publish Dialog="SelectWebSiteDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="2">1</Publish>
<Publish Dialog="SelectWebSiteDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1">1</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="SelectWebSiteDlg" Order="1">NOT Installed</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed</Publish>

See that I use DoAction to call the CA I defined earlier? This is how you hook up a CA to the “click event” of a button (or any control for that matter). Not quite as easy as in Win Forms, but it’s still quite logical.

Also note that I’ve changed the Next event for InstallDirDlg and the Back event for VerifyReadyDlg to point to my new dialog. This so the wizard will navigate correctly when the user clicks the next and back buttons.

Changes to Product.wxs (the main WiX file)

Custom Table – Storing web site info
I promised to cover my custom table in more detail later, so here it is. First the code:

<CustomTable Id="AvailableWebSites" >
  <Column Id="WebSiteNo" Category="Identifier" PrimaryKey="yes" Type="int" Width="4" />
  <Column Id="WebSiteDescription" Category="Text" Type="string" PrimaryKey="no"/>
  <Column Id="WebSitePort" Category="Text" Type="string" PrimaryKey="no"/>
  <Column Id="WebSiteIP" Category="Text" Type="string" PrimaryKey="no" Nullable="yes"/>
  <Column Id="WebSiteHeader" Category="Text" Type="string" PrimaryKey="no" Nullable="yes"/>
  <Row>
    <Data Column="WebSiteNo">0</Data>
    <Data Column="WebSiteDescription">Bogus</Data>
    <Data Column="WebSitePort">0</Data>
    <Data Column="WebSiteIP"></Data>
    <Data Column="WebSiteHeader"></Data>
  </Row>
</CustomTable>

When a user have selected the web site to install to, instead of querying IIS for details about the web site (web site number, description, port etc), I can just get it from my AvailableWebSite table. Note that I’ve added some dummy data in the first row. Without this the custom table was not stored to the MSI and I did not find any other way of getting this to work.

Properties: Define, store and retrieve
Earlier I created a CA that stored info about the selected web site into some public properties. Where did these properties come from? Well, they’re defined in the Product.wxs file like this:

<Property Id="WEBSITE_DESCRIPTION">
  <RegistrySearch Id="WebSiteDescription"
          Name="WebSiteDescription"
          Root="HKLM"
          Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
          Type="raw" />
</Property>
<Property Id="WEBSITE_PORT">
  <RegistrySearch Id="WebSitePort"
          Name="WebSitePort"
          Root="HKLM"
          Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
          Type="raw" />
</Property>
<Property Id="WEBSITE_IP">
  <RegistrySearch Id="WebSiteIP"
          Name="WebSiteIP"
          Root="HKLM"
          Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
          Type="raw" />
</Property>
<Property Id="WEBSITE_HEADER">
  <RegistrySearch Id="WebSiteHeader"
          Name="WebSiteHeader"
          Root="HKLM"
          Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
          Type="raw" />
</Property>

I’ve also defined a registry search within each property. Why? In order to have access to these values during e.g. repair or uninstall we need to store them in the registry for later use. Why? Well, the Windows Installer does not maintain state (except from INSTALLDIR and maybe a few others). Meaning any values or selections a user did during installation is not kept for later use. Since we will need to know which web site the user installed the application to, so we can remove it on uninstall, we need to store the state to the registry. If the application has been installed before, the registry search above get’s these values from the registry and stores them in their respective properties.

But how did they end up in the registry to begin with? I do that by defining a new component like this:

<Component Id="PersistWebSiteValues" Guid="C3DAE2E2-FB49-48ba-ACB0-B2B5B726AE65">
  <RegistryKey Root="HKLM" Key="SOFTWARE\torresdal.net\SimpleWebApp\Install">
    <RegistryValue Name="WebSiteDescription" Type="string" Value="[WEBSITE_DESCRIPTION]"/>
    <RegistryValue Name="WebSitePort" Type="string" Value="[WEBSITE_PORT]"/>
    <RegistryValue Name="WebSiteIP" Type="string" Value="[WEBSITE_IP]"/>
    <RegistryValue Name="WebSiteHeader" Type="string" Value="[WEBSITE_HEADER]"/>
  </RegistryKey>
</Component>

And we need to reference the component under the Feature tag:

<ComponentRef Id="PersistWebSiteValues" />

Registry key’s in WiX require a component, which is good. That means that these keys/values will be added on install and removed on uninstall, saving us manual work. The registry key above is created at SOFTWARE\torresdal.net\SimpleWebApp\Install. The actual values are the properties as you see in the Value attributes. To reference a property just use [MY_PROPERTY]. This comes in handy many times when authoring MSI installations.

Reference CA’s
One of the CA’s (the UpdatePropsWithSelectedWebSite) is actually already referenced in the SelectWebSiteDlg, but we need to reference the GetWebSites CA as well:

<CustomAction Id="GetIISWebSites" BinaryKey="IISCA" DllEntry="GetWebSites" Execute="immediate"  Return="check" />
<Binary Id="IISCA" SourceFile="$(var.SolutionDir)IISCustomAction\bin\Debug\IISCustomAction.CA.dll" />

The CustomAction tag defines a logical entry that we can use later to call this CA by using the id GetIISWebSites. In addition there is a Binary node that tells where WiX can find this CA when build the project. The CustomAction node point to this by using the BynaryKey attribute.

Then we need to make sure it’s being called:

<InstallUISequence>
  <Custom Action="GetIISWebSites" After="CostFinalize" Overridable="yes">NOT Installed</Custom>
</InstallUISequence>

The above code tells the installer to run this CA in the UISequence only when the application is not already installed (defined by the NOT Installed condition).

Change the WebSite standard action
In order for the installer to pick up the properties we’ve set for the selected web site (description, port, ip etc) we need to change the code we had previously:

<iis:WebSite Id='DefaultWebSite' Description='Default Web Site'>
    <iis:WebAddress Id='AllUnassigned' Port='80' />
</iis:WebSite>

To this:

<iis:WebSite Id="SelectedWebSite" Description="[WEBSITE_DESCRIPTION]">
  <iis:WebAddress Id="AllUnassigned" Port="[WEBSITE_PORT]" IP="[WEBSITE_IP]" Header="[WEBSITE_HEADER]" />
</iis:WebSite>

Makes sense right? I’ve also changed the id to SelectedWebSite which is more accurate in this case, meaning you need to update the WebSite reference in WebVirtualDir in the IISAppplication component as well. If you fail to do this the WiX compiler will complain, so you’re in safe hands :-)

The complete Product.wxs
There’s a lot of snippets above, so for your convenience I’ve included the complete code for Product.wxs here so you see everything together:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" xmlns:iis="http://schemas.microsoft.com/wix/IIsExtension">
  <Product Id="77da05c4-1644-4bc3-ac14-c0f721fe31fe" Name="Simple Web App" Language="1033" Version="1.0.0.0" Manufacturer="Jon Torresdal" UpgradeCode="a897ccc5-1c81-47e0-a837-65c07a72a7bc">
    <Package InstallerVersion="400" Compressed="yes" />
    <Media Id="1" Cabinet="WixProject1.cab" EmbedCab="yes" />
    <Property Id="TARGETVDIR" Value="SimpleWebApp"/>
    <Property Id="WEBSITE_DESCRIPTION">
      <RegistrySearch Id="WebSiteDescription"
              Name="WebSiteDescription"
              Root="HKLM"
              Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
              Type="raw" />
    </Property>
    <Property Id="WEBSITE_PORT">
      <RegistrySearch Id="WebSitePort"
              Name="WebSitePort"
              Root="HKLM"
              Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
              Type="raw" />
    </Property>
    <Property Id="WEBSITE_IP">
      <RegistrySearch Id="WebSiteIP"
              Name="WebSiteIP"
              Root="HKLM"
              Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
              Type="raw" />
    </Property>
    <Property Id="WEBSITE_HEADER">
      <RegistrySearch Id="WebSiteHeader"
              Name="WebSiteHeader"
              Root="HKLM"
              Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
              Type="raw" />
    </Property>
    <CustomAction Id="GetIISWebSites" BinaryKey="IISCA" DllEntry="GetWebSites" Execute="immediate"  Return="check" />
    <Binary Id="IISCA" SourceFile="$(var.SolutionDir)IISCustomAction\bin\Debug\IISCustomAction.CA.dll" />
    <InstallUISequence>
      <Custom Action="GetIISWebSites" After="CostFinalize" Overridable="yes">NOT Installed</Custom>
    </InstallUISequence>
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder">
        <Directory Id="INSTALLLOCATION" Name="SimpleWebApp">
          <Component Id="Default.aspx" Guid="fc46b1a2-35e9-4a17-896b-80b2daaae567">
            <File Id="Default.aspx" Name="Default.aspx" Source="$(var.SolutionDir)SimpleWebApp\Default.aspx" DiskId="1" KeyPath="yes" />
          </Component>
          <Component Id="Web.config" Guid="2ED81B77-F153-4003-9006-4770D789D4B6">
            <File Id="Web.config" Name="Web.config" Source="$(var.SolutionDir)SimpleWebApp\Web.config" DiskId="1" KeyPath="yes" />
            <util:XmlFile Id="AppSettingsAddNode" File="[INSTALLLOCATION]Web.config" Action="createElement" ElementPath="/configuration/appSettings" Name="add" Sequence="1" />
            <util:XmlFile Id="AppSettingsKeyAttribute" Action="setValue" File="[INSTALLLOCATION]Web.config" ElementPath="/configuration/appSettings/add" Name="key" Value="AddedDuringInstall" Sequence="2" />
            <util:XmlFile Id="AppSettingsValueAttribute" Action="setValue" File="[INSTALLLOCATION]Web.config" ElementPath="/configuration/appSettings/add" Name="value" Value="This text was added during installation." Sequence="3" />
          </Component>
          <Directory Id="binFolder" Name="bin">
            <Component Id="SimpleWebApp.dll" Guid="7FC6DA37-12E5-463d-8E7E-08F73E40CCF2">
              <File Id="SimpleWebApp.dll" Name="SimpleWebApp.dll" Source="$(var.SolutionDir)SimpleWebApp\Bin\SimpleWebApp.dll" DiskId="1" KeyPath="yes" />
            </Component>
          </Directory>
        </Directory>
      </Directory>
      <Directory Id="ProgramMenuFolder">
        <Directory Id="MyWebAppStartMenuFolder" Name="SimpleWebApp">
          <Component Id="StartMenuFolder" Guid="B3AEC4C4-3F8E-4865-B87A-B750533776B5" >
            <util:InternetShortcut Id="SimpleWebAppShortcut" Name="SimpleWebApp" Target="http://localhost/SimpleWebApp/Default.aspx" Directory="MyWebAppStartMenuFolder" />
            <RemoveFolder Id="RemoveStartMenuFolder1" On="uninstall"/>
            <RegistryKey Root="HKCU" Key="SOFTWARE\torresdal.net\SimpleWebApp\SimpleWebAppShortcut">
              <RegistryValue Type="string" Value="Default Value"/>
            </RegistryKey>
          </Component>
        </Directory>
      </Directory>
      <Component Id="IISApplication" Guid="FFA12D9C-5AEC-45f8-AA7D-5C4CEC7FA466">
        <iis:WebAppPool Id="SWAAppPool" Name="SWAAppPool" />
        <iis:WebVirtualDir Id="VirtualDir" Alias="[TARGETVDIR]" Directory="INSTALLLOCATION" WebSite="SelectedWebSite">
          <iis:WebApplication Id="SimpleWebAppApp" Name="[TARGETVDIR]" WebAppPool="SWAAppPool" />
          <iis:WebDirProperties Id="WebVirtualDirProperties" Execute="yes" Script="yes" Read="yes" WindowsAuthentication="no" AnonymousAccess="yes" IIsControlledPassword="yes" />
        </iis:WebVirtualDir>
      </Component>
      <Component Id="PersistWebSiteValues" Guid="C3DAE2E2-FB49-48ba-ACB0-B2B5B726AE65">
        <RegistryKey Root="HKLM" Key="SOFTWARE\torresdal.net\SimpleWebApp\Install">
          <RegistryValue Name="WebSiteDescription" Type="string" Value="[WEBSITE_DESCRIPTION]"/>
          <RegistryValue Name="WebSitePort" Type="string" Value="[WEBSITE_PORT]"/>
          <RegistryValue Name="WebSiteIP" Type="string" Value="[WEBSITE_IP]"/>
          <RegistryValue Name="WebSiteHeader" Type="string" Value="[WEBSITE_HEADER]"/>
        </RegistryKey>
      </Component>
    </Directory>
    <CustomTable Id="AvailableWebSites" >
      <Column Id="WebSiteNo" Category="Identifier" PrimaryKey="yes" Type="int" Width="4" />
      <Column Id="WebSiteDescription" Category="Text" Type="string" PrimaryKey="no"/>
      <Column Id="WebSitePort" Category="Text" Type="string" PrimaryKey="no"/>
      <Column Id="WebSiteIP" Category="Text" Type="string" PrimaryKey="no" Nullable="yes"/>
      <Column Id="WebSiteHeader" Category="Text" Type="string" PrimaryKey="no" Nullable="yes"/>
      <Row>
        <Data Column="WebSiteNo">0</Data>
        <Data Column="WebSiteDescription">Bogus</Data>
        <Data Column="WebSitePort">0</Data>
        <Data Column="WebSiteIP"></Data>
        <Data Column="WebSiteHeader"></Data>
      </Row>
    </CustomTable>
    <iis:WebSite Id="SelectedWebSite" Description="[WEBSITE_DESCRIPTION]">
      <iis:WebAddress Id="AllUnassigned" Port="[WEBSITE_PORT]" IP="[WEBSITE_IP]" Header="[WEBSITE_HEADER]" />
    </iis:WebSite>
    <Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION" />
    <UIRef Id="MyUI" />
    <Feature Id="ProductFeature" Title="SimpleWebApp" Level="1">
      <ComponentRef Id="Default.aspx" />
      <ComponentRef Id="Web.config" />
      <ComponentRef Id="SimpleWebApp.dll" />
      <ComponentRef Id="StartMenuFolder" />
      <ComponentRef Id="IISApplication" />
      <ComponentRef Id="PersistWebSiteValues" />
    </Feature>
  </Product>
</Wix>

The End! 
That’s that! If you’ve come this far you (hopefully) have a functional CA with goodies that allow your user to select web sites in your installer. Hope you found this post useful and let me know if you find any errors or questions, and I’ll help you out. Btw here’s the end result:

MsiSelectWebSite

.Net | CSharp | Deployment | IIS | WiX
Friday, October 24, 2008 10:41:23 PM (W. Europe Daylight Time, UTC+02:00)
Sunday, December 23, 2007

iis7logoSP's are a necessity, but what I want to know is what have they done to improve IIS 7? Here's what the release notes for SP1 RC says:

IIS was included in some Windows Vista SKUs to enable web-based developers to write and test their applications. IIS in Windows Server 2008 is a significant server role which requires Internet-level scalability and performance requirements. The IIS7 components have gone through significant performance and reliability enhancements since Windows Vista originally shipped, in order to be a large-scale server component. These changes do not affect most Windows Vista users who do not even have the IIS7 components installed, however because Windows Vista and Windows Server are aligned, these changes are included in Windows Vista SP1.

You can find the "complete" change log here: http://technet2.microsoft.com/windowsvista/en/library/005f921e-f706-401e-abb5-eec42ea0a03e1033.mspx?mfr=true

So does this mean that there is a full UI and ftp support? The current IIS available in Vista is very limited by this. You can configure most of the stuff in config files (like I did with http redirect and logging bandwidth and referrers), but I would at least expect the features found in IIS 6 to be available from the UI in IIS 7. I've googled around to find some  more info, but nothing yet. I guess I'll have to install the RC to find out. Not sure if I want to risk it though...

IIS | Microsoft | Software | Vista | Windows
Sunday, December 23, 2007 12:59:30 AM (W. Europe Standard Time, UTC+01:00)
Saturday, August 11, 2007
Christian Weyer has an article on MSDN Mag together with Steve Maine and Dominick Baier about using Windows Process Activation Service (WAS) for non http protocols. Actually it's about much more than that, but that is what I found interesting. Hosting WCF services in WAS gives you the possibility to use NetTcp, Named Pipes and MSMQ protocols. For more info about WAS you should read the article. Anyway, using WAS as a host for WCF services sounds like a good idea. You don't have to create and maintain your own NT Service and you get stuff like "on-demand activation, process health monitoring, enterprise-class manageability and rapid failure protection" for free. But then you also get some lifetime management "features":

"...WAS does demand activation (as you know, the "A" in "WAS" stands for activation). This means that the application domain hosting a service only gets created when a request message comes in. Application domains shut down again after a configurable idle period. There are also several reasons why WAS or the ASP.NET runtime may decide to recycle the application domain or even the whole worker process."
So I suspect there would be some overhead of creating and shutting down the app domain, unless you can set the idle period to infinite of course. And then you have the recycle bit... Hmmm.... Do I want that?

If I were asked: "WAS or NT Service?", I think I'll go for the NT Service, unless there's something I totally missed out on here... which definitely could be the case <smile>.

If you want to try this out you might find these code samples interesting in addition to the article mentioned.
.Net | IIS | WCF
Saturday, August 11, 2007 1:46:31 AM (W. Europe Daylight Time, UTC+02:00)
Wednesday, February 21, 2007
I'm using SmarterStats for my blog statistics and have been annoyed by two things for a while. I get no statistics for bandwidth and referers. I looked around on the web and found that these things are actually not logged in IIS by default. I found this article describing how to enable it in IIS 6. Now I had to find out how to do the same thing in IIS 7. As I and others have mentioned earlier, IIS 7 in Vista don't have much UI yet, so most of the work is done in config files. I checked out the schema and came up with this:

<sites>
  <
site name="Default Web Site" id="1">
   
<
application path="/" applicationPool="ASP.NET 1.1">
     
<
virtualDirectory path="/" physicalPath="" />
   
</
application>
    <
bindings>
      <
binding protocol="http" bindingInformation="*:80:" />
   
</
bindings>
    <
logFile logExtFileFlags="Date, Time, ClientIP, UserName, ServerIP, Method, UriStem, UriQuery, HttpStatus, Win32Status, ServerPort, UserAgent, HttpSubStatus, BytesSent, BytesRecv, Referer" />

  </site>
</
sites>

After these changes I went into SmarterStats and found values for both bandwidth and referers. Fantastic!
IIS | Vista
Wednesday, February 21, 2007 7:29:47 PM (W. Europe Standard Time, UTC+01:00)
Wednesday, January 24, 2007
MsdnLive.jpgDon't forget to register for MSDN Live in Bergen 13. February! It's no surprise that Ajax and IIS 7 attract a lot of people to this event, so register now before it's full. You don't want to become the guy, who wasn't there, would you? ;) You can even pick up some Tips & Tricks for VS 2005. If this Tips & Tricks session is the same as at TechEd, you'll have to see it!
Wednesday, January 24, 2007 12:37:07 AM (W. Europe Standard Time, UTC+01:00)
Wednesday, December 27, 2006
When I moved my blog from Blogger to DasBlog I wanted to keep rss.xml and atom.xml, and redirect them to the new DasBlog url’s. I googled a bit but didn’t find much. And the ones I found I wasn’t able to use (maybe because I’m running Vista RC2). Anyway, last night enough was enough and I decided to put this to an end. I started to look at IIS’s config file applicationHost.config (usually found at C:\Windows\System32\inetsrv\config) and one section got my attention: <httpRedirect enabled="false" />

The problem was finding schema documentation for this element. After a while I found this (http://www.iis.net/default.aspx?tabid=2&subtabid=25&i=946&p=24). To solve my problem I used this:

<httpRedirect     enabled="true"

exactDestination="true

httpResponseStatus="Permanent">

        <add      wildcard="/rss.xml"

destination="/SyndicationService.asmx/GetRss" />

        <add      wildcard="/atom.xml"

destination="/SyndicationService.asmx/GetAtom" />

</httpRedirect>

So if you have a similar problem I hope this helps.

Update:
I upgraded to Vista Release and suddenly the redirect didn't work anymore. I tried and tried and tried and gave up. I tried again and came over this:


WindowsFeatures.jpg

That nailed it. Don't know why I didn't see this before, but it would be nice though if the error message said something about enabling this feature.
DasBlog | IIS | Vista | Web
Wednesday, December 27, 2006 3:27:57 PM (W. Europe Standard Time, UTC+01:00)
RSS RSS - Comments Twitter LinkedIn
         
SEARCH
 
 
         
TOP POSTS
   
         
NAVIGATION
   
         
CATEGORIES
  .Net (57) Agile (29) Ajax (5) Architecture (17) Articles (1) ASP.NET (3) ASP.NET-MVC (1) Blogging (12) Books (2) BPEL (1) CleanCode (1) CloudComputing (7) Community (2) CSharp (11) DasBlog (5) Database (2) DDD (5) Deployment (14) DSL (1) Events (37) ExtremeProgramming (6) Fun (6) Gadgets (4) IIS (8) InfoQ (4) Java (2) Lean (2) Linq (2) MemoryLeaks (5) Microsoft (37) MVC (1) NDC (2) NNUG (35) Other (10) Patterns (9) Performance (3) Scrum (17) Security (3) Silverlight (4) Software (19) TeamManagement (11) TechEd (7) Testing (4) Tools (23) TvGuide (1) Vista (15) VisualStudio (16) WCF (7) Web (15) Windows (10) WiX (9) Work (16) Workflow (3)  
         
ARCHIVE
   
         
BLOGROLL
   
         
ON THIS PAGE...
 
Using IIS7 And Application Request Routing as Load Balancer – Part 2
Using IIS7 And Application Request Routing as Load Balancer – Part 1
WiX and DTF: Using a Custom Action to list available web sites on IIS
Windows Vista SP1 - IIS 7
Hosting WCF services in WAS
Logging bandwidth and referers on IIS 7
MSDN Live in Bergen
Http redirect on IIS7