I’m really enjoying Angular and Azure, but getting them to work well together is sometimes a pain. Right now, there’s some real catch-up that Azure App Services needs to do in order to embrace anything beyond Angular 7.
That’s because Angular 8 and up (“Angular 8+”) requires Node 12.x, and the latest version of Node.js that Azure App Services supports is 10.6.0. As a result, you cannot do a “build” of an Angular 8+ App on Azure.
Angular is moving rapidly, and they’re already in 9.0 pre-release. I’ve recently moved my own projects to Angular 8.
And I’ve sadly discovered I cannot currently use the standard “Deploy from Github” method to deploy Angular apps to Azure, because the build (which takes place on Azure, using this method) will fail, with an error that the version of Node.js is too outdated. And no, you cannot easily install new versions of Node.js on Azure App Services, because one of the design goals of that App Services platform is to have a curated set of known services, not a “free machine in the cloud” that you can install anything and everything to. You can certainly choose different versions of Node, but you cannot choose anything compatible with Angular 8+ at this writing.
That’s a shame, because continuous integration is a really nice feature of Azure. With it, you do a simple check-in and push to GitHub on a designated deploy branch (I usually choose “master” for this purpose), and then Azure fetches the build from GitHub, does the build locally on your App Services instance, and, with the right tweaks to your deployment script, copies it to the right directory. (Such a build-and-deploy custom script is covered in a prior post on this blog.)
Yes, I could try dockerizing everything, but I really don’t want to do that. I prefer not to have docker always running on my dev machines, and for most of my smaller projects, it’s overkill, because I’m just deploying a Single Page Application and one API.
So, Back to FTP
In the meantime, I’m resorting to simple, tried-and-true FTP deployment with a little automation help from Python.
Remember, all Azure really needs is the contents of your “dist” folder, which is built with the command: “ng build –prod”.
I’ve written a simple python script to do the (Angular 8+) build locally on my dev machine (after testing locally of course), and then it will automatically deploy it via FTP succeeds. Since most of my Angular apps are pretty small (and I like to have a single Git repo with the API back-end and Angular front-end), the process completes far faster than the Continuous Integration approach. I wouldn’t recommend this for large-team, large-scale projects, but it works very well for smaller and especially solo projects.
I expect that eventually, the Azure team will upgrade the versions of Node they support to 12+. When that happens, I’ll likely switch back to Continuous Integration deployment.
If you’d like to go the “build locally and FTP the final ‘dist’ site” route, save this file (called deploybuild.py) into your Angular app’s “src” folder:
# this file is called deploybuild.py
import os.path, os
from ftplib import FTP, error_perm
host = 'azure.host.name.goes.here.azurewebsites.windows.net'
port = 21
ftp = FTP()
ftp.connect(host,port)
ftp.login('username','password')
ftp.cwd('/site/wwwroot')
# set filenameCV to your full, absolute path to the "dist" folder
# of the completed build on your local machine when you do
# an "ng build --prod"
filenameCV = "c:\\build\\your-project-folder\\dist"
def placeFiles(ftp, path):
for name in os.listdir(path):
localpath = os.path.join(path, name)
if os.path.isfile(localpath):
print("STOR", name, localpath)
ftp.storbinary('STOR ' + name, open(localpath,'rb'))
elif os.path.isdir(localpath):
print("MKD", name)
try:
ftp.mkd(name)
# ignore "directory already exists"
except error_perm as e:
if not e.args[0].startswith('550'):
raise
print("CWD", name)
ftp.cwd(name)
placeFiles(ftp, localpath)
print("CWD", "..")
ftp.cwd("..")
# first, do the build locally
buildResult = os.system("ng build --prod")
# if there is an error, stop
if (buildResult==1):
print("ERROR IN BUILD. NOT DEPLOYING.")
ftp.quit()
exit
placeFiles(ftp, filenameCV)
ftp.quit()
To run this script, once you’re ready to deploy your local build, you just issue:
python deploybuild.py
Note that you will get a “cannot rmdir <directory name>” error during the build if you happen to have the project open in another window (say, a manual FTP client.) That’s because “ng build –prod” first wants to clear-out the dist directory, and it cannot, if you have the window open in another application.