Showing posts with label TeamCity. Show all posts
Showing posts with label TeamCity. Show all posts

Monday, October 15, 2012

Deploy .NET window service artifact to a remote server from TeamCity (Updated)



- This post is an update of my previous post. Basically, it's the same approach of 90% similar except I won't use Remote Powershell in this post. Config remote PowerShell is such a pain in the ass, I reckon. I had to setup a deployment on another box which have IIS actually but someone in my team had already removed the Default Web Site so I couldn't find any quick and easy way to make Remote Powershell work. The error message Powershell prints out is so stupid which does not even tell me what it wants.

“Set-WSManQuickConfig : The client cannot connect to the destination specified in the request. Verify that the service on the destination is running and is accepting requests. Consult the logs and documentation for the WS-Management service running on the destination, most commonly IIS or WinRM. If the destination is the WinRM service, run the following command on the destination to analyze and configure the WinRM service: “winrm quickconfig”. At line:50 char:33 + Set-WSManQuickConfig <<<< -force + CategoryInfo : InvalidOperation: (:) [Set-WSManQuickConfig], InvalidOperationException + FullyQualifiedErrorId : WsManError,Microsoft.WSMan.Management.SetWSManQuickConfigCommand“

- Ok, there is another reason to hate Microsoft. It's time to abandon PS, I tried pstools before Remote Power Shell and got other problems so I won't waste time to go back to the very old tool as Power Shell is much more power full. So writting a simple console WCF application to communitcate between TeamCity and the Remote server is my choice.

- And the tool's name is DD which is a shortname of "Distributed Deployment". In this post, I'll sum up with details how to setup deployment for a windows service from TeamCity.

- Unlike web application, a window service is often a long running process in background. A .NET windows service has an OnStop method for you to clean up resource before stopping, which is cool. HOWEVER, when you try to stop the service using "net stop servicename", it does stop the service but the process will not end as fast as it can. I reckon a .NET window service can host multiple window services which are classes inherit from ServiceBase class so it could be a reason that makes the window services manager wait a little while for all potential services within a process to stop before ending the main process.

- In some cases like mine, I want the service stop immediately when it can so I have to somehow call Environment.Exit to make the process stop asap. Apparently I cannot use TASK KILL like the previous post as it was such a hacky way and it could corrupt my data. So my approach is letting the program listen to a command, when receiving an exit signal, the app should cleanup resources and invoke Enrironment.Exit. So if you need something like this, go on reading.

I/ Things you need to prepare:

  • Remote server: aka target server, a server that we'll install the service on
  • TeamCity server: aka build server, a server that has TeamCity installed
  • 1 Project slot in Teamcity as I want to deploy whenever I click "Run" on a Teamcity deployment project instead of auto deployment after code build, so it'll cost you 1 project in 20 available slots of free TeamCity
  • Wget: this tool will download a compressed artifac from Teamcity
  • 7zip: this tool will be used to decompressed the artifac
  • DD: this tool will do 2 things: listen on a deployment command from teamcity and send exit signal to your long running window service


II/ Code involved:

1/ Assume that your service class is AwesomeService.cs, implement interface ISignalListener and add event WhenStop:
public partial class AwesomeService : ServiceBase, ISignalListener
{
    public Action WhenStop { get; set; }
    public void Exit()
    {
        Stop();
        if (WhenStop != null)
        {
            WhenStop();
        }
    }
    
    // ...
}

2/ In your service code, anywhere before service start such as Program.cs, add this code:
var svc = new AwesomeService();            
//NOTE: Wait for signal from Teamcity Deployment
Server.Start<ISignalListener>(svc, "AwesomeService");
Timer timer;
svc.WhenStop = () =>
{
    // NOTE: Will end process after 1 second
    timer = new Timer(o => Environment.Exit(0), null, 1000, Timeout.Infinite);
};
ServiceBase.Run(svc);
// ...

