Desired State Configuration SQL Always On Deployment

This DSC configuration assumes you have already created the gMSAs (group managed service accounts) with appropriate SPNs for your deployment, as well as the computer accounts used for the Windows failover cluster and the SQL always on group listener. The configuration also assumes you have a file share setup with the SQL install media and Windows SXS folders for the versions of Windows and SQL server you will be deploying.

Overview

The configuration will create a new Windows failover cluster and SQL Always On group. The primary replica configuration will ensure the existence of the failover cluster and always on group. Each secondary node will wait for the Windows cluster to be created then join as a secondary node. After the SQL configuration tasks have been completed the secondary node will wait for the Always On group to be available then configure itself as a replica.

Configuration Data

The configuration is setup to support SQL server 2016, 2017, and 2019 on Windows server 2016, and 2019. It assumes you are using gMSA accounts for the SQL service and SQL agent service. “PSDscAllowDomainUser” and “PSDscAllowPlainTextPassword” are included to simplify the example. You would ideally secure the MOF file with a certificate.

$CD = @{
    AllNodes = @(
        @{
            NodeName = '*'
            InstanceName = 'MSSQLSERVER'
            SqlVersion = "2019"
            WindowsVersion = "2019"
            SQLSysAdminAccounts = 'SQLAdminAccounts'
            Features = 'SQLENGINE,FullText,Replication'
            SQLSvcAccountName = 'gMSASvcAccount'
            AgtSvcAccountName = 'GMSAAgtAccount'
            AvailabilityGroupName = 'SQLAGN'
            AvailabilityGroupIPAddress = '192.168.1.101/255.255.255.0'
            ClusterName = 'Clustername'
            ClusterIPAddress = '192.168.1.100/24'
            FileShareWitness = '\\FileShareWitnessPath\Clustername'
            PSDscAllowDomainUser = $true
            PSDscAllowPlainTextPassword = $true
        },
        @{
            NodeName = 'TestNodeOne'
            SqlAGRole = 'PrimaryReplica'
        },
        @{
            NodeName = 'TestNodeTwo'
            SqlAGRole = 'SecondaryReplica'
        }
    )
}

Configuration Block

The configuration takes two parameters, $SqlAdministratorCredential will be used to perform all SQL specific administrative tasks $ActiveDirectoryAdministratorCredential will be used for the Windows failover cluster tasks. The SqlServerDsc and xFailOverCluster DSC resources are required for this configuration.

Configuration DSC_SQL_AG
{
    param
	(
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]$SqlAdministratorCredential,
		[Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]$ActiveDirectoryAdministratorCredential
    )
    Import-DscResource -ModuleName SqlServerDsc
    Import-DscResource -ModuleName xFailOverCluster

SQL Setup Dependencies

Two switch blocks are used to configure the unique dependencies for the the primary and secondary role SQL setup resource.

The primary replica’s SQL setup resource will have a dependency setup for the “CreateCluster” resource, while the secondary replica will have a dependency setup for the “JoinSecondNodeToCluster” resource.

The SQL version switch assumes you have setup a file share that contains the install media for each of the SQL versions you would like the configuration to be able to deploy.

# Switch statement defines role specific dependencies for SQL setup resource
Switch($Node.SqlAGRole)
{
    'PrimaryReplica'
    {
        $RoleDependsOn = '[xCluster]CreateCluster'
    }
    'SecondaryReplica'
    {
        $RoleDependsOn = '[xCluster]JoinSecondNodeToCluster'
    }
}

# Switch statement defines SQL version specific dependencies for SQL setup resource
Switch($Node.SqlVersion)
{
    "2016"
    {
        $SqlSourcePath = '\\SqlSourcePath\SQL-2016'
        $NetFramework35 = $True
        $SqlDependsOn = '[WindowsFeature]NetFramework35', '[WindowsFeature]NetFramework45', $RoleDependsOn
    }
    "2017"
    {
        $SqlSourcePath = '\\SqlSourcePath\SQL-2017'
        $NetFramework35 = $True
        $SqlDependsOn = '[WindowsFeature]NetFramework35', '[WindowsFeature]NetFramework45', $RoleDependsOn
    }
    "2019"
    {
        $SqlSourcePath = '\\SqlSourcePath\SQL-2019'
        $SqlDependsOn = '[WindowsFeature]NetFramework45', $RoleDependsOn
    }
}

Service Account Variables

Next we will configure two variables for use in the SQL resources. First we take the name of the SQL service and SQL Agent service accounts and convert them to domain\UserName format.

# Convert the gMSA account names into domain\username format for future use
$SqlSvcAccount = ('DomainName\' + $Node.SQLSvcAccountName + '$')
$SqlAgtAccount = ('DomainName\' + $Node.AgtSvcAccountName + '$')

Then we create PSCredentail objects that will be used with the SQL setup resource to define the service accounts.

If a service account username has a dollar sign at the end of the name it will be considered a Managed Service Account. Any password passed in the credential object will be ignored. Initially we used a NULL password in the PSCredential objects but we received an error indicating NULL passwords where not allowed. To get around that error we configured a fake password string to be used when creating the credential objects.

# Generate a fake password string to prevent NULL password errors during execution
$password = ('temppassword' | ConvertTo-SecureString -AsPlainText -Force)
# Create PSCredential objects for use in SQL setup resource
$SqlSvcCred = new-object -typename System.Management.Automation.PSCredential -argumentlist $SqlSvcAccount,$password
$SqlAgtCred = new-object -typename System.Management.Automation.PSCredential -argumentlist $SqlAgtAccount,$password

Windows Cluster Resources

The Windows clustering feature is added to all nodes. The primary replica will create the cluster based on the cluster name and IP address provided in the configuration data. Each of the secondary replicas will wait for the cluster to be created then attempt to join as a secondary node.

WindowsFeature AddFailoverFeature
{
    Ensure = 'Present'
    Name   = 'Failover-clustering'
}

WindowsFeature AddRemoteServerAdministrationToolsClusteringPowerShellFeature
{
    Ensure    = 'Present'
    Name      = 'RSAT-Clustering-PowerShell'
    DependsOn = '[WindowsFeature]AddFailoverFeature'
}

WindowsFeature AddRemoteServerAdministrationToolsClusteringCmdInterfaceFeature
{
    Ensure    = 'Present'
    Name      = 'RSAT-Clustering-CmdInterface'
    DependsOn = '[WindowsFeature]AddRemoteServerAdministrationToolsClusteringPowerShellFeature'
}

if ($Node.SqlAGRole -eq 'PrimaryReplica')
{
    xCluster CreateCluster
    {
        Name                          = $Node.ClusterName
        StaticIPAddress               = $Node.ClusterIPAddress
        DomainAdministratorCredential = $ActiveDirectoryAdministratorCredential
        DependsOn                     = '[WindowsFeature]AddRemoteServerAdministrationToolsClusteringCmdInterfaceFeature'
    }
    
    xClusterQuorum SetQuorumToNodeAndFileShareMajority
    {
        IsSingleInstance = 'Yes'
        Type             = 'NodeAndFileShareMajority'
        Resource         = $Node.FileShareWitness
        DependsOn        = '[xCluster]JoinSecondNodeToCluster'
    }
}

if ($Node.SqlAGRole -eq 'SecondaryReplica')
{
    xWaitForCluster WaitForClusterCreation
    {
        Name             = $Node.ClusterName
        RetryIntervalSec = 10
        RetryCount       = 60
        DependsOn        = '[WindowsFeature]AddRemoteServerAdministrationToolsClusteringCmdInterfaceFeature'
    }

    xCluster JoinSecondNodeToCluster
    {
        Name                          = $Node.ClusterName
        StaticIPAddress               = $Node.ClusterIPAddress
        DomainAdministratorCredential = $ActiveDirectoryAdministratorCredential
        DependsOn                     = '[xWaitForCluster]WaitForClusterCreation'
    }
}

SQL Server Resources

In the SQL setup resource we have defined custom directory paths for the user database, temp database, and log directories. You will need to update these fields to be relevant to your drive configuration.

SqlSetup 'InstallSqlInstance'
{
    InstanceName         = $Node.InstanceName
    Features             = $Node.Features
    SQLCollation         = 'SQL_Latin1_General_CP1_CI_AS'
    SQLSysAdminAccounts  = $Node.SQLSysAdminAccounts
    InstallSharedDir     = 'C:\Program Files\Microsoft SQL Server'
    InstallSharedWOWDir  = 'C:\Program Files (x86)\Microsoft SQL Server'
    InstanceDir          = 'C:\Program Files\Microsoft SQL Server'
    SQLUserDBDir         = 'G:\MSSQL\Data'
    SQLUserDBLogDir      = 'F:\MSSQL\Data'
    SQLTempDBDir         = 'T:\MSSQL\Data'
    SQLTempDBLogDir      = 'T:\MSSQL\Data'
    SQLBackupDir         = 'G:\SQLBackups'
    SourcePath           = $SqlSourcePath
    SQLSvcAccount        = $SqlSvcCred
    AgtSvcAccount        = $SqlAgtCred
    UpdateEnabled        = 'False'
    ForceReboot          = $false
    PsDscRunAsCredential = $SqlAdministratorCredential
    DependsOn            = $SqlDependsOn
}

After the SQL install we configure SQL user accounts and permissions in preparation for the Always On group. Next we use the SqlServerMemory resource to dynamically set the memory assigned to SQL based on the SQL Max Memory Calculator website.

SqlServerLogin Add_WindowsUserSqlSvc
{
    Ensure               = 'Present'
    Name                 = $SqlSvcAccount
    LoginType            = 'WindowsUser'
    ServerName           = 'LocalHost'
    InstanceName         = $Node.InstanceName
    PsDscRunAsCredential = $SqlAdministratorCredential
    DependsOn            = '[SqlSetup]InstallSqlInstance'
}

SqlServerLogin Add_WindowsUserSqlAgt
{
    Ensure               = 'Present'
    Name                 = $SqlAgtAccount
    LoginType            = 'WindowsUser'
    ServerName           = 'LocalHost'
    InstanceName         = $Node.InstanceName
    PsDscRunAsCredential = $SqlAdministratorCredential
    DependsOn            = '[SqlSetup]InstallSqlInstance'
}

SqlServerLogin Add_WindowsUserClusSvc
{
    Ensure               = 'Present'
    Name                 = 'NT SERVICE\ClusSvc'
    LoginType            = 'WindowsUser'
    ServerName           = 'LocalHost'
    InstanceName         = $Node.InstanceName
    PsDscRunAsCredential = $SqlAdministratorCredential
    DependsOn            = '[SqlSetup]InstallSqlInstance'
}

SqlServerPermission SQLConfigureServerPermissionSYSTEMSvc
{
    Ensure               = 'Present'
    ServerName           = 'LocalHost'
    InstanceName         = $Node.InstanceName
    Principal            = $SqlSvcAccount
    Permission           = 'AlterAnyAvailabilityGroup', 'ViewServerState', 'AlterAnyEndPoint', 'ConnectSql'
    DependsOn            = '[SqlServerLogin]Add_WindowsUserSqlSvc'

    PsDscRunAsCredential = $SqlAdministratorCredential
}

SqlServerPermission SQLConfigureServerPermissionSYSTEMAgt
{
    Ensure               = 'Present'
    ServerName           = 'LocalHost'
    InstanceName         = $Node.InstanceName
    Principal            = $SqlAgtAccount
    Permission           = 'AlterAnyAvailabilityGroup', 'ViewServerState', 'AlterAnyEndPoint', 'ConnectSql'
    DependsOn            = '[SqlServerLogin]Add_WindowsUserSqlAgt'
    PsDscRunAsCredential = $SqlAdministratorCredential
}

SqlServerPermission AddNTServiceClusSvcPermissions
{
    Ensure               = 'Present'
    ServerName           = 'LocalHost'
    InstanceName         = $Node.InstanceName
    Principal            = 'NT SERVICE\ClusSvc'
    Permission           = 'AlterAnyAvailabilityGroup', 'ViewServerState'
    DependsOn            = '[SqlServerLogin]Add_WindowsUserClusSvc'
    PsDscRunAsCredential = $SqlAdministratorCredential
}
    
SqlServerRole Add_ServerRole_AdminSqlforBI
{
    Ensure               = 'Present'
    ServerRoleName       = 'sysadmin'
    MembersToInclude     = $SqlSvcAccount, $SqlAgtAccount
    ServerName           = 'LocalHost'
    InstanceName         = $Node.InstanceName
    DependsOn            = '[SqlServerLogin]Add_WindowsUserSqlSvc', '[SqlServerLogin]Add_WindowsUserSqlAgt'
    PsDscRunAsCredential = $SqlAdministratorCredential
}

SqlServerMemory Set_SQLServerMaxMemory_ToAuto
{
    Ensure               = 'Present'
    DynamicAlloc         = $true
    ServerName           = 'LocalHost'
    InstanceName         = $Node.InstanceName
    DependsOn            = '[SqlSetup]InstallSqlInstance'
    PsDscRunAsCredential = $SqlAdministratorCredential
}

Windows Service Resources

When I first started deploying SQL with gMSA accounts I ran into an issue where the SQL service was unable to retrieve the gMSA account password when the service first attempted to start after a reboot. Based on this blog post from Wayne Sheffield I have included the following resources to reconfigure the service for delayed startup and set dependencies on the W32Time and netlogon services. This prevents the SQL service from attempting to start before the netlogon service which ensures your server will be able to retrieve the gMSA password when it attempts to start SQL.

# Configure service dependencies to prevent SQL service from trying to authenticate the gMSA accounts before the netlogon service has started.
Service SqlSvcDependencies
{
    Name = 'MSSQLSERVER'
    Ensure = 'Present'
    Dependencies = 'W32Time','Netlogon'
    DependsOn            = '[SqlSetup]InstallSqlInstance'
    PsDscRunAsCredential    = $SqlAdministratorCredential
}

Registry SqlSvcDelayAutoStart
{
    Key = 'HKLM:\SYSTEM\CurrentControlSet\Services\MSSQLSERVER'
    Ensure = 'Present'
    ValueName = 'DelayedAutoStart'
    ValueType = 'Binary'
    ValueData = '0x01'
    Force = $true
    DependsOn = '[Service]SqlSvcDependencies'
    PsDscRunAsCredential    = $SqlAdministratorCredential
}

Registry SqlAgtDelayAutoStart
{
    Key = 'HKLM:\SYSTEM\CurrentControlSet\Services\sqlserveragent'
    Ensure = 'Present'
    ValueName = 'DelayedAutoStart'
    ValueType = 'Binary'
    ValueData = '0x01'
    Force = $true
    DependsOn = '[Service]SqlSvcDependencies'
    PsDscRunAsCredential    = $SqlAdministratorCredential
}

SQL Always On Resources

Finally we get to the Always On Resources. We configure and enable an HADR endpoint on all nodes in the configuration. The primary replica node will create the Always On group and listener based on the configuration data. Each secondary node will wait for the availability group to become available then add itself as a replica.

SqlServerEndpoint HADREndpoint
{
    EndPointName         = 'HADR'
    Ensure               = 'Present'
    Port                 = 5022
    ServerName           = $Node.NodeName
    InstanceName         = $Node.InstanceName
    DependsOn            = '[SqlSetup]InstallSqlInstance'
    PsDscRunAsCredential = $SqlAdministratorCredential
}

SqlAlwaysOnService EnableHADR
{
    Ensure               = 'Present'
    InstanceName         = $Node.InstanceName
    ServerName           = $Node.NodeName
    DependsOn            = '[SqlServerEndpoint]HADREndpoint'
    PsDscRunAsCredential = $SqlAdministratorCredential
}

if ( $Node.SqlAGRole -eq 'PrimaryReplica' )
{
    SqlAG AddAG
    {
        Ensure               = 'Present'
        Name                 = $Node.AvailabilityGroupName
        InstanceName         = $Node.InstanceName
        ServerName           = $Node.NodeName
        AvailabilityMode     = 'SynchronousCommit'
        FailoverMode         = 'Automatic'
        DependsOn            = '[SqlAlwaysOnService]EnableHADR', '[SqlServerEndpoint]HADREndpoint', '[SqlServerPermission]AddNTServiceClusSvcPermissions'
        PsDscRunAsCredential = $SqlAdministratorCredential
    }

    SqlAGListener AvailabilityGroupListener
    {
        Ensure               = 'Present'
        ServerName           = $Node.NodeName
        InstanceName         = $Node.InstanceName
        AvailabilityGroup    = $Node.AvailabilityGroupName
        Name                 = $Node.AvailabilityGroupName
        IpAddress            = $Node.AvailabilityGroupIPAddress
        Port                 = 1433
        DependsOn            = '[SqlAG]AddAG'
        PsDscRunAsCredential = $SqlAdministratorCredential
    }
}

if ( $Node.SqlAGRole -eq 'SecondaryReplica' )
{
    # Wait for SQL AG to be created on primary node before attempting to join secondary node
    SqlWaitForAG SQLConfigureAGWait
    {
        Name                 = $Node.AvailabilityGroupName
        RetryIntervalSec     = 20
        RetryCount           = 30
        PsDscRunAsCredential = $SqlAdministratorCredential
    }

    SqlAGReplica AddReplica
    {
        Ensure                     = 'Present'
        Name                       = $Node.NodeName
        AvailabilityGroupName      = $Node.AvailabilityGroupName
        ServerName                 = $Node.NodeName
        InstanceName               = $Node.InstanceName
        AvailabilityMode           = 'SynchronousCommit'
        FailoverMode               = 'Automatic'
        PrimaryReplicaServerName   = ( $AllNodes | Where-Object { $_.SqlAGRole -eq 'PrimaryReplica' } ).NodeName
        PrimaryReplicaInstanceName = ( $AllNodes | Where-Object { $_.SqlAGRole -eq 'PrimaryReplica' } ).InstanceName
        DependsOn                  = '[SqlAlwaysOnService]EnableHADR', '[SqlWaitForAG]SQLConfigureAGWait'
        PsDscRunAsCredential = $SqlAdministratorCredential
    }

Here is the full configuration.