Serving an Angular App on Azure's CDN with Codeship and Docker

Written by: Karl Hughes
12 min read

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"]

CDNs visualized[/caption]

This is perfect for a single-page JavaScript app like Angular because unlike a server-side application, users just need to download the assets and then their browser does all the work. The downside to delivering your application via a CDN is that CDNs typically can't perform server-side logic or run build scripts, so you'll have to find another solution for that part of your process.

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-tutorial that 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.

  • We'll build and deploy the app using Codeship, Azure cloud storage, and Docker.

  • 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

For this tutorial, we'll use the open-source Rubric Creator we maintain at The Graide Network. You can see a final version of the app at rubriccreator.com.

Local setup

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 git@github.com:<your_github_username>/rubric-creator.git
cd rubric-creator</your_github_username>

Next, install the local npm dependencies and the latest version of Angular CLI if you have't already:

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-services.yml and codeship-steps.yml file to the root directory of your project:

codeship-services.yml

version: "3.5"
services:
  frontend:
    build: ./

codeship-steps.yml

- 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 lint, 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.

In order to do this, we'll need to create a new blob storage account in Azure using either the Azure portal or the Azure command line interface. I prefer the portal, but either way should work.

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

Any application that accesses Azure without your username and password needs to be registered in Active Directory. This can be done with the CLI or the Azure portal.

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.

Copy the .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 codeship.aes.

Finally, run 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-services.yml and codeship-steps.yml files respectively:

codeship-services.yml

  cdn-prod-deployer:
    build: ./
    dockerfile: ./Dockerfile.build.prod
    encrypted_env_file: .env.encrypted
    volumes:
      - ./docker:/docker

codeship-steps.yml

...
  # 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 docker/execute.sh:

#!/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 /dist folder 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 index.html file.

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

First, you need to create an Azure CDN profile and endpoint. You can search for CDN or visit this URL to bring up the prompt.

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.

Rewriting URLs

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 rubric-creator-prod/index.html$2.

    • 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:

Now, visit your app at the endpoint URL or the custom domain (if you set one up), and it should be up and running!

Conclusion

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.

Finally, if you have any questions or hit a snag, feel free to leave a comment below, find me on Twitter, or report an issue in our Rubric Creator repository.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.