What You'll Be Building

In this codelab, we'll dive into the Google Cloud and build micro-services using Docker containers, backed by data powered by Firebase Cloud Firestore, tackling a fictitious e-commerce site.

This is the outcome of what we'll be building during this session:

MicroServices UI

Sinopsis

This is a fictitious e-commerce site called "What's Buggin' You?" that deals with selling bug-killing pesticide products. Customers can order products from a product list, and add them to their shopping cart for later purchase.

In a matter of minutes, you'll become familiar with building Docker containers to build a micro-services architecture in the Google Cloud while learning the following:

No installation required!

We'll use the Google Cloud Console, the Firebase Console and the Google Cloud Shell, and leverage Google infrastructure FOR FREE to complete this hands-on workshop.

A Gmail account is all you need pretty much!

Let's proceed!

The following architecture is what we'll be accomplishing in this session:

MicroServices UI

This application will consist of the following:

Each of the four main features of this e-commerce site will be developed independently and in isolation, however they will communicate with one another in a decoupled fashion using the real-time capabilities of Firebase, hence the reason why each will be deployed as a separate container, and each will manage its own data store using Firebase Cloud Firestore.

Building Containerized Applications

Before diving into containerized applications, you must learn two basic concepts, such as containers and images and what they represent in this whole app packaging paradigm.

What is Docker?

Docker is an open platform for developing, shipping, and running applications. Docker enables you to separate your applications from your infrastructure so you can deliver software quickly.

MicroServices UI

What are Containers?

A container is a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another. It is basically a runnable instance of an image.

MicroServices UI

What is an Image?

MicroServices UI

A Docker image is a lightweight, standalone, executable package of software that includes everything needed to run an application: code, runtime, system tools, system libraries and settings.

An image is a read-only template with instructions for creating a Docker container. Often, an image is based on another image, with some additional customization. For example, you may build an image which is based on the ubuntu image, but installs the Apache web server and your application, as well as the configuration details needed to make your application run.

Images become containers at runtime and in the case of Docker containers – images become containers when they run on Docker Engine, the industry-leading container runtime, allowing containerized applications to run anywhere consistenly on any infrastructure.

You can learn more about Docker, containers and images and how containers differ from virtual machines by following this link.

What is Firebase?

MicroServices UI

Firebase is an app development platform that helps you build and grow apps and games users love. Backed by the Google Cloud and trusted by millions of businesses around the world, and it contains a suite of products for building robust mobile, web and desktop apps that can scale to billions of users.

In this session we'll be using Firebase Cloud Firestore, a NoSQL document database that lets you easily store, sync, and query data for your mobile and web apps - at global scale. One of its best features is its serverless nature, in which you can stand up a Firebase backend without provisioning your own servers. You can access Cloud Firestore with traditional client libraries such as Node, Python, Go and Java.

Firebase Data Model

Unlike a SQL database, there are no tables or rows. Instead, you store data in documents, which are organized into collections.

MicroServices UI

Each document contains a set of key-value pairs. Cloud Firestore is optimized for storing large collections of small documents.

All documents must be stored in collections. Documents can contain subcollections and nested objects, both of which can include primitive fields like strings or complex objects like lists.

Documents

In Cloud Firestore, the unit of storage is the document. A document is a lightweight record that contains fields, which map to values. Each document is identified by a name.

MicroServices UI

Collections

Documents live in collections, which are simply containers for documents. For example, you could have a products collection to contain your various products, each represented by a document:

MicroServices UI

Cloud Firestore is schemaless, so you have complete freedom over what fields you put in each document and what data types you store in those fields. Documents within the same collection can all contain different fields or store different types of data in those fields. However, it's a good idea to use the same fields and data types across multiple documents, so that you can query the documents more easily.

In Firebase is how we will store the data that will power our application, also we'll leverage one of its most powerful features: real-time updates.

Pulling Data from Firebase

There are three ways to retrieve data stored in Cloud Firestore. Any of these methods can be used with documents, collections of documents, or the results of queries:

When you set a listener, Cloud Firestore sends your listener an initial snapshot of the data, and then another snapshot each time the document changes.

Get Realtime updates with Cloud Firestore

You can listen to a document with the onSnapshot() method. An initial call using the callback you provide creates a document snapshot immediately with the current contents of the single document. Then, each time the contents change, another call updates the document snapshot.

Later when we build our product features, we will use the Firebase SDK for web to tap into the Firebase functionality that will allow us to both retrieve and listen to changes on our Cloud Firestore database, as shown below:

MicroServices UI

Let's start by setting up Firebase and create the documents and collections that will hydrate our web application.

In this section we'll create a Firebase project, we'll create our first Cloud Firestore database and create two of the collections that will power our data.

Let's start by navigating to the Firebase Console by clicking on this link. Use a valid Gmail account, preferably one that you haven't used before for setting up Google Cloud or Firebase, and with no billing accounts attached to it. Remember this Gmail account for later steps in this workshop.

Create your first project by clicking on the Create a Project button:

MicroServices UI

In the wizard, provide a name for your project, and accept the terms by checking the checkboxes below:

MicroServices UI

