65.9K
CodeProject 正在变化。 阅读更多。
Home

部署器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (16投票s)

2008年4月1日

Apache

14分钟阅读

viewsIcon

86050

downloadIcon

563

自动化部署 Windows 服务、ClickOnce 和其他 .NET 应用程序。

引言

当今软件工程中有许多事情让我感到震惊。即使你只是稍微了解大多数软件开发项目,严重的缺陷也会显而易见。团队经常每天重复一些晦涩的仪式(例如将数据库从开发环境备份到测试环境)来保持开发工作的进行……最令人惊讶的是,当你与开发人员交谈时,许多人会告诉你他们知道问题所在,但没有时间或意愿去创建一些可以解决这些问题的东西。每次遇到这种情况,我都会对自己说:

工欲善其事,必先利其器。

这句谚语在软件工程中的应用再恰当不过了。因为,我们的工具(不像铁匠的工具)不需要任何实体材料来制作。或者正如 Frederick P. Brooks 在他传奇的《人月神话》中所说的那样

程序员,如同诗人,只是稍微远离纯粹的思维。他们凭空创造,用想象力构建他们的空中楼阁。

缺乏知识或主动性的开发人员将屈服于软件项目的流程,并通过手动执行日常重复性任务。而优秀的开发人员则会识别出某些模式,并运用他们的技能来构建自动化这些模式的工具。我甚至会说,能够 pinpoint 他们作为思维工具的不足之处,是区分那些碰巧成为程序员的人和那些应该成为程序员的人的关键。因为,在连续一周每天执行枯燥的任务后,任何真正的程序员都会问自己:“我为什么要这样做?电脑不能帮我做吗?”(如果你第二个问题是“能不能让别人替我做?”,那你就是一个管理型人才)。

我们这个职业的另一个巨大优势是,我们可以轻松获得工具。像 CodeProject 这样的网站之所以伟大,就是因为它们赋予了我们给予和获取的能力。它们使我们能够将自己用作编码工具,只做别人尚未实现的事情。通过分享我的代码,我希望能够节省别人花费在制作我可能觉得有用的工具上的时间。

目录

问题

我刚完成了一个项目,该项目的测试和生产地点距离开发人员所在地 400 公里。最初,每次代码更改都需要手动复制构建的 DLL。开发人员会连接到 FTP 服务器并上传文件。然后,他们会使用远程桌面连接到一台机器(在大多数情况下,使用另一个远程桌面作为代理),并根据部署类型执行一系列适当的操作。如果他正在部署 Windows 服务,他将转到“管理工具”->“服务”,并在部署期间停止/启动该服务。如果他正在部署 ClickOnce 应用程序,他需要将文件放置到适当的位置后重新签名清单。并且,在任何情况下,他都需要手动更新配置文件,合并缺失的节或更新需要更改的节。由于许可限制(每台机器只能有两个远程桌面连接),开发人员经常不得不等待同事完成。

我想任何部署到远程机器的人都经历过类似的情况。上面描述的过程的问题在于它是由人驱动的——需要持续的关注和思考。在被迫重复几次上述过程并感到厌倦后,我开始思考如何优化整个过程。

解决方案

很明显,存在几种部署类型。有时,仅仅复制文件就足够了;有时,必须停止服务;有时,需要签名清单。在每种情况下,流程如下:

  1. 上传文件到服务器
  2. 执行特定的预处理任务
  3. 将文件复制到目标位置
  4. 修改配置文件
  5. 执行特定的后处理任务

每次部署的特定需求需要某种形式的元数据来解释为了实现成功部署需要做什么。因此,我决定创建一个基于 XML 的规范,并将其与应用程序文件一起打包到 zip 文件中。创建 zip 文件后,我将使用 FTP 将其上传到服务器,然后将所有事情交给 Windows 服务(我开发了这个服务来理解规范并命名为 Deployer)来为我处理。

XML 定义

这是一个示例清单,说明了在名为 *tyrion* 和 *otherTestMachine* 的机器上的部署过程。

<?xml version="1.0" encoding="utf-8" ?>
<deployer>
  <deployInfo type="xcopy" machineName="tyrion" lookFurther="false">
    <targetPath clear="false" backup="false">c:\deployer-xcopy</targetPath>
    <configReplaces>
      <entry>
        <find>Blah</find>
        <replace>Eh1</replace>
      </entry>
    </configReplaces>
    <configReplaces searchExpression="*.xml">
      <entry>
        <find>Change Xml Entry</find>
        <replace>New value</replace>
      </entry>
    </configReplaces>
  </deployInfo> 

  <deployInfo type="xcopy" machineName="otherTestMachine">
    <targetPath>d:\deployer-xcopy</targetPath>
  </deployInfo> 
</deployer>

在 XML 中良好命名的元素能够自文档化;我希望我成功地选择了正确的名称,以便您能够理解大部分规范。为了确保这一点,这里有一个简要的解释——当一个包含应用程序 DLL 和此 XML 的 zip 文件到达服务器时,我的 Windows 服务会将所有内容解压缩到一个临时文件夹,并搜索 *deployer.xml*(规范的标准名称)。

找到文件后,它将被加载到内存中,并搜索 `deployerInfo` 元素,该元素要么包含运行服务的机器的机器名,要么不包含任何机器名(`` 将执行指定的部署,无论机器名称是什么)。

找到合适的 `deployerInfo` 标签后,将评估部署类型。我们的示例中有一个 XCopy 类型的部署,因此服务只会将文件从 zip 复制到 *c:\deployer-xcopy* 文件夹。由于 `clear` 和 `backup` 属性设置为 false,服务不会清除或备份目标文件夹。

最后,将搜索 *c:\deployer-xcopy* 目录下的 *.config* 文件(如果未指定则为默认搜索表达式),在其中将所有 *Blah* 的出现替换为 *Eh1*。对 *.xml* 文件(` `)也将执行相同的操作,其中 *Change Xml Entry* 将被替换为 *New value*。

Deployer 最强大的地方在于它可以轻松地将一个应用程序部署到不同的机器/环境。如果您将带有此 XML 的 zip 文件部署到 *otherTestMachine*,它只会将内容复制到 *d:\deployer-xcopy*,而不会触及 *.config* 和 *.xml* 文件(与 *tyrion* 部署相反)。

使用 `lookFuther` 属性,您可以轻松地调整一个包的多次部署。默认情况下,此属性为 true——如果您指定两个 `` 元素,Deployer 将执行双重部署,这对于拥有服务器集群的情况非常有用。在下面的 *deployer.xml* 示例中,名为 *cluster* 的机器上的 Deployer 服务将执行三次 XCopy 部署,分别部署到 *server1*、*server2* 和 *server3*;由于 `lookFurther` 属性为 false,因此不会部署到 *server4*。

<?xml version="1.0" encoding="utf-8" ?>
<deployer>
  <deployInfo type="xcopy" machineName="cluster">
    <targetPath>\\server1\deployer-xcopy</targetPath>
  </deployInfo> 

  <deployInfo type="xcopy" machineName="cluster">
    <targetPath>\\server2\deployer-xcopy</targetPath>
  </deployInfo> 

  <deployInfo type="xcopy" machineName="cluster" lookFurther="false">
    <targetPath>\\server3\deployer-xcopy</targetPath>
  </deployInfo> 

  <deployInfo type="xcopy" machineName="cluster">
    <targetPath>\\server4\deployer-xcopy</targetPath>
  </deployInfo> 
</deployer>

部署类型

除了已经展示的 XCopy 部署类型外,我还开发了另外三种:Service、ClickOnce 和 DatabaseScript。您可以在接下来的图片中看到继承树。

Figure 1 – Deployment types

图 1 – 部署类型

基本上,Service 部署是 XCopy,在适当的时候停止/启动 Windows 服务;ClickOnce 是 XCopy,在文件就位后签名清单;DatabaseScript 直接继承自抽象的 BaseDeployType,因为它不需要复制文件,它只需要连接到数据库并执行用户打包在 zip 文件中的脚本。

让我们来看看每种部署类型的具体细节。

Service

这是一个示例服务部署定义规范。

<?xml version="1.0" encoding="utf-8" ?>
<deployer>
  <deployInfo type="service" machineName="test9">
    <targetPath clear="false" backup="true">c:\deployed</targetPath>
    <backupInfo>
      <exclude path="~\someFiles" />
    </backupInfo>
    <serviceMachine>test9</serviceMachine>
    <serviceName>SomeUniqueName</serviceName>
  </deployInfo> 
</deployer>

缺失的 `configReplaces` 部分并不意味着您不能在这里使用它。我只是想通过这个例子强调它是可选的(就像我们将在接下来的示例中看到的一些其他部分一样)。

`ServiceMachine` 是一个可选元素,用于指定服务所在机器的名称(默认值为 .,表示本地机器)。这使得在远程机器上部署成为可能,前提是您以域管理员帐户运行 Deployer 服务(有关更多信息,请参阅本文末尾)。`ServiceName` 是注册服务的唯一字符串。它将用于在机器的服务集合中查找服务以控制它。

