React deployment pipeline
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:
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,
e em cada uma configurei um aprovador:
Finalmente, configurei as conexões com a conta Azure, e GitHub:
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:
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.
And our application is now published in 3 environments, each with its own configuration variables: