Azure Service Bus Emulator sur Kubernetes

Le 18 novembre dernier, Microsoft a publié son émulateur pour Azure Service Bus. Celui-ci complète notamment Azurite (l’émulateur pour Azure Storage), et permet enfin de développer une solution entièrement en local, malgré une dépendance à Azure Service Bus. Outre les aspects financiers, avoir un émulateur permet une meilleur résilience en télétravail: jusqu’à maintenant, une interruption de connexion internet rendait presqu’impossible le développement d’un projet dépendant du Service Bus.

Présentation officielle

La documentation officielle est ici:

Overview of the Azure Service Bus emulator (documentation)
Introducing Local emulator for Azure Service Bus (article d’introduction)
Azure Service Bus Emulator Installer (dépôt GitHub)

L’émulateur est proprosé sous la forme d’une image docker, et dépend de SQL Server. Les divers guides actuellement publiés par Microsoft sont orientés Docker Desktop.

Disposant d’un cluster Kubernetes, j’ai préféré déployer l’émulateur dessus, plutôt que surcharger mon ordinateur portable avec Docker. Cette dernière option m’est également utile en complément, lorsque je suis en déplacement (j’utilise alors un cluster Minikube dans Docker Desktop sur WSL, ce qui me permet de réutiliser à peu près les mêmes manifestes Kubernetes). Mais le reste du temps, je n’utilise pas Docker Desktop.

Déploiement sur Kubernetes

Il y a deux composants à déployer: SQL Server et Service Bus Emulator. J’ai essentiellement traduit les exemples proposés par Microsoft pour Docker, en les adaptant au format des manifestes de Kubernetes.

Curieusement, la documentation de Microsoft pointe vers l’utilisateur de l’image docker mcr.microsoft.com/azure-sql-edge qui est documenté comme étant en fin de vie (fin prévue pour le 30 September 2025). L’image à utiliser est donc mcr.microsoft.com/mssql/server (avec le MSSQL_PID correspondant à l’édition Express).

SQL Server Express

Le groupe de manifestes suivant contient:

  • Un secret (pensez à y mettre votre mot de passe pour le compte sa de SQL Server). La façon dont vous gérez vos secrets peut varier, c’est pourquoi j’ai été au plus simple dans cet exemple. Personnellement, j’utilise ArgoCD avec ArgoCD Vault Plugin et 1Password Connect.
  • Un déploiement.
  • Un service.

On notera l’absence de persistent volume claim: cette configuration sert à stocker des données temporaires. Les données ne seront pas persistées si le conteneur est supprimé. Pour Azure Service Bus Emulator, ce dernier de toutes façon réinitialise sa base de données à chaque redémarrage, d’après ce que j’ai pu observer dans ses logs. Si vous souhaitez déployer SQL Server de manière plus pérenne, cet exemple ne convient pas. Pour lever tout doute, j’ai nommé le déploiement « sqlserver-inmemory ».


apiVersion: v1
kind: Secret
metadata:
  name: sqlserver-secrets
type: Opaque
stringData:
  MSSQL_SA_PASSWORD: "..."
---
# See https://mcr.microsoft.com/en-us/artifact/mar/mssql/server/about
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sqlserver-inmemory-deployment
  labels:
    app.kubernetes.io/name: sqlserver-inmemory
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: sqlserver-inmemory
  template:
    metadata:
      labels:
        app.kubernetes.io/name: sqlserver-inmemory
    spec:
      containers:
      - name: sqlserver-inmemory
        image: "mcr.microsoft.com/mssql/server:2022-CU16-ubuntu-22.04"
        resources:
          limits:
            memory: "2Gi"
            cpu: "250m"
        ports:
        - name: tcp
          containerPort: 1433
          protocol: TCP
        env:
        - name: MSSQL_PID
          value: "Express"
        - name: ACCEPT_EULA
          value: "Y"
        - name: MSSQL_SA_PASSWORD
          valueFrom:
            secretKeyRef:
              name: sqlserver-secrets
              key: MSSQL_SA_PASSWORD
