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!