Developing distributed applications with Tye

15 minute read

Working on solutions that consist of many applications can be tiresome and the recent push for microservice-based architectures only made the situation worse.

If there is one advantage of monolithic systems is that developers are one F5 keypress away (at least in Visual Studio) from building, launching and being able to test their product.

Among many other things, Docker Compose and Kubernetes try to solve this.

The problem with these tools is their steep learning curve. Being based on containers, each application needs to be executed in a container/pod. This means creating docker files, building the containers, finding a way for the debuggers to attach to a process into a container and so on.

Tye aims at improving this aspect of the development cycle.

In the GitHub repo, we can see its mission statement:

Tye is a developer tool that makes developing, testing, and deploying microservices and distributed applications easier. Project Tye includes a local orchestrator to make developing microservices easier and the ability to deploy microservices to Kubernetes with minimal configuration.

Unlike Docker Compose and Kubernetes, containers are just one of the many options available to compose the application. Tye is able to orchestrate containers, executables, and, for .NET developers, projects. All this while providing each piece support for service discovery, log collection, replicas.

Tye is currently in experimental phase to give the developers time to understand the needs of the tools user base with the help of quick iterations and user surveys.

For example, at .NET Conf 2020, the development team shared how the survey highlighted how users focus more on local development than on the deployment part of the tooling. This can obviously be related to the experimental stage of the project as I don’t see many shops reworking their CI/CD pipelines for a tool in the early stage like Tye.

I will be using the following repository to back some of the snippets of this post: https://github.com/Kralizek/ToDoList.

Getting Tye

Tye is offered as a .NET tool. This means that, granted you have the .NET SDK installed, you will be able to get it by using dotnet tool install.

Currently, because the application is still in preview, you’ll need to specify the exact version and, if you want to use the latest bits, make sure to point to the NuGet repository where the night builds are uploaded.

Right now, to install the latest published version, I suggest going to the NuGet page for Tye and copy the command to execute in your prompt.

At the time of writing, the latest published version is the 0.5.0-alpha.20555.1, so you can install this version by executing the following command

> dotnet tool install --global Microsoft.Tye --version 0.5.0-alpha.20555.1

If you want to use the night builds, I suggest looking at the official documentation.

First run

When working with .NET projects and solutions, Tye is able to run without any configuration file by taking advantages of files like launchsettings.json and similar artifacts.

What you need to do is simply execute in your prompt

> tye run

Tye will scan the folder and execute dotnet run for each .NET projects found.

In case you didn’t install the tool globally, you’ll need to use the dotnet launcher: dotnet tye run.

This is a sample of the output in the console

> dotnet tye run
Loading Application Details...
Launching Tye Host...

[22:57:58 INF] Executing application from C:\Users\Renato\Development\Tests\ToDoList\ToDoList.sln
[22:57:58 INF] Dashboard running on http://127.0.0.1:8000
[22:57:58 INF] Building projects
[22:58:01 INF] Launching service service_3cf6b7c5-1: C:\Users\Renato\Development\Tests\ToDoList\src\resource-access\Service\bin\Debug\netcoreapp3.1\Service.exe
[22:58:01 INF] Launching service web_739a6e1e-3: C:\Users\Renato\Development\Tests\ToDoList\src\clients\Web\bin\Debug\netcoreapp3.1\Web.exe
[22:58:01 INF] Launching service webapi_457ed087-9: C:\Users\Renato\Development\Tests\ToDoList\src\entrypoints\WebAPI\bin\Debug\net5.0\WebAPI.exe
[22:58:01 INF] web_739a6e1e-3 running on process id 12696 bound to http://localhost:55851, https://localhost:55852
[22:58:01 INF] Replica web_739a6e1e-3 is moving to a ready state
[22:58:01 INF] service_3cf6b7c5-1 running on process id 14232 bound to http://localhost:55855, https://localhost:55856
[22:58:01 INF] Replica service_3cf6b7c5-1 is moving to a ready state
[22:58:01 INF] webapi_457ed087-9 running on process id 24128 bound to http://localhost:55853, https://localhost:55854
[22:58:01 INF] Replica webapi_457ed087-9 is moving to a ready state
[22:58:01 INF] Selected process 14232.
[22:58:01 INF] Selected process 12696.
[22:58:01 INF] Listening for event pipe events for service_3cf6b7c5-1 on process id 14232
[22:58:01 INF] Listening for event pipe events for web_739a6e1e-3 on process id 12696
[22:58:01 INF] Selected process 24128.
[22:58:01 INF] Listening for event pipe events for webapi_457ed087-9 on process id 24128

