Developing distributed applications with Tye
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.
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!
Support this blog
If you liked this article, consider supporting this blog by buying me a pizza!