An event-driven architecture can let you seamlessly extend your application or improve the scalability, if you can handle the eventual consistency. But your app may not be ready for this yet, or you don’t own the code in the app. A recently added a feature to faasd
got me thinking about event driven architecture powered by the Postgres WAL. Which means we can seamlessly extend your app without needing to change the app!
This post will show you how to quickly deploy Postgresql along with an event listener and some custom functions. From there it’s up to you what you build.
The core components of faasd
are defined and deployed via a Compose spec. Yes that Compose made famous by Docker Compose. This is great because it allows us to add our own custom sidecar containers to deploy along side faasd
and more importantly along side and exposed to our OpenFaaS functions.
faasd is OpenFaaS reimagined, but without the cost and complexity of Kubernetes. It runs on a single host with very modest requirements, making it fast and easy to manage.
This is super cool, but not the feature that got me thinking. It may not sound like much, but faasd
0.10.0+ now has support to set the container user! This is important because if I want to run a database next to my functions and use a local folder to provide durable persistence, I need to set the container user for Postgres.
Now that I can run Postgres and functions together, I started thinking “I should have easy access to the WAL”. Cue evil laughter.
I started looking at wal2json
and but ultimately ended up looking at wal-listener
because I looked at the OpenFaaS ecosystem and we already have event driven functions via the nats-connector
and faasd
already installs NATS. With wal-listenr
plus `Postgres just work directly on top of that.
Get started
If you have faasd
up and running, then you can skip this part, but if not, then I recommend you grab multipass
, start a new VM and run this
git clone https://github.com/openfaas/faasd
cd faasd
./hack/install.sh
You can also get started with cloud-config, check out this tutorial
Add postgres to the mix
I created a small proof-of-concept repo that adds postgres, wal-listener, and a simple “receiver” function that just prints all of the events it receives. It doesn’t do anything, but you can simply replace the function with any logic you want, first thing that comes to my mind webhooks Parse the event and forward it to another function or service.
-
clone the example repo
git clone https://github.com/LucasRoesler/openfaas-wal-listener.git cd openfaas-wal-listener
-
Copy the new docker-compose
make install # or manually run # sudo cp -rf postgres /var/lib/faasd # sudo mkdir -p /var/lib/faasd/postgres/pgdata # sudo mkdir -p /var/lib/faasd/postgresql/run # sudo cp -rf wal_listener /var/lib/faasd # sudo cp docker-compose.yaml /var/lib/faasd # sudo chown -R 1000:1000 /var/lib/faasd/postgres
-
Restart faasd
make restart # or manually: sudo systemctl restart faasd faasd-provider
-
Deploy a function to receive database events and print them out
faas-cli deploy --name receive-event \ --image theaxer/receive-message:latest \ --fprocess='./handler' \ --annotation topic="sample_app"
You can then verify that the connector is sending events to tne receiver fucntion by manually publishing the topic (you need to install the natscli)
$ nats pub sample_app "manual push"
15:21:39 Published 11 bytes to "sample_app"
$ faas-cli logs receive-event
2021-01-24T14:21:17Z 2021/01/24 14:21:17 Started logging stderr from function.
2021-01-24T14:21:17Z 2021/01/24 14:21:17 Started logging stdout from function.
2021-01-24T14:21:17Z 2021/01/24 14:21:17 OperationalMode: http
2021-01-24T14:21:17Z 2021/01/24 14:21:17 Timeouts: read: 10s, write: 10s hard: 10s.
2021-01-24T14:21:17Z 2021/01/24 14:21:17 Listening on port: 8080
2021-01-24T14:21:17Z 2021/01/24 14:21:17 Writing lock-file to: /tmp/.lock
2021-01-24T14:21:17Z 2021/01/24 14:21:17 Metrics listening on port: 8081
2021-01-24T14:21:17Z Forking - ./handler []
2021-01-24T14:21:39Z 2021/01/24 14:21:39 POST / - 200 OK - ContentLength: 27
2021-01-24T14:21:39Z 2021/01/24 14:21:39 stderr: 2021/01/24 14:21:39 received "manual push"
Verify db events are sent
The example repo contains a small SQL script that will create a User
table and upsert several users, the events from wal-listener
will look like
{
"id": "cd03a0b0-8c98-4809-9347-9f469c773de0",
"schema": "public",
"table": "users",
"action": "UPDATE",
"data": {
"created_at": "2021-01-23 16:50:53.543372+00",
"updated_at": "2021-01-23 16:50:53.543372+00",
"name": "Sarah Walker",
"email": "walker@nerdherd.com",
"id": "3879dc32-78cd-456d-8a64-fe7fab540a7f"
},
"commitTime": "2021-01-24T15:05:00.007715Z"
}
Let’s see it in action:
-
Generate some changes in the db
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE TABLE IF NOT EXISTS "users" ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), created_at timestamptz NOT NULL DEFAULT NOW(), updated_at timestamptz NOT NULL DEFAULT NOW(), name text, email text UNIQUE ); CREATE UNIQUE INDEX IF NOT EXISTS users_email_idx ON users (email); INSERT INTO users (name, email) VALUES ('John Wick', 'wick@trouble.com'), ('James Bond', 'bond@mi6.com'), ('Elim Garak', 'garak@ds9.com'), ('Sarah Walker', 'walker@nerdherd.com') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name RETURNING *;
This script is safe to run several time, on the repeated runs it will generate UPDATE events for the function.
You can run it with the Makefile (or manually), the db password is
supersecret
make init-db # psql -U postgres -h localhost -d app -f postgres/init_app.sql
-
You can then see the logs as wal-listener reacts to the inserts by using
sudo journalctl -f -t openfaas:wal-listener --since="5 minutes ago"
These are cool, but magic is when we look at the function logs.
-
Finally, see that our receiver function was invoked
$ faas-cli logs receive-event POST / - 200 OK - ContentLength: 386 stderr: 2021/01/24 15:04:25 received "{\"id\":\"92c431a7-9027-4656-9d39-f52ee90d5dd6\",\"schema\":\"public\",\"table\":\"users\",\"action\":\"UPDATE\",\"data\":{\"created_at\":\"2021-01-23 16:50:53.543372+00\",\"updated_at\":\"2021-01-23 16:50:53.543372+00\",\"name\":\"John Wick\",\"email\":\"wick@trouble.com\",\"id\":\"99a9c7bf-8f31-4c30-998e-9740f87bdaa0\"},\"commitTime\":\"2021-01-24T15:04:25.58604Z\"}" POST / - 200 OK - ContentLength: 383 stderr: 2021/01/24 15:04:25 received "{\"id\":\"80a8d33a-0ae5-4aed-825d-1a7f9e24adfb\",\"schema\":\"public\",\"table\":\"users\",\"action\":\"UPDATE\",\"data\":{\"name\":\"James Bond\",\"email\":\"bond@mi6.com\",\"id\":\"5a6a9672-722a-40c9-8f10-17cf527a6b41\",\"created_at\":\"2021-01-23 16:50:53.543372+00\",\"updated_at\":\"2021-01-23 16:50:53.543372+00\"},\"commitTime\":\"2021-01-24T15:04:25.58604Z\"}" POST / - 200 OK - ContentLength: 384 stderr: 2021/01/24 15:04:25 received "{\"id\":\"326ea179-dddb-4c04-bd56-9f02bdec9aaa\",\"schema\":\"public\",\"table\":\"users\",\"action\":\"UPDATE\",\"data\":{\"created_at\":\"2021-01-23 16:50:53.543372+00\",\"updated_at\":\"2021-01-23 16:50:53.543372+00\",\"name\":\"Elim Garak\",\"email\":\"garak@ds9.com\",\"id\":\"b1f3cc48-6366-4bff-b0c5-3f5beed02f44\"},\"commitTime\":\"2021-01-24T15:04:25.58604Z\"}" POST / - 200 OK - ContentLength: 392 stderr: 2021/01/24 15:04:25 received "{\"id\":\"15755624-195a-4617-8d60-6b7cb748caf9\",\"schema\":\"public\",\"table\":\"users\",\"action\":\"UPDATE\",\"data\":{\"id\":\"3879dc32-78cd-456d-8a64-fe7fab540a7f\",\"created_at\":\"2021-01-23 16:50:53.543372+00\",\"updated_at\":\"2021-01-23 16:50:53.543372+00\",\"name\":\"Sarah Walker\",\"email\":\"walker@nerdherd.com\"},\"commitTime\":\"2021-01-24T15:04:25.58604Z\"}" POST / - 200 OK - ContentLength: 387 stderr: 2021/01/24 15:05:00 received "{\"id\":\"38f4ae55-86a1-466a-8ebd-c2f5a1054439\",\"schema\":\"public\",\"table\":\"users\",\"action\":\"UPDATE\",\"data\":{\"updated_at\":\"2021-01-23 16:50:53.543372+00\",\"name\":\"John Wick\",\"email\":\"wick@trouble.com\",\"id\":\"99a9c7bf-8f31-4c30-998e-9740f87bdaa0\",\"created_at\":\"2021-01-23 16:50:53.543372+00\"},\"commitTime\":\"2021-01-24T15:05:00.007715Z\"}" stderr: 2021/01/24 15:05:00 received "{\"id\":\"bce01097-815d-4d73-90b9-89230ddca39b\",\"schema\":\"public\",\"table\":\"users\",\"action\":\"UPDATE\",\"data\":{\"email\":\"bond@mi6.com\",\"id\":\"5a6a9672-722a-40c9-8f10-17cf527a6b41\",\"created_at\":\"2021-01-23 16:50:53.543372+00\",\"updated_at\":\"2021-01-23 16:50:53.543372+00\",\"name\":\"James Bond\"},\"commitTime\":\"2021-01-24T15:05:00.007715Z\"}" POST / - 200 OK - ContentLength: 384 POST / - 200 OK - ContentLength: 385 stderr: 2021/01/24 15:05:00 received "{\"id\":\"45f0fc1f-a44f-4546-a7dd-2fec7079667d\",\"schema\":\"public\",\"table\":\"users\",\"action\":\"UPDATE\",\"data\":{\"name\":\"Elim Garak\",\"email\":\"garak@ds9.com\",\"id\":\"b1f3cc48-6366-4bff-b0c5-3f5beed02f44\",\"created_at\":\"2021-01-23 16:50:53.543372+00\",\"updated_at\":\"2021-01-23 16:50:53.543372+00\"},\"commitTime\":\"2021-01-24T15:05:00.007715Z\"}" POST / - 200 OK - ContentLength: 393 stderr: 2021/01/24 15:05:00 received "{\"id\":\"cd03a0b0-8c98-4809-9347-9f469c773de0\",\"schema\":\"public\",\"table\":\"users\",\"action\":\"UPDATE\",\"data\":{\"created_at\":\"2021-01-23 16:50:53.543372+00\",\"updated_at\":\"2021-01-23 16:50:53.543372+00\",\"name\":\"Sarah Walker\",\"email\":\"walker@nerdherd.com\",\"id\":\"3879dc32-78cd-456d-8a64-fe7fab540a7f\"},\"commitTime\":\"2021-01-24T15:05:00.007715Z\"}"
I trimmed log timestamps for legibility.
Next steps
One tiny service and we have integrated Postgres into OpenFaaS. But, this example is just the tip of the iceberg, the example doesn’t do anything. If you have a postgres application, you could easily deploy wal-listener
for your database, it will work with your local self-hosted Postgres and cloud hosted Postgres like RDS. You could send events from your application to an an automation system like Zapier or n8n
If you don’t have Postgres, let’s say you are a MySQL fan or a NoSQL fan using MongoDB, don’t worry I won’t hold it against you. Also, you can mimic the same workflow by just swapping out wal-listener
. The lapidus project supports both MySQL and MongoDB, but I am not a MySQL or Mongo expert, so YMMV.
Another connector library was also recommended to me, debezium. It has several connectors including Postgre, MySQL, MongoDB, Oracle, and Cassandra.
You can also find a walk-through of how to deploy and customize faasd
in Alex’s new book Serverless for Everyone Else.
If you build something awesome, let me know on Twitter or stop by the OpenFaas Slack and share it with the community.