Inspecting the log, we can see that Tye detected the solution file and used it to discover the applications to execute.

Then, after all projects were built, it launched a service for each project keeping track of process id and the ports that each service opened.

Finally, it ensured that each service had reached a ready state and started listend for their log events.

Exploring the dashboard

Every time we tell Tye to host our services, a dashboard is made available at the local address http://127.0.0.1:8000.

Here is what it looks like.

The dashboard of Tye

For each of the hosted services, the dashboard allows us to:

  • Inspect the logs
  • Inspect the metrics
  • See and navigate to the bindings
  • See how many replicas are active and how many times the application has restarted

There is clearly some polish to do, but it’s clear the potential and the central role the dashboard will have in the development experience.

Once your work is done, simply press CTRL+C in your console to exit.

Tye will take care of stopping all services.

Adding a configuration file

Tye uses a YAML file called tye.yaml to configure the different aspects of the solution to start.

The file contains a description for each service part of the solution and other settings.

When a tye.yaml is detected in the folder, Tye uses it and avoids scanning the folder.

On the other hand, we can leverage the scanning abilities of Tye for one last time to generate the initial version of the configuration file.

Simply execute the following command to generate the file.

> tye init

Inspecting the file, you will see something like this

name: todolist
services:
- name: web
  project: src/clients/Web/Web.csproj
- name: webapi
  project: src/entrypoints/WebAPI/WebAPI.csproj
- name: service
  project: src/resource-access/Service/Service.csproj

The init process discovered three projects in the folder and created a service definition for each one of them.

Executing tye run will produce the same result as before.

The configuration file has many options. Exploring all of them in this post would be prentetious but I suggest reading the official documentation and, if you can ingest JSON schemas, the Tye schema.

In case you want to inspect the configuration used to run the application, you can look at the JSON document returned from the endpoint http://127.0.0.1:8000/api/v1/services. Here you can see all the services instantiated, their configuration, the environment variable passed to them and much more.

Service properties

All services types share the same settings like:

  • bindings that are exposed by the service
  • environment variables to be passed to the service
  • the amount of replicas to be instantiated
  • tags to decorate a service

Bindings

Specifying the bindings in the configuration file serves two purposes:

  • make it possible for Tye to notify other services about the ports the service is available
  • whenever possible, instruct the service about which port and protocol it should use

Obviously, the latter is possible only if the service can accept such configuration from external sources.

In their default setup, ASP.NET Core applications can be configured by the ASPNETCORE_URLS environment variable.

A binding is made of two parts: the port number and the protocol. Neither is required. If the port number is missing, Tye will pick a random one.

services:
- name: web
  bindings:
  - protocol: https

Bindings also support connection strings that can be used to communicate other services how to connect to a given binding.

services:
- name: postgres
  image:  postgres
  env:
  - name: POSTGRES_PASSWORD
    value: "pass@word1"
  bindings:
  - port: 5432
    connectionString: Server=${host};Port=${port};User Id=postgres;Password=${env:POSTGRES_PASSWORD};

In the snippet above, notice how the connection string is composed by other values available via environment variables and other binding properties.

Finally, bindings can be given a name to distinguish between multiple bindings of the same service. A name is required when the same service exposes more than one binding.

services:
- name: web
  bindings:
  - protocol: https
    name: https
  - protocol: http
    name: http

Environment variables

When instantiating a program, Tye will pass a set of environment variables.

In the configuration file, it is possible to specify additional variables to be passed to the service.

When specyfing environment variables, two syntaxes are available. A typical VAR=VALUE and one that follows the YAML structure.

services:
- name: web
  env:
  - FOO=BAR
- name: app
  env:
  - name: Hello
    value: World

Replicas

Some problems might occur when developing locally with a single application and deploying multiple instances of the same program.

To simulate this scenario, Tye supports launching multiple instances of the same services via the replicas field (default: 1).

When replicas is set to a value greater than 1, Tye instantiates a sidecar container that is used as a simple load balancer to dispatch request across the different replicas.

services:
- name: web
  replicas: 2

Tags

