SharePointDsc icon indicating copy to clipboard operation
SharePointDsc copied to clipboard

SPProductUpdate: Add possibility to use Zero-Downtime Patching with SPDsc

Open ykuijs opened this issue 6 years ago • 33 comments

@andikrueger and I have been discussing the possibility to patch SharePoint using Zero-Downtime Patching with SharePointDsc. Since this requires directing traffic to other servers in the SharePoint farm, this will require implementing a solution to make sure the server is not serving traffic.

We both see that a lot of companies don't configure their load balancing the proper way. Their appliances only ping the SharePoint WFE IP addresses. If the IIS services are stopped or not responding (intentionally or because of some sort of issue), the server doesn't serve any requests, but the load balancer still gets a ping response and considers the server as operational.

Therefore load balancer solutions should at least check for HTTP status codes or better yet, retrieve a page and check for specific content every 10 to 15 seconds.

If this is the case, there is a possibility to do ZDP with SPDsc:

  1. Prepare WFE by fooling the LB by either: a. Create a IIS Rewrite rule, to send out 503 status codes b. Changing the IP of the SharePoint Web application binding - changing the HTTP listening port c. Setting a firewall rule, that blocks all traffic coming from the load balancer IP (which is the easiest to create) d. Create a new IIS Web Application that catches all the traffic to that server and does a redirect to the IP address of any other WFE (with the host-header attached) e. Have a PowerShell script check the local web applications and create an HTML file in an anonymous web site. If the checks are good, the file contains the text "Ok". If the checks fail, the file contains the text "Failed". The LB checks this HTML file and depending on the result, considers the server as operational or not. f. Any other ways to temporally disable this WFE from taking request issued by the LB.
  2. Now the LB should route the traffic to the remaining WFEs.
  3. Enable Side-by-side on WFE 1
  4. Install the patches
  5. Reboot the server
  6. Disable the LB fooling solution on this WFE
  7. Prepare WFE 2 by using either method mentioned in 1.
  8. Proceed with the patching on this server, etc

We are curious what other think of this solution? If there are any other ideas that might be better as the ones suggested above.

ykuijs avatar Jul 18 '19 18:07 ykuijs

Assuming, we do have a proper LB solution in place, there would still be the need to create a new SPZeroDownTimePatching resource. This resource would be needed two times per WFE per configuration to enable side-by-side and disable side-by-side properly.

andikrueger avatar Jul 18 '19 18:07 andikrueger

I do not agree with creating a new resource. The reason for this is that one resource would have the enabled state and the other the disabled state, so these will always be in conflict with each other and the server will never be in a compliant state. Each time a configuration is applied, the first resource would enabled SbS and the second would disable it again. If "Apply and Autocorrect" is configured, this would constantly enabled/disabled during each consistency run.

I rather would extend the SPProductUpdate resource to enable side-by-side at the beginning of the set method and to disable it at the end of the set method.

ykuijs avatar Jul 18 '19 21:07 ykuijs

Wouldn’t extending the Set-Method lead to one WFE being more up-to-date than another (not yet patched) WFE and in this way impact the user experience?

andikrueger avatar Jul 18 '19 21:07 andikrueger

I did some drawing this morning. The following picture shows the several steps needed to patch a 2 WFE with several App Servers Farm with DSC and ZDP.

Zero Downtime Patching

Following color codes for the drawing:

  • red - could be problematic in one configuration as these resources would conflict (assuming to use NetworkingDsc with the Firewall Resource)
  • blue - these resources could be challenging.
    • Reboot: Reboot should onlz occure, if needed. Additional Parameters should be used: https://powershell.org/forums/topic/handling-reboots-in-dsc/
    • SPConfigWizard should only run, if really needed. @ykuijs Is this already the case?

andikrueger avatar Jul 19 '19 08:07 andikrueger

