Desired State Configuration #4 – SQL Server Installation und Konfiguration

So endlich ist es soweit… ich habe meine SQL Server Installation mit Powershell Desired State Configuration fertig und möchte euch nun meinen abschließenden Blog-Beitrag präsentieren. Auch wenn noch nicht alles 100% fertig ist und der Ablauf leider noch nicht so ganz so ist, wie ich es mir vorstelle, – es müssen eigentlich noch zu viele Dinge manuell gemacht werden und wie ich mit dem Thema Reboot umgehe ist noch ungeklärt – dennoch möchte ich euch hier meinen Code vorstellen.

Einleitung zur Vorgehensweise

Ich schreibe hier aktuell über Desired State Configuration als Push-Modell, ggfs kommt ein Pull-Modell später noch hinzu. Das heißt ich führe meine Skripte von meiner Workstation aus, die eigentlichen Skripte liegen auf dem Domänen-Controller, alle Server und Workstations sind in einer Domäne und ich führe die Skripte als Domänen-Administrator aus. Noch einmal zur Verdeutlichung meiner Umgebung hier ein Bild von meiner Test-Umgebung:

Die Verzeichnisstruktur liegt auf dem Domänen-Controller in der allgemein zugänglichen SYSVOL-Freigabe, kann aber natürlich auch auf jeder anderen (zentralen) Freigabe liegen. Hier liegen die Ordner für die notwendigen Powershell-Module und SQL Server Installationsmedien.

  • DC01
    • SYSVOL
      • DSC
        • SQL
          • Updates
        • Modules
        • Scripts
        • Configurations

Auf den Ziel-Servern kann man sich ebenfalls eine frei wählbare Struktur ausdenken, erst einmal geht es aber um das Kopieren von Quell- auf Ziel-Server… die Powershell-Module kopiere ich in das Standard-WindowsPowershell-Verzeichnis ( C:\Windows\system32\WindowsPowerShell\v1.0\Modules) und das SQL Server Setup liegt auf dem D-Laufwerk meiner Azure-Maschinen (D:\SQL2017).

Voraussetzungen für Desired State Configuration

Natürlich müssen auch gewisse Voraussetzungen für die Nutzung von DSC gegeben sein, ich beschränke mich hier auf die rudimentären Themen und verweise ansonsten gerne auf den ersten Teil meiner DSC-Blogpost-Serie, in dem ich auf die Details dazu eingehe. Mir ist im Zuge meiner Recherchen bzw Versuche noch ein Problem untergekommen, auf das ich kurz hinweisen möchte (kann sich aber um ein Problem speziell aus meiner Test-Umgebung handeln)… ich konnte erfolgreich die ersten 3 Server mittels DSC ausrollen, beim vierten allerdings bekam ich immer die Fehlermeldung, dass der Server nicht erreicht werden kann… auf ein Löschen/Re-Deploy des Servers (Azure sei Dank) brachte keine Besserung… hier hatte sich der DNS-Server eine falsche lokale IP-Adresse für den Server „gemerkt“ und konnte somit nicht die Namensauflösung vollbringen. Nachdem ich allerdings auf meiner Workstation den DNS-Cache gelöscht hatte, war es mir möglich auch den vierten SQL Server erfolgreich zu installieren und konfigurieren.

