Overview

In this article we will be looking at how to setup, build and deploy a vanilla Angular Single Page Application (SPA) on Google CloudRun using GitHub Actions. 

If you, like me, have struggled in the past in finding a suitable Cloud service where to run your Angular applications, this article might be of interest. 

I wanted a full stack environment that would allow me to: 

  • Check in changes to my Angular SPA in GitHub
  • Automatically trigger a build that would run tests
  • Deploy the app on the Cloud so that it would be available via a URL

Before GitHub Actions and Container As A Service (CaaS) were in the picture, my traditional setup was: 

  • Check in the code to GitHub
  • Use a CI/CD tool (like Jenkins or Circle CI)
  • Use a service like Heroku and Vercel

The problem with this type of stack was that these were all different tools and I wasn’t in control of them or the infrastructure. Additionally, even for Angular apps that I wrote to learn the language, or for experiments, I had to pay the cost of keeping the service always on, even if very few people accessed it. 

Meet the GitHub Actions / Cloud Run (GACR) stack

This is my favourite DevOps stack now: 

  • GitHub for Source Code Management
  • GitHub Actions for CI/CD pipelines
  • Google Cloud Run for running Microservices

The advantages of Container As A Service (CaaS)

With Container As A Service platforms (CaaS), a Cloud provider offers a platform where to deploy Microservices. So what’s new about that? There is a plethora of container platforms already in existence (e.g. all the Kubernetes platforms like GKE or EKS or AKS). While they are no doubt a huge improvement compared to more traditional platforms of the past (e.g. virtual machines or physical environments) they have a couple of characteristics that are not always desirable: 

  • Microservices that run on these platforms are always on. This means you pay for them even if nobody accesses them 
  • With platforms like GKE and AKS you need to manage your Kubernetes Clusters. This requires significant knowledge, effort and incurs significant costs. 

CaaS platforms, like Google Cloud Run or Amazon Fargate have main advantages that address the issues above: 

  • They manage the container infrastructure for you
  • You can set them up to serve content (and therefore pay for usage) only when clients access your services
  • You can scale down to zero. This means that if nobody accesses your Microservice, the containers will not be running and you won’t pay for usage

The last two points are especially important for individuals (like developers), Startups and small businesses. Before CaaS, choosing a Cloud Kubernetes platform meant that we had to pay not just for the platform but also for each Microservice running on it. This represented a barrier to experimentation and research because people without funding wouldn’t feel encouraged to try Microservices on the Cloud. I used to resort to deploying Microservices on GKE, quickly test them and switch them off because didn’t want to pay for something non-commercial. CaaS removes that barrier and therefore fosters innovation and experimentation. 

The setup for this article

I’ll be upfront with you. The setup to run a full CI/CD pipeline using GitHub actions and Google Cloud Run as described in this article is not simple. However my promise to you is that once you get it, you will not want to go back. Also, the approach described in this article is not the only way. One could setup a Cloud Run service to connect to GitHub and build and deploy the service every time code is pushed. However, I prefer coding my CI/CD pipelines as I get better control over what’s happening

To follow the steps in this article you will need the following: 

I’ll share a way to install and configure what’s needed. This is not the only way and the setup has not been not hardened for production; however it works for me. Also the setup is for Mac (it should work very similarly on *Nix operating systems). If you’re running on Windows you will need to Google how to setup the equivalent for your operating system. 

Setting up Homebrew

Homebrew is a package manager that works well on Mac OS. It’s very easy to install. You can just run the following commands: 

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

brew update

brew doctor

Next update your shell path by adding brew to it (either in your bash_profile or .zshrc files. You’ll need to source these files or restart your shell once done for it to take effect): 

export PATH="/usr/local/bin:$PATH"

This gives you the brew command which in turns allows you to install many packages, including the angular-cli used in this article. 

Setting up npm and Node with Homebrew

To install npm and Node with Homebrew run the following command: 

brew install node

To test your installation you can run the following commands. On my terminal I get the results that I show with a ‘->’ 

npm -v
-> 9.6.5
node -v
-> v18.16.0

If you see the above, congratulations, you have installed npm and Node successfully

Setting up Angular with npm

