Octopus Deploy and Powershell

Needed to deploy a pretty cool dynamic proxy called traefik, which is built on golang and can be deployed as a single binary and along with some simply configs can take a lot of difficulties of say an nginx reverse proxy out of the way.

Key points

  • Service is running on Windows so it needs to be a Windows Service (I would have preferred to use Linux and Systemd, but that option is not available at this time)
  • The golang binary for Traefik is not a service but a command line program, but you can servicify this with the Non-Sucking Service Manager (nssm).  The name is completely appropriate as writing Windows Services is not great.

Stop service and delete

$Service = Get-WmiObject -Class Win32_Service -Filter "Name='Traefik'" 
if ( $Service ) {
	$Service.StopService()
}

Get—Process "traefik windows—amd64" -ErrorAction SilentlyContinue | Stop-Process -PassThru -Force
Get—Process "traefik" -ErrorAction SilentlyContinue | Stop-Process -PassThru -Force
# Delete the service entry
$Service = Get-WmiObject -Class Win32_Service -Filter "Name='Traefik'" 
if ( $Service ) {
	$Service.Delete()
}

Install NSSM

When you install a program to be servicified by NSSM it will add a registry key that sets the NSSM path as a source for the Windows Event Log.  This means that you can stop the service, but to delete the nssm.exe file you will need to restart the EventLog service, once this is done the event log source is no longer locked and the nssm.exe file can be deleted.

$Folder = $TraefikInstallPath
if (Test-Path -Path $Folder) {
	Set-Location $Folder
	if (Test-Path -Path "nssm") {
		Start-Process "nssm" -Wait -ArgumentList "stop Traefik"
		Start-Process "nssm" —Wait -ArgumentList "remove Traefik confirm"
	}
	Set-Location 'C:\'
	Restart-Service —Name "EventLog" -Force
	Remove-Item —Path $Folder -Force —Recurse -Verbose
}

Write out configuration files from templates

Here the configuration files are stored in Octopus variables {TLS.Certificate, TLS.Key} configured to for each enviroment etc and then we will write the variable out to a file on the machines

$hostName = [Environment]::MachineName
$certFileName = '{0}.cer' —f $hostName
$keyFileName = '{0}.key' -f $hostName

Set-Location $TraefikInstallPath

$OctopusParameters["TLS.Certificate"] | Out-File —FilePath $certFileName —Force
$OctopusParameters["ILS.Key"] | Out-File —FilePath $keyFileName -Force

Write config

Again using the technique above and writing Octopus variables as templates to be written out as configuration files for Traefik to use.  

Be extremely careful to check what PowerShell writes characters out here as in pre Powershell 6 the default -f output is ucs-2 encoding which is going to make standard golang toml processing blow up.  Then you may think it is ok to specify the encoding to utf8 instead with a parameter like so -encoding utf8 which will write the output in what golang should work with ok, but now again Powershell is going to be weird because that utf8 is going to have BOM or byte order markings in the bytes and again golang blows up.   A way around all this chaos at least in Windows (arrgggh) is to specify the encoding as ascii and be done.  Now the golang toml readers of Traefik will no longer get byte confused and blow up.

$hostFullName = [System.Net.DNS]::GetHostByName($Null).HostName
$hostShortName = [Environment]::MachineName

Set-Location $TraefikInstallPath
$OctopusParameters["Traefik.StartFile"] -replace "`n`", "`r`n" | Out-File -FilePath 'traefik.toml' -Force -encoding ascii
$OctopusParameters["Traefik.ConfigFile] -f $hostFullName, $hostShortName -replace "`n`", "`r`n" | Out-File -FilePath 'config.toml' -Force -encoding ascii

Traefik example file - traefik.toml

[log]
	# level = "DEBUG"

[api]
	dashboard = true
	
[entryPoints]
	[entryPoints.websecure]
		address = ":17443"
		[entryPoints.websecure.http.tls]
		
[providers.file]
	filename = "config.toml"
	watch = true

Traefik provider config example file - config.toml

[http.routers]
	[http.routers.Router-1]
		# By default, routers listen to every entry points
		entryPoints = ["websecure"]
		rule = "PathPrefix(`/<WebAppPath>`)"
		service = "TheService"
		
[http.serversTransports.ServersTransport1]
	disableHTTP2 = true

[http.middlewares]
	[http.services.TheService.loadBalancer]
		serversTransport = "ServersTransport1"
		[[http.services.TheService.loadBalancer.servers]]
			url = "https://fully-qualified-domain-name:1234"
			
[[tls.certificates]]
	certFile = "{1}.cer"
	keyFile = "{1}.key"
	
[[tls.stores]]
	[tls.stores.default]
		[tls.stores.default.defaultCertificate]
			certFile = "{1}.cer"
			keyFile = "{1}.key"

Here need to disable HTTP2 for Windows Authentication to flow through the traefik proxy

Setup Service installation

To properly get nssm to recognize and register the program as a service you should provide the full path to exe, otherwise it will register a blank service point for nssm which means it appears as though the service is running without error, but nothing is actually running other than the nssm service with no end point

Set-Location $TraefikInstallPath

$installPath = Join-Path -Path $TraefikInstallPath -ChildPath "traefik.exe"
$installArgs = "install Traefik {0}" -f $installPath

Start—Process "nssm" —-Wait -Argumenthist $installArgs
Start—Process "nssm" -Wait —ArgumentList "start Traefik"

Set-Service -Name "Traefik" -StartupType Automatic