3/ Prepare the deploy.bat script. I like to control the deployment process in a batch file instead of implement the steps in a program as I think people will have their own steps and a batch file is simple enough to manage. Again this batch file will basically do these things:
  • Use wget to download the latest artifact from TeamCity.
  • Use 7zip to extract the artifact, copy it to a temporary folder.
  • Save the artifact to Artifacts folder
  • Backup the whole current service folder
  • Stop target window service
  • Copy over new files and old configuration files
  • Start target window service
  • Clean up

Here is the basic code that anyone can use, just keep it somewhere, we'll need to copy the batch file to RemoteServer.
@echo off
SETLOCAL
:: 0/ --------- Set some local variables
SET Environment.ExecutingFolder="C:\Deployment"
SET Environment.7zip="C:\Program Files (x86)\7-Zip\7z.exe"
SET Environment.Wget="C:\Deployment\wget.exe"

SET TeamCity.User=your-teamcity-account
SET TeamCity.Password=your-teamcity-password
SET TeamCity.BuildTypeId=teamcity-build-type
SET TeamCity.Artifact=awesomeservice.{build.number}.zip ::

SET AwesomeService.TargetFolderName=AwesomeService
SET AwesomeService.TargetFolderPath=C:\AwesomeService
SET AwesomeService.ServiceName=AwesomeService
SET AwesomeService.ImageName=AwesomeService.exe
CD /D %Environment.ExecutingFolder%
 
ECHO 1/ --------- Get latest artifact from TeamCity, AwesomeService
%Environment.Wget% -q --http-user=%TeamCity.User% --http-password=%TeamCity.Password% --auth-no-challenge http://your.teamcity.url.com/repository/download/%TeamCity.BuildTypeId%/.lastSuccessful/%TeamCity.Artifact%
REN *.zip* *.zip
ECHO Found following artifact
DIR /B *zip


ECHO 2/ --------- Extract the artifact to folder __Temp ---------------
%Environment.7zip% e -y -o__Temp *.zip


ECHO 3/ --------- Store the artifact ------------------ 
MOVE /Y *.zip Artifacts\


ECHO 4/ --------- Backup current service folder --------------- 
for %%a in (%AwesomeService.TargetFolderPath%) do set Temp.LastDate=%%~ta
SET Temp.LastDate=%Temp.LastDate:~6,4%-%Temp.LastDate:~0,2%-%Temp.LastDate:~3,2% %Temp.LastDate:~11,2%%Temp.LastDate:~14,2%%Temp.LastDate:~17,2%
ECHO Last deployment: %Temp.LastDate%
ECHO Now backup files to folder %AwesomeService.TargetFolderName%.%Temp.LastDate%
XCOPY /E /I /H /R /Y %AwesomeService.TargetFolderPath% "%AwesomeService.TargetFolderName%.%Temp.LastDate%"


ECHO 5/ --------- Stop %AwesomeService.ServiceName% service ---------------
DD AwesomeService /wait 50
ECHO Wait 2 more seconds
ping 1.1.1.1 -n 1 -w 2000 > NUL

ECHO 6/ --------- Deploy new files and copy over old configs ----------------------
ECHO ... Deploy latest assemblies
XCOPY /E /H /R /Y __Temp %AwesomeService.TargetFolderPath%

ECHO ... Deploy old configs 
COPY /Y "%AwesomeService.TargetFolderName%.%Temp.LastDate%\*.config" %AwesomeService.TargetFolderPath%

ECHO ... Delete log files 
DEL /F /Q %AwesomeService.TargetFolderPath%\Logs\log.txt* > NUL
pause

ECHO 7/ --------- Start %AwesomeService.ServiceName% service ---------------
net start %AwesomeService.ServiceName% 

ECHO 8/ --------- Cleanup --------------------------------- 
::DEL /F /Q /S *jsessionid*
RD /S /Q __Temp
ENDLOCAL