In the next step, disable Google Analytics as we don't need it for this project (you can enable later if needed). Then, click on "Create Project":

MicroServices UI

Wait until the project gets created (this may take a few seconds):

MicroServices UI

Once completed, you should get the following message that your project is ready. Click "Continue" to proceed:

MicroServices UI

You've landed at the home page of the Firebase Console, where you can see all features in Firebase at your disposal. We'll focus on Firebase Cloud Firestore for now, so in the left navigation menu, under Build, click on Firestore Database:

MicroServices UI

In the Cloud Firestore landing page, click on "Create database":

MicroServices UI

On the first step of the database creation wizard, select to Start in test mode, which will lower the security rules on the database so we can set it up quickly (this last 30 days, so make sure to update your security rules after a month). Click "Next":

MicroServices UI

Next step deals with the location of the Cloud Firestore database. Pick a multi-regional location for increased availability - you can keep the default nam5 (us-central) if you want. Click "Enable":

MicroServices UI

Wait a few seconds until your database is provisioned in the Google Cloud:

MicroServices UI

We've landed on our Cloud Firestore database called gcloud-microservices-web. We must start now by creating collections, which will be the containers for our documents. Create your first collection by clicking on the Start collection button:

MicroServices UI

Assign it the ID of products; click "Next":

MicroServices UI

With our products collection created, you can proceed to start adding documents. Let's add a first document to this collection. You can either assign your own ID, or use the Auto-ID option.

