TU Tran

Technologies should serve for business purpose.

NAVIGATION - SEARCH

[TinyERP]Deploy to Azure with Powershell

Overview

Up to now, We can deploy our webapi project to azure using:

  • Visual Studio: Using azure publishing profile
  • FTP: both uploading manual or automatically (using FTP publishing profile of VS)

In this article, We will continue build and deploy our webapi project using PowerShell. more specified steps were as below:

  1. Build and publish webapi project to local folder.
  2. Upload to azure.

Now, let checkout how to make it.

Note: We will use this article as the part of building CI (Continuous Integration) tool for our ERP (TinuERP) in the future.

How to deploy TinyERP to azure?

The sample code for this article was located in "https://github.com/techcoaching/TinyERP" (feature/deploy_to_azure_using_powershell branch).

Run the code on your PowerShell

Check the source code, there is "build" folder in root folder:

All the script for building and uploading was located in this folder only.

The starting point is "index.ps1" file. Just run this file from your powershell command line as below:

We run ".\index.ps1" from "d:\projects\tfs_tinyerp\build". Please be aware that this is the folder which contains the build script.

  • ".\index.ps1": this will run the script in index.ps1 file.
  • We can ignore "ps1" extension and run command as ".\index".

If we run "index.ps1" (without ".\" ahead, we will receive this error message:

By default, we was not allowed to run the scrip on PowerShell, so running ".\index.ps1" will raise this error:

Restart your PowerShell with administrator mode and run "Set-ExecutionPolicy -ExecutionPolicy UnRestricted". For more information, see Using the Set-ExecutionPolicy Cmdlet

If all settings were correct, the result of ".\index.ps1" command will be:

From the highlighted color, we can recognized that, there are 3 parts:

  1. Prepare:
    • Check and create output folder if was not exist
    • Empty output folder if need
  2. Build
    • Using MSBuild for building the webapi project
    • Copy published file to output folder
  3. Upload
    • Get information of remote server
    • Upload published file to remote server using FTP

We will go in detail for each phase as below.

Prepare

As mentioned, This phase will help us verifying and creating necessary resources if need.

This makes sure that the build phase can start its works.

Check "index.ps1", at the end of the file, we have:

[BuildAgent] $buildAgent=[BuildAgent]::new($fileToBuild, $solutionDir, $projectOutputFolder, $cleanOutputFolderBeforeBuild, $azureSettingFile)
$buildAgent.Build()

This code creates new instance of BuildAgent and call to Build method. I prefer BuildAgent should be AzureDeploymentAgent and Build should be "Deploy" for more meaningful. it was just naming convention, We will update it later.

You can see some "using module" statement on the top of file. Do not scare it. This was the same meaning as "using" statement in c#.

There are some variables, let me explain a little bit before going to detail:

$Global:logger = [DefaultLogger]::new()
# path to csproj file to build
[string] $fileToBuild="$($solutionDir)\api\Application.Api\App.Api.csproj"
# this is where compiled files were stored
[string] $projectOutputFolder="$($outputDir)\webapi"
# set TRUE to clear output folder in each build
[string] $cleanOutputFolderBeforeBuild=$TRUE
# path to azure publish profile
[string] $azureSettingFile="d:\temp\azure\tinyerp.PublishSettings"


  •  $Global:logger: we use this for writing something to console. this was just wrapper of Write-Host command-let. This was good if we want to write the log information to file later in future.
  • $fileToBuild: this is path to project where we want to build/ publish
  • $projectOutputFolder: this is path to local folder where we want to store the published files.
  • $cleanOutputFolderBeforeBuild: TRUE to empty output folder, otherwise, this folder will be overridden by published files. So some unexpected files were still remained in this folder. Be aware of this.
  • $azureSettingFile: this file was stored azure information which published files were uploaded to.

Beside, those variables, we have some intermediated variables, such as $root, $outputDir ... this help us removing code duplication when calculating necessary variables (such as: $projectOutputFolder)

Look at "agents\buildAgent.psm1", we see the definition for Build method:

Build(){
	$Global:logger.Write("Start building '$($this.FileToBuild)' output to '$($this.OutputFolder)', clearDest: $($this.ClearDest)")
	if($this.IsValidRequest($this.FileToBuild) -ne  $TRUE){
		$Global:logger.Write( "'$($this.FileToBuild)' was not existed. Please specify project to build")
		return
	}
	$Global:logger.Write("Preparing ...", "cyan")
	$this.OnBeforeBuild()
	$Global:logger.Write("Preparing was completed", "cyan")
	$Global:logger.Write("Building ...", "cyan")
	$this.Building()
	$Global:logger.Write("Building was completed ...", "cyan")
	$Global:logger.Write("Upload to remote host ...", "cyan")
	$this.UploadArtifactsToRemote()
	$Global:logger.Write("Upload was completed...", "cyan")
	$Global:logger.Write("Building '$($this.FileToBuild)' was completed.")
}

I was not a coding guru. But hope this was easier for you understanding the purpose of this method.

Firstly, We check if the build request was valid or not (just simple if the build file was existed or not.

System will show error message in the case of inexisted file:

Otherwise, we will do some preparation:

OnBeforeBuild(){
	if (($this.ClearDest -eq $TRUE) -and [FileHelper]::ExistFolder($this.OutputFolder)){
		Write-Host "Deleting '$($this.OutputFolder)' ..."
		Remove-Item $this.OutputFolder -force -recurse
		Write-Host "'$($this.OutputFolder)' folder was deleted"
	}
	if([FileHelper]::ExistFolder($this.OutputFolder) -ne $TRUE){
		Write-Host "Creating '$($this.OutputFolder)' folder ..."
		New-Item -ItemType Directory -Force -Path $this.OutputFolder
		Write-Host "'$($this.OutputFolder)' folder was created ..."
	}
}

This code will check if output folder was not exist, it will create new folder.

It will also clear output folder if we set $cleanOutputFolderBeforeBuild to $TRUE

Up to now, system make sure that the build file was existed and necessary output folder was ready.

You can add more validation rules if need as we do not want to make much complicated at the moment.

Secondly, building the project:

$Global:logger.Write("Building ...", "cyan")
$this.Building()
$Global:logger.Write("Building was completed ...", "cyan")
BuildProject(){
	$GLobal:logger.Write("Starting building '$($this.FileToBuild)' ...")
	$cmd = "$($Global:msbuildPath) $($this.FileToBuild) /p:SolutionDir=$($this.SolutionDir) /p:DeployOnBuild=true /t:Package /p:PublishProfile=FolderProfile /p:Configuration=Release /p:CreatePackageOnPublish=true "
	$Global:logger.Write("Running build command '$($cmd)'")
	Invoke-Expression $cmd
	$GLobal:logger.Write("Building '$($this.FileToBuild)' was completed...")
}

Currently, I'm using MSBuild as build engine. You can improve the code and support more build engine if need.
After that, Copying the published file to output folder:

[string] $projectDir=[System.IO.Path]::GetDirectoryName($this.FileToBuild)
[string] $copyFrom = [System.String]::Format("{0}\obj\Release\Package\PackageTmp\*", $projectDir)
[FileHelper]::CopyFolder($copyFrom, $this.OutputFolder)

By default, published files were located in "<root_project_folder>\obj\{debug|release|...}\Package\PackageTmp" folder.

To this point, we have published files were ready in output folder. the next step will upload those file to azure

Lastly, Upload to remote server. But which server?

We can not see any azure server information, up to now.

Check again in "index.ps1" file, we have mentioned to:

# path to azure publish profile
[string] $azureSettingFile="d:\temp\azure\tinyerp.PublishSettings"

This file contains information of our application on azure, So we can read from this file and upload published files appropriately.

To get this file, Go to your app on azure, which we want to deploy the pushed files to:

And content of "publish profile" looks like:

<publishData>
	<publishProfile 
		profileName="tinyerp - Web Deploy" 
		publishMethod="MSDeploy" 
		publishUrl="tinyerp.scm.azurewebsites.net:443" 
		msdeploySite="tinyerp" 
		userName="$tinyerp" 
		userPWD="user-password" 
		destinationAppUrl="http://tinyerp.azurewebsites.net">
		<databases />
	</publishProfile>
	<publishProfile 
		profileName="tinyerp - FTP" 
		publishMethod="FTP" 
		publishUrl="ftp://waws-prod-sn1-113.ftp.azurewebsites.windows.net/site/wwwroot" 
		ftpPassiveMode="True" 
		userName="tinyerp\$tinyerp" 
		userPWD="user-password" 
		destinationAppUrl="http://tinyerp.azurewebsites.net">
		<databases />
	</publishProfile>
</publishData>

This file contains information for web-deploy and ftp. In this article, we will use the second. this also contains the path to ftp server, user name and password.

Let save this file to folder on your local and update value of "$azureSettingFile" is the full path to that file. In my sample, it was "d:\temp\azure\tinyerp.PublishSettings".

Look at the code in Build method of BuildAgent, we have this code:

$Global:logger.Write("Upload to remote host ...", "cyan")
$this.UploadArtifactsToRemote()
$Global:logger.Write("Upload was completed...", "cyan")

And detail of UploadArtifactsToRemote:

UploadArtifactsToRemote(){
	[FTPInformation] $ftpInfo = [FTPInformation]::Load($this.AzurePublishSettingFile)
	
	[FTP] $ftp = [FTP]::new($this.OutputFolder, $ftpInfo.Path, $ftpInfo.UserName, $ftpInfo.Password)
	$files = [FileHelper]::GetAllFiles($this.OutputFolder)
	ForEach($file in $files){
		$relativePath=$file.replace($this.OutputFolder + "\",'')
		if([FileHelper]::ContainFolder($relativePath) -and ($ftp.CreateFolder([System.IO.Path]::GetDirectoryName($relativePath)).Status -eq [ActionStatusType]::Fail)){
			$Global:logger.Error("Error while working on '$($relativePath)'. Please check in the console for more information.")
			Exit [AppStatusCode]::Error
		}
		$Global:logger.Write("Start uploading '$($relativePath)'...")
		$ftp.Upload($relativePath)
		$Global:logger.Write("'$($relativePath)' was uploaded.")
	}
}

In this method, We read azure information from "publish profile" file:

[FTPInformation] $ftpInfo = [FTPInformation]::Load($this.AzurePublishSettingFile)

and use this method for initializing an FTP object (see common\ftpClient.psm1 for more information):

[FTP] $ftp = [FTP]::new($this.OutputFolder, $ftpInfo.Path, $ftpInfo.UserName, $ftpInfo.Password)

Then, we get the list of all files located in output folder (recursive):

$files = [FileHelper]::GetAllFiles($this.OutputFolder)

and upload to remove server using $ftp object above:

ForEach($file in $files){
	$relativePath=$file.replace($this.OutputFolder + "\",'')
	if([FileHelper]::ContainFolder($relativePath) -and ($ftp.CreateFolder([System.IO.Path]::GetDirectoryName($relativePath)).Status -eq [ActionStatusType]::Fail)){
		$Global:logger.Error("Error while working on '$($relativePath)'. Please check in the console for more information.")
		Exit [AppStatusCode]::Error
	}
	$Global:logger.Write("Start uploading '$($relativePath)'...")
	$ftp.Upload($relativePath)
	$Global:logger.Write("'$($relativePath)' was uploaded.")
}

The process is really simple.

See the video below for more information. how does it work:

 In video above, You can see how the code will work.

Currently, Uploading files to remote server is rather slow as FTPCLient was created by myself. Just showing you that how it should work, it was synchronous.

You can improve to use asynchronous and multiple connections.

In your real application, Please try to search some FTP client library for PowerShell. It was support more features for us.

But how to build the TinyERP to staging/ UAT or production?

(to be continues)

 

For more information about other articles in this series

Thank you for reading,

Note: Please like and share to your friends if you think this is useful article, I really appreciate

 

 

Add comment