compute/sqlserver/powershell/create-availability-group.ps1 (329 lines of code) (raw):

#Requires -Version 5 # Copyright(c) 2020 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy of # the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. ############################################################################## #.SYNOPSIS # Set up a two node SQL Server Availability Group # #.DESCRIPTION # This script will set up a two node Availability Group in SQL Server. # It would normally be called by the script # create-sql-instance-availability-group.ps1, but it can also be called by # itself. Before running this script you must install 'Google Cloud SDK' # which includes the Powershell CmdLets for Google Cloud. # See https://cloud.google.com/tools/powershell/docs/ # The script expects the file parameters-config.ps1 to exist in the same # directory. # #.NOTES # AUTHOR: Anibal Santiago - @SQLThinker # #.EXAMPLE # create-availability-group.ps1 # Before running the script make you need to modify the file # parameters-config.ps1 with the parameters specific to your configuration # # Note: May need to run this command if server blocks PowerShell scripts from running # Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force ############################################################################## $ErrorActionPreference = 'Stop' # If we are not able to figure out $PSScriptRoot, assume the scripts are in C:\Scripts if ($PSScriptRoot -eq "") { $script_path = "C:\Scripts" } else { $script_path = $PSScriptRoot } ################################################################################ # Read the parameters for this script. They are found in the file # parameters-config.ps1. We assume it is in the same folder as this script ################################################################################ if (!($cred)) { . "$script_path\parameters-config.ps1" } ################################################################################ # Create a file share witness ################################################################################ $session_option = New-CimSessionOption -ProxyAuthentication Kerberos -SkipCACheck -SkipCNCheck -UseSsl # Find the domain controller name $logonserver = $env:LOGONSERVER -replace "\\","" # If unable to figure out the logon server, use the name of the domain controller. if ($logonserver -eq "") { $logonserver = $domain_cntr } $dcsession = New-CimSession -ComputerName $logonserver -SessionOption $session_option # Create shared folder if it does not exist if (!( Get-SmbShare -CimSession $dcsession | Where-Object Name -eq "QWitness" )) { New-SmbShare -Name "QWitness" -Path "C:\QWitness" -Description "SQL File Share Witness" -CimSession $dcsession -FullAccess "$domain\Administrator" } # Grant access to the shared folder to both nodes Grant-SmbShareAccess -Name "QWitness" -AccessRight Full -CimSession $dcsession -Force -AccountName "$node1`$","$node2`$" # Remove-SmbShare -Name "QWitness" -CimSession $dcsession -Force ################################################################################ # Create the test database in Node1 ################################################################################ Import-Module SQLPS -DisableNameChecking $objServer = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server ` -ArgumentList 'localhost' # Only continue if the database does not exist $objDB = $objServer.Databases[$db_name] if (!($objDB)) { Write-Host "$(Get-Date) $hostname - Creating the database $db_name" $objDB = New-Object ` -TypeName Microsoft.SqlServer.Management.Smo.Database($objServer, $db_name) # Create the primary file group and add it to the database $objPrimaryFG = New-Object ` -TypeName Microsoft.SqlServer.Management.Smo.Filegroup($objDB, 'PRIMARY') $objDB.Filegroups.Add($objPrimaryFG) # Create a single data file and add it to the Primary filegroup $dataFileName = $db_name + '_Data' $objData = New-Object ` -TypeName Microsoft.SqlServer.Management.Smo.DataFile($objPrimaryFG, $dataFileName) $objData.FileName = $sql_data + '\' + $dataFileName + '.mdf' $objData.Size = ($data_size * 1024) $objData.GrowthType = 'KB' $objData.Growth = ($data_growth * 1024) $objData.IsPrimaryFile = 'true' $objPrimaryFG.Files.Add($objData) # Create the log file and add it to the database $logName = $db_name + '_Log' $objLog = New-Object Microsoft.SqlServer.Management.Smo.LogFile($objDB, $logName) $objLog.FileName = $sql_log + '\' + $logName + '.ldf' $objLog.Size = ($log_size * 1024) $objLog.GrowthType = 'KB' $objLog.Growth = ($log_growth * 1024) $objDB.LogFiles.Add($objLog) # Create the database $objDB.Script() # Show a script with the command we are about to run $objDB.Create() # Create the database $objDB.SetOwner('sa') # Change the owner to sa } else { Write-Host "$(Get-Date) $hostname - Skipping the creation of the $db_name DB" } ################################################################################ # Create a shared folder for initial backup used by Availability Groups # We choose Node 2, but it can be any network share accesible by both nodes ################################################################################ Invoke-Command -ComputerName $node2 -ScriptBlock { param($sql_backup, $share_name, $node1) # Create folder for the backup in Node2. # For this demo script we just use one of the nodes for simplicity if (!(Test-Path -Path $sql_backup )) { New-item -ItemType Directory $sql_backup | Out-Null } # Create a Windows share for the folder Write-Host "$(Get-Date) $hostname - Creating Windows Share: $share_name" Get-SmbShare | Where-Object -Property Name -eq $share_name | Remove-SmbShare -Force New-SMBShare –Name $share_name –Path $sql_backup –FullAccess "$($node1)`$","NT SERVICE\MSSQLSERVER" | Out-Null } -ArgumentList $sql_backup, $share_name, $node1 # We may need to remove AD objects, so we will need the RSAT-AD-PowerShell Install-WindowsFeature RSAT-AD-PowerShell Import-Module ActiveDirectory ################################################################################ # Create the Windows Failover Cluster ################################################################################ # Check if the Node1 is not a member of a cluster before trying to create it if (!( Get-Service -ComputerName $node1 -DisplayName 'Cluster Service' | Where Status -EQ 'Running' )) { # Remove the AD objects for the cluster and AG if they exist Write-Host "$(Get-Date) Removing AD Objects if they exist" Get-ADComputer -Filter 'Name -eq $name_wsfc' | Set-ADObject -ProtectedFromAccidentalDeletion:$false -PassThru | Remove-ADComputer -Confirm:$false Get-ADComputer -Filter 'Name -eq $name_ag' | Set-ADObject -ProtectedFromAccidentalDeletion:$false -PassThru | Remove-ADComputer -Confirm:$false Get-ADComputer -Filter 'Name -eq $name_ag_listener' | Set-ADObject -ProtectedFromAccidentalDeletion:$false -PassThru | Remove-ADComputer -Confirm:$false Write-Host "$(Get-Date) Creating WSFC:$name_wsfc; Nodes($node1, $node2)" # Enable clustering (just in case is not already done) Install-WindowsFeature Failover-Clustering -IncludeManagementTools | Format-Table | Out-String # Create the cluster New-Cluster -Name $name_wsfc -Node $node1,$node2 -NoStorage ` -StaticAddress $ip_wsfc1,$ip_wsfc2 | Format-Table | Out-String # It is recommended to have a Witness in a 2-node cluster # We use a file share called "quorum" in the domain controller # Uncomment this command if using a fileshare witness #Get-Cluster | Set-ClusterQuorum -NodeAndFileShareMajority "\\$ip_domain_cntr\quorum" # Enable Always-On on both nodes Enable-SqlAlwaysOn -ServerInstance $node1 -Force Enable-SqlAlwaysOn -ServerInstance $node2 -Force } else { Write-Host "$(Get-Date) $hostname - Skipping the creation of the cluster" } ################################################################################ # Create the SQL Server endpoints for the Availability Groups ################################################################################ # First remove the endpoints if they exist if (Test-Path -Path "SQLSERVER:SQL\$node1\DEFAULT\Endpoints\Hadr_endpoint") { Remove-Item -Path "SQLSERVER:SQL\$node1\DEFAULT\Endpoints\Hadr_endpoint" } if (Test-Path -Path "SQLSERVER:SQL\$node2\DEFAULT\Endpoints\Hadr_endpoint") { Remove-Item -Path "SQLSERVER:SQL\$node2\DEFAULT\Endpoints\Hadr_endpoint" } # Create the endpoints in both nodes Invoke-Command -ComputerName $node1,$node2 -ScriptBlock { param($domain_netbios, $node1, $node2) $hostname = [System.Net.Dns]::GetHostName() Write-Host "$(Get-Date) $hostname - Creating endpoint on node $hostname" # Figure out the remote node to grant connect permmission if ( $hostname.ToLower() -eq $node1.ToLower() ) { $remote_node = "$($domain_netbios)\$($node2)`$" } if ( $hostname.ToLower() -eq $node2.ToLower() ) { $remote_node = "$($domain_netbios)\$($node1)`$" } # Creating endpoint $endpoint = New-SqlHadrEndpoint "Hadr_endpoint" ` -Port 5022 ` -Path "SQLSERVER:\SQL\$hostname\Default" Set-SqlHadrEndpoint -InputObject $endpoint -State "Started" | Out-Null # Grant connect permissions to the endpoints $query = " ` IF SUSER_ID('$($remote_node)') IS NULL CREATE LOGIN [$($remote_node)] FROM WINDOWS ` GO GRANT CONNECT ON ENDPOINT::[Hadr_endpoint] TO [$($remote_node)] ` GO ` IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='AlwaysOn_health') ` BEGIN ` ALTER EVENT SESSION [AlwaysOn_health] ON SERVER WITH (STARTUP_STATE=ON); ` END ` IF NOT EXISTS(SELECT * FROM sys.dm_xe_sessions WHERE name='AlwaysOn_health') ` BEGIN ` ALTER EVENT SESSION [AlwaysOn_health] ON SERVER STATE=START; ` END ` GO " Write-Host "$(Get-Date) $hostname - Granting permission to endpoint" Write-Host $query Invoke-Sqlcmd -Query $query } -ArgumentList $domain_netbios, $node1, $node2 ################################################################################ # Create the Availability Group ################################################################################ Write-Host "$(Get-Date) Creating the SQL Server Availability Group" $ErrorActionPreference = 'Stop' Import-Module SQLPS -DisableNameChecking # Check if the AG is already setup $AG = Get-ChildItem SQLSERVER:\SQL\$($node1)\DEFAULT\AvailabilityGroups if ( !($AG) ) { $hostname = [System.Net.Dns]::GetHostName() # Backup my database and its log on the primary Write-Host "$(Get-Date) $hostname - Creating backups of database $db_name" $backupDB = "\\$node2\$share_name\$($db_name)_db.bak" Backup-SqlDatabase ` -Database $db_name ` -BackupFile $backupDB ` -ServerInstance $node1 ` -Initialize $backupLog = "\\$node2\$share_name\$($db_name)_log.bak" Backup-SqlDatabase ` -Database $db_name ` -BackupFile $backupLog ` -ServerInstance $node1 ` -BackupAction Log -Initialize # Restore the database and log on the secondary (using NO RECOVERY) Write-Host "$(Get-Date) $hostname - Restoring backups of database in $node2" Restore-SqlDatabase ` -Database $db_name ` -BackupFile $backupDB ` -ServerInstance $node2 ` -NoRecovery -ReplaceDatabase Restore-SqlDatabase ` -Database $db_name ` -BackupFile $backupLog ` -ServerInstance $node2 ` -RestoreAction Log ` -NoRecovery # Find the version of SQL Server that Node 1 is running $Srv = Get-Item SQLSERVER:\SQL\$($node1)\DEFAULT $Version = ($Srv.Version) # Create an in-memory representation of the primary replica $primaryReplica = New-SqlAvailabilityReplica ` -Name $node1 ` -EndpointURL "TCP://$($node1).$($domain):5022" ` -AvailabilityMode SynchronousCommit ` -FailoverMode Automatic ` -Version $Version ` -AsTemplate # Create an in-memory representation of the secondary replica $secondaryReplica = New-SqlAvailabilityReplica ` -Name $node2 ` -EndpointURL "TCP://$($node2).$($domain):5022" ` -AvailabilityMode SynchronousCommit ` -FailoverMode Automatic ` -Version $Version ` -AsTemplate # Create the availability group Write-Host "$(Get-Date) $hostname - Create Availability Group $name_ag" New-SqlAvailabilityGroup ` -Name $name_ag ` -Path "SQLSERVER:\SQL\$($node1)\DEFAULT" ` -AvailabilityReplica @($primaryReplica,$secondaryReplica) ` -Database $db_name # Join the secondary replica to the availability group. Write-Host "$(Get-Date) $hostname - Join $node2 to the Availability Group" Join-SqlAvailabilityGroup ` -Path "SQLSERVER:\SQL\$($node2)\DEFAULT" ` -Name $name_ag # Join the secondary database to the availability group. Write-Host "$(Get-Date) $hostname - Join DB in $node2 to Availability Group" #Add-SqlAvailabilityDatabase ` # -Path "SQLSERVER:\SQL\$($node2)\DEFAULT\AvailabilityGroups\$($name_ag)" ` # -Database $db_name # In SQL Server 2017 the Add-SqlAvailabilityDatabase was failing so decided to run it as a query instead Invoke-Command -ComputerName $node2 -ScriptBlock { param($db_name, $name_ag) $hostname = [System.Net.Dns]::GetHostName() Write-Host "$(Get-Date) $hostname - Adding database [$db_name] to Availability Group [$name_ag]" $query = "ALTER DATABASE [$db_name] SET HADR AVAILABILITY GROUP = [$name_ag]" Write-Host $query Invoke-Sqlcmd -Query $query } -ArgumentList $db_name, $name_ag # Create the listener # Notice we use /24 netmask. It should match the netmask of the IP address of the nodes. Write-Host "$(Get-Date) $hostname - Create Listener ($ip_ag_listener1, $ip_ag_listener2)" New-SqlAvailabilityGroupListener ` -Name $name_ag_listener ` -StaticIp "$($ip_ag_listener1)/255.255.255.0","$($ip_ag_listener2)/255.255.255.0" ` -Path SQLSERVER:\SQL\$($node1)\DEFAULT\AvailabilityGroups\$($name_ag) | Format-Table | Out-String } else { Write-Host "$(Get-Date) $hostname - Skip creation of the Availability Group." Write-Host "$(Get-Date) This node already member of the Availability Group" } Write-Host "$(Get-Date) SQL Server Availability Group $name_ag is now ready" ################################################################################ # Remove the shared folder in Node 2 as it is no longer needed ################################################################################ Invoke-Command -ComputerName $node2 -ScriptBlock { param($sql_backup, $share_name) $hostname = [System.Net.Dns]::GetHostName() # Create a Windows share for the folder Write-Host "$(Get-Date) $hostname - Removing Windows Share: $share_name" Get-SmbShare | Where-Object -Property Name -eq $share_name | Remove-SmbShare -Force Write-Host "$(Get-Date) $hostname - Removing folder: $sql_backup" if ((Test-Path -Path $sql_backup )) { Remove-Item -Recurse -Force $sql_backup | Out-Null } } -ArgumentList $sql_backup, $share_name, $domain, $svc_acct, $node1 # Get-WSManCredSSP # Use these commands to destroy the AG and WSFC if you are re-doing the creation again # Remove-SqlAvailabilityGroup -Path "SQLSERVER:\SQL\$($node1)\DEFAULT\AvailabilityGroups\$($name_ag)" # Remove-Cluster -Force -CleanupAD # Disable-SqlAlwaysOn -ServerInstance $node1 -Force # Disable-SqlAlwaysOn -ServerInstance $node2 -Force