Add the ID prod1001, and the following fields (you'll need to click on the Add field option to add the remaining fields):

Your document should look like below. Click "Save" when ready:

MicroServices UI

After saving the document, your data structure should look like this:

MicroServices UI

Add the remaining documents to the products collection (prod1002) by clicking on the Add document option, and providing their data as follows:

Add a third document (prod1003) as follows:

After adding all documents, your collection should look like this:

MicroServices UI

Let's add one more collection (orders) that will hold the orders placed by our users.

Same as before, click on the Start collection to add a new collection; add orders as its collection ID; click "Next":

MicroServices UI

Let's add one document for now since it's required to create a collection, and add the following fields, with their corresponding values:

Your new order document should look like below; click "Save":

MicroServices UI

We will come back later to the Firebase Console to grab the configuration settings that will allow us to connect to our Cloud Firestore database and pull its data.

Now we're fully set up on the backend side of things, with two collections (products - with three documents, orders with one document) and now we're ready to consume their data.

With that said, let's now package our product features as containers!

For the next step in setting up our microservices architecture, we will first connect to the Google Cloud Console and leverage Cloud Shell.

Cloud Shell is an interactive shell environment for Google Cloud that lets you learn and experiment with Google Cloud and manage your projects and resources from your web browser. It is FREE service for all Google Cloud Users, and provisions 5GB of persistent disk storage mounted.

For more information on how to use the Cloud Shell and its many features, follow this link.

To access the Google Cloud Shell, first navigate to the Google Cloud Console with the same Gmail account you used to set up your Firebase project by following this link; if prompted, agree and continue:

MicroServices UI

Once in the Google Cloud Console, you can see all features you have available, even under the FREE tier. Some features may require setting up a billing account prior to accessing them, but we will only use the Cloud Shell (both Terminal and Editor), as well as Cloud Firestore (which is free with limits).

If prompted to start a "FREE Trial", click on "Dismiss" - on the FREE Trial, you are required to put in a credit card, but you won't be charged. This workshop is ONLY for accessing the FREE TIER features, NOT THE FREE TRIAL, just so we're clear.

Access the Cloud Shell

To access the Cloud Shell, click on the terminal icon at the top right section of the console:

MicroServices UI

The bottom half of the screen opens up, showing you a message describing what the Cloud Shell is and what you have access through it. Click "Continue":

MicroServices UI

After a few seconds, Google Cloud provisions a tiny Debian-based Compute Engine VM instance with a shell interface, through which you can run commands, access a few pre-loaded tools, as well as access your 5GB of persistent disk storage mounted as your $HOME directory. This session lasts about an hour if not attended, however all files you store in your home directory persist between sessions.

MicroServices UI

In this terminal / shell experience, you can run commands, such as gcloud config list which gives you a list of the basic configuration options in your Google Cloud environment. Try running the command gcloud config list, and if prompted to authorize Cloud Shell, click on "Authorize":

MicroServices UI

Let's run another command; try running the following command:

gcloud config list project

This command should list the project in the Google Cloud that encompasses all resources running on your cloud environment.

You should get something like:

project (unset)

If so, it's because you haven't selected a project to associate your cloud resources with.

At the top left of the Google Cloud Console, locate the "Select a project" dropdown:

MicroServices UI

If you've followed all the steps correctly up until now, you should get a dialog window showing you the SAME PROJECT NAME you created when you were in the Firebase Console - this proves once and for all how tighly connected Firebase and the Google Cloud are! Firebase sits on top of the Google Cloud, which means we should be able to access our Cloud Firestore database right from here, the Google Cloud Console! We'll try that later. Click on the name of the project.

MicroServices UI

Close and re-open the Cloud Shell Terminal, and if you run the command gcloud config list project again, you should get the following output, in which you see the project properly set now:

MicroServices UI

The rest of this workshop we'll do it by creating folders and files, so if you're hardcore and want to use the terminal for all of this, suit yourself - otherwise follow along as we'll use the Cloud Shell Editor - a companion web-based editor that allows you to interact with the contents of your home direction in a GUI-based interface as opposed to through the terminal.

At the top of the Cloud Shell Terminal, locate the "Open Editor" option and click it:

MicroServices UI

Now your view switches to that of a text editor; you can switch back to the terminal if you wish to by locating the "Open Terminal" option at the top of this editor:

MicroServices UI

Once in the Cloud Shell Editor, we'll build our application structure, separating each feature into its own folder, simulating the feature and code boundaries.

We'll be building our architecture by creating a folder structure that will simulate separate features being developed by multiple teams.

Create the following folder structure, at the root of your workspace, by right-clicking anywhere on the left column of the Cloud Shell Editor, select New Folder and create each folder as follows:

MicroServices UI

Each of the features above will be developed, tested and deployed in isolation, at its own cadence, and then integrated into the e-commerce feature as a single unit. The features can communicate with each other in a decoupled fashion via messages (in this case, we're using Firebase's real-time capabilities and snapshot listeners).

Let's start developing the features one by one.

Let's work on the footer feature for this e-commerce store. Imagine a separate team working on just the footer, which contains links to content related to the products available, etc. The footer is nothing more than a simple HTML site, hosted on its own Docker container running inside an NGINX web server.

Let's start!

Locate the footer folder, and create a file called index.html by right-clicking inside the folder and selecting the New File option. Drop the code below inside this file:

<!DOCTYPE html>
<html>
    <head>
        <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;900&display=swap" rel="stylesheet">
        <style>
            body {
                font-family: 'Montserrat', sans-serif;
                padding: 0;
                margin: 0;
                background: #000;
            }

            .footer {
                color: #FFF;
                padding: 30px;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }

            .footer > .material-icons {
                flex: 1;
                font-size: 60px !important;
            }

            .footer-nav {
                flex: 2;
                display: flex;
                justify-content: end;
            }

            .footer-nav > ul {
                list-style: none;
                display: inline-flex;
            }

            .footer-nav > ul > li {
                padding: 0px 20px;
            }
        </style>
    </head>
    <body>
        <footer>
            <div class="footer">
                <span class="material-icons">pest_control</span>
                <nav class="footer-nav">
                    <ul>
                        <li>About</li>
                        <li>Contact Us</li>
                        <li>Products</li>
                    </ul>
                </nav>
            </div>
        </footer>
    </body>
</html>

Now, in order to deploy this as an independent, separate, isolated container, you need to add a Dockerfile, which is simply a text-based script containing instructions on how to create a container image. We'll create the image first, then deploy a container based on that image.

Create the Dockerfile

Right-click inside the footer folder, and add a file called Dockerfile; drop the code below inside this file:

MicroServices UI

FROM nginx:latest
COPY . /usr/share/nginx/html/

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Notice the script above; our Docker image will be based on the latest version of the base NGINX image publicly available. The COPY command will copy the contents of the current directory into the /usr/share/nginx/html/ path, once the container is up and running. We will expose port 80 since this is a web server, and then set the daemon option to off. This directive tells Nginx to stay in the foreground. For containers this is useful as best practice is for one container = one process. One server (container) has only one service.

Build the Footer Docker image

With the code and the Dockerfile in place, it's time to build the image from which we'll create our container later.

Open up a Terminal (either by clicking on the Terminal > New Terminal menu option or clicking on the Open Terminal button to switch back to the Cloud Shell). Make sure to be inside of the footer folder to run the following commands.

MicroServices UI

Run the following command:

docker build -t footer-img .

The above docker build command creates an image with the tag footer-img (with the -t command), the dot (.) at the end means to take the current folder contents to build the image. You must have a Dockerfile at the root of the folder from where you'll build your image.

If prompted to authorize the shell, make sure to do so:

MicroServices UI

After a few seconds, if all went well, you should get a message like this at the terminal:

Successfully built a355943b6e28
Successfully tagged footer-img:latest

Verify that the Docker image got built successfully by running the command docker image ls, which lists the images that have been built by you and images that have been pulled down. You will see an output like this:

$> docker image ls

REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
footer-img   latest    a355943b6e28   6 minutes ago   142MB
nginx        latest    2b7d6430f78d   2 weeks ago     142MB

You should see both images footer-image and nginx.

Run the Footer Container

With the images already built, now let's proceed to run a container based on our newly created footer-image.

Still inside the footer folder, run the following command:

docker run -d -p 8083:80 --name footer-container footer-img

Let's dissect this command:

Verify that the container is successfully running by running the following command; you should get similar output to this:

$> docker ps

CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS         PORTS                  NAMES
bd35ac7da8a3   footer-img   "/docker-entrypoint...."   4 minutes ago   Up 4 minutes   0.0.0.0:8083->80/tcp   footer-container

Now, to preview it on the browser and viewing the contents being served from this container, locate the Web Preview option, click on Change port:

MicroServices UI

Change it to port 8083 which maps to the footer-container container, then click on Change and Preview:

MicroServices UI

You will get a browser window that launches with the URL loading the contents of the footer-container web server, showing the footer web page; you should see something like this:

MicroServices UI

We have a fully functional Docker container running a containerized web app that represents just the footer of the application, in a very encapsulated, self-contained and independent fashion.

Let's keep moving along and work on the rest of the features!

The shopping cart feature of this e-commerce site will keep track of the products the users order. For the sake of this workshop, we'll make it so one product maps to a single order. The shopping cart will display the orders made as well as a way to clear the contents of the cart.

This feature can be developed also in isolation, or by a completely separate team.

Let's start!

Set up a Firebase Web App to get the Connection details

Before we proced, we must go back to the Firebase Console and set up a web app, from which we'll grab the configuration details in order to be able to connect to Firebase Cloud Firestore DB from both our shopping cart and product features.

Go back to the Firebase Console with your existing credentials. Click on Add App:

MicroServices UI

Then, select the platform - in our case, Web:

MicroServices UI

In the wizard that appears, on the first step, register your web app by using a nickname - do not select Firebase Hosting as our web apps will already be hosted within containers running NGINX:

MicroServices UI

The next step will show you how to add the Firebase SDK, either via NPM (if you're building backend apps) or by using a script tag (if you're building web-based client apps). Select the Use a <script> tag to get the corresponding Firebase configuration.

MicroServices UI

Copy the block of code that looks like the following - store it somewhere since we'll need it later:

// COPY YOUR OWN CONFIGURATION FROM YOUR FIREBASE PROJECT
const firebaseConfig = {
    apiKey: "AIzaSyAmOSK7giyvrWqloOAW8kRwcOzisdmo9GU",
    authDomain: "fir-data-360222.firebaseapp.com",
    projectId: "firebasedata-360222",
    storageBucket: "firebasedata-360222.appspot.com",
    messagingSenderId: "849197791561",
    appId: "1:849197791561:web:d223eaf691532593134298"
};

Click on Contine to Console.

After this step, you can close the Firebase Console and get back to the Google Cloud Console to finish the rest of the features.

Develop the feature

Locate the shopping-cart folder we created earlier. Right-click on the folder and create an index.html file. Drop the contents of the code below inside this file:

<!DOCTYPE html>
<html>
    <head>
        <style>
            body {
                font-family: 'Montserrat', sans-serif;
                padding: 0;
                margin: 0;
            }

            .header {
                background: #FFE200;
                padding: 30px;
                display: flex;
                align-items: center;
                justify-content: space-between;
            }

            .header > .material-icons {
                font-size: 80px !important;
            }

            .header-main-title {
                flex: 1;
                margin-left: 10px;
            }
            
            .header-title {
                font-weight: bold;
                font-size: 40px;
            }

            #shopping-cart {
                display: flex;
                align-items: center;
            }

            #shopping-cart > .material-icons {
                font-size: 30px !important;
            }

            #shopping-cart > .trash {
                margin-left: 20px;
                cursor: pointer;
            }

            #shopping-cart > #cart-items {
                font-size: 30px;
                margin-left: 10px;
                font-weight: bold;
            }
        </style>
        <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;900&display=swap" rel="stylesheet">
    </head>
    <body>
        <div class="header">
            <span class="material-icons">pest_control</span>
            <div class="header-main-title">
                <div class="header-title">What's Bugging' you?</div>
                <div class="header-subtitle">Pest Control Products Online Store</div>
            </div>
            <div id="shopping-cart">
                <span class="material-icons">shopping_cart</span>
                <div id="cart-items"></div>
            </div>
        </div>
        <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script>
        <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-firestore.js"></script>
        <script type="module">
            
            // <YOUR OWN FIREBASE PROJECT CONFIGURATION>
            /*
            const firebaseConfig = {
              apiKey: "AIzaSyAmOSK7giyvrWqloOAW8kRwcOzisdmo9GU",
              authDomain: "fir-data-360222.firebaseapp.com",
              projectId: "firebasedata-360222",
              storageBucket: "firebasedata-360222.appspot.com",
              messagingSenderId: "849197791561",
              appId: "1:849197791561:web:d223eaf691532593134298"
            };
            */
          
            // Initialize Firebase
            firebase.initializeApp(firebaseConfig);
            const firestore = firebase.firestore();

            // listen to changes occurring on the "orders" collection
            firestore.collection('orders').onSnapshot((snapshot) => {
                document.getElementById('cart-items').innerText = snapshot.docs.length;

                let foundTrashCan = document.getElementById('shopping-cart').getElementsByClassName('trash');
                if (foundTrashCan && foundTrashCan.length > 0) {
                    document.getElementById('shopping-cart').removeChild(foundTrashCan[0]);
                }

                if (snapshot.docs.length > 0) {
                    let trashCanIcon = document.createElement('span');
                    trashCanIcon.className = 'material-icons trash';
                    trashCanIcon.innerText = 'delete_forever';
                    document.getElementById('shopping-cart').appendChild(trashCanIcon);
                    
                    // upon clicking on the trash icon,
                    // delete all orders from the "orders" collection
                    trashCanIcon.addEventListener('click', async () => {
                        let allOrders = await firestore.collection('orders').get();
                        allOrders.docs.forEach((doc) => {
                            doc.ref.delete();
                        });
                    });
                }
            });
        </script>
    </body>
