Setting application environment variables in IIS without restarts : Andrew Lock

Setting application environment variables in IIS without restarts
by: Andrew Lock
blow post content copied from  Andrew Lock | .NET Escapades
click here to view original post


I recently needed to set some environment variables for an application that was running in IIS on Windows. It turns out there's lots of different ways to do that. In this post, I describe some of the many ways to do that, both from the Windows and IIS UI and from the command line. For each approach I describe the caveats, advantages, and disadvantages.

How does IIS work?

IIS is a huge beast, with decades of legacy, but to understand how to control setting environment variables for an application, it helps to have a basic understanding of how IIS works under the hood. This introduction to IIS on Microsoft Learn provides a good overview, including the interaction between the main components involved.

If we consider how an HTTP request coming from a client reaches an ASP.NET application running in IIS, there are a variety of components to consider:

  • HTTP.sys runs as part of Windows. It's a kernel-mode driver that handles HTTP requests from the network, and forwards them to a worker process.
  • Windows Process Activation Service (WAS) is responsible for managing and configuration application pool configuration and worker processes.
  • World Wide Web Publishing Service (W3SVC) acts as an adapter between WAS and HTTP.sys. It is responsible for updating HTTP.sys when app pool configuration changes, and for notifying WAS when a new request is created. WAS and W3SVC run inside the same process, svchost.exe.
  • Application pools are responsible for starting worker processes for handling requests. Multiple sites can be mapped to the same application pool.
  • Worker processes are responsible for running the managed and native modules required to handle a request. As the name suggests, worker processes are separated from each other by a process boundary, running in a process called w3wp.exe. However each pool can (optionally) host multiple distinct sites or applications inside the same process. Additionally, a single application pool may optionally contain multiple worker process (termed a web garden).

The following diagram shows how the components interact in IIS, when an HTTP request is received from a client.

How IIS processes an HTTP request

As shown in the diagram, the general flow is as follows:

  1. A client sends a request. HTTP.sys intercepts it from the network
  2. HTTP.sys contacts W3SVC/WAS to retrieve configuration details for the request.
  3. WAS retrieves configuration information for the request, based on global configuration sources the applicationHost.config, and web.config for individual sites.
  4. WAS passes the app pool and site configuration to W3SVC which reconfigures HTTP.sys with the config.
  5. WAS starts a worker process to handle the incoming request, if one does not already exist.
  6. Once the worker process is running, HTTP.sys passes the request details to the worker process, which generates a response and returns it to HTTP.sys.
  7. HTTP.sys forwards the request to the client.

On subsequent requests, if a worker process already exists, HTTP.sys can forward the request directly to the process. If configuration changes subsequently, WAS creates a new worker process, and W2SVC updates HTTP.sys's configuration.

Now we understand the overall process, we can consider how to configure our worker processes.

Changing the environment variables for a worker process

As a reminder, the goal is to change the environment variables for the worker processes, w3wp.exe, which handle our requests. There are a variety of possible ways to do that which I'm going to run through in this post:

  • Set system environment variables
  • Set the environment variables for the W3SVC/WAS service.
  • Set environment variables at the application pool level in applicationHost.config

This isn't an exhaustive list. It's also possible to set environment variables for an individual application, and separately for ASP.NET Core applications. In this post I'm looking at how to set variables somewhat "globally".

We'll start by looking at how to set system environment variables.

Setting system environment variables

The "system" environment variables apply to all processes that run on your machine. Typically when you're setting variables using PowerShell you might you syntax something like the following:

$env:MY_SPECIAL_KEY = "Some value"

However, that only sets the variable for the current session, rather than setting it globally. It's actually easiest to use the System.Environment .NET type to set the environment variable at the system level:

[System.Environment]::SetEnvironmentVariable("MY_SPECIAL_KEY", "Some value", [System.EnvironmentVariableTarget]::Machine)

After setting the variable, we need to stop the WAS and W3SVC services so that they pick up the environment changes. The easiest way to do this is to run the following from an elevated command/PowerShell prompt:

# Stop the WAS service. This automatically stops the W3SVC service too.
net stop /y was
# Restart the services, so they pick up the new variables
net start w3svc

This is arguably one of the easiest ways to set environment variables for the worker processes, but it's also the most crude. Any process running on the system will retrieve the environment variables, which might not be something you want to do.

An easy of example of where you might not want to do this is if you're setting the .NET profiling API environment variables: setting these at the system level would mean every .NET process on your Windows machine would be processed, which is practically certain to have a significant stability impact.

Let's assume we don't want to set the variables at the system level. The next best thing is to set them for the IIS services instead.

