Deploying a Windows Service remotely with Powershell

As with any deployment automation, there’s a fair amount of duct tape and chicken wire involved in deploying a Windows Service remotely.

My preferred approach is to compile an installation package that contains the service files, configuration and a Powershell script installer. By “package” I really just mean a bunch of files in a folder. This folder can copied to the server and the installation script executed remotely.

Preparation: Ensuring you can run scripts remotely

To execute a Powershell script remotely you will need to have Windows Remote Management set up on your target environment. You can enable this in an elevated Powershell session using the following commandlet:

Enable-PSRemoting –Force

There’s a further tweak you’ll have to make to Windows Firewall rules. By default any access to remote management is restricted to computers on the same subnet. You can open this up by making an adjustment in Windows Firewall:

  • Open the “Windows Remote Management (HTTP-In)”rule
  • Select the “Scope” tab
  • Check “Any Address” in the “Remote IP addresses” section.

Step one: Copy the files to the remote server

There are many ways to show-horn a set of files onto a remote location, and the net use utility can be used if file sharing is enabled on the target environment. This allows you to create a temporary mapped drive using the following syntax:

net use "\\[COMPUTER]\c$" "[PASSWORD]" /USER:"[USERNAME]" /persistent:no

The errors this utility emits are fairly arcane, but the most common ones are:

  • “System error 53” – the specified path could not be found
  • “System error 86” – the username or password are incorrect
  • “System error 5” – the server is not accepting remote connections

Once your share is up then you can use the Powershell copy-item to move the files, i.e.

Copy-Item [PACKAGEPATH]\* \\[COMPUTER]\c$\installers -recurse

Although the connection will be dropped when you log off, you should try to tidy up after yourself by closing the connection explicitly once you have finished copying the files, i.e.

net use "\\$ComputerName\c$" /USER:"$Username" /delete

Step two: Run a deployment script remotely

You can use Powershell remote management to run a script on another server using the invoke-command commandlet. This does require a set of credentials which can be created from a secure string using the script below:

$pw = convertto-securestring -AsPlainText -Force -String "[PASSWORD]"
$credentials = new-object -typename System.Management.Automation.PSCredential -argumentlist "[USERNAME]",$pw

The $credentials object can then be supplied to the credentials switch as shown below:

invoke-command -ComputerName [COMPUTER] -Credential $credentials -scriptblock { [STATEMENT TO EXECUTE] }

Care should be taken with the script block element as this will execute in a new context on the remote machine. If you want it to understand any parameters then you will have to specify them explicitly using the –ArgumentList switch.

In the example below, the $ComputerName variable will be available to the remotely executed statement as $computer.

$Parameters = $ComputerName
invoke-command { param($computer) [STATEMENT TO EXECUTE] } -ArgumentList $Parameters

Step three: The deployment script

This is the script that executes in the target environment to actually deploy the service, i.e.

  • Stop the service
  • De-install the service and remove the files
  • Copy over the new service files
  • Install the service

To stop the service you’ll need to get an instance of the service from instrumentation using the Get-WmiObject commandlet. This is preferred to the more direct Get-Service commandlet because it does not throw an error if the service does not exist.

If an instance of the service is returned you can stop it directly using the stop-service commandlet. When stopping a service it’s best to force a short pause to give the service time to close down properly.

$service = Get-WmiObject -Class Win32_Service -Filter "Name = '$ServiceName'"
if ($service -ne $null) 
{
  $service1 | stop-service -Force
  Start-sleep -s 10
}

Once you’re certain that the service is closed you can remove it by calling Delete() on the service instance:

$service.Delete()

To install the service you are better off locating the installutil.exe utility that ships with the .Net framework. You could also consider shipping it with your installation package. Once located, you can install the service by passing the executable path to the utility:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe "$exePath"

Now that you can assume the service is installed you can use Get-Service to start it up:

Get-Service -Name $ServiceName -ComputerName $ComputerName | Set-Service -Status Running