</html>

In the HTML code above, find the line YOUR OWN FIREBASE PROJECT CONFIGURATION and replace it with your own firebaseConfig object (retrieved from the Firebase Console when we registered the web app).

Let's inspect the code a little bit.

We import both firebase-app.js and firebase-firestore.js JS libraries from publicly available CDNs; this the Firebase Javascript SDK that provides web apps with the Firebase functionality to fetch, query and store data in Firebase Cloud Firestore.

We initialize the firebase instance by calling firebase.initializeApp and feeding the firebaseConfig to it.

Then we get a reference to the firestore so we can access our Cloud Firestore database and pull its data.

We then listen to changes on a specific collection (orders) using the onSnapshot method and attaching a callback to it, which will be triggered upon orders being added / removed.

We pull the data from the snapshot received by querying its docs, and parse it accordingly to display the number of orders.

We also wire up a click event to our trash icon so inside we can do a delete on all document references by looping through them and deleting them using doc.ref.delete().

Create the Dockerfile

Still inside the shopping-cart folder, right-click and create a new file called Dockerfile; drop the code below inside this file:

FROM nginx:latest
COPY . /usr/share/nginx/html/

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

The code above will be pretty much the same for all web apps we'll be building, just so you know. It will use the base NGINX image publicly available, copy all files inside this folder, expose port 80 and run NGINX with the the daemon option off.