I have thought about this and the best thought I had was a script wrapper around the DSC resources to achieve the goal. I thought the enabling of SideBySide was best handled with a Function as well as setting the proper version. Then for dealing with the farm to do true ZDP, you can only execute on half the farm resources. I worked with Nik and this seems feasible within my DSC script to identify the Nodes AllNodes = @( @{ NodeName = "Servername-CA" CertificateFile = "\FAAMECK003\E$\DSCConfig\1Certificate\DSCAuthor.cer" Thumbprint = "?97815e8e2c4906e5b22bae09eaf3aec0e801f432" PSDscAllowDomainUser = $true Role = "WebSet1" }

The Role allows me to have a set of WFE, App, and DC servers. Then when calling the upgrade installation and such you just have

Node $AllNodes.Where{$_.Role -eq $NodeType}.NodeName The $NodeType is passed or changed in the wrapper for which "SET" of WFE's to do, Set1 or Set2, so that the DSC execution is controlled and operating on exactly the half of the farm controlled by the wrapper. All of this to be run from an external server to the farm so that each server could be rebooted without losing context of where the upgrade process was.

Apologies if this is considered insane, I just was trying to figure out a method to do it on my own, I should have checked here sooner ..

jimbrayDel avatar Sep 17 '19 15:09 jimbrayDel

Hi @jimbrayDel Not entirely sure what you mean here. Can you elaborate a little more?

ykuijs avatar Sep 19 '19 09:09 ykuijs

As I see it @jimbrayDel would introduce a new key-value pair Role="WebSet1" or Role="WebSet2" for every node. With having this information in the configuration file, we would be able to handle the patch process more efficiently, as we would be able to identify the farm servers more easily, that needs to be patched simultaneously.

Using Role is ambiguous, as it it conflicts with the MinRoles. I would use ServerGroup as Key.

andikrueger avatar Sep 19 '19 11:09 andikrueger

@andikrueger Exactly what I was trying to say, I agree with your thought that Role could be confusing, and ServerGroup would work just fine. The Goal I am working on is to have the ZDP be completely automated without any manual intervention required.

jimbrayDel avatar Sep 19 '19 14:09 jimbrayDel

@ykuijs @andikrueger Trying to make this as simple as possible.

  • Don't really care which server is WFE. If node is performing SPProductUpdate, block load balancer IP with Firewall Rule. When SPProductUpdate is done, return to load balancer.
  • Make sure EnableSideBySide = $true for all WebApplications (This never gets set to $false)
  • Assume redundant environment. Must have 2 Servers for each role or role group.
  • Can be done with Script Resource and WaitForAll Resource
  • Requires Admin to enter IP of load balancer in ConfigData section; Everything else is automatic
  • Assumed Environment where ServerGroup # is in the node name, i.e., SPWEBFE1 is ServerGroup1 and SPWEBFE2 is ServerGroup2 (Easily changed).

Steps:

  1. Server #1 of the role gets placed into ServerGroup1, Server #2 of the role gets placed into ServerGroup2.
  2. Remove ServerGroup1 from load balancer
  3. SPProductUpdate ServerGroup1
  4. Return ServerGroup1 to load balancer
  5. Remove ServerGroup2 from load balancer
  6. SPProductUpdate ServerGroup2
  7. Return ServerGroup2 to load balancer
  8. PSConfigWizard ServerGroup1
  9. PSConfigWizard ServerGroup2
  10. Set SideBySideToken = highest # folder name in hive on all servers (foreach WebApplication)
Configuration SPFarmUpdateZDT
{
    $CredsSPFarm = Get-Credential -Message "Farm Account Service Account"
    Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
    Import-DscResource -ModuleName SharePointDSC -ModuleVersion 3.6.0.0
    Import-DscResource -ModuleName PSDesiredStateConfiguration
    #ServerGroup 1
    Node $AllNodes.Where{$_.ServerGroup -eq 1}.NodeName
    {
        Script EnableSideBySideFarmWide
        {
            SetScript = 
            {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $WebApps = Get-SPWebApplication
                Write-Verbose "Enabling Side By Side Farm Wide"   
                foreach($webApp in $webapps){
                    $Webapp.WebService.EnableSideBySide = $true
                    $WebApp.Update()
                }
            }
            TestScript = 
            {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $WebApps = Get-SPWebApplication
                foreach($webApp in $webapps){
                    if($Webapp.WebService.EnableSideBySide -eq $false){
                        return $false
                    }
                }
                return $true
            }
            GetScript = 
            {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $WebApps = Get-SPWebApplication
                foreach($webApp in $webapps){
                    if($Webapp.WebService.EnableSideBySide -eq $false){
                        return $false
                    }
                }
                return $true
            }
        }
        Script BlockLoadBalancer{
            GetScript = {            
                Return @{            
                    Result = Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                }            
            }            
            TestScript = {            
                $rule =  Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                if ($rule) {            
                    Write-Verbose "Enabled"            
                    return $true
                } 
                else {            
                    Write-Verbose "Disabled"            
                    Return $false            
                }            
            }            
            SetScript = {
                $remoteAddress = ($AllNodes | Select-Object -first 1).LoadBalancerIPAddress
                Write-Verbose "Blocking communication with LoadBalancer"            
                New-NetFirewallRule -Direction Inbound -DisplayName "Block Load Balancer" -Name "BlockLoadBalancer" -RemoteAddress $remoteAddress -Action Block                            
            }            
        }
        SPProductUpdate ServerGroup1
        {
            SetupFile = "C:\Patch\CU.exe"
            ShutdownServices = $true
            BinaryInstallDays = @("sat", "sun")
            BinaryInstallTime = "12:00am to 4:00am"
            PsDscRunAsCredential = $CredsSPFarm
        }
        WaitForAll SPProuductUpdateAllNodes
        {
            ResourceName = "[SPProductUpdate]ServerGroup2"
            NodeName = $AllNodes.Where{$_.ServerGroup -eq 2}.NodeName
            RetryIntervalSec = 300
            RetryCount = 720
            DependsOn = "[SPProductUpdate]ServerGroup1"
        }
        SPConfigWizard ServerGroup1
        {
            Ensure = "Present"
            DatabaseUpgradeDays = @("sat", "sun")
            DatabaseUpgradeTime = "12:00am to 4:00am"
            PsDscRunAsCredential = $CredsSPFarm
            IsSingleInstance = "Yes"
            DependsOn = "[WaitForAll]SPProuductUpdateAllNodes"
            
        }
        Script UnBlockLoadBalancerServerGroup1{
            
            GetScript = {            
                Return @{            
                    Result = Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                }            
            }            
            TestScript = {            
                $rule =  Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                if ($rule) {            
                    Write-Verbose "Enabled"            
                    return $false
                } 
                else {            
                    Write-Verbose "Disabled"            
                    Return $true            
                }            
            }            
            SetScript = {
                $remoteAddress = ($AllNodes | Select-Object -first 1).LoadBalancerIPAddress
                Write-Verbose "Blocking communication with LoadBalancer"            
                New-NetFirewallRule -Direction Inbound -DisplayName "Block Load Balancer" -Name "BlockLoadBalancer" -RemoteAddress $remoteAddress -Action Block                            
            }
            DependsOn = "[SPProductUpdate]ServerGroup2"
        }
    }
    #ServerGroup 2
    Node $AllNodes.Where{$_.ServerGroup -eq 2}.NodeName
    {
        WaitForAll UnBlockServerGroup1
        {
            ResourceName = "[Script]UnBlockLoadBalancerServerGroup1"
            NodeName = $AllNodes.Where{$_.ServerGroup -eq 1}.NodeName
            RetryIntervalSec = 300
            RetryCount = 720
        }
        Script BlockLoadBalancerServerGroup2{
            GetScript = {            
                Return @{            
                    Result = Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                }            
            }            
            # Must return a boolean: $true or $false            
            TestScript = {            
                $rule =  Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                if ($rule) {            
                    Write-Verbose "Enabled"            
                    return $true
                } 
                else {            
                    Write-Verbose "Disabled"            
                    Return $false            
                }            
            }            
            SetScript = {
                $remoteAddress = ($AllNodes | Select-Object -first 1).LoadBalancerIPAddress
                Write-Verbose "Blocking communication with LoadBalancer"            
                New-NetFirewallRule -Direction Inbound -DisplayName "Block Load Balancer" -Name "BlockLoadBalancer" -RemoteAddress $remoteAddress -Action Block                            
            }            
            DependsOn = "[WaitForAll]UnblockServerGroup1"
        }
        SPProductUpdate ServerGroup2
        {
            SetupFile = "C:\Patch\CU.exe"
            ShutdownServices = $true
            BinaryInstallDays = @("sat", "sun")
            BinaryInstallTime = "12:00am to 4:00am"
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn = "[Script]BlockLoadBalancerServerGroup2"
        }
        WaitForAll SPConfigWizardServerGroup1
        {
            ResourceName = "[SPConfigWizard]ServerGroup1"
            NodeName = $AllNodes.Where{$_.ServerGroup -eq 1}.NodeName
            RetryIntervalSec = 300
            RetryCount = 720
            DependsOn = "[SPProductUpdate]ServerGroup2"
        }
        SPConfigWizard ServerGroup2
        {
            Ensure = "Present"
            DatabaseUpgradeDays = @("sat", "sun")
            DatabaseUpgradeTime = "12:00am to 4:00am"
            PsDscRunAsCredential = $CredsSPFarm
            IsSingleInstance = "Yes"
            DependsOn = "[WaitForAll]SPConfigWizardServerGroup1"
        }
        Script SetSideBySideTokenFarmWide
        {
            SetScript = 
            {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $path = [Microsoft.SharePoint.Utilities.SPUtility]::GetGenericSetupPath("TEMPLATE\Layouts")
                Folders = Get-ChildItem $path -Directory | Where-Object name -match '\d+.\d+.\d+.\d+'
                $latest = ''
                switch($folders.count){
                    0: {break;}
                    1: {$latest = $folders.name}
                    2: {
                        if( ($folders[0].name).replace('.','') -gt ($folders[1].name).replace('.','') ){
                            $latest = $folders[0].name
                        }
                        else{$latest = $folders[1].name}
                    }
                }
                $WebApps = Get-SPWebApplication
                foreach($webApp in $webapps){
                    if($WebaApp.WebService.EnableSideBySide -eq $true){
                        $Webapp.WebService.SideBySideToken = $latest
                        $WebApp.Update()
                    }
                }
            }
            TestScript = {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $path = [Microsoft.SharePoint.Utilities.SPUtility]::GetGenericSetupPath("TEMPLATE\Layouts")
                Folders = Get-ChildItem $path -Directory | where name -match '\d+.\d+.\d+.\d+'
                $latest = ''
                switch($folders.count){
                    0: {break;}
                    1: {$latest = $folders.name}
                    2: {
                        if( ($folders[0].name).replace('.','') -gt ($folders[1].name).replace('.','') ){
                            $latest = $folders[0].name
                        }
                        else{$latest = $folders[1].name}
                    }
                }
                $WebApps = Get-SPWebApplication
                foreach($webApp in $webapps){
                    if($Webapp.WebService.SideBySideToken -ne $latest -and $WebApp.WebService.EnableSideBySide){
                        return $false
                    }
                }
                return $true
            }
            GetScript = {
                Add-PSSnapin Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue
                $WebApps = Get-SPWebApplication
                return @{Result = $WebApps | ForEach-Object{
                    @{DisplayName = $_.DisplayName; Url = $_.Url; SideBySideToken = $_.WebService.SideBySiteToken}
                    }
                }
            }
            DependsOn = "[SPConfigWizard]ServerGroup2"
        }
        Script UnBlockLoadBalancer{
            GetScript = {            
                Return @{            
                    Result = Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                }            
            }            
            TestScript = {            
                $rule =  Get-NetFirewallRule -Name "BlockLoadBalancer" -ErrorAction SilentlyContinue
                if ($rule) {            
                    Write-Verbose "Enabled"            
                    return $false
                } 
                else {            
                    Write-Verbose "Disabled"            
                    Return $true            
                }            
            }            
            SetScript = {
                $remoteAddress = ($AllNodes | Select-Object -first 1).LoadBalancerIPAddress
                Write-Verbose "Blocking communication with LoadBalancer"            
                New-NetFirewallRule -Direction Inbound -DisplayName "Block Load Balancer" -Name "BlockLoadBalancer" -RemoteAddress $remoteAddress -Action Block                            
            }
            DependsOn = "[SPProductUpdate]ServerGroup2"
        }
    }
}
$ConfigData = @{
    AllNodes = [array] ((Get-SPFarm).Servers | Where-Object Role -ne "Invalid" | ForEach-Object{
        @{
            NodeName = $_.Address;
            ServerGroup = $_.Address -replace '\D', '';
            LoadBalancerIPAddress = "10.10.10.1";
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser = $true;
        }
    })
}
SPFarmUpdateZDT -ConfigurationData $ConfigData

New to DSC, would appreciate feedback, comments.

kayodebristol avatar Nov 05 '19 16:11 kayodebristol

Hi @kayodebristol

Thanks for sharing your idea. I see one major issue with this idea: DSC isn't meant for deploying separate configurations for each change. You basically update a configuration with the change you want to implement and DSC goes out and does that change for you. So you specify the state you want the environment to be in.

In your example, you specified two script resource per server group that will cause issues: For ServerGroup1, these are "BlockLoadBalancer" and "UnBlockLoadBalancerServerGroup1" For ServerGroup2, these are "BlockLoadBalancerServerGroup2" and "UnBlockLoadBalancer"

The reason these two resource will cause issues is that the desired state of these resources are the exact opposite of each other. So it is impossible that the server will ever be in a compliant state, since it will always be incompliant with one or the other.

So basically you created a configuration that specifies a value to be 0 and 1 at the same time. If you configure the Local Configuration Manager as "Apply and Autocorrect", this means every 15 minutes (by default) traffic is blocked by the BlockLoadBalancer resource (since the firewall rule doesn't exist and therefore the server is incompliant) and shortly after that the traffic is allowed again by the UnBlockLoadBalancer resource (since the firewall rule does exist and therefore the server is incompliant).

If we want to enable this scenario, we need to implement this routine in the SPProductUpdate resource.

ykuijs avatar Nov 05 '19 19:11 ykuijs

@ykuijs Great explanation, thank you. Totally get it. There might be another way, if one could detect when SPProductUpdate was running... maybe with a mutex? Nix the UnBlockLoadBalancer script resources and change the BlockLoadBalancer Script resource to something like this:

Script BlockLoadBalancerWhilePatching{ SetScript { Try { $mutex = [Theading.Mutex]::OpenExisting('Global\[SPProductUpdate Mutex Name]'); #not sure if there is such a thing, but if there is, Loadbalancer will be blocked New-NetFirewallRule -Direction Inbound -DisplayName "SP Block Load Balancer While Patching - DSC" -Name "SPBLBWPDSC" -RemoteAddress $remoteAddress -Action Block -ErrorAction SilentlyContinue; $mutex = $null; } Catch { #OpenExitisting throws an exemption if the mutex name is not found if($null -eq $mutex){ $Remove-NetFireWallRule -Name SPBLBWPDSC } else{$mutex = $null} } } }

Initially, I thought I could use 'Global_MSIExecute' to detect it, but that didn't work in testing. Did I get lost in the rabbit hole, again, maybe?

kayodebristol avatar Nov 06 '19 11:11 kayodebristol

Did you have a look at this idea: #1097?

andikrueger avatar Nov 06 '19 11:11 andikrueger

@andikrueger Yes. #1097 Deals with EnableSideBySide & SideBySideToken. I handle that with EnableSideBySideFarmWide and SetSideBySideTokenFarmWide Script resources in SPFarmUpdateZDT Configruation, above.

@ykuijs pointed out that my handling of the loadbalancer (UnBlockLoadBalancer & BlockLoadBalancer script resources) won't work. So, my previous post was specific to addressing the loadbalancer part of the problem.

I read somewhere that writing script resources was a good first step for creating custom resources... I think I'm too green to write a custom resource, but I want to contribute. Thought submitting script resources that could accomplish ZDTP would be beneficial. No?

kayodebristol avatar Nov 06 '19 11:11 kayodebristol

@kayodebristol your input is highly appreciated!

Yes, the networking part is challenging. As @ykuijs pointed out we can’t have to resources within one configuration that do say “do it” and “don’t do it” at the same time. The same applies to the SideBySidePart.

The easiest solution I can think of (and @ykuijs mentioned above) is to extend the SPProductUpdate Resource with a switch parameter ZDP. This switch would work like ShutdownServices and would disable incoming network traffic.

We won’t need it for

  • Reboot
  • SPConfigWizard as they will shutdown the services anyway.

Doing it this way won’t conflict in the configuration.

I’m happy to create some draft resources for ZDP with DSC.

What’s your opinion on the extension described above?

andikrueger avatar Nov 06 '19 12:11 andikrueger

@andikrueger

Looks like I missed something. Node should be pulled out of loadbalancer during SPProductUpdate and SPConfigWizard steps. It doesn't matter that SPConfigWizard shuts down services. Dumb loadbalancer will still direct traffic to node until TCP response is blocked.

Think I'm in favor of a separate resource instead of extending existing... I would ideally like the node removed from the loadbalancer during all "patch" activities (Windows Update, SPProductUpdate, & SPConfigWizard). Perhaps a property of this resource would be a Trigger = All (default) | Windows | SharePoint. Just a thought.

Still not tracking on the SideBySide piece, however. Assumptions:

  1. Once EnableSideBySide is set to $true for a SPWebApplication, it can stay enabled for the life of the SPWebApplication. zero downtime patching steps never mention setting EnableSideBySide back to false.
  2. SideBySideToken value needs to change, but the configuration stays the same, latest (highest version number folder name). The zero downtime patching steps, above, set the SideBySideToken to the latest at the end of the patching process, that's it. The SetSideBySideTokenFarmWide script resource accomplishes that and DependsOn = "[SPConfigWizard]ServerGroup2" (all servers have the latest code). The SideBySideToken value will change, but always to the latest folder version. The configuration, apply the latest, remains the same throughout.

Am i still missing something?

kayodebristol avatar Nov 06 '19 13:11 kayodebristol

I think, there is a challenge with your approach of a seperate resource. Within the resource that removes the node from load-balancing we would need to know, if this node would need to install any updates. Otherwise this node would block traffic and would need to allow traffic seconds later during a configuration run.

This block and do not block is my main concern. This is similar to https://github.com/PowerShell/SharePointDsc/issues/1096#issuecomment-549985611

AFAIK running config wizard will stop the IIS service. This is equal to set blocking firewall rules in place. The server wont answer to your request.

This leaves us with the SPProductUpdate resource. Within the Set-Method of this resource, we could easily block the network traffic and unblock the traffic too. Doing so will not conflict.

The part about the SideBySide Token:

  1. SharePoint allows to set and unset the property. We should have this ability in DSC too. That's why there should be option available to disable side by side.
  2. Yes. We would need to set to the highest version number at the end of the process.

andikrueger avatar Nov 06 '19 14:11 andikrueger

@andikrueger I disagree. User gets 404 if server doesn't reply. That's not zero down time. The blocking firewall rule is used because even the most basic load balancer does a ping (except perhaps DNS load balancing, but you can't help everyone). If the node doesn't ping, it is removed.

SideBySide settings have the specific goal of having users get the same code. It doesn't facilitate zero down time at all. Just consistency.

Zero down time to me means that the user never gets a 404. So, the load balancer never sends a request to a server that's not ready to handle that request. So, node should be removed from load balancer during "patch" activities. Placed back in load balancer patch activity completes.

The only requirement, as far as timing goes, is that the node must be placed back in the load balancer before it's partner node or ServerGroup is removed. Seconds not required. WaitForAll can do that.

SideBySideToken and EnableSideBySide are 2 separate SPWebApplication Settings.

$WebApp.WebService.EnableSideBySide = $false (default) | $true #This directs SharePoint to copy code to the version named folder

$WebApp.WebService.SideBySideToken = "" (Default) | x.x.x.x (folder name) #Tells SharePoint which folder to serve code from.

If you turn on ZDT patching with it's own resource, i.e. Enabled = $true, SideBySideToken = [Latest folder] & EnableSideBySide = $true. Enabled = $false, SideBySideToken = '' & EnableSideBySide = $false. But, once you go ZDT why would you go back? Lost a node, so you can't do high availability. OK, still doesn't make sense to turn it off. Imagine a single server farm, if you patch it you have down time. No matter your SideBySide setting your down time will be the same. Actually, thinking about, it should be on by default. But that's a different team.

Yes, figuring out how to implement the block during certain times is a challenge, but you have to extend both SPProductUpdate and PSConfigWizard with a redundant setting. And it doesn't help during Windows patching. A separate resource is harder, I get it, but it's the more elegant solution, if we can figure it out.

The only problem I see is being able to detect when SPConfigWizard, PSProductUpdate, or Windows Updates are in progress... If can't figure that out, then extending both resources is the only option.

kayodebristol avatar Nov 06 '19 14:11 kayodebristol

You are right about the 404 status code. I'm totally with you in case of ZDP, the user should not be confronted with any 404 or any other error messages. From a users perspektiv: SharePoint should be up an running.

@ykuijs and I had a discussion about this technical debt before and we came to the conclusion, that we would need a basic LB-guidance:

Therefore load balancer solutions should at least check for HTTP status codes or better yet, retrieve a page and check for specific content every 10 to 15 seconds. see: https://github.com/PowerShell/SharePointDsc/issues/1096#issue-469922845

This would eliminate every ping based solution, which is very error prone and basically not recommended for production.

andikrueger avatar Nov 06 '19 14:11 andikrueger

@kayodebristol I think what @andikrueger is referring to for the Load balancer is most intelligent devices today especially BigIP can be configured to check for a static page on IIS and based on the text can remove or add a server to the working pool. In our case we have a html page with a simple up/down that the BigIp reads if the text is "Down" then it removes the server from the pool, and vice versa. We had already automated this ability for some of our scripting.

Function F5Status ($status,$WebServ) # used to set Status.HTML for servers to remove from loadbalancing and to add back to load balancing { $line1 = "<HTML>" $line3 = "</HTML>" $nwline = "rn" $content = $line1 + $nwline + $status + $nwline + $line3 $uncPath = "F$\inetpub\wwwroot" Remove-Item "\$webserv$uncPath\status.html" -ErrorAction SilentlyContinue New-Item "\$webserv$uncPath\status.html" -type File Add-Content -Path "\$webserv$uncPath\status.html" $content if (Select-String -Path "\$webserver$uncPath\status.html" -Pattern $status) {Write-Output "\$webserver$uncPath\status.html done."}

}

jimbrayDel avatar Nov 06 '19 15:11 jimbrayDel

Interesting the pasting action removed some text from the function. here it is with the characters escaped Function F5Status ($status,$WebServ) # used to set Status.HTML for servers to remove from loadbalancing and to add back to load balancing { $line1 = "'<HTML>'" $line3 = "'</HTML>'" $nwline = "rn" $content = $line1 + $nwline + $status + $nwline + $line3 $uncPath = "E$\inetpub\wwwroot" Remove-Item "\$webserv$uncPath\status.html" -ErrorAction SilentlyContinue New-Item "\$webserv$uncPath\status.html" -type File Add-Content -Path "\$webserv$uncPath\status.html" $content if (Select-String -Path "\$webserver$uncPath\status.html" -Pattern $status) {Write-Output "\$webserver$uncPath\status.html done."}

}

jimbrayDel avatar Nov 06 '19 15:11 jimbrayDel

$line1 = "\ H.T.M.L" $line3 = "</H.T.M.L>'"

anyway if this doesn't work, line 1 and line 3 are the open and closed anchors for H-T-M-L

jimbrayDel avatar Nov 06 '19 15:11 jimbrayDel

@jimbrayDel thank you for the example. This is what I was referring to. There are so many appliances that offer these options to check for status pages or static content.

andikrueger avatar Nov 06 '19 16:11 andikrueger

@jimbrayDel In theory, Yes. In practice, not so much. ADFS authentication sends back a redirect, which even the most advanced load balancers have issues with. A static, anonymous auth site, sure. But now I've got my security guys yelling at me. Maybe Windows auth on the site with a service account on my load balancer. Now one service account can take down my whole farm (deleted, password changed, expired, etc...).

The firewall rule is the most elegant, and handles most cases, almost all loadbalancers/configs. I thought the only question was if both SPProductUpdate and PSConfigWizard would have to be extended. I think so.

I'm not saying something else wouldn't work... just that it's not as elegant and doesn't handle as many use cases.

Would love to be able to say, "Patching SharePoint? Just run this DSC, fogetaboutit!" Disclaimer: Must have high availability, minimum to TCP health monitoring, avoid contact with eyes and skin and avoid inhaling fumes. Don't try this in your living room; these are trained professionals.

kayodebristol avatar Nov 06 '19 16:11 kayodebristol

@kayodebristol I am not sure I understand about the ADFS, what I was refeering to is removing the server from the load balancer pool. This would eliminate any traffic to the server which would be before any ADFS authentication would even take place. Also on pre-existing users that are already on a server the load balancer should relocate them once you take the server out of the pool. That is unless you are using "Persistence" or "Sticky" configurations that locks the Load balancer on a particular server, but that practice is not recommended. We have Distributed Cache service so we should not need sticky or persistence any more, we can allow users to bounce around. What am I missing or overlooked?

jimbrayDel avatar Nov 06 '19 16:11 jimbrayDel

@jimbrayDel I get it. Some security Nazis in certain environments will call that unauthenticated content. Crazy, I agree. But some folks even block icmp between servers. Even using authenticated content can be problematic. Blocking all inbound traffic with a firewall rules doesn't work in every case, but it works in most cases, and requires less effort than altering a status.html file, is all.

kayodebristol avatar Nov 06 '19 18:11 kayodebristol

Was reminded that Windows Firewall is also disabled in some environments in favor of other 3rd party tools like McAfee. Perhaps we leave load balancing out for the time being and just focus on EnableSideBySide & SideBySideToken? Folks with intelligent load balancers will benefit and folks without will at least have one less manual step.

kayodebristol avatar Nov 07 '19 07:11 kayodebristol

Good point with 3rd Party tools. That’s why we would need to focus on one scenario. I would still go with Windows Firewall for the time being and make this option optional.

Side by side: YES!

Let me try to provide an experimental PR for ZDP. I have something in mind.

andikrueger avatar Nov 07 '19 07:11 andikrueger

@andikrueger Awesome! I revised my example configuration (SPZDPDSCConfig). Removed load balancing stuff, and cleaned up a bit to only do the SideBySide stuff on localhost. Maybe it will be useful in some way.

kayodebristol avatar Nov 07 '19 11:11 kayodebristol

Well, you should consider that some companies will use GPOs to configure Firewall settings, In this case you won't be able to activate or deactivate any firewall rules...PS will not return any errors, however the setting will not be effective.

Why not simply stopping IIS, this should cause any LB to switch to any other WFE...

Regards, Thomas

ThomasLie avatar Feb 21 '20 09:02 ThomasLie

Good feedback on Firewall Settings beeing controlled by GPO.

@ykuijs and I discussed "stopping IIS" before. By stopping IIS we will not be able to do some manual testing during the update process.

andikrueger avatar Feb 21 '20 10:02 andikrueger