In my last blog post, I have written about how to turn a Raspberry Pi into a serverless platform with the lightweight OpenFaaS implementation faasd. If you haven't checked it out already, you can find the link to the post below:

Turn your Raspberry Pi into a serverless platform with faasd
Build a lightweight serverless platform on your Raspberry Pi with OpenFaas’ faasd and deploy your first function.
Please note that a Raspberry Pi is not required here.
faasd can be installed without issues on any other machine or cloud instance as well.
This blog post from Alex Ellis describes how to build a faasd serverless appliance on DigitalOcan:
https://blog.alexellis.io/deploy-serverless-faasd-with-cloud-init/

I did that only as a small hobby project. But then, I wanted to use this setup for a real-world use case, and also use the opportunity to try another useful open-source project which is inlets.

Inlets is a cloud native tunnel that allows you to expose private endpoints to the Internet. In this case, it will be used to expose the OpenFaaS gateway running on my Raspberry Pi board to receive incoming Stripe Webhook events and dispatch them to the relevant function.

For more details about inlets, check the official docs and resources available at https://docs.inlets.dev/  

So in this post, I will describe how I built a local OpenFaaS function that serves as a Stripe webhook. This function  simply sends a Slack notification for every new charge.succeeded event.

The architecture

Pictured: conceptual architecture with faasd for functions, inlets for the tunnel and stripe to generate webhooks.

Inlets is an open source HTTP tunnel which we can use to get a public IP address to receive webhooks. The inlets client will be running on the Raspberry Pi and connects to the inlets server using a websocket connection to listen for any incoming requests.

In our setup, every Stripe webhook event that is received by the inlets client will be forwarded to the OpenFaaS gateway on the Raspberry Pi, which will in turn trigger the stripe-slack-notifier function to send a new Slack notification.

Preparing the Setup

Obtain an exit node with inlets

First, make sure you have created an inlets exit server following this nice guide by Alex Ellis:

Get HTTPS for your local endpoints with inlets and Caddy
Learn how to get incoming HTTP traffic for webhooks, events and for serving websites and APIs from your local network and computer with inlets and Caddy.

This would allow you to have an exit node with a public domain that can be used for receiving webhook events. In the setup described earlier, the inlets client is running on the Raspberry Pi, so I have chosen the upstream to be http://127.0.0.1:8080, which is the OpenFaaS Gateway URL.

Using the token provided by the inlets server, the command below exposes the OpenFaaS Gateway to the Internet:

$ inlets client --remote wss://${REMOTE_EXIT_NODE} --upstream http://127.0.0.1:8080 --token ${INLETS_TOKEN}

Our function will then be available from the Internet using the https://${REMOTE_EXIT_NODE}/function/stripe-slack-notifier

Add a Stripe Webhook

We need to add a new Stripe webhook using the

Create a Slack App

To receive incoming webhooks we need to create an App in the Slack dashboard.

Create a function to receive webhooks from Stripe

The function stripe-slack-notifier was built using the python3-http template that is available in the OpenFaaS template store.

Creating a new function using faas-cli new

$ faas-cli template store list | grep python3-http
python3-http             openfaas-incubator Python 3.6 with Flask and HTTP
$ faas-cli template store pull python3-http

# Make sure to set this to you Docker hub username
$ export DOCKER_REGISTRY_PREFIX=<Docker hub username>

$ faas-cli new stripe-slack-notifier --lang python3-http --prefix ${DOCKER_REGISTRY_PREFIX}
Folder: stripe-slack-notifier created.
  ___                   _____           ____
 / _ \ _ __   ___ _ __ |  ___|_ _  __ _/ ___|
| | | | '_ \ / _ \ '_ \| |_ / _` |/ _` \___ \
| |_| | |_) |  __/ | | |  _| (_| | (_| |___) |
 \___/| .__/ \___|_| |_|_|  \__,_|\__,_|____/
      |_|


Function created in folder: stripe-slack-notifier
Stack file written: stripe-slack-notifier.yml
$ tree .
.
├── stripe-slack-notifier
│   ├── handler.py
│   └── requirements.txt
└── stripe-slack-notifier.yml

The final source code for the function can be found in this Github repository:

mehyedes/stripe-slack-notifier
Simple function that sends slack notifications based on Stripe events - mehyedes/stripe-slack-notifier

The function is written in python. It will verify the received payload using the Stripe API key and the webhook singing secret, and then send a slack notification:

import stripe
from babel import numbers
from slack_webhook import Slack

def fetch_secret(secret_name):
    secret_file = open(f"/var/openfaas/secrets/{secret_name}", 'r')
    return secret_file.read()


def handle(event, context):

    # Make sure to create the secrets below
    webhook_url = fetch_secret("slack-webhook-url")
    stripe.api_key = fetch_secret("stripe-secret-key")
    webhook_secret = fetch_secret("webhook-secret")
    
    payload = event.body
    received_sig = event.headers.get("Stripe-Signature", None)
    
    try:
        event = stripe.Webhook.construct_event(
            payload, received_sig, webhook_secret
        )
    except ValueError:
        print("Error while decoding event!")
        return {
            "body": "Bad payload",
            "statusCode": 400
        }
    except stripe.error.SignatureVerificationError:
        print("Invalid signature!")
        return {
            "body": "Bad signature", 
            "statusCode": 400
        }

    # Fail for all other event types  
    if event.type != "charge.succeeded":
        return {
            "body":"Unsupported event type",
            "statusCode": 422
        }
  
    amount = numbers.format_currency(
      event.data.object.amount / 100,
      event.data.object.currency.upper(), 
      locale='en'
    )

    try:
        slack = Slack(url=webhook_url)
        slack.post(text=f"You have a received a new payment of {amount} :moneybag: :tada:")
    except:
        print("An error occured when trying to send slack message.")
        return {
            "body": "Could not send slack message", 
            "statusCode": 500
        }
    return {
        "body": "Notification was sent successfully to Slack", 
        "statusCode": 200
    }
stripe-slack-notifier/handler.py

OpenFaaS secrets are used to pass the following data to the function:

  • Stripe API key
  • Webhook signing secret
  • Slack Webhook URL

So they need to be created manually with faas-cli before deploying the function:

$ faas-cli secret create slack-webhook-url \
  --from-literal=${SLACK_WEBHOOK_URL} --gateway http://raspberrypi.loc:8080
$ faas-cli secret create stripe-secret-key \
  --from-literal=${STRIPE_API_KEY} --gateway http://raspberrypi.loc:8080
$ faas-cli secret create webhook-secret \
  --from-literal=${WEBHOOK_SIGNING_SECRET} --gateway http://raspberrypi.loc:8080
$ faas-cli secret list --gateway http://raspberrypi.loc:8080  

NAME
slack-webhook-url
stripe-secret-key
webhook-secret

Building & deploying the function

Since the function will be running on Raspberry Pi, we need to build the Docker image for armv7.
Fortunately, this has become easier with the multi platform builds feature by buildx available in docker v19.03.

But since we have already exposed our OpenFaaS gateway using inlets, we can use Github Actions to build and deploy the latest version of our function to the Raspberry Pi with every git push. The workflow file used to automate this can be found here.

Manually, this can be achieved with the following steps:

  1. Generate the Docker build context for the function
$ faas-cli build --shrinkwrap -f stripe-slack-notifier.yml

2.   This step is not needed if using Docker for desktop as this would be done automatically for you. On Linux, we need to register other platforms like armv7 with the kernel using the docker/binfmt image:

$ docker run --privileged --rm docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64

3.   Create a new builder for buildx

$ docker buildx create --name armhf --platform linux/arm/v7 
armhf

$ docker buildx ls 
NAME/NODE DRIVER/ENDPOINT             STATUS  PLATFORMS
armhf *   docker-container                    
  armhf0  unix:///var/run/docker.sock running linux/arm/v7, linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
default   docker                              
  default default                     running linux/amd64, linux/386

4.   Finally build and push the image for both armv7 and amd64 platforms:

$ DOCKER_BUILD_KIT=1 DOCKER_CLI_EXPERIMENTAL=enabled docker buildx build --platform linux/arm/v7,linux/amd64 -t ${DOCKER_REGISTRY_PREFIX}/stripe-slack-notifier:latest --push .

A new Docker image is now available on the Docker registry for both amd64 and armv7 architectures:

To deploy the function to the Raspberry Pi, we can finally run the command below from our laptop:

$ faas-cli deploy  -f stripe-slack-notifier.yml -g https://raspberrypi.loc:8080 

Test the function

Check the status of the function with faas-cli describe

$ faas-cli describe stripe-slack-notifier --gateway http://raspberrypi.loc:8080
Name:                stripe-slack-notifier
Status:              Ready
Replicas:            1
Available replicas:  1
Invocations:         0
Image:               
Function process:    
URL:                 http://raspberrypi.loc:8080/function/stripe-slack-notifier
Async URL:           http://raspberrypi.loc:8080/async-function/stripe-slack-notifier

We can test that it's now by triggering some test events from the Stripe dashboard:

After a few moments, we can see a similar message in Slack:

The function only supports charge.succeeded Stripe events for now, so any other type of event will receive Unsupported event type error from the function, and no Slack messages will be sent.

Conclusion

In this blog post, I have described how we can create a serverless webhook handler for Stripe API events using faasd and inlets.

As we have seen in this post, faasd can be a great alternative to running OpenFaaS without the need for managing a Kubernetes or Docker Swarm cluster. Combined with inlets, they can be used together for running and exposing functions for real-world use cases.

Things that you might want to do next: