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

with and record types in C# 9.0

In the previous post we looked at the new C# record type. Whilst we can make record types mutable, they’re particularly well suited to immutability. When used as immutable types we need a way to make new immutable types based upon previous ones.

Let’s use our Person record type from the previous post but sprinkled with C# 9 goodness in the form of init

public record Person
{
   public string FirstName { get; init; }
   public string LastName { get; init; }
}

The init syntax gives us the ability to create instances of a Person, assigning values during the construction phase of the record type using standard initialiser syntax.

For example

var p = new Person 
{ 
   FirstName = "Scooby", 
   LastName = "Doo" 
};

However, this post is mean’t to be about the with keyword with allows us to take a record type and create a new instance baed upon and existing record with changes. i.e. we want to take the Person p and create a new record with the FirstName = “Scrappy”, like this

var scrappy = p with { FirstName = "Scrappy" };

the result of outputting this to the console would be

Person { FirstName = Scrappy, LastName = Doo }

C# 9.0 record type

As of yesterday I got the updated version of Visual Studio 2019 (16.8.1) which includes .NET 5 and C# 9.0 – I don’t intend to post about every feature of C# 9.0 – for those interested simply go and take a look at What’s new in C# 9.0.

One feature that’s quite interesting is C# Records.

Record types “are a reference type that provides synthesized methods to provide value semantics for equality”. So basically we can create multiple instances of a record and compare them for equality. Here’s an example of a record type

public record Person
{
   public Person(string firstName, string lastName)
   {
      FirstName = firstName;
      LastName = lastName;
   }

   public string LastName { get; }
   public string FirstName { get; }
}

It’s that easy, we simply declare our type as a record instead of a class.

Now, if we create a couple of instances of a Person that look like this

Person person1 = new("Scooby", "Doo");
Person person2 = new("Scooby", "Doo");

and we compare them using ==/Equals, they will result in the two instances being the same (unlike with classes with would compare equality by reference).

Another feature of records, over classes is. If you use ToString() on an instance of a class you’ll see something like

TestApp.Person

For a record type you’ll instead get

Person { LastName = Doo, FirstName = Scooby }

this is facilitated by the compiler adding a PrintMembers protected method which generates this output.