Setting environment variables for W3SVC and WAS

W3SVC and WAS run as Windows services, inside an instance of svchost.exe. It's possible to scope environment variables specifically to these services, but to do so, you need to edit the registry.

You can add environment variables for a service by editing the HKLM:SYSTEM\CurrentControlSet\Services\<SERVICE NAME> registry key. For example, to add an environment variable to a service called SomeService, you can use something like this:

Set-ItemProperty HKLM:SYSTEM\CurrentControlSet\Services\<SERVICE NAME> -Name Environment -Value 'MY_SPECIAL_KEY=Something' -Type MultiString

Note that the above example changes the environment variables to the value you provide, it doesn't append to it. If you want to include multiple variables separate the values with a line break, e.g. VAL1=1`r`nVAL2=2.

To update the W3SVC and WAS services to ensure they have our environment variables, we can use the following:

Set-ItemProperty HKLM:SYSTEM\CurrentControlSet\Services\W3SVC -Name Environment -Value 'MY_SPECIAL_KEY=Something' -Type MultiString
Set-ItemProperty HKLM:SYSTEM\CurrentControlSet\Services\WAS -Name Environment -Value 'MY_SPECIAL_KEY=Something' -Type MultiString

You can also do this via the Registry Editor UI. Add a new multi-string value to the key called Environment, and add a line in the KEY=VALUE format for each variable

Updating the environment variables for the WAS service using the registry editor

Just as when we set the system variables, this doesn't change the variables for the already-running services, so we need to stop and start them again, to ensure they pick up the values from the registry:

net stop /y was
net start w3svc

The environment variables you set on the W3SVC and WAS services are inherited by the worker processes that WAS starts, so each w3wp.exe process will have the MY_SPECIAL_KEY variable.

This is better than before, in that we've limited the scope of our variable. We're now only applying it to the processes we're interested in.

Setting environment variables in applicationHost.config

Configuration in IIS combines values from multiple sources: the root web.config file, the applicationHost.config file, web.config files for individual applications etc. I'm not going to go into great detail here, instead we're going to look at a single file: applicationHost.config.

applicationHost.config contains the definition of your app pools (among many other things) in the <system.applicationHost> section and is what WAS reads when determining how to create worker processes to handle a request. The following is a snippet from an applicationHost.config file that contains two application pools:

<system.applicationHost>
  <applicationPools>

    <add name="DefaultAppPool" />
    <add name="dotnet7" autoStart="true" managedRuntimeVersion="">
      <environmentVariables>
        <add name="MY_VAL" value="1" />
      </environmentVariables>
    </add>

    <applicationPoolDefaults managedRuntimeVersion="v4.0">
      <processModel identityType="ApplicationPoolIdentity" loadUserProfile="true" setProfileEnvironment="false" />
    </applicationPoolDefaults>
  </applicationPools>

This configuration defines two application pools using the <add> element:

  • DefaultAppPool
  • dotnet7

There's also an <applicationPoolDefaults> element which, as you might expect, defines the default behaviour for application pools. individual app pools can override the default behaviour, as you can see where the dotnet7 pool has managedRuntimeVersion="" which corresponds to the recommended "No Managed Code" setting for .NET Core apps.

The dotnet7 pool in the above example already defines a custom environment variables, indicated by the nested <environmentVariables> element. This ensures the w3wp.exe worker process also has the environment variable MY_VAL=1.

If we want to add environment variables in applicationHost.config, we have two main options:

  • Add an environment variable to the <applicationPoolDefaults> element and inherit the variable in other pools.
  • Add an environment variable to a specific pool, just as the dotnet7 pool has. This overrides any values set in <applicationPoolDefaults>; they're not merged at runtime.

If we assume that we want to add the environment variable to all worker processes, then we may need to take both approaches. Adding the variable to <applicationPoolDefaults> ensures that all variables without additional configuration will have the environment variable. It also ensures any new pools created will also have the environment variable by default.

However, if a pool separately defines its <environmentVariables>, as is the case for the dotnet7 pool above, then we must also add the environment variable explicitly to this pool, as it won't inherit the values provided in <applicationPoolDefaults>.

There are multiple ways we could modify the applicationHost.config to match our requirements. If you're feeling really brave, you could directly edit the applicationHost.config file found at C:\Windows\System32\inetsrv\config\applicationHost.config, but I don't recommend it 😅 Instead I'll show how to make the changes using the IIS UI and using the appcmd.exe tool that comes with IIS.

I recently discovered there's also an open source REST API implementation for managing IIS available here: https://github.com/microsoft/IIS.Administration/ but I haven't tried using it yet!

