Pedro Roque

Pedro Roque

Software Architect

React deployment pipeline

February 18, 2020
devops
azure
pipelines
react

Today, we will see how to create a publishing pipeline for a single-page app, in this case, a React.js application.

Many are already familiar with the classic method for creating pipelines for build and release, where we set up the entire pipeline using the wizards in the Azure DevOps portal.

This method makes it relatively easy to create both build and release pipelines, but it is not practical when you have many pipelines to manage.

That's where the YAML configuration comes in. Our pipelines are thus mere YAML files that can be reused, versioned, and easily replicated.

In this example, we will configure the build of a React application, with automatic deployment to the DEV environment and publication subject to approval for staging and production.

For this demo, I created a standard React application (npx create-react-app). The only changes that were made:

  • Creating a .env file to store our development configurations
  • Modifying App.js to use our configurations
  • Configuration of Express in the public folder to serve the static content of our site

You can find the code on GitHub.

If you run the application on your machine, you should see this:

react app running locally

Before we create our pipeline, we need to have our resources configured on Azure (App Service Linux and Node.js Web App). We also need to configure our deployment environments in Azure DevOps. In my case, I created the QA and PRD environments,

deployment evironments

e em cada uma configurei um aprovador:

approvals

Finalmente, configurei as conexões com a conta Azure, e GitHub:

service connections

Now we're ready to start our pipeline! For the sake of organization, I like to create all artifacts related to pipelines in a dedicated folder:

Pipeline

Lets start by defining my variables:

1variables:
2
3  azureSubscription: '------------------------------'
4  srcFolder: 'pipe-demo'
5  webAppNameDev: 'react-deploy-dev'
6  webAppNameQA: 'react-deploy-qa'
7  webAppNamePrd: 'react-deploy-prd'
8  vmImageName: 'ubuntu-latest'

These variables will help us create the various steps of our pipeline and make script reuse easier.

The next step will be to indicate that it is a multi-stage pipeline.

1stages:
2  - stage: buildDev
3    displayName: 'Build React App'

This section of the script indicates that we are working with a multi-stage pipeline, with the initial stage focused on building the application:

1- stage: buildDev
2    displayName: 'Build React App'
3
4    jobs:
5      - job: 'build_and_test'
6        variables:
7          REACT_APP_HELLO: 'Running on dev'
8        steps:
9          - task: NodeTool@0
10            inputs: 
11              versionSpec: '10.x'
12        
13          - script: |
14              cd $(srcFolder)
15              npm install
16              npm run build          
17            displayName: 'Install and Build'
18
19          - task: CopyFiles@2
20            displayName: 'Copy build output'
21            inputs:
22              SourceFolder: '$(srcFolder)/build'
23              Contents: '**/*'
24              TargetFolder: '$(Build.ArtifactStagingDirectory)'
25
26          - task: ArchiveFiles@2
27            displayName: 'Archive output'
28            inputs:
29              rootFolderOrFile: $(Build.ArtifactStagingDirectory)
30              archiveType: 'zip'
31              archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).dev.zip'
32              includeRootFolder: false
33
34          - task: PublishBuildArtifacts@1
35            inputs:
36              pathtoPublish: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).dev.zip'

In this stage, the environment configuration variables are declared, and five tasks are executed:

  • A node task to install the dependencies and build the application.
  • We copy the build result (build folder) to the pipeline's staging folder.
  • We archive our application into a dev.zip file.
  • We publish the file in the pipeline artifacts.

Since the REACT_APP_HELLO variable is defined within a job, its scope is limited to the job where it is declared. Defining configuration variables directly in YAML might seem strange, but we must remember that we are building an application that runs in the user's browser. So, there is no point in worrying about secrets at this stage. If it's a secret, don't include it in a SPA!

1  - stage: deployDev
2    displayName: 'Deploy Dev'
3    dependsOn: buildDev
4    condition: succeeded()

Here, we declare that the deployment stage is dependent on the build stage being successfully completed.

1    jobs:
2      - deployment: deploy
3        displayName: Deploy to Dev
4        environment: 'development'
5        pool:
6          vmImage: 'ubuntu-latest'

The environment is defined in the publishing job, and in the case of development, no approval is configured, making the deployment happen automatically.

1        strategy:
2          runOnce:
3            deploy:
4              steps:
5                - task: DownloadPipelineArtifact@2
6                  displayName: "Downloading build artifacts"
7                  inputs:
8                    buildType: current
9                    targetPath: '$(System.ArtifactsDirectory)'
10                - task: AzureRmWebAppDeployment@4
11                  inputs:
12                    ConnectionType: AzureRM
13                    azureSubscription: '$(azureSubscription)'
14                    appType: webAppLinux
15                    WebAppName: '$(webAppNameDev)'
16                    packageForLinux: '$(System.ArtifactsDirectory)/drop/$(Build.BuildId).dev.zip'
17                    StartupCommand: 'node index.js'
18                    ScriptType: 'Inline Script'
19                    InlineScript: 'npm install'

The publishing job is configured to encompass two tasks: the first downloads the publication artifact, and the second publishes it to an Azure web app. It's important to highlight the definition of the script to run npm install, which will install the Express dependencies and the definition of the server entry point as node index.js.

To define the publication in the QA and Production environments, we need to build the application for each environment and publish it as we did in dev.

1  - stage: buildQA
2    displayName: 'Build React App'
3    dependsOn: deployDev
4    condition: succeeded()
5
6    jobs:
7      - job: 'build'
8        variables:
9          REACT_APP_HELLO: 'Running on QA'
10        steps:
11          - task: NodeTool@0
12            inputs: 
13              versionSpec: '10.x'
14        
15          - script: |
16              cd $(srcFolder)
17              npm install
18              npm run build          
19            displayName: 'Install and Build'
20
21          - task: CopyFiles@2
22            displayName: 'Copy build output'
23            inputs:
24              SourceFolder: '$(srcFolder)/build'
25              Contents: '**/*'
26              TargetFolder: '$(Build.ArtifactStagingDirectory)'
27
28          - task: ArchiveFiles@2
29            displayName: 'Archive output'
30            inputs:
31              rootFolderOrFile: $(Build.ArtifactStagingDirectory)
32              archiveType: 'zip'
33              archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).QA.zip'
34              includeRootFolder: false
35
36          - task: PublishBuildArtifacts@1
37            inputs:
38              pathtoPublish: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).QA.zip'
39
40  - stage: deployQA
41    displayName: 'Deploy QA'
42    dependsOn: buildQA
43    condition: succeeded()
44
45    jobs:
46      - deployment: deploy
47        displayName: Deploy to QA
48        environment: 'QA'
49        pool:
50          vmImage: 'ubuntu-latest'
51        strategy:
52          runOnce:
53            deploy:
54              steps:
55                - task: DownloadPipelineArtifact@2
56                  displayName: "Downloading build artifacts"
57                  inputs:
58                    buildType: current
59                    targetPath: '$(System.ArtifactsDirectory)'
60
61                - task: AzureRmWebAppDeployment@4
62                  inputs:
63                    ConnectionType: AzureRM
64                    azureSubscription: '$(azureSubscription)'
65                    appType: webAppLinux
66                    WebAppName: '$(webAppNameQA)'
67                    packageForLinux: '$(System.ArtifactsDirectory)/drop/$(Build.BuildId).QA.zip'
68                    StartupCommand: 'node index.js'
69                    ScriptType: 'Inline Script'
70                    InlineScript: 'npm install'
71
72  - stage: buildPrd
73    displayName: 'Build React Production App'
74    dependsOn: deployQA
75    condition: succeeded()
76
77    jobs:
78      - job: 'build'
79        variables:
80          REACT_APP_HELLO: 'Running on Production'
81        steps:
82          - task: NodeTool@0
83            inputs: 
84              versionSpec: '10.x'
85        
86          - script: |
87              cd $(srcFolder)
88              npm install
89              npm run build          
90            displayName: 'Install and Build'
91
92          - task: CopyFiles@2
93            displayName: 'Copy build output'
94            inputs:
95              SourceFolder: '$(srcFolder)/build'
96              Contents: '**/*'
97              TargetFolder: '$(Build.ArtifactStagingDirectory)'
98
99          - task: ArchiveFiles@2
100            displayName: 'Archive output'
101            inputs:
102              rootFolderOrFile: $(Build.ArtifactStagingDirectory)
103              archiveType: 'zip'
104              archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).PRD.zip'
105              includeRootFolder: false
106
107          - task: PublishBuildArtifacts@1
108            inputs:
109              pathtoPublish: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).PRD.zip'
110
111  - stage: deployPrd
112    displayName: 'Deploy Production'
113    dependsOn: buildPrd
114    condition: succeeded()
115
116    jobs:
117      - deployment: deploy
118        displayName: Deploy to Production
119        environment: 'QA'
120        pool:
121          vmImage: 'ubuntu-latest'
122        strategy:
123          runOnce:
124            deploy:
125              steps:
126                - task: DownloadPipelineArtifact@2
127                  displayName: "Downloading build artifacts"
128                  inputs:
129                    buildType: current
130                    targetPath: '$(System.ArtifactsDirectory)'
131
132                - task: AzureRmWebAppDeployment@4
133                  inputs:
134                    ConnectionType: AzureRM
135                    azureSubscription: '$(azureSubscription)'
136                    appType: webAppLinux
137                    WebAppName: '$(webAppNamePrd)'
138                    packageForLinux: '$(System.ArtifactsDirectory)/drop/$(Build.BuildId).PRD.zip'
139                    StartupCommand: 'node index.js'
140                    ScriptType: 'Inline Script'
141                    InlineScript: 'npm install'

After that, our pipeline will be ready to automate the entire process with a versioned script in Git.

pipeline completo

And our application is now published in 3 environments, each with its own configuration variables:

dev

qa

prod