At Black Marble, we have had a long standing Azure DevOps Team Project that we used for end-to-end demos of the principles of DevOps called Living the Dream. This used a legacy codebase, the old Microsoft Fabrikam demo, and showed that can be deployed using modern tools.
As I had no similar demo for GitHub Enterprise, I thought it would be interesting to see how the migration process taking my Azure DevOps implementation over to GitHub would go. This is a good learning exercise as it is the type of problem that many of our enterprise clients will need to do if changing DevOps platform. My key aim was to do the minimum to get the CI/CD process moved from Azure Pipelines to GitHub Action
Moving the source
This was easy, I just imported the Git repo from Azure DevOps
Build
The build for my solution was fairly easy, the project is a pair of Visual Studio solutions, one for the ARM code and the other for the Website code.
The key point was to make sure I passed in the correct parameters to make sure the web site was packaged up using WebDeploy
Build-Solution:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@v1.0.2
- name: Restore NuGet packages
working-directory: ${{env.GITHUB_WORKSPACE}}
run: nuget restore ${{env.SOLUTION_FILE_PATH}}
- name: Build Solution
working-directory: ${{env.GITHUB_WORKSPACE}}
# Add additional options to the MSBuild command line here (like platform or verbosity level).
# See https://docs.microsoft.com/visualstudio/msbuild/msbuild-command-line-reference
run: msbuild /m /p:Configuration=${{env.BUILD_CONFIGURATION}} /p:DeployOnBuild=true /p:PublishProfile=${{env.BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}}
Deployment
The majority of the work was required to get the solution deployed i.e.
- Creating all the required Azure resources using an ARM template
- Deploying the website via MSDeploy.
In Azure DevOps I had tasks to manage these steps, for GitHub actions, though some actions exist, I also had to write some scripts.
I ended up putting the Actions required for the ARM and Solution deployment in reusable workflows, so I could call the steps at multiple locations on my workflows (for the test deployment and the production deployment) without repeating actions.
ARM
The workflow for the ARM was as follows. Nothing that special, the key points to note are
- You have to use the Azure Login action, this in effect replaces the Azure Pipelines service connection.
- I have to use a script to check the Azure resource group exists prior to the deployment. In Azure Pipelines the ARM task will create the resource group if not present, but this is not so with the GitHub Action
- I inject all my ARM parameters as inline parameters (as opposed to a file), this was just to keep the same pattern as had been used on Azure DevOps
name: Reuseable workflow to publish ARM to Azure resource group
on:
workflow_call:
inputs:
environment:
required: true
type: string
# all secrets inherited
jobs:
Integration-ARM:
runs-on: windows-latest
environment:
name: ${{ inputs.environment }}
steps:
- name: Download ARM Build Artifact
uses: actions/download-artifact@v3.0.1
id: arm-download
with:
# Artifact name
name: ARM
path: ./ARM
- name: Azure Login
uses: Azure/login@v1.4.6
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Ensure Azure Resource Group is created
run: |
$rgexists = az group exists -n ${{ secrets.AzureResourceGroup}}
if ($rgexists -eq 'false') {
az group create --name ${{ secrets.AzureResourceGroup}} --location ${{ secrets.AZURELOCATION }}
write-host "Creating Azure Resource Group"
}
shell: pwsh
- name: Deploy Azure Resource Manager (ARM) Template
uses: Azure/arm-deploy@v1.0.8
with:
scope: 'resourcegroup'
subscriptionId: ${{ secrets.AZURESUBSCRIPTION }}
region: ${{ secrets.AZURELOCATION }}
resourceGroupName: ${{ secrets.AzureResourceGroup}}
template: '.\\ARM\\Templates\\WebSiteSQLDatabase.json'
deploymentMode: 'Incremental'
parameters: 'hostingPlanName="${{ secrets.HostingPlanName}}" hostingPlanSku="${{ secrets.hostingPlanSku}}" hostingPlanCapacity="${{ secrets.hostingPlanCapacity}}" webSiteName="${{ secrets.Sitename}}" sqlserverName="${{ secrets.sqlservername}}" sqlServerAdminLogin="${{ secrets.SQLUser}}" sqlServerAdminPassword="${{ secrets.SQLPassword}}" databaseName="${{ secrets.databasename}}" collation="SQL_Latin1_General_CP1_CI_AS" edition="Standard" maxSizeBytes="1073741824" requestedServiceObjectiveName="S0" appInsightsLocation="${{ secrets.AzureLocation}}" VersionTag="1.2.3"" DeploymentDate="2022-10-28"" EnvironmentTag="tag"'
Solution
The MSDeploy was more problematic. It is fair to say this is not a currently fashionable technology. Web deploy has very much moved to the ‘copy a zip file’ approach. There was no GitHub Action available to run MSDeploy against Azure, so I had to work out the parameters and script it.
Again the key points to note
- I could not find a GitHub Action that would automatically update a configuration file replacing tokens found with values from environment variables and secrets. There a few that do part of the job, but not it all. This is something I might well write, but for now a script that replaces the tokens did the job - and yes I know it is probably better practice to set these values in the Azure WebApp directly, but as I said I was trying for a like for like replacement.
- The MSDeploy relies on pulling down the publish profile then calling the MSDeploy EXE which is present on the GitHub Agent. This took a while to get right, but once it was done, it is reliable
name: Reuseable workflow to publish web solution to Azure WebApp
on:
workflow_call:
inputs:
environment:
required: true
type: string
# all secrets inherited
jobs:
Integration-ARM:
runs-on: windows-latest
environment:
name: ${{ inputs.environment }}
steps:
- name: Download Web Deploy Solution Build Artifact
uses: actions/download-artifact@v3.0.1
with:
# Artifact name
name: 'Webdeploy-Package'
path: ./WebDeploy
- name: Replace tokens in configuration file
run: |
$file = ".\WebDeploy\FabrikamFiber.Web.SetParameters.xml"
$filecontent = Get-Content -Path $file
$filecontent = $filecontent -replace "__Sitename__", "${{secrets.Sitename}}"
$filecontent = $filecontent -replace "__LOCATION__", "${{secrets.LOCATION}}"
$filecontent = $filecontent -replace "__GENERATETESTDATA__", "${{secrets.GENERATETESTDATA}}"
$filecontent = $filecontent -replace "__sqlservername__", "${{secrets.sqlservername}}"
$filecontent = $filecontent -replace "__databasename__", "${{secrets.databasename}}"
$filecontent = $filecontent -replace "__SQLUser__", "${{secrets.SQLUser}}"
$filecontent = $filecontent -replace "__SQLPassword__", "${{secrets.SQLPassword}}"
$filecontent | Out-File $file
cat $file
shell: pwsh
- name: Azure Login
uses: Azure/login@v1.4.6
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: 'Deploy web site with MSDeploy'
run: |
$publishProfile = az webapp deployment list-publishing-profiles --resource-group ${{ secrets.AzureResourceGroup}} --name ${{ secrets.Sitename }} --query "[?publishMethod=='MSDeploy']" --subscription "${{ secrets.AZURESUBSCRIPTION}}" | convertfrom-json
$shortPath = (New-Object -ComObject Scripting.FileSystemObject).GetFolder("./WebDeploy").ShortPath
& "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe" -verb:sync -source:package="$shortpath\FabrikamFiber.Web.zip" -setParamFile:"$shortpath\FabrikamFiber.Web.SetParameters.xml" -dest:auto,ComputerName="https://$($publishProfile.msdeploySite).scm.azurewebsites.net/msdeploy.axd?site=$($publishProfile.msdeploySite)",UserName=$($publishProfile.userName),Password=$($publishProfile.userPWD),AuthType='Basic' -verbose -debug -disableLink:AppPoolExtension -disableLink:ContentExtension -disableLink:CertificateExtension
shell: pwsh
Summary
So I now have the core of my ‘Living the Dream’ demo on GitHub, the is more of course I can do, but it is a good start and has been a good learning experience.
This form of activity is something I would recommend to anyone trying to get their had around the intricacies of GitHub, or any technology new to them. You always learn more I think when trying to do your own project as opposed to just following a lab.