Using the IIS UI

The IIS UI is really… something. To make the changes we want, adding an environment variable to the <applicationPoolDefaults> element and to each of the application pools, we need to click around dozens of times 😅 I'll describe the flow here, but I've only included a few key screenshots:

  1. Go to the root of the IIS Server Manager UI, and open Configuration Editor
  2. From the drop-down, select the system.applicationHost/applicationPools section:

Setting the system.applicationHost/applicationPools section

  1. Expand the applicationPoolDefaults element, click on environmentVariables, and click the ellipsis (…) on the right hand side, which opens the collection editor.

Editing the applicationPoolDefaults environemntVariables

  1. You can add environment variables to the collection by clicking Add in the right panel and entering the key and value in the lower pane.

Adding an environemntVariable

  1. Once you've added all the variables you need, close the window.
  2. Open the app pool collection, by clicking the ellipsis (…) on the top row where it says (Collection)
  3. This opens the application pools list. Choose a pool to edit from the top pane.
  4. In the bottom pane, click on environmentVariables, and click the ellipsis (…) on the right hand side, which opens the collection editor.

Editing the environemntVariables for a specific pool

  1. As before, add environment variables to the collection by clicking Add in the right panel and entering the key and value in the lower pane.
  2. Close the environment variable collection window, and repeat steps 8 and 9 for each app pool.
  3. After you've made all the required changes, Close the application pool window.
  4. Click Apply from the right hand pane, to update the application pools and to restart all the worker processes.

After working through all these steps our applicationHost.config looks something like this:

<applicationPools>
  <add name="DefaultAppPool" />
  <add name="dotnet7" autoStart="true" managedRuntimeVersion="">
    <environmentVariables>
      <add name="MY_VAL" value="1" />
      <add name="MY_SPECIAL_KEY" value="Something" />
    </environmentVariables>
  </add>

  <applicationPoolDefaults managedRuntimeVersion="v4.0">
    <processModel identityType="ApplicationPoolIdentity" loadUserProfile="true" setProfileEnvironment="false" />
    <environmentVariables>
      <add name="MY_SPECIAL_KEY" value="Something" />
    </environmentVariables>
  </applicationPoolDefaults>
</applicationPools>

You can see that both the dotnet7 and applicationPoolDefaults elements have the environment variable we want. I chose not to add it to DefaultAppPool in this case, as it inherits the default environment variables, but you could have added it there as well.

Using the IIS UI is "easy" but it's definitely convoluted. A better approach is to use the command line.

Using appcmd.exe

appcmd.exe ships with IIS, and provides a somewhat easier way to interact with applicationHost.config. That said, this is still a pretty old command line program, so it's not the prettiest!

Viewing the help for appcmd.exe you can see there's a lot you can do with it:

C:\Windows\System32\inetsrv\appcmd.exe /?

General purpose IIS command line administration tool.

APPCMD (command) (object-type) <identifier> </parameter1:value1 ...>

Supported object types:

  SITE      Administration of virtual sites
  APP       Administration of applications
  VDIR      Administration of virtual directories
  APPPOOL   Administration of application pools
  CONFIG    Administration of general configuration sections
  WP        Administration of worker processes
  REQUEST   Administration of HTTP requests
  MODULE    Administration of server modules
  BACKUP    Administration of server configuration backups
  TRACE     Working with failed request trace logs
  BINDING   Object for working with SSL bindings

(To list commands supported by each object use /?, e.g. 'appcmd.exe site /?')

We're going to be working with the config command. We can perform the same modifications as in the previous section with a single large command. I've broken it over multiple lines for clarity, but you should remove these before running the command yourself

C:\Windows\System32\inetsrv\appcmd.exe set config -section:system.applicationHost/applicationPools
  /+"[name='dotnet7'].environmentVariables.[name='MY_SPECIAL_KEY',value='Something']"
  /+"applicationPoolDefaults.environmentVariables.[name='MY_SPECIAL_KEY',value='Something']"
  /commit:apphost;

Note that the above command adds the environment variables if they don't exist, but it will fail if the variables already exist. To update an existing value, you should use syntax similar to the following:

C:\Windows\System32\inetsrv\appcmd.exe set config -section:system.applicationHost/applicationPools
  /"[name='dotnet7'].environmentVariables.[name='MY_SPECIAL_KEY'].value:'Something'"
  /"applicationPoolDefaults.environmentVariables.[name='MY_SPECIAL_KEY'].value:'Something'"
  /commit:apphost;