---
apiVersion: v1
kind: Service
metadata:
  name: sqlserver-inmemory-service
  labels:
    app.kubernetes.io/name: sqlserver-inmemory
spec:
  selector:
    app.kubernetes.io/name: sqlserver-inmemory
  type: ClusterIP
  ports:
  - name: tcp
    port: 1433
    protocol: TCP
    targetPort: 1433

Une fois ces manifestes déployés, les logs du conteneur devraient indiquer au bout d’un court moment que le serveur est prêt à recevoir des connexion:


[...]
A self-generated certificate was successfully loaded for encryption.
Server is listening on [ 'any'  1433] accept sockets 1.
Server is listening on [ 'any'  1433] accept sockets 1.
Server is listening on [ ::1  1431] accept sockets 1.
Server is listening on [ 127.0.0.1  1431] accept sockets 1.
SQL Server is now ready for client connections. This is an informational message; no user action is required.

Azure Service Bus Emulator

Cette fois, le groupe de manifestes suivant contient:

  • Un ConfigMap: vous devrez l’adapter pour vos besoins. C’est ici que les topics, souscriptions, files sont configurées.
  • Un déploiement. Celui-ci contient un initContainer qui force l’attente du service SQL Server avant de déployer l’émulateur.
  • Un service.

# See https://learn.microsoft.com/en-us/azure/service-bus-messaging/test-locally-with-service-bus-emulator?tabs=docker-linux-container
apiVersion: v1
kind: ConfigMap
metadata:
  name: azure-sb-emulator-config
  labels:
    app.kubernetes.io/name: azure-sb-emulator
data:
  config.json: |
    {
      "UserConfig": {
        "Namespaces": [
          {
            "Name": "sbemulatorns",
            "Queues": [ ],
            "Topics": [
              {
                "Name": "my-topic",
                "Properties": {
                  "DefaultMessageTimeToLive": "PT1H",
                  "DuplicateDetectionHistoryTimeWindow": "PT20S",
                  "RequiresDuplicateDetection": false
                },
                "Subscriptions": [
                  {
                    "Name": "MyApp1-SubscribeTo-MyTopic1",
                    "Properties": {
                      "DeadLetteringOnMessageExpiration": false,
                      "DefaultMessageTimeToLive": "PT1H",
                      "LockDuration": "PT1M",
                      "MaxDeliveryCount": 10,
                      "ForwardDeadLetteredMessagesTo": "",
                      "ForwardTo": "",
                      "RequiresSession": false
                    },
                    "Rules": []
                  }
                ]
              },
            ]
          }
        ],
        "Logging": {
          "Type": "Console"
        }
      }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: azure-sb-emulator-deployment
  labels:
    app.kubernetes.io/name: azure-sb-emulator
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: azure-sb-emulator
  template:
    metadata:
      labels:
        app.kubernetes.io/name: azure-sb-emulator
    spec:
      initContainers:
      - name: init-wait-sqlserver
        image: alpine:3
        imagePullPolicy: IfNotPresent
        command: ["sh", "-c", "for i in $(seq 1 300); do nc -zvw1 sqlserver-inmemory-service 1433 && exit 0 || sleep 3; done; exit 1"]
      volumes:
      - name: azure-sb-emulator-config
        configMap:
          name: azure-sb-emulator-config
      containers:
      - name: azure-sb-emulator
        image: "mcr.microsoft.com/azure-messaging/servicebus-emulator:1.0.1"
        ports:
        - name: sb
          containerPort: 5672
          protocol: TCP
        volumeMounts:
        - mountPath: /ServiceBus_Emulator/ConfigFiles/Config.json
          name: azure-sb-emulator-config
          subPath: config.json
        env:
        - name: ACCEPT_EULA
          value: "Y"
        - name: SQL_SERVER
          value: sqlserver-inmemory-service
        - name: MSSQL_SA_PASSWORD
          valueFrom:
            secretKeyRef:
              name: sqlserver-secrets
              key: MSSQL_SA_PASSWORD
---
apiVersion: v1
kind: Service
metadata:
  name: azure-sb-emulator-service
  labels:
    app.kubernetes.io/name: azure-sb-emulator
spec:
  selector:
    app.kubernetes.io/name: azure-sb-emulator
  type: ClusterIP
  ports:
  - name: sb
    port: 5672
    protocol: TCP
    targetPort: 5672

Une fois déployé, si tout se passe bien, les logs de l’émulateur montreront que la base SQL Server est initialisée et que l’émulateur est prêt à recevoir des connexions.

On notera quelques défaut de jeunesse, comme le mot de passe du compte sa de SQL Server écrit en clair plusieurs fois dans les logs:


[...]
info: EmulatorLauncher[0]
      Emulator Service is Launching On Platform:CBL-Mariner/Linux,X64
info: EmulatorHealthCheckHelper[0]
      Waiting for 15 seconds before starting the health check for SQL
info: SQL-Setup[0]
      Dropping databases 'SbMessageContainerDatabase00001' at 'Data Source=sqlserver-inmemory-service,1433;User id=sa;Password=My-Password;Initial Catalog=master;Encrypt=false;'...
info: SQL-Setup[0]
      Dropping databases 'SbGatewayDatabase' at 'Data Source=sqlserver-inmemory-service,1433;User id=sa;Password=My-Password;Initial Catalog=master;Encrypt=false;'...
info: SQL-Setup[0]
      Creating database 'SbGatewayDatabase' at 'Data Source=sqlserver-inmemory-service,1433;User id=sa;Password=My-Password;Initial Catalog=master;Encrypt=false;'...
info: SQL-Setup[0]
      CREATE DATABASE SbGatewayDatabase
info: SQL-Setup[0]
      Creating database 'SbMessageContainerDatabase00001' at 'Data Source=sqlserver-inmemory-service,1433;User id=sa;Password=My-Password;Initial Catalog=master;Encrypt=false;'...
info: SQL-Setup[0]
      CREATE DATABASE SbMessageContainerDatabase00001
[...]
info: a.F.aFu[0]
      Triggering Entity Sync
info: a.F.aFu[0]
      Entity Sync complete; Operation Result:True
info: a.F.aFu[0]
      User defined entities created for SB Emulator
info: a.F.aFO[0]
      Emulator Service is Successfully Up!
[13:55:58 FTL] >Trc Id="30588" Ch="Operational" Lvl="Critical" Kw="4000000000000100" UTC="2024-11-23T13:55:58.289Z" Msg="ContainerId: 1 failed to report load to Winfab runtime.  Exception Details: ExceptionId: 2ef0af95-9f0d-427d-a3a6-93c8d7d0c2c7-System.ArgumentException: Service 'Q.Qd' not found.
   at P.Ph.A[A]()
   at a.A.aAp.aJ()
   at a.A.aAp.A(Object)" />

Connexion à l’émulateur

Pour se connecter à l’émulateur depuis notre code, on peut utiliser cette chaîne de connexion:


"Endpoint=sb://127.0.0.1;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"

Microsoft documente localhost dans ses exemples, mais dans mon cas cela ne fonctionne pas, et il m’a fallu utiliser 127.0.0.1. J’ai également essayé d’utiliser un port différent de celui par défaut sans succès. C’est pourquoi le service déployé est de type ClusterIP et pas NodePort. Pour que cela fonctionne, il faut établir une redirection de port avec kubectl:


kubectl port-forward svc/azure-sb-emulator-service 5672:5672

Mes premiers tests fonctionnent sans problème particulier (je peux publier des messages dans un topic, et les consommer via une souscription).