Also nachdem ich nun alle Server entsprechend in der Domäne habe, alle Firewalls und Dienste konfiguriert sind, können wir loslegen mit dem eigentlichen SQL Server Rollout mittels DSC. Entgegen meiner ersten Versuche (siehe Beitrag #2 zu DSC) bin ich jetzt dazu übergegangen alles mit Desired State Configuration zu machen und nur noch wenige Schritte mit reinem Powershell. Aber dazu kommen wir gleich…

In meinem aktuellen Projekt geht es um die regelmäßige Bereitstellung einer SQL Server Umgebung mit AlwaysOn AvailibilityGroups für einige Applikationen OHNE Patching. Das heißt es muss jeden Monat eine neue Umgebung ausgerollt, welche immer den letzten Patch-Level hat. Um dies zu erreichen, habe ich diese Skripte geschrieben. Damit meine Skripte mehrfach bzw modular einsetzbar sind, habe ich die Arbeitsschritte aufgeteilt:

  1. Vorbereitung, Installation und Konfiguration eines SQL Servers (2017 Developer)
  2. Migration der vorhandenen Umgebung auf die neue Umgebung
  3. Vorbereitung, Rollout und Aktivierung der AlwaysOn AvailibilityGroups

Meine Powershell Skripte

Desired-State-Configuration - Meine-Powershell-Skripte

Kurz zur Erläuterung:
Meine Hilfsskripte, welche ich im letzten Blogbeitrag schon erläutert habe

  • Connect Network Share => Erstellt eine Verbindung mit meinem Azure-Fileshare, setzt die PSGallery auf Trusted und „Unblocked“ meine Powershell-Skripte
  • Init_new_Machines => Öffnet die Windows-Firewall für die Netzwerk sowie File und Print-Dienste, formatiert alle Laufwerke, Domänen-Join
  • Remove AG and its secDB => Löscht auf beiden Servern die AGs und auf dem Secondary die verbliebene Datenbank
  • Update Modules => wie der Name sagt, es werden die notwendigen Powershell bzw DSC-Module aktualisiert.
  • 1_SQLServer-Rollout-DSC => Es erfolgt eine Standalone-Installation eines SQL Server 2017 mit Konfiguration nach Best-Practices und Installation der Ola Hallengren Maintenance Skripte
  • 2_Migrate_UserDatabases => Migration aller Datenbank-Objekte mit dbatools
  • 3_Rollout-Start-AvailGroup => Es erfolgt die Vorbereitung, Konfiguration und Start der einzurichtenden Availibilty-Groups mittels DSC und dbatools (noch… eventuell baue ich das auch auf DSC um, aber es ist so schön einfach 😉 )
  • SQLServerRollutwithAG => Ist ein Sammelskript bzw soll ein Sammelskript werden, welches alle Skripte / den Workflow entsprechend steuert
  • SQLServerConfiguration => ein JSON-File, was die Parameter für die Umgebung enthält

Part 1 – Installation und Konfiguration eines SQL Servers mit Desired State Configuration

Ich werde nicht im Detail auf das Skript eingehen und auch nicht alle Code-Zeilen zeigen, sondern „nur“ einen groben Überblick geben, wie ich vorgegangen bin und was ich womit realisiert habe…
Einleitung erfolgt mit der benötigten Powershell-Version, DSC-Modulen und Modulversionen… anschließend die Parameter, die man bei der Nutzung per Kommandozeile eingeben kann/muss.

## At the time of this writing, above modules/version were used. Future versions of this module could have breaking changes and will need to be retested
#Requires -Version 5
#Requires -Modules @{ ModuleName="xPSDesiredStateConfiguration"; ModuleVersion="8.4.0.0" }
#Requires -Modules @{ ModuleName="SqlServerDsc"; ModuleVersion="12.1.0.0" }
#Requires -Modules @{ ModuleName="SecurityPolicyDsc"; ModuleVersion="2.6.0.0" }
#Requires -Modules @{ ModuleName="StorageDSC"; ModuleVersion="4.3.0.0" }
## At the time of this writing, above modules/version were used. Future versions of this module could have breaking changes and will need to be retested

param
(
    # Computer name to install SQL Server On
    [Parameter(Mandatory=$true)]
    [String]
    $ComputerName,

    # Plaintext Password of User who has installation permissons on destination server
    [Parameter(Mandatory = $false)]
    [String]
    $InstallerPwd,

    # Plaintext Password of User who should run the sql server service => e.g. DEV2\sqlserv
    [Parameter(Mandatory = $false)]
    [String]
    $SQLServicePwd,

    # Plaintext Password of User who should run the sql Agent service => e.g. DEV2\sqlserv
    [Parameter(Mandatory = $false)]
    [String]
    $SQLAgentPwd,

    # Plaintext Password of the SA User
    [Parameter(Mandatory = $false)]
    [String]
    $SAPwd,

    # Will it be just for preparation?
    [Parameter(Mandatory=$false)]
    [Switch]
    $Prepare,

    # Will it be just for SQL Server configuration?
    [Parameter(Mandatory=$false)]
    [Switch]
    $Configure
)

Es folgen einige einleitende Zeilen, um die Konfigurationdatei einzulesen und aus JSON umzuwandeln, sowie den „OutputPath“ zu definieren. Der OutputPath ist der Ablage-Ordner für die MOF-Files (also die eigentliche Konfigurationsdatei).

Clear-Host
Write-host "Starting DSC process on"$ComputerName.ToUpper()

## Getting ConfigurationValues from JSON-File
$ConfigurationFile = "SQLServerConfiguration.json"
$Configuration = (Get-Content $ConfigurationFile) -join "`n"  | ConvertFrom-Json

## Defining path to MOF-Files
$OutputPath = $Configuration.General.DSCConfigFolder

Die Parameter werden anschließend überprüft und zu Credentials umgewandelt, damit man später sowohl Username und Passwort getrennt und als Credential verwenden kann. Und schon geht es los mit den ersten DSC-Configurations für das Bereitstellen der notwendigen Powershell-Module, der Installationsmedien und Mounten des ISO-Images . Ich habe aus Debug- und Flexibilitätsgründen mehrere Configurations gebaut um später das Skript für weitere Zwecke (wie man in den Parametern erahnen kann) zu nutzen. Man kann grundsätzlich alles in eine Configuration legen, aber um modular zu bleiben…

Vorbereitungen – Kopieren der Powershell Module und Installationsmedien

Configuration Preparation {
    Import-DscResource -ModuleName PSDesiredStateConfiguration
    
    Node $AllNodes.NodeName {
        LocalConfigurationManager {
            DebugMode = "ForceModuleImport"
            RebootNodeIfNeeded = $true
        }

        File PowershellModules {
            Ensure = 'Present'
            Type = 'Directory'
            SourcePath = $Configuration.InstallationMedia.ModuleSourcePath
            DestinationPath = $Configuration.InstallationMedia.ModuleDestinationPath
            Recurse = $true
        }

        File InstallationFolder {
            Ensure = 'Present'
            Type = 'Directory'
            SourcePath = $Configuration.InstallationMedia.InstallationMediaSourcePath
            DestinationPath = $Configuration.InstallationMedia.InstallationMediaDestinationPath
            Recurse = $true
        }

        WindowsFeature NET-Framework-Core {
            Name = "NET-Framework-Core"
            Ensure = "Present"
            IncludeAllSubFeature = $true
        }
    }
}

Configuration ISOMount {
    Import-DscResource -Module PSDesiredStateConfiguration
    Import-DscResource -ModuleName StorageDsc
    
    Node $AllNodes.NodeName {
        MountImage ISO {
            ImagePath   = $Node.SourcePath
            DriveLetter = 'S'
        }

        WaitForVolume WaitForISO {
            DriveLetter      = 'S'
            RetryIntervalSec = 5
            RetryCount       = 10
        }
    }
}

Wenn also das kopierte ISO-Image am Zielserver gemountet wurde, könnte man – wie ich es im ersten Versuch gemacht habe bzw Chris Lumnah in seinem Blogbeitrag beschrieben hat – mittels Script-Ressource realisiert, könnte man die „setup.exe“ mit der „ConfigurationFile.ini“ aufrufen und eine „unattend-Installation“ durchführen. Ich habe nach zahlreichen Versuchen und einigem Lesen mich für das SQLServerDSC-Modul entschieden, das ist „einfacher“ und nicht ganz so Fehler-anfällig.

Ausführung des eigentlichen SQL Server Setup

Configuration SQLServerSetup {
    Import-DscResource -Module PSDesiredStateConfiguration
    Import-DscResource -Module SqlServerDsc
    Import-DscResource -ModuleName StorageDsc

    Node $AllNodes.NodeName {
        LocalConfigurationManager {
            DebugMode               = "ForceModuleImport"
            RebootNodeIfNeeded      = $true
        }

        WindowsFeature NET-Framework-Core {
            Name                    = "NET-Framework-Core"
            Ensure                  = "Present"
            IncludeAllSubFeature    = $true
        }

        SQLSetup InstallDefaultInstance {
            PsDscRunAsCredential    = $InstallerCredentials
            DependsOn               = '[WindowsFeature]NET-Framework-Core'
            Action                  = "Install"
            UpdateEnabled           = $Configuration.InstallSQL.UpdateEnabled
            UpdateSource            = $Configuration.InstallSQL.UpdateSource
            Features                = $Configuration.InstallSQL.Features
            InstanceName            = $Configuration.InstallSQL.InstanceName
            SQLCollation            = $Configuration.InstallSQL.SQLCollation

            SecurityMode            = $Configuration.InstallSQL.SecurityMode
            SAPwd                   = $SQLSACredentials
            
            AgtSvcAccount           = $SQLServiceCredentials
            SQLSvcAccount           = $SQLServiceCredentials
            #ISSvcAccount           = $Node.ISSvcAccount
            #FTSvcAccount           = $Node.FTSvcAccount
            
            InstallSharedDir        = $Configuration.InstallSQL.InstallSharedDir
            InstallSharedWOWDir     = $Configuration.InstallSQL.InstallSharedWOWDir
            InstanceDir             = $Configuration.InstallSQL.InstanceDir
            InstallSQLDataDir       = $Configuration.InstallSQL.InstallSQLDataDir
            SQLUserDBDir            = $Configuration.InstallSQL.SQLUserDBDir
            SQLUserDBLogDir         = $Configuration.InstallSQL.SQLUserDBLogDir
            SQLTempDBDir            = $Configuration.InstallSQL.SQLTempDBDir
            SQLTempDBLogDir         = $Configuration.InstallSQL.SQLTempDBLogDir
            SQLBackupDir            = $Configuration.InstallSQL.SQLBackupDir

            SourcePath              = 'S:\'
        }
    }
}

Viel brauche ich dazu wohl nicht zu schreiben, da es recht selbst erklärend ist! Weiter geht es mit der eigentlichen Konfiguration des SQL Server nachdem die Installation erfolgreich durchgeführt wurde. Hier richte mich nach den gängigen Best-Practice Empfehlungen der Community zu SQL Servern, wie zum Beispiel „MaxDOP“ und den „Cost Threshold For Parallelism“ oder den maximalen Speicherbedarf des SQL Servers zu begrenzen. Auszugsweise 😉

abschließende Konfiguration des SQL Servers

SqlServerNetwork 'ChangeTcpIpOnDefaultInstance'
        {
            ...
        }
        
        SQLServerMemory SetMAXMemory {
            ...
        }

        SQLServerMaxDop SetMAXDOP {
            ...
        }

        SQLServerConfiguration 'CostThreshold4Parallelism' {
            ...
        }

        UserRightsAssignment PerformVolumeMaintenanceTasks {
            ...
        }

        UserRightsAssignment LockPagesInMemory {
            ...
        }

        SqlWindowsFirewall Create_FirewallRules_For_SQL2017 {
            ...
        }

        SqlServiceAccount SetServiceAccount_User {
            ...
        }

        SqlServiceAccount SetAgentAccount_User {
            ...
        }

Wie man die einzelnen DSC-Ressourcen einsetzt und welche Wert benötigt werden oder nur optional sind, kann man sehr gut in der Microsoft-Dokumentation (nicht alle Examples sind 100%, aber man erhält einen Eindruck) nachlesen. => https://github.com/PowerShell/SqlServerDsc
Nun fehlt nur noch das „Unmount“ des ISO-Images, danach die Schritte „Vorbereitung“, „Installation“ und „Konfiguration“ abgeschlossen. Wie in Desired State Configuration mit Powershell üblich, kommen jetzt am Ende des Skriptes noch die Zuweisungen, „Berechnungen“ und Aufrufe. Beispielsweise für MaxDOP und MaxMemory verwende ich folgende Zeilen:

## Getting CPU-Count and calculate MaxDOP
$CpuCount = 0
$Memory = Get-CimInstance Win32_PhysicalMemory -cn $ComputerName | Measure-Object -Property capacity -Sum 
$MaxMemory = [math]::round((($Memory.Sum * 0.8)/1MB),0)
$CPUCount = (Get-CimInstance Win32_Processor -cn $ComputerName -Property NumberOfLogicalProcessors).NumberOfLogicalProcessors
if ($CPUCount -eq 1) {
    $CPUCount = 1
} elseif ($CPUCount -gt 8) {
    $CPUCount = 8
} else {
    $CPUCount = $CPUCount/2
}

Erstellen der MOF-Datei und Starten der DSC-Konfiguration auf dem Zielserver

Um einen Eindruck davon zu vermitteln, wie ich die einzelnen Schritte der DSC-Installation aufrufe bzw einteile, hier noch ein Beispiel-Abschnitt aus meinem Skript:

if ($Prepare) { 
    $ConfigurationData.AllNodes += @{
        NodeName = $ComputerName
    }
    ## Create MOF-Files
    Preparation -ConfigurationData $ConfigurationData -OutputPath "$OutputPath\Preparation\"
    ## Providing all PowerShell Modules
    Start-DscConfiguration -ComputerName $ComputerName -Path "$OutputPath\Preparation" -Verbose -Wait -Force
}

Im „Prepare“-Schritt benötige ich als Parameter nur den Namen des Zielservers, erstelle mit dem Aufruf des Configuration-Teils, der Übergabe der ConfigurationData und dem „OutputPath“ eine MOF-Datei und meta.MOF-Datei. Diese MOF-Datei kann man separat erstellen, um sie dann als späteren Schritt im Skript oder wie hier direkt nach dem Erstellen auszuführen, theoretisch kann man die MOF-Datei auch mit einem eigenen Skript erstellen und mit einem zweiten Powershell-Skript einlesen und dann auf dem Zielserver ausführen. Ich starte hier die DSC-Konfiguration im direkt Anschluss an das Erstellen gegen den Zielserver aus.

SQL Server Datenbank Wartung mit Ola Hallengren

Als abschließenden Schritt in meiner langen Aufführung möchte ich die Grundlage schaffen, dass Backups und Index-Pflege mit den bekannten Skripten von Ola Hallengren erfolgen können. Da ich auch in weiteren Schritten meines Deployments – Migration und Anlage der Availibility-Groups – auf das Powershell-Community-Modul „dbatools“ nutze, greife auch hier auf die Vereinfachung dafür zurück. Wenn also eine Internet-Verbindung besteht, wird das Skript automatisch heruntergeladen und installiert, wenn nicht wird auf die lokale Kopie zurück gegriffen. (natürlich muss die Datei vorher manuell herunterladen und ablegen)

## Check if Internet is available and download Ola Hallengren Database Maintenance otherwise look into given folder
If (Test-Connection -computer "www.google.com" -count 1 -quiet) {
## Internet-Connection is available
    Write-Host -ForegroundColor Green "$(Get-Date): Connection up!"
    Write-Host "Installing OH-MaintenanceScript from the Internet"
    Install-DbaMaintenanceSolution -SqlInstance $ComputerName -BackupLocation $Configuration.InstallSQL.SQLBACKUPDIR -CleanupTime 167 -ReplaceExisting -InstallJobs
} else {
    Invoke-DbaQuery -SqlInstance $ComputerName -File $Configuration.InstallSQL.InstallationMediaSourcePath"\MaintenanceSolution.sql"
}

In den nächsten Tagen werde ich diese Reihe weiterführen und erzählen wie ich die Migration bewerkstellige und wie das Skript zur Vorbereitung und zum Rollout der Availibility-Groups aussieht. Denn auch hier gibt es das ein oder andere zu beachten.
Für Fragen und/oder Diskussionen rund um das Thema stehe ich gerne zur Verfügung!

Desired State Configuration #3.1 – Lessons learned – Powershell-Skripte

Wie ich in meinem letzten Blog-Beitrag geschrieben habe, musste ich meine DSC-Entwicklungsumgebung komplett neu aufbauen, was sehr ärgerlich war, aber mir eben auch einen Neuanfang bescherte. Da ich bereits bei meinen ersten Versuchen mich hin und wieder geärgert hatte, dass ich alles manuell gemacht habe, mussten nun ein paar Skripte gebaut werden, damit auch alles nachvollziehbar und reproduzierbar ist. Da ich mir sicher sein wollte, auch im Falle eines Re-Deployments, alles identisch aufzusetzen.

Meine Test-Umgebung in Azure

Das Aufsetzen der Maschinen in Azure ist im Grunde jedem selber überlassen, wie er es macht, hier kommt mein Ansatz um preisgünstige, identische Maschinen auszurollen, die nur aus dem Betriebssystem Windows 2016 Datacenter und 4 Datenplatten bestehen. Zusätzlich habe ich – aus Kostengründen – einen automatisierten Shutdown-Schedule eingefügt, da ich das Stoppen der Maschinen manchmal vergesse.

DSC - Overview - New Environment
Clear-Host
#
# Login into Azure and define which Subscription should be used
#
Write-Host "Defining Azure Credentials"

$azureAccountName ="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$azurePassword = ConvertTo-SecureString "XYZ" -AsPlainText -Force
$psCred = New-Object System.Management.Automation.PSCredential($azureAccountName, $azurePassword)
$mySubscriptionID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

function Login
{
    Add-AzureRmAccount -Credential $psCred -ServicePrincipal -TenantId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" | Out-null
}

Write-Host "Credentials defined"
Login
####

Set-AzureRmContext -SubscriptionId $mySubscriptionID | Out-null

# Variables for SQL Server VM
## Global
$servername = "SQLVM04"
$resourcegroupname = "RG-SQLServer-Testumgebung"

## Storage
$vm_storagename = "rgsqlservertestumgebungd"
$vm_storagesku = 'Premium_LRS'

## Network
$vm_publicIPName = $servername + '-ip'
$vm_interfacename = $servername + '-nic'
$vm_vnetname = "Demo-VNet"
$vm_subnetname = 'Standard'
$vm_VNetAddressPrefix = '10.1.0.0/16'
$vm_VNetSubnetAddressPrefix = '10.1.0.0/24'
$vm_TCPIPAllocationMethod = 'Dynamic'
$vm_domainname = $servername + 'domain'

##Compute
$vm_vmname = $servername
$vm_computername = $vm_vmname
$vm_size = 'Standard_DS1_v2'
$vm_osdiskname = $servername + '_OSDisk'

##Image
$vm_publishername = 'MicrosoftWindowsServer'
$vm_offername = 'WindowsServer'
$vm_sku = '2016-Datacenter'
$vm_version = 'latest'

Clear-Host 
$ErrorActionPreference = "Stop"

#
#   Process
#
# Start
Write-Host "Starting with 'Create Azure Virtual Machine'" -foregroundcolor "white"
Write-Host "Checking if Resourcegroup exists..." -foregroundcolor "white"

# Create a resource group
Get-AzureRmResourceGroup -Name $resourcegroupname -ev notPresent -ea 0 | out-null
if ($notPresent) {
    Write-Host $resourcegroupname "does not exist... have to create it..." -foregroundcolor "DarkYellow"
    New-AzureRmResourceGroup -Name $resourcegroupname -Location $location | Out-null
    Write-Host $resourcegroupname "successfully created" -foregroundcolor "white"
} else {
    $location = (Get-AzureRmResourceGroup -Name $resourcegroupname).Location
    Write-Host $resourcegroupname "already exists" -foregroundcolor "green"
}

# Storage
Write-Host "Checking if StorageAccount exists..." -foregroundcolor "white"
Get-AzureRmStorageAccount -ResourceGroupName $resourcegroupname -Name $vm_storagename -ev notPresent -ea 0 | out-null
if ($notPresent) {
    $StorageAccount = New-AzureRmStorageAccount -ResourceGroupName $resourcegroupname -Name $vm_storagename -SkuName $vm_storagesku -Kind "Storage" -Location $location | out-null
} else {
    Write-Host "StorageAccount" $vm_storagename "already exists" -foregroundcolor "green"
    $StorageAccount = Get-AzureRmStorageAccount -ResourceGroupName $resourcegroupname -Name $vm_storagename
}

# Network
Write-Host "Configuration of Network Objects..." -foregroundcolor "white"
Get-AzureRmVirtualNetwork -ResourceGroupName $resourcegroupname -Name $vm_vnetname -ev notPresent -ea 0 | out-null
if ($notPresent) {
    $SubnetConfig = New-AzureRmVirtualNetworkSubnetConfig -Name $vm_subnetname -AddressPrefix $vm_VNetSubnetAddressPrefix #-WarningAction SilentlyContinue | Out-Null
    $VNet = New-AzureRmVirtualNetwork -Name $vm_vnetname -ResourceGroupName $resourcegroupname -Location $location -AddressPrefix $vm_VNetAddressPrefix -Subnet $SubnetConfig #-WarningAction SilentlyContinue | Out-Null
    $PublicIp = New-AzureRmPublicIpAddress -Name $vm_interfacename -ResourceGroupName $resourcegroupname -Location $location -AllocationMethod $vm_TCPIPAllocationMethod -DomainNameLabel $vm_domainname.toLower() -WarningAction SilentlyContinue | Out-Null
    $Interface = New-AzureRmNetworkInterface -Name $vm_interfacename -ResourceGroupName $resourcegroupname -Location $location -SubnetId $VNet.Subnets[0].Id -PublicIpAddressId $PublicIp.Id #-WarningAction SilentlyContinue | Out-Null
} else {
    $VNet = Get-AzureRmVirtualNetwork -Name $vm_vnetname -ResourceGroupName $resourcegroupname 
    $SubnetConfig = Get-AzureRmVirtualNetworkSubnetConfig -VirtualNetwork $VNet -Name $vm_subnetname 
    $PublicIp = New-AzureRmPublicIpAddress -Name $vm_publicIPName -ResourceGroupName $resourcegroupname -Location $location -AllocationMethod $vm_TCPIPAllocationMethod -DomainNameLabel $vm_domainname.ToLower() -WarningAction SilentlyContinue | Out-Null
    $Interface = New-AzureRmNetworkInterface -Name $vm_interfacename.ToLower() -ResourceGroupName $resourcegroupname -Location $location -SubnetId $VNet.Subnets[0].Id -PublicIpAddressId $PublicIp.Id
}
Write-Host "Configuration of Network Objects... successfully finished" -foregroundcolor "green"

# Compute
Write-Host "Configuration of Compute Objects..." -foregroundcolor "white"
$VirtualMachine = New-AzureRmVMConfig -VMName $vm_vmname -VMSize $vm_size -WarningAction SilentlyContinue
$Credential = Get-Credential -Message "Type the name and password of the local administrator account."
$VirtualMachine = Set-AzureRmVMOperatingSystem -VM $VirtualMachine -Windows -ComputerName $vm_computername -Credential $Credential -ProvisionVMAgent -EnableAutoUpdate -WarningAction SilentlyContinue #-TimeZone = $TimeZone
$VirtualMachine = Add-AzureRmVMNetworkInterface -VM $VirtualMachine -Id $Interface.Id -WarningAction SilentlyContinue
$StorageAccount = Get-AzureRmStorageAccount -ResourceGroupName $resourcegroupname -Name $vm_storagename
$OSDiskUri = $StorageAccount.PrimaryEndpoints.Blob.ToString() + "vhds/" + $vm_osdiskname + ".vhd"
$VirtualMachine = Set-AzureRmVMOSDisk -VM $VirtualMachine -Name $vm_osdiskname -VhdUri $OSDiskUri -Caching ReadOnly -CreateOption FromImage -WarningAction SilentlyContinue

# Image
Write-Host "Configuration of Image for the VirtualMachine..." -foregroundcolor "white"
$VirtualMachine = Set-AzureRmVMSourceImage -VM $VirtualMachine -PublisherName $vm_publishername -Offer $vm_offername -Skus $vm_sku -Version $vm_version -WarningAction SilentlyContinue

# Create the VM in Azure
Write-Host "Creation of the completely configured VirtualMachine..." -foregroundcolor "white"
New-AzureRmVM -ResourceGroupName $resourcegroupname -Location $location -VM $VirtualMachine -WarningAction SilentlyContinue

# Enable Auto-Shutdown
$VM = Get-AzureRmVM -ResourceGroupName $resourcegroupname -Name $vm_vmname
$VMResourceId = $VM.Id

$Properties = @{}
$Properties.Add('status', 'Enabled')
$Properties.Add('taskType', 'ComputeVmShutdownTask')
$Properties.Add('dailyRecurrence', @{'time'= 2300})
$Properties.Add('timeZoneId', "W. Europe Standard Time")
$Properties.Add('notificationSettings', @{status='Disabled'; timeInMinutes=15})
$Properties.Add('targetResourceId', $VMResourceId)

#Error
try {
    $ScheduledShutdownResourceId = "/subscriptions/$mySubscriptionID/resourceGroups/$resourcegroupname/providers/microsoft.devtestlab/schedules/shutdown-computevm-$vm_vmname"
    Get-AzureRmResource -Location $location -ResourceId $ScheduledShutdownResourceId -ErrorAction Stop
    Set-AzureRmResource -ResourceId $ScheduledShutdownResourceId -Properties $Properties  -Force
}
catch {
    New-AzureRmResource -Location $location -ResourceId $ScheduledShutdownResourceId -Properties $Properties  -Force    
}

## Add Datadiscs
for ($i=1; $i -le 4; $i++) {
    $DiskName = "$servername-datadisk-$i"
    $VhdUri = "https://" + $vm_storagename + ".blob.core.windows.net/vhds/" + $DiskName + ".vhd"
    Add-AzureRmVMDataDisk -VM $VM -Name $DiskName -VhdUri $VhdUri -LUN $i -Caching ReadOnly -DiskSizeinGB 256 -CreateOption Empty
}

Update-AzureRmVM -ResourceGroupName $ResourceGroupName -VM $VM 

# THE END
Write-Host "Script finished!" -foregroundcolor "green"

Vielleicht kann man es einfacher machen, aber da meine Ressourcengruppe bereits besteht/bestand und ich im Falle eines „Neuaufbaus“ weder meine Entwickler-Workstation und auch nicht meinen Domänen-Controller erneuern wollte, wäre dieses Skript in der Lage entweder alles komplett neu anzulegen (inkl. Ressourcengruppe und StorageAccount) oder eben auf bereits vorhandenem aufzusetzen. Ich bin also weiterhin relativ flexibel…
Was mir noch als „Fehler“ aufgefallen ist und ich noch nicht gefixt habe… es wird zwar eine Public-IP vergeben, diese aber nicht der Maschine zugewiesen (attached)… das habe ich erst einmal über das Portal korrigiert und wenn ich die Zeit dazu finde, werde ich auch das Powershell Skript anpassen.

Kurz zu den einzelnen Maschinen

Die Azure Windows Server „Standard DS1 v2“ sind derzeit mit nur 1 vCPU und 3,75GB RAM ausgestattet, verfügen über maximal 3200 IOPS und einem 7GB großen D-Laufwerk (derzeitige Kosten pro Monat – 83,45 Euro). Diese Ausstattung reicht für meine Desired State Configuration Versuche absolut aus, da es nicht um SQL Server Workloads geht, sondern rein um das automatisierte Deployment.

Die zusätzlich angehängten Datenplatten sind auch nur einfache Standard-HDD, um die Konfiguration des SQL Server gleich „richtig“ anzupassen, d.h. die Datendateien und TransactionLogFiles sowie die TempDB entsprechend aufzuteilen. Womit wir auch schon beim nächsten Powershell-Skript sind, was mir die Neu-Erstellung der Umgebung ein wenig vereinfacht hat…

Konfiguration der Ziel-SQL Server mit Powershell

Hier gibt es eigentlich keine große „MAGIC“ aber es ist halt schneller machbar und man muss nicht alle Einstellungen in den unterschiedlichen Snap-ins manuell suchen und ändern. Zumal das Initialisieren, Formatieren und Zuweisen eines Laufwerks-Buchstaben kann unter Umständen (bei der Ausstattung meiner Server 😉 ) etwas länger dauern…

Die paar Zeilen konnte ich so aus dem Internet kopieren
Das Öffnen der Firewall um das Browsen im Netzwerk zu erlauben, ebenso um File- und Printer-Sharing zu ermöglichen kommt zum Beispiel von hier => TechNet-Forum – Windows 10 Support

  1. Network Discovery:
    netsh advfirewall firewall set rule group=”network discovery” new enable=yes
  2. File and Printer Sharing:
    netsh firewall set service type=fileandprint mode=enable profile=all

die zweite Zeile gibt einen Fehler/Hinweis aus, dass sie mittlerweile „deprecated“ ist und man doch bitte das Kommando anpassen sollte.
netsh advfirewall firewall set rule group=”File and Printer Sharing” new enable=yes

Das Formatieren und Benennen der eingehängten Datenplatten kommt von den „Scripting Guys“

Get-Disk |
Where partitionstyle -eq 'raw' |
Initialize-Disk -PartitionStyle MBR -PassThru |
New-Partition -AssignDriveLetter -UseMaximumSize |
Format-Volume -FileSystem NTFS -NewFileSystemLabel "disk2" -Confirm:$false

Einzig NewFileSystemLabel habe ich entfernt, da ich diesen Wert nicht weiter brauche. Und zu guter letzt, das „Joinen“ der Server in die SQLScripting Domäne.

Add-Computer -DomainName sqlscripting -Credential sqlscripting\admin -Restart -Force

Vorbereitung ist alles – gerade bei Automatisierung

Also musste noch ein Skript her, welches mir meine Powershell Module immer wieder aktualisiert (wenn ich es denn möchte) und diese an den relevanten Punkten im Netz bereitstellt, so dass ich damit überall arbeiten kann. So bekommt ihr auch einen Überblick welche Powershell-Module ich alle heruntergeladen habe bzw für mein DSC-Projekt benötige. Auch hilft mir dieses Skript, diese Module zu verteilen, da ich ja von den Servern aus keinen Zugriff auf das Internet habe und alternativ muss ich die Module auf meinem Internet-PC herunterladen und sie dann manuell auf meine Entwicklungs-Workstation (ggfs ohne Internet) zu kopieren.

clear-Host

## Definde Variables
$LocalModuleDir = "C:\Program Files\WindowsPowerShell\Modules"
$RemoteModuleDir = "\\dc01\SYSVOL\sqlscripting.demo.org\DSC\Modules"

Write-Host "Starting to update local modules folder and import necessary modules"

## Check if local folder exists
Write-Host "Checking if Folder exists"
if(!(Test-Path -Path $RemoteModuleDir )){
    New-Item -ItemType directory -Path $RemoteModuleDir
} else {
    Remove-Item -Path "$RemoteModuleDir\*" -Recurse -Force
    Start-Sleep 10
}

If (Test-Connection -computer "www.google.com" -count 1 -quiet) {
    ## Internet-Connection is available
    Write-Host -ForegroundColor Green "$(Get-Date): Connection up!"
    
    ## Download and save modules to folder for DSC
    Write-Host "Downloading Modules from the Internet"
    Save-Module -LiteralPath $RemoteModuleDir -Name "xPSDesiredStateConfiguration" -Repository "PSGallery"
    Save-Module -LiteralPath $RemoteModuleDir -Name "dbatools" -Repository "PSGallery"
    Save-Module -LiteralPath $RemoteModuleDir -Name "xSQLServer" -Repository "PSGallery"
    Save-Module -LiteralPath $RemoteModuleDir -Name "SQLServer" -Repository "PSGallery"
    Save-Module -LiteralPath $RemoteModuleDir -Name "SecurityPolicyDsc" -Repository "PSGallery"
    Save-Module -LiteralPath $RemoteModuleDir -Name "xPendingReboot" -Repository "PSGallery"
    Save-Module -LiteralPath $RemoteModuleDir -Name "StorageDSC" -Repository "PSGallery"

    ## Load Modules for Developement and Debugging
    Install-Module -Name "xPSDesiredStateConfiguration"
    Install-Module -Name "dbatools"
    Install-Module -Name "xSQLServer"
    Install-Module -Name "SqlServer"
    Install-Module -Name "SecurityPolicyDsc"
    Install-Module -Name "xPendingReboot"
    Install-Module -Name "StorageDSC"

} else {
    ## Internet-Connection is available
    Write-Host -ForegroundColor DarkYellow "$(Get-Date): Connection missing! Working locally!"
    
    ## Load Modules for Developement and Debugging
    Import-Module -Name "xPSDesiredStateConfiguration"
    Import-Module -Name "dbatools"
    Import-Module -Name "xSQLServer"
    Import-Module -Name "SqlServer"
    Import-Module -Name "SecurityPolicyDsc"
    Import-Module -Name "xPendingReboot"
    Import-Module -Name "StorageDSC"
}

## Copy Modules from Download-Path to Module-Path
Write-Host "Copying Modules from $RemoteModuleDir to $LocalModuleDir"
Start-Job -Name CopyModulesJob -ScriptBlock { 
    Copy-Item -Path $RemoteModuleDir -Destination $LocalModuleDir -Recurse -Force
}
Wait-Job -Name CopyModulesJob | Remove-Job

Write-Host "Module Updates successfully finished!"

Ich hoffe euch helfen meine Skripte ein wenig eure eigenen Projekte umzusetzen oder zumindest zeigen Sie euch einen Weg wie man dorthin kommen könnte. Für Fragen oder Anregungen stehe ich natürlich immer zur Verfügung bzw bin ich jederzeit offen.

Update 08. Januar 2019
Ich habe das obige Skript noch einmal angepasst, da ich für die folgende Schritte in der Automatisierung weitere Module brauche und irgendwie war mir das bei 3 Zeilen für jedes Modul einfach zu viel…
Daher habe ich die Module erst einmal in ein Array gepackt und durchlaufe das Array entsprechend. Macht die Administration des Skriptes später wesentlich einfacher 😉 ( und das Skript kürzer )

clear-Host

## Definde Variables
$LocalModuleDir = "C:\Program Files\WindowsPowerShell\Modules"
$RemoteModuleDir = "\\dc01\SYSVOL\sqlscripting.demo.org\DSC\Modules"

$NeededModules = @(
    "xPSDesiredStateConfiguration",
    "dbatools",
    "xSQLServer",
    "SqlServer",
    "SecurityPolicyDsc",
    "xPendingReboot",
    "StorageDSC",
    "NetworkingDsc",
    "xActiveDirectory",
    "ComputerManagementDsc", 
    "xFailOverCluster",
    "xSmbShare"
)

Write-Host "Starting to update local modules folder and import necessary modules"
## Check if local folder exists
Write-Host "Checking if Folder exists"
if(!(Test-Path -Path $RemoteModuleDir )){
    New-Item -ItemType directory -Path $RemoteModuleDir
} else {
    Write-Host -ForegroundColor darkyellow "Folder exists - we have to clean up! Wait a moment..."
    Remove-Item -Path "$RemoteModuleDir\*" -Recurse -Force
    Start-Sleep 10
}

If (Test-Connection -computer "www.google.com" -count 1 -quiet) {
    ## Internet-Connection is available
    Write-Host -ForegroundColor Green "$(Get-Date): Connection up!"
    Write-Host "Downloading Modules from the Internet"
    for ($i=0; $i -lt $NeededModules.length; $i++) {
        Write-Host "Saving and installing Module"$NeededModules[$i].tostring()
        ## Download and save modules to folder for DSC
        Save-Module -LiteralPath $RemoteModuleDir -Name $NeededModules[$i] -Repository "PSGallery"
        ## Load Modules for Developement and Debugging
        Install-Module -Name $NeededModules[$i] -Force
    }
} else {
    ## Internet-Connection is available
    Write-Host -ForegroundColor DarkYellow "$(Get-Date): Connection missing! Working locally!"
    ## Load Modules for Developement and Debugging
    for ($i=0; $i -lt $NeededModules.length; $i++) {
        Write-Host "just importing this - '"+$NeededModules[$i]+"'"
        Import-Module -Name $NeededModules[$i]
    }
}

## Copy Modules from Download-Path to Module-Path
Write-Host "Copying Modules from $RemoteModuleDir to $LocalModuleDir"
Start-Job -Name CopyModulesJob -ScriptBlock { 
    Copy-Item -Path $RemoteModuleDir -Destination $LocalModuleDir -Recurse -Force
}
Wait-Job -Name CopyModulesJob | Remove-Job

Write-Host "Module Updates successfully finished!"

Desired State Configuration #3 – Details zu SQL Server Installation mit Murphy

EDIT:
Falls ihr nach einer fertigen Lösung sucht, bitte geduldet euch… ich erläutere hier meinen Projekt-Verlauf, wie ich wann wie vorgegangen bin… als letzten Blogbeitrag werde ich die „fertige“ Lösung vorstellen.

Vielen Dank für euer Verständnis

Mit dem folgenden Beitrag möchte ich ein wenig in die Tiefe einsteigen, um euch zu zeigen, wie ich was wo kopiert und angepasst habe, damit mein Desired State Configuration SQL Server Rollout einwandfrei läuft. Nachdem ich in den ersten beiden Beiträgen die Vorbereitungen und Grundlagen meines Deployments erklärt und dargestellt habe, möchte ich heute auf die eigentliche Installation erläutern wie ich sie umgesetzt habe (es gibt auch einen anderen Weg, den ich später ausprobieren möchte – wie das so in Projekten ist herrscht ein wenig Termindruck 😉 )

Meine Skripte befinden sich während ich diesen Beitrag schreibe in einem Zustand, den man „Funktioniert“ nennen kann. Meine Skripte sind aktuell in der Lage von einem zentralen Server aus, auf einem (oder mehreren) Server alle notwendigen Ordner zu erstellen, die benötigten Dateien (ISO-Image, Updates, Powershell Module etc) auf den ZielServer zu kopieren, dort dann gemäß Vorgaben einen SQL Server 2017 inklusive letztem Patch-Level zu installieren und dann diesen SQL Server gemäß Best-Practice-Vorgaben zu konfigurieren.

Wie bereits im zweiten Teil der DSC Serie erläutert, habe ich mittels DSC-Configuration alle notwendigen Dateien – unter anderem das aktuelle ISO-Image des SQL Servers 2017 und das letzte Cumulative Update – auf den Zielserver kopiert. (Im Bild wurde nur noch der SOLL-Zustand kontrolliert, es waren keine Aktionen notwendig!)

Auf diesem Stand kann man aufbauen und endlich mit der eigentlichen Installation beginnen, dazu benötigen wir

  • das ISO-Image (en_sql_server_2017_developer_x64_dvd_11296168.iso)
  • das letzte Update (SQLServer2017-KB4466404-x64.exe)
  • die ConfigurationFile.ini
  • und die benötigten Powershell-Module (z.B. dbatools, SecurityPolicyDsc, SqlServerDsc usw.)

In der ConfigData.psd habe ich hinterlegt, welche Rolle bzw welche Konfigurations-Parameter der einzelne Server erhalten soll, was in meinem aktuellen Beispiel für alle Server identisch ist, aber in anderen Umgebungen unter Umständen anders sein kann.

@{
    AllNodes = @(
        @{
            NodeName        = "*"
            PSDscAllowPlainTextPassword = $true
            PSDscAllowDomainUser =$true
            SQLInstallPath     = "D:\SQL2017"
            InstanceName    = "MSSQLServer"
        },
        @{
            NodeName        = "sqlvm01"
            Role            = "SqlServer"
        },
        @{
            NodeName        = "sqlvm02"
            Role            = "SqlServer"
        }
        @{
            NodeName        = "sqlvm03"
            Role            = "SqlServer"
        }
        @{
            NodeName        = "sqlvm04"
            Role            = "SqlServer"
        }
    )
}

Murphy hat auch zugeschlagen

Aber von vorne… Auf meinen Test-Maschinen sqlvm01 und sqlvm02 habe ich „rumgespielt“, ohne wirklich zu protokollieren, was ich wie wann warum gemacht habe… also was habe ich – aus welchen Gründen – auf den beiden Maschinen lokal ausgeführt bzw durchgeführt, was hinterher dazu führte, dass meine Skripte funktionierten??? Bereits beim sqlvm03 und später auch beim sqlvm04 funktionierten meine Skripte nicht mehr wie gewünscht… Eigentlich war ich sehr zufrieden wie meine Skripte auf den ersten beiden Maschinen einwandfrei alle Dateien erhielten, dann der SQL Server 2017 installiert wurde und abschließend konfiguriert wurde mittels Desired State Configuration. Alle vier SQL Server wurden identisch in der selben Ressourcengruppe ausgerollt, konfiguriert. Folgende Aktivitäten habe ich auf allen Maschinen durchgeführt, insbesondere auf den Maschinen sqlvm03 und sqlvm04 (dort explizit nur diese Aktivitäten)

  • Network Discovery aktiviert
  • alle Platten eingebunden und formatiert
  • in die Ziel-Domäne gehängt

Und dann vom Domänen-Controller aus mittels DSC-Push die Configurations – wie zuvor beschrieben – auf die Zielserver ausgerollt… aber leider wollte die Script-Ressource nicht so wie ich… auf 01/02 konnte ohne Probleme das ISO-Image für die SQL Server Installation als Laufwerk gemountet werden, aber auf 03/04 ums verr….en nicht… Egal was ich machte, egal wie ich es auch drehte, ich konnte nie auf das Laufwerk und somit auch nicht auf die „setup.exe“ zugreifen.

Der Neubeginn – Alles zurück auf 0,25

Wie es dann manchmal auch so ist… man hat natürlich keine Sicherung von irgendwelchen Skripten (egal ob funktionierend oder nicht)… ich hatte zumindest im selben Ordner immer wieder Version-Ordner angelegt, wenn ich mit „Meilensteinen“ erfolgreich war, aber eine richtige Sicherung hatte ich nicht und schon gar nicht meine Arbeiten in eine Versionierungs-Software eingebunden. So kam es, wie es kommen musste! Ich löschte mittels Powershell den falschen Ordner und alles war weg, nur was noch in Visual Studio Code geöffnet war (3 Dateien) konnte ich retten.
Eigentlich habe ich – keine Ahnung wo der Fehler herkam – nahezu die komplette C-Platte gelöscht… kein Internet Browser mehr, kein Powershell mehr, es ging fast nichts mehr… es war halt am einfachsten den Server neu aufzusetzen (wenn man wie ich kein Backup hat) => es war ja auch nur der Domänen-Controller 😮

Dieses unschöne Ereignisse habe ich aber zum Anlass genommen, meine komplette Entwicklungsumgebung aufzuräumen und „ein wenig anders“ aufzusetzen… eigentlich ist alles beim alten geblieben, nur das ich nicht mehr auf dem Domänen-Controller selber entwickeln werde und meine Skripte nicht mehr lokal auf dem Server ablege, bei anderen Projekten bin ich bereits diesen Weg gegangen. Also einen neuen Windows Server 2016 (Standard DS1 v2) aufsetzen, die Active Directory und DNS Rolle ausrollen und konfigurieren. Zusätzlich gibt es jetzt eine Windows 10 Maschine (Standard A4m v2), auf der ich meine Skripte entwickeln werde.

aus Fehlern lernen macht stärker

Um also meinem Lernziel oder auch dem Projektziel in Sachen „Automatisierte Installation von SQL Servern mit Desired State Configuration“ wieder näher zukommen, musste ich leider wieder von vorne beginnen… diesmal aber mit einem etwas anderen Lösungsansatz. 😉
Meine erste Lessions-learned… Erstelle eine Sicherung aus einer „Blanko-Maschine“, damit man schneller auf den ursprünglichen Server zurückkommt. Meine zweite Lessions-learned… mache ansonsten alles mit Skripten (die werde ich in einem weiteren Blog-Post veröffentlichen)

Aber wieder zurück zum Theme DSC – ich bleibe bei meinen einleitenden Aktivitäten, dem Kopieren der Powershell-Module und den Installationsmedien auf den jeweiligen Zielserver, was auch geblieben ist ist das Überprüfen ob das notwendige .NET-Framework installiert ist, ansonsten wird es nachinstalliert. Diesmal orientiere ich mich ein wenig (oder auch etwas mehr) an Chris Lumnah (Blog), der anhand einer Einführung in DSC aus der Microsoft Virtual Academy und eigenem Wissen ein Script zusammen gebaut hat, welches ich adaptieren möchte, da es nicht mit der DSC-Script-Ressource arbeitet, sondern mit der „richtigen“ SQLServer-DSC-Ressource.

Mehr dann im nächsten Blog-Beitrag, da ich nun leider wieder einen Tag für das Neu-Aufsetzen bzw den Neuanfang meiner DSC-Skripte aufbringen musste…
Sorry und Danke für euer Verständnis!

Desired State Configuration #2 – Erste Schritte zur SQL Server Installation

EDIT:
Falls ihr nach einer fertigen Lösung sucht, bitte geduldet euch… ich erläutere hier meinen Projekt-Verlauf, wie ich wann wie vorgegangen bin… als letzten Blogbeitrag werde ich die „fertige“ Lösung vorstellen.

Vielen Dank für euer Verständnis

Nach meinem ersten Beitrag – den Vorbereitungen zur Nutzung von Desired State Configuration (DSC) – kann man nun die erste Schritte zur Installation eines SQL Servers einleiten. Was benötigen wir also für Voraussetzungen für die Installation eines SQL Servers auf einem neuen Windows Server?

Zu meiner Test-Umgebung… ich nutze für die Entwicklung meiner Skripte in diesem Fall eine Dev-Umgebung in Azure bestehend aus mindestens zwei Windows Servern. Ich nutze einen Server als Domänencontroller und die zweite Maschine als SQL Server, um hier meine Skripte zu testen und die Funktion zu testen. Einen oder mehrere Server kann und werde ich nach Bedarf ausrollen, um eben meine Demos zu halten bzw meine Skripte zu entwickeln und testen.

Entwicklungsumgebung für meine SQL Server Installationsversuche mit Desired State Configuration in Azure
Entwicklungs-Umgebung für Ola Hallengren und Powershell-Skripte

Voraussetzungen für die Installation

Um eine einfache SQL Server Installation – im Sinne meines Kunden-Projektes – auf einem neuen Server durchzuführen benötigen wir dreierlei Dinge:

  • .NET Framework 4.5 (Minimum)
  • Installations-Medium SQL Server 2017 Standard Edition
  • und das letzte Update im Sinne des letzten Cumulative Updates

Um das Installations-Medium auf dem neuen Server bereitzustellen, bedarf es mindestens eines Ordner auf irgendeinem Laufwerk. Nun gibt es mehrere Möglichkeiten diesen Ordner bereitzustellen…

  • manuelle Anlage des Ordners
  • Anlage des Ordners mittels DSC
  • Anlage des Ordners mittels Powershell

Da wir ins diesem Projekt, dieser Beitragsserie bestrebt sind alles zu automatisieren, fällt natürlich das manuelle Anlegen des Ordners weg. Je nach dem wie man vorgehen möchte, welche Voraussetzungen man erfüllen muss/möchte bzw in welcher Reihenfolge man vorgehen möchte, kann man nun wählen zwischen Desired State Configuration oder Powershell. In diesem Projekt möchte ich darstellen, wie man nun zur erst auf dem Zielserver ein entsprechendes Verzeichnis anlegt und dann dort mit einfachen Mitteln das Installationsmedium ablegt.

Anlage des Ordners mit Powershell

Um mein Vorhaben zu realisieren, müsste ich theoretisch erst eine Desired State-Configuration für die Folder-Anlage erstellen sowie implementieren und die Kopier-Aktion starten, dann die eigentliche Installation starten, das versuche ich erst in einem zweiten Schritt, jetzt starte ich mit der „einfacheren“ Vorgehensweise. Dazu nutze ich das „Invoke-Command“ und prüfe ob der Ordner auf dem Zielserver vorhanden ist oder nicht, wenn nicht wird der Ordner neu angelegt.

Invoke-Command -ComputerName $NodeName -ScriptBlock { 
param ($LocalInstallFolder)
if (!(Test-Path -Path $LocalInstallFolder )) {
    New-Item -ItemType directory -Path $LocalInstallFolder | Out-Null
} else {
    Remove-Item -Recurse -Force $LocalInstallFolder
    New-Item -ItemType directory -Path $LocalInstallFolder | Out-Null
} -ArgumentList "D:\$LocalInstallFolder\"

Warum lösche ich erst das Zielverzeichnis? Na klar, wenn ich das Skript mehrfach ausführe, dann muss immer das aktuelle Installationsmedium und das aktuelle Update dort bereit liegen, daher wird erst einmal das Verzeichnis gelöscht, um dann neu erstellt zu werden.
Wenn die Ziel-Verzeichnisse vorhanden sind, dann kann man die benötigeten Dateien/Verzeichnisse kopieren.

Kopieren des Installationsmediums

Anfänglich hatte ich meine Dateien alle mit „Copy-Item“ vom Netzlaufwerk auf den Zielserver kopiert, da ich aber doch recht viel mit der Powershell ISE entwickel, fehlte mir eine „Fortschrittsanzeige“… daher bin ich später auf „Start-BitsTransfer“ umgestiegen.

Write-host "Copy SQL-Image to"$NodeName.ToUpper()
$DestinationPath = "\\$NodeName\d$\$LocalInstallFolder\"
Start-BitsTransfer -Source ..\SQL\* -Destination $DestinationPath -Description "..\SQL\* will be moved to $DestinationPath" -DisplayName "Copy SQL-Image" 
Start-BitsTransfer -Source ..\SQL\Updates\* -Destination "$DestinationPath\Updates\" -Description "..\SQL\Updates\* will be moved to $DestinationPath" -DisplayName "Copy SQL-Updates" 

Aber das war mir dann irgendwie auch zu aufwendig und kompliziert, so irgendwie „Work-around“… aber zu mindestens funktionierte es 😉 Genauso bin ich vorgegangen, als die notwendigen Powershell-Module auf den Zielserver kopiert habe. Aber mit jedem Tag und jedem weiteren Versuch sich mit Desired State Configuration auseinanderzusetzen, lernt man dazu.
So bin ich jetzt dazu übergegangen auch dieses vorbereitende Kopieren mittels DSC zu realisieren.

Configuration CopyInstallationMedia
{
    Node $AllNodes.where{ $_.Role.Contains("SqlServer") }.NodeName
    {
        File InstallationFolder 
        {
            Ensure = 'Present'
            Type = 'Directory'
            SourcePath = "\\dc1\NetworkShare\SQL\"
            DestinationPath = "D:\SQL2017\"
            Recurse = $true
        }
    
        File PowershellModules 
        {
            Ensure = 'Present'
            Type = 'Directory'
            SourcePath = "\\dc1\NetworkShare\Modules\"
            DestinationPath = "C:\Windows\system32\WindowsPowerShell\v1.0\Modules\"
            Recurse = $true
        }
    }
}

Clear-Host
$OutputPath = "\\dc1\DSC-ConfigShare"
CopyInstallationMedia -OutputPath "$OutputPath\CopyInstallationMedia\"    

Start-DscConfiguration -ComputerName sqlvm02 -Path \\DC1\DSC-ConfigShare\CopyInstallationMedia -Wait -Verbose -Force

Aufbau meiner DSC-Konfigurationen

Ich habe mir zwecks besserer Übersicht und vereinfachtem, schrittweisem Ausprobieren sowie Nachvollziehbarkeit mehrere Abschnitte in meiner Desired-State-Configuration angelegt. Die jeweilige Konfiguration kann ich nun gezielt und einzeln aufrufen und „verfeinern“.

Configuration CopyInstallationMedia
{
    Node $AllNodes.where{ $_.Role.Contains("SqlServer") }.NodeName
    {
        File InstallationFolder 
        {
            ...
        }

        File PowershellModules 
        {
            ...
        }

        Configuration ConfigureSQL
        {
            ...
        }
}

Natürlich bedarf es auch einiger Parameter, die man nicht ungeachtet lassen darf, diese werden eingangs des Skriptes definiert. In meinen Fall benötige ich zur Installation und späteren Konfiguration zumindest den Pfad wo die Installationsmedien zentral abgelegt werden und den Zielserver, wo die DSC-Konfiguration ausgerollt werden soll.

param
(
    # Path where all Install media will be located
    [Parameter(Mandatory=$true)]
    [String]
    $InstallPath,

    # Computer name to install SQL Server On
    [Parameter(Mandatory=$true)]
    [String]
    $ComputerName
)

Nachdem ich nun die Konfiguration unterteilt und definiert habe, kann ich notwendige Skripte bzw Module hinzufügen und das Erstellen der MOF-Files einleiten. Anhand dieser MOF-Files wird dann die IST-Konfiguration mit der SOLL-Konfiguration abgeglichen und entsprechend korrigiert. Da sich zwischen letztmaligen Erstellen der MOF-Files und „Jetzt“ etwas an der SOLL-Konfiguration geändert haben könnte, lasse ich sicherheitshalber immer die Files neu erstellen, um diese direkt im Anschluss auf den Zielserver auszurollen. Zur näheren Erläuterung meines Skript-Abschnittes… Ich rufe die jeweilige Konfiguration auf, weise ihr eine Konfigurationsdatei hinzu und definiere einen Pfad für die Ablage der MOF-Files. Als Abschluss wird die jeweilige SOLL-Konfiguration vom zentralen Ablageort auf den Zielserver ausgerollt.
Wie man nun auch nachvollziehen kann, bin ich in der Lage die einzelnen Schritte auch separat für Debug-Zwecke auszuführen, entweder manuell nacheinander oder nur einzelne Konfigurationen wie zum Beispiel die bloße Konfiguration eines SQL Servers.

Write-host "Starting DSC process on"$NodeName.ToUpper()
Import-Module $PSScriptRoot\ConfigureSQLServer.psm1 -Force

## Create MOF-Files
$OutputPath = "\\dc1\DSC-ConfigShare"
CopyInstallationMedia -ConfigurationData \\dc1\NetworkShare\scripts\configData.psd1 -OutputPath "$OutputPath\CopyInstallationMedia\"
SQLInstall -ConfigurationData \\dc1\NetworkShare\scripts\configData.psd1 -OutputPath "$OutputPath\SQLInstall\"
ConfigureSQL -ConfigurationData \\dc1\NetworkShare\scripts\configData.psd1 -OutputPath "$OutputPath\SQLConfig\"

## Use MOF-Files to establish desired state configuration
Start-DscConfiguration -ComputerName $Computername -Path \\DC1\DSC-ConfigShare\CopyInstallationMedia -Wait -Verbose -Force     
Start-DscConfiguration -ComputerName $Computername -Path \\DC1\DSC-ConfigShare\SQLInstall -Wait -Verbose -Force     
Start-DscConfiguration -ComputerName $Computername -Path \\DC1\DSC-ConfigShare\SQLConfig -Wait -Verbose -Force

So sieht dann meine Ordner-Struktur bzw Datei-Struktur auf dem zentralen Server aus, für jede SOLL-Konfiguration gibt es einen Ordner und für jeden Zielserver eine einzelne Datei.

Mehr zum Thema Desired State Configuration natürlich in der Microsoft Dokumentation

Desired State Configuration – Einfache SQL Server Administration mit DSC

Ich hatte mich vor etwa zwei Jahren erstmalig mit DSC auseinandergestzt, um für den SQLSaturday #605 eine Demo zu zaubern, die die Konfiguragtion des SQL Servers ständig überwacht und gffs korrigiert. Jetzt habe ich wieder ein sehr schönes Kundenprojekt, bei dem ich die Wahl habe zwischen kompletter Umsetzung in Powershell (via dbatools) oder eben ein wenig DSC in Verbindung mit dbatools.

Aufgabenstellung

Auf „Knopfdruck“ sollen zwei neue SQL Server installiert und konfiguriert werden, auf denen dann mehrere Datenbanken in Basic Availibility Groups laufen. Hierzu habe ich mir also meine Azure-Test-Umgebung erweitert und möchte euch nun meinen Weg zur einer funktionierenden SQL Server Umgebung darstellen.

Folgenden Themen werde ich in den nächsten Wochen entsprechend beleuchten:

  • Vorbereitung der Server
  • Installation des SQL Servers
  • Konfiguration des SQL Servers
  • Backup/Restore Automatisierung
  • Erstellung der BAGs aus den Backups
  • Aktivierung und Abschluss der neuen Server

Voraussetzungen für DSC schaffen

Ich gehe von folgender Ausgangslage aus:

Die neuen Server sind bis Oberkante Betriebssystem installiert und konfiguriert, hängen in einer Domäne und habe das aktuelle Windows Management Framework installiert, idealerweise ist auch bereits Windows Remote Management (WinRM) installiert und aktiviert… wenn nicht überprüfen wir das und holen dies ggfs nach.

Anfänglich wollte ich auch den Server noch mit Powershell in die Domäne hängen, hätte mich aber dabei ein wenig im Kreis gedreht ohne zu einer zufriedenstellenden Lösung zu kommen… daher hier nur ein Versuch mittles Screenshot… 😉

Ok, also weiter… wie bekomme ich also auf allen Zielservern WinRM automatisch ausgerollt und konfiguriert, sowie alle dazugehörigen Firewall-Ports geöffnet… Google half mir etwas bei der Ideenfindung, aber meine Gedanken liefen auch so schon in die richtige Richtung => Domänen-Gruppen-Richtlinien (aka Group-Policies).

Dazu also in der GruppenRichtlinienVerwaltung der Domäne eine Gruppenrichtlinie erstellen, damit man auch eine ordentliche und nachvollziehbare Struktur aufbauen kann.

1.) Service einrichten

Neuen Service hinzufügen und entsprechend konfigurieren

Computer Configuration > Preferences > Control Panel Settings > Services

2.) Zugriff auf Windows Remote Shell erlauben

Zumindest dem Service den Zugriff erlauben, falls das nicht ausreichend sein sollte, auch noch der RemoteShell den Zugriff erlauben.

Computer Configuration > Policies > Administrative Templates > Windows Components > Windows Remote Management (WinRM) > WinRM Services

Nun zu den Löchern in der Firewall

3.) Firewall öffnen für winRM

