In this series:
- Distributed Tracing with Jaeger
- Simplifying the setup with Tye (this article)
Tye is an experimental dotnet tool from Microsoft that aims to make developing, testing, and deploying microservices easier. Tye’s opinionated nature greatly simplifies the lifecycle of development and deployment of .NET Core microservices.
To understand the benefits of Tye, let’s enumerate the steps involved in the development and deployment of the DCalculator application to Kubernetes:
- Create dockerfiles for the services.
- Configure the address of the Log service in the Calculator service and ensure that the setting is appropriately configured in the various environments.
- Build containers and upload them to a container registry.
- Attach debugger to container processes.
- Write YAML specifications for services and apply them on your Kubernetes cluster to deploy your services.
- Store connection strings in Kubernetes secrets.
- Use tools such as K9s to view your application logs.
Project Tye removes all these steps from the developer workflow. You only need to write a simple configuration file, know a few commands, and Tye will take care of the rest.
Tye vs. Docker Compose
Docker Compose is an excellent tool for orchestrating containers. However, a complex application comprises multiple other components such as containers, executables, and .NET projects in local and remote repositories. Unlike Docker Compose, Tye is built for .NET applications and uses a convention-based model for service discovery, resolving dependencies, and automatic containerization of .NET applications.
Tye has several utilities that assist developers in developing applications and deploying them to Kubernetes without requiring them to write complex manifests and dockerfiles.
Source Code
You can download the source code of the demo application from my GitHub repository.
Installation and Dashboard
Since Tye is still in preview, you need to specify the version of Tye that you wish to install. To install the latest version of Tye, execute the command specified in the NuGet page for Tye, which at the time of writing is the following:
dotnet tool install -g Microsoft.Tye --version "0.6.0-alpha.21070.5"
Navigate to the root directory of your solution, which contains your project folders, and execute the following command:
tye run --dashboard
Tye will automatically discover the .NET Core projects and execute dotnet run
for each of them. It will also host a dashboard at http://127.0.0.1:8000/ that lists the discovered services as follows:
The dashboard allows you to inspect the logs, metrics, bindings, and replicas. You can exit Tye by entering Ctrl + C in the console, and Tye will terminate all the services that it started.
Tye Configuration File
In addition to the services, the DCalculator application also consists of the Jaeger all-in-one container. To specify application dependencies and customize any other aspect of your solution, Tye relies on a YAML file called tye.yaml
. If Tye detects this file in the solution root folder, it stops the project discovery process and relies on the information in the file to discover the services.
To create the tye.yaml
file, execute the following command:
tye init
The configuration file generated by the init
command will already contain the details of the services discovered by Tye. Let’s add the Jaeger service to the configuration as follows:
name: DCalculator
services:
- name: Calculator
project: Calculator/Calculator.csproj
- name: LogService
project: LogService/LogService.csproj
- name: Jaeger
image: jaegertracing/all-in-one:1.22
bindings:
- port: 14268
connectionString: http://${host}:${port}/api/traces
name: http-thrift
- port: 16686
name: ui
In addition to .NET Core projects, one of the service types supported by Tye is a Docker container. You can specify either the name of the container’s image or the location of the Dockerfile, and Tye will configure and start the container appropriately.
Remember that Jaeger requires ports 14268 and 16686 for exposing the HTTP collector and the user interface. The Jaeger UDP collector can receive traces and spans on ports 5775, 6831, and 6832. However, due to an open issue with the container to host port mapping in Tye, we can’t use the UDP collector with Tye currently. You can read more about the purpose of the various Jaeger ports in its deployment guide.
Tye bindings support the connection string property, which communicates the connection information to the other services. The Jaeger HTTP collector requires an instance of HttpSender
that takes the collector endpoint address as an argument. Rather than hardcoding the endpoint’s address in code, we will surface the address as a connection string. You must have noticed that we can compose the connection string using binding properties such as host
and port
. Apart from properties, you can interpolate environment variables in the connection string as well.
You must specify a name for the binding to uniquely identify it if there are more than one bindings of the same service. In addition to bindings, there are several options that you can configure to customize services in Tye. I recommend that you explore the YAML schema specification to understand the supported configuration settings and their expected values.
Start Docker Desktop and relaunch the Tye dashboard. You will find Jaeger service in the list of services managed by Tye.
Note that Tye used the binding information to expose the container ports that we configured to the host. You can navigate to the Jaeger UI at http://localhost:16686/ to verify whether the Jaeger instance is healthy.
Tye’ing the Services: Service Discovery with Tye
On every execution, Tye dynamically assigns ports to the Calculator and Log services. To help a service discover other services, Tye supplies the binding information of all the services to every service through environment variables. You can read more about how the process works in practice in the Service Discovery reference document.
You can install the Microsoft.Tye.Extensions.Configuration NuGet package that contains the logic to read the environment variables and find the address of the target service. The library adds a few extension methods to IConfiguration
to retrieve service addresses and connection strings from bindings.
Install the Microsoft.Tye.Extensions.Configuration NuGet package to the Calculator service and Log service projects. Let’s first make modifications to the Calculator service to enable it to communicate with the Log service. Navigate to the ConfigureServices
method of the Startup
class and modify the base address used by the HttpClient
as follows:
services.AddHttpClient("logService",
c =>
{
c.BaseAddress = Configuration.GetServiceUri("LogService");
});
Easy, isn’t it? Let’s also configure our Jaeger reporter to send traces to the HTTP collector endpoint that we configured in Tye. Update the method to resolve an instance of the ITracer
interface as follows:
services.AddSingleton<ITracer>(sp =>
{
var serviceName = sp.GetRequiredService<IWebHostEnvironment>().ApplicationName;
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var reporter = new RemoteReporter.Builder()
.WithLoggerFactory(loggerFactory)
.WithSender(
new HttpSender(Configuration.GetConnectionString("Jaeger", "http-thrift"))
.Build();
var tracer = new Tracer.Builder(serviceName)
// The constant sampler reports every span.
.WithSampler(new ConstSampler(true))
// LoggingReporter prints every reported span to the logging framework.
.WithReporter(reporter)
.Build();
return tracer;
});
The GetConnectionString
method takes the names of the service and the binding as input to resolve the value of the connection string. Since our Log service also depends on Jaeger, make the same change to the ITracer
resolver in the Log service.
Execute a few runs of the Calculator service using the Swagger UI, thereby generating a few traces. Visit the Jaeger dashboard at http://localhost:16686/ to view the traces generated by the services.
The tye run
command has several useful flags that you can use. For example, the --watch
flag configures Tye to watch the file system for any changes to the source code of services and restart the relevant service if a change is detected. The argument --debug *
will attach a debugger to all the services. You can replace the argument value *
with the name of a service to attach the debugger to the process running that service.
Running Services as Containers
Tye can package our services and execute them in containers without any additional effort. Execute the tye run
command with the --docker
flag as follows:
tye run --docker
Upon execution, Tye will pull the base image for the projects, build the projects and images, create a network, and finally run containers from those images.
You can see that all the applications are now running as Linux-based containers. We did not have to write dockerfiles or establish networking between containers with docker-compose. Tye used the project structure and the dependencies specified in the configuration file to launch our application in containers.
As we discussed earlier, publishing an application to Kubernetes is a non-trivial activity. However, with Tye, you can deploy your application to Kubernetes by simply using the tye deploy
command. To deploy your application to a Kubernetes cluster, use the following command to launch the application deployment process in the interactive mode:
tye deploy --interactive
In interactive mode, you will receive prompts to enter details such as container registry and connection strings. You can read about deployment to Kubernetes using Tye in detail in the deployment reference document on GitHub. For now, let’s inspect the Kubernetes manifest that Tye generates for our application using the following command:
tye generate
The generate
command will create a file named dCalculator-generate-production.yaml
with the following content:
kind: Deployment
apiVersion: apps/v1
metadata:
name: calculator
labels:
app.kubernetes.io/name: "calculator"
app.kubernetes.io/part-of: "DCalculator"
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: calculator
template:
metadata:
labels:
app.kubernetes.io/name: "calculator"
app.kubernetes.io/part-of: "DCalculator"
spec:
containers:
- name: calculator
image: calculator:1.0.0
imagePullPolicy: Always
env:
- name: DOTNET_LOGGING__CONSOLE__DISABLECOLORS
value: "true"
- name: ASPNETCORE_URLS
value: "http://*"
- name: PORT
value: "80"
- name: SERVICE__CALCULATOR__PROTOCOL
value: "http"
- name: SERVICE__CALCULATOR__PORT
value: "80"
- name: SERVICE__CALCULATOR__HOST
value: "calculator"
- name: SERVICE__LOGSERVICE__PROTOCOL
value: "http"
- name: SERVICE__LOGSERVICE__PORT
value: "80"
- name: SERVICE__LOGSERVICE__HOST
value: "logservice"
- name: CONNECTIONSTRINGS__JAEGER__HTTP-THRIFT
valueFrom:
secretKeyRef:
name: "binding-production-jaeger-http-thrift-secret"
key: "connectionstring"
- name: SERVICE__JAEGER__UI__PROTOCOL
valueFrom:
secretKeyRef:
name: "binding-production-jaeger-ui-secret"
key: "protocol"
- name: SERVICE__JAEGER__UI__HOST
valueFrom:
secretKeyRef:
name: "binding-production-jaeger-ui-secret"
key: "host"
- name: SERVICE__JAEGER__UI__PORT
valueFrom:
secretKeyRef:
name: "binding-production-jaeger-ui-secret"
key: "port"
ports:
- containerPort: 80
---
kind: Service
apiVersion: v1
metadata:
name: calculator
labels:
app.kubernetes.io/name: "calculator"
app.kubernetes.io/part-of: "DCalculator"
spec:
selector:
app.kubernetes.io/name: calculator
type: ClusterIP
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: logservice
labels:
app.kubernetes.io/name: "logservice"
app.kubernetes.io/part-of: "DCalculator"
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: logservice
template:
metadata:
labels:
app.kubernetes.io/name: "logservice"
app.kubernetes.io/part-of: "DCalculator"
spec:
containers:
- name: logservice
image: logservice:1.0.0
imagePullPolicy: Always
env:
- name: DOTNET_LOGGING__CONSOLE__DISABLECOLORS
value: "true"
- name: ASPNETCORE_URLS
value: "http://*"
- name: PORT
value: "80"
- name: SERVICE__LOGSERVICE__PROTOCOL
value: "http"
- name: SERVICE__LOGSERVICE__PORT
value: "80"
- name: SERVICE__LOGSERVICE__HOST
value: "logservice"
- name: SERVICE__CALCULATOR__PROTOCOL
value: "http"
- name: SERVICE__CALCULATOR__PORT
value: "80"
- name: SERVICE__CALCULATOR__HOST
value: "calculator"
- name: CONNECTIONSTRINGS__JAEGER__HTTP-THRIFT
valueFrom:
secretKeyRef:
name: "binding-production-jaeger-http-thrift-secret"
key: "connectionstring"
- name: SERVICE__JAEGER__UI__PROTOCOL
valueFrom:
secretKeyRef:
name: "binding-production-jaeger-ui-secret"
key: "protocol"
- name: SERVICE__JAEGER__UI__HOST
valueFrom:
secretKeyRef:
name: "binding-production-jaeger-ui-secret"
key: "host"
- name: SERVICE__JAEGER__UI__PORT
valueFrom:
secretKeyRef:
name: "binding-production-jaeger-ui-secret"
key: "port"
ports:
- containerPort: 80
---
kind: Service
apiVersion: v1
metadata:
name: logservice
labels:
app.kubernetes.io/name: "logservice"
app.kubernetes.io/part-of: "DCalculator"
spec:
selector:
app.kubernetes.io/name: logservice
type: ClusterIP
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
You can see that Tye created specifications of a deployment and a service object for each of our Calculator and Log services in the manifest. The secrets specified in the specifications are generated and applied when you execute the deploy
command.
Summary
In this article, we simplified our application development and deployment processes using Project Tye. Tye is currently an experimental tool but fast moving towards becoming a mature product.
Since Tye overlays existing tools such as Docker and Kubernetes, you can safely use it in your development workflow without affecting the production environment. If you liked this series or have any suggestions, please comment below or send me a tweet at @rahulrai_in
Did you enjoy reading this article? I can notify you the next time I publish on this blog... ✍