February 2, 2023

Simple Azure Service Bus Emulator – Finally Here!

Why is there no emulator for it? There is one now! Modern development environments and fast integration tests often require services deployed locally. Recently, it has become more visible as companies shift towards more rapid release cycles, containerization, and prioritized feedback loops.

Problem

First, let’s understand the problem we are trying to solve with an emulator. You might know from your own experience that when optimizing development experience, you want to design for maximum isolation. Thanks to this, unwanted interference is limited, and one person’s problem doesn’t affect the whole team immediately. At the same time, we prefer solutions in which infrastructure can be threaded as cattle – be disposed of as quickly as it can be provisioned.

Unfortunately, Microsoft has not yet released any official emulator for Azure Service Bus, requiring all developers and integration tests to connect to Azure provisioned instance (or use other techniques listed later in the post). There is still hope for such an emulator to be released, as we have seen Cosmos DB or Azure Storage emulators available, well documented, containerized, and invested in.

Architecture

Now, if you are interested in how it all works, let’s take a look at the relatively simple-looking design ๐Ÿ˜‰

On the left of the picture, we have an application connecting to an Azure Service Bus Emulator on standard TLS-enabled TCP port 5672 using AMQP 1.0 protocol. Then on the right, we have the emulator translating most of the messages to AMQP 0.9 using the standard Rabbit MQ client library. In the end, in a way, our emulator is just a proxy, but it has to do a few more tricks to make it Service Bus compatible.

These are a few of Azure Service Bus’s AMQP 1.0 dialect specifics we handle on top of AMQP 1.0 <> AMQP 0.9 translation:

  • MSSBCBS authentication mechanism
  • Accessible on TLS port 5671, with a valid server certificate, as enforced by client libraries
  • Handle Service Bus specific management commands, like Peek or Lock Update
  • Set some metadata fields that client libraries expect

Installation

Before we start, note that all examples require a Windows machine with Docker installed. Even though exact commands might be slightly different on Linux or Mac OS, the idea behind them remains the same, so you can follow the same steps to get it running.

All in all, these are the three simple steps we need to execute to use the emulator.

  1. Start docker containers,
  2. Trust server’s test certificate issuer,
  3. Update connection string

First, start the emulator’s containers with the following sample docker-compose.yml. It will run the RabbitMQ server and the emulator accessible on AMQP standard TCP port 5671.

docker compose up --detach
version: "3.9"

services:
  rabbit:
    image: rabbitmq:3-management
    ports:
      - "15672:15672"
    healthcheck:
      test: rabbitmq-diagnostics check_port_connectivity
      interval: 1s
      timeout: 30s
      retries: 60

  emulator:
    image: devopsifyme/sbemu:latest
    environment:
      - EMULATOR__RABBITMQ__HOST=rabbit
      - EMULATOR__RABBITMQ__USERNAME=guest
      - EMULATOR__RABBITMQ__PASSWORD=guest
    ports:
      - "5671:5671"
    links: 
      - rabbit
    depends_on:
      rabbit:
        condition: service_healthy

Now, connect to Rabbit MQ Management UI on http://localhost:15672 (username: guest and password: guest) and verify if you can see a connection from the emulator. Later, you will see all the queues, topics, and subscriptions in this UI. It will even be possible to trigger some messages manually if you wish.

Next, we need to trust the issuer of the emulator’s server certificate. Therefore, first copy it out of the container, then import it to the Trusted Root Certificates of your current user.

$containerPrefix = Split-Path (Get-Location) -Leaf
docker cp $containerPrefix-emulator-1:/app/cacert.cer cacert.cer
Import-Certificate -FilePath cacert.cer -CertStoreLocation cert:\CurrentUser\Root

Finally, update your Azure Service Bus Connection String to the one below. You must use the exact one, as the emulator will accept only specified authentication keys.

Endpoint=sb://localhost/;SharedAccessKeyName=all;SharedAccessKey=CLwo3FQ3S39Z4pFOQDefaiUd1dSsli4XOAj3Y9Uh1E=;EnableAmqpLinkRedirect=false

At this point, you are ready to test your application! Here is how it went for me with our Example Azure Function. On the left side, you can see the Azure Function console; on the right side, you can see output from Rabbit MQ (yellow) and Azure Service Bus Emulator (blue). After both containers are started, you can see message exchange and emulator tracing all AMQP messages received and sent.

That is pretty much it – you now have a local Azure Service Bus Emulator on your machine. As you might imagine, using it for any local development or integration testing is pretty straightforward. Please comment or contact us on LinkedIn or GitHub if you feel we could better support your use case.

Alternatives

Let’s see what other options are available for your local development experience and why we concluded that we need an emulator. Just by quickly looking at the Stack Overflow, we can notice that thousands of people starting their journey with Service Bus are wondering the same thing – how to enable a smooth local development experience.

ProsCons
Emulator* Developers have easier, simplified environment to use
* No code changes required
* Can be run offline
* Can be run in isolation
* Can be quickly setup
* Can be containerized
* Depending on implementation, not all features are supported
Use different bus locally* Quick and easy, when using libraries like MassTransit or NServiceBus
* Developers have easier, simplified environment to use
* Two code paths depending on the environment
* Can behave differently than on cloud
* Potentially learn and manage two products (if all developers do operations)
One SB namespace* Provisioned once
* Using actual product
* Doesn’t work in teams with more than one person
* Not possible to run more than one integration suite at one time
* Requires internet connectivity at all times
* Secrets management
One SB namespace with developer isolation* Provisioned once
* Using actual product
* Requires code changes to support agreed naming conventions that all follow
* Requires internet connectivity at all times
* Secrets management
* Access control, interference
Multiple SB namespaces* Each person / test run can provision its namespace
* Using actual product
* Name has to be globally unique, naming convention has to be designed
* Requires internet connectivity at all times
* Secrets management
* Everybody needs to know which subscription to use, which SKU to select, etc.

For a message bus, a local environment is a strict requirement. Therefore, we are left with two options: an emulator or using a different bus locally. Most of the time, I would advocate for the latter – if you are already using Mass Transit, add a few extra configuration lines and switch to RabbitMQ locally.

On the other hand, I have seen code and projects that are not feasible (needed rewrite, the team has no skills, different priorities, no support in framework, etc.). In this situation, I would recommend using a 3rd party emulator since 1st party one is not an option (yet?).

Lastly, every team, project, and environment is different – context matters a lot. Therefore the best solution for you might be different than for others. For us, we opted for an emulator as it seems more accessible for the developers at this time.

What if…

… I cannot use Docker

Sure! First, grab binaries from the latest release here devopsifyme-sbemulator (github.com), install your Rabbit MQ server if you don’t have one, and finally, configure and run the emulator. You will also need to provide your own trusted server certificate.

… we connected directly to AMQP 1.0 Rabbit MQ endpoint?

Long story short, it will not work out of the box ๐Ÿ˜‰ But eventually, I have managed to get it to work with some hacks ๐Ÿ˜‰ Here is the example application: devopsifyme-sbemulator (github.com). Please run the ‘devopsifyme/sbemu-rabbitmq’ image instead of the vanilla rabbitmq from Docker Hub, as that one runs TLS on port 5671 with a proper server certificate and AMQP 1.0 enabled.

There are two routes one can take to try to make things work:

  • Implement Rabbit MQ plugin in Erlang
  • Patch MSIL of Azure.Messaging.ServiceBus assembly

I first started with a plugin, but after initial successes, more and more problems were popping up. Therefore I gave up, admitting I am not an Erlang expert nor a Rabbit MQ internals expert ๐Ÿ˜‰ It just felt like too much of an effort for a small fun project ๐Ÿ™‚

Since none of these two seem maintainable at this point, the second one is much more appealing and easier for me having strong C# background. I have used Harmony 2 (pardeike.net) library, which allows us to inject code into existing methods, even the internal ones ๐Ÿ˜‰ After some tweaks, I was able to send and receive messages from a queue successfully ๐Ÿ™‚

The following snippet shows how we inject code into the SaslAnonymousHandler constructor to override its argument’s value. This is very shady stuff if you ask me ๐Ÿ™‚

[HarmonyPatch(typeof(SaslAnonymousHandler), MethodType.Constructor, new[] { typeof(string) })]
internal static class ForceAnonymousSaslPatch
{
    static void Prefix(ref string name)
    {
        name = "ANONYMOUS";
    }
}

This is what we have to patch in Azure.Messaging.ServiceBus assembly provided by Microsoft, to get basics to work:

  • Replace MSSBCBS mechanism with ANONYMOUS
  • Disabled CSB authentication
  • Set Max Message Size for a link
  • Patch queue names to be compatible with Rabbit MQ

OK, that was fun – but I hope you can clearly see it is a dead end, as is just unmaintainable long run ๐Ÿ˜‰

… we connected directly to AMQP 1.0 ActiveMQ endpoint?

I have not investigated this that much, as it failed out of the door with ActiveMQ saying, “cannot decode AMQP frame.”

… we used Azure Storage Queue (Azurite) instead of Rabbit MQ?

Why not? ๐Ÿ™‚ Using Storage Explorer to manage and peek at queues might get a much better user experience than the currently implemented Rabbit MQ backend.

The emulator is internally pluggable, so it is possible to replace the backend implementation. I will be more than happy to accept contributions if someone is willing to spend the time ๐Ÿ™‚

References

Microsoft

Inspired by

People looking for help

3 Comments

  • For some reason i was able to create both containers using the yml file provided but it didnt create the connection between the 2 as the first step said it would.

    • It turned out to be a change in behavior introduced in one of the later versions causing the connection with RabbitMQ being established on first connection from SB client. This is now fixed by introducing health checks in docker image. //PS thanks Jorge for our conversation on email!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.