Letzte Konfiguration, damit alle Server in der Domäne mittles dieser Gruppenrichtlinie konfiguriert werden, um später in der Lage zu sein über DSC (Desired State Configuration) einen SQL Server zu installieren.

Computer Configurations > Policies > Windows Settings > Security Settings > Windows Firewall and Advanced Security > Windows Firewall and Advanced Security 

Nun eine neue Inbound-Rule erstellen, damit auch Zugriffe von aussen (hier vom DomainController auf die SQL Server) ermöglicht werden.

Diese Regel wird explizit für das Windows Remote Management erstellt, um damit alle Verbindungen innerhalb der Private und Domain-Netzwerkes zu erlauben, das Public-Netzwerk wird sicherheitshalber nicht berücksichtigt.

Nun kann man diese neue Gruppen-Richtlinie auf alle Server ausrollen und sollte danach in der Lage sein, alle Server mittels DSC zu konfigurieren und administrieren.

4.) Gruppen-Richtlinien aktualisieren

Den neuen Server nun in die Domäne bringen und somit sollte der Server eigentlich die neu erstellten Richtlinien automatisch erhalten. Um aber absolut sicherzustellen, dass dem so ist kann man auch die Gruppen-Richtlinien auf dem Server selber manuell einmal aktualisieren.

Nun sollte der Konfigurator des Server mit DSC nichts mehr im Wege stehen und man kann auf dem zentralen Administrations-Server die entsprechenden Regel erstellen und ausrollen. Dazu aber mehr im nächsten Blogbeitrag 😉

Quelle: How to Enable WinRM via Group Policy

Update der Gruppen-Richtlinien aufgrund von Fehlern in der Skript-Ausführung

Nachdem ich den ersten Beitrag fertig hatte und für weitere Beiträge sowie das Kunden-Projekt recherchiert und getestet habe, musste ich leider feststellen, dass ich im späteren Verlauf des Skriptes darauf angewiesen bin mittels WMI bestimmte Werte wie RAM und CPU zu ermitteln. Dies geht nur, wenn ich eine Verbindung vom zentralen Management-Server aus zum Zielserver aufbauen kann. Hierzu fehlte leider eine Regel in der Firewall der Server, die eine eingehende und ausgehende Kommunikation mit dem RPC-Server/-Service zulässt. Also musste ich noch eine weitere Anpassung an der Gruppen-Richtlinie vornehmen.