After the publishing the series - Building Serverless CRUD services in Go with DynamoDB, I've been exploring different ways to build serverless CRUD services. I spent a day trying out Openfaas, MongoDB, Arkade and k3d and I think it is a good idea to write a post and share some mistakes that I've had.
Prerequisites
You need to install Docker on your machine and you need to register for a Docker Hub account as your Docker images will be stored there.
Install Docker
Check out Docker Official Installation Page
Register Docker Hub account
Register via https://hub.docker.com/
Install k3d
k3d
is a little helper to run k3s in docker, where k3s
is the lightweight Kubernetes distribution by Rancher. It actually removes millions of lines of code from k8s. If you just need a learning playground, k3s is definitely your choice.
Check out k3d Github Page to see the installation guide.
When creating a cluster,
k3d
utiliseskubectl
andkubectl
is not part ofk3d
. If you don't havekubectl
, please install and set up here.
Once you've installed k3d
and kubectl
, run
k3d create
It creates a new single-node cluster which is a docker container.
INFO[0000] Created cluster network with ID d198f2b6085ea710ab9b8cd7fa711e69acfe1f3750a2faa5efec06255723e130
INFO[0000] Created docker volume k3d-k3s-default-images
INFO[0000] Creating cluster [k3s-default]
INFO[0000] Creating server using docker.io/rancher/k3s:v1.0.1...
INFO[0000] Pulling image docker.io/rancher/k3s:v1.0.1...
INFO[0029] SUCCESS: created cluster [k3s-default]
INFO[0029] You can now use the cluster with:
We need to make kubectl
to use the kubeconfig for that cluster
export KUBECONFIG="$(k3d get-kubeconfig --name='k3s-default')"
Let's take a look at the cluster info
kubectl cluster-info
You should see
Kubernetes master is running at https://localhost:6443
CoreDNS is running at https://localhost:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://localhost:6443/api/v1/namespaces/kube-system/services/https:metrics-server:/proxy
By running the following command,
kubectl get node
You should see there is a node provisioned by k3d
.
NAME STATUS ROLES AGE VERSION
k3d-k3s-default-server Ready master 8m42s v1.16.3-k3s.2
Install arkade
Moving on to arkade, it provides a simple Golang CLI with strongly-typed flags to install charts and apps to your cluster in one command. Originally, the codebase is derived from k3sup which I've contributed last month.
Let's install the latest version of arkade
curl -sLS https://dl.get-arkade.dev | sudo sh
You should see
Downloading package https://github.com/alexellis/arkade/releases/download/0.2.1/arkade-darwin as /tmp/arkade-darwin
Download complete.
Running with sufficient permissions to attempt to move arkade to /usr/local/bin
New version of arkade installed to /usr/local/bin
Creating alias 'ark' for 'arkade'.
_ _
__ _ _ __| | ____ _ __| | ___
/ _` | '__| |/ / _` |/ _` |/ _ \
| (_| | | | < (_| | (_| | __/
\__,_|_| |_|\_\__,_|\__,_|\___|
Get Kubernetes apps the easy way
Version: 0.2.1
Git Commit: 29ff156bbeee7f5ce935eeca37d98f319ef4e684
Install MongoDB
MongoDB is a cross-platform document-oriented database program which is classified as a NoSQL database program, and it uses JSON-like documents with schema.
We can use arkade to install mongodb to our cluster
arkade install mongodb
You can run arkade info mongodb
to get more info about the connection.
Once you've installed MongoDB, it can be accessed via port 27017 on the following DNS name from within your cluster:
mongodb.default.svc.cluster.local
First, let's get the MongoDB root password
export MONGODB_ROOT_PASSWORD=$(kubectl get secret --namespace default mongodb -o jsonpath="{.data.mongodb-root-password}" | base64 --decode)
Connect to your database run the following command:
kubectl run --namespace default mongodb-client --rm --tty -i --restart='Never' --image docker.io/bitnami/mongodb:4.2.4-debian-10-r0 --command -- mongo admin --host mongodb --authenticationDatabase admin -u root -p $MONGODB_ROOT_PASSWORD
Execute the following commands if you need to connect to your database from outside the cluster
kubectl port-forward --namespace default svc/mongodb 27017:27017 &
mongo --host 127.0.0.1 --authenticationDatabase admin -p $MONGODB_ROOT_PASSWORD
By default, the selected db is admin
. Let's create a new one.
use k3d-mongodb-crud
You should see
switched to db k3d-mongodb-crud
Then, create a collection called foo
.
db.createCollection("foo")
Since you've logged in as a root account and this account has nothing to do with this database. Hence, you need to create a new user with read and write permission. Without this step, you will encounter authentication error later on.
Replace <your_password>
before executing the below command. To keep it simple for this demonstration, I used the same password as MongoDB root password, i.e. $MONGODB_ROOT_PASSWORD
.
db.createUser({ user:"admin", pwd: "<your_password>", roles: [{role: "readWrite", db: "k3d-mongodb-crud"}] })
Verify it
> db.getUsers()
[
{
"_id" : "k3d-mongodb-crud.admin",
"userId" : UUID("72671c44-7306-4abb-bf57-efa87286a0f0"),
"user" : "admin",
"db" : "k3d-mongodb-crud",
"roles" : [
{
"role" : "readWrite",
"db" : "k3d-mongodb-crud"
}
],
"mechanisms" : [
"SCRAM-SHA-1",
"SCRAM-SHA-256"
]
}
]
Install OpenFaaS
OpenFaaS allows us to deploy event-driven functions and micro-services to Kubernetes easily.
Install the latest faas-cli
curl -SLsf https://cli.openfaas.com | sudo sh
Forward the gateway to your machine
kubectl rollout status -n openfaas deploy/gateway
kubectl port-forward -n openfaas svc/gateway 8080:8080 &
Setup our project
OpenFaaS provides a curated list of functions for your kickstart.
faas-cli template store list | grep go
go openfaas Classic Golang template
golang-http openfaas-incubator Golang HTTP template
golang-middleware openfaas-incubator Golang Middleware template
Let's pick golang-middleware
faas-cli template store pull golang-middleware
Fetch templates from repository: https://github.com/openfaas-incubator/golang-http-template at master
2020/03/28 14:55:08 Attempting to expand templates from https://github.com/openfaas-incubator/golang-http-template
2020/03/28 14:55:11 Fetched 4 template(s) : [golang-http golang-http-armhf golang-middleware golang-middleware-armhf] from https://github.com/openfaas-incubator/golang-http-template
The project will be built into a Docker image and push to Docker Hub.
Replace
wingkwong
with your Docker Hub username
faas-cli new --lang golang-middleware k3d-mongodb-crud --prefix=wingkwong
Folder: k3d-mongodb-crud created.
___ _____ ____
/ _ \ _ __ ___ _ __ | ___|_ _ __ _/ ___|
| | | | '_ \ / _ \ '_ \| |_ / _` |/ _` \___ \
| |_| | |_) | __/ | | | _| (_| | (_| |___) |
\___/| .__/ \___|_| |_|_| \__,_|\__,_|____/
|_|
Function created in folder: k3d-mongodb-crud
Stack file written: k3d-mongodb-crud.yml
Let's take a look at the project structure. All files are generated by faas-cli
.
├── k3d-mongodb-crud
│ └── handler.go
├── k3d-mongodb-crud.yml
└── template
├── golang-http
│ ├── Dockerfile
│ ├── function
│ │ └── handler.go
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ ├── template.yml
│ └── vendor
│ ├── github.com
│ └── modules.txt
├── golang-http-armhf
│ ├── Dockerfile
│ ├── function
│ │ ├── Gopkg.toml
│ │ └── handler.go
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ ├── template.yml
│ └── vendor
│ ├── github.com
│ └── modules.txt
├── golang-middleware
│ ├── Dockerfile
│ ├── function
│ │ └── handler.go
│ ├── go.mod
│ ├── main.go
│ └── template.yml
└── golang-middleware-armhf
├── Dockerfile
├── function
│ └── handler.go
├── go.mod
├── main.go
└── template.yml
Here's k3d-mongodb-crud.yml
, which is our configuration file for the deployment.
version: 1.0
provider:
name: openfaas
gateway: http://127.0.0.1:8080
functions:
k3d-mongodb-crud:
lang: golang-middleware
handler: ./k3d-mongodb-crud
image: wingkwong/k3d-mongodb-crud:latest
Let's build and deploy the project to the cluster
faas-cli up -f k3d-mongodb-crud.yml
Oops..it failed to deploy due to unauthorised access
Deploying: k3d-mongodb-crud.
WARNING! Communication is not secure, please consider using HTTPS. Letsencrypt.org offers free SSL/TLS certificates.
Handling connection for 8080
unauthorized access, run "faas-cli login" to setup authentication for this server
Function 'k3d-mongodb-crud' failed to deploy with status code: 401
Let's get the password from Secret
PASSWORD=$(kubectl get secret -n openfaas basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode; echo)
Log into the gateway by running
echo -n $PASSWORD | faas-cli login --username admin --password-stdin
Okay..another error is occurred
Calling the OpenFaaS server to validate the credentials...
Cannot connect to OpenFaaS on URL: http://127.0.0.1. Get https://127.0.0.1:443/system/functions: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs
This is because the name localhost maps to an IPv6 alias meaning that the CLI may hang on certain Linux distributions. As suggested by Openfaas, you may have following solutions:
1. Use the -g or --gateway argument with 127.0.0.1:8080 or similar
2. Set the OPENFAAS_URL environmental variable to 127.0.0.1:8080 or similar
3. Edit the /etc/hosts file on your machine and remove the IPv6 alias for localhost (this forces the use of IPv4)
Let's take the second solution and retry it
export OPENFAAS_URL="127.0.0.1:8080"
PASSWORD=$(kubectl get secret -n openfaas basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode; echo)
echo -n $PASSWORD | faas-cli login --username admin --password-stdin
Calling the OpenFaaS server to validate the credentials...
Handling connection for 8080
WARNING! Communication is not secure, please consider using HTTPS. Letsencrypt.org offers free SSL/TLS certificates.
credentials saved for admin http://127.0.0.1:8080
It looks good now. Let's deploy again.
faas-cli up -f k3d-mongodb-crud.yml
Deploy successfully!
Deployed. 202 Accepted.
URL: http://127.0.0.1:8080/function/k3d-mongodb-crud
Go to http://127.0.0.1:8080/ui/
. You should see the function on the portal.
Finally we've all the setup. Let's move on to the coding part.
Start Coding
Go to k3d-mongodb-crud.yml
, add environment
and secrets
under your function.
version: 1.0
provider:
name: openfaas
gateway: http://127.0.0.1:8080
functions:
k3d-mongodb-crud:
lang: golang-middleware
handler: ./k3d-mongodb-crud
image: wingkwong/k3d-mongodb-crud:latest
environment:
mongo_host: mongodb.default.svc.cluster.local:27017
mongo_database: k3d-mongodb-crud
mongo_collection: foo
write_debug: true
combine_output: false
secrets:
- mongo-db-username
- mongo-db-password
Back to the terminal, create two secrets by running
faas-cli secret create mongo-db-username --from-literal admin
faas-cli secret create mongo-db-password --from-literal $MONGODB_ROOT_PASSWORD
You should see
Creating secret: mongo-db-username
Creating secret: mongo-db-password
Back to k3d-mongodb-crud/handler.go
, import the libraries that we will use in this project
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"github.com/openfaas/openfaas-cloud/sdk"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
Declare some variables
var (
sess *mgo.Session
mongoDatabase = os.Getenv("mongo_database")
mongoCollection = os.Getenv("mongo_collection")
)
Add a new function called init
where we establish a persistent connection to MongoDB.
As we've created our mongo-db-username
and mongo-db-password
in previous steps, we use sdk.ReadSecret
to retrieve the value from the secret file.
Then we need to use DialWithInfo to establishe a new session to the cluster identified by info. Before that, we need DialInfo
to hold our options.
func init() {
var err error
mongoHost := os.Getenv("mongo_host")
mongoUsername, _ := sdk.ReadSecret("mongo-db-username")
mongoPassword, _ := sdk.ReadSecret("mongo-db-password")
if _, err := os.Open("/var/openfaas/secrets/mongo-db-password"); err != nil {
panic(err.Error())
}
info := &mgo.DialInfo{
Addrs: []string{mongoHost},
Timeout: 60 * time.Second,
Database: mongoDatabase,
Username: mongoUsername,
Password: mongoPassword,
}
if sess, err = mgo.DialWithInfo(info); err != nil {
panic(err.Error())
}
}
Let's create a Foo struct with two fields Bar
and Baz
type Foo struct {
Bar string
Baz string
}
Delete anything inside Handle function and add the below structure
func Handle(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
} else if r.Method == http.MethodGet {
} else if r.Method == http.MethodPut {
} else if r.Method == http.MethodDelete {
}
}
For CREATE
, as we've established sess
in init()
function. We can just use it to run Insert
. The below code snippet will insert 4 records to Collection foo
in Database k3d-mongodb-crud
. If it goes wrong, it will throw an internal server error. IF not, it will return the defined json output.
if r.Method == http.MethodPost {
fmt.Println("4 records will be inserted")
if err := sess.DB(mongoDatabase).C(mongoCollection).Insert(
&Foo{Bar: "bar", Baz: "baz"},
&Foo{Bar: "bar1", Baz: "baz1"},
&Foo{Bar: "bar2", Baz: "baz2"},
&Foo{Bar: "bar3", Baz: "baz3"},
); err != nil {
http.Error(w, fmt.Sprintf("Failed to insert: %s", err.Error()), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"success":true, "message": "4 records have been inserted"}`))
}
For GET
, similar to CREATE
, use sess.DB(mongoDatabase).C(mongoCollection)
to call Find
. In this example, we do not input any filter so we just need to put bson.M{}
. All records will be retrieved and stored in foo. This time we do not output our json output but the actual result.
Remember if you find all records, make sure foo is an array or you will get MongoDB Error: result argument must be a slice address
.
else if r.Method == http.MethodGet {
fmt.Println("All records will be listed")
var foo []Foo
err := sess.DB(mongoDatabase).C(mongoCollection).Find(bson.M{}).All(&foo)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to read: %s", err.Error()), http.StatusInternalServerError)
return
}
out, err := json.Marshal(foo)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to marshal: %s", err.Error()), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(out)
}
For UPDATE
, it is getting simple. Call Update
with the the one
else if r.Method == http.MethodPut {
fmt.Println("bar1 will be updated to bar1-updated")
if err := sess.DB(mongoDatabase).C(mongoCollection).Update(bson.M{"bar": "bar1"}, bson.M{"$set": bson.M{"bar": "bar1-updated"}}); err != nil {
http.Error(w, fmt.Sprintf("Failed to update: %s", err.Error()), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"success":true, "message": "bar1 has been updated to bar1-updated"}`))
}
Let's build and deploy
faas-cli up -f k3d-mongodb-crud.yml
Oops
Step 10/29 : RUN cat function/GO_REPLACE.txt >> ./go.mod || exit 0
---> Running in 2103ee3002c2
cat: can't open 'function/GO_REPLACE.txt': No such file or directory
This comes from k3d-mongodb-crud/build/k3d-mongodb-crud/Dockerfile
# Add user overrides to the root go.mod, which is the only place "replace" can be used
RUN cat function/GO_REPLACE.txt >> ./go.mod || exit 0
cd k3d-mongodb-crud/
export GO111MODULE=on
go mod init
go get
go mod tidy
cat go.mod > GO_REPLACE.txt
GO_REPLACE.txt should look like
module github.com/go-serverless/k3d-mongodb-crud/k3d-mongodb-crud
go 1.14
require (
github.com/alexellis/hmac v0.0.0-20180624211220-5c52ab81c0de // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/openfaas/faas-provider v0.15.0 // indirect
github.com/openfaas/openfaas-cloud v0.0.0-20200319114858-76ce15eb291a
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22
gopkg.in/yaml.v2 v2.2.8 // indirect
)
Let's build and deploy again
faas-cli up -f k3d-mongodb-crud.yml
Testing
Currently I don't see OpenFaaS UI Portal allow us to select the HTTP request method.
Hence, once your endpoints are ready, use curl to test instead
Create
curl http://127.0.0.1:8080/function/k3d-mongodb-crud --request POST
{"success":true, "message": "4 records have been inserted"}
Go to MongoDB, check the collection
> db.foo.find()
{ "_id" : ObjectId("5e8014f10c4812773ee77f16"), "bar" : "bar", "baz" : "baz" }
{ "_id" : ObjectId("5e8014f10c4812773ee77f17"), "bar" : "bar1", "baz" : "baz1" }
{ "_id" : ObjectId("5e8014f10c4812773ee77f18"), "bar" : "bar2", "baz" : "baz2" }
{ "_id" : ObjectId("5e8014f10c4812773ee77f19"), "bar" : "bar3", "baz" : "baz3" }
Read
curl http://127.0.0.1:8080/function/k3d-mongodb-crud --request GET
[{"Bar":"bar","Baz":"baz"},{"Bar":"bar1","Baz":"baz1"},{"Bar":"bar2","Baz":"baz2"},{"Bar":"bar3","Baz":"baz3"}]
Update
curl http://127.0.0.1:8080/function/k3d-mongodb-crud --request PUT
{"success":true, "message": "bar1 has been updated to bar1-updated"}
Go to MongoDB, check the collection
> db.foo.find()
{ "_id" : ObjectId("5e8014f10c4812773ee77f16"), "bar" : "bar", "baz" : "baz" }
{ "_id" : ObjectId("5e8014f10c4812773ee77f17"), "bar" : "bar1-updated", "baz" : "baz1" }
{ "_id" : ObjectId("5e8014f10c4812773ee77f18"), "bar" : "bar2", "baz" : "baz2" }
{ "_id" : ObjectId("5e8014f10c4812773ee77f19"), "bar" : "bar3", "baz" : "baz3" }
Delete
curl http://127.0.0.1:8080/function/k3d-mongodb-crud --request DELETE
{"success":true, "message": "bar1 has been deleted"}
> db.foo.find()
{ "_id" : ObjectId("5e8014f10c4812773ee77f17"), "bar" : "bar1-updated", "baz" : "baz1" }
{ "_id" : ObjectId("5e8014f10c4812773ee77f18"), "bar" : "bar2", "baz" : "baz2" }
{ "_id" : ObjectId("5e8014f10c4812773ee77f19"), "bar" : "bar3", "baz" : "baz3" }
It looks good. If you've encountered some errors, you can check the log to have more insights by running
faas-cli logs k3d-mongodb-crud
Clean up
To clean up, simply just run
k3d delete
Wrap up
Of course this is just a learning playground. In the real life scenarios, we take user input from the Request body and we may filter the requests by r.URL.Path
. Besides, the functions should be separated for better readability.
The full source code for this post is available
{% github go-serverless/k3d-mongodb-crud %}
If you are looking for Python version, please check out Get storage for your functions with Python and MongoDB.
I've also referenced a blog post Building a TODO API in Golang with Kubernetes written by Alex Ellis (The founder of OpenFaaS).
Here's some useful links Template Store - OpenFaaS Troubleshooting Guide - OpenFaaS mgo.v2 API Documentation OpenFaaS workshop