6

部署自動化 - web.config PowerShell 更新函式庫

 3 years ago
source link: https://blog.darkthread.net/blog/ps-web-conf-lib/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
web.config PowerShell 更新函式庫-黑暗執行緒

有些 IIS 設定要靠改 web.config 完成,有些環境較一致,可以預先寫好覆寫即可,但如果更新的 web.config 有多個且內容不同,最無腦的做法是寫成操作指示請相關人員執行:「打開 web.conf,找到 system.webSesrver/httpProtocol/customHeaders 節點(若沒有請新增),新增一個 <add name="X-Frame-Options" value="NONE" />」。人工作業的最大好處是可以隨機應變,靠簡單描述完成複雜操作,且能處理非預期情境。

但凡是人工作業,就免不了眼花手滑腦抽風的可能,執行品質會因操作者素質(菜鳥 vs 老司機)、身心狀態(加班到快往生或剛被老闆洗完臉)而異;遇上批次大量執行的場合,人工作業效率較差會是另一項問題。先前研究過 用 PowerShell 實現 IIS 安裝、網站設定自動化,現在來試試把「web.config 修改」這項手工活也自動化。

我的設計構想如下:

  1. 為簡化設定語法並保留更大客製彈性,我不打算用 XML-Document-Transform 形式定義異動項目,而是計劃提供一些方便的函式簡化,讓開發人員或 DevOps 工程師用寫程式的方式完成 web.config 修改。(我總覺得寫成一行一行指令還比像咒語般的 XML 直覺好讀多了,也算個人偏好)
  2. 主動保存修改前 web.config 備份以及修改後結果,並側錄執行過程(使用 Start-Transcript),方便稽核與問題追查。
  3. 借用 git diff 產生異動對照表,可經過人工複核再更新,多一層保險機制(但也可以省略)
  4. 由站台名稱及網站應用程式名稱找到 web.config 位置,也支援子目錄 web.config 更新
  5. 以 XPath 指定對象,提供設定 Attribute、附加 Attribute 值、刪除 Element 之快速指令
  6. 允許存取 XmlElement,可直接操作 XML 物件,以滿足更複雜的應用情境

先來看它用起來像什麼樣子,我寫了一個測試,分別修改 Default Web Site 的 ConfLab 網站應用程式 web.config 及其子目錄 SubFolder/web.config。函式庫放在 WebConfLib.ps1,更新腳本先引用它,用 SetConfPath 指定站台及網站應用程式名稱,此時會開一個 BAK-yyyyMMddHHmmss 資料夾備份原有 web.config 及執行 Log。測試涵蓋了在現有 Attribute 值附加內容、更新 Attrubte 值、移除 XmlElement、新增 XmlElement 並設定 Attribute、依現有 Attribute 值決定新值、直接指定 InnerXML 等應用情境,最後呼叫 CommitConfChanges 確認修改內容及更新。

. ..\WebConfLib.ps1

# 測試前置作業
function PrepareTest() {
    $appPath = (Get-WebApplication -Site 'Default Web Site' -Name 'ConfLab').PhysicalPath
    Copy-Item "$appPath\web.sample.config" "$appPath\web.config" -Force
    Copy-Item "$appPath\SubFolder\web.sample.config" "$appPath\SubFolder\web.config" -Force
}
PrepareTest

# 測試一 
SetConfPath 'Default Web Site' 'ConfLab'

# 在現有 Attribute 值附加內容
AppendXmlElementAttrs 'appSettings/add[@key="ClientIps"]' @{ value = ";192.168.1.1" }
# 更新 Attrubte 值
SetXmlElementAttrs 'appSettings/add[@key="FOO"]' @{ value = "BAR" }
# 移除 XmlElement
RemoveXmlElement 'appSettings/add[@key="Garbage"]'
# 新增 XmlElement 並設定 Attribute
SetXmlElementAttrs 'system.webServer/httpProtocol/customHeaders/add[@name="X-Frame-Options"]' @{ value = 'SAMEORIGIN' }
# 新增 XmlElement 並設定多項 Attribute
SetXmlElementAttrs 'system.web/httpCookies' @{ requireSSL = 'false'; httpOnlyCookies = 'true' }

