实践笔记:IIS + URL Rewrite + ARR 实现 ASP.NET Core 蓝绿部署

最近用户有个需求:更新 ASP.NET Core 应用时,要让访问不中断且用户无感知,部署环境为 Windows Server + IIS。自然想到了蓝绿部署,之前没有应用过 URL Rewrite + ARR,就趁此实践一下。
原本想着很简单:对 URL 重写规则不熟,直接问 ai。结果反倒被 ai 误导,折腾了好一阵子才搞好,在此分享配置过程、重写规则以及相关代码。


1 概念简介

开始之前,简要说明一下本文涉及的三个关键概念及其在本方案中的作用:

  • URL Rewrite
    IIS 的 URL 重写模块,可根据设置的规则匹配并处理请求。在本方案中,它承担关键的请求分发工作:通过在一个 Switch 站点中配置重写规则,将请求按需转发到 Blue 站点或 Green 站点,实现版本之间的快速切换。

  • ARR(Application Request Routing)
    IIS 的反向代理扩展,提供代理与转发能力。需要说明的是本方案没有使用 ARR 的 Server Farms 功能,而是依赖其反向代理能力以便支持 URL Rewrite 的 “代理式重写”:有了 ARR 重写才能有反代效果。

  • 蓝绿部署(Blue-Green Deployment)
    一种应用程序发布策略,即准备两套功能一致的环境(蓝/绿),在同一时间只有一个环境(如蓝)承载线上流量。新版本部署到闲置环境(绿),测试通过后,通过切换流量瞬间完成发布,实现零停机和快速回滚。


2 最终部署结构

先说结果:

IIS 蓝绿部署动画

示例里的部署目录结构(本文里会以此目录为例):

Folders

即:在 IIS 里创建 3 个站点:Switch 站点、Blue 站点以及 Green 站点,需要共享的配置、缓存、附件等位于站外。数据库自然也用同一数据库。

用户通过 Switch 站点 http://192.168.0.116:9080 访问系统(各站点端口可根据你的实际情况设置),Switch 站点再根据设置的重写规则,将用户请求导向 Blue 站点或 Green 站点。蓝绿站点同时只有其中的一个为用户提供服务。

初始部署时(v1.0.0),应用发布到 Blue 站点,并让 Switch 站点将请求导向 Blue 站点,用户开始正常访问。
第一次更新(v1.0.1),新版本发布到 Green 站点并进行测试、预热,然后让 Switch 站点将请求导向 Green 站点,用户访问不中断。
第二次更新(v1.0.2),Blue 站点此时处于空闲状态,因此可以安全地将其停掉,并删除旧版本、放入新版本进行测试、预热,然后让 Switch 站点再将请求导向 Blue 站点,用户访问仍不会中断。
如此重复,滚动更新。

这里 有个蓝绿部署示例应用,可分别用两个浏览器去登录切换试试:在浏览器A里打开一个列表或编辑页面,然后在浏览器B里切换一下站点,再回到浏览器A里继续进行分页查询或点击保存按钮,响应将依然正常。如果只有一个浏览器,可分别用正常模式和无痕模式去登录。


3 环境准备

先确保 IIS 与 ASP.NET Core 运行环境已安装好,运行环境版本要与你发布应用时指定的一致。

3.1 安装 URL Rewrite

下载地址:https://www.iis.net/downloads/microsoft/url-rewrite,到页面底部下载适用自己的安装包。
要确认是否安装:打开 IIS 管理器,在左侧选中一个站点,看看右侧功能列表里有没有 "URL 重写"

3.2 ARR 安装与配置

下载地址:https://www.iis.net/downloads/microsoft/application-request-routing
安装好后,打开 IIS 管理器,在左侧选中计算机名服务器名,在右侧功能列表里找到 "Application Request Routing Cache"

ARR1

双击打开,在右侧找到并点击 "Server Proxy Settings"

ARR2

然后按照下图配置:

ARR3

为何取消选中 "Reverse rewrite host in response headers":因为选中后响应头中的 Host 会被强行替换成 Switch 的 Host,这在跨域回调时可能会有问题。

至此 ARR 就配置好了,因为我们不需要使用其 Server Farms 功能。


4 蓝绿站点创建与配置

创建 Blue 站点和 Green 站点,路径分别指向 QAdminAppBlue 目录和 QAdminAppGreen 目录,将用来放置应用程序文件。
让两个站点分别监听 5001 和 5002 端口(端口号你可自行调整),各自使用独立的应用程序池并把应用程序池的 .NET CLR 版本均置为 "无托管代码"

另外,把蓝绿站点均绑定到 IP 地址 127.0.0.1 上:因为你不应允许用户绕过 Switch 站点直接访问 Blue 站点和 Green 站点。
当然也可通过其它途径达到此目的,比如用 Windows 防火墙。