The differences in the above command are subtle:

  • Use /+ to add a value; use / when updating a variable
  • When adding a new environment variable, define both the name and value attributes in an array
  • When updating the value of an existing environment variable, use .value:'<VALUE>'

It took me a while to figure out the syntax for how to update these values when they already exist. If it wasn't for this guide I would probably have given up entirely 😅

Theoretically, this blog should be finished, but I wanted to show just one more "trick", and that's how to avoid automatically restarting your worker pools when you update applicationHost.config.

Avoiding app-pool restarts on configuration changes

By default, whenever you make changes to applicationhost.config, your app pools will be restarted. This makes sense: if you've made a change to configuration, you probably want the new configuration to be applied as soon as possible.

However, restarting an application pool might result in down time for your application, if requests arrive and cannot be serviced while the worker process is being killed and restarted. In some cases you don't need the configuration to be applied immediately and might be willing to wait until the app pool is restarted "naturally", when a new version of the app is deployed for example.

The <environmentVariables> element nested inside <applicationPools> has a sibling element, <recycling> which controls some of the characteristics of application pool recycling. Of particular interest to us is the disallowRotationOnConfigChange attribute, which exists on both individual app pools and on <applicationPoolDefaults>:

<applicationPools>
  <add name="DefaultAppPool" />
  <add name="dotnet7" autoStart="true" managedRuntimeVersion="">
    <environmentVariables>
      <add name="MY_SPECIAL_KEY" value="'Something'" />
    </environmentVariables>
    <!-- 👇 Pool-specific recycling settings -->
    <recycling disallowRotationOnConfigChange="false" />
  </add>

  <applicationPoolDefaults managedRuntimeVersion="v4.0">
    <processModel identityType="ApplicationPoolIdentity" loadUserProfile="true" setProfileEnvironment="false" />
    <environmentVariables>
      <add name="MY_SPECIAL_KEY" value="'Something'" />
    </environmentVariables>
    <!-- 👇 Default recycling settings for pools that don't override -->
    <recycling disallowRotationOnConfigChange="true" />
  </applicationPoolDefaults>
</applicationPools>

disallowRotationOnConfigChange defaults to false. This means that when when the configuration of an application pool changes, the W3SVC service causes the worker process with the old configuration to be shut down. A new worker process, which uses the new configuration, is created in its place.

If you set disallowRotationOnConfigChange to true, this rotation no longer happens. The worker process with the old configuration keeps running and handling requests. The next time the app pool is recycled for some reason, the worker process will naturally pick up the new configuration.

Interestingly, changing the value of this property doesn't trigger a configuration change itself, whether you're setting the value to true or false. This provides a mechanism for temporarily disabling the recycling, for a specific configuration change, by temporarily enabling recycling. For example, we could add our variables while also not forcing recycling, using something like the following:

C:\Windows\System32\inetsrv\appcmd.exe set config -section:system.applicationHost/applicationPools
  /"[name='dotnet7'].recycling.disallowRotationOnConfigChange:true"
  /"applicationPoolDefaults.recycling.disallowRotationOnConfigChange:true"
  /+"[name='dotnet7'].environmentVariables.[name='MY_SPECIAL_KEY',value='Something']"
  /+"applicationPoolDefaults.environmentVariables.[name='MY_SPECIAL_KEY',value='Something']"
  /commit:apphost;

This applies the configuration without restarting the app pool. We can then restore the original behaviour subsequently by setting the property to false:

C:\Windows\System32\inetsrv\appcmd.exe set config -section:system.applicationHost/applicationPools
  /"[name='dotnet7'].recycling.disallowRotationOnConfigChange:true"
  /"applicationPoolDefaults.recycling.disallowRotationOnConfigChange:true"
  /commit:apphost;

And with that we have a mechanism for applying configurations with restarts. It feels like a bit of a hack, but hey, it works!

Summary

In this post I began by describing the basic architecture of IIS and some of its components, such as HTTP.sys, W3SVC and WAS. I then described some of the ways to apply environment variables to IIS worker processes, starting with setting system environment variables, followed by setting service-level variables to W3SVC and WAS, and finally using applicationHost.config to add environment variables either to the application pool defaults or to specific application pools.

As a bonus, I showed how you can use the recycling.disallowRotationOnConfigChange attribute to prevent application pools from restarting automatically when you change its configuration.


February 18, 2025 at 02:30PM
Click here for more details...

=============================
The original post is available in Andrew Lock | .NET Escapades by Andrew Lock
this post has been published as it is through automation. Automation script brings all the top bloggers post under a single umbrella.
The purpose of this blog, Follow the top Salesforce bloggers and collect all blogs in a single place through automation.
============================

Salesforce