Let's build the image.

Build the Shopping Cart Docker image

With the code and the Dockerfile in place, let's build its corresponding image.

If you still have the Terminal open, navigate to the shopping-cart folder, or open a new one.

Run the following command:

docker build -t shopping-cart-img .

Same as before, we will be building a Docker image tagged shopping-cart-img from the contents of this folder.

After a few seconds, you should get a successful message that the image was built.

Verify that the Docker image got successfully build by running the docker image ls command, where you should see the newly created image:

$> docker image ls

REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
footer-img          latest    b2b5a750d832   16 seconds ago   142MB
shopping-cart-img   latest    5a393cb354c3   52 seconds ago   142MB
nginx               latest    2b7d6430f78d   2 weeks ago      142MB

At this point you should see all images being built up to this point.

Run the Shopping Cart Container

Still inside the shopping-cart folder, run the following command:

docker run -d -p 8081:80 --name shopping-cart-container shopping-cart-img

With the above command, we are running a container named shopping-cart-container based off of the shopping-cart-img image created earlier, and exposing the internal port 80 to the external port 8081; notice we're using a separate port for this web app. Make sure to honor this so there are no conflicts between ports.

Verify that the container is successfully running, along the footer-container container by running docker ps:

$> docker ps

CONTAINER ID   IMAGE               COMMAND                  CREATED          STATUS          PORTS                  NAMES
c3ff8fd8a4ef   shopping-cart-img   "/docker-entrypoint...."   3 seconds ago    Up 2 seconds    0.0.0.0:8081->80/tcp   shopping-cart-container
9c91e7825dbc   footer-img          "/docker-entrypoint...."   25 seconds ago   Up 25 seconds   0.0.0.0:8083->80/tcp   footer-container

Do the same exercise as before to preview the shopping cart web app in the browser by activating the Web Preview feature, changing the port to 8081, then clicking on Change and Preview. You should get another browser window with the following output:

MicroServices UI

As you can see, this feature will represent the header portion of our e-commerce site which encapsulates the functionality of a simple shopping cart, showing the number of orders placed and the capability to clear the orders from the cart. Simple as that.

With this completed, let's proceed to do the products feature.

The product feature of this e-commerce site will display all products available in the inventory (coming from the products database we populated earlier). Users are able to see the product information, and clicking on any product in particular to add them to their cart. This feature communicates seamlessly with the shopping-cart functionality via well-synchronized, orchestrated updates to both the orders and products collections, and listening to the corresponding changes occurring to those collections, and processing the responses accordingly.

Let's build it!

Locate the products folder we created earlier. Right-click on the folder and create an index.html file. Drop the contents of the code below:

