Azure Data Studio – Wird alles zum SQL Notebook ?

Animiert durch die Beiträge von Gianluca und Rob, sowie meiner eigenen Begeisterung, kommt hier ein Beitrag zu den neuen SQL Notebook im Azure Data Studio. Bereits mehrfach im Netz verkündet, wie zum Beispiel von Vicky Harp – als Microsoft Product Manager für u.a. das Azure Data Studio – twitterte diese Anfang März folgendes:

Das Azure Data Studio

Das Azure Data Studio ist nun die kommende, plattformübergreifende Oberfläche neben dem SQL Server Management Studio um Datenbanken zu verwalten. Das SQL Server Management Studio (SSMS) ist in der Hauptsache DAS Verwaltungstool für alle SQL relevanten Services rund die SQL Server Produkte, egal ob in der Cloud, Hybrid oder OnPrem, ABER auf einer Windows-Umgebung basierend. Mit dem Azure Data Studio gibt Microsoft auch anderen Plattformen (wie zum Beispiel dem Mac OS) die Möglichkeit eine grafische Oberfläche für die Datenbank-Administration zu bieten. Das Azure Data Studio basiert auf der gleichen Engine und UI wie Visual Studio Code und lässt sich genau so mit Marketplace-Items erweitern. Mit dem aktuellen Release (März 2019) kommt endlich auch die Unterstützung von Powershell (Powershell-Console) hinzu, ebenso wie die Verwaltungsmöglichkeit von PostgreSQL-Datenbanken (z.B. IntelliSense, Keyword completion, Code snippets)

Azure Data Studio - Powershell Console

Das neue SQL Notebook

Nein, es ist keine spezielle Hardware für den SQL Server Administrator 😉
Mit den neuen SQL Notebooks ist man nun in der Lage Abfolgen von bestimmten Statements dokumentiert zu schreiben und weiterzureichen. Jetzt werden sicherlich viele sagen, „Ja, aber das kann ich mit einer >normalen< SQL-Datei auch!“… klar, ihr habt recht! Aber mit den SQL Notebooks kann man das Ganze auch noch inklusive Ergebnis in EINER Datei speichern und vor allem „Hübscher“ 😉

Wenn man also ein neues Notebook erstellt, kann man oben auswählen, ob man einen neuen Text- oder Code-Block hinzufügen möchte. Code-Blöcke sollten immer den ausführbaren Code enthalten und Text-Blöcke entsprechend formatierbare Texte, Beschreibungen oder Anweisungen! So lassen sich sehr schön zum Beispiel Handlungsanweisungen in ein solches Notebook hinterlegen.

Denkbares Szenario für den Helpdesk

  • Kunde ruft an und meldet Problem mit dem SQL Server
  • Helpdesk entnimmt der Handlungsanweisung, dass er ein bestimmtes Notebook dem Kunden schicken soll
  • Kunde öffnet das Notebook mit dem Azure Data Studio und führt die enstprechenden Schritte gemäß Anleitung aus
  • der Kunde speichert die Ergebnis in diesem Notebook und schickt es (samt den Ergebnissen) an den Helpdesk zurück
  • Anhand der Ergebnisse kann der Helpdesk dann entscheiden was weiter erfolgen muss/kann/soll

Oder wie in meinem folgenden Beispiel kann man die Konfiguration des SQL Servers nach der Installation durchführen und dokumentieren!

Azure Data Studio - SQL Notebook

Hier gibt es bereits jetzt vielfältige Möglichkeiten und es werden mit den kommenden Updates sicherlich mehr werden, wenn man dann zum Beispiel – nachdem Powershell Code ausführbar wäre – aus einem SQL Notebook heraus auch direkt eine Migration mit dbatools durchführen oder ein Big-Data-Cluster für den SQL Server 2019 deployen… oder oder oder

Im Laufe des MVP Summits 2019 in Seattle hatte ich die Möglichkeit direkt mit Vicky und Ken (beide Product Manager) zu reden und man könne sich vorstellen, dass in Zukunft zum Beispiel (wie oben bereits von mir skizziert) der Microsoft-Support ebenfalls nur noch SQL Notebooks verschickt und man somit schneller eine Kundenanfrage oder -problem lösen kann, da die einzelnen Schritte innerhalb einer Datei mit dem jeweiligen Code dokumentiert sind. Mit der Einbindung von z.B. PanDoc lässt sich das Ergebnis bzw das SQL Notebook mit den Ergebnissen auch in ein PDF exportieren, um so aktuelle Konfigurationen zu dokumentieren und archivieren.

Viele, viele Möglichkeiten mit einem wunderbaren neuen Feature im Azure Data Studio.

Office 365 auf Azure Windows 10 – Admin-Berechtigung

