代码改变世界

天行健,君子以自强不息

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

摘要

软件流水线能把程序员从繁琐的发布工作中解脱出来,但是跑在Windows IIS里的传统Web应用程序,用Docker的方式不是最方便的。本文详细描述如何用Windows的OpenSSH Server来上传网站后,用PowerShell创建和修改IIS的虚拟目录应用程序。

一、自动打包传统ASP.NET Web应用程序

1、Gitlab仓库中.gitlab-ci.yml中的配置

打包会员订单站点:
  stage: publish-web
  tags:
    - runner-windows
  before_script:
    - . $BUILD_WEB_SCRIPT
  script:
    - echo "开始部署Passport……"
    - cd $MSBUILD_PATH
    - Build-Web $MSBUILD_PATH $CI_PROJECT_DIR "/****/*******.Orders.Web/********.Orders.Web.csproj" $YEE_CUSTOMER_ORDERS_WEB_OUTPUT_DIR

说明:

  • before_script下的命令,是注册PowerShell的函数。因为服务器上的脚本是function。
  • scripts的“Build-Web”就是PowerShell的函数名称,接受4个参数。

2、Windows Gitlab Runner中的PowerShell脚本

function Build-Web {
	param (
		[string]$msbuild_dir,
		[string]$ci_project_dir,
		[string]$project_path,
		[string]$web_output_path
	)

$project_file = $ci_project_dir + "\\" + $project_path
if(( $project_path -like "/*") -or ($project_path -like "\\*")){
	$project_file = $ci_project_dir + $project_path
}
echo "项目文件"$project_file
echo "版本号"$version
echo "网站输出路径"$web_output_path

cd $ci_project_dir
COPY-ITEM -PATH 'x:/gitlab-runner/scripts/download.bat' -Destination .
.\download.bat

if ($LASTEXITCODE -ne 0)  {
	throw "下载Directory.Build.props及其相关文件时出错。"
}

dotnet nuget locals http-cache -c

$exePath = $msbuild_dir + "\\msbuild.exe"
if(Test-Path -Path $exePath){
}
else
{
	throw "MSBuild.exe directory not found:" + $msbuild_dir
}
cd $msbuild_dir

.\msbuild -restore $project_file /v:m
 if ($LASTEXITCODE -ne 0) {
	throw "还原项目时出错:" + $project_file
}

cd X:\gitlab-runner\tools\binding-redirect-helper
.\BindingRedirect check --project:$project_file --save:true
if ($LASTEXITCODE -ne 0) {
	throw "自动更新web.config中的绑定重定向时时出错:" + $project_file
}
cd $msbuild_dir

.\msbuild $project_file /t:ResolveReferences /t:Compile /p:configuration="Release" /t:_CopyWebApplication /p:WebProjectOutputDir=$web_output_path /p:OutputPath=$web_output_path"\bin" /v:m

Exit $LASTEXITCODE
}

说明:

  • download.bat是配合Directory.Build.props技术,下载一堆相关文件。下载后清空NuGet的Http缓存;
  • msbuild.exe先还原,再生成,分成了两步;
  • /v:m是msbuild命令执行时给出最少的日志。需要调试的时候,用/v:diag;
  • /t:_CopyWebApplication,需要在Web项目的.csproj文件中Import这个目标,在前文有详细说明;
  • WebProjectOutputDir和OutputPath两个输出目录都要配置,一个管.aspx、.ascx和.css等资源文件,一个管dll。

4个参数的说明:

  • $msbuild_dir msbuild.exe程序所在目录的路径;
  • $ci_project_dir 这个地址是Gitlab内置的函数给的,就是Gitlab Runner从Gitlab服务器签出代码后的工作目录;
  • $project_path,项目的.csproj文件,相对于仓库根目录的路径;
  • $web_output_path的路径,一定要在解决方案之外。因为下一个负责上传的stage是拿不到这个stage执行时候目录下的文件的。

二、自动上传打包的网站到Web服务器

这里很复杂,需要几个准备工作

  • Web服务器安装Open SSH Server;
  • Web服务器配置好管理员账户的SSH公钥,并允许使用秘钥而不是密码来登入;

1、Gitlab仓库根目录.gitlab-ci.yml中的配置

上传会员订单网站到开发站:
  stage: upload-web
  tags:
    - runner-windows
  script:
    - cd ${GITLAB_RUNNER_POWERSHELL_DIR}
    - |
      powershell.exe .\upload-web.ps1 `
        -ssh_rsa_file $SSH_RSA_FILE `
        -remote_username $DEV_TEST_SERVER_LOGIN_NAME `
        -remote_server $DEV_TEST_SERVER `
        -remote_root_dir_name loda `
        -remote_site_name $REMOTE_WINDOWS_SITE_NAME `
        -remote_windows_web_app_pool $REMOTE_WINDOWS_WEB_APP_POOL `
        -web_app_name orders `
        -web_output_path $YEE_CUSTOMER_ORDERS_WEB_OUTPUT_DIR `
        -version_id $YEE_ORDERCENTER_VERSION_ID

说明:

  • 上述命令是多行,换了行的。参数只能是美元符号开头直接跟参数名称,如果$YEE_ORDERCENTER_VERSION_ID换成${YEE_ORDERCENTER_VERSION_ID}这种大括号包裹参数名称,就会出错;
  • 每一行里不能有双引号。比如倒数第3行,orders不能写成“orders”

参数说明:

  • $SSH_RSA_FILE 用于SSH登入Web服务器的私钥的完整路径,这个私钥是放在Gitlab Runner服务器的。私钥所在的文件夹的权限要特殊配置:只有运行Gitlab Runner这个Windows服务器的账户有只读权限。否则SSH会报错;
  • $DEV_TEST_SERVER_LOGIN_NAME 用于SSH登入Web服务器的用户名,比如Administrator;
  • $DEV_TEST_SERVER Web服务器的DNS名称或IP地址;
  • remote_root_dir_name 这个参数,是为了Web服务器安全。Web服务器上,我们的网站如果放在“D:\apps",这里传入loda,就是说网站根目录是“D:\apps\loda”;
  • remote_site_name 这个参数,就是IIS中网站的名称,比如:Default Web Site。但是空格在命令从Gitlab Runner服务器传递到Web服务器会造成别的歧义,所以在Web服务器上的网站名字,实际上不能有空格;
  • remote_windows_web_app_pool 这个就是Web服务器上,IIS应用程序池的名字,因为Gitlab CI传参数给Gitlab Runner的PowerShell,又从这个PowerShell执行SSH到Web服务器,参数传递中空格有特别含义,所以这个应用程序池的名字和网站名字一样:不能有空格;
  • web_app_name 这个名字,就是IIS网站根目录下的名字,会出现在URL里,所以不能有小数点不能有空格,只能是英文字母、数字和连接符;
  • web_output_path 网站打包后,放在Gitlab Runner的路径,这个路径是绝对路径,不能实在stage工作目录内部;
  • version_id 我们NuGet的包的版本号都是小数点分隔的,但是作为Web路径的时候,小数点有特别的含义,所以我们的函数收到带小数点的版本号后,会把小数点替换成下划线。

2、Windows Gitlab Runner服务器上被调用的PowerShell脚本

下边的脚本,最值得说的就是sshCmd。
因为这个脚本在Gitlab Runner服务器上,被Gitlab Runner调用以后,要SSH到Web服务器去执行命令。

# 设置环境变量以支持UTF-8编码
param (
	[Parameter(Mandatory=$true)]
	[string]$ssh_rsa_file,
	[Parameter(Mandatory=$true)]
	[string]$web_output_path,
	[Parameter(Mandatory=$true)]
	[string]$remote_username,
	[Parameter(Mandatory=$true)]
	[string]$remote_server,
	[Parameter(Mandatory=$true)]
	[string]$remote_root_dir_name,
	[Parameter(Mandatory=$true)]
	[string]$remote_site_name,
	[Parameter(Mandatory=$true)]
	[string]$remote_windows_web_app_pool,
	[Parameter(Mandatory=$true)]
	[string]$web_app_name,
	[Parameter(Mandatory=$true)]
	[string]$version_id
)
chcp 65001
 
$version_id = $version_id.Replace('.','_') 

# 确保所有参数值中的特殊字符被适当转义,特别是对于包含在路径或字符串中的双引号
$escapedSiteName = $remote_site_name -replace "'", "''"
$escapedPool = $remote_windows_web_app_pool -replace "'", "''"
$escapedAppName = $web_app_name -replace "'", "''"
$escapedVersion = $version_id -replace "'", "''"

$remote_script_path = "c:\for-gitlab-runner\Setup-LodaWebApp.ps1"

# 构建远程脚本调用的参数字符串,保持简单并确保参数间以空格分隔
$remoteParams = "-remote_site_name '$escapedSiteName' -remote_windows_web_app_pool '$escapedPool' -web_app_name '$escapedAppName' -version_id '$escapedVersion'"

# 构建完整的SSH命令,注意引号的正确使用和转义
$sshCmd = "ssh -i `"$ssh_rsa_file`" -o ""StrictHostKeyChecking=no"" -v `"$remote_username@$remote_server`" 'powershell.exe -ExecutionPolicy Bypass -File `"$remote_script_path`" $remoteParams'"
# 注意:$remote_script_path 应替换为实际的远程脚本路径,如"c:\for-gitlab-runner\Setup-LodaWebApp.ps1"

# 打印命令以供调试
Write-Host "Executing Command: $sshCmd"

# 执行SSH命令
Invoke-Expression $sshCmd


if ($LASTEXITCODE -ne 0)  {
	if ($Errors.Count -gt 0) {
    Write-Host "清空远程目录时出错:" $Errors[0].Exception.Message
    Write-Host "错误信息:" $Errors[0].Exception.Message
    Write-Host "堆栈跟踪:" $Errors[0].Exception.StackTrace
}
else {
    Write-Host "没有错误信息记录。"
}
}
$remote_dir = "C:\" + $remote_root_dir_name + "\" + $web_app_name + "\" + $version_id
scp -i $ssh_rsa_file -o StrictHostKeyChecking=no -rv ${web_output_path}"\*" ${remote_username}@${remote_server}:$remote_dir

Exit $LASTEXITCODE

3、Web服务器上,被Gitlab Runner的PowerShell调用的PowerShell脚本

看这个标题就拗口,理解后就很简单,这个脚本的作用是:

  • 为Web程序创建新目录,这个目录的名字,是带了version_id的。因为web程序在IIS里运行的时候,dll文件无法被覆盖,所以要准备新目录;
  • 在网站根目录下,以这个新目录创建应用程序。如果应用程序已经存在,则NEW-WebApplication的最后一个参数-Force,就强制把应用程序物理路径给修改到新目录

下边是脚本内容:

 param(
    [Parameter(Mandatory=$true)]
    [string]$remote_site_name,
    [Parameter(Mandatory=$true)]
    [string]$remote_windows_web_app_pool,
    [Parameter(Mandatory=$true)]
    [string]$web_app_name,
    [Parameter(Mandatory=$true)]
    [string]$version_id
)

chcp 65001

Write-Host "已°?绑㨮定¡§的Ì?参?数ºy及¡ã其?值¦Ì:"
$PSBoundParameters.GetEnumerator() | ForEach-Object {
    Write-Host "$($_.Key): $($_.Value)"
}
$version_id = $version_id.Replace('.','_')

$AppPhysicalPath = "C:\loda\" + ${web_app_name} + "\" + $version_id
New-Item -Path $AppPhysicalPath -ItemType Directory -Force

# 定¡§义°?IIS PowerShell模¡ê块¨¦路¡¤径?,ê?根¨´据Y实º¦Ì际¨º情¨¦况?调Ì¡Â整?
$IISModulePath = "C:\Windows\System32\WindowsPowerShell\v1.0\Modules\WebAdministration\WebAdministration.psd1"
if (!(Test-Path $IISModulePath)) {
    Write-Error "IIS Administration PowerShell module not found at $IISModulePath"
    exit 1
}

Import-Module $IISModulePath

# 确¨¡¤保À¡ê应®|用®?池?存ä?在¨²
if (!(Get-ChildItem IIS:\AppPools | Where-Object { $_.Name -eq $remote_windows_web_app_pool })) {
    Write-Error "Application Pool '$remote_windows_web_app_pool' does not exist."
    exit 1
}

# 确¨¡¤保À¡ê站?点Ì?存ä?在¨²
$sitePath = "IIS:\Sites\$remote_site_name"
if (!(Test-Path $sitePath)) {
    Write-Error "Site '$remote_site_name' does not exist."
    exit 1
}

# 创ä¡ä建¡§应®|用®?程¨¬序¨°
Write-Host "Creating application '$web_app_name' under site '$remote_site_name' with physical path '$AppPhysicalPath'"
NEW-WebApplication -Name $web_app_name -Site $remote_site_name -ApplicationPool $remote_windows_web_app_pool -PhysicalPath $AppPhysicalPath -Force

if($LASTEXITCODE -ne 0)
{
    Write-Error "Failed to create the application."
    exit $LASTEXITCODE
}

# 确¨¡¤认¨?操¨´作Á¡Â成¨¦功|
$appPath = Join-Path $sitePath $web_app_name
if (Test-Path $appPath) {
    Write-Host "Application created successfully at '$appPath'."
} else {
    Write-Error "Failed to create the application."
}

exit 0

参数说明

前两个参数的remote_其实要删除才合理。因为这里已经是目的地了,是别人的remote。

posted on 2024-06-05 09:54  终南山人  阅读(23)  评论(0编辑  收藏  举报