In the previous article I explained how to set up multiple environments for Azure Pipelines. In this article I will explain how you can deploy the artifacts to the on-premise VM’s. This are the artifacts you build in the pipeline. I am using an template for this, because we are using the same script three times. An template avoids code repetition.
I have three stages with a deployment job:
- acceptance
- test
- production.
By defining the name of the environment it knows to which VM it needs to deploy. You can add a job with a task by adding the following to the azure-pipelines.yml
- stage: deployAcceptatie
displayName: Deploy Acceptatie
dependsOn:
- Build
jobs:
- deployment: VMDeploy_Acceptatie
displayName: deploy Acceptatie
environment:
name: Service Portaal VM acceptatie
resourceType: VirtualMachine
tags: acceptatie
strategy:
runOnce:
deploy:
steps:
- template: deploy.yml
Deploy with the deployment template
We use the task template. Create a new file called deploy.yml. I am using pm2 to run the node application. You could also use some other service like docker. This is very dependent on your VM and how your VM is configured (or what you want to use).
steps:
- download: current
displayName: Download backend
artifact: 'backend'
- task: DownloadPipelineArtifact@2
displayName: Download frontend
inputs:
artifact: 'frontend'
path: '$(Pipeline.Workspace)/frontend$'
patterns: 'dist/**'
- task: Bash@3
displayName: Stop application
inputs:
targetType: 'inline'
script: |
export HOME=/home/pm2
chown -R pm2:pm2 /home/azure_pipeline/
su -c "pm2 stop all" -m "pm2"
workingDirectory: '/opt/backend'
- task: Bash@3
displayName: Backend deploy
inputs:
targetType: 'inline'
script: |
mv -v $(Pipeline.Workspace)/backend/dist ./dist
mv -v $(Pipeline.Workspace)/backend/node_modules ./node_modules
workingDirectory: '/opt/backend'
- task: Bash@3
displayName: Frontend deploy
inputs:
targetType: 'inline'
script: |
mv -v $(Pipeline.Workspace)/frontend/* ./dist
workingDirectory: '/opt/frontend'
- task: Bash@3
displayName: Start application
inputs:
targetType: 'inline'
script: |
export HOME=/home/pm2
su -c "pm2 start all" -m "pm2"
workingDirectory: '/opt/backend'
The highlighted code is the code specific to your VM. Do this for all the three environments. The final azure-pipelines.yml will look like this:
trigger:
- master
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
jobs:
- job: Frontend
steps:
- task: Npm@1
inputs:
command: 'ci'
workingDir: '$(System.DefaultWorkingDirectory)/frontend'
- task: Bash@3
inputs:
targetType: 'inline'
script: 'npm run ng build'
workingDirectory: '$(System.DefaultWorkingDirectory)/frontend'
- task: DeleteFiles@1
displayName: 'Delete JUnit files'
inputs:
SourceFolder: '$(System.DefaultWorkingDirectory)/frontend/junit'
Contents: 'TESTS*.xml'
- task: Npm@1
displayName: 'Test Angular'
inputs:
command: custom
customCommand: run test -- --watch=false --code-coverage
workingDir: '$(System.DefaultWorkingDirectory)/frontend'
- task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
displayName: ReportGenerator
inputs:
reports: '$(Build.SourcesDirectory)/frontend/coverage/cobertura-coverage.xml'
targetdir: '$(Build.SourcesDirectory)/frontend/coverage'
reporttypes: 'Cobertura'
assemblyfilters: '-xunit*'
- task: PublishTestResults@2
displayName: 'Publish Angular test results'
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '$(System.DefaultWorkingDirectory)/frontend/junit/TESTS-*.xml'
searchFolder: '$(System.DefaultWorkingDirectory)/frontend/junit'
testRunTitle: 'Frontend mocha tests'
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(Build.SourcesDirectory)/frontend/coverage'
artifact: 'frontendCoverage'
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(Pipeline.Workspace)/frontend'
artifact: 'frontend'
publishLocation: 'pipeline'
- job: Backend
steps:
- task: Npm@1
inputs:
command: 'ci'
workingDir: '$(System.DefaultWorkingDirectory)/backend'
- task: Bash@3
inputs:
targetType: 'inline'
script: 'npm run build'
workingDirectory: '$(System.DefaultWorkingDirectory)/backend'
- task: Npm@1
displayName: run test backend report
inputs:
command: 'custom'
workingDir: 'backend'
customCommand: 'run test-azure-report'
- task: Npm@1
displayName: run test backend coverage
inputs:
command: 'custom'
workingDir: 'backend'
customCommand: 'run test-azure-coverage'
- task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
displayName: ReportGenerator
inputs:
reports: '$(Build.SourcesDirectory)/backend/coverage/cobertura-coverage.xml'
targetdir: '$(Build.SourcesDirectory)/backend/coverage'
reporttypes: 'Cobertura'
assemblyfilters: '-xunit*'
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'junit.xml'
searchFolder: '$(Build.SourcesDirectory)/backend'
testRunTitle: 'Run backend jest tests'
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(Build.SourcesDirectory)/backend/coverage'
artifact: 'backendCoverage'
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(Pipeline.Workspace)/backend'
artifact: 'backend'
publishLocation: 'pipeline'
- job: codeCoverage
displayName: publish code coverage
dependsOn:
- Frontend
- Backend
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
inputs:
artifact: backend
path: $(Pipeline.Workspace)/backend/backend
itemPattern: '!node_modules/**'
- task: DownloadPipelineArtifact@2
inputs:
artifact: frontend
path: $(Pipeline.Workspace)/frontend/frontend
itemPattern: '!node_modules/**'
- download: current
artifact: backendCoverage
- download: current
artifact: frontendCoverage
- task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
displayName: ReportGenerator
inputs:
reports: '$(Pipeline.Workspace)/backendCoverage/Cobertura.xml;$(Pipeline.Workspace)/frontendCoverage/Cobertura.xml'
targetdir: '$(Pipeline.Workspace)/coverage'
reporttypes: 'HtmlInline_AzurePipelines;Cobertura'
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Pipeline.Workspace)/coverage/*.xml'
reportDirectory: '$(Pipeline.Workspace)/coverage'
- stage: deployAcceptance
displayName: Deploy Acceptance
dependsOn:
- Build
jobs:
- deployment: VMDeploy_Acceptance
displayName: deploy Acceptance
environment:
name: Service Portaal VM acceptance
resourceType: VirtualMachine
strategy:
runOnce:
deploy:
steps:
- template: deploy.yml
- stage: deployTest
displayName: Deploy Test
dependsOn:
- Build
- deployAcceptatie
jobs:
- deployment: VMDeploy_Test
displayName: deploy Test
environment:
name: Service Portaal VM test
resourceType: VirtualMachine
strategy:
runOnce:
deploy:
steps:
- template: deploy.yml
- stage: deployProduction
displayName: Deploy Production
dependsOn:
- Build
- deployAcceptance
- deployTest
jobs:
- deployment: VMDeploy_Production
displayName: deploy Production
environment:
name: Service Portaal VM productie
resourceType: VirtualMachine
strategy:
runOnce:
deploy:
steps:
- template: deploy.yml
Conditions
Now if you only want to deploy production with a push on master, we can add a condition. I am using the next condition.
- stage: deployProduction
condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master'))
displayName: Deploy Production
dependsOn:
- Build
- deployAcceptance
- deployTest
jobs:
- deployment: VMDeploy_Production
displayName: deploy Production
environment:
name: Service Portaal VM productie
resourceType: VirtualMachine
strategy:
runOnce:
deploy:
steps:
- template: deploy.yml
And in your Azure Pipelines dashboard it should now look something like this. Some of the stages have been skipped here, because I didn’t want them to deploy yet.
Now you should have a pipeline in Azure Pipelines, with multiple deployment environment on self-hosted VM’s, using Node.js and Angular.
If you have any questions or suggestions, feel free to leave them below. I hope I helped some of you out!