CommitConfChanges

# 測試二 進階應用

SetConfPath 'Default Web Site' 'ConfLab' 'SubFolder'

# 依現有 Attribute 值決定新值
$newNum = [int]::Parse((GetXmlElementAttr 'appSettings/add[@key="Number"]' 'value')) + 1
SetXmlElementAttrs 'appSettings/add[@key="Number"]' @{ value = $newNum }
# 執行 XPATH 查詢,依現況決定更新方式
[System.Xml.XmlElement]$node = GetXmlNode 'system.web'
if ($node -and $node.SelectSingleNode('compilation[@debug="true"]')) {
    # 置換 XML
    SetXmlElementInnerXml 'system.web/authorization' @"
<allow users="*" />
"@
}

CommitConfChanges -Force # 不需確認直接更新

執行結果:

補充說明:

  1. 指定站台及網站應用程式名稱,系統找到 web.config 所在位置 (因為有呼叫 Get-WebApplication,需以管理者身分執行)
  2. 開啟前先備份原有 web.config (若放棄更新會刪除備份資料夾)
  3. 啟動轉譯將執行過程寫成 Log
  4. 顯示更新細節
  5. 使用 git diff 對照修改處
  6. 確認後才更新
  7. 進階應用:讀取現值計算後決定新值
  8. 直接換掉整個 XML 內容

這些函式已能符合我絕大部分的 web.config 修改需求,還有不足的地方就等未來再改良了。

關於 git.exe,我採用較彈性的做法,若伺服器有安裝 Git,用 where git 找到的位置,否則會尋找 WebConfLib.ps1 同目錄下的可攜版 PortableGit,若都沒有就停用差異對照功能。

附上 WebConfLib.ps1 雛型給大家參考:

$ErrorActionPreference = 'STOP'
# 全域變數
[string]$configPath = ''
[string]$bakPath = ''
[System.Xml.XmlDocument]$xmlDoc = New-Object System.Xml.XmlDocument

# 自動偵測 git.exe 路徑
$gitPath = ''
. where.exe git | ForEach-Object { 
    if ($_.EndsWith('git.exe')) { $gitPath = $_ }
}
if ([string]::IsNullOrEmpty($gitPath) -and (Test-Path $PSScriptRoot\PortableGit\bin\git.exe)) {
    $gitPath = Resolve-Path $PSScriptRoot\PortableGit\bin\git.exe
}

function SetConfPath {
    param (
        [Parameter(Mandatory = $true)][string]$site, 
        [Parameter(Mandatory = $true)][string]$appName, 
        [string]$folder = ''
    )
    $app = Get-WebApplication -Site $site -Name $appName
    if (!$app) { throw "$site/$appName not found" }
    
    $configPath = [IO.Path]::Combine($app.PhysicalPath, $folder, 'web.config')
    Write-Host "### 設定檔路徑 = $configPath" -ForegroundColor Yellow
    # 依時間產生備份資料夾路徑
    $bakPath = Join-Path (Get-Location).Path ('BAK-' + (Get-Date -Format 'yyyyMMddHHmmss'))
    # 備份原檔
    [IO.Directory]::CreateDirectory($bakPath) | Out-Null
    if (Test-Path $configPath) {
        Write-Host "備份舊版 $configPath" -ForegroundColor Cyan
        Copy-Item $configPath (Join-Path $bakPath 'web.orig.config')
    }
    else {
        Write-Host "原本無設定檔" -ForegroundColor Cyan
        '' | Out-File (Join-Path $bakPath 'web.orig.config')
        # 產生空白 web.config
        @"
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
</configuration>
"@ | Out-File $configPath -Encoding utf8
    }
    $xmlDoc.Load($configPath)
    Set-Variable -Name configPath -Value $configPath -Scope 1
    Set-Variable -Name bakPath -Value $bakPath -Scope 1
    Start-Transcript -Path (Join-Path $bakPath "Update.log")
}