The service definitions can be enriched with tags. These are helpful when your applications is composed by many services and you want Tye to run only a subset of them and maybe F5 from Visual Studio the one you’re actively working on.

Let’s take this simple (and incomplete) configuration file.

services:
- name: service-1
  tags:
  - backend
- name: service-2
  tags:
  - backend
- name: web
  tags:
  - frontend

We can decide to run only the backend services by executing

> tye run --tags backend

Doings so, we instruct Tye to start only those services tagged as backend. Once the system is up, we can modify the configuration of our frontend application to connect to the available backend services.

Services types

Tye supports different kind of services.

Each service type has some specific setting.

.NET projects

When executing tye run, Tye will take care of building the projects and launching them.

When targeting a project, the service description must include a project entry that points to the project file.

Additionally, it is possible to specify the flag build: false to avoid the service to be built upon starting the system.

Here is a sample project service.

services:
- name: web
  project: src/clients/Web/Web.csproj

Docker containers

Tye supports two flavors of containers:

  • containers created off images to be pulled from a repository
  • containers created off Dockerfile when the system is starting

In the first case, the service descriptor will have to specify the image name

services:
- name: rabbit
  image: rabbitmq:3-management

In the second case, the service descriptor will have to specify the path to the Dockerfile to build

services:
- name: app
  dockerFile: containers/my-app/Dockerfile

When specifying the bindings for the service, it is possible to leverage the port-forwarding ability of Docker by using the containerPort property of the binding.

This is convenient when the application running in the container doesn’t support port configuration but we still want to use random port numbers during development.

In the snippet below, we are configuring a container running redis so that Tye will assign a random port to the container while the traffic will be forwarded from said port to the redis default port.

services:
- name: redis
  image: redis
  bindings:
    - containerPort: 6379

Docker containers can be configured with arguments to be passed to the container. This can be done via the args field.

services:
- name: xray
  dockerFile: containers/xray/Dockerfile
  args: "-o"

Finally, Docker containers supports volumes and they work exactly like in normal Docker containers.

services:
- name: xray
  dockerFile: containers/xray/Dockerfile
  volumes:
  - source: C:\Users\Renato\.aws\
    target: /root/.aws

Executables

A service of type executable can be used to work with other types of applications that are not yet supported natively by Tye. An example can be an Angular application.

services:
- name: web
  executable: node
  args: .\node_modules\@angular\cli\bin\ng serve web
  workingDirectory: web
  bindings:
  - port: 4200

With this configuration, Tye will start and stop the node server like any other service.

I would expect some kind of support for node.js applications sometimes in the future.

External services

If the service is running outside of our control and we can’t allow Tye to stop and start it, we can set the flag external: true to communicate Tye that the service exists but it cannot be controlled.

Here I configure the Angular app used earlier as an external application.

services:
- name: web
  external: true
  bindings:
  - port: 4200

In this case, it will be up to the developer to ensure that the Angular application is running before executing tye run.

Including another application

Tye supports the possibility to include another application by including its tye.yaml file.`

services:
- name: external-app
  include: ..\external-app\tye.yaml

All the services imported from the included application will be shown in the dashboard and can be handled as if they were part of the application.

The main difference is that services from the included application will not be affected by the deploy command and that bindings are not exposed across different applications.

Alternatively, it is possible to include an application by referencing its GitHub repository address.

services:
- name: external-app
  repository: https://github.com/Kralizek/ToDoList

In this case, the repository will be cloned and scanned for a tye.yaml file to be included.

A small quirk I encountered while playing around with included applications is that the two applications need to have the same name.

Service discovery

Service discovery is the process by which one service figures out the address of another service without relying on any hardcoded information.

This is desiderable because it makes it easier to deploy your applications in different stages and generally more resilient to infrastructural changes.

Tye’s approach to service discovery tries to be as little invasive as possible relying on environment variables and, wherever possible, integrating with the hosted services capabilities.

As mentioned above, when launching a service, Tye injects a series of environment variables. Among these, variables with the different bindings from the hosted services are exposed.

services:
- name: web
  project: src/web/web.csproj
  bindings:
  - protocol: https
- name: backend
  project: src/backend/backend.csproj
  bindings:
  - name: grpc
    protocol: https

When launching the application, each service will receive this set of variables (among others)

Name Value Generated value
SERVICE__WEB__PROTOCOL https  
SERVICE__WEB__PORT  
SERVICE__WEB__HOST  
SERVICE__BACKEND__GRPC__PROTOCOL https  
SERVICE__BACKEND__GRPC__PORT  
SERVICE__BACKEND__GRPC__HOST  

These environment variables can be used to let each service find the address of its dependencies.

When working with ASP.NET Core applications, developers can use the package Microsoft.Tye.Extensions.Configuration.

This package adds extension methods to the IConfiguration type to easily consume the environment variables when setting up services.

For example, here I’m registering a GRPC client to an address retrieved via the extension method GetServiceUri.

services.AddGrpcClient<ToDo.ToDoClient>(o =>
{
    o.Address = Configuration.GetServiceUri(service: "backend", binding: "grpc");
});

Please notice how I specify both the service name and the binding name. If no name is given to the binding in the Tye configuration file, we can omit that parameter.

Connection strings

The system used for service discovery can also be used to inject connection strings into depending services.

Connection strings can be added to the definition of a binding and will be passed down to services as environment variables.

An important peculiarity of the connection strings is the possibility to combine values from the binding like the hostname, the port, the protocol and other environment variables.

services:
- name: postgres
  image:  postgres
  env:
  - name: POSTGRES_PASSWORD
    value: "pass@word1"
  bindings:
  - port: 5432
    connectionString: Server=${host};Port=${port};User Id=postgres;Password=${env:POSTGRES_PASSWORD};

In the snippet above, you can see how the connection string is composed by interpolating elements from the binding definition. Important to note that elements that are not specified in the binding definition: in this case the autogenerated value will be used to compose the connection string.

When running a system with the service declared above, the other services will see an environment variable named CONNECTIONSTRING__POSTGRES whose value will be based on the template specified in the binding.

In case the binding is explicitly named, the environment variable will be named CONNECTIONSTRING__POSTGRES__BINDINGNAME.

Like for service discovery, developers working on a ASP.NET Core service can use the package Microsoft.Tye.Extensions.Configuration.

This package exposes a GetConnectionString extension method that makes it trivial to retrieve the connection string.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<BloggingContext>(options =>
        options.UseNpgsql(Configuration.GetConnectionString("postgres")));
}

The extension method has the same name of the ConfigurationExtensions.GetConnectionString available in the Microsoft.Extensions.Configuration.Abstractions package on purpose.

The idea behind this is to overload the older extension method with one that supports a named binding (via an optional parameter). This very limited interference is possible because the environment variables are named following the ASP.NET Core standard.

Run options

The run commands has few flags or parameters that can be used to customize the execution of the application.

To see the full list, simply use the included help flag.

> tye run -h

In this post I will show the most interesting ones.

Debug

While extremely helpful, just running all the services composing the application does only half the job Tye is called to accomplish.

The other half is providing a conveniente way to debug and troubleshoot the different parts of the application.

Granted, Tye is still at experimental stage. This means that there is no integration with neither Visual Studio or Visual Studio Code but it already offers some tools useful when debugging the application.

At the current stage, the whole debugging experience revolves around the “attach to process” workflow that those who work with IIS know very well.

The easiest thing to do is to start your application using the tye run command and attach to the needed process. This already would help most of the use cases since services are often reacting to external calls.

In case you need to debug a service before it starts accepting connections, you can instruct Tye to stop the execution of the service’s process until a debugger is attached.

This can be achieved by executing the command

> tye run --debug my-service

In case you want to attach a debugger to all services, you can use * instead of the service name.

> tye run --debug *

Watch

Another trick that Tye offers to improve the development workflow is the possibility to enable a file system watcher and restart any service that is modified while the application is running. Note that only the modified service is restarted, not the whole application.

To turn on this capability, simply add the --watch switch to the run command.

> tye run --watch

Docker

Finally, it is possible to instruct Tye to package all service whose type is project to execute them in a container.

To do so, simply add the --docker switch to the run command.

> tye run --docker

Recap

In this post I have showed the promising Project Tye currently in the works at Microsoft.

The project is still in its experimental phase but it was featured both at Ignite and .NET Conf so I’m very optimistic that it can grow into a mature product.

Personally, I’ll start using it in my development workflow even if it’s still in preview since there is no risk of affecting the production environment.

I strongly suggest you doing the same and I would also encourage you to fill in their survey to help shape this product!