In this article, I go through my entire learning process, and also reveal some roadblocks that took a lot more effort than necessary to solve. I provide a straightforward set of instructions as to how to properly deploy your React web app to Azure App Services in a variety of ways. Hopefully, if you’re looking to embark on a similar journey, I can help clear up some of these cloudy skies for you.
Introduction
I began walking down this learning path with realistically zero experience in any web-based technologies. Going in, my toolkit consisted of a few blunt instruments – object oriented programming, some vague notion of HTML, API calls from languages like Python and VB.NET, and some very shallow understanding of sockets. I didn’t know what to expect going in, except that the world of front-end development was going to be very different from my familiar desktop environments.
Table of Contents
Picking the Technologies
To begin with, I selected a few technologies that I wanted to pursue. Because web development is such a broad space with so many different actors, trying to learn everything at once is a virtual impossibility. Instead, I drew names out of a hat (figuratively) and chose just a small number to focus on.
I knew that HTML and CSS would serve a very large majority of the work I had to do. Even with tools like JavaScript, the root of any website was still a hypertext file being displayed in a browser. Fortunately, both are well-documented and have been exhaustively written of. Coupled with my ever-growing fondness of XAML (Microsoft’s XML-based user interface language), I was able to readily accept the syntax and usage of both.
Then, I had to choose a flavor of JavaScript. I wanted to pick something that was in wide use, that had been around for enough time that it was stable and well-documented, and had enough tutorials online that I could feasibly wander aimlessly and find myself at the end goal, smelling the roses along the way. I picked React.js for its single-page application model, JSX, and its interesting approach to stateful components.
Finally, I had to pick where to host my project. One possibility was that I would simply run an npm server on the little Linux PC I rescued from my university’s surplus store. I’d done similar things before, using the PC as a mothership for home automation IoT projects. However, I did recently pick up an Azure cert, so I wanted to keep the ball rolling in that respect. I would host it on Azure App Services.
Quick Lessons from React
With React came a host of new possibilities and limitations from the other languages I was familiar with. To put it simply, JavaScript looks like C# (or whatever C variant you know), behaves like Python, and yet introduces a whole host of other functionalities that are unique. The first (and worst) thing I came across when trying to build my first app arose from my stubbornly-old mindset: object states.
Imagine you’re tasked with implementing a board game – tic-tac-toe, for instance – in a traditional desktop application. In fact, this is exactly what I did first, since I’d recently done a programming exercise with a friend learning to code themselves. How might you approach this problem?
To my object-oriented mind, the solution was straightforward; create a Grid
object that could host a two-dimensional array of Cell
objects. The Grid
could evaluate if a row, column, or diagonal of cells were in a vulnerable or winning state, and the Cells
could hold information about which player owned it. The Cells
can then decide how they want to render themselves, and the Grid
would simply display that. The benefit of this would be that the Grid
didn’t need to know how the Cells
were implemented, and the Cells
could host additional behavior like hovering, displaying warnings, animations, ignoring clicks, and so on. With these prototype objects, theoretically any board game could be built.
A not-so-quick trial later, I discovered that this approach would require significant adaptation in order to implement in React. The concept of an ‘object’ was relatively abstracted, and instance methods were not guaranteed. In other words, it was not a safe assumption that calling a method of an object would result in the actual object being modified.
The biggest change that I had to realize was that the fundamental philosophy of web programming isn’t to have a hundred secret objects passing data around to be mutated and polished; everything had to result in some visible, tangible change. You didn’t have one gigantic data object you distributed to many smaller functions that each extracted one bit of information; instead, you have a hundred public bits of data, each going exactly where they needed to and no further, showing some kind of visual difference to the user.
I suppose that eventually, when your web app grows in complexity, there will arise scenarios where you may need to start shifting back into the realm of object-oriented programming. However, the argument can be made that at that point, your app is too big, and your logic has a better home on a server instead.
When I began piecing things together, I encountered some unobvious axioms. There were a few optimizations within React that a programmer had to understand and code around; without knowing them, it would almost appear as if the app refused to work. I don’t want to go too deep into the JavaScript language and React framework, since this article is meant to help you see the path among the undergrowth, so I’ll just briefly cover a few points.
-
First, keep your app state as high as it makes sense to. Having one primary component that trickles data down is easier to manage and troubleshoot than having many subcomponents each holding their own state.
-
Use props (properties) exhaustively. Gone is the paradigm of a child asking its parent for data via functions; pass it in every time you require the child component be built.
-
Always set up your component’s state object as soon as possible. React literally reacts to changes in state to determine when to redraw a component. Store as much data as possible in the state, unless you have something that changes extremely often without requiring a redraw.
-
As an extension of that, you may find the need to bind a particular function to an instance. In the constructor, use this.<methodname> = this.<methodname>.bind(this)
to ensure the instance's method always has access to the same instance's fields.
-
Use lambdas. At almost every point in an app’s flow, you can use a lambda to replace what would otherwise be an uninteresting string literal. Need a simple function to handle a button press? No need to add a named function to a class that doesn’t use it. {()=>{}}
is your friend.
-
Object types are not guaranteed, which will cause headaches. For instance, I had a numeric state controlled by a numeric input form. You might expect that a numeric input’s value is always a number – nope! Trying to instantiate an array via new Array(this.state.num)
fails. Any time you must be absolutely certain a value is numericized, make sure you do it (in this case, Number(input.value)
works).
-
Some React functions are asynchronous, which means their execution is unpredictable. If you have something that absolutely must occur after an async call, attach a lambda as a callback function. For anything that can wait, or can fail gracefully in a null
reference exception, use optional chaining.
-
Make sure you add any packages you’re hoping to use into the package.json file, and run npm install
to automatically download them.
-
Get used to using document.getElementById()
to replace template HTML layouts with more specialized JavaScript objects.
-
Finally, make sure you know the different HTML tags before trying to do anything too complicated. I’m definitely still struggling in that regard, and I’m regularly confused as to what tag makes most sense. Using online references aren't cheating!
Setting Up Azure DevOps and App Service
Having finished a create-react-app
bootstrapped single-page application, I now needed a place to show it. While there do exist tutorials online for creating an Azure App Service host for Node.js apps, I was not able to find a single one that ran through every single necessary step to get it up and running. Perhaps this was due to Azure’s constant growth and evolution as a platform, so the requirements changed over time, but I’m here to present a quick and dirty tutorial to do so.
I’m going to assume you know how to navigate Github, and have already uploaded your code to your own repository. You can also visit my repository to see how I implemented my project (you'll find some of the residual material from testing all of the instructions below), as well as my project website.
-
Create your Azure account, if you haven’t already. Then, create a new App Service resource. Pick your subscription and resource group, creating a new one if you wish. Fill in your instance name, selecting Code as your Publish option, and pick a Node.js runtime stack (since we’re building a React app). Importantly, choose an operating system you’re willing to work with. Depending on whether you select Linux or Windows, some of the final steps will differ. Pick a region close to you, and choose the Free tier plan for now. You can then finish the creation process.
-
While you’re waiting for this to be deployed, create an Azure DevOps account if you haven’t already. You’ll want to create a new project (probably keep it private, unless you want it to be public). Or, if you have a lot of build pipelines in one big project, that’s fine too – just so long as you keep track of everything carefully.
-
You’ll see a Pipelines menu item, and a sub-menu item called Pipelines as well. Open that and create a new pipeline. Here, you have two options – use the YAML editor to script your pipeline, or use the Classic editor to build the pipeline manually. Using YAML will create a file within your repository that contains the build information, which then runs whenever a new commit occurs. I’m going to be using the Classic editor, which is a bit easier to tweak and customize. If you want to use the YAML editor, see the separate instructions below.
-
Open the classic editor and select GitHub as your code source. Authorize DevOps to connect with your Github account. Use the ellipsis (...) button to help you find the repository that contains your React app, and then select your branch.
-
When prompted to select a template, start with an Empty Job. You’ll then be presented with an editor where you can choose blocks to add to the deployment pipeline. The first node is already set for you, titled ‘Get sources’, which downloads the code in the repository as the first step of the pipeline. The first agent job is also loaded in for you, devoid of all life.
As of the writing of this article, there isn’t a default option for a Node.js deployment to an Azure Web App, hence the Empty Job.
-
There are three or four nodes to add, depending on which OS you chose earlier; this will be explained at a later step. Click the plus (+) sign next to the agent job element to add a new step. A searchable menu appears – look for ‘npm
’ and select the most basic option published by Microsoft. The element is then added to your job. Select it and modify it to run the ‘install
’ command, ignoring all other options.
-
Add a second ‘npm
’ element. Choose a custom command and enter ‘run build
’ in the command box. Ignore all other options.
-
Add a ‘Publish Artifact
’ element. You’ll need to enter two important parameters: the path to publish, and an artifact name. Keep track of these two carefully. I recommend ‘build’ and ‘artifact’ respectively, although the default artifact name is ‘drop’.
-
If you selected a Windows host for your app service, you’ll need to provide a web.config file to address routing options for the IIS webserver. Make sure you have one created with the necessary options (see example), and put it somewhere in your repository – I left mine in the /src folder. (If you selected a Linux host, you can skip this step, but be aware there’s a final step you must take at the end.) Add a ‘Copy files
’ element immediately before the Publish Artifact element, at the third position. You may leave the source folder empty if you have your config file at the root; otherwise, specify its parent directory. Enter ‘web.config’ into the contents box, and enter your path to publish name from step 8 into the target folder – again, build is the recommended name.
-
Save your pipeline! If you do not explicitly save it, your work will be lost.
-
On the navigation bar, go to Pipelines > Releases. You’re now going to set up the release process for the compiled result. Create a new release pipeline (not a new release!), and start with an empty pipeline again. In the editor, you’ll find two blocks labelled Artifacts and Stages – we’ll add an artifact first. Add an artifact and select Build as the source type. You’ll need to select the source build pipeline, which is the one you saved in step 10. Modify the source alias to something easy to remember, such as ‘_artifact’ or ‘pipeline-result’. Save this.
-
Return to the release editor with the two boxes. You’ll notice that there’s a lightning bolt on the upper right of the artifact element. Click it to open it, and enable the continuous deployment trigger switch. This will allow your release to automatically trigger whenever your build pipeline completes. Ignore the branch filters.
-
Back at the release editor, open Stage 1 by clicking on the ‘1 job, 0 task’ link. You’ll be greeted with a familiar pipeline editor. Add an ‘Azure App Service deploy’ element. Choose your subscription (hint, pick a service connection, not the actual subscription). Pick your App Service type, depending on which OS you selected earlier; Web App on Windows or Linux. Find your App Service name in the list. If you don’t see it, make sure you select the correct OS; confirm that your actual App Service resource on Azure has the OS you wanted to use. Finally, edit the package/folder input box. Since you haven’t run your pipeline yet, the browse button won’t help. Instead, just enter:
$(System.DefaultWorkingDirectory)/_artifact/artifact
(and replace the two directories at the end with the corresponding names of your source alias (step 11) and artifact name (step 8) respectively).
-
Finally, return to your first build pipeline from step 10. You may now open it and run it. You can follow along as Agent job 1 completes installation of your Node.js packages, builds a production version of your web app, and saves it all into an artifact. You’ll then see, if you open your release pipeline, that it is deployed to your Azure App Service resource.
-
If you return to your App Service resource and look at the Deployment Center, you’ll see a successful deployment from Azure Pipelines. If you selected a Windows host OS, you’re all set to go. Visiting your site will show your app, and if you implement React’s routers, assuming your web.config is correctly configured, your React app can handle those navigations as well.
-
If you selected a Linux host OS, visiting your site right now will not work. This is because you still need to configure one last thing; the Linux host needs to use pm2
, a process manager for JavaScript. You will need to set it to run at every container startup. In the App Service resource, navigate to Configuration, General settings, and enter this startup command:
pm2 serve /home/site/wwwroot --no-daemon --spa
This tells the host to serve the correct directory as the root directory of your site, and the --spa
flag indicates that this is a single-page application so that routing is directed automatically to your root index.html file, instead of actually searching for a non-existent directory. Save this, and restart your app service. Your site should now work.
If you decided to use the YAML editor instead of the classic editor, the instructions are a bit shorter, but the final setup is a little trickier. While this method allows for an almost-immediate setup, it takes longer for the initial deployment, it’s more difficult to tweak later on, and you’re limited to a Linux host (for the time being).
-
Continuing from step 3 of the classic setup – select your repository and project, and DevOps will prompt you to choose a pipeline configuration. You can take the shortcut ‘Node.js React Web App to Linux on Azure’ to set up most of what you’ll need. Unfortunately, at the time of writing, there’s no Windows counterpart, and there’s no easy way to modify the auto-generated YAML file to work on Windows hosts. Select your App Service, then validate and configure. Do not complete the setup process yet; there are a few settings to change.
-
You can change the Stack version to 12 or 14 if you desire; change the line towards the end of the document that says NODE|10.10
to your desired version, such as NODE|12-lts
. Examine the startup command; the default one will not work. Attempting to navigate to your app service page right now will result in an application error, 502, or 404. Follow any one (pick just one!) of options A, B or C to modify the StartupCommand
property and select a serving method. Option B is my recommendation.
-
The Development Build method: Modify it to 'cd /home/site/wwwroot && npm run start'
. The default one will attempt to start a development server to host your code, but it will try to do so from the root folder, where package.json doesn’t exist. Adding the change-directory command will point the npm command to the right place.
This will take a significant amount of time to start up on the free tier, since it is recompiling your development code into a debug-able mode, so be wary that your site may not be available for several minutes.
-
The Process Manager method: Change it to 'pm2 serve /home/site/wwwroot/build --no-daemon --spa'
. This method, as with step 16 of the classic setup, starts a webserver that serves from the built version of your app. This method is fast and provides more options during deployment, but does require a more verbose command.
-
The Npx Serve method: Change it to '
cd /home/site/wwwroot/build && npx serve'
. ‘npx serve’ automatically downloads and installs the ‘serve’ package if it doesn’t exist, then begins serving the production build of your app from your build directory. However, if you’re building a single-page app, you’ll need to create a configuration file to rewrite paths back to the main app. pm2’s --spa
flag is a simpler approach, hence the recommendation to use option B instead of option C.
-
Save your pipeline! This will insert a YAML file into your repository in a new commit, which is then used to inform future deployments. After the commit, you should see the pipeline running – you can follow along as the build steps are automatically executed. After a few minutes (or longer, if you selected option A) you should be able to visit your site.
As I mentioned before, all of the tutorials I’ve found online so far select just one of these methods to describe, and often don’t explain the last configuration steps to get the page up and running. Hopefully, the instructions I’ve provided here can help you get past the final hurdles.
If everything has gone to plan, congratulations! Your web app is now available to the public, hooked up to any new commits you make to your repository.
As a final tip, you can modify the continuous integration triggers to ignore changes to your README.md file, YAML config, and so on if you so desire. If you used the classic editor, simply edit your pipeline, and find the ‘Triggers’ menu item next to Tasks and Variables. You can then add path filter exclusions to ignore files that shouldn’t trigger an automatic build and deployment. If you used the YAML editor, edit your pipeline, and find the ellipsis button next to the Variables and Run buttons at the top. Within the ellipsis submenu, you’ll find the Triggers menu item – override the trigger, and similarly add exclusions. Make sure you save after you’ve added your filters!
Setting Up an AWS Host
Great – now that I’ve shown you five (classic Windows, classic Linux, YAML dev build, YAML pm2, YAML serve) options to deploying a React app to Azure App Services, it’s time to introduce a sixth: AWS Amplify. If Azure is not your cup of tea, or you're looking for a way to breeze through setup with minimal hassle, Amplify might work out for you.
I’ll be straightforward – I attempted to deploy the same app to Elastic Beanstalk as well, but I wasn’t familiar enough with it to get it working. Instead, I found that Amplify was probably a better option, anyways.
-
Create an AWS account if you haven’t already. Search for the Amplify service. On the Amplify console main page, select ‘Deploy’.
-
Select GitHub (if that’s where your repository is). Complete the GitHub authorization, then select the correct repository and branch. You can leave the build settings as-is. Go ahead and Save and Deploy.
Amplify should take care of the rest for you. Unlike Azure, however, you won’t be able to select your host OS, pricing tier, or have a site name similar to that of your project by default (a custom domain is required). You also cannot change the trigger to filter out unwanted CI deployments; it relies on a webhook stored on your GitHub repository settings to notify AWS that a new commit was pushed. You might be able to find a combination of webhook events, such as GitHub deployments or releases, that can substitute specific file exclusions. Alternatively, you can refer to AWS's documentation to completely disable automatic builds, or to add a text tag to any insignificant commits to skip deployment.
Final Thoughts
This has been a learning experience for me. I won’t lie, I’m still not sure I completely understand what I’m doing here. Nonetheless, it’s always rewarding to see an app I’ve slowly pieced together being put on display on a public website. I think it’s good to look at that and think, “hey! I did that! I made that!”. In the current tumultuous atmosphere (for many reasons), having moments where you can sit back and enjoy a little bit of creativity and take pride in your work is absolutely paramount to maintaining a healthy mentality. Hopefully, I can inspire you to take on a project like this of your own, or at the very least introduce a few new concepts that may have seemed too out-of-reach for you before.
Take care and be well!
History
- 13th November, 2020: Initial version