<!DOCTYPE html>
<html>
    <head>
        <style>
            body {
                font-family: 'Montserrat', sans-serif;
                padding: 0;
                margin: 0;
                position: absolute;
                top: 0;
                bottom: 0;
                left: 0;
                right: 0;
            }

            .bug-product {
                padding: 30px;
                margin: 5px;
                text-align: center;
                border-radius: 20px;
                border: 5px solid #FFF;
                cursor: pointer;
                position: relative;
            }

            .bug-product:hover {
                border: 5px solid #f4f4f4;
            }

            .bug-product > img {
                width: 200px;
                height: 200px;
                margin-bottom: 10px;
            }

            .bug-product-title {
                font-weight: bold;
                width: 200px;
                text-align: center;
                margin: 0px auto;
            }

            .bug-product-description {
                color: grey;
                text-overflow: ellipsis;
                white-space: nowrap;
                width: 200px;
                overflow: hidden;
                text-align: center;
                margin: 10px;
                display: inline-block;
            }

            .bug-product-price {
                font-weight: bold;
            }

            .bug-product > .in-cart {
                position: absolute;
                font-size: 40px;
                color: #bbbb00;
                top: 0;
                right: 0;
                padding: 20px;
            }

            .bug-product-btn {
                background: #FFE200;
                border-radius: 50px;
                padding: 10px 20px;
                display: inline-flex;
                align-items: center;
                font-weight: bold;
                margin-top: 10px;
                cursor: pointer;
            }

            .bug-product-btn-label {
                margin-left: 20px;
            }

            .main-content {
                flex: 1;
                height: 100%;
            }

            .main-content-wrapper {
                padding: 30px;
                display: flex;
                flex-direction: column;
            }

            .main-content-wrapper > h2 {
                padding: 0px;
                margin: 0px;
            }

            #main-grid {
                margin-top: 20px;
                display: grid;
                grid-template-columns:  1fr 1fr 1fr;
            }

            @media screen and (max-width: 930px) {
                #main-grid {
                    grid-template-columns:  1fr !important;
                }
            }

            #product-loading-container {
                flex: 1;
                display: flex;
                justify-content: center;
                align-items: center;
                text-align: center;
                color: #c3b962;
            }

            #product-loading-container .material-icons {
                font-size: 50px;
            }
        </style>
        <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;900&display=swap" rel="stylesheet">
    </head>
    <body>
        <section class="main-content">
            <section class="main-content-wrapper">
                <h2>Bug-Killing Products</h2>
                <div>Please select one of our best-selling products:</div>
                <section id="main-grid"></section>
                <section id="product-loading-container">
                    <div class="product-loading">
                        <div><span class="material-icons">bug_report</span></div>
                        <div>Loading Products...</div>
                    </div>
                </section>
            </section>
        </section>
        <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script>
        <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-firestore.js"></script>
        <script type="module">
            
            // <YOUR OWN FIREBASE PROJECT CONFIGURATION>
            /*
            const firebaseConfig = {
              apiKey: "AIzaSyAmOSK7giyvrWqloOAW8kRwcOzisdmo9GU",
              authDomain: "fir-data-360222.firebaseapp.com",
              projectId: "firebasedata-360222",
              storageBucket: "firebasedata-360222.appspot.com",
              messagingSenderId: "849197791561",
              appId: "1:849197791561:web:d223eaf691532593134298"
            };
            */
          
            // Initialize Firebase
            firebase.initializeApp(firebaseConfig);
            const firestore = firebase.firestore();

            // listen to changes occurring on the "orders" collection
            firestore.collection('orders').onSnapshot((snapshot) => {
                
                clearAllInCartItems();

                // loop through 
                snapshot.docs.forEach((doc) => {
                    let prodInCart = document.getElementById(doc.id);
                    
                    if (prodInCart) {

                        let prodInCartEls = prodInCart.getElementsByClassName('in-cart');

                        if (prodInCartEls && prodInCartEls.length == 0) {
                            const addedToCardIndicator = document.createElement('span');
                            addedToCardIndicator.className = 'material-icons in-cart';
                            addedToCardIndicator.innerText = 'task_alt';
                            prodInCart.appendChild(addedToCardIndicator);
                        }
                    }
                });
            });

            // listen to changes in the "products" collection
            firestore.collection("products").onSnapshot((snapshot) => {
                document.getElementById('main-grid').innerHTML = '';
                document.getElementById('product-loading-container').remove();
                
                snapshot.docs.forEach(async (doc) => {
                    const data = doc.data();
                    await generateProductContent(doc.id, data);
                });
            });

            // clear all items in the cart
            function clearAllInCartItems() {
                let allBugProducts = document.getElementsByClassName('bug-product');
                if (allBugProducts && allBugProducts.length > 0) {
                    for(let p = 0; p < allBugProducts.length; p++) {
                        let prod = allBugProducts[p];
                        let inCartElements = prod.getElementsByClassName('in-cart');
                        if (inCartElements && inCartElements.length > 0) {
                            for(let i = 0; i < inCartElements.length; i++) {
                                prod.removeChild(inCartElements[i]);
                            }
                        }
                    }
                }
            }

            // check whether a product, provided its unique id
            // is in the cart, so we can show a little
            // checkbox on it
            async function isProductInCart(id) {
                let docRef = await firestore.collection('orders').doc(id).get();
                return docRef.exists ? 
                '<span class="material-icons in-cart">task_alt</span>': '';
            }

            // builds the HTML template to display the product information
            async function generateProductContent(id, data) {
                let product = document.createElement('div');
                product.className = 'bug-product';
                product.id = id;
                let isProduct = await isProductInCart(id);
                product.innerHTML = `
                    ${ isProduct }
                    <img src="${data['img']}">
                    <div class="bug-product-title">${data['name']}</div>
                    <div class="bug-product-description">${data['description']}</div>
                    <div class="bug-product-price">$${data['price']}</div>
                    <div class="bug-product-btn">
                        <span class="material-icons">add_shopping_cart</span>
                        <span class="bug-product-btn-label">Add To Cart</span>
                    </div>`;
            
                document.getElementById('main-grid').appendChild(product);
                
                // upon clicking on a product
                // add it to the "orders" collection
                product.addEventListener('click', () => {
                    firestore.collection("orders").doc(id).set({
                        "order": new Date().getTime(),
                        "productId": id
                    });
                });
            }
        </script>
    </body>