- The url to the artifac in TeamCity will look like:
http://teamcity-ip-address:80/repository/download/bt2/3907:id/service.name.1.12.1234.zip - So dedend on the build type id of your artifac, change it in the above deploy.bat

III/ Setup steps:

* On Remote Server

- Download and install 7zip
- Assume that you put all custom tools and Deploy.bat in C:\Deployment. Create folder C:\Deployment\Artifacs to store your teamcity artifacs.
The Deployment folder should look like this:
 Volume in drive C is OS
 Volume Serial Number is ABCD-EFGH

 Directory of C:\Deployment

14/10/2012  02:59 PM    <DIR>          .
14/10/2012  02:59 PM    <DIR>          ..
14/10/2012  02:58 PM    <DIR>          Artifacts
14/10/2012  02:58 PM            38,912 DD.exe
14/10/2012  02:58 PM             2,558 Deploy.bat
14/10/2012  02:58 PM           401,408 wget.exe
               4 File(s)        442,878 bytes
               3 Dir(s)  405,532,868,608 bytes free

- Run DD tool:
C:\Deployment\DD -listen 5555 -token YourS3cur!tyTok3n

- Or install it as a window service, remember to start it once installed ;)
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe /listen=5555 /token=YourS3cur!tyTok3n /i C:\Deployment\DD.exe

* On Team City Server

- Copy DD.exe and put somewhere such as C:\Program Files\DD.exe
- Add new project such as: Awesome service deployment
- Add 1 build step like this picture:

Command parameters:
-execute C:\Deployment\deploy.bat -target remoteserver-ip:5555 -token YourS3cur!tyTok3n



That's it. Your AwesomeService should be deployed whenever you click Run on the deployment project from TeamCity. Obviously you could adjust some thing in the deploy.bat to suit your needs, let me know if you have any problems.

Cheers

Wednesday, April 4, 2012

Deploy window service artifact to a remote server after a successful build on TeamCity


There is a new version of this approach: http://thoai-nguyen.blogspot.com.au/2012/10/deploy-dot-NET-window-service-TeamCity-Updated.html




Deploy window services from TeamCity is such a pain since you don't have something like WebDeploy as deploying a web application. Optopus is a product on the market to do it but I don't want to pay for this simple task. So here is the way I did.

Tool: powershell, window commands, wget, 7zip
Prerequisite: TeamCity server and the remote server have to be configured to use Remote Commands in Windows PowerShell


1/ Config both servers follow the above link. TeamCity server has to be a trusted client to the remote server and vice versa

2/ Download wget and put somewhere on the remote server such as C:\DeploymentScripts\wget.exe

3/ Download and install 7zip on remote server where you installed your windows service.

4/ Write deployment script and put somewhere on the remote server such as C:\DeploymentScripts\MyApp\deploy.bat

It can be a powershell script but I decided to use a batch file for simplicity.
Basically, the script when triggered, will
  • Use wget to download the latest artifact from TeamCity.
  • Use 7zip to extract the artifact, copy it to a temporary folder.
  • Save the artifact to Artifacts folder
  • Backup the whole current service folder
  • Stop target window service
  • Copy over new files and old configuration files
  • Start target window service
  • Clean up

Here is the script, replace some value in the script to match your requirement.
@echo off:: Deployment scripts for my window service