5 Switch 站点创建与配置

这里是本方案里最关键的配置部分。

5.1 创建 Switch 站点

创建 Switch 站点,路径指向 QAdminAppSwitch目录,该目录下将只有个 web.config 文件,内容为 URL 重写规则。
让 Switch 站点监听 9080 端口(我本机 80 已被占用,你按实际情况设置),也使用独立的应用程序池并把其 .NET CLR 版本置为 "无托管代码"
将 Switch 站点绑定到对外使用的一个 IP 地址上(比如 192.168.0.116),如果是要通过域名访问,绑定时再设置一下主机名为你的域名。
用户将通过你设置的 IP 或域名访问应用系统。

5.2 书写 URL 重写规则

在 IIS 管理器 => Switch 站点 => URL 重写 里,可进行重写规则的配置,配置将存到站点根目录下的 web.config 文件里。
以下是所需要的完整的重写规则,你可直接拷贝到 Switch 站点的 web.config 里使用。其中的蓝绿站点端口你按实际情况修改。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules useOriginalURLEncoding="false">
        <clear />
        <!-- 蓝站点 https 规则  -->
        <rule name="RouteToBlueHttps" enabled="true" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
            <add input="{HTTPS}" pattern="on" ignoreCase="true" />
          </conditions>
          <serverVariables>
            <set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
            <set name="HTTP_X_FORWARDED_PROTO" value="https" />
            <set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
          </serverVariables>
          <action type="Rewrite" url="http://127.0.0.1:5001{UNENCODED_URL}" appendQueryString="false" />
        </rule>
        <!-- 蓝站点 http 规则  -->
        <rule name="RouteToBlueHttp" enabled="true" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
            <add input="{HTTPS}" pattern="off" ignoreCase="true" />
          </conditions>
          <serverVariables>
            <set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
            <set name="HTTP_X_FORWARDED_PROTO" value="http" />
            <set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
          </serverVariables>
          <action type="Rewrite" url="http://127.0.0.1:5001{UNENCODED_URL}" appendQueryString="false" />
        </rule>
        <!-- 绿站点 https 规则  -->
        <rule name="RouteToGreenHttps" enabled="false" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
            <add input="{HTTPS}" pattern="on" ignoreCase="true" />
          </conditions>
          <serverVariables>
            <set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
            <set name="HTTP_X_FORWARDED_PROTO" value="https" />
            <set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
          </serverVariables>
          <action type="Rewrite" url="http://127.0.0.1:5002{UNENCODED_URL}" appendQueryString="false" />
        </rule>
        <!-- 绿站点 http 规则  -->
        <rule name="RouteToGreenHttp" enabled="false" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
            <add input="{HTTPS}" pattern="off" ignoreCase="true" />
          </conditions>
          <serverVariables>
            <set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
            <set name="HTTP_X_FORWARDED_PROTO" value="http" />
            <set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
          </serverVariables>
          <action type="Rewrite" url="http://127.0.0.1:5002{UNENCODED_URL}" appendQueryString="false" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

5.3 添加允许的服务器变量

规则里写的转发相关服务器变量需要添加进来才能正常使用。
打开 IIS 管理器,选中 Switch 站点,在右侧功能列表里找到 "URL 重写"

URLRewrite

双击打开 "URL 重写",然后点击右侧的 "查看服务器变量"

Role1

在该界面将 "HTTP_X_FORWARDED_HOST""HTTP_X_FORWARDED_PROTO""HTTP_X_FORWARDED_FOR" 添加进来:

Role2

5.4 规则的简要解释

  • 最关键的要求是:用户在浏览器里输入的 URL,能够完整的、不被做任何改动的转给蓝绿站点里的 App
    这个费了点周折,比如 URL:"/TestPage/aa%2Fbb",本意是请求 "/TestPage" 页面,路由参数为 "aa/bb",因为该参数里有斜杠,因此用编码后的 "aa%2Fbb" 传递。但测试时发现转给 App 的请求是 "/TestPage/aa/bb",造成 404。
    最终在 这里 找到了答案:使用 {UNENCODED_URL} 并设置 useOriginalURLEncodingfalse

  • 里边的服务器变量设置用来确保传递正确的 host、scheme 以及客户端 IP 地址给 App
    比如 App 里拿到的 host 将是 "192.168.0.116:9080" 而不是 "127.0.0.1:5001""127.0.0.1:5002"
    要与代码配合实现,见后边章节。

  • 为何给蓝绿分别设置了两条规则
    因为 URL 重写没法自适应 http/https,只有个 {HTTPS} 变量(值为 "on"/"off"),为了让 http、https 均能正常访问,只能各自写两条规则。
    如果你的应用只需要 http/https 中的一种访问,可以删掉不需要的规则。

  • 蓝绿的切换
    蓝绿的切换就是对应规则的启用与停用,哪个站点规则启用(enabled="true"),就导向哪个站点。不能同时都启用。
    不能在 IIS 里手动去启用、禁用规则,这会造成访问中断,而是要通过代码去实现,见后边章节。


6 应用调整

应用也需要加入一些初始化代码,以及做出一些相应的调整才能适应蓝绿部署环境。

6.1 应用初始化中的两项必要配置

  • 配置数据保护(Data Protection)以共享密钥
    必须使用 AddDataProtection() 指定蓝绿站点使用同一套密钥存储,不然会出现 Cookie 无法识别等问题。
    比如用共享文件夹:
builder.Services.AddDataProtection()
    .SetApplicationName("myApp")
    // 应用上一级目录的 myAppKeys 目录下
    .PersistKeysToFileSystem(new DirectoryInfo($"{AppContext.BaseDirectory}../myAppKeys"));

或存于 Redis:

var redis = ConnectionMultiplexer.Connect("<URI>");
builder.Services.AddDataProtection()
    .SetApplicationName("myApp")
    .PersistKeysToStackExchangeRedis(redis, "DataProtection-Keys");
  • 配置转发头中间件(Forwarded Headers)
    必须使用 UseForwardedHeaders() 配置转发头中间件以确保 App 够获取真实的 host、scheme 以及客户端 IP 地址,比如 App 里拿到的 host 将是 "192.168.0.116:9080" 而不是 "127.0.0.1:5001""127.0.0.1:5002",拿到的 scheme 则是实际的 scheme(http/https)。
// 在 builder.Build() 后立即调用:
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
});

6.2 其它事项

  • 识别自己所在环境
    蓝绿部署环境下,应用通常需要知道自己运行在 Blue 还是 Green 里,比如日志要增加 ServerNode 项,以便记录在哪个登录、在哪个执行的操作等等。
    至于如何识别自己所在的环境,可直接根据应用自己所在的目录名称来判断:
private static string _whoami()
{
    string dirName = new DirectoryInfo(AppContext.BaseDirectory).Name;
    bool isBlue = dirName.Contains("blue", StringComparison.OrdinalIgnoreCase);
    bool isGreen = dirName.Contains("green", StringComparison.OrdinalIgnoreCase);
    if (isBlue && isGreen)
        return "Ambiguous";
    if ((!isBlue) && (!isGreen))
        return "Unknown";
    return isBlue ? "Blue" : "Green";
});
  • 配置共享
    把配置文件放到一个共享目录下,比如应用上一级目录下的 configs 目录。
    就是说应用目录下不要有发布后需要更改的配置文件,这样更新时就可放心地删除旧版、拷贝新版了,不然一旦疏忽会造成混乱或异常。
    以下代码将使应用使用其上一级目录下的 configs 目录下的 appConfig.json 配置文件,供你参考:
string appConfigFile = $"{AppContext.BaseDirectory}../configs/appConfig.json";
string appConfigFileEnv = $"{AppContext.BaseDirectory}../configs/appConfig.{builder.Environment.EnvironmentName}.json";
builder.Configuration.AddJsonFile(appConfigFile, false, true);
builder.Configuration.AddJsonFile(appConfigFileEnv, true, true);

如果蓝绿下的 App 需要不同的配置:在共享配置文件里书写两套配置,App 里则用一套代码就可让蓝绿各自读取自己的:
配置:

{
  "MyApp_Blue": {
    "Foo": "abc",
  },
  "MyApp_Green": {
    "Foo": "def",
  },
}

读取:

// 参见前边的 _whoami()
string foo= builder.Configuration[$"MyApp_{_whoami()}:Foo"];
  • 分布式缓存
    如果有需要共享的缓存,则需要改用分布式缓存。比如用到了 Session。

  • 文件上传
    若有附件上传,则同样要使用同一个共享目录。

  • 后台任务/定时任务
    若有后台任务或定时任务,蓝绿将都在执行。
    如果任务允许蓝绿同时运行,或允许一前一后运行,或者不允许同时运行但可中断,就没什么问题。否则需要将任务独立出来,并独立运行(比如用 Windows Service)。

  • 向后兼容
    应用需要考虑向后兼容性。
    如果新版本使用了与旧版不兼容的会话结构、加密格式、字段结构等等,就无法进行平滑切换,因此需要考虑向后的兼容性,比如新增的字段要确保允许 NULL 或设有默认值等。
    如果确实无法兼容,就只能短时中断访问了,根据实际情况可采取提前通知、低峰操作等方式升级。


7 蓝绿如何切换

蓝绿的切换过程实际上就是启用/禁用 Switch 站点里的对应规则。
但是不能在 IIS 里手动去启用、禁用规则,这会造成访问中断,而是通过用脚本或代码修改 Switch 站点里的 web.config 文件来进行切换:修改对应规则的 enabledtrue/false,比如用 PowerShell 脚本。

