Synchronizing Server Objects for Availability Groups

Updated: 9/8/2017

In addition to the release based method demonstrated below. I created a SQL Agent job based method using dbatools here. This method was created in response to a lot of feedback from DBAs like you.

Original

After we create a strategy for synchronizing our data for disaster recovery, we then need to consider keeping our server objects synchronized. Depending upon which objects you are working with there are various solutions available to you.

  • xp_help_revlogin (2005 and below, 2008 and higher) is a MS created, T-SQL based, solution for moving logins while persisting SIDs and passwords without the DBA having the know the passwords in advance.
  • SSIS Transfer Logins Task is an SSIS based version of xp_help_revlogin but it doesn’t retain the SQL account passwords for you.
  • For a more complete solution, including objects like linked server, check out this SSIS / C# solution on MSSQLTips, here.
  • Phil Factor has provided us with a PowerShell solution which I prefer, if you do not have input into your deployment processes or if a lot of ad hoc scripts and configurations end up in production.

All of these options have their merits and I have personally used the last bullet for many servers in the past. However, I prefer to keep all of the server objects in sync at deploy-time, whenever I can. Being able to interject a step into your company’s deployment process might be a dream for some of us but that is what I am recommending.

Synchronization method

The method that I recommend is a pre-deployment step. For those of you using SSDT, this would be like a pre-pre-deployment step because I am referring to an exterior process that will come before the SSDT pre-deployment script. With Availability Groups specifically, we can take advantage of the dynamic management view sys.availability_replicas to identify the replicas in the group. This is useful because your deployers can push to the primary node, or the Availability Group Listener, and your scripts will still update all nodes without any form of hard coding.

Below is a PowerShell script that I have used to deploy SQL files to the various nodes of my Availability Groups. First it will detect the nodes of the AG, and then spawn asynchronous jobs to deploy the server objects to each one.

Finding the nodes (T-SQL)

SELECT r.replica_server_name
, g.name [availability_group_name]
FROM sys.availability_replicas r
INNER JOIN sys.availability_groups g
ON g.group_id = r.group_id

PowerShell deploy

###################################################################################
### Help Context ###
###################################################################################

<#
.SYNOPSIS

.DESCRIPTION

.EXAMPLE

.NOTES

.INPUTS
[string]$serverName - "The name of one of the replicas in an Availability Group"
[string]$availabilityGroupName - "The name of the availability group that you are targeting. Optional, if there is only one group."

.OUTPUTS
None.
#>

[CmdletBinding(SupportsShouldProcess=$TRUE)]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
HelpMessage="The name of one of the replicas in an Availability Group")]
[string]$serverName,
[Parameter(Mandatory=$false,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
HelpMessage="The name of the availability group that you are targeting. Optional, if there is only one group.")]
[string]$availabilityGroupName
)

##############################################
# Set global variables
##############################################

$Error.Clear();
$currentLocation = Get-Location;
$errorLogFileName = "$currentLocation\DatabaseServerObjects-Deployment-Log.txt";
$deploymentFile = "$currentLocation\DatabaseServerObjects-Deployment.sql";
$queryTimeout = 60;
$connectionTimeout = 30;
$date = Get-Date -Format G;

##############################################
# Establish error handling
##############################################

Start-Transcript $errorLogFileName;
Write-Host "Executing scripts in $deploymentFile | begun at: $date " -foregroundcolor darkblue -backgroundcolor green;

Trap
{
# Handle the error
$err = $_.Exception
write-error $err.Message
while( $err.InnerException ) {
$err = $err.InnerException
write-error $err.Message
};
}

set-psdebug -strict
$ErrorActionPreference = "continue"

##############################################
# Initialization
##############################################

Import-Module SQLPS -DisableNameChecking;

##############################################
# Set variables & Input validation
##############################################

$connString = "Data Source=$serverName; MultiSubnetFailover=True; Integrated Security=True; Connection Timeout=$connectionTimeout";
$conn = New-Object System.Data.SqlClient.SqlConnection $connString;
$selectedReplica = New-Object Microsoft.SqlServer.Management.SMO.Server $conn;

if($selectedReplica.Databases["master"] -eq $null)
{
throw "A connection for $serverName could not be established.";
return;
}

$sqlCmd = "SELECT r.replica_server_name, g.name [availability_group_name] FROM sys.availability_replicas r INNER JOIN sys.availability_groups g ON g.group_id = r.group_id";
[System.Data.DataTable]$availabilityGroupConfiguration = $selectedReplica.Databases["master"].ExecuteWithResults($sqlCmd).Tables[0];

if($availabilityGroupConfiguration.Rows.Count -eq 0)
{
write-warning "No Availability Group found. Deploying to only the server instance inputted.";
[string]$availabilityGroup = $availabilityGroupName;
[System.Data.DataRow]$newRow = $availabilityGroupConfiguration.NewRow();
$newRow["availability_group_name"] = $availabilityGroupName;
$newRow["replica_server_name"] = $serverName;
[System.Data.DataTable]$availabilityGroupConfiguration.Rows.Add($newRow);
}
else
{
$availabilityGroup = $availabilityGroupConfiguration.availability_group_name | Get-Unique;
}