function GetXmlNode([string]$xpath, [bool]$autoCreate = $false) {
    $node = $xmlDoc.DocumentElement.SelectSingleNode($xpath) 
    if ($node) { return $node }
    if (!$autoCreate) { return $null }
    $currNode = $xmlDoc.DocumentElement
    $xpath.Split('/') | ForEach-Object {
        $elName = $_
        $m = [System.Text.RegularExpressions.Regex]::Match($elName, "[a-zA-z]+\[@(?<n>.+)=`"(?<v>.+)`"]")
        if ($m.Success) {
            $keyAttrName = $m.Groups['n'].Value
            $keyAttrVal = $m.Groups['v'].Value.Trim("'", "`"")
            $elName = $_.Split('[')[0]
        }
        if (!$currNode.SelectSingleNode($_)) {
            $node = $xmlDoc.CreateElement($elName)    
            if ($keyAttrName) { $node.SetAttribute($keyAttrName, $keyAttrVal) }
            $currNode = $currNode.AppendChild($node) 
        }
        else {
            $currNode = $currNode.SelectSingleNode($_)
        }
    }    
    return $xmlDoc.DocumentElement.SelectSingleNode($xpath)
}

function SetXmlElementAttrs([string]$xpath, [Hashtable]$attrs) {
    $node = (GetXmlNode $xpath $true)
    Write-Host "* 設定屬性:$xpath " -ForegroundColor Yellow
    $attrs.Keys | ForEach-Object {       
        Write-Host "  $_ = $($attrs[$_])" -ForegroundColor White
        $node.SetAttribute([string]$_, [string]$attrs[$_])
    }
}
function GetXmlElementAttr([string]$xpath, [string]$attrName) {
    $node = GetXmlNode $xpath
    if (!$node) { return '' }
    return $node.GetAttribute($attrName)
}

function SetXmlElementInnerXml([string]$xpath, [string]$xml) {
    $node = (GetXmlNode $xpath $true)
    Write-Host "* 設定XML:$xpath" -ForegroundColor Yellow
    Write-Host "  $xml" -ForegroundColor White
    $node.InnerXML = $xml
}
function AppendXmlElementAttrs([string]$xpath, [Hashtable]$attrs) {
    $node = (GetXmlNode $xpath $true)
    Write-Host "* 附加屬性 $xpath" -ForegroundColor Yellow
    $attrs.Keys | ForEach-Object {
        $old = $node.GetAttribute($_)
        $new = $old + [string]$attrs[$_]
        Write-Host "  $old => $new" -ForegroundColor White
        $node.SetAttribute([string]$_, $new)
    }
}
function RemoveXmlElement([string]$xpath) {
    $node = GetXmlNode $xpath
    if ($node) { 
        Write-Host "移除 $xpath" -ForegroundColor Yellow
        $node.ParentNode.RemoveChild($node) | Out-Null 
    }
}
function ShowDiff([string]$origFile, [string]$newFile) {
    if ([string]::IsNullOrEmpty($gitPath)) {
        Write-Host '系統未安裝 Git 或 PortableGit,無法提供修改確認' -ForegroundColor Red
        return;
    }
    "Git [$gitPath]"
    Write-Host "本次異動對照如下,請確認:" -ForegroundColor Yellow
    $header = $true
    . $gitPath diff --no-index $origFile $newFile 2>&1 | ForEach-Object {
        if ($header) {
            if ($_.StartsWith('@@ ')) { $header = $false }
        }
        else {
            $color = 'Cyan'
            if ($_.StartsWith('+')) {
                $color = 'Green'
            }
            elseif ($_.StartsWith('-')) {
                $color = 'Red'
            }
            Write-Host $_ -ForegroundColor $color
        }
    }
}

function CommitConfChanges([switch][bool]$force) {
    $newConfPath = Join-Path $bakPath 'web.new.config'
    $origConfPath = Join-Path $bakPath 'web.orig.config'
    $xmlDoc.Save($newConfPath)
    ShowDiff $origConfPath $newConfPath
    if ($force -or (Read-Host "確定要更新?Y/N") -ieq 'Y') {
        Copy-Item $newConfPath $configPath 
        Write-Host "已更新 $configPath" -ForegroundColor Cyan
        Stop-Transcript
    }
    else {
        Write-Host "放棄修改" -ForegroundColor Red
        Stop-Transcript
        . cmd.exe /c "rmdir $bakPath /s /q"
    }
}

Recommend

  • 14
    • titangene.github.io 4 years ago
    • Cache

    GitLab Page 自動部署 Vue CLI 專案

    手動將 Vue CLI build 出來,然後再 push 至遠端部署,這些步驟雖然很簡單,但這樣不是很有效率,所以應透過自動部署來處理。本篇介紹如何透過 GitLab CI/CD 來將 Vue...

  • 10

    PowerShell - 將多參數以陣列變數傳入函式-黑暗執行緒分享最近學到的 PowerShell 小技巧。 假設我有個接受多個參數的函式,有三種參數寫法。第一種是寫成 FuncName Arg1 Arg2 Arg3... 依序列出,中間以空白間隔(注意:不要加 ( ) 及 ,,參考:

  • 9

    最近熱愛 low-code 或 no-code 的解決方案,簡單拖拉幾個設定,或是複製之前寫好的 JSON,就可以快速完成一個日常的自動化工作,減少越來越多的人工操作,重點是幾乎不用花錢,也不用花心思在部署上。今天我要來整理幾個

  • 13

    如何利用 PowerShell 自動將應用程式註冊到工作排程器 (Task Scheduler)最近一直在處理不同專案的 CI/CD 作業,有個自動化排程作業就需要將應用程式註冊到 工作排程器 (Task Scheduler) 之中,因此就研究了一下如何利用 PowerShell 來完成這個...

  • 6

    Chrome/Edge 自動更新及版本檢查 PowerShell 排程-黑暗執行緒Chrome/Edge Chromium 內建「自動偵測新版本,提醒使用者下載更新」功能。但從資安管理角度,若新版涉及重要安全修補,晚一天更新就多曝險一天。遇到認真積極的網管,沒即時更新 Chrome 可是會被通緝...

  • 0
    • blog.niclin.tw 3 years ago
    • Cache

    ES6 箭頭函式 (Arrow functions)

    很多人箭頭函式寫久了卻不清楚和一般 function 的差異在哪,大概能記得的就是簡潔好寫這樣。不過還是有些細節要注意,寫法上也有可以縮寫的方式。// 普通寫法 const currentLanguage = (user) => { return user.locale } // 縮寫...

  • 5

    先前整理過用 PowerShell 設定 IIS 的技巧,將原本 GUI 操作轉成指令檔,可以減少人為操作失誤的風險,並能實現自動化部署的理想。 最近又遇到類似需求,多學會一些安...

  • 6

    自動改用管理者權限執行 .ps1-黑暗執行緒 有些 PowerShell 指令必須以管理者權限執行,當 .ps1 包含需要高權限動作,執行時要記得開「Windows PowerShell (系統管理者)」下指令。

  • 9

    將 Playwright 網頁自動操作程式部署到客戶端執行-黑暗執行緒 上回提到我想用 Playwright for .NET 也可以用來開發網頁操作自動化機器人,但部署到客戶端...

  • 8

    自動補齊離線安裝 NuGet 套件-黑暗執行緒 在一般情況下,NuGet 套件會在編譯時自動從網路下載安裝,不需我們費心。但現實世界不如想像美好,有時你需要在無法上網的環境編譯專案,簡單解法是開個本地資料夾當成 NuGet 套件來源

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK