Microservices
We can characterize a microservice architecture as a monolithic application decomposed into many smaller independent applications. Each microservice is responsible for a subset of the monolith's functionality and executes in a separate process.In our reference application, each microservice maps to a specific domain's functional boundaries and exposes its publicly accessible methods via the service's REST API. This approach allows independent management, maintenance, and scaling of individual microservices. This independence increases development and deployment velocities and provides more granular scalability than we could achieve from a classic monolithic application. With finer-grained scalability, we can better manage resource consumption by elastically scaling individual service in response to their load.
The reference application's public API is composed of a collection of domain-specific microservices, each exposing a REST API endpoint employing synchronous Request/Response methods. These synchronous methods are called by the application's consumers using the traditional HTTP Requests/ HTTP Response pattern.
Each microservice is packaged as a Docker container to enable deployment to a Kubernetes cluster for application orchestration. We demonstrated this for both a local ( Microk8s) and cloud ( Amazon Web Service's Elastic Kubernetes Service (EKS)) Kubernetes cluster.
When many technology professionals hear the term microservices, this is often the architecture they conjure in their minds. However, this is not the only approach.
Functions
A function is a packaged sequence of instructions that performs a specific task. It is the smallest unit of computation exposed by a microservice. Up to this point, we have built our microservices from a collection of related domain-specific functions and packaged them as a unit. An alternate approach is to decompose our application microservices beyond domain boundaries down to the individual function (or event) level.Single function microservices provide us with the highest degree of granularity. This increase in granularity gives us more control over an application's resource consumption, availability, and scalability. However, this increase in granularity comes with a corresponding increase in service management, discovery, and API Gateway overhead.
Function-As-A-Service (Faas)
To avoid the hassle of managing the administrative overhead, many cloud providers offer services that provide a runtime-framework that hosts these single-function microservices. These products are often referred to as Function-as-a-Service as well as the somewhat misleading Serverless Computing. Both terms refer to the idea of a standalone function deployed within a runtime-framework managed by a FaaS provider. The runtime framework provides the deployed function on-demand handling both function invocation and per-use billing.This approach can often be more cost-effective when functions are called infrequently. Rather than paying for underutilized computing resources, the FaaS provider only charges for the computing resources used for the duration of the function's execution. Unlike the domain-specific microservices we encountered in the reference application, functions are generally event-driven and are better suited to short-lived, single-purpose, stateless, event-driven operations.
Additionally, FaaS simplifies deployment and scaling by delegating the management of the function's runtime environment and handling elastic scaling to the provider. The developer focuses solely on the development of the function.
A hybrid approach
When developing an application, the choice to use domain-specific microservices or functions is not mutually exclusive. It is often advantageous to employ both approaches. A good rule of thumb is to employ domain-specific microservices for logically-related functions. Use functions for simple microservices that perform a single action in response to an event. While this may slightly increase the architectural complexity, it can significantly decrease resource consumption leading to lower operational costs.FaaS and Vendor Lock-in
At this point, you may be wondering why we haven't mentioned any specific FaaS providers. All of the major cloud providers have some form of FaaS offering. Amazon Web Services has AWS Lambda, Microsoft Azure has Azure Functions, Google Cloud has Cloud Functions, and IBM has IBM Cloud Functions. In addition to their FaaS offerings, each vendor also brings the risk of vendor lock-in. Whether it is by a proprietary API or a ight integration on other provider services, leveraging a cloud provider's FaaS offering poses potential complications if and when you need to migrate the application to another cloud provider.A better way: Kubernetes & Open Source FaaS
We can avoid vendor lock-in by using an open-source FaaS framework deployed on a Kubernetes cluster. Thisapproach gives us a non-proprietary FaaS that we can deploy anywhere we can run Kubernetes. Since Kubernetes runs on all major cloud providers, we now have a portable FaaS platform that can also deploy our REST microservices. There are several open-source FaaS platforms available; however, in this article we will be focusing on OpenFaaS.What is OpenFaas
OpenFaas is an open-source framework for building serverless functions that can run any CLI-driven program packaged in a Docker container. While most cloud provider FaaS products support a limited set of languages, OpenFaaS allows us to build our functions in whatever language we find most appropriate. We also have several options for deploying OpenFaaS. It can be deployed in standalone mode, in a Docker-Swarm, or as part of a Kubernetes cluster. This flexibility allows it to be deployed in a corporate data center or in a public/private cloud using Kubernetes (which is what we will be doing). OpenFaaS also have a large assortment of event-trigger types to invoke the function. These include CLI, Cron, HTTP/webhooks, Async/NATS streaming, Kafka, Redis, MQTT, and custom event types. Lastly, OpenFaaS provides function autoscaling for you (you can of course tweak it as necessary).Deploy OpenFaaS on Kubernetes
Before we can begin writing our first OpenFaaS function, we will need to prepare our environment. We will be deploying OpenFaas to the local MicroK8S Kubernetes instance we created in an earlier article. If you don't already have a Microk8S cluster, you can follow the article's directions from From Docker-Compose to Kubernetes - Part I to set up your own cluster.OpenFaas CLI
Once we have an operational cluster, we will need to install the OpenFaaS CLI. The CLI allows us to interact with OpenFaaS once deployed. To install on Linux or MacOS issue the following command:curl -sL https://cli.openfaas.com | sudo shTo install on Windows, we will be using Git Bash, which provides BASH emulation. Enter the following command:
curl -sL https://cli.openfaas.com | shWe can verify the CLI works by invoking it with the help flag:
faas-cli --helpWhen installed correctly, you should see the following:
Manage your OpenFaaS functions from the command line Usage: faas-cli [flags] faas-cli [command] Available Commands: auth Obtain a token for your OpenFaaS gateway build Builds OpenFaaS function containers cloud OpenFaaS Cloud commands completion Generates shell auto completion deploy Deploy OpenFaaS functions describe Describe an OpenFaaS function generate Generate Kubernetes CRD YAML file help Help about any command invoke Invoke an OpenFaaS function list List OpenFaaS functions login Log in to OpenFaaS gateway logout Log out from OpenFaaS gateway logs Tail logs from your functions namespaces List OpenFaaS namespaces new Create a new template in the current folder with the name given as name push Push OpenFaaS functions to remote registry (Docker Hub) remove Remove deployed OpenFaaS functions secret OpenFaaS secret commands store OpenFaaS store commands template OpenFaaS template store and pull commands up Builds, pushes and deploys OpenFaaS function containers version Display the clients version information Flags: --filter string Wildcard to match with function names in YAML file -h, --help help for faas-cli --regex string Regex to match with function names in YAML file -f, --yaml string Path to YAML file describing function(s) Use "faas-cli [command] --help" for more information about a command.
Deploying OpenFaaS with Helm3
Our next step is to deploy OpenFaaS using Helm3. If you haven't installed Helm3 in your MicroK8S cluster, refer to the earlier article Packaging Kubernetes applications with Helm. Now that we have Helm3 installed and a working MicroK8S cluster, we are ready to begin.We begin by creating two namespaces in our cluster, one for OpenFaas's core services and a second for our functions. We can do this using the following kubectl command:
microk8s kubectl apply -f https://raw.githubusercontent.com/openfaas/faas-netes/master/namespaces.ymlWhich applies the following namespaces.yml:
loading...
Our MicroK8S cluster now contains two additional namespaces: openfaas and openfaas.
We can now use Helm to add the OpenFaaS chart repository with the following command:
microk8s helm3 repo add openfaas https://openfaas.github.io/faas-netes/We should see that openfaas has been added to our repositories:
"openfaas" has been added to your repositoriesWe can now deploy the OpenFaaS Helm chart using the following command:
helm repo update \ && helm upgrade openfaas --install openfaas/openfaas \ --namespace openfaas \ --set functionNamespace=openfaas-fn \ --set generateBasicAuth=trueYou should see output similar to following:
Hang tight while we grab the latest from your chart repositories... ...Successfully got an update from the "openfaas" chart repository Update Complete. ⎈ Happy Helming!⎈ Release "openfaas" does not exist. Installing it now. NAME: openfaas LAST DEPLOYED: Tue Dec 8 17:16:08 2020 NAMESPACE: openfaas STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: To verify that openfaas has started, run: kubectl -n openfaas get deployments -l "release=openfaas, app=openfaas" To retrieve the admin password, run: echo $(kubectl -n openfaas get secret basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode)Included in the helm notes section are instructions for verifying that OpenFaas has started and how to retrieve the admin password. Remember, we are running in MicroK8S!. You will need to include the microk8s prefix to each kubectl command.
To verify OpenFaaS, we use the command:
microk8s kubectl -n openfaas get deployments -l "release=openfaas, app=openfaas"You should see output similar to this:
NAME READY UP-TO-DATE AVAILABLE AGE gateway 1/1 1 1 41s prometheus 1/1 1 1 41s basic-auth-plugin 1/1 1 1 41s nats 1/1 1 1 41s alertmanager 1/1 1 1 41s queue-worker 1/1 1 1 41s faas-idler 1/1 1 1 41sWe can now obtain the password for our OpenFaaS instance using the following command:
PASSWORD=$(microk8s kubectl -n openfaas get secret basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode) && \ echo "OpenFaaS admin password: $PASSWORD"You should see a string of characters similar to this (yours will be different):
PaQaGjczC6g9We now want to get the address of the OpenFaas gateway. We can accomplish this with the following command:
k get svc -n openfaas gateway-external -o wideYou should see something like this:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR gateway-external NodePort 10.152.183.11 <none> 8080:31112/TCP 14m app=gatewayWe can see from the output that our external gateway is of type NodePort with an IP address of 10.152.183.11 on ports 8080 & 31112. To interact with OpenFaas using the faas-cli, we need to set the OPENFAAS_URL environment variable. We can do this using the following command (you will need to set your Cluster IP address):
export OPENFAAS_URL=http://<YOUR CLUSTER IP ADDRESS>:8080We can now finally login using faas-cli:
faas-cli login -g $OPENFAAS_URL -u admin --password <your generated password>Yu should see output similar to the following:
WARNING! Using --password is insecure, consider using: cat ~/faas_pass.txt | faas-cli login -u user --password-stdin Calling the OpenFaaS server to validate the credentials... WARNING! Communication is not secure, please consider using HTTPS. Letsencrypt.org offers free SSL/TLS certificates. credentials saved for admin http://10.152.183.11:8080Let's check that everything is working correctly by calling the following command:
faas-cli versionAnd you should see output similar to below:
___ _____ ____ / _ \ _ __ ___ _ __ | ___|_ _ __ _/ ___| | | | | '_ \ / _ \ '_ \| |_ / _` |/ _` \___ \ | |_| | |_) | __/ | | | _| (_| | (_| |___) | \___/| .__/ \___|_| |_|_| \__,_|\__,_|____/ |_| CLI: commit: c12d57c39ac4cc6eef3c9bba2fb45113d882432f version: 0.12.14 Gateway uri: http://10.152.183.11:8080 version: 0.20.2 sha: 9bbb25e3c7c4cc5cd355edb3a122f8c7812e32db commit: Fix a bug that caused the services list to keep growing Provider name: faas-netes orchestration: kubernetes version: 0.12.9 sha: c402b912ce21f0bf01bc3aa45ebc330decc41ac5 Your faas-cli version (0.12.14) may be out of date. Version: 0.12.19 is now available on GitHub.Congratulations, you now have a working instance of OpenFaaS deployed on your MicroK8s cluster!
Building your first function
With OpenFaas, building functions is extremely easy. The faas-cli provides a project scaffolding feature that creates a skeleton project for your function in one of twelve Classic OpenFaaS templates. These templates can generate function projects in csharp, dockerfile, go, java11, java11-vert-x, node, node12, php7, python, python3, python3-debian, ruby. Additionally, while out of scope for this article, OpenFaaS supports custom templates. With custom templates, we can support languages and frameworks not included in the set of Classic templates. For more information on custom templates, check out Going Serverless with OpenFaaS and Golang - Building Optimized Templates.Generating a Function Project with FAAS-CLI and Java
The goal of our first function example is to reproduce the canonical Hello World program as a function. We will build our first function using the java11 template. Open a terminal and navigate to a directory where you want your function project to reside. In that directory, execute the following command:faas-cli new hello-world --lang java11The output should be similar to this:
2020/10/04 22:01:21 No templates found in current directory. 2020/10/04 22:01:21 Attempting to expand templates from https://github.com/openfaas/templates.git 2020/10/04 22:01:21 Fetched 12 template(s) : [csharp dockerfile go java11 java11-vert-x node node12 php7 python python3 python3-debian ruby] from https://github.com/openfaas/templates.git Folder: hello-world created. ___ _____ ____ / _ \ _ __ ___ _ __ | ___|_ _ __ _/ ___| | | | | '_ \ / _ \ '_ \| |_ / _` |/ _` \___ \ | |_| | |_) | __/ | | | _| (_| | (_| |___) | \___/| .__/ \___|_| |_|_| \__,_|\__,_|____/ |_| Function created in folder: hello-world Stack file written: hello-world.yml Notes: You have created a function using the java11 template which uses an LTS version of the OpenJDK.When the faas-cli executes with the new command, it checks the local templates directory for the referenced --lang template. Since we ran the command from a pristine directory, faas-cli downloaded the Classic OpenFaaS templates. If we list the contents of our directory, we will see that faas-cli created a file and two directories:
hello-world hello-world.yml template
Hello-World Directory
The hello-world directory contains the project's source files. If we tree the hello-world directory, we see the contents of the function project:./hello-world ├── build.gradle ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main │ └── java │ └── com │ └── openfaas │ └── function │ └── Handler.java └── test └── java └── HandlerTest.java 10 directories, 8 filesThe java11 template creates a Gradle build file for the project and includes the necessary Gradle support files. We also see that the project created the com.openfaas.function Java package that contains a single Handler.java source file.
This Handler class contains the Handle(IRequest req) method which provides the entry point to function. In the Handle method, we create a new Response instance and set our "Hello World" string in the response body. We complete the handler method by returning the response object.
package com.openfaas.function; import com.openfaas.model.IHandler; import com.openfaas.model.IResponse; import com.openfaas.model.IRequest; import com.openfaas.model.Response; public class Handler extends com.openfaas.model.AbstractHandler { public IResponse Handle(IRequest req) { Response res = new Response(); res.setBody("Hello, world!"); return res; } }
hello-world.yml
The hello-world.yml contains project metadata used by the faas-cli to build, push, deploy, and invoke the function.
version: 1.0
provider:
name: openfaas
gateway: http://127.0.0.1:8080
functions:
hello-world:
lang: java11
handler: ./hello-world
image: hello-world:latest
The
faas-cli
new command generates a
hello-world.yml file that contains the project's metadata. We must edit this file before the
faas-cli can use it to
build,
package, or
deploy the function. . Fortunately, we only need to edit the
image name. Change the name value to include the fully qualified name of the Docker image that should be generated.
In our example, we change the image name value to thinkmicroservices/hello-world:latest. The faas-cli uses the image name during the build to generate a local container image. When the faas-cli push command is invoked, it uses your local Docker credentials to push the image to your Docker Hub repository. When the faas-cli deploy command is invoked, the image is retrieved from Docker Hub and deployed as the function.
templates
The templates directory includes the downloaded "Classic OpenFaaS templates" used to generate the function project. If you choose to create a custom OpenFaasS template, you need to place it in the templates directory.Building the OpenFaaS function
Once you have edited the function to your requirements, you will need to build the function. This process includes any compilation and packaging of the application and the generation of the function's container image. We invoke the build using the following command:faas-cli build -f hello-world.ymlWe pass the build command along with the name of the project yaml file (e.g., hello-world.yml) into the faas-cli. We will see output similar to this:
Building hello-world. Clearing temporary build folder: ./build/hello-world/ Preparing: ./hello-world/ build/hello-world/function Building: thinkmicroservices/hello-world:latest with java11 template. Please wait.. Sending build context to Docker daemon 142.3kB Step 1/30 : FROM openjdk:11-jdk-slim as builder ---> 8206ad34e1d2 Step 2/30 : ENV GRADLE_VER=6.1.1 ---> Using cache ---> 18c06a84f662 Step 3/30 : RUN apt-get update -qqy && apt-get install -qqy --no-install-recommends curl ca-certificates unzip ---> Using cache ---> 05e5916317db Step 4/30 : RUN mkdir -p /opt/ && cd /opt/ && echo "Downloading gradle.." && curl -sSfL "https://services.gradle.org/distributions/gradle-${GRADLE_VER}-bin.zip" -o gradle-$GRADLE_VER-bin.zip && unzip gradle-$GRADLE_VER-bin.zip -d /opt/ && rm gradle-$GRADLE_VER-bin.zip ---> Using cache ---> 66955611c5b2 Step 5/30 : ENV GRADLE_HOME=/opt/gradle-$GRADLE_VER/ ---> Using cache ---> 368e4ac89c18 Step 6/30 : ENV PATH=$PATH:$GRADLE_HOME/bin ---> Using cache ---> f5e11b760256 Step 7/30 : RUN mkdir -p /home/app/libs ---> Using cache ---> cf4cb4b714e2 Step 8/30 : ENV GRADLE_OPTS="-Dorg.gradle.daemon=false" ---> Using cache ---> 6b3bbe9ecccd Step 9/30 : WORKDIR /home/app ---> Using cache ---> 37932c680a95 Step 10/30 : COPY . /home/app/ ---> Using cache ---> a32f55434996 Step 11/30 : RUN gradle build ---> Using cache ---> a1edbd6d6fbc Step 12/30 : RUN find . ---> Using cache ---> 154502290bfa Step 13/30 : FROM openfaas/of-watchdog:0.7.6 as watchdog ---> 7580c7c70fce Step 14/30 : FROM openjdk:11-jre-slim as ship ---> e5a708708f23 Step 15/30 : RUN apt-get update -qqy && apt-get install -qqy --no-install-recommends unzip ---> Using cache ---> ccd0718ad4f2 Step 16/30 : RUN addgroup --system app && adduser --system --ingroup app app ---> Using cache ---> 1fd9a0b44dba Step 17/30 : COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog ---> Using cache ---> 2852d0ac3661 Step 18/30 : RUN chmod +x /usr/bin/fwatchdog ---> Using cache ---> 6cd7d7e7da35 Step 19/30 : WORKDIR /home/app ---> Using cache ---> 42c69362bc95 Step 20/30 : COPY --from=builder /home/app/function/build/distributions/function-1.0.zip ./function-1.0.zip ---> Using cache ---> faa45df3478a Step 21/30 : user app ---> Using cache ---> 36b8d6c447dd Step 22/30 : RUN unzip ./function-1.0.zip ---> Using cache ---> 4978c9a3ef65 Step 23/30 : WORKDIR /home/app/ ---> Using cache ---> 5d4a1d513399 Step 24/30 : ENV upstream_url="http://127.0.0.1:8082" ---> Using cache ---> 50a871ab0470 Step 25/30 : ENV mode="http" ---> Using cache ---> f89263342f01 Step 26/30 : ENV CLASSPATH="/home/app/function-1.0/function-1.0.jar:/home/app/function-1.0/lib/*" ---> Using cache ---> 117202b5a3ef Step 27/30 : ENV fprocess="java -XX:+UseContainerSupport com.openfaas.entrypoint.App" ---> Using cache ---> 6ff10d0906c2 Step 28/30 : EXPOSE 8080 ---> Using cache ---> a8f5ccfe2a36 Step 29/30 : HEALTHCHECK --interval=5s CMD [ -e /tmp/.lock ] || exit 1 ---> Using cache ---> 6dff58f99847 Step 30/30 : CMD ["fwatchdog"] ---> Using cache ---> 695dd08045c9 Successfully built 695dd08045c9 Successfully tagged thinkmicroservices/hello-world:latest Image: thinkmicroservices/hello-world:latest built. [0] < Building hello-world done in 0.79s. [0] Worker done. Total build time: 0.79sIn the output of the build command, we see that the Gradle build executes, and the container image (in this case, thinkmicroservices/hello-world:latest) was built for our function.
Pushing the Function
The next step is to push the newly create function container image to our container repository. This is accomplished using the following command:faas-cli push -f hello-world.ymlThe output will look similar to this:
Pushing hello-world [thinkmicroservices/hello-world:latest]. The push refers to repository [docker.io/thinkmicroservices/hello-world] cef7d5fd6a57: Pushed 36618d9e1b78: Pushed 50114cd3f456: Pushed cdcd7a9b55b1: Layer already exists 867657f68396: Pushed 5f07bab336e0: Pushed 167efff21776: Layer already exists fee20f1b745d: Pushed d0fe97fa8b8c: Pushed latest: digest: sha256:46f5e4fef03ec1da303c8d1e36cb350b0be83e83f928e8a880e2e98fee20aece size: 2423 [0] < Pushing hello-world [thinkmicroservices/hello-world:latest] done. [0] Worker done.Here we see the hello-world function container image published to the target repository. Now that the container image is in our repository, we can finally deploy it to the OpenFaaS instance running on our MicroK8S cluster.
Deploying the Hello-world function
We are now ready to deploy our function packaged as a container image and published to our container repository. Again, we will use the faas-cli command with the deploy command, the hello-world.yml file, and the gateway URL.faas-cli deploy -f hello-world.yml --gateway http://10.152.183.11:8080Your output should look similar to:
Deploying: hello-world. WARNING! Communication is not secure, please consider using HTTPS. Letsencrypt.org offers free SSL/TLS certificates. Deployed. 202 Accepted. URL: http://10.152.183.11:8080/function/hello-world.openfaas-fnWe can verify that function was deployed using the following command:
faas-cli list --gateway http://10.152.183.11:8080You should see the following:
Function Invocations Replicas hello-world 0 1Here we see the hello-world function has been deployed with a single replica. We are now ready to invoke our function.
Invoking the Hello-World function
The final step of our process is to invoke our function. Once again, we can use the faas-cli to test that our function is working.faas-cli invoke -f hello-world.yml hello-world --gateway http://10.152.183.11:8080The function is invoked and will display the following:
Reading from STDIN - hit (Control + D) to stop. Hello, world!We aren't passing any data from STDIN, so you can press Control-D to complete the invocation> You will see the following response returned from the function:
Hello, world!If we run the faas-cli list command, we can see that the number of invocations has incremented:
faas-cli list --gateway http://10.152.183.11:8080
Function Invocations Replicas hello-world 1 1Here we see that the OpenFaaS application is capturing the number of times our function has been invoked.
In addition to using the the faas-cli, we can also invoke the function using the function's HTTP endpoint. We will use cURL to call the function, but you can use any mechanism capable of making an HTTP request (including your browser). We simply pass cURL the gateway address (in this case) and the path to the function ( /function/hello-world.openfaas-fn):
curl http://10.152.183.11:8080/function/hello-world.openfaas-fn
Hello, world!If we rerun the faas-cli list command, we see that the number of invocations has incremented again:
faas-cli list --gateway http://10.152.183.11:8080
Function Invocations Replicas hello-world 2 1
As mentioned earlier, the HTTP endpoint is only one mechanism for invoking the function. For a comprehensive list of available event triggers, refer to the OpenFaaS Triggers page.
Redeploying the function
Over time, defect fixes and requirement changes will likely necessitate modifications and redeployment of the function. After making the necessary changes, perform another build, and deploy as described above. OpenFaas will replace the current function with the new function for subsequent invocations.Removing the function
If or when the function is no longer required, we can remove it simply by calling:faas-cli rm -f hello-world.yml --gateway http://10.152.183.11:8080The output should appear as follows:
Deleting: hello-world.openfaas-fn Removing old function.We can verify the function has been removed by calling the faas-cli list command.
faas-cli list --gateway http://10.152.183.11:8080We will get the following empty list of functions:
Function Invocations Replicas
Twitter
Facebook
Reddit
LinkedIn
Email