通过使用 `backupInfo` 元素,您可以排除某些文件夹/文件不进行备份。当您的应用程序自动生成包含临时文件的文件夹,而这些文件在应用程序功能方面并不需要时,这一点尤其有用。`backupInfo` 元素适用于所有 XCopy 部署。

ClickOnce

在我之前的部署文章中收到的关于 ClickOnce 的大部分问题都与 ClickOnce 相关。这并非没有原因——微软的开发团队使得默认情况下使用 ClickOnce 变得非常容易。但是,如果您想更改任何内容……那么,祝您好运:开始翻阅写得糟糕的 MSDN 页面,希望您不会碰到死胡同。

两个最常重复的问题是:

  • 我能否将我的 ClickOnce 应用程序部署到自定义路径(*c:\Program Files\MyApp*)?
  • 如何在不使清单值和签名失效的情况下更改配置文件或将安装文件夹移动到新路径?

不幸的是,我无法帮助您解决第一个问题,因为 ClickOnce 会强制部署到 *c:\Documents and Settings\%User%\...*。但是,Deployer 可以轻松地回答第二个问题,因为它自动化了签名清单的过程。

<?xml version="1.0" encoding="utf-8" ?>
<deployer>
  <deployInfo type="clickOnce" machineName="tyrion">
    <targetPath>c:\deployer-clickOnce</targetPath>
    <usePackedKey keyPassword="123">true</usePackedKey>
    <providerUrl>\\tyrion\oblik\</providerUrl>
    <configReplaces>
      <entry>
        <find>Šmeker</find>
        <replace>Milorad Cavic</replace>
      </entry>
    </configReplaces>
    <dllCache>
      <dll name="stdole.dll" copyTo="{manifestDirectory}" />
      <dll name="Interop.VSFlex7L.dll" copyTo="{manifestDirectory}" />
    </dllCache>
  </deployInfo>
</deployer>

除了已经看到的 `targetPath` 和 `configReplaces` 元素外,第一个新元素是 `usePackedKey`。它允许您使用您与 *deployer.xml* 和应用程序文件一起打包到 zip 中的 PFX 密钥来签名清单。如果您省略此元素,Deployer 将使用随源代码一起提供的默认密钥(Deployer 项目中的 *clickOnceKey.pfx*,该密钥将于 2040 年到期)签名清单。

`ProviderUrl` 指定您的 ClickOnce 安装在部署后可见的 URL,这为您提供了将 ClickOnce 安装轻松移动到不同测试环境的机会。

最后,`dllCache` 有助于减小 zip 压缩包的大小。Deployer 有一个特殊的 *dllcache* 文件夹,您可以在其中放置所有重用的 DLL,然后这些 DLL 可以在 *deployer.xml* 中进行引用……这样,您就不需要在每次上传新版本应用程序时都将它们包含在 zip 文件中——Deployer 将在部署过程中从缓存中复制 DLL。

`copyTo` 属性中的特殊值会在运行时进行评估和替换。目前支持两个:

  • `{manifestDirectory}` – 将被替换为包含应用程序清单(`clickOnceApp.manifest`)的文件夹路径。仅支持 ClickOnce 部署。
  • `{targetDirectory}` – 将被替换为目标文件夹(在 `targetPath` 中指定的文件夹)的路径。支持所有 XCopy 部署。

当然,`dllCache` 适用于所有 XCopy 部署。

DatabaseScript

当您准备好 SQL 脚本时,连接到远程桌面并在 SQL Management Studio 中执行它并没有多大麻烦,但仍然最好能够通过 FTP 完成所有事情。特别是,如果您创建了部署包,可以一次性设置好整个 Windows 服务(创建数据库、安装服务并启动它)。

<?xml version="1.0" encoding="utf-8" ?>
<deployer>
  <deployInfo type="databaseScript" machineName="psimr">
    <connectionString>Data Source=psimr;Initial Catalog=
          PsiClient;Integrated Security=True</connectionString>
    <isolationLevel>ReadCommitted</isolationLevel>
  </deployInfo>
</deployer>

在 *deployer.xml* 中,您只需指定一个有效的 `connectionString`;`isolationLevel` 是可选的(默认为 `ReadCommitted`),并且接受 `System.Data.IsolationLevel` 枚举的任何有效项。

设置 Deployer

如果您想在本地计算机上测试 Deployer,请下载源代码,重新编译所有内容,然后按 F5 启动调试模式。然后,创建一个 zip 文件,其中包含例如 *TestApp/bin/Debug* 中的文件,并将其复制到 Deployer 的 drop 路径(默认为 *c:\!deployer\drop*)。

处理完 zip 文件后,Deployer 将在 *logs* 目录(默认为 *c:\!deployer\logs*)中创建一个日志文件,该文件会告诉您部署过程如何。

当您想在远程计算机上设置 Deployer 时,请使用 Release 配置构建 Windows 服务。完成编译后,将 *bin/Release* 目录中的所有内容上传到远程计算机,并在命令提示符下键入以下命令来安装服务(就像安装任何 Windows 服务一样):

set path=c:\windows\Microsoft.NET\Framework\v2.0.50727
installutil -i %pathToYourServiceEXE%

(卸载时,您将指定 `-u` 而不是 `-i` 标志)。

现在,您需要做的是设置到您的 drop 和 logs 路径的 FTP 访问。一篇关于在 Windows Server 上设置 FTP 服务的优秀文章可以在这里找到

编写您自己的 deployer.xml

下载源代码后,在 Deployer 项目中查找 *deployer-full.xml* 和 *deployer-full.xsd* 文件。XML 文件包含每种部署类型的示例,您可以轻松地通过复制粘贴您需要的块来创建自己的 *deployer.xml*。

除了能够验证 Deployer XML 外,*deployer-full.xsd* 还可以为 Visual Studio 提供智能感知。我准备了一个包,您需要将其解压缩到您的 Visual Studio 的 *xml\schemas* 路径——对于 VS2005,默认路径是 *%ProgramFiles%\Microsoft Visual Studio 8\Xml\Schemas\*;对于 VS2008,则是 *%ProgramFiles%\Microsoft Visual Studio 9.0\Xml\Schemas\*。完成此操作后,只需重新启动 IDE,下次编辑 XML 文件时,您就会获得漂亮的智能感知,如下图所示:

Figure 2 - Intellisense when typing Deployer manifests

图 2 - 键入 Deployer 清单时的智能感知

如果您对 XSD 智能感知在 Visual Studio 中的工作原理感兴趣,请查看此链接

安全注意事项

我以域管理员帐户运行 Deployer,这使我能够轻松访问网络上的所有必需资源,从而保持 Deployer XML 清洁,无需访问密码。当然,这会打开一个大的安全漏洞,如果有人获得了登录 FTP 的凭据。我建议使用 FTP over SSL,以防止以明文格式发送凭据。

一个稍微更好的选择是使用特定的域帐户运行 Deployer,并根据需要授予对资源的访问权限。我之所以没有使用这个选项,仅仅是因为管理上的过度。

最后,如果您不使用 databaseScript 和 service 部署,您可以以某种低权限用户帐户运行 Deployer。如果您需要 databaseScript 部署,请在连接字符串中放入用户 ID 和密码,但不要共享您的 *deployer.xml* 文件,并限制对 Deployer 的存档和备份文件夹的访问。这样,您就只会缺少需要连接到远程机器才能执行的服务部署。

以上选项均不能完全解决安全问题。这些段落的重点是让您思考如何使用 Deployer——您必须了解相关的风险。我的 Deployer 站点的 FTP 凭据实际上是我在远程系统上的 Windows 帐户的凭据……所以,如果有人窃取了这些凭据,他将使用远程桌面来操纵系统;他不会创建 Deployer 包来执行他想要的操作(除非他是一个喜欢玩自动化的黑客!)。

致谢

我要感谢我的同事 Aleksandar Mirilovic,他通过编写各种服务的 Deployer XMLs 来测试 Deployer 的实际应用,并提供了许多好主意。*Mirilo, Mirilo... šta bi tebe smirilo...*

特别感谢 Vladimir Ilic(*Vlajko, Vlajko... kuku majko...*),我向他承诺,一旦我找到一个好的 C# RAR 库/包装器,我将尽快实现对 RAR 文件的支持。

结论

与我喜欢的任何代码一样,我对这个项目有很多可以提高可用性的想法。例如,我的同事 Mirilo 和我之前在一次讨论中设想的 Visual Studio 插件,它可以读取 *deployer.xml*,准备 zip 文件,并通过 FTP 上传,这将使远程部署变得极其简单(您不必离开 VS IDE 窗口)。此外,一种能够注册和配置虚拟目录或网站的 IIS 部署类型也将是一个不错的补充。

不幸的是,由于我缺乏空闲时间(我刚刚收到了《呆伯特漫画》的订阅),我创建了一个CodePlex Deployer 网站,并将源代码交给您处置。当然,如果您只是需要关于某个功能的快速帮助,我随时可用。

一如既往,我希望您能给出好评。如果您发现文章特别有用或特别无用,请花时间发表评论;我将很乐意回复。

历史

  • 2008年4月1日 – 文章的初始版本(发布文章的有趣日期;)
© . All rights reserved.