# If there is only one AG, then the object type will be System.String.
# System.Object[] indicates multiple rows, so we need to look for the AG name from the input and purge other records.
if($availabilityGroup.GetType().FullName -eq 'System.Object[]')
{
for($i = 0; $i -lt 0; $i++)
{
if($availabilityGroupConfiguration.Rows[$i].availability_group_name -ne "$availabilityGroupName")
{
$availabilityGroupConfiguration.Rows[$i].Delete();
}
}

if($availabilityGroupConfiguration.Rows.Count -eq 0)
{
throw "Could not determine Availability Group or the inputted Availability Group name was invalid.";
}

[string]$availabilityGroup = $availabilityGroupName;
}

if(($availabilityGroup -ne $availabilityGroupName) -and ($availabilityGroupName.Length -gt 0))
{
throw "Derived Availability Group does not match the inputted Availability Group name.";
}

##############################################
# Execute
##############################################

$jobs = @();
[string]$serverList = "";
foreach($replica in $availabilityGroupConfiguration)
{
$serverList += ($replica.replica_server_name);
$serverList += ", ";

[string[]]$args = @();
$args += $replica.replica_server_name;
$args += "master";
$args += $queryTimeout;
$args += $connectionTimeout;
$args += $deploymentFile;
$args += $currentLocation;

$scriptBlock = {[Environment]::CurrentDirectory = $args[5]; Invoke-Sqlcmd -ServerInstance $args[0] -Database $args[1] `
-QueryTimeout $args[2] -ConnectionTimeout $args[3] -InputFile $args[4] -Verbose }

$jobs += Start-Job -ScriptBlock $scriptBlock -ArgumentList $args;
}
$serverList = $serverList.Substring(0,$serverList.Length-2);

$jobs | Wait-Job | Receive-Job | Remove-Job;

##############################################
# Output
##############################################

$date = Get-Date -Format G;
Write-Host "The scripts in $deploymentFile have been executed on for: $serverList | completed at: $date " -foregroundcolor darkblue -backgroundcolor green;

Stop-Transcript;

if ($Error.Count -ne 0)
{
$ErrorActionPreference = "stop"
throw;
}

Create object scripts

You can see, above, that I am calling a T-SQL file named DatabaseServerObjects-Deployment.sql. This is where all of the real work happens. I like to use the SQLCMD r: command to call smaller, more manageable scripts rather than one large script. I’d start with this directory structure.

project dir tree

Each file should contain repeatable T-SQL (read more on repeatable T-SQL here and how to make AG aware SQL Agent Jobs here), such as …

USE [master]

IF EXISTS (SELECT TOP 1 1 FROM sys.server_principals WHERE name = N'LIBERTY\demo1')
AND IS_SRVROLEMEMBER ('sysadmin','LIBERTY\demo1') = 0
BEGIN
EXEC sp_addsrvrolemember @loginame = 'LIBERTY\demo1', @rolename = 'sysadmin'
END

IF EXISTS (SELECT TOP 1 1 FROM sys.server_principals WHERE name = N'LIBERTY\demo2')
AND IS_SRVROLEMEMBER ('sysadmin','LIBERTY\demo2') = 0
BEGIN
EXEC sp_addsrvrolemember @loginame = 'LIBERTY\demo2', @rolename = 'sysadmin'
END

Then, use this T-SQL script to call the various pieces.

:setvar dtm "SYSUTCDATETIME()"
:setvar dtm2012 "CONVERT(DATETIME2,'1/1/2012')"
:setvar dtm2013 "CONVERT(DATETIME2,'1/1/2013')"
:setvar userAdmin "'00000000-0000-0000-0000-000000000000'"

BEGIN
DECLARE @vServerObjectsDeploymentStartTime DATETIME = GETDATE();
PRINT 'Server Context: ' + @@SERVERNAME
PRINT
'
+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+ +-+
Begin -- Server Objects Deployment Script
+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+ +-+
'
+ CONVERT(VARCHAR(30),GETDATE(),120);

--Security
PRINT 'Executing CreateLogins.sql'
:r .\Logins\CreateLogins.sql

PRINT 'Executing ServerRoleMemberships.sql'
:r .\ServerAccess\ServerRoleMemberships.sql

PRINT 'Executing ServerPermissions.sql'
:r .\ServerAccess\ServerPermissions.sql

PRINT 'Executing SystemDatabaseUsers.sql'
:r .\SystemDatabaseObjects\SystemDatabaseUsers.sql

PRINT 'Executing SystemDatabaseRoleMemberships.sql'
:r .\SystemDatabaseObjects\SystemDatabaseRoleMemberships.SQL

--SQL Agent Operators
PRINT 'Executing DBA.sql'
:r .\SQLAgent\Operators\DBA.SQL

--Extended Event Sessions
PRINT ' Executing Successful-Logins.sql'
:r .\ExtendedEventSessions\Successful-Logins.sql

--SQL Agent Jobs
PRINT 'Executing VerifyReplicaLogins.sql'
:r .\SQLAgent\Jobs\VerifyReplicaLogins.sql

PRINT '
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+
Server Objects Deployment Script - Duration =
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+
'
+ CONVERT(VARCHAR(5),DATEDIFF(ss,@vServerObjectsDeploymentStartTime,GETDATE())) + ' seconds';
PRINT '
+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+ +-+
End -- Server Objects Deployment Script
+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+ +-+
'
+ CONVERT(VARCHAR(30),GETDATE(),120);
END

Once that is all in place, you can call the PowerShell script as a pre-deployment step every time and your entire Availability Group will be in-sync across all replicas.

This article has 2 comments

  1. did not see any .sql or .ps scripts.

Leave a Reply

%d bloggers like this: