For a Service Fabric project I’ve been working on, the project team has implemented a release management pipeline in Visual Studio Team Services. When a commit is pushed to the master branch, the build is run and (if succesful) a release is created and automatically deployed. The release pipeline basically does two things: 1) make sure the environment is up to date using some ARM templates and 2) upgrade the Service Fabric application to the new version.

Upgrading the application requires that you increment the version numbers in the application and service manifests. One way to deal with this is simply by incrementing the version number of each service whether or not it has actually been modified. You can do this by using an auto-generated build number as explained in Colin’s blogpost. In a lot of situations this works fine because Service Fabric allows for zero downtime deployments.

However, we needed to be sure that some services aren’t unnecessarily upgraded. Some of the stateful services in the application maintain connections to external hardware devices. We don’t want to interrupt these each time we need to roll-out a change in any of the other services.

The solution we use for now is manually incrementing the version numbers of the modified services before we commit to the master branch (which is obviously not ideal because you must be careful not to make any mistakes). Fortunately, the Visual Studio tools help here a bit by providing a nice UI (right-click Edit Manifest Versions... on a Service Fabric project).

Using the incremented versions numbers, we create a diff application package using a PowerShell script. A diff package contains only the files that changed between the last and current versions. It does however always contain the full application manifest and all service manifest files. You can read more on differential packages here.

We prefer a diff package to a full application package because even though nothing may have changed in the code of a particular service, the assemblies created by the build may have been changed when compared to the previous version. When this is the case and the version number is not incremented, Service Fabric will raise an error when you try to deploy the application. By using a diff package we are sure that we only include the assemblies for the services that have actual changes.

The diff package script performs the following steps:

  1. Creates the output folder for the diff package. If it already exists it will be deleted, so watch out.
  2. Retrieves application version from the cluster to verify that we really do need a diff package.
  3. Includes the application manifest in the diff package.
  4. Iterates over the service manifests and compares the version numbers with the version numbers of the currently deployed services. Code and config packages with higher version numbers are included in the diff package.

Before running the script, make sure you’re connected to your cluster using Connect-ServiceFabricCluster.

<#
.SYNOPSIS 
Creates a diff package for deploying a Service Fabric application to a cluster.

.DESCRIPTION
This script creates a diff packages based on a given package directory and the current state of the cluster.

.PARAMETER ApplicationPackagePath
Path to the directory containing the original application package (input directory).

.PARAMETER DiffPackagePath
Path to the directory where to output the created diff application package.

.PARAMETER ApplicationName
Name of the Service Fabric application.

#>

Param
(
    [String]
    $ApplicationPackagePath,

    [String]
    $DiffPackagePath,

    [String]
    $ApplicationName
)

###############################################################################
# Create output directory.
###############################################################################

Write-Host "Creating diff package from $ApplicationPackagePath"

# Ensure that the output directory for the Diff package exists.
if (Test-Path $DiffPackagePath)
{
    Remove-Item -Path $DiffPackagePath\* -Force -Recurse | Out-Null
}

New-Item -ItemType Directory -Path $DiffPackagePath

###############################################################################
# Check that the new version number is higher than the currently running application.
###############################################################################

# Try to get the application info from the cluster.
$application = Get-ServiceFabricApplication -ApplicationName $ApplicationName

# If we've got a result, a version of the application is already running.
if ($application -ne $null) {

    Write-Host "Detected version $($application.ApplicationTypeVersion) of application running on cluster"

} else {

    Write-Host "No current version detected. Using full application package."

    Copy-Item -Path (Join-Path $ApplicationPackagePath '*') -Destination $DiffPackagePath -Recurse
    exit
}

# Load the application manifest.
$applicationManifestPath = Join-Path $ApplicationPackagePath "ApplicationManifest.xml"
$appManifestXml = [xml] (Get-Content $applicationManifestPath)
if (!$appManifestXml)
{
    $errMsg = "Failed to load application manifest XML."
    throw $errMsg
}

$applicationTypeName = $appManifestXml.ApplicationManifest.ApplicationTypeName

###############################################################################
# Create the diff package
###############################################################################

Copy-Item -Path $applicationManifestPath -Destination $DiffPackagePath

# Iterate over service manifests. For each service, verify whether it must be part of the diff package.
# If not, remove the service files from the diff package directory.
$appManifestXml.ApplicationManifest.ChildNodes | foreach {

    if ($_.LocalName -eq 'ServiceManifestImport') {

        $serviceManifestName = $_.ServiceManifestRef.ServiceManifestName
        $serviceManifestVersion = $_.ServiceManifestRef.ServiceManifestVersion
        $servicePath = [io.path]::Combine($ApplicationPackagePath, $serviceManifestName)
        $serviceManifestPath = [io.path]::Combine($servicePath, 'ServiceManifest.xml') 

        # Load the new service manifest XML.
        $newServiceManifest = [xml] (Get-Content $serviceManifestPath)
        if (!$newServiceManifest)
        {
            $errMsg = "Failed to load service manifest XML for $serviceManifestName."
            throw $errMsg
        }

        $newCodePackageVersion = $newServiceManifest.ServiceManifest.CodePackage.Version
        $newConfigPackageVersion = $newServiceManifest.ServiceManifest.ConfigPackage.Version

        # Load the old service manifest.
        $oldServiceManifest = [xml] (Get-ServiceFabricServiceManifest -ApplicationTypeName $applicationTypeName -ApplicationTypeVersion $application.ApplicationTypeVersion -ServiceManifestName $serviceManifestName -ErrorAction SilentlyContinue)
        $oldCodePackageVersion = '0.0.0';
        $oldConfigPackageVersion = '0.0.0';

        if ($oldServiceManifest)
        {
            $oldCodePackageVersion = $oldServiceManifest.ServiceManifest.CodePackage.Version
            $oldConfigPackageVersion = $oldServiceManifest.ServiceManifest.ConfigPackage.Version
        }

        # Create the destination directory
        $diffServicePath = Join-Path $DiffPackagePath $serviceManifestName
        New-Item -ItemType Directory -Path $diffServicePath | Out-Null

        # Copy the service manifest
        Copy-Item -Path $serviceManifestPath -Destination $diffServicePath

        # Copy the CodePackage if it is updated
        if ([System.Version]$newCodePackageVersion -gt [System.Version]$oldCodePackageVersion) {

            Write-Host "Including code package for service $serviceManifestName (version $serviceManifestVersion)."

            Copy-Item -Path (Join-Path $servicePath 'Code') -Destination $diffServicePath -Recurse
        }
        else {
            Write-Host "Excluding code package for service $serviceManifestName (version $serviceManifestVersion)."
        }

        # Copy the Config if it is updated
        if ([System.Version]$newConfigPackageVersion -gt [System.Version]$oldConfigPackageVersion) {

            Write-Host "Including config package for service $serviceManifestName (version $serviceManifestVersion)."

            Copy-Item -Path (Join-Path $servicePath 'Config') -Destination $diffServicePath -Recurse
        }
        else {
            Write-Host "Excluding config package for service $serviceManifestName (version $serviceManifestVersion)."
        }
    }
}