</html>

Let's go over the code above for a bit.

Make sure to replace the Firebase Config object (const firebaseConfig) with your own configuration retrieved earlier when we registered the web app.

We initialize the firebase instance (firebase.initializeApp) and then fetch a firestore instance.

Listen to changes occurring on both orders and products collection.

When listening on the orders collection, upon receiving a notification, make sure to clear the products that are in the shopping cart on the initial load, just to be in sync, and display the checkbox indicator whether the product is in the cart or not.

While listening on the products collection, capture the snapshot containing all documents; loop through them and build an HTML template containing the product info (generateProductContent). Inside this template, wire up a click event so the users can trigger an add action by adding the clicked product by id, as shown in the code below:

///.. rest of the code omitted for brevity

product.addEventListener('click', () => {

    // add a new order document to the "orders" collection,
    // using the product id as the key, and a small map object
    // containing an order number and the product id 
    firestore.collection("orders").doc(id).set({
        "order": new Date().getTime(),
        "productId": id
    });
});

With all that in place, let's proceed and create the corresponding Dockerfile that we'll use to build the corresponding image.

Create the Dockerfile

Inside the products folder, right-click on the folder and create a new file called Dockerfile; drop the code below inside this file:

FROM nginx:latest
COPY . /usr/share/nginx/html/

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Now let's build the image for the container that will host the products functionality.

Build the Products Docker image

With the code and the Dockerfile in place, let's build its corresponding image.

If you still have the Terminal open, navigate to the products folder, or open a new one.

Run the following command:

docker build -t products-img .

This builds a Docker image tagged products-img from the contents of this folder.

Same drill as before, verify the Docker image was build by checking the output from the command, and running the command docker image ls:

$> docker image ls

REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
products-img        latest    0eab72bd4dd0   11 seconds ago   142MB
footer-img          latest    b2b5a750d832   37 minutes ago   142MB
shopping-cart-img   latest    5a393cb354c3   37 minutes ago   142MB
nginx               latest    2b7d6430f78d   2 weeks ago      142MB

Run the Products Container

Still inside the products folder, run the following command:

docker run -d -p 8082:80 --name products-container products-img

With the above command, we are running a container named products-container based off of the products-img image created earlier, and exposing the internal port 80 to the external port 8082.

Verify that the three containers are successfully running (footer-container, shopping-cart-container, products-container) by running docker ps:

$> docker ps

CONTAINER ID   IMAGE               COMMAND                  CREATED          STATUS          PORTS                  NAMES
4c8a16e6460a   products-img        "/docker-entrypoint...."   4 seconds ago    Up 2 seconds    0.0.0.0:8082->80/tcp   products-container
c3ff8fd8a4ef   shopping-cart-img   "/docker-entrypoint...."   35 minutes ago   Up 35 minutes   0.0.0.0:8081->80/tcp   shopping-cart-container
9c91e7825dbc   footer-img          "/docker-entrypoint...."   35 minutes ago   Up 35 minutes   0.0.0.0:8083->80/tcp   footer-container

Go ahead and preview the products web app in the browser by activating the Web Preview feature, changing the port to 8082, then clicking on Change and Preview. You should get another browser window with the following output:

MicroServices UI

If you've followed all steps and done everything correctly, you should see a web app displaying all products we previously stored on the Firebase Cloud Firestore DB, in the products collection.

Now that we've built all our main three features, it's time to bring them all together and assemble them inside our e-commerce.

With all the pieces built in isolation, we'll refer to a commonly used term in the industry known as "Micro-Frontends", which is a variation on the Microservices approach, but applied to the Frontend.

The idea behind Micro-Frontends is to think about a website or web app as a composition of features which are owned by independent teams. Each team has a distinct area of business or mission it cares about and specialises in (i.e. shopping cart, products, footer). A team is cross functional and develops its features end-to-end, from database to user interface.

MicroServices UI

In our case, each team focuses on a feature of the overall e-commerce site, and come together within a shell interface that integrates them. However, each of these features could well live on its own, making it highly reusable and portable.

It also promotes autonomy on the developing team, as they can implement in the language or platform that is best suited for their feature, as well as the cadence in which updates get released, as it provides independence in their release pipelines.

MicroServices UI

In our e-commerce example, the e-commerce site portion will serve as a shell with regions in which each of the features will render itself, while the synchronization and communication happens behind the scenes using strategies such as messages and events.

Let's proceed and build out the e-commerce shell web app.

Build the e-commerce shell web app

Locate the e-commerce folder, and follow the same strategy as in the previous containerized web apps: create a file called index.html; drop the bottom code into that file:

<!DOCTYPE html>
<html>
    <head>
        <style>
            body {
                font-family: 'Montserrat', sans-serif;
                padding: 0;
                margin: 0;
                position: absolute;
                top: 0;
                bottom: 0;
                left: 0;
                right: 0;
                display: flex;
                flex-direction: column;
            }

            .container {
                height: 100%;
                display: flex;
                flex-direction: column;
            }

            main {
                flex: 1;
            }

            iframe {
                border: none;
                width: 100%;
                height: 100%;
            }

        </style>
        <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
        <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;900&display=swap" rel="stylesheet">
    </head>
    <body>
        <div class="container">
            <header>
                <iframe src="SHOPPING_CART_URL"></iframe>
            </header>
            <main>
                <iframe src="PRODUCTS_URL"></iframe>
            </main>
            <footer>
                <iframe src="FOOTER_URL"></iframe>
            </footer>
        </div>
    </body>
</html>

Notice how the HTML structure consists of three regions: header, main and footer, corresponding to our shopping cart, producst and footer features respectively. Also notice how each region has an iframe tag inside; this is how we will host each of the web apps we built out earlier, that way they can be hosted independently but rendered as a portion of the whole UI, and when they come together, it will look like a cohesive unit.

Feed the corresponding URL from each of our running containerized web apps (remember the URLS we were previewing on their specified ports? yeah, we need those now!). Grab each corresponding URL and paste it on each iframe; locate the SHOPPING_CART_URL, PRODUCTS_URL and FOOTER_URL and replace accordingly.

Copy the URL all the way until the xxx.cloudshell.dev suffix; no need to grab the rest of the URL.

Your code may look like this after this change:

// ... rest of the code omitted

 <div class="container">
    <header>
        <iframe src="https://8082-e6589e84-32d3-4913-a227-80041f558c8e.cs-us-east1-vpcf.cloudshell.dev"></iframe>
    </header>
    <main>
        <iframe src="https://8083-e6589e84-32d3-4913-a227-80041f558c8e.cs-us-east1-vpcf.cloudshell.dev"></iframe>
    </main>
    <footer>
        <iframe src="https://8084-e6589e84-32d3-4913-a227-80041f558c8e.cs-us-east1-vpcf.cloudshell.dev"></iframe>
    </footer>
</div>

Let's host this on its own container, but first we should create the Dockerfile so we can build its corresponding image.

Create the Dockerfile

Inside the e-commerce folder, right-click on the folder and create a new file called Dockerfile; drop the code below inside this file:

FROM nginx:latest
COPY . /usr/share/nginx/html/

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Now let's build the image for the container that will host the products functionality.

Build the E-Commerce Docker Image

With the code and the Dockerfile in place, let's build its corresponding image.

Navigate to the products folder, or open a new one.

Run the following command:

docker build -t ecommerce-img .

This builds a Docker image tagged ecommerce-img from the contents of this folder.

Same drill as before, verify the Docker image was build by checking the output from the command, and running the command docker image ls:

$> docker image ls

REPOSITORY          TAG       IMAGE ID       CREATED         SIZE
ecommerce-img       latest    2b87ed436efa   4 seconds ago   142MB
products-img        latest    c1d52b42da1d   2 minutes ago   142MB
shopping-cart-img   latest    2ed75a46d771   2 minutes ago   142MB
footer-img          latest    46e8382d1308   3 minutes ago   142MB
nginx               latest    2b7d6430f78d   2 weeks ago     142MB

Run the E-Commerce Container

Still inside the e-commerce folder, run the following command:

docker run -d -p 8080:80 --name ecommerce-container ecommerce-img

With the above command, we are running a container named ecommrece-container based off of the ecommerce-img image created earlier, and exposing the internal port 80 to the external port 8080.

Verify one last time that all containers are running by running the command docker ps:

$> docker ps

CONTAINER ID   IMAGE               COMMAND                  CREATED          STATUS          PORTS                  NAMES
cbafaf0d2a88   ecommerce-img       "/docker-entrypoint...."   34 seconds ago   Up 34 seconds   0.0.0.0:8080->80/tcp   ecommerce-container
a5f0ec80f2a6   products-img        "/docker-entrypoint...."   9 minutes ago    Up 9 minutes    0.0.0.0:8082->80/tcp   products-container
c86a06a5b595   shopping-cart-img   "/docker-entrypoint...."   9 minutes ago    Up 9 minutes    0.0.0.0:8081->80/tcp   shopping-cart-container
019a12837821   footer-img          "/docker-entrypoint...."   10 minutes ago   Up 10 minutes   0.0.0.0:8083->80/tcp   footer-container

If all went well, proceed to preview the containerized e-commerce shell web app, which should contain all integrated features in a single web app. Preview it on port 8080 and you should get the following output:

MicroServices UI

Amazing work! We accomplished a lot in this workshop, where we deployed several apps as microservices, and integrated them using a micro-frontend strategy to display them as a cohesive user interface. Great job!

Congrats in making it this far! In this codelab, we accomplished the following:

Please don't forget to follow me on social media: