Implementing a Sidecar Pattern Without Kubernetes

November 04, 2021

A sidecar pattern essentially allows a running process to communicate with a separate process (or sidecar) in order to offload some non-essential functionality. This is typically logging, or something similarly auxiliary to the main function. You tend to find sidecars in Kubernetes clusters (in fact, you can have more than one container in a pod for exactly this reason); however, the pattern is just that: a pattern; and can therefore apply to pretty much anything.

In this post, I’m going to cover how you might set-up two independent containers, and then implement the sidecar pattern using docker compose.

Console App

The code for this is going to be in .Net Core 6, so let’s create a console app:

sidecar

It will be easier later on if you follow the following directory structure for the projects:



sidecar-test
	console-app
	web-api

For the purpose of the console app, just put it in a parent directory - so the code lives in /sidecar-test/console-app (we’ll come back to the API).

We’ll need to add docker support for this, which you can do in Visual Studio (right click the project and add docker support):

sidecar

Once you’ve created the docker build file, you may need to edit it slightly; here’s mine:



FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src

COPY ["sidecar-test.csproj", "sidecar-test/"]
WORKDIR "/src/sidecar-test"
RUN dotnet restore "sidecar-test.csproj"
COPY . .

RUN dotnet build "sidecar-test.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "sidecar-test.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "sidecar-test.dll"]

If you find that the dockerfile doesn’t work, then you might find the following posts helpful: Beginner’s Guide to Docker – Part 2 – Debugging a Docker Build Beginner’s Guide to Docker – Part 3 – Debugging a Docker Build (Continued)

If you’d rather not wade through that, then the Reader’s Digest version is:



docker build -t sidecar-test .

Once the docker files are building, experiment with running them:



docker run --restart=always -d sidecar-test

Assuming this works, you should see that the output shows Hello World (as per the standard console app).

Web API

For the web API, again, we’re going to go with the default Web API - just create a new project in VS and select ASP.Net Core Web API:

sidecar

Out of the box, this gives you a Weather Forecaster API, so we’ll stick with that for now, as we’re interested in the pattern itself and not the specifics.

Again, add docker support; here’s mine:



FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src

COPY ["sidecar-test-api.csproj", "sidecar-test-api/"]
WORKDIR "/src/sidecar-test-api"
RUN dotnet restore "sidecar-test-api.csproj"
COPY . .

RUN dotnet build "sidecar-test-api.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "sidecar-test-api.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "sidecar-test-api.dll"]


You should now be able to build that in a similar fashion as before:



docker build -t sidecar-test-api .

You can now run this, although unlike before, you’ll need to map the ports. The port mapping works by mapping the external port to the internal port - so to connect to the mapping below, you would connect to http://localhost:5001, but that will connect to port 80 inside the container.



docker run --restart=always -d -p 5001:80 sidecar-test

Now that you can connect to the Web API, let’s now implement the sidecar pattern.

Docker Compose

We now have two independent containers; however, they cannot, and do not, communicate. Let’s remind ourselves of the directory structure that we decided on at the start of the post:



sidecar-test
	console-app
	web-api

We now want to open the parent sidecar-test directory. In there, create a file called docker-compose.yml. It should look like this:



version: '3'
services:
  sidecar-api:
    build: .\\sidecar-test-sidecar\\web-api
    ports: 
      - "5001:80"
  sidecar-program:
    build: .\\sidecar-test\\console-app
    depends\_on: 
      - "sidecar-api"


There’s lots to unpick here. Let’s start with services - this contains all the individual services that we’re going to run in this file; each one has a build section, which tells compose to call docker build on the path specified. You can create the image separately and replace this with an image section. The ports section maps directly to the docker -p switch.

There’s two other important parts of this, this first is the service name (sidecar-api, sidecar-program) - we’ll come back to that shortly. The second is the depends_on - which tells docker compose to ensure that the dependant is built and running first (we could switch the order of these and it would still start the API first).

Running The Sidecar

Finally, we can run our sidecar. You can do this by navigating to the parent sidecar-test directory, and typing:



docker compose up

If you’re changing and running the code, and using PowerShell, you can use the following command:



docker-compose down; docker-compose build --no-cache; docker-compose up

Which will force a rebuild every time.

However, if you run this now, you’ll notice that, whilst it runs, it does nothing.

Linking The Two

Docker Compose has a nifty little feature - all the services in the compose file are networked together, and can be referenced by using their service name.

In the console app, change the main method to:



        public static async Task Main(string[] args)
        {
            Console.WriteLine("Get Weather");
            var httpClient = new HttpClient();
            httpClient.BaseAddress = new Uri("http://sidecar-api");
            var result = await httpClient.GetAsync("WeatherForecast");
            result.EnsureSuccessStatusCode();
            var content = await result.Content.ReadAsStringAsync();
            Console.WriteLine(content);
        }


We won’t focus on the fact that I’m directly instantiating HttpClient here; but notice how we are referencing the API, by http://sidecar-api, and because they are both part of the same compose file, this just works.

If we now recompile the docker files:



docker-compose down; docker-compose build --no-cache; docker-compose up

We should have a running sidecar - admittedly it doesn’t behave like a sidecar, but that’s just why it gets called, not the architecture.

References

https://docs.docker.com/compose/



Profile picture

A blog about one man's journey through code… and some pictures of the Peak District
Twitter

© Paul Michaels 2024