Deploying & Using KEDA

We’re often using HPA (Horizontal Pod Autoscaling) or may looked at VPA (Vertical Pod Autoscaling) but there’s also KEDA (Kubernetes Event-driven Autoscaling) which is an operator to scale workloads based upon (as the name suggests) events, for example queue triggers could come from a message bus/queue such as RabbitMQ. KEDA can also be used in such scenarios to scale to 0 pods (almost like the way we might use Azure functions).

Installing

To install KEDA, just add the repo to helm then update

Note: see official documentation https://keda.sh/docs/2.19/deploy, which I’ve partially reproduced here

  • helm repo add kedacore https://kedacore.github.io/charts
  • helm repo update

Now to install KEDA run

  • helm install keda kedacore/keda –namespace keda –create-namespace

As you’ll probably expect from this line, a new namespace keda is added to our Kubernetes cluster.

Kubernetes kind ScaledObject

We’ll want to configure K8s to scale our objects. Let’s use an example already on the web as it’s a perfectly simple illustration of using RabbitMQ to trigger scaling of our service. In this case we’ll scale down to 0 so essentially when our service is not being used (obviously you’d probably only want this for less frequently uses service due to cold-start etc.) we can scale the resources to zero to free up available namespace based memory etc.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: my-scaledobject
spec:
  scaleTargetRef:
    name: my-deployment
  minReplicaCount: 0
  maxReplicaCount: 10
  triggers:
    - type: rabbitmq
      metadata:
        queueName: my-queue
        host: amqp://user:pass@rabbitmq:5672/
        mode: QueueLength
        value: "10"   # messages per pod
      authenticationRef:
        name: rabbitmq-secret 

In this example we are using ScaledObject as the kind (this is a KEDA object). This YAML will allow K8s to auto-scale my-deployment pods based upon RabbitMQ queue activity, as stated previously this will scaled down to zero and we ensure a max scaling of 10 – we need to ensure that even if the backlog on the queue is large, we’re not going to scale out of control.

The scaleTargetRef tells KEDA which K8s workload to scale, in this case we expect a deployment names my-deployment.

The triggers section tells KEDA what metric to monitor, in this case KEDA will monitor queue metrics, of the queue my-queue. When messages appear in the queue, KEDA scale up the consumer pods and when the queue drains KEDA scales down the pods.

In our example we also use authenticationRef to securely store RabbitMQ credentials within K8s secrets.

For example, if your queue suddenly receives 100 messages and with this threshold of 1- messages per pod then KEDA will scale to 10 pods.

What KEDA gives us is the ability to use event-driven scaling based upon real workloads as opposed to using CPU or memory.

More uses

KEDA has a built in Azure Blob Storage scaler, allowing is to scale based upon the number of blobs in a container, this can be useful when long running jobs are triggered by files being written into blob storage. For example, processing uploads files, batch process triggered by blobs presence etc.

Support exists for Rabbit MQ as already seen but also for Azure Service Bus where (again) we can scaled based upon queue length as well as topic and subscription message count. Examples would include REST API to worker pods scaling, background processing as well as event driven microservices.

Redis List or Redis stream scalers are supported as well as Azure Managed Redis. Here we could scale based upon job queues within Redis lists, stream based event processing etc.

References

KEDA scalers
KEDA with Azure services

Let’s talk Server-Sent Events

Server-Sent Events (SSE) are a lightweight streaming/push technology for pushing updates to a web client over a single long-live HTTP connection.

For example as use case where your web client connects to a server and periodically receives events or notifications from the server. Alternates to SSE might be long polling the server for updates, web sockets, Signal R etc. SSE is simpler than setting up web sockets and for C#/.NET developers can be seen to be similar to asynchronous streams.

All modern browser should support SSE using EventSource. SSE also supports automatic reconnection but one key thing to remember is this is only for one-way updates (unlike Signal R).

ASP.NET Sample

Let’s create an ASP.NET server sample with an endpoint /events of type text/event-stream. This will just look and send “ticks” to a client.

