By the end of this chapter the reader should be able to:
We experienced working with server-based deployments in the previous chapters. Despite the great convenience brought to the software industry by using cloud services, you've seen that there is still the responsibility of starting and stopping the servers. We can now understand how wasteful it can be to forget to stop a service when it's not needed and how problematic it can be to forget to start the server when it's needed.
The serverless concept was introduced in order to avoid these kinds of problems by leveraging the autonomous nature of cloud services. In the serverless model, the servers are automatically provisioned and started whenever needed and terminated when they're not.
The term 'Serverless' doesn't indicate that there are no servers altogether; it just indicates that the management of the servers is entirely delegated to the cloud provider.
In this chapter, we'll see three ways to leverage serverless technology; serverless functions, serverless containers, and serverless apps.
Chapter 3 introduced the microservices architecture and how it enhances the modularity and maintainability of the system. The serverless model provides an opportunity to implement the microservices pattern in combination with the event-driven pattern. Other architectural patterns like pipe-filter and asynchronous messaging can be brought to the mix in order to create more sophisticated applications.
Using serverless functions, you can break down your application into small functions (that only need a few minutes to be executed) and tie them with specific events. The resources necessary to execute the function would get started after the triggering event takes place and would keep running during the execution time, then get terminated immediately after the function finishes execution or when the time limit is exceeded.
GCP allows us to create serverless functions using the 'Google Cloud Function' service. In the next section, we'll practice using GCF.
First, we need to enable cloud functions API for the project.
'APIs & Services' to open the sub-menu, then click on 'Library'.
Figure 8.1: GCP APIs and Services
Now, we can create and deploy our first cloud function.
Go back to the navigation menu, scroll down to the 'Serverless' category, and click on 'Cloud Functions'.
Figure 8.2: GCP Cloud Functions
If this is the first function you create, you'll see a page that looks like the figure below. Click on 'CREATE FUNCTION'.
Figure 8.3: First Google Cloud Function
The 'Create function' dialogue will show up, allowing us to specify the settings for the function we are about to create. As you can see in figure 6.4, there are two main sections we need to configure, the 'Basics' and the 'Trigger'.
Figure 8.4: Creating a GCF
The 'Trigger' section is where we specify the trigger type and event that would cause the function to be triggered for execution. In general, trigger types may include different events, and we'd need to specify one of them to trigger the function. However, the HTTP trigger type (the default) has only one event, which is the reception of the HTTP request, so we don't need to specify it.
Click 'SAVE' then 'NEXT' to go to the code section, as shown in figure 8.5. There are a few things to pay attention to here:
Finally, the default code that we're going to use as our first example receives the HTTP request and responds with the value of a parameter called 'message' received in the request, either in the body in the case of a POST request or in the query-string in the case o a GET request. If no message was sent with the request, the response would be 'Hello World'.
Figure 8.5: GCF Source Code
Once the deployment is complete, you can see that function-1 is listed under the functions list.
Figure 8.6: function-1 deployed
Figure 8.7: Testing the first GCF
The inline editor is a nice feature to test the service. However, it's not very convenient to use for real development. In this section, we'll see how to deploy the same sample GCF through the SDK.
In the Function details page of 'function-1', go to the 'SOURCE' tab then click on 'DOWNLOAD ZIP'.
Figure 8.8: Download the sample source code
Now we need to know what command we can use for the deployment. As mentioned before, we can find the most recent command syntax by searching the documentation.
Figure 8.9: Command to deploy a GCF from a local environment
Figure 8.9 shows the description of the gcloud function deploy command.
So, the command we need to execute would look like this:
$ gcloud functions deploy function-1-from-sdk --entry-point helloWorld --runtime nodejs16 --trigger-http --allow-unauthenticated
If the deployment was successful, you should see the output with information about the GCF, including lines similar to the snippet shown in figure 8.10.
Figure 8.10: GCF Successfully deployed using gcloud tool
If you go to the Functions list page on the console, you should be able to see 'function-1-from-sdk' listed, and it should work exactly the same way as function-1.
Figure 8.11: function-1-from-sdk listed
Now that we understand how cloud functions work, let's try to do something a bit more meaningful. We'll create a mailing-list subscription app with two microservices implemented as serverless functions with a static webpage as the frontend.
Figure 8.12: Mailing List Subscription App
As you can see in figure 8.12, there are three tiny components of the app. The user would enter their email on the static HTML page and click on a button that would trigger the 'send-verification-email' cloud function. The function would then send an email to the submitted address asking the user to click on a link to verify their email, then terminates. At a later time, whenever the user decides to open their email and click on the link, only then the 'verify-email' function will be triggered, which sends a confirmation message to the user in the response.
We'll start the deployment with the verify-email function since it doesn't depend on any of the other components. Then we'll deploy the send-verification-email function, and finally, the subscribe.html page.
Figure 8.13: Code for the verifyEmail function
Figure 8.14: package.json for verifyEmail
You may notice that the code is very similar to the sample code we've seen in function-1. Only a slight change in the message content and the name of the query-string property and the package.json file is also not very different. The only difference is that, since we're not using the default name 'index.js', we had to specify the name of the main module that we had in the file 'verifyEmail.js'. So the deployment of this function should be pretty similar to the sample function deployment we did in the previous section.
Deploy the cloud function using the command:
> gcloud functions deploy verify-email --entry-point verifyEmail --runtime nodejs16 --trigger-http --allow-unauthenticated
For the send-verification-email, we need a dependency, namely, the nodoemailer npm package. If you want to test the code locally, you'll need to install the package using the command npm install nodemailer. However, for the cloud function, we don't have to worry about the actual installation; all we need to do is to have the package name and version included in the dependencies property in the package.json file, as shown in figure 8.15.
Figure 8.15: package.json file for the sendVerificationEmail function
Sending an actual email would require careful security consideration, which might distract from the focus of this chapter; therefore, we will simulate sending an email using 'ethereal.email'. If you are not familiar with JavaScript, don't worry about understanding the details of the code because the scope of this book is about the deployment, not about the development, so this code will be made available for you.
There are only a few things you need to pay attention to in the code shown in figure 8.16 below.
Figure 8.16: Send Verification Email Code
Now that we have the code ready, we can deploy it using the following command.
> gcloud functions deploy send-verification-email --entry-point sendVerificationEmail --runtime nodejs16 --trigger-http --allow-unauthenticated
If the deployment was successful, you should be able to see it listed under the Functions list, and you can test it to make sure it works before we move on.
Note: If you wanted to make any changes to a function after deploying a function (e.g., fixing bugs), you can just re-deploy it by running the same gcloud functions deploy command.
Here's a simpler simulation in which the link to open the email goes to a simulated email message on the same page to avoid working with nodemailer and the ethereal fake SMTP server (since it's not very reliable, as a free service, we shouldn't expect too much)
Figure 8.17: (Alternative) Send Verification Email Code
Finally, for the front end, we only need to create a static HTML page that has a form with the action set to the URL of send-verification-email and an input field with the name 'email' and a submit button.
Figure 8.18: subsribe.html code
To test the whole application, open the subscribe.html page in a browser, enter an email in the input field, and click on 'Subscribe'.
Figure 8.19: Testing - subscribe.html
Clicking on the submission button of an HTML form generates an HTTP request to the URL specified in the action attribute, here the URL to send-verification-email. The request contains the values in the input fields as parameters in the query string. That request triggers the cloud function, which sends the verification email.
Figure 8.20: send-verification-email triggered
You can open the email message sent by the function by clicking on the link that came in the response.
Figure 8.21: Verification Email message
Figure 8.22: Verification Email message in the simpler simulation example
In the body of the message, clicking on the link triggers the verify-email cloud function, which finally sends the message that the email address has been verified.
Figure 8.23: verify-email Triggered
Under the 'STORAGE' category, click on 'Storage'.
Figure 8.24: Object Storage service in GCP
Under the 'Storage Browser', you'll see a bucket with a name prefixed by 'gcf-sources'.
Figure 7.25: Storage bucket for GCF sources
Click on that bucket name to see what's in it. You'll see a DO_NOT_DELETE_THE_BUCKET.md file stating that this bucket was automatically generated and that you shouldn't tamper. Then there's a folder for each cloud function that contains the source code for this function.
Figure 8.26: Contents of the gcf-sources bucket
Under the 'Tools' category, click on 'Container Registry'.
Figure 8.27: GCP's Container Registry Service
Under the repositories list, you'll see a repository named 'gcf'. So, behind the scenes, google cloud functions are implemented as containers.
Figure 8.28: gcf repository in the container registry
If you clicked on the repository name, then the region name, then one of the automatically generated names, you can see that it's indeed a docker image with the tag based on the name that we specified for the cloud function.
Figure 8.29: send-verification-email version 1 container image
We have the option to create our own container and deploy it in a serverless fashion as well. We'll see how to do that in the next section.
We have seen in chapter 5 how beneficial containerization can be. However, deploying the container on an orchestrator like Kubernetes is still a server-based framework. This means that you, as the developer (or the operations or the DevOps person), would be responsible for the existence and well being of the servers/hosts; even though much of it is automated by Kubernetes, you still have to issue the commands and set the configuration of the automation. In addition to that, the resources may still be wasted because at least some of the servers will have to keep running even when no requests are coming to it. Of course, this cannot be avoided for some applications, and the server-based deployment would be most fitting. However, when this is not the case, there's a serverless option for deploying containers in GCP, using the 'Cloud Run' service.
First, let's explore another service that allows us to build and push a container image to GCP's containers registry with only one command. We don't even have to install Docker for it.
In the following steps, I'm going to build the hello-i-am-here image that we discussed in chapter 5 from my local machine, which doesn't have Docker installed, using Google's 'Cloud Build' service.
Figure 8.30: package.json with start script needed by Cloud Run added
Figure 8.31: The contents of the .dockerignore file
Now that we have the contents ready, we can use the 'Cloud Build' service to create the image.
> gcloud builds submit --tag gcr.io/<Project-ID>/hello-i-am-here
Figure 8.32: hello-i-am-here image created
Let's now deploy our 'hello-i-am-here' image using google run.
Under the 'SERVERLESS' category, click on 'Cloud Run'.
Figure 8.33: GCP Cloud Run service
On the 'Cloud Run Services' page, click on "Create Service".
Figure 8.34: Create a Cloud Run Service
The first option in the 'Create Service' form allows us to choose between deploying one revision from an existing image or to continuously deploy new revisions from a repository. For the sake of this exercise, we'll choose the first option by clicking on the radio button then clicking 'SELECT'.
Figure 8.35: Cloud Run Service Deployment settings
A side menu will show up with the images available for us to choose from. We'll select the 'hello-i-am-here' image that we just created.
Figure 8.36: Selecting hello-i-am-here image
A nice thing to notice here is the autoscaling configuration (arrow 3 in figure 8.36). As you can see, we can set a minimum and a maximum number of instances. The exact number of instances at a given moment will be determined automatically according to the demand. If we set the minimum to zero, we won't have to pay any fees for the instances when there's no demand. However, there's one disadvantage of this, provisioning the instance and preparing it when the first request comes in takes time, which is called the 'cold start'. We can have one instance running all the time to avoid that delay if it matters to our business.
Figure 8.37: Configuring a Cloud Run Service
Once the service is created, we should be able to see the trigger URL under the service details, and it runs as expected.
Figure 8.38: Cloud Run service successfully deployed
One thing to notice here, if you remember, when we deployed the services through Kubernetes, the host name that came in the response was the pod automatically generated name, but here it's just "localhost". So a 'Cloud Run' deployment is not identical to a Kubernetes deployment, and it's not identical to a serverless function either. It just shares some features with both of them.
What if we want to deploy an application with multiple components like the mailing list subscription app we created in section 8.3 using 'Cloud Run'? In that case, we'll need to do the following steps for each of the components
Getting to build the container image ourselves means you need to add extra work (i.e. all the steps that were done automatically by GCF); in exchange, we would have more control over how the container is built, and also, there's no restriction on the execution time.For example, in order to build a container image in which the verifyEmail function would work properly, we'll need a web server that would receive the HTTP request, pass it on to the application, then receive the response from our application to send it to the user. One way to do that is to create our application as an express server, so the code will have to change a bit.
const express = require('express');
const server = express();
server.get('/', (req, res) =>
{
let message = 'Email ' + req.query.email + ' has been verified!'
res.status(200).send(message);
}
);
server.listen(8080, '0.0.0.0', () => {console.log('listening on port 8080.');});
Listing 8.1: verifyEmail microservice implemented as an express app
Add express to the dependencies and the start script in package.json as in listing 8.2
{
"name": "verifyemail",
"version": "1.0.0",
"description": "",
"main": "verifyEmail.js",
"scripts": {
"start": "node verifyEmail.js"
},
"dependencies": {
"express": "^4.17.3"
}
}
Listing 8.2: package.json file for the verifyEmail microservice
From node:latest
WORKDIR /usr/src/web-apps/verify-email
ADD verifyEmail.js .
ADD package.json .
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node" , "verifyEmail.js"]
Listing 8.3: Dockerfile for the verifyEmail microservice
Once the submission is done, we should be able to see the image in the container registry
Figure 8.39: verify-email container image submitted to the Container Registry on GCP
Create a service using Cloud Run, test it, and keep the URL to use in the sendVerificaionEmail microservice, as shown in line 16 in listing 8.4.
Figure 8.40: verify-email microservice deployed as a Cloud Run service
1. const nodemailer = require("nodemailer");
2. const express = require('express');
3. const server = express();
4. server.get('/',
5. (req,res) => {
6. // A simpler simulation
7. let preview = "A verification email sent to your account: <a href='#openemail'> Go To Email </a>";
8. for (let i=0;i<100;i++) preview += "<br>"; // just adding empty lines
9. preview += "<hr> <h1 id='openemail'> Email Server Simulation</h1> <hr>";
10. preview +=
11. `<b>from:</b> 'maillingList@subscription.com'<br>
12. <b>to:</b> ${req.query.email} <br>
13. <b>subject:</b> "Verify Your email"<br>
14. Thank you for your interest in our mailing list
15. <br>
16. <a href="https://verify-email-fcdun4ebkq-uc.a.run.app?email=${req.query.email}">
17. Click here to verify your email
18. </a>
19. `
20. res.status(200).send(preview);
21. }
22. );
23. server.listen(8080, '0.0.0.0', () => {console.log('listening on port 8080.');});
Listing 8.4: send sendVerificationEmail microservice implemented as an express app
We'll repeat the same steps to create a Cloud Run service for the sendVerificationEmail microservice, test it and keep the URL to include in the final component, which is the static HTML page representing the front-end of the application as in listing 8.5
Figure 8.41: send-verification-email microservice deployed as a Cloud Run service
<html>
<head>
<title>Mailing List Subscription</title>
</head>
<body>
<form action='https://send-verification-email-fcdun4ebkq-uc.a.run.app'>
Email: <input type="text" name="email">
<input type="submit" value="Subscribe">
<hr/>
</form>
</body>
</html>
Listing 8.5: The front-end HTML page with the URL to the Cloud Run service as the action
Google App Engine is the third service that allows us to create a serverless deployment. It's very similar to creating the services using Cloud Run, except that we can issue the deployment directly from the development environment without creating the container image or anything related to it. The App Engine will receive the source code, store it (temporarily) in Cloud Storage until it builds the container image then deploy it to create the accessible service.We can only create one application per project; however, the application may contain multiple services that can work either cooperatively with or independently from each other. The services/microservices in the application may even have different environments. If including the services/microservices in the same application is an issue, we'll just need to create a different project for each application.
runtime: nodejs16
gcloud init
command to prepare the development environment for the App Engine deployment, but we already have those two steps done, so there is no need to redo them here.Let's start by creating the App itself using the console. As usual, we start at the navigation menu, then under the 'SERVERLESS' category, click on 'App Engine'.
Figure 8.42: Serverless App Engine service
If we didn't create the application for the project already, we'd be greeted with a welcome message and a button to create the application.
Then it will ask us to choose a region and a service account, which we can keep both as default.
We'll then be directed to choose the language and the environment. We'll choose Node.js since it's what we need for our service, and we'll leave the default 'Standard' environment.
The right-side section gives us instructions on how to deploy the application. We already have the first two steps done; we just need to execute the third step to deploy the service.
Issue the command gcloud app deploy
, then type 'y' to continue
Once the deployment is done, we can go to the services tab to see that we just created the default service for the app.
Click on the name of the service to send an HTTP request to its URL that you can see in the address bar in the figure below. The response comes as expected.
After the deployment is done, we can refresh the services page to see that the version number is now 2.
And the response comes without the 'undefined' bug.
We can also see the two versions of the container image under the container registry service.
See if you can deploy the email subscription app with its two microservices using App Engine.
In this chapter, we discussed serverless deployment options. We practiced using two of the serverless services on GCP, namely, 'Google Cloud Functions', which allows us to create serverless functions, 'Google Run', which allows us to create serverless containers deployments, and 'App engine' which allows us to create complete applications in a serverless fashion. In addition to that, we also practiced working with the Cloud Build service, which allows us to build and submit/push Docker containers to GCP's containers register.
[GCP Screenshots] "Google and the Google logo are registered trademarks of Google LLC, used with permission."Unless otherwise stated, all images in this chapter were created by the author Shaimaa Ali using either MS PowerPoint or MS Visio or both. Code screenshot made using MS VSCode.
© Shaimaa Ali 2022