实践笔记: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 里创建 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"

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

然后按照下图配置:

为何取消选中 "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 复制代码
<?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 重写"

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

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

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 无法识别等问题。
    比如用共享文件夹:
csharp 复制代码
builder.Services.AddDataProtection()
    .SetApplicationName("myApp")
    // 应用上一级目录的 myAppKeys 目录下
    .PersistKeysToFileSystem(new DirectoryInfo($"{AppContext.BaseDirectory}../myAppKeys"));

或存于 Redis:

csharp 复制代码
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)。
csharp 复制代码
// 在 builder.Build() 后立即调用:
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
});

6.2 其它事项

  • 识别自己所在环境
    蓝绿部署环境下,应用通常需要知道自己运行在 Blue 还是 Green 里,比如日志要增加 ServerNode 项,以便记录在哪个登录、在哪个执行的操作等等。
    至于如何识别自己所在的环境,可直接根据应用自己所在的目录名称来判断:
csharp 复制代码
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 配置文件,供你参考:
csharp 复制代码
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 里则用一套代码就可让蓝绿各自读取自己的:

配置:

json 复制代码
{
  "MyApp_Blue": {
    "Foo": "abc",
  },
  "MyApp_Green": {
    "Foo": "def",
  },
}

读取:

csharp 复制代码
// 参见前边的 _whoami()
string foo= builder.Configuration[$"MyApp_{_whoami()}:Foo"];
  • 分布式缓存

    如果有需要共享的缓存,则需要改用分布式缓存。比如用到了 Session。

  • 文件上传

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

  • 后台任务/定时任务

    若有后台任务或定时任务,蓝绿将都在执行。

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

  • 向后兼容

    应用需要考虑向后兼容性。

    如果新版本使用了与旧版不兼容的会话结构、加密格式、字段结构等等,就无法进行平滑切换,因此需要考虑向后的兼容性,比如新增的字段要确保允许 NULL 或设有默认值等。

    如果确实无法兼容,就只能短时中断访问了,根据实际情况可采取提前通知、低峰操作等方式升级。


7 蓝绿如何切换

蓝绿的切换过程实际上就是启用/禁用 Switch 站点里的对应规则。

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

我是在应用里设计了一个只有超级管理员用户访问的页面,在其中进行切换操作。

以下是用来获取当前启用的环境以及进行蓝绿切换的 C# 方法,你可直接使用。

csharp 复制代码
/// <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

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

相关推荐
ManageEngine卓豪7 天前
如何在IIS中配置HTTP重定向
http·iis·http重定向
准时准点睡觉2 个月前
HTTP 错误 403.14 - Forbidden Web 服务器被配置为不列出此目录的内容——错误代码:0x00000000
运维·服务器·iis·asp.net
是萝卜干呀2 个月前
IIS 部署 asp.net core 项目时,出现500.19、500.31问题的解决方案
后端·iis·asp.net·hosting bundle
haonuy*4 个月前
文件解析漏洞 iis apache nginx中间件相关漏洞详解
nginx·iis·apache·文件解析漏洞
mytudousi5 个月前
.Net 9.0环境下WebApi发布到IIS
iis·.net web api
专注VB编程开发20年6 个月前
在 ASP.NET 中,HTTP 处理程序(HttpHandler)是处理 HTTP 请求的核心组件
iis·asp.net·asp.net core
w23617346016 个月前
IIS入门指南:原理、部署与实战
架构·iis
csdn_aspnet7 个月前
Windows .NET Core 应用程序部署到 IIS 解决首次访问加载慢的问题
iis·.netcore
csdn_aspnet7 个月前
Windows Server .NET Core 应用程序部署到 IIS 解决首次访问加载慢的问题
windows·iis·.netcore