我是在应用里设计了一个只有超级管理员用户访问的页面,在其中进行切换操作。
以下是用来获取当前启用的环境以及进行蓝绿切换的 C# 方法,你可直接使用。

/// <summary>
/// 获取当前 web.config 里启用的环境。
/// </summary>
/// <param name="webConfigPath">Switch 站点的 web.config 文件完整路径。</param>
/// <returns></returns>
private static string _getCurrentEnvironmentInConfiguration(string webConfigPath)
{
    if (!System.IO.File.Exists(webConfigPath))
        throw new FileNotFoundException("未找到 Switch 站点的 web.config 文件。", webConfigPath);

    XDocument doc = XDocument.Load(webConfigPath);

    // 所有 Blue 规则节点
    var blueRules = doc
        .Descendants("rule")
        .Where(r => ((string)r.Attribute("name")).StartsWith("RouteToBlue", StringComparison.OrdinalIgnoreCase))
        .ToList();

    // 所有 Green 规则节点
    var greenRules = doc
        .Descendants("rule")
        .Where(r => ((string)r.Attribute("name")).StartsWith("RouteToGreen", StringComparison.OrdinalIgnoreCase))
        .ToList();

    if (blueRules.Count == 0 || greenRules.Count == 0)
        throw new InvalidOperationException("未找到 RouteToBlue 或 RouteToGreen 规则,请检查 web.config。");

    bool blueEnabled = blueRules.All(r => (string)r.Attribute("enabled") != "false");
    bool greenEnabled = greenRules.All(r => (string)r.Attribute("enabled") != "false");

    if (blueEnabled && greenEnabled)
        return "All";
    if ((!blueEnabled) && (!greenEnabled))
        return "NoneOrAmbiguous";
    return blueEnabled ? "Blue" : "Green";
}

/// <summary>
/// 切换蓝绿环境。
/// </summary>
/// <param name="webConfigPath">Switch 站点的 web.config 文件完整路径。</param>
/// <returns>返回已启用的环境。</returns>
private static string _toggleEnvironment(string webConfigPath)
{
    if (!System.IO.File.Exists(webConfigPath))
        throw new FileNotFoundException("未找到 Switch 站点的 web.config 文件。", webConfigPath);

    XDocument doc = XDocument.Load(webConfigPath);

    // 所有 Blue 规则节点
    var blueRules = doc
        .Descendants("rule")
        .Where(r => ((string)r.Attribute("name")).StartsWith("RouteToBlue", StringComparison.OrdinalIgnoreCase))
        .ToList();

    // 所有 Green 规则节点
    var greenRules = doc
        .Descendants("rule")
        .Where(r => ((string)r.Attribute("name")).StartsWith("RouteToGreen", StringComparison.OrdinalIgnoreCase))
        .ToList();

    if (blueRules.Count == 0 || greenRules.Count == 0)
        throw new InvalidOperationException("未找到 RouteToBlue 或 RouteToGreen 规则,请检查 web.config。");

    bool blueEnabled = blueRules.All(r => (string)r.Attribute("enabled") != "false");
    bool greenEnabled = greenRules.All(r => (string)r.Attribute("enabled") != "false");

    string targetEnv;
    if (blueEnabled && !greenEnabled)
    {
        // 当前是蓝 → 切换到绿
        foreach (var r in blueRules)
            r.SetAttributeValue("enabled", "false");
        foreach (var r in greenRules)
            r.SetAttributeValue("enabled", "true");
        targetEnv = "Green";
    }
    else if (greenEnabled && !blueEnabled)
    {
        // 当前是绿 → 切换到蓝
        foreach (var r in blueRules)
            r.SetAttributeValue("enabled", "true");
        foreach (var r in greenRules)
            r.SetAttributeValue("enabled", "false");
        targetEnv = "Blue";
    }
    else
    {
        // 若都已启用、都已停用或状态混杂,则切换到蓝
        foreach (var r in blueRules)
            r.SetAttributeValue("enabled", "true");
        foreach (var r in greenRules)
            r.SetAttributeValue("enabled", "false");
        targetEnv = "Blue";
    }

    // UTF-8 编码保存,并确保不写入 BOM,以防止 IIS 读取出错
    using (var writer = new StreamWriter(webConfigPath, false, new System.Text.UTF8Encoding(false)))
    {
        doc.Save(writer);
    }

    return targetEnv;
}

8 切换测试

用 k6 分别对 Windows Server 2012 R2 + IIS8.5 和 Win11 + IIS10 下的蓝绿部署进行了切换测试,尚未出现访问中断的情况。


作者:木南W

出处:https://www.cnblogs.com/munanwang/p/19234857

转载请注明作者并在页面明显位置给出原文链接。

posted @ 2025-11-20 10:28  木南W  阅读(114)  评论(6)    收藏  举报