Communicating between services/applications in Kubernetes

If you have, say a service and client in a single Pod, then you’re supplied a virtual ip address and the services etc. within the Pod are accessible via localhost, but you’re more likely to want to deploy services in their own Pods (unless they are tightly coupled) to allow scaling per service etc.

How do we communicate with a services in a different Pod to our application?

A common scenario here is, we have a a client application, for example the razordemo application in the post Deploying an ASP.NET core application into a Docker image within Kubernetes and it might be using the WeatherForecast service that is created using

dotnet new webapi -o weatherservice --no-https -f net5.0

We’re not going to go into the code of the actual services apart from show the pieces that matter for communication, so let’s assumed we’ve deployed weatherservice to k8s using a configuration such as

apiVersion: apps/v1
kind: Deployment
metadata:
  name: weatherservice
  namespace: default
spec:
  selector:
    matchLabels:
      run: weatherservice
  replicas: 1
  template:
    metadata:
      labels:
        run: weatherservice
    spec:
      containers:
      - name: weatherservice
        image: localhost:32000/weatherservice
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: weatherservice
  namespace: default
  labels:
    run: weatherservice
spec:
  ports:
    - port: 80
      protocol: TCP
  selector:
    run: weatherservice

This service will exist in it’s own virtual network running on port 80, we may scale this service according to our needs and without affecting other services or clients.

If we then deploy our razordemo as per Deploying an ASP.NET core application into a Docker image within Kubernetes – it will also exist in it’s own virtual network, also running on port 80.

To communicate from razordemo to the weatherservice we simply use the service name (if we’re on the same cluster) for example http://weatherservice.

Here’s a simple example of razordemo code for getting weatherservice data…

var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("http://weatherservice");

var response = await httpClient.GetAsync("/weatherforecast");
var results = JsonConvert.DeserializeObject<WeatherForecast[]>(await response.Content.ReadAsStringAsync());

The first two lines will probably be set in your ASP.NET core Startup.cs file, although better still is for us to store the URL within configuration via an environment variable within k8s, so the client is agnostic to the service name that we deployed the weatherservice with.

Deploying an ASP.NET core application into a Docker image within Kubernetes

In the previous post we looked and an “off the shelf” image of nginx, which we deployed to Kubernetes and were able to access externally using Ingress. This post follows on from that one, so do refer back to it if you have any issues with the following configurations etc.

Let’s look at the steps for deploying our own Docker image to k8s and better still let’s deploy a dotnet core webapp.

Note: Kubernetes is deprecating its support for Docker, however this does not mean we cannot deployed docker images, just that we need to use the docker shim or generated container-d (or other container) images.

The App

We’ll create a standard dotnet ASP.NET core Razor application which you can obviously do what you wish to, but we’ll take the default implementation and turn this into a docker image and then deploy it to k8s.

So to start with…

  • Create a .NET core Razor application (mine’s named razordemo), you can do this from Visual Studio or using
    dotnet new webapp -o razordemo --no-https -f net5.0
    
  • If you’re running this on a remote machine don’t forget to change launchSettings.json localhost to 0.0.0.0 if you need to.
  • Run dotnet build

It’d be good to see this is all working, so if let’s run the demo using

dotnet run

Now use your browser to access http://your-server-ip:5000/ and you should see the Razor demo home page, or use curl to see if you get valid HTML returned, i.e.

curl http://your-server-ip:5000

Generating the Docker image

Note: If you changed launchSettings.json to use 0.0.0.0, reset this to localhost.

Here’s the Dockerfile for building an image, it’s basically going to publish a release build of our Razor application then set up the image to run the razordemo.dll via dotnet from a Docker instance.

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY razordemo.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "razordemo.dll"]

Now run docker build using the following

docker build . -t razordemo --no-cache

If you want to check the image works as expect then run the following

docker run -d -p 5000:80 razordemo 

Now check the image is running okay by using curl as we did previously. If all worked you should see the Razor demo home page again, but now we’re seeing if within the docker instance.

Docker in the local registry

Next up, we want to deploy this docker image to k8s.

k8s will try to get an image from a remote registry and we don’t want to deploy this image outside of our network, so we need to rebuild the image, slightly differently using

docker build . -t localhost:32000/razordemo --no-cache

Reference: see Using the built-in registry for more information on the built-in registry.

Before going any further, I’m using Ubuntu and micro8s, so will need to enable the local registry using

microk8s enable registry

I can’t recall if this is required, but I also enabled k8s DNS using

microk8s.enable dns

Find the image ID for our generated image using

docker images

Now use the following commands, where {the image id} was the one found from the above command

docker tag {the image id} localhost:32000/razordemo
docker push localhost:32000/razordemo

The configuration

This is a configuration based upon my previous post (the file is named razordemo.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
spec:
  selector:
    matchLabels:
      run: webapp
  replicas: 1
  template:
    metadata:
      labels:
        run: webapp
    spec:
      containers:
      - name: webapp
        image: localhost:32000/razordemo
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: webapp
  labels:
    run: webapp
spec:
  ports:
    - port: 80
      protocol: TCP
  selector:
    run: webapp
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: razor-ingress
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: webapp
            port: 
              number: 80

Now apply this configuration to k8s using (don’t forget to change the file name to whatever you named your file)

kubectl apply -f ./razordemo.yaml

Now we should be able to check the deployed image, by either using the k8s dashboard or run

kubectl get ep webapp

Note the endpoint and curl to that endpoint, if all worked well you should be able to see the ASP.NET generate home page HTML and better still access http://your-server-ip from another machine and see the webpage.

Deploying and exposing an nginx server in kubernetes

In this post we’re going to create the configuration for a deployment, service and ingress controller to access an nginx instance to our network. I’ve picked nginx simply to demonstrate the process of these steps and obviously, out of the box, we get a webpage to view to know whether everything work – feel free to can change the image used to something else for your own uses.

Deployment

Below is the deployment configuration for k8s, creating a deployment named my-nginx. This will generate 2 replicas and expose the image on port 80 of the container.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80

Service

Now we’ll create the configuration for the my-nginx service

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx

Ingress

We’re going to use Ingress to expose our service to the network, alternatively we could use a load balancer, but this options requires an external load balancer, so more like used on the cloud, or we could use NodePort which allows us to assigna port to a service so that any request to the port is forwarded to the node – the main problem with this is the port may change, instead Ingress acts like a load balancer within our cluster and will allow us to configure things to route port 80 calls through to our my-nginx service as if it was running outside of k8s.

We’re going to need to enable ingress within k8s. As we’re using Ubuntu and microk8s, run the following

microk8s enable ingress

The following is the configuration for this ingress.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: http-ingress
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-nginx
            port: 
              number: 80

Running and testing

Now we need to apply the configuration to our kubernetes instance, so I’ve saved all the previously defined configuration sections into a single file named my-nginx.yaml. Now just run the following to run the configuration in k8s

kubectl apply -f my-nginx.yaml

If you like, you can check the endpoint(s) assigned within k8s using

kubectl get ep my-nginx

and then curl the one or more endpoints (in our example 2 endpoints). Or we can jump straight to the interesting bit and access your k8s host’s ip and if all worked, you’ll be directed to one of the replicates nginx instances and we should see the “Welcome to nginx!” page.

Installing Kubernetes on Ubuntu

In this post I’m just going to list the steps for installing Kubernetes (k8s) on Ubuntu server using the single node, lightweight MicroK8s.

  • If you don’t have it installed, install snap

    sudo apt install snapd
    

Now let’s go through the steps to get microk8s installed

  • Install microk8s

    sudo snap install microk8s --classic
    
  • You may wish to change permissions to save using sudo everytime, if so run the following and change {username} to your user name.

    sudo usermod -a -G microk8s {username}
    sudo chown -f -R {username} ~/.kube
    
  • Verify the installation
    sudo microk8s.status
    
  • To save us typing microk8s to run k8s command let’s alias it
    sudo snap alias microk8s.kubectl kubectl
    

Accessing the Dashboard

Whilst you can now run kubectl command, the Web based dashboard is really useful – so lets deploy it using

kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.1.0/aio/deploy/recommended.yaml

By default the Dashboard can only be accessed from the local machine and from a security standpoint you may even prefer not to be running a dashboard, but if you decide you want it (for example for dev work) then…

As per Accessing Dashboard

kubectl -n kubernetes-dashboard edit service kubernetes-dashboard

Change type: ClusterIP to type: NodePort and save this file.

You’ll need to run the proxy

kubectl proxy&

Now to get the port for the dashboard, run the following

kubectl -n kubernetes-dashboard get service kubernetes-dashboard

Example output (take from Accessing Dashboard)

NAME                   TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
kubernetes-dashboard   NodePort   10.100.124.90   <nodes>       443:31707/TCP   21h

You can now access the dashboard remotely now on the port 31707 (see the PORT(S) listed above), for example https://{master-ip}:31707 where {master-ip} is the server’s ip address.

To get the token for the dashboard we need to set up and few things

kubectl describe secret -n kube-system | grep deployment -A 12

Then copy the whole token to the Dashboard Token edit box and login.

To enable skipping of the requirement for a token etc. on the dashboard (should only be used on a development installation) run

kubectl edit deployment kubernetes-dashboard -n kubernetes-dashboard

then add the following line

- args:
   - --auto-generate-certificates
   - --namespace=kubernetes-dashboard
   - --enable-skip-login  # this argument allows us to skip login

Interacting with Eureka via it’s REST operations

In previous posts we’ve used Steeltoe to interact with Eureka. Obviously Steeltoe gives us good patterns for interacting with Eureka but also has the capability to work with other service discovery registries etc.

Steeltoe’s great, but what if we wanted to just write our own Eureka either for learning how things work or maybe we just want to write our own for .NET or for another language/technology?

Eureka exposes REST operations for everything you’ll need to do with a service registry, let’s take a look.

Note: I’ll give examples using localhost, obviously change to your Eureka host/ip and I’ll also show data based upon the weatherapi we’ve worked on previously.

Query for all applications and their instances

  • GET method:
    http://localhost:8761/eureka/apps
    
  • Response:
    <applications>
      <versions__delta>1</versions__delta>
      <apps__hashcode>UP_1_</apps__hashcode>
      <application>
        <name>WEATHERAPI</name>
        <instance>
          <instanceId>localhost:weatherapi:5001</instanceId>
          <hostName>localhost</hostName>
          <app>WEATHERAPI</app>
          <ipAddr>localhost</ipAddr>
          <status>UP</status>
          <overriddenstatus>UNKNOWN</overriddenstatus>
          <port enabled="false">8080</port>
          <securePort enabled="true">5001</securePort>
          <countryId>1</countryId>
          <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
            <name>MyOwn</name>
          </dataCenterInfo>
          <leaseInfo>
            <renewalIntervalInSecs>30</renewalIntervalInSecs>
            <durationInSecs>90</durationInSecs>
            <registrationTimestamp>1611512017058</registrationTimestamp>
            <lastRenewalTimestamp>1611515916921</lastRenewalTimestamp>
            <evictionTimestamp>0</evictionTimestamp>
            <serviceUpTimestamp>1611512016403</serviceUpTimestamp>
          </leaseInfo>
          <metadata class="java.util.Collections$EmptyMap"/>
          <homePageUrl>https://localhost:5001/</homePageUrl>
          <statusPageUrl>https://localhost:5001/actuator/info</statusPageUrl>
          <healthCheckUrl>https://localhost:5001/actuator/health</healthCheckUrl>
          <vipAddress>weatherapi</vipAddress>
          <secureVipAddress>weatherapi</secureVipAddress>
          <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
          <lastUpdatedTimestamp>1611512017058</lastUpdatedTimestamp>
          <lastDirtyTimestamp>1611512016217</lastDirtyTimestamp>
          <actionType>ADDED</actionType>
        </instance>
      </application>
    </applications>
    

Query for instances for a specific application id

  • GET method:
    http://localhost:8761/eureka/apps/{your app id}
    
    // Example
    http://localhost:8761/eureka/apps/weatherapi
    
  • Response:
    <application>
        <name>WEATHERAPI</name>
        <instance>
            <instanceId>localhost:weatherapi:5001</instanceId>
            <hostName>localhost</hostName>
            <app>WEATHERAPI</app>
            <ipAddr>localhost</ipAddr>
            <status>UP</status>
            <overriddenstatus>UNKNOWN</overriddenstatus>
            <port enabled="false">8080</port>
            <securePort enabled="true">5001</securePort>
            <countryId>1</countryId>
            <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
                <name>MyOwn</name>
            </dataCenterInfo>
            <leaseInfo>
                <renewalIntervalInSecs>30</renewalIntervalInSecs>
                <durationInSecs>90</durationInSecs>
                <registrationTimestamp>1611512017058</registrationTimestamp>
                <lastRenewalTimestamp>1611516936969</lastRenewalTimestamp>
                <evictionTimestamp>0</evictionTimestamp>
                <serviceUpTimestamp>1611512016403</serviceUpTimestamp>
            </leaseInfo>
            <metadata class="java.util.Collections$EmptyMap"/>
            <homePageUrl>https://localhost:5001/</homePageUrl>
            <statusPageUrl>https://localhost:5001/actuator/info</statusPageUrl>
            <healthCheckUrl>https://localhost:5001/actuator/health</healthCheckUrl>
            <vipAddress>weatherapi</vipAddress>
            <secureVipAddress>weatherapi</secureVipAddress>
            <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
            <lastUpdatedTimestamp>1611512017058</lastUpdatedTimestamp>
            <lastDirtyTimestamp>1611512016217</lastDirtyTimestamp>
            <actionType>ADDED</actionType>
        </instance>
    </application>
    

Query for instances by its instance id

  • GET method:
    http://localhost:8761/eureka/apps/{your app id}/{your instance id}
    
    // Example
    http://localhost:8761/eureka/apps/weatherapi/localhost:weatherapi:5001
    
  • Response:
    <instance>
        <instanceId>localhost:weatherapi:5001</instanceId>
        <hostName>localhost</hostName>
        <app>WEATHERAPI</app>
        <ipAddr>localhost</ipAddr>
        <status>UP</status>
        <overriddenstatus>UNKNOWN</overriddenstatus>
        <port enabled="false">8080</port>
        <securePort enabled="true">5001</securePort>
        <countryId>1</countryId>
        <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
            <name>MyOwn</name>
        </dataCenterInfo>
        <leaseInfo>
            <renewalIntervalInSecs>30</renewalIntervalInSecs>
            <durationInSecs>90</durationInSecs>
            <registrationTimestamp>1611512017058</registrationTimestamp>
            <lastRenewalTimestamp>1611517116997</lastRenewalTimestamp>
            <evictionTimestamp>0</evictionTimestamp>
            <serviceUpTimestamp>1611512016403</serviceUpTimestamp>
        </leaseInfo>
        <metadata class="java.util.Collections$EmptyMap"/>
        <homePageUrl>https://localhost:5001/</homePageUrl>
        <statusPageUrl>https://localhost:5001/actuator/info</statusPageUrl>
        <healthCheckUrl>https://localhost:5001/actuator/health</healthCheckUrl>
        <vipAddress>weatherapi</vipAddress>
        <secureVipAddress>weatherapi</secureVipAddress>
        <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
        <lastUpdatedTimestamp>1611512017058</lastUpdatedTimestamp>
        <lastDirtyTimestamp>1611512016217</lastDirtyTimestamp>
        <actionType>ADDED</actionType>
    </instance>
    

Register an application instance

  • POST method:
    Headers: “Content-Type”: “application/json”, “Accept”: “application/json”

    http://localhost:8761/eureka/apps/{your app id}
    
    // Example
    http://localhost:8761/eureka/apps/myapp
    
  • Body:

    {
      "instance": {
        "hostName": "myhost",
        "app": "myapp",
        "vipAddress": "myservice",
        "secureVipAddress": "myservice",
        "ipAddr": "10.0.0.10",
        "status": "STARTING",
        "port": {"$": "8080", "@enabled": "true"},
        "securePort": {"$": "8443", "@enabled": "true"},
        "healthCheckUrl": "http://myservice:8080/healthcheck",
        "statusPageUrl": "http://myservice:8080/status",
        "homePageUrl": "http://myservice:8080",
        "dataCenterInfo": {
          "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", 
          "name": "MyOwn"
        }
      }
    }
    
  • Response: 204 No Content

As you will see from the status we’ve set this service to STARTING and hence we need set the status to UP when our service is running. Eureka, ofcourse, is just a registry and hence you service need to updated information as required.

Delete an instance

  • DELETE method:
    http://localhost:8761/eureka/apps/{your app id}/{your instance id}
    
    // Example
    http://localhost:8761/eureka/apps/myapp/myhost
    
  • Response: 200 OK

Send an application instance heartbeat

  • PUT method:
    http://localhost:8761/eureka/apps/{your app id}/{your instance id}
    
    // Example
    http://localhost:8761/eureka/apps/myapp/myhost
    
  • Response: 200 OK, or 404 if the instance doesn’t exist

Take an instance out of service

  • PUT method:
    http://localhost:8761/eureka/apps/{your app id}/{your instance id}/status?value=OUT_OF_SERVICE
    
    // Example
    http://localhost:8761/eureka/apps/myapp/myhost/status?value=OUT_OF_SERVICE
    
  • Response: 200 OK, or 500 on failure

Now in the Eureka dashboard you’ll see the large, red OUT_OF_SERVICE text.

Remove the out of service state

  • DELETE method:
    http://localhost:8761/eureka/apps/{your app id}/{your instance id}/status?value=UP
    
    // Example
    http://localhost:8761/eureka/apps/myapp/myhost/status?value=UP
    
  • Response: 200 OK, or 500 on failure

The status UP is optional, it’s a suggestion for the status after the removal of the OUT OF SERVICE override.

Update meta data

  • PUT method:
    http://localhost:8761/eureka/apps/{your app id}/{your instance id}/metadata?key=val
    
    // Example
    http://localhost:8761/eureka/apps/myapp/myhost/metadata?Version=2
    
  • Response: 200 OK, or 500 on failure

Query for all instance of a vip address

  • GET method:
    http://localhost:8761/eureka/vips/{your vipAddress}
    
    // Example
    http://localhost:8761/eureka/vips/myservice
    
  • Response: 200 OK, or 404 if vipAddress does not exist
    <applications>
        <versions__delta>-1</versions__delta>
        <apps__hashcode>UP_1_</apps__hashcode>
        <application>
            <name>MYAPP</name>
            <instance>
                <hostName>myhost</hostName>
                <app>MYAPP</app>
                <ipAddr>10.0.0.10</ipAddr>
                <status>UP</status>
                <overriddenstatus>UNKNOWN</overriddenstatus>
                <port enabled="true">8080</port>
                <securePort enabled="true">8443</securePort>
                <countryId>1</countryId>
                <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
                    <name>MyOwn</name>
                </dataCenterInfo>
                <leaseInfo>
                    <renewalIntervalInSecs>30</renewalIntervalInSecs>
                    <durationInSecs>90</durationInSecs>
                    <registrationTimestamp>1611519061947</registrationTimestamp>
                    <lastRenewalTimestamp>1611519061947</lastRenewalTimestamp>
                    <evictionTimestamp>0</evictionTimestamp>
                    <serviceUpTimestamp>1611518524498</serviceUpTimestamp>
                </leaseInfo>
                <metadata>
                    <Version>2</Version>
                </metadata>
                <homePageUrl>http://myservice:8080</homePageUrl>
                <statusPageUrl>http://myservice:8080/status</statusPageUrl>
                <healthCheckUrl>http://myservice:8080/healthcheck</healthCheckUrl>
                <vipAddress>myservice</vipAddress>
                <secureVipAddress>myservice</secureVipAddress>
                <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
                <lastUpdatedTimestamp>1611519061947</lastUpdatedTimestamp>
                <lastDirtyTimestamp>1611518524498</lastDirtyTimestamp>
                <actionType>ADDED</actionType>
            </instance>
        </application>
    </applications>
    

Query for all instance of a secure vip address

  • GET method:
    http://localhost:8761/eureka/svips/{your svipAddress}
    
    // Example
    http://localhost:8761/eureka/svips/myservice
    
  • Response: 200 OK, or 404 is svipAddress does not exist
    <applications>
        <versions__delta>-1</versions__delta>
        <apps__hashcode>UP_1_</apps__hashcode>
        <application>
            <name>MYAPP</name>
            <instance>
                <hostName>myhost</hostName>
                <app>MYAPP</app>
                <ipAddr>10.0.0.10</ipAddr>
                <status>UP</status>
                <overriddenstatus>UNKNOWN</overriddenstatus>
                <port enabled="true">8080</port>
                <securePort enabled="true">8443</securePort>
                <countryId>1</countryId>
                <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
                    <name>MyOwn</name>
                </dataCenterInfo>
                <leaseInfo>
                    <renewalIntervalInSecs>30</renewalIntervalInSecs>
                    <durationInSecs>90</durationInSecs>
                    <registrationTimestamp>1611519061947</registrationTimestamp>
                    <lastRenewalTimestamp>1611519061947</lastRenewalTimestamp>
                    <evictionTimestamp>0</evictionTimestamp>
                    <serviceUpTimestamp>1611518524498</serviceUpTimestamp>
                </leaseInfo>
                <metadata>
                    <Version>2</Version>
                </metadata>
                <homePageUrl>http://myservice:8080</homePageUrl>
                <statusPageUrl>http://myservice:8080/status</statusPageUrl>
                <healthCheckUrl>http://myservice:8080/healthcheck</healthCheckUrl>
                <vipAddress>myservice</vipAddress>
                <secureVipAddress>myservice</secureVipAddress>
                <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
                <lastUpdatedTimestamp>1611519061947</lastUpdatedTimestamp>
                <lastDirtyTimestamp>1611518524498</lastDirtyTimestamp>
                <actionType>ADDED</actionType>
            </instance>
        </application>
    </applications>
    

Eureka Discovery with Steeltoe outside of ASP.NET

Whilst the previous post showed how we use Steeltoe in an ASP.NET application, sometimes we might not want to (or be able) to use all the nice methods for automatic service discovery etc.

In this post we’re going to create a simple .NET core console application and interact with the discovery service at a slightly lower level.

So…

  • Create yourself a .NET core console application
  • Add the NuGet package Steeltoe.Discovery.Eureka

We need to configure the discovery client. You can use your own configuration method but for this example we’ll hard code parameters into a EurekaClientConfig object.

var config = new EurekaClientConfig
{
   EurekaServerServiceUrls = "http://localhost:8761/eureka/",
   ShouldFetchRegistry = true,
   ShouldRegisterWithEureka = false
};

We’re just writing a client, hence we don’t want to register the application with Eureka, but we do want to fetch registry data.

Next we will create the discovery client, access the applications and get our weatherapi application…

var discoveryClient = new DiscoveryClient(config);
var applications = discoveryClient.Applications;
var service = applications?.GetRegisteredApplication("weatherapi");

Finally we want to find instances of the services associated with the application which are UP and get the URL associated with one of the instances.

var instance = service?.Instances.FirstOrDefault(info => info.Status == InstanceStatus.UP);
if(instance != null)                    
{
   var client = new HttpClient
   {
      BaseAddress = new Uri(instance.HomePageUrl)
   };
   var response = await client.GetAsync("weatherforecast");
   Console.WriteLine(await response.Content.ReadAsStringAsync());
}

If an instance is found we just simply use HttpClient to invoke a GET method on the service and output the response to the console, just to prove everything worked.

This code doesn’t use any load balancing strategies, I’ll leave that to the reader to look into as in a real world scenario we wouldn’t want all clients to use the first instance only.

Creating an ASP.NET client using Eureka and Steeltoe

In the previous post Eureka Server (using Steeltoe) revisited I went through the process of running a Eureka instance and creating a default template based ASP.NET Web API which registers itself with the Eureka server as the application weatherapi.

Let’s now create a basic ASP.NET MVC project to interact with the Eureka server, get an instance of the API service and use it.

Note: This post is almost 100% based on the Channel 9 video Service Discovery with Steeltoe, so credit should go to Tim Hess for the sample code.

  • Create an ASP.NET Core application and then select Web Application (Model View Controller)
  • Add the following NuGet packages, Steeltoe.Discovery.ClientCore, Steeltoe.Discovery.Eureka and System.Net.Http.Json
  • Update the appsettings.json with the following
      "eureka": {
        "client": {
          "serviceUrl": "http://locahost:8761/eureka/",
          "shouldFetchRegistry": "true",
          "shouldRegisterWithEureka": false,
          "validateCertificates": false
        }
      }
    

    Notice we set shouldFetchRegistry to true as we want to get the latest registry information, and we set shouldRegisterWidthEureka to false as, in this case, we don’t want to register this client. Ofcourse change this is your client also exposes services.

  • Within Startup.cs, ConfigureServices add the following
    services.AddDiscoveryClient(Configuration);
    services.AddHttpClient("weather", 
       client => client.BaseAddress = new Uri("http://weatherapi/"))
          .AddServiceDiscovery();
    

    The interesting thing here is that we associate the name “weather” with an address which looks like a URL but really is the name of the service within Eureka that we want to access. Then, by using the AddServiceDiscovery this will be converted to an instance URL representing the instance of the service associated with the app name.

    Note: we can also use load balancing strategies, such as round robin or random.

  • Finally within the Startup.cs, method Configure, add the following
    app.UseDiscoveryClient();
    
  • We’re going to simply copy the WeatherForecast class from the service and add to the client, here it is

    public class WeatherForecast
    {
       public DateTime Date { get; set; }
       public int TemperatureC { get; set; }
       public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
       public string Summary { get; set; }
    }
    
  • Within HomeController.cs we need to add a variable of type IHttpClientFactory which will be injected into the constructor and then uses to call defined HTTP client, this will then used service discovery to return the URL of an instance (as already discussed) and then we’re use that to call the weather API to get a list of values. Here’s the changes required to HomeController.cs
    private readonly IHttpClientFactory _httpClientFactory;
    
    public HomeController(IHttpClientFactory httpClientFactory, ILogger<HomeController> logger)
    {
       _httpClientFactory = httpClientFactory;
       _logger = logger;
    }
    
    public async Task<IActionResult> Index()
    {
       var client = _httpClientFactory.CreateClient("weather");
       var weather = await client.GetFromJsonAsync<IList<WeatherForecast>>("weatherforecast");
       return View(weather);
    }
    
  • Finally, let’s change the Index.cshtml to display the weather forecast data returned in the Index method. Firstly we declare the @model and then simply create a table to output the items from that model in columns and rows, so here’s the change to be added to the top of the file
    @model IList<EurekaWebClient.Controllers.WeatherForecast>
    

    and within the main div, just add the following

    <table class="table">
       <tr><th>Day of Week</th><th>Summary</th><th>Temp</th></tr>
       @foreach (var weather in Model)
       {
          <tr><td>@weather.Date.DayOfWeek</td><td>@weather.Summary</td><td>@weather.TemperatureF</td></tr>
       }
    </table>
    

That should be it. Ensure Eureka is running, your service is up and you should now see the weather service data in a nice little table.

Eureka Server (using Steeltoe) revisited

The Eureka server is used as a registry for maintaining lists of services and their endpoints. It’s used a lot in the microservice world by way of a microservice registering it’s existence somewhere (in this case in Eureka). When a client (whether it’s another service or anything else for that matter) want to access a service it asks the registry for an instance, in this case we connect to the Eureka server and find the services instance(s) that are available for a specific application name and are ofcourse UP.

Installing and running Eureka Server

In a previous post Spring boot Eureka server we wrote a Java application to run a Eureka server. In this post we’re going to use Docker to host the server and then use Steeltoe with .NET to interact with the instance.

To get an image of Eureka server, let’s use the Steeltoe docker image (Spring and others exist, the Steeltoe image is not intended for production, but is fine for what we want to do)

docker pull steeltoeoss/eureka-server

Now run up the docker image using

docker run --publish 8761:8761 steeltoeoss/eureka-server

If all goes well, connect to the Spring Eureka dashboard using

http://locahost:8761

change localhost to the ip/host you’re running the Eureka server from, now you should now see the Spring Eureka web page.

Registering a .NET core client with Eureka

I’ve a post on this topic A .NET service registered with Eureka, but let’s go through the process again with the current version of Steeltoe (as the NuGet packages have changed somewhat).

  • Create an ASP.NET Core Web Application, as this will represent a REST service that we want to interact with. My project is called RegisterExample and is an API project.
  • Add the NuGet package Steeltoe.Discovery.ClientCore and Steeltoe.Discovery.Eureka
  • In Startup.cs within ConfigureServices add the following
    services.AddDiscoveryClient(Configuration);
    
  • Within the Configure method add the following
    app.UseDiscoveryClient();
    
  • Finally add the following to the appsettings.json file
    // Eureka info
      "eureka": {
        "client": {
          "serviceUrl": "http://localhost:8761/eureka/",
          "shouldFetchRegistry": "false",
          "shouldRegisterWithEureka": true,
          "validateCertificates": false
        },
        "instance": {
          "appName": "weatherapi",
          "port": "8080",
          "ipAddress": "localhost",
          "preferIpAddress": true
        }
      }
    

We’re going to leave the API with the default weatherapi, hence the appName in the appsettings.

Notice we set the value for “shouldFetchRegistry” to false as this service will not be acting as a client to any other services. Obviously change this is you also need to discovery other services. “shouldRegisterWithEureka” is set to true as we want this service to automatically register itself with Eureka.

Now navigate to the URL of your Eureka server again (or refresh) and you should see a new Application. In my case I have an application with the name weatherapi. This name comes from our appsettings.json configuration application/name.

Info and Health

If you click on the instance link within the Eureka server dashboard, you will navigate to

https://localhost:5001/info

(or whatever ip/hostname and port your service is running on) you get a 404, so let’s fix that in our project.

Info, by default will display some basic information about the application, product version etc. However you can also add further custom information if you want, but example the git build SHA1 hash or just some general info.

  • Add NuGet package Steeltoe.Management.EndpointCore
  • In Startup.cs ConfigureServices, add the following
    services.AddSingleton<IInfoContributor, MyInfoContributor>();
    services.AddInfoActuator(Configuration);
    services.AddHealthActuator(Configuration);
    

    The first of these lines will add our implementation of an IInfoContributor to allow for custom info.

  • Still in Startup.cs, but now the method Configure, add the following to the UseEndpoints endpoint routes

    endpoints.Map<InfoEndpoint>();
    endpoints.Map<HealthEndpoint>();
    
  • Now we’ll create a simple implementation of and IInfoContributor which allows us to add our own info, so add the following class

    public class MyInfoContributor : IInfoContributor
    {
       public void Contribute(IInfoBuilder builder)
       {
          builder.WithInfo("MyInfo", new { SomeName = "Scooby Doo" });
       }
    }
    

Now when we run our service we hope to see our info data, however by default Steeltoe seems to set the info and health endpoint to /actuator/info and /actuator/health respectively. Eureka seems to expect /info. So go to the appsettings.json and add the following to the Instance section

"StatusPageUrlPath": "/actuator/info",
"HealthCheckUrlPath": "/actuator/health" 

Note: I’m not sure what I’m missing here and why the defaults don’t match up, but these configuration changes will tell the Eureka server (when we register our service with it) that it should use these paths for info and health.

Now, if you run the service again for /actuator/info you should see something like this

{"MyInfo":{"someName":"Scooby Doo"},
"applicationVersionInfo":{"ProductName":"RegisterExample",
"FileVersion":"1.0.0.0","ProductVersion":"1.0.0"},
"steeltoeVersionInfo":"ProductName":"Steeltoe.Management.Endpoint",
"FileVersion":"3.0.2.0","ProductVersion":
"3.0.2\u002B4089779c66d127f40325a3be9b613149b3b090f2"}}

and health something like

{"status":"UP","details":{"liveness":{},"diskSpace":{"total":4000769372160,
"free":3889734168576,"threshold":10485760,"status":"UP"},"eurekaServer":
{"remoteInstStatus":"UNKNOWN","fetchStatus":"Not fetching","heartbeat":
"Successful","heartbeatStatus":"UP","heartbeatTime":"2021-01-23T20:40:44",
"status":"UP","applications":"NONE"},"readiness":{}}}

Setting up Swift on Ubuntu 18.04

Let’s setup a swift development environment on Ubuntu 18.04. “Why?”, you might ask, as swift was written by Apple for Mac and iOS development and I do happen to have a Apple Mac with everything installed there, my answer is “why not”, let’s give it a try.

  • Go to https://swift.org/download/#releases and locate the version of Swift you want to download, I picked Swift 5.3.2, Ubuntu 18.04. Download this to your machine.
  • From ~./Downloads run
    tar -xvzf swift-5.3.2-RELEASE-ubuntu18.04.tar.gz 
    

    Obviously replace the .tar.gz with whatever version you download.

  • Now would probably be a good time to move the resultant decompressed folder to where you want it to be kept, mine’s in a ~/Home/swift directory.
  • Open .bashrc and add the following line (or just export the path if you want it temporary without adding to .bashrc)
    export PATH=$PATH:$HOME/swift/swift-5.3.2-RELEASE-ubuntu18.04/usr/bin
    

    Don’t forget to save the .bashrc file if you’ve gone that route and ensure the path to swift usr/bin matches where you moved your files to

  • I’m going to use VSCode, as my editor, and there’s several Swift extensions, I installed Swift Language 0.2.0. This has the largest number of downloads, I’ve no idea how good this is compared to others, but that’s the one I’ve installed for now.
  • If all went well, open a terminal window in VSCode or just use the a bash terminal and type
    swift --version
    

    If all went well you’ll see the Swift version and Target listed

Getting Started

Now we have swift installed (hopefully), let’s look at the sort of “Getting Started” type of things you’ll want to try.

Let’s use the swift command to create a simple executable application. So run the following for your terminal

swift package init --type executable

This will use swift’s package manager to create a new executable application with good old “Hello World”.

To build this, simple execute the following command

swift build

and to run this we simply execute the command

swift run

When making changes to your code you can actually just save the file(s) and use the run command which will build and run the application.

The command which generated this source created a Package.swift file which is where we add dependencies etc. Source code is stored in the Sources folder, and here the file is named main.swift which simply contains

print("Hello World!")

In the Tests folder we have the tests for the application. We’re not going to go into those now except to say, you can run the following command to run the tests

swift test

HTML Canvas

HTML Canvas allows us to draw graphics (using JavaScript). We can create a canvas element and use a WebGL API to draw lines, fill rectangles etc.

Let’s demonstrate some of the canvas and drawing functionality by creating a simple Tic-Tac-Toe UI. Start of by creating a HTML file and within that we add a canvas element, for example

<canvas id="tic-tac-toe-board"></canvas>

Now in our JavaScript (or in my case I’m going to use TypeScript) we can get the element using the id and then start interacting with the canvas using WebGL, for for example here’s a “main” method which we’d run in the HTML body’s onload event. The first thing we need to do is get the canvas element using

const board = <HTMLCanvasElement> document.getElementById('tic-tac-toe-board');

Next up we need to get the element’s context using

const ctx = board.getContext('2d');  
if(ctx != null) {
}

Once we have the context we can use methods, such as fillRect, moveTo, lineTo etc. as well as set properties on the context, for example

const ctx = board.getContext('2d');  
if(ctx != null) {
  ctx.fillStyle = 'white';
  ctx.fillRect(0, 0, board.clientWidth, board.clientHeight);
}

Below is an example of an implementation of code to draw a Tic-Tac-Toe board and handle click events.

function main() {
  const board = <HTMLCanvasElement> document.getElementById('tic-tac-toe-board');

  const size = 500;
  const lineColour = "#ddd";
  const lineStart = 4;
  const lineLength = size - 5;
  const sectionSize = size / 3;

  board.width = size;
  board.height = size;

  const elemLeft = board.offsetLeft + board.clientLeft;
  const elemTop = board.offsetTop + board.clientTop;
  const elemWidth = board.clientWidth;
  const elemHeight = board.clientHeight;

  // example of handling click event
  board.addEventListener('click', ev => {
     const x = ev.pageX - elemLeft;
     const y = ev.pageY - elemTop;

     console.log(`X: ${x}, Y: ${y}`);
  });

  const ctx = board.getContext('2d');  
  if(ctx != null) {
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, elemWidth, elemHeight);

    ctx.lineWidth = 10;
    ctx.lineCap = 'round';
    ctx.strokeStyle = lineColour;
    ctx.beginPath();

    for (let y = 1; y <= 2; y++) {  
      ctx.moveTo(lineStart, y * sectionSize);
      ctx.lineTo(lineLength, y * sectionSize);
    }
      
    for (let x = 1; x <= 2; x++) {
      ctx.moveTo(x * sectionSize, lineStart);
      ctx.lineTo(x * sectionSize, lineLength);
    }
      
    ctx.stroke();
  }
}

Source code for this post is available on github