SETLOCAL
:: 0/ --------- Set some local variables
SET Environment.ExecutingFolder="C:\DeploymentScripts\MyApp"
SET Environment.7zip="C:\Program Files (x86)\7-Zip\7z.exe"
SET Environment.Wget="C:\DeploymentScripts\wget.exe"
SET TeamCity.User=van
SET TeamCity.Password=vanpassword
SET TeamCity.BuildTypeId=bt19
SET TeamCity.Artifact=MyAppService.{build.number}.zip
SET TeamCity.Branch=Default Branch
SET MyApp.TargetFolderName=MyApp.Service
SET MyApp.TargetFolderPath=C:\WhereMyAppInstalled\%MyApp.TargetFolderName%
SET MyApp.ServiceName=MyAppServiceName
SET MyApp.ImageName=MyApp.Service.exe
CD /D %Environment.ExecutingFolder%

 
ECHO 1/ --------- Get latest artifact from TeamCity, MyApp %TeamCity.Branch% ----------------- bt19: build type ID for MyApp on %TeamCity.Branch%
%Environment.Wget% -q --http-user=%TeamCity.User% --http-password=%TeamCity.Password% --auth-no-challenge http://my.teamcity.url.com/repository/download/%TeamCity.BuildTypeId%/.lastSuccessful/%TeamCity.Artifact%
REN *.zip* *.zip
ECHO Found following artifact
DIR /B *zip


ECHO 2/ --------- Extract the artifact to folder __Temp ---------------
%Environment.7zip% e -y -o__Temp *.zip


ECHO 3/ --------- Store the artifact ------------------ 
MOVE /Y *.zip Artifacts\


ECHO 4/ --------- Backup current service folder --------------- 
for %%a in (%MyApp.TargetFolderPath%) do set Temp.LastDate=%%~ta
SET Temp.LastDate=%Temp.LastDate:~6,4%-%Temp.LastDate:~0,2%-%Temp.LastDate:~3,2% %Temp.LastDate:~11,2%%Temp.LastDate:~14,2%%Temp.LastDate:~17,2%
ECHO Last deployment: %Temp.LastDate%
ECHO Now backup files to folder %MyApp.TargetFolderName%.%Temp.LastDate%
XCOPY /E /I /H /R /Y %MyApp.TargetFolderPath% "%MyApp.TargetFolderName%.%Temp.LastDate%"


ECHO 5/ --------- Stop %MyApp.ServiceName% service ---------------
net stop %MyApp.ServiceName%
ECHO Wait 15 seconds
ping 1.1.1.1 -n 1 -w 15000 > nul
ECHO Try to kill the process %MyApp.ImageName% if it's still running
TASKKILL /F /FI "IMAGENAME eq %MyApp.ImageName%" /T > nul

ECHO 6/ --------- Deploy new files & copy over old configs ----------------------
XCOPY /E /H /R /Y __Temp %MyApp.TargetFolderPath%
COPY /Y "%MyApp.TargetFolderName%.%Temp.LastDate%\*.config" %MyApp.TargetFolderPath%


ECHO 7/ --------- Start %MyApp.ServiceName% service ---------------
net start %MyApp.ServiceName%


ECHO 8/ --------- Cleanup --------------------------------- 
RD /S /Q __Temp
ENDLOCAL

5/ Open powershell as Administrator on TeamCity server and execute following command:
read-host -assecurestring | convertfrom-securestring | out-file C:\Users\Administrator\MyAppSecurePassword.txt

Then enter the password for the remote server. The secure password will be stored in the above file. It will then be used by TeamCity for future powershell commands against the remote server.

6/ Go to TeamCity management website, add a new project with 1 step that will execute the following powershell command:
$username = "Administrator"
$password = cat C:\Users\Administrator\MyAppSecurePassword.txt | convertto-securestring
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $password
$serverNameOrIp = "xxx.xxx.xxx.xxx"
Invoke-command -computername $serverNameOrIp -ScriptBlock {C:\DeploymentScripts\MyApp\deploy.bat} -Credential $cred

Notes:
  • Administrator is the Admin username on the remote server
  • C:\Users\Administrator\MyAppSecurePassword.txt is the secure file that store the secure password we created at step 5.
  • serverNameOrIp value has to be changed to the remote server's ip address where you installed the windows service

So after the code is checked in and successfully built, we can trigger the deployment by clicking "Run" from the above created project. The deploy.bat script above can be triggered anytime from the remote server for testing purpose. It's simple enough to read and modify when more steps need to be involved.


Cheers