There are many options for hosting an Angular application, but if you want to minimize your server costs and ensure the fastest site speed possible for your users, one of the best solutions is to use a content delivery network.
A content delivery network (or CDN) distributes copies of your application's static files across many different servers, so when a user visits your site, they'll be routed to download the files closest and most quickly available to them.
[caption id="attachment_6503" align="aligncenter" width="961"]
Last year when we started building a new frontend in Angular, we realized that hosting our app on a CDN would give us greater flexibility and scalability as we grew, and since we were using Microsoft Azure for our server-side applications, we decided to use their CDN for our frontend.
While we've been using Codeship as part of our continuous delivery process since I joined The Graide Network, publishing our frontend Angular application to Azure's Content Delivery Network was trickier than I expected. Hopefully this tutorial makes it easier for you.
In this post, we'll go through the entire process of deploying an Angular 5 application to Azure's CDN:
We'll start by cloning an open-source Angular app (a rubric creation tool for teachers). I made a special branch in the project called
codeship-tutorialthat has intentionally omitted some of the key code we'll be adding during this walkthrough.
Then we'll set up Codeship Pro to run our tests and linting.
And finally, we'll configure Azure's CDN to serve our single-page Angular app.
I'll point out some "gotchas" that we ran into when using this process to release our Rubric Creator at The Graide Network, as well as some resources I found helpful along the way.
1: Setting up the Angular Application
First, fork the Rubric Creator project to your own GitHub account. Then, clone the
codeship-tutorial branch of the forked version of the project into a local directory and navigate to it:
git clone -b codeship-tutorial email@example.com:<your_github_username>/rubric-creator.git cd rubric-creator</your_github_username>
npm install npm install -g @angular/cli
At this point, the application should be ready to run locally with Angular's local development server:
ng serve > ** NG Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
As the prompt tells you, you should be able to view the local version of the application at
localhost:4200. You should also be able to run the test suite and linter using Angular CLI:
ng test > 10% building modules 1/1 modules 0 active29 05 2018 08:33:01.435:WARN [karma]: No captured browser, open http://localhost:9876/ > Chrome 66.0.3359 (Mac OS X 10.13.4): Executed 45 of 45 SUCCESS (1.629 secs / 1.592 secs) ng lint > All files pass linting.
2: Running Tests and Linting with Codeship
In order to run Codeship tests on this project, you'll need to use Codeship Pro. Use their guide to set up a new project and link it to the fork of the GitHub repository you made above. You should also download Jet so that you can run the Codeship build process locally and encrypt environmental variables.
Once you have a project linked to your fork of the GitHub project, add a
codeship-steps.yml file to the root directory of your project:
version: "3.5" services: frontend: build: ./
- type: serial steps: - type: parallel steps: # Run the linter - service: frontend command: ng lint # Run the tests - service: frontend command: ng test --browsers Chrome_no_sandbox --watch false # Ensure AOT build works - service: frontend command: ng build --prod --aot true
Commit this change and push to your fork's
controller branch. Codeship will use the Docker image defined in the
Dockerfile at the root of the project to run
ng test, and
ng build --prod --aot true in parallel.
If any of those processes fail, you'll get a failing build on Codeship. Otherwise you should see a success page like this when the build is complete:
Congratulations, you just got continuous integration with Angular set up using Codeship. Now let's move on to the trickier part: deploying this application using Azure.
3: Deploying to Azure Cloud Storage
Content delivery networks typically have a single "origin" server, and that's true of Azure's CDN. While this origin can be any static file system, the easiest way to serve up your Angular app is to deploy it to an Azure Blob Storage account.
Once you've created an account, create a blob container with "Blob" public access:
At this point, you could upload files manually, but what we really want is for Codeship to automatically do that when we update the controller branch. In order to automate the file upload process, you'll need your Azure storage account keys and you'll need to create an Azure Active Directory application that can upload files to your storage account.
Getting your Azure storage account keys
Your storage account keys allow Azure command line apps access to upload, download, or delete files in the storage account we just created. These keys can be obtained via the Azure CLI or via the Azure portal. You'll just need one of these keys for this tutorial:
Registering an Azure Active Directory (AD) application
Make sure the application you register has permissions to modify files in the Azure storage account and container you just created.
Building for production
In addition to getting Azure ready to host our files, we need to prep a Docker image that will build our Angular application for a production environment and run our Azure CLI script. Fortunately, Docker released multi-stage builds in 2017, which makes this prospect much easier.
Create a new Dockerfile (called
Dockerfile.build.prod) in the root directory of the repository:
# Angular base image FROM teracy/angular-cli:1.5.0 RUN npm update -g @angular/cli@~1.7.4 WORKDIR /angular # Install packages COPY ./package.json /angular/package.json COPY ./package-lock.json /angular/package-lock.json RUN npm install --silent # Add code COPY ./ /angular # Run the build process RUN ng build --prod --aot true --deploy-url <your_deploy_url> # Next stage: Azure CLI FROM azuresdk/azure-cli-python COPY --from=0 /angular/dist /dist</your_deploy_url>
As you can surmise from the comments, this Dockerfile installs Angular CLI 1.7 and the rest of the npm packages, copies the code from the repository into the working directory of the image, builds for production (make sure to change the
--deploy-url to your desired application URL), and finally copies the
dist folder into an Azure CLI image.
What we're left with is a Docker image that can run the Azure CLI commands we need to save files in our Azure storage account.
Setting encrypted environmental variables
Next, we need to add our build-specific secrets to the cloned repository. Because we don't want to expose these variables to the public, we'll use Codeship's encryption feature.
.env.example file to
.env in the root of the repository. Add the values above from the Azure storage account and Active Directory application. You can leave
AZURE_CACHE_CONTROL equal to
"no-cache, no-store, must-revalidate" for now. Eventually we'll want to set this to ensure the CDN caches our content, but for debugging, it's easier to skip caching.
In order to encrypt this file, copy your Codeship AES key from the project's setting page and add it to a file at the root of your repository called
jet encrypt .env .env.encrypted to save an encrypted version of your
.env file to the project. Only this encrypted file should be checked into version control. More detailed instructions for encrypting
.env files with Jet are available in Codeship's documentation.
The deployment script
Now we're ready to write a deployment script that Codeship will run each time the continuous integration passes. First, add the following to the
codeship-steps.yml files respectively:
cdn-prod-deployer: build: ./ dockerfile: ./Dockerfile.build.prod encrypted_env_file: .env.encrypted volumes: - ./docker:/docker
... # Deploy when master branch is pushed - service: cdn-prod-deployer command: bash /docker/execute.sh tag: master
This instructs Codeship to use a bash script to deploy whenever the
controller branch is updated. Now you can create this shell script in
#!/usr/bin/env bash echo "Azure Login:" az login --service-principal -u $AZURE_APP_ID -p $AZURE_SECRET_KEY --tenant $AZURE_SUBSCRIPTION_ID echo "Files to transfer:" ls /dist echo "Transferring:" az storage blob upload-batch -d $AZURE_CONTAINER_PATH -s /dist --content-cache-control "$AZURE_CACHE_CONTROL" --account-key $AZURE_STORAGE_ACCOUNT_KEY --account-name $AZURE_STORAGE_ACCOUNT_NAME echo "Deployment complete."
This file does three things:
It logs into the Azure CLI from within the Docker container that runs the script.
It lists the files in the
/distfolder so you can see all the Angular files that will be transferred.
It transfers the contents of the folder to the Azure storage container using your storage account key.
In order to verify that the new deployment script works, commit your changes and push them to your repository's controller branch. Codeship should copy the files to your Azure storage account, and you should see them in the storage container's Overview.
You can also access your application in a web browser -- albeit with serious limitations -- by navigating directly to the URL for your blob storage container's
For example, our storage account URL for the Rubric Creator app is
https://tgncdnstorage.blob.core.windows.net/rubric-creator-prod/index.html, but you'll notice that many pages show errors, and you cannot navigate directly to any URLs. In order to make this application truly usable, we'll need to put Azure's CDN in front of it.
4: Serving the Site with Azure CDN
Now that our application's files are stored in Azure's blob storage, we can use the Azure CDN to serve them, fix our URL issues, and add a custom domain.
Setting up Azure CDN
Enter a name, select a resource group (or create one), select Verizon Premium for the pricing tier, and check the box to Create new CDN endpoint now.
The Verizon Premium tier is necessary for URL rewriting. If you select a different tier, you'll have to use ugly URLs or put a webserver in front of the CDN, which kind of defeats the purpose for a single-page app like this.
When creating the endpoint, select Storage for the origin type and the origin hostname you set up in the previous step in this tutorial.
Once the endpoint has been created, go to the settings for the Origin and set the Origin path. This should be the name of the blob container set up above.
Next, you can add a custom domain (this is optional) from the Overview settings page for the CDN you set up. You can also enable SSL on the domain for free using Azure, but it will take a few minutes for the certificate to be issued and attached to your site.
Purging the CDN on updates
Whenever we push an update to our Angular app, we want to make sure that the CDN is cleared so that it uses the most recent version of our app. In order to do this, we need to purge the cache, so open up the
docker/execute.sh folder in the rubric-creator project and add the following:
... echo "Purging cache:" az cdn endpoint purge -n $AZURE_ENDPOINT_NAME --content-paths / --profile-name $AZURE_CDN_PROFILE_NAME --resource-group $AZURE_CDN_RESOURCE_GROUP echo "CDN Deployment complete."
We also need to add three variables to our
.env file and update the
AZURE_CACHE_CONTROL variable. You can get these variables from your new CDN endpoint, profile, and resource group in Azure. Mine looks something like this:
... AZURE_CACHE_CONTROL="max-age=3600" AZURE_ENDPOINT_NAME=tgn-rubric-creator-prod AZURE_CDN_PROFILE_NAME=tgn-cdn AZURE_CDN_RESOURCE_GROUP=group-production
Now the files in your Azure storage account will be cached for 3600 seconds (1 hour). This allows the CDN to store your content at the edge of its network and ensure faster delivery based on a user's location.
Once you've updated the
.env file, use Jet to reencrypt it, push the changes, and make sure the build passes and the endpoint is purged. Once the Codeship build is complete, head over to the CDN endpoint URL's
index.html file (eg,
https://tgn-rubric-creator-prod.azureedge.net/index.html) to see the site up and working.
Unfortunately, there's still the pesky
index.html path in the URL. Let's remove that using some redirect rules.
It took me a few tries to get this working because I found several resources that got me almost there but not quite. Hao Luo wrote a tutorial on deploying a Hugo app to the Azure CDN, and several questions (1, 2, 3, 4) have been asked on Stack Overflow.
After a few attempts, here's what worked for me.
Go to the CDN profile Overview page and click Manage.
This will take you to Azure's offsite CDN management system.
Use the top navigation bar to go to HTTP Large > Rules Engine, and you should see the HTTP Large Object Rules Engine.
Create a rule called Rewrite index.html.
Enter If and Always in the first selection box. This makes sure the rule always runs.
Add a Feature and select URL Rewrite.
For Source, select the CDN endpoint you just created.
In the pattern box, enter
For Destination, select the same endpoint.
In the pattern box, enter
Create another Feature that is a URL Rewrite and has exactly the same configuration as the first, but enter
((?:[^\?]*/)?[^\?/.]+)($|\?.*)in the Source pattern box.
Mine ended up looking like this:
You can also add a redirect rule to force users to
HTTPSif you'd like. This process is covered in the Azure CDN documentation.
Now, visit your app at the endpoint URL or the custom domain (if you set one up), and it should be up and running!
While the initial setup to host an Angular application on Azure's CDN takes some effort, once the deployment process is automated with Codeship, you shouldn't have to mess with it much. The great thing is that your Angular application's hosting cost should be very low for the amount of traffic it can serve, as Azure's edge storage is very efficient for distributing your files globally.
Of course, using the CDN is not the only way to host an Angular 2+ app on Azure. If speed is less of an issue and you prefer a simpler method, you can also use Codeship and Azure Web Apps, or even GitHub Pages (albeit with a "hack" to get the URL structure right) to host your Angular applications.