app.MapGet("/events", async (HttpContext context, CancellationToken cancellationToken) =>
{
    context.Response.ContentType = "text/event-stream";
    context.Response.Headers.CacheControl = "no-cache";

    try
    {
        for (var i = 1; i <= 20; i++)
        {
            var payload = JsonSerializer.Serialize(new
            {
                id = i,
                message = $"Server tick {i}",
                sentAtUtc = DateTimeOffset.UtcNow
            });

            await context.Response.WriteAsync($"id: {i}\n", cancellationToken);
            await context.Response.WriteAsync("event: tick\n", cancellationToken);
            await context.Response.WriteAsync($"data: {payload}\n\n", cancellationToken);
            await context.Response.Body.FlushAsync(cancellationToken);

            await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
        }
    }
    catch (OperationCanceledException)
    {
    }
});

Pretty simple. As mentioned we need to set the ContentType to text/event-stream. SSE must never be cached, hence context.Response.Headers.CacheControl = “no-cache”. The handler should not return until all responses are sent, i.e. it goes into a loop or similar and writes responses.

It’s also important to note that the SSE spec requires the following wire format

  • id: An optional event ID
  • event: An optional even name
  • datae: The actual payload
  • Ends with a blank line: Notice of the line setting the data we have two new lines, i.e. one blank line. Without this the browser will not dispatch the event
  • Flush after each event: This will force the server to push the event immediately instead of buffering, otherwise the client might receive events in chunks

C# client code

Let’s look at what’s required for a C# client to interact with our server.

The HttpClient looks like this

using var httpClient = new HttpClient
{
    BaseAddress = new Uri("http://localhost:5127")
};

Now we’ll created the request tot he /events endpoint and accept the text/event-stream i.e.

using var cancellation = new CancellationTokenSource();

using var request = new HttpRequestMessage(HttpMethod.Get, "/events");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));

using var response = await httpClient.SendAsync(
    request,
    HttpCompletionOption.ResponseHeadersRead,
    cancellation.Token);

response.EnsureSuccessStatusCode();

Okay, seo we’ve configured things and called the endpoint, now let’s look at how we might read the SSE

await using var stream = await response.Content.ReadAsStreamAsync(cancellation.Token);
using var reader = new StreamReader(stream);

while (!cancellation.IsCancellationRequested)
{
  var line = await reader.ReadLineAsync(cancellation.Token);

  if (line is null)
  {
    break;
  }

  if (line.Length == 0)
  {
    if (eventDataBuilder.Length > 0)
    {
      Console.WriteLine($"[{DateTime.Now:T}] id={eventId}, event={eventName}, data={eventDataBuilder}");
    }

    eventId = string.Empty;
    eventName = string.Empty;
    eventDataBuilder.Clear();
    continue;
  }

  if (line.StartsWith("id: ", StringComparison.Ordinal))
  {
    eventId = line[4..];
    continue;
  }

  if (line.StartsWith("event: ", StringComparison.Ordinal))
  {
    eventName = line[7..];
    continue;
  }

  if (line.StartsWith("data: ", StringComparison.Ordinal))
  {
    if (eventDataBuilder.Length > 0)
    {
      eventDataBuilder.Append('\n');
    }

    eventDataBuilder.Append(line[6..]);
}

In the example code, above, we read each line from the SSE and get each part until we’re ready to put the read data together to write to the console.

Typescript client

The following example is a React Typescript implementation of a simply UI with connect buttons etc. to connect to the events URL, using an EventSource we add a listener to and output the SSE’s as they arrive.

type TickEvent = {
  id: number
  message: string
  sentAtUtc: string
}

const maxItems = 50

function App() {
  const [events, setEvents] = useState<TickEvent[]>([])
  const [isConnected, setIsConnected] = useState(false)
  const [status, setStatus] = useState('Disconnected')
  const eventSourceRef = useRef<EventSource | null>(null)

  useEffect(() => {
    return () => {
      eventSourceRef.current?.close()
      eventSourceRef.current = null
    }
  }, [])

  const connect = () => {
    if (eventSourceRef.current) {
      return
    }

    setStatus('Connecting...')

    const eventSource = new EventSource(import.meta.env.VITE_SSE_URL ?? '/events')
    eventSourceRef.current = eventSource

    eventSource.onopen = () => {
      setIsConnected(true)
      setStatus('Connected')
    }

    eventSource.addEventListener('tick', (event) => {
      const messageEvent = event as MessageEvent<string>

      try {
        const payload = JSON.parse(messageEvent.data) as TickEvent
        setEvents((previous) => [payload, ...previous].slice(0, maxItems))
      } catch {
        setStatus('Received malformed event payload')
      }
    })

    eventSource.onerror = () => {
      setStatus('Connection lost or closed by server')
      setIsConnected(false)
      eventSource.close()
      eventSourceRef.current = null
    }
  }

  const disconnect = () => {
    eventSourceRef.current?.close()
    eventSourceRef.current = null
    setIsConnected(false)
    setStatus('Disconnected')
  }

  const clear = () => setEvents([])

  return (
    <main className="app">
      <h1>Server Sent Events Demo</h1>
      <p className="subtitle">React + TypeScript client for the ServerSentEventsSample API</p>

      <div className="controls">
        <button onClick={connect} disabled={isConnected}>Connect</button>
        <button onClick={disconnect} disabled={!isConnected}>Disconnect</button>
        <button onClick={clear} disabled={events.length === 0}>Clear</button>
      </div>

      <p className="status">
        Status: <strong>{status}</strong>
      </p>

      <ul className="event-list">
        {events.length === 0 && <li className="empty">No events yet.</li>}
        {events.map((item) => (
          <li key={`${item.id}-${item.sentAtUtc}`}>
            <div className="row">
              <span className="id">#{item.id}</span>
              <span>{item.message}</span>
            </div>
            <time dateTime={item.sentAtUtc}>
              {new Date(item.sentAtUtc).toLocaleTimeString()}
            </time>
          </li>
        ))}
      </ul>
    </main>
  )
}

Source Code

Full code for these examples is available on Github.

Inlining methods in C#

Inlining is a simple process of, essentially replacing a method call with the contents of the call, it’s main benefits are around performance in that it removed the method call/stack frame overhead and is especially useful in tight looks or hot path scenarios.

An example in C++ we might have something like

class Math {
public:
    inline int square(int x);
};

inline int Math::square(int x) {
    return x * x;
}

Math m;
int y = m.square(5);

where the square method might be compiled down to

int y = 5 * 5;

This post isn’t about C++ inlining but there are similarities with C# in that a method may be implicitly inline, i.e. no need for the inline keyword and where we might request a method to be inlined – the compiler in this case may or may not fulfil the request.

In C# the same implicit inlining can be seen with code such as

public static int Square(int x) => x * x;

and again we can request or hint to the JIT to inline – this is the key thing to take away from this post, we can request inlining but essentially the compiler will make the decision on whether it should inline our code.

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Square(int x) => x * x;

What cannot be inlined?

  • Methods which are too large, i.e. a method body which exceeds JITA internal size threshold.
  • Virtual/interfaces calls cannot be inlined unless the JIT can de-virtualize them.
  • Generic methods with unresolved types.
  • try/catch/finally blocks are no eligible for inlining
  • async/iterator methods i.e. async/await and yield return both compile to a state machine which cannot be inlined.
  • Methods with lock keyword, this essentially compiles to Monitor.Enter/Monitor.Exit with try/finally and hence cannot be inlined.
  • Methods with stackalloc cannot be inlined.
  • Methods with unsafe and some other pointer operations cannot be inlined.
  • Might seem obvious but marking your methods as MethodImplOptions.NoInlining will not allow this method to be inlined.
  • P/Invoke and extern methods.
  • Inlining may be disabled for debug builds, and other compiler optimizations

Scheduled Azure Devops pipelines

I wanted to run some tasks once a day. The idea being we run application to check from any drift/changes to configuration etc. Luckily this is simple in Azure devops.

We create a YAML pipeline with no trigger and create a cronjob style schedule instead as below

trigger: none

schedules:
- cron: "0 7 * * *"
  displayName: Daily
  branches:
    include:
    - main
  always: true

pool:
  vmImage: 'ubuntu-latest'

steps:
- task: UseDotNet@2
  inputs:
    packageType: 'sdk'
    version: '10.x'

- script: dotnet build ./tools/TestDrift/TestDrift.csproj -c Release
  displayName: Test for drift

- script: |
    dotnet ./tools/TestDrift/bin/Release/net10.0/TestDrift.dll
  displayName: Run Test for drift

- task: PublishTestResults@1
  inputs:
    testResultsFormat: 'JUnit'
    testResultsFiles: ./tools/TestDrift/bin/Release/net10.0/drift-results.xml
    failTaskOnFailedTests: true

In this example we’re publishing test results. Azure devops supports several formats, see the testResultsFormat variable. We’re just creating an XML file named drift-results.xml with the following format


<testsuite tests="0" failures="0">
  <testcase name="check site" />
  <testcase name="check pipeline">
    <failure message="pipeline check failed" />
  </testcase>
</testsuite>

In C# we’d do something like

var suite = new XElement("testsuite");
var total = GetTotalTests();
var failures = 0;

var testCase = new XElement("testcase",
   new XAttribute("name", "check pipeline")
);

// run some test
var success = RunSomeTest();

if(!success)
{
  failures++;
  testCase.Add(new XElement("failure",
    new XAttribute("message", "Some test name")
  ));
}

suite.Add(testCase);

// completed
suite.SetAttributeValue("tests", total);
suite.SetAttributeValue("failures", failures);

var exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var outputPath = Path.Combine(exeDir, "tls-results.xml");

File.WriteAllText(outputPath, suite.ToString());

Using one of the valid formats, such as the JUnit format, will also result in Azure pipeline build showing a Test tab with our test results listed.

Sending email via the Azure Communication Service

I’m wanting to send emails from an Azure function upon a call from my React UI. Azure has the Communications Service and Email Communication Service for this functionality.

In the Azure Portla

  • Create a Communication service resource to your resource group
  • Create an Email Communication resource to your resource group

Add a free Azure subdomain

If you now go to the Email Communication Service | Overview, you can “Add a free Azure subdomain”. This is a really quick and simply way to get a domain up and running but has some limitations of quotas that are less than if you use a custom domain. This said, let’s click the “1-click add” and create an Azure subdomain.

When completed you’ll see the Settings | Provision domains, where it should show a domain name, domain type of Azure subdomain and all status should be verified.

Add a custom domain

Before try anything out let’s cover the custom domain. We’ll assume you have a domain on a non Azure DNS, for example GoDaddy. In the Azure Email Communication Service | Overview, click the “Setup” button.

  • Enter your domain
  • Re-enter to confirm
  • Click the confirm button
  • Click the Add button
  • We now need to verify the domain, so click Verify Domain and copy the TXT value
  • In my instance my DNS supplier offers a “Verify domain” option but you can just as easily add a TXT type with value @ then add the copied TXT value OR use the “Verify Doman Ownership” button if one exists
  • Once validation has completed go to the Email Communication Service | Settings | Provision domains and you’ll notice SPF, DKIM and DKIM2 are not verified, i.e. they’ll show “Configure”
  • Click on “Configure” in the SPF (any will do) this will show configuration for SPF, DKIM and DKIM2
  • For SPF, copy the SPF value, go to your DNS supplier and create a new DNS record of type TXT, a name of @ and paste your value into the value
  • For DKIM, copy the DKM record name, go to your DNS supplier and create a new DNS record of type CNAME, paste the record name into the name of your record and then copy the DKIM value from Azure into the CNAME value (if you have an options for Proxy, set to DNS only)
  • Finally, for DKIM2, copy the DKM2 record name, go to your DNS supplier and create a new DNS record of type CNAME, paste the record name into the name of your record and then copy the DKIM2 value from Azure into the CNAME value (if you have an options for Proxy, set to DNS only)
  • Go back to the Azure SPF configuration and click Next then Done

Verification can take some time, but when completed your custom domain should show Domain Status, SPF Status, DKIM status and DKIM2 status all Verified.

Connecting to the Communication Service

We’ve configured our domains, now we want to connect the domains to the “Communication Service” that you created earlier.

  • From the “Communication Service” go to Email | Domains
  • Click on Connect domains
  • Select your subscription, resource group, the your email service and finally the verified domain you wish to use – you can add multiple verified domains, so for example a custom domain and your Azure free subdomain.

Now all that’s left is to test the email, so

  • From the “Communication Service” go to Email | Try Email
  • Select the domain
  • Select your sender
  • Enter one or more recipients
  • I’ll leave the rest as default
  • If all fields are correct a “Send” button will appear, click it to send the email.

Whilst trying the email out you’ll have noticed the source code on the right – this gives you the code to place in your Azure function or other services.

Code

Here’s an example of the code generated via “Try Email”

using System;
using System.Collections.Generic;
using Azure;
using Azure.Communication.Email;

string connectionString = Environment.GetEnvironmentVariable("COMMUNICATION_SERVICES_CONNECTION_STRING");
var emailClient = new EmailClient(connectionString);


var emailMessage = new EmailMessage(
    senderAddress: "DoNotReply@<from_domain>",
    content: new EmailContent("Test Email")
    {
        PlainText = @"Hello world via email.",
        Html = @"
		<html>
			<body>
				<h1>
					Hello world via email.
				</h1>
			</body>
		</html>"
    },
    recipients: new EmailRecipients(new List<EmailAddress>
    {
        new EmailAddress("<to_email>")
    }));
    

EmailSendOperation emailSendOperation = emailClient.Send(
    WaitUntil.Completed,
    emailMessage);

ReturnType and Parameters in Typescript

Typescript has a couple of types which are useful for describing types when none are strictly specified.

Let’s assume we have this simple function, which takes a string parameter and returns

function getData(key: string) {
   return { key, firstName: "Scooby", lastName: "Doo" }
}

Using ReturnType creates a type that matches the getData return. i.e. { key: string, firstName: string, lastName: string } whereas the Parameters will be a tuple [key: string]

type T = ReturnType<typeof getData>;
type T = Parameters<typeof getData>;

Sentiment analysis using Python and TextBlob

Let’s create a really simple FastAPI with, create yourself our app file (app.py) and requirements file (requirements.txt).

We’re going to use TextBlob to process our text.

In the requirements.txt add the following

fastapi
uvicorn
textblob

In the app.py add the following imports

from fastapi import FastAPI
from textblob import TextBlob

Now let’s create the FastAPI and a POST endpoint named sentiment, the code should look like this

@app.post("/sentiment")
def analyze_sentiment(payload: dict):
    text = payload["text"]
    blob = TextBlob(text)
    polarity = blob.sentiment.polarity
    subjectivity = blob.sentiment.subjectivity
    return {
        "polarity": polarity,
        "subjectivity": subjectivity
    }

Don’t forget to run pip install

pip install -r requirements.txt

or if you’re using PyCharm, let this install the dependencies.

Run the app using

uvicorn app:app --reload

Note: as we’re using FastAPI we can access the OpenAPI interface using http://localhost:8000/docs

Now from curl run

curl -X POST http://localhost:8000/sentiment -H "Content-Type: application/json" -d '{"text": "I absolutely love this!"}'

and you’ve see a result along the following lines

{"polarity":0.625,"subjectivity":0.6}

Polarity is within the range [-1.0, 1.0], where -1.0 is a very negative sentiment, 0, neutral sentiment and 1.0 very positive sentiment. Subjectivity is in the range [0.0. 1.0] where 0.0 is very objective (i.e. facts or neutral statements) and 1.0 is very subjective (i.e. opinions, feelings or personal judgement).

Pick and Omit in Typescript

Pick and Omit are used to pick fields from a type to create a new type or, in the case of Omit, returns a type with supplied fields omitted.

For example, if we have a simple type such as this

type Person = {
  firstName: string
  lastName: string
  age: number
}

We might wish to create a new type based upon the Person type but with only the first name. We can use Pick to pick the fields like this

function pickSample(person: Pick<Person, "firstName">): string {
  return `Hello, ${person.firstName}!`
}

We can do the opposite using Omit, hence excluding fields

function omitSample(person: Omit<Person, "age">): string {
  return `Hello, ${person.firstName} ${person.lastName}`;
}

CSS functions

CSS functions extend CSS to give it a more “programming language” set of features, i.e. we can create functions, with parameters, even add type safety and return values.

Let’s start by looking at the basic syntax on a simple function which returns a given value depending on the “responsive design” size.

@function --responsive(--sm, --md, --lg: no-value) {
  result: var(--lg);

  @media(width <= 600px) {
    result: var(--sm);
  }
  @media(width > 600px) and (width <= 800px) {
    result: var(--md);
  }
  @media(width > 800px) {
    result: var(--lg);
  }
}

What’s happening here is the function is declared with the @function and the name (in this case responsive) is prefixed with — as are any parameters. Hence we have three parameters, the first is what’s return if the width is <= 600px and so on. The result: is not quite equivalent to a return as it does not shortcut and return a value, instead if you set the result: later then the last result is used as the “returned value”.

Here’s an example of us setting a 200px square with different colours upon the different break points

div {
  width: 200px;
  height: 200px;
  background: --responsive(
    blue,
    green,
    red);
}

We can also supply default values to a function, for example we could set a default “no-value” using

@function --responsive(--sm, --md, --lg: no-value)

Here’s an example of us setting other defaults with values

@function --responsive(--sm: blue, --md: green, --lg: red)

Interestingly we can also make things type safe, for example let’s set each parameter as being a color and the return value also being of type color

@function --responsive(--sm <color>: blue, --md <color>: green, --lg <color>: red) returns <color>

We can also supply multiple types, so let’s assume we want a –opacity function where the amount can be a percentage or number, then we might write something like

@function --opacity(--color, --opacity type(<number> | <percentage>): 0.5) returns <color> {
  result: rgb(from var(--color) r g b / var(--opacity));
}

and in usage

div {
  width: 200px;
  height: 200px;
  background: --opacity(blue, 80%);
  /* background: --opacity(blue, 0.3); */
}

Auto-discovery using Vite (and React)

I’m messing around with a Shell application in React and wanting to load information from apps/components that are added to the shell dynamically, i.e. via auto-discovery.

Now, we’re using React for this example so we can create React apps using Vite for what we will call, our components, as these can be loaded into React as components. The reason to create these as standalone apps would be for use to develop against.

So let’s assume I have a main application, the Shell app and this can get routes for functionality (our apps/components) which may get added later on in the development process, basically a pluggable type of architecture which just uses React components along with those components supplying routing information to allow us to plug them into the Shell.

To give it more context, I build a Shell application with Search and later on want to add a Document UI, so it’d be nice if the Document can be worked on separately and when ready, deployed to a specific locations where the Shell (upon refresh) can discover the new component and wire into the Shell.

Vite has a feature import.meta.glob which we can use to discover our routes.tsx that are deployed to a specific path, i.e.

const modules = import.meta.glob<Record<string, unknown>>(
  '../../apps/*/src/routes.tsx', { eager: true }
);

This will return keys for the file paths and values being the functions that import from the files or modules. For example it might locate components such as

apps/search/src/routes.tsx
apps/document/src/routes.tsx
apps/export/src/routes.tsx
apps/settings/src/routes.tsx

If eager is missing (or false) then Vite lazy imports the functions and you’d then need to call them yourself, with eager:true, Vite imports the modules immediately.

If we use something like this


import type { RouteObject } from 'react-router-dom';

const modules = import.meta.glob<Record<string, unknown>>(
  '../../apps/*/src/routes.tsx', { eager: true }
);

export const allAppRoutes: RouteObject[] = Object.values(modules)
  .flatMap((mod) => {
    const arr = Object.values(mod).find(
      (v): v is RouteObject[] => Array.isArray(v) && 
        v.every(item => typeof item === 'object' && 
           item !== null && 'path' in item && 'element' in item)
    );
    return arr || [];
  });

Then we could use the routes via the react-router-dom in App.tsx, like this

const baseRoutes = [
  { path: '/', element: <div>Welcome to the Shell</div> },
];

const routes = [
  ...baseRoutes,
  ...allAppRoutes,
];
return (
  <AuthContext.Provider value={auth}>
    <ShellLayout>
      <Suspense fallback={<div>Loading…</div>}>
        <Routes>
          {routes.map(({ path, element }) => (
            <Route key={path} path={path} element={element} />
          ))}
        </Routes>
      </Suspense>
    </ShellLayout>
  </AuthContext.Provider>
);

and finally here’s an example routes.tsx file for our components

import SearchApp from './App';

export const searchRoutes = [
  { path: '/search/*', element: <SearchApp /> },
];