An alternative method to webhooks might be pushing an event/message onto an AWS SQS queue and having any consumer deal with it however they want.
Unfortunately the other application might not use Laravel, or, if it does, the job name might not be compatible with your project.
I will outline with the use terraform
to setup a SQS queue, and provide some commands to push raw messages, and the few steps required to set up the queue, and a job to process messages in Laravel.
AWS
We just need to create a simple queue, (not FIFO) and I use the default settings.
The next steps are to allow us to create messages through the cli, and optionally, create the queue using terraform
.
Create a profile and set the config and credentials:
aws --profile=ally-api-webhooks configure
When enqueueing a message (or anything else, for that matter) with awscli
it closes out to a pager (less
by default) which is super annoying, so the following command will disable it (alternatively you could use an environment variable AWS_PAGER
).
aws --profile=ally-api-webhooks configure set cli_pager ''
Terraform (optional)
The terraform
script is simple, since SQS is relatively simple.
variables.tf
:
variable "aws_profile" {
type = string
default = "ally-api-webhooks"
}
variable "aws_region" {
type = string
default = "eu-west-2"
}
variable "queue_name" {
type = string
default = "webhook_queue"
}
main.tf
:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
provider "aws" {
profile = var.aws_profile
region = var.aws_region
shared_config_files = [pathexpand("~/.aws/config")]
shared_credentials_files = [pathexpand("~/.aws/credentials")]
}
resources.tf
:
resource "aws_sqs_queue" "webhook_queue" {
name = var.queue_name
}
resource "aws_sqs_queue_policy" "webhook_queue_policy" {
queue_url = aws_sqs_queue.webhook_queue.id
policy = <<POLICY
{
"Version": "2008-10-17",
"Id": "webhook_queue_policy",
"Statement": [
{
"Sid": "webhook_queue",
"Effect": "Allow",
"Principal": {
"AWS": "${data.aws_caller_identity.current.account_id}"
},
"Action": [
"SQS:*"
],
"Resource": "arn:aws:sqs:eu-west-2:${data.aws_caller_identity.current.account_id}:"
}
]
}
POLICY
}
data.tf
:
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
output.tf
:
output "queue_url" {
value = aws_sqs_queue.webhook_queue.url
}
output "region" {
value = data.aws_region.current.name
}
output "SQS_PREFIX" {
value = replace(aws_sqs_queue.webhook_queue.url, "/${var.queue_name}", "")
}
output "SQS_QUEUE" {
value = var.queue_name
}
The core commands for terraform
are:
terraform init
terraform fmt
terraform plan
terraform apply
This might take a minute, arguably quicker to create through the web UI.
It will also give you some values to set as environment variables for queue config later on.
Project (optional)
This is for example purposes, so I create a fresh install.
docker run \
--rm \
--tty \
--interactive \
--volume="$(pwd)/src:/app" \
--volume="${COMPOSER_HOME:-$HOME/.composer}:/tmp" \
--workdir=/app \
thecodingmachine/php:7.4-v4-cli \
composer create-project laravel/laravel=^8 src
Then to get a shell in the container:
docker run \
--rm \
--tty \
--interactive \
--volume="$(pwd)/src:/app" \
--volume="${COMPOSER_HOME:-$HOME/.composer}:/tmp" \
--workdir=/app \
thecodingmachine/php:7.4-v4-cli \
bash
Driver
I use primitivesense/laravel-raw-sqs-connector
to interpret arbitrary SQS messages to be handled by a Job
in Laravel world.
composer require primitivesense/laravel-raw-sqs-connector
After install, add their service provider into your providers
in config/app.php
:
<?php
return [
'providers' => [
/*
* Package Service Providers...
*/
\PrimitiveSense\LaravelRawSqsConnector\RawSqsServiceProvider::class,
],
];
Queue
Duplicate the sqs
connection in config/queue.php
, renaming it to sqs-plain
.
Change the driver
from sqs
to raw-sqs
- this new driver is from the package.
config/queue.php
:
<?php
return [
'connections' => [
'sqs-plain' => [
// custom driver from the service provider
'driver' => 'raw-sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
],
];
Job
Create a new job to handle the SQS messages, i.e. php artisan make:job ProcessWebhook
.
Make the following changes:
app/Jobs/ProcessWebhook.php
:
<?php
namespace App\Jobs;
-use Illuminate\Bus\Queueable;
-use Illuminate\Contracts\Queue\ShouldQueue;
-use Illuminate\Foundation\Bus\Dispatchable;
-use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\SerializesModels;
+use PrimitiveSense\LaravelRawSqsConnector\RawSqsJob;
-class ProcessWebhook implements ShouldQueue
+class ProcessWebhook extends RawSqsJob
{
- use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle()
{
+ // TODO: handle the job, use $this->getData() to get SQS message contents
}
}
Finally, set job_class
to \App\Jobs\ProcessWebhook::class
in your sqs-plain
queue connection in config/queue.php
, i.e.:
<?php
return [
'connections' => [
'sqs-plain' => [
// custom driver from the service provider
'driver' => 'raw-sqs',
'job_class' => \App\Jobs\ProcessWebhook::class,
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
],
];
You should be good to consume messages now.
Producer
Since the scenario is to process an arbitrary SQS payload with a Job
, we will use aws sqs send-message
to enqueue a ‘job’.
cat <<EOF | aws \
--profile=ally-api-webhooks \
sqs \
send-message \
--queue-url=$(terraform output -raw queue_url) \
--message-body file:///dev/stdin
{
"request_headers": [],
"request_body": {
"from": "$(hostname)",
"at": "$(date)"
}
}
EOF
Consumer
The last step is to run the queue.
Monitor/verify configuration:
php artisan queue:monitor sqs-plain:webhook_queue
Process the message:
php artisan queue:work --once --queue=webhook_queue sqs-plain