Setting up Angular is really easy. You can use npm to install the Angular Client, which is all you need to start creating Angular applications.

npm install -g @angular/cli

To check your installation you can run the following command: 

ng version

ng is the command for the Angular client which we will use in this article

Enabling GitHub Actions

All free and paid accounts have GitHub Actions enabled by default. GitHub provides, at the time of writing, 2,000 free minutes of GitHub Actions, which should give you plenty of minutes to run the builds described in this article. 

To check your setup, head to your GitHub profile and choose Settings. On the left, you should see a Menu item called Actions. Here it’s how it looks for me: 

Google Cloud setup

To use Google’s services you will need the following: 

  • An account
  • A project
  • the gcloud command line

While you should be able to setup a Google account and a project, you will need to install the gcloud command for your shell. Thankfully, this is very easy. Just follow the instructions on this page

Please make sure you initialise your Google environment as described in the above link. 

Setting up Google Cloud Identity Federation

I have created a blog that explains how to set this up. Please follow the instructions there.

Building the Angular application

The code for this article is available here.

Building a vanilla angular application is really easy. Just follow these steps: 

  • Open a terminal and point it to a folder where you would like the code to reside
  • Run the following command (in this article we will call the application: angular-cloudrun-example)
ng new angular-cloudrun-example
  • When asked if you need routing, say No
  • Press enter to select CSS as default styling
  • That’s it! After a while the Angular cli will create the folder angular-cloudrun-example
  • cd into that folder and run the following command: 
ng serve

If everything goes well, head to http://localhost:4200 and you should see something similar to this: 

Congratulations, you have a valid Angular app running.

Preparing the Angular app for Cloud Run

An Angular application runs on Node and applications on Cloud Run are deployed as Docker containers. So we will need to setup few things in the Angular app for this to work. 

Set up the entry point to be served by Node

The first thing we’ll do is to create an index.js file that will be used as the entry point in our application. The file is very simple: 

var express = require("express");
var app = express();

app.use(express.static("dist/angular-cloudrun-devops"));
app.get('/', function (req, res) {
  res.redirect('/');
});

app.listen(4200);

The above instructs Express (a Node web framework) to serve all static content from the dist/angular-cloudrun-devops folder. The dist folder is created when building the Angular application. We can build a vanilla Angular app with the command npm run build which in turn invokes the command ng build. This scripts are defined in the package.json file in the root of your project. 

If you run npm build from the root of your project, it will create the dist folder with a folder containing the content that runs our application. 

Since we’re talking of the package.json file, please open it in your editor and under the scripts session, add the following command, which we will use later during the Continuous Integration of our app: 

"test:prod": "ng test --browsers=ChromeHeadless --watch=false --code-coverage"

Defining the Docker build

The Docker definition is also quite simple. At the roon of your project, create a Dockerfile with the following contentng content: 

FROM node:18-alpine
WORKDIR /usr/app
COPY ./ /usr/app
RUN npm install -g @angular/cli
RUN npm install
RUN npm run build
EXPOSE 4200
CMD ["node", "index.js"]
  • It builds the image on top of Node. I’m using version 18-alpine. Feel free to use whichever Node version you prefer, although 18.x+ would be preferred. 
  • It copies all the content from the Angular app to the /usr/app folder on the container
  • It installs the Angular cli on the container
  • It runs npm install on the container to install all the dependencies required by the Angular app
  • It runs npm build so that a distributable and executable version of our app is created
  • It exposes port 4200 on the container (the default port for Angular apps)
  • It declared that upon starting the container, the command node index.js should run. We created the index.js file in one of the steps above

Create a .dockerignore file in the root of your project with the content below. This will avoid unnecessary files being sent to the Docker server during the build. Especially the node_modules folder contains a lot of libraries and none are required for the Docker build process as the Docker build will download all the necessary libraries on the container:

**/node_modules/
**/dist
.git
npm-debug.log
.coverage
.coverage.*
.env

Let’s build the first image now and push it to the Google container registry

Run the following commands from a terminal pointed at the root of your project (change the Google cloud region as per your requirements. Consult the Google documentation for the available regions for the Google container registry. I have chosen eu.gcr.io because I’m based in the UK). Please note that you will have to replace ‘blogging-384618’ with your Google project id: 

docker build -t eu.gcr.io/blogging-384618/angular-cloudrun-devops .
docker push eu.gcr.io/blogging-384618/angular-cloudrun-devops

If everything goes well, you should see this image in the Google Container Registry for your project. 

Creating a Cloud Run service

Before enabling continuous CI/CD with GitHub Actions, we want to create a Cloud Run service. We can create the Cloud Run service with the following command (replace the values in <> with your own): 

gcloud run deploy <service-name> --image <image-name> --region <your-region> --platform managed --allow-unauthenticated --port 4200 --min-instances 0 --max-instances 1

To verify, head off to your Google console and search for Cloud Run. Select it and you should see the service running with a green tick (I called this service auto-generated). 

Verify that the region is set to your region and “Allow unauthenticated” appears. 

You can click on the service and then on “Edit and Deploy New revision”. 

  • Verify that the min number of instances is 0 and max is 1
  • Verify the port is set to 4200

Defining and enabling the CI/CD pipeline with GitHub Actions

Now that the service is in place and we have verified it works, we can create a GitHub action following these steps: 

  • Create a .github/workflows folder at the root of your project
  • Create a build.yml (the name can be anything as long as it’s a YML file) file under the workflows folder with the following content (replacing the values with the ones for your project): 
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs

name: Node.js CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:

    permissions:
      contents: 'read'
      id-token: 'write'

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
    - uses: actions/checkout@v3
    - name: Builds code and run tests
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    - run: npm ci
    - run: npm run test:prod

    - uses: 'actions/checkout@v3'

    - uses: 'google-github-actions/auth@v1'
      with:
        workload_identity_provider: 'projects/<project-id>/locations/global/workloadIdentityPools/<service-account-name>/providers/<provider-id>'
        service_account: '<service-account-email>'

    - id: deploys-image-to-gcr
      uses: RafikFarhad/push-to-gcr-github-action@v5-beta
      with:
        registry: gcr.io
        project_id: blogging-384618
        image_name: angular-cloudrun-devops
        image_tag: latest

    - id: 'deploy'
      uses: 'google-github-actions/deploy-cloudrun@v1'
      with:
        service: 'angular-cloudrun-devops'
        region: 'europe-west2'
        image: 'eu.gcr.io/blogging-384618/angular-cloudrun-devops:latest'

    - name: 'Use output'
      run: 'curl "${{ steps.deploy.outputs.url }}"'

Let’s analyse the main parts of this Action: 

  • Here we are instructing GitHub to run this action only on pushes and PRs on the main branch. 
  • The next part with “permissions” is really important. By setting the id-token permission to ‘write’ we’re instructing GitHub to create the token file that will be sent to GCP during the build. 
  • Here we’re instructing GitHub to use Node version 18 and we’re executing the npm run test:prod script added previously. This runs all the tests in the Angular app. 
  • Here we’re instructing GitHub to checkout the code
  • To Authenticate with Google. This is the most important part and one where people tend to struggle. Please note the value for “workload_identity_provider” and “service account“. You will need to replace my values with yours.
    • The format for workload_identity_provider is: projects/<project number>/locations/global/workloadIdentityPool/<service account>/providers/<name of provider>. 
    • Luckily for you, you can copy this value from the Google cloud console for the Identity Service

Just copy everything from projects onward

  • The value for service account can be copied directly from the Google console under the Service Accounts menu, for the service account you created earlier: 
  • The “deploys-image-to-gcr” id in the build file instructs the build to deploy the Docker image to the gcr repository (basically doing automatically what we earlier did manually). 
  • Finally we instruct GitHub to deploy this image to Cloud Run using the Google GitHub action. You will need to change the values with the ones belonging to your project and set your preferred region. 

Time to run the CI/CD pipeline

Running the entire DevOps cycle is now easy. Create a repository in GitHub and push your local code to it. Upon pushing, GitHub will detect that there is an action to run and will run it. Now you can make continuous changes to your code, push them and have your c

Facebook
Twitter
LinkedIn

© Techwings Limited – 2023

techwings.io is a registered trademark in the UK: UK00003892059