实践笔记: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 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}并设置useOriginalURLEncoding为false。 -
里边的服务器变量设置用来确保传递正确的 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 文件来进行切换:修改对应规则的 enabled 为 true/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
转载请注明作者并在页面明显位置给出原文链接。

浙公网安备 33010602011771号