Als Erinnerung für mich selber und für den einen oder anderen von euch… die Probleme haben Office 365 auf einer Windows 10 VM in Azure zu installieren. Bei meinen ersten Versuchen bin ich auch auf diesen Fehler / dieses Problem gestoßen, konnte aber leider keine wirkliche Lösung im Netz finden (hatte aber wohl auch keine Zeit und Geduld um intensiver auf die Suche zu gehen. Jetzt trat dieses Problem erneut auf und ich habe dieses Mal auf Anhieb etwas gefunden oder einfach nur die richtigen Suchbegriffe eingegeben 😉

Ausgangslage – meine virtuelle Windows 10 Maschine

Aus verschiedenen Gründen hatte ich mir in Azure eine Workstation ausgerollt, damit ich „flexibel“ bin und alles an einem Ort bearbeiten kann, ohne dabei meinen (Firmen-)Laptop nutzen zu müssen. Da ich mit dieser virtuellen Maschine nur lokale Tätigkeiten – beispielsweise Powershell-Programmierung, Powerpoint oder Erstellung von Demos – machen möchte, brauche ich keine Anbindung an das Azure Active Directory und nutze den initial eingerichteten User (mit administrativen Rechten) für meinen Login bzw meine Arbeiten.

Nun brauche ich aber für meine Vorträge eine aktuelle Powerpoint-Umgebung, um alle Vorteile nutzen zu können und eine durchdachte und optisch ansprechende Präsentation zu erstellen. Und dank meines MVPs habe ich auch die Möglichkeit auf mehreren Maschinen Office 365 zu installieren. Also habe ich das Office 365 Click-to-Run Installations-Packet heruntergeladen und versucht zu installieren… erhielt aber – egal was ich versuchte obige Fehlermeldung. Also musste ich nach einer Lösung suchen, da ich mir nicht vorstellen konnte, dass das so von Microsoft gewollt ist…

verschiedene Lösungsansätze zur Installation von Office 365

Bei meiner Suche nach einer Lösung bin ich heute morgen über einen Beitrag in der Microsoft Dynamics Community gestolpert und habe dort erste weiterführende Informationen erhalten. Man könne das Problem für diese „einmalige“ Problem relativ schnell und leicht umgehen, in dem man einfach einen neuen User mit Administrator-Rechten anlegt und diesen dann zur Installation der Software nutzt. Ausprobiert habe ich dies nicht, da ich eine „dauerhafte“ Lösung wollte und auch wissen wollte, wie ich das zukünftig auf allen Maschinen umgehen kann. Ich suchte also nach einer Lösung und keinem „Work-around“… 😉

Also weiterlesen in dem Beitrag… im Text versteckt gibt es einen weiteren Link zu einer Seite, die noch mehr Lösungen beinhaltet, darunter auch „dauerhafte“ Lösungen! Also weiterlesen…

Das Problem trat bei mir nur auf, wenn ich Office 365 installieren wollte, aber anscheinend tritt das auch bei der Installation weiterer Software auf und nicht nur in Azure sondern auch on-premise…

Nun kann mal beispielsweise über die UAC (User Account Control) eine Stufe herunterschalten, um dem Problem aus dem Weg zu gehen, aber in gewissen Umgebungen mag dies nicht möglich sein, ich fand den Lösungsweg 2 wesentlich charmanter und sicherer…

Method 2: Enable Admin Approval Mode for the Built-in Administrator Account

Das Folgende habe ich aus dem Blogbeitrag kopiert und übersetzt.

  1.  Über Win+R den Ausführen-Dialog öffnen
  2. secpol.msc eingeben und mit OK bestätigen.
  3. Dann im Baum links die folgenden Ordner öffnen
    • Local Policies => Security Options
  4. Im rechten Fenster User Account Control: Admin Approval Mode for the Built-in Administrator account suchen. Über einen Rechts-Klick und dann auf Eigenschaften.
  5. Die Option Enabled auswählen. Mit Apply und dann OK bestätigen.
  6. Erneut über Win+R den Ausführen-Dialog öffnen und regedit eingeben und ausführen.
  7. Dann den folgenden Pfad suchen :HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\UIPI . Auf der rechten Seite über einen Rechts-Klick auf „Default“ und dann „Modify“ den Wert ändern.
  8. Einzugebender Wert is : 0x00000001(1)
  9. Die Änderungen mit einem Klick auf den OK Button schliessen.

Nicht zu vergessen, danach zu booten, damit die Werte einwandfrei übernommen werden… ich konnte direkt im Anschluss einwandfrei mein Office 365 Paket installieren und meine Präsentationen für die SQL-Konferenz in Darmstadt vorbereiten 😉

(Werbung)

Wir sehen uns auf der SQL-Konferenz ansonsten vielen Dank für das Lesen dieses Beitrages und ich hoffe wie immer, zumindest einem geholfen zu haben !!!

SQLSaturday Linz 2019

Am Freitag, den 17. Jänner war es wieder soweit… die Microsoft Data-Platform Community Österreich traf sich zu einer neuen Ausgabe des SQLSaturday (der in Österreich immer ein Freitag ist 😉 )
Wolfgang Strasser (b|t) und Markus Ehrenmüller (b|t) (und deren OrganistationsTeam) hatten erneut eingeladen, diesmal allerdings nicht wie in der Vorjahren nach Wien, sondern nach Linz in Oberösterreich. Ich hatte wieder mehrere Vorträge eingereicht und wurde mit dem aktuellen Thema „Azure SQL Database – Managed Instance“ ausgewählt und durfte somit für zwei Tage nach Linz reisen. Es war mir auf jeden Fall eine große Freude ein Teil der internationalen DataPlatform-Community zu sein (aka #sqlfamily)!

Linz Sehenswürdigkeiten - SQLSaturday Linz #810 2019

Während den ersten Gedanken und Gesprächen zum SQL-Saturday in Linz, also wie kommt man da hin, welches Hotel sollte gebucht werden, wo ist der Veranstaltungsort, musste ich dann schon mal feststellen, dass dies nicht so einfach werden würde – zumindest von Hamburg aus…
Egal, was tut man nicht alles für die Community und um Freunde wieder zusehen?!

Ich habe mich dann an meine Lieblings-Fluggesellschaft mit Start in HAMburg und Ziel in LiNZ… und siehe da es gibt tatsächlich Verbindungen nach Linz… was ich aber im ersten Eifer übersehen habe, war die Tatsache das es sich um „Fly&Rail“ handelte… Also führte mich meine Anreise von Hamburg nach Wien und direkt von Wiener Flughafen Tiefbahnhof zum Linzer Hauptbahnhof. Fehlte noch eine Unterkunft die preislich in mein Budget passt… die Hotels im näheren Umfeld um den Veranstaltungsort „Tabakfabrik“ waren entweder ausgebucht oder in Sachen Preis/Leistung nicht wirklich passend. Also habe ich mich wieder auf meine Anfänge besonnen (klingt irgendwie blöd, aber „isso“)… Airbnb lässt grüßen… ich habe bei meiner Suche eine kleine Wohnung in der Altstadt gefunden, die sich sehr gut anhörte und von den Wegen her alles irgendwie innerhalb von 20 Minuten Fussweg erreichbar machte… was wünscht man sich mehr, vor allem wenn der Preis im Vergleich zum Hotel rund 60% niedriger liegt.

Sightseeing in Linz und das Thema Schnee

Zum Zeitpunkt des SQLSaturdays in Linz litten der Süden Deutschland und Teile von Österreich unter einer extremen Schneelast und man hätte meinen können, dass ganz Österreich zumindest ein wenig weiß sei… aber leider weit gefehlt (oder vielleicht als Reisender auch besser so 😉 ). Die Linzer Innenstadt war in keinster Weise weiß, auch die umliegenden Hügel waren nahezu grün. Durch die warme Witterung in Linz hatte ich aber die Möglichkeit mir die Stadt ein wenig anzuschauen sowie die schönen Gebäude und Gassen. Eine wirklich schöne Stadt mit einer interessanten Geschichte.

SQLSaturday Linz - Linzer Dom

Der SQLSaturday ( der wie immer auf einem Freitag war) wurde in einer alten Tabakfabrik veranstaltet, das Gebäude wird von zahlreichen Startup-Unternehmen als Firmenbüro und Produktionsstätte genutzt. Die Lokalität war etwas besonders im Vergleich zu den mir bisher bekannten Veranstaltungsorten, da sie sehr lang gezogen war und eher unübersichtlich. Die technische Einrichtung für die Vorträge war aber hervorragend geeignet und sowohl Veranstaltungsort als auch das Orga-Team taten ihr Bestes, diesen Tag zu etwas großen werden zu lassen. Einzig die Kapazitäten der Kaffeemaschinen wurden bis ans Limit ausgereizt 😉

Mein Vortrag – Azure SQL Managed Instance

Natürlich gab es wieder eine Vielzahl verschiedener Vorträge aus allen Beireichen der Data-Plattform von internationalen Sprechern vorgetragen! Genau diese Mischung macht es so interessant solche Veranstaltungen zu besuchen… ich bin einfach von der Mischung und der Gemeinschaft fasziniert. Man trifft alte Bekannte wieder, aber auch neue Bekannte, die man bisher nur aus den sozialen Medien kannte. Der Hashtag #SQLFamily ist nicht nur ein Schlagwort, sondern wird auch im „Real Life“ so gelebt. Es macht einfach Spaß ein Teil dieser Community zu sein!

In Linz habe ich erstmal die „kurze“ Version meines Vortrages zur „Azure SQL Managed Instance“ gehalten und hatte eine kleine, aber interessierte Zuhörerschar. Alles hat wie gewünscht funktioniert, alle Fragen konnte ich einigermaßen beantworten. Und durch die Interaktion mit den Zuhörern neue Aspekte kennenlernen und ebenfalls etwas lernen. Ich hatte auf jeden Fall viel Spaß und werde diese Veranstaltung auch im kommenden Jahr besuchen bzw weiter empfehlen!

Azure SQL Database - Managed Instance

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:

Powershell Desired State Configuration - Overview - New Environment

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…

Powershell - Einbinden der Laufwerke - Freischalten der Firewall - Einbinden in die Domäne

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!"