Today I want to share how I’m keeping a copy of instances’ configurations using dbatools.
Chrissy LeMaire (B | T) wrote about it before on the Simplifying disaster recovery with dbatools blog post.
In this post, I will add one step and save the output on a GIT repository.
Pre-requirements
- You need a GIT repository
- GIT tools installed on the server where you are running the script so you can commit your changes
- dbatools
- A list or a place to get all instances that you want to run the export
Preparation
Git repository
Clone your repository to a location where dbatools can write to.
NOTE: To fully automate this process, I recommend making use of an access token (github | gitlab documentation as examples) instead of user/password as we don’t want to be asked for the password when committing the changes.
The list of instances from where we will export the configurations
I’m using a central database with a table that contains my list of servers.
I’m using the dbatools’ command Invoke-DbaQuery
to get that list.
# Where we will get the list of servers $centralServer = "centralServer" $centralDatabase = "centralDatabase" $query = "SELECT ConnString FROM <table/view>" # Get the list of servers $ServerList = Invoke-DbaQuery -SqlInstance $centralServer -Database $centralDatabase -Query $query | Select-Object -ExpandProperty ConnString
Running dbatools’ Export-DbaInstance command
A quick walk-through in case you have never used this command before.
Execution
If you have never used this command, you can test for a single instance by running the following:
Export-DbaInstance -SqlInstance "devInstance" -Path "D:temp"
This will create all scripts in the D:temp
folder. A folder named “devInstance-{date}” will be created.
In this folder, you will find 1 file per ‘object type’. The file names are in the form of “#-.sql” where the # is a number that represents the iterator on the order that the internal calls of the underlying functions happen.
Heads up
This means that if we call it with no exclusions but then we call it again but with -Exclude SpConfigure
the scripts names will be different.
For the first case, we will have a 1-sp_configure.sql
but for the second the number 1 will appear as 1-customerrors.sql
. This isn’t a problem when exporting on demand and/or occasionally, but if we want to leverage on GIT to track the differences this can be confusing.
Let’s keep this in mind and I will explain later how to avoid this.
“This also exports credentials, linked servers and Logins, right? What about the passwords?”
Good point! We can export objects that deal with passwords. Do you want to save them in clear text?
Maybe, maybe not. It’s up to you. Here I will share a version where clear-text passwords are excluded from the exported scripts regarding credentials and linked servers, but I will keep the hashed password for the logins.
How does that works?
Introducing the -ExcludePassword
parameter, as mentioned on the documentation (don’t forget to use and abuse Get-Help
):
If this switch is used, the scripts will not include passwords for Credentials, LinkedServers or Logins.
Just add -ExcludePassword
like this:
Export-DbaInstance -SqlInstance "devInstance" -Path "D:temp" -ExcludePassword
If you run with this switch and if open the scripts, you will see that for:
– Logins: No hashed password is present
– Credentials & LinkedServers will have their clear text passwords replaced by ‘EnterStrongPasswordHere’ and ‘#####’ respectively.
GIT commands I’m using
Here are the 4 git commands that I’m using:
– git pull
-> To make sure I have the most recent version of the repository on my local folder
– git add .
-> Will stage all changes for the next commit
– git commit -m"some message"
-> Will do the commit of the changes with a specific message
– git push
-> Will push the changes to the central repository
The first one is run before triggering the Export-DbaInstance
and the rest only after all the other steps finish.
A couple of notes before showing the full script
1 – When running the command, I use a temp
folder for the -Path
parameter (you will understand why in a second). I have added this folder to my .gitignore
the file inside Instances
folder so it won’t be synchronized.
2 – Do you remember the “Heads up” I have done earlier in the post about the outputted files’ names? Let’s nail that one.
GIT is great to keep track of the changes that happened on a file. However, for that to happen, we need to make sure that the file name is the same. Because of the example I have mentioned before, my workaround goes by some renaming convention.
Files’ names
After the export command finish and before committing the changes to our GIT repository I run the following command:
# Find .sql files where name starts with an number and rename files to exclude numeric part "#-<NAME>.sql" (remove the "#-") Get-ChildItem -Path $tempPath -Recurse -Filter "*.sql" | Where {$_.Name -match '^[0-9]+.*'} | Foreach-Object {Rename-Item -Path $_.FullName -NewName $($_ -split '-')[1] -Force}
The $tempPath
represents my main folder where all the exported folders will be created and within these folders, we will have our scripts (hence the -Recurse
parameter).
1 – We are getting (Get-ChildItem
) on all folders and sub-folders (-Recurse
) all files with extension .sql
(-Filter
).
2 – We filter the results to only get files whose names start with one or more digits ($_.Name -match '^[0-9]+.*'
)
3 – Foreach-Object
file we have found we rename it by splitting the file name by the ‘-‘ char and using the second part of the result of the split [1]
([0]
will contain the number)
Folders’ names
Using the same logic, we remove the suffix “-date” from the folder’s name.
# Remove the suffix "-datetime" Get-ChildItem -Path $tempPath | Foreach-Object {Rename-Item -Path $_.FullName -NewName $_.Name.Substring(0, $_.Name.LastIndexOf('-')) -Force}
In this case, I have decided to use the Substring
method along with the LastIndexOf('-')
because the ‘-‘ char is a valid character to use as an instance name.
NOTE: We can use the -split
method anyway but we will need then to join all the occurrences excluding the last one. This way you see two different ways to accomplish the same result.
#Example with '-split' and '-join' $folderName = "SQL-SERVER-01-20200602" $split = $folderName -split '-' $split[0..($split.Count-2)] -join '-'
Move folder with the files from the temp folder to the final folder
The final PowerShell steps before we commit the changes are, after renaming the folder and its files, move them and overwrite on the repository folder and clean-up our temp
folder
# Copy the folders/files from the temp directory to one level up (overwrite) Copy-Item -Path "$tempPath*" -Destination $instancesPath -Recurse -Force # Clean-up temp folder Get-ChildItem $tempPath | Remove-Item -Force -Recurse -Confirm:$false
Because my temp folder exists as a sub-folder of my repository my -Destination
parameter is getting the parent folder to replace the existing files.
The full script
Here is the full script.
Copy, save the script within your repository folder and change the following variables:
Line 2, 3 and 4.
Line 7: If your column is not named as ConnString
(what are the odds?) you also need to change the end of this line.
Line 31: Use Get-Help Export-DbaInstance -Parameter Exclude
and decide what you want to exclude if any.
# Where we will get the list of servers $centralServer = "centralServer" $centralDatabase = "centralDatabase" $query = "SELECT ConnString FROM <table>" # Get the list of servers $ServerList = Invoke-DbaQuery -SqlInstance $centralServer -Database $centralDatabase -Query $query | Select-Object -ExpandProperty ConnString $instancesPath = "$PSScriptRootInstances" $tempPath = "$instancesPathtemp" # Change location to be able to run GIT commands on the local repository Set-Location -Path $PSScriptRoot # get folder up-to-date git pull # Create/clear temp folder if (Test-Path -Path $tempPath) { # Clean the folder Get-ChildItem $tempPath | Remove-Item -Force -Recurse -Confirm:$false } else { $null = New-Item -Path $tempPath -ItemType Directory } <# Databases -> Exclude databases will not script the RESTORE statements for last backup. We don't need this because we use a 3rd party tool and this was slowing down the execution PolicyManagement and ReplicationSettings -> We don't use Credentials and LinkedServers -> We script as a second step to hide passwords (because -ExcludePassword will also hide hashed ones from logins, and this we want to keep) #> $excludeObjects = "Databases", "PolicyManagement", "ReplicationSettings", "Credentials", "LinkedServers" foreach($server in $ServerList) { # Run the export and get a collection of files generated $outputDirectory = Export-DbaInstance -SqlInstance $server -Path $tempPath -Exclude $excludeObjects -NoPrefix # Extract the directory full path of the export to use next $instanceOutDir = $outputDirectory.Directory | Select-Object -ExpandProperty FullName -Unique # Export credentials and LinkedServers but excluding the password. Output to same folder Export-DbaCredential -SqlInstance $server -FilePath "$instanceOutDirCredentials.sql" -ExcludePassword Export-DbaLinkedServer -SqlInstance $server -FilePath "$instanceOutDirLinkedServers.sql" -ExcludePassword } # Find .sql files where the name starts with a number and rename files to exclude numeric part "#-<NAME>.sql" (remove the "#-") Get-ChildItem -Path $tempPath -Recurse -Filter "*.sql" | Where {$_.Name -match '^[0-9]+.*'} | Foreach-Object {Rename-Item -Path $_.FullName -NewName $($_ -split '-')[1] -Force} # Remove the suffix "-datetime" Get-ChildItem -Path $tempPath | Foreach-Object {Rename-Item -Path $_.FullName -NewName $_.Name.Substring(0, $_.Name.LastIndexOf('-')) -Force} # Copy the folders/files from the temp directory to one level up (overwrite) Copy-Item -Path "$tempPath*" -Destination $instancesPath -Recurse -Force # Clean-up temp folder Get-ChildItem $tempPath | Remove-Item -Force -Recurse -Confirm:$false # Add/commit/push the changes git add . git commit -m "Export-DbaInstance @ $((Get-Date).ToString("yyyyMMdd-HHmmss"))" git push
Example of the output for one of the instances
Summary
We have seen how to leverage Export-DbaInstance
dbatools’ command to export instance’s configuration as backup and/or for disaster recovery purposes.
On top of that, I have shown how you can format the results so you can add it to GIT and track the changes.
I hope this gives you, at least, a good starting point to implement backups over the time of your instances configurations.
Adjust for your needs, test, and keep it running.
In the second and final part of this process, we will implement parallelism to lower down our execution times.
Stay tuned.
Thanks for reading!