Cześć! Bardzo interesujący tydzień za nami! W końcu pojawił się .NET 5, a wraz z nim 3-dniowa konferencja pełna interesujących prelekcji! Jeśli nie mieliście na to jeszcze czasu, to gorąco polecam zajrzenie tutaj i obejrzenie niektórych wykładów. Kilka z nich było poświęcone projektowi Tye. Temat ten bardzo mnie zainteresował, więc postanowiłem samodzielnie spróbować użyć tego narzędzie. Efektem mojego eksperymentu jest wpis, który właśnie czytasz w Cesarstwie-Dev! Nie będę wchodził w techniczne szczegóły tego projektu, lecz pokażę kolejne efekty pierwszych kilku godzin pracy z Tye. Mam nadzieję, że jesteście równie zainteresowani co ja! Zaczynajmy!

Czym jest Tye?

Na to pytanie niech odpowiedzą nam sami twórcy tego narzędzia!

Tye is a 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.

Tye

Jak możemy przeczytać, Tye jest narzędziem, które ma ułatwić .NET developerom implementację i wdrażanie systemów mikroserwisowych. Jako programiści mamy bardzo wiele technologii, które wspomagają wykonywanie powyższych technologii. Dlaczego więc powinniśmy zainteresować się tym nowym projektem? Moim zdaniem jego wielką zaletą jest fakt, że jest wyspecjalizowane w pracy z aplikacjami napisanymi w ekosystemie .NET, ale również obsługuje technologię Docker. Microsoft stworzył narzędzie, które powinno świetnie współgrać z naszymi aplikacjami, a dodatkowo wspierać integracje z aplikacjami napisanymi w innych technologiach (na przykład takimi jak Redis czy RabbitMq).

Projekt Tye jest narzędziem, które pozwala łączyć nasze aplikacje w podobny sposób do Kubernetesa – za pomocą konfiguracji zapisanej w pliku yaml. Głównymi celami są uproszczenie implementacji na lokalnym komputerze oraz wdrażanie systemów mikroserwisowych przy minimalnym nakładzie pracy. Mimo początkowej fazy projektu (wciąż jest w wersji alpha) uważam, że może z powodzeniem zredukować nakład pracy związany z przygotowywaniem systemów rozproszonych. Oby dalej to szło w tym kierunku! Przejdźmy już do mięska i zobaczmy, co Tye oferuje nam już teraz!

Jak zacząć?

Na sam początek powiem, że przedstawione kroki będą wymagały zainstalowanego Dockera oraz .NET 5. Docker nie jest jednak potrzebny, dopóki nie dodamy aplikacji innych niż .NET-owe projekty. Niezależnie, każdego gorąco zachęcam do przejścia jak największej liczby kroków razem ze mną. Zacznijmy więc od instalacji! Otwórzmy konsolę i napiszmy poniższe polecenie:

dotnet tool install -g Microsoft.Tye --version 0.5.0-alpha.20555.1+fae47325b0c8d7dafcdec5d1248191b24b2adc23

Powyższa instrukcja zainstaluje Tye w wersji, z której korzystam obecnie. Następnie upewnijmy się, że Tye jest poprawnie zainstalowany:

tye --version
> 0.5.0-alpha.20555.1+fae47325b0c8d7dafcdec5d1248191b24b2adc23

Po zainstalowaniu możemy zacząć tworzyć nasz projekt! Wykorzystamy do tego bazowe projekty, które będziemy modyfikować zgodnie z potrzebami. Stwórzmy więc projekt backendowy oraz frontendowy.

mkdir TyeTest
cd TyeTest
dotnet new razor -n frontend
dotnet new webapi -n backend
dotnet new sln
dotnet sln add frontend backend

Powinniśmy otrzymać dzięki temu solucję z dwoma projektami, które są bazowymi projektami. Spróbujmy je uruchomić!

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

> 
> [15:12:06 INF] Executing application from D:\Source\Cesarstwo\TyeTest\TyeTest.sln
> [15:12:06 INF] Dashboard running on http://127.0.0.1:8000
> [15:12:06 INF] Building projects
> [15:12:10 INF] Launching service backend_6e819338-9: D:\Source\Cesarstwo\TyeTest\backend\bin\Debug\net5.0\backend.exe
> [15:12:10 INF] Launching service frontend_4a2e289d-f: D:\Source\Cesarstwo\TyeTest\frontend\bin\Debug\net5.0\frontend.exe
> [15:12:11 INF] backend_6e819338-9 running on process id 14300 bound to http://localhost:49308, https://localhost:49309
> [15:12:11 INF] Replica backend_6e819338-9 is moving to a ready state
> [15:12:11 INF] frontend_4a2e289d-f running on process id 12740 bound to http://localhost:49306, https://localhost:49307
> [15:12:11 INF] Replica frontend_4a2e289d-f is moving to a ready state
> [15:12:12 INF] Selected process 14300.
> [15:12:12 INF] Listening for event pipe events for backend_6e819338-9 on process id 14300
> [15:12:12 INF] Selected process 12740.
> [15:12:12 INF] Listening for event pipe events for frontend_4a2e289d-f on process id 12740

Jeśli wszystko poszło zgodnie z planem powinniśmy móc zobaczyć dashboard pod adresem localhost:8000. Sprawdźmy to!

Warto zaznaczyć, że porty są przypisywane losowo, jeśli nie są sprecyzowane w konfiguracji. Do tego wrócimy później, teraz przejdźmy do linków z kolumny Bindings, i zobaczmy czy nasze aplikacje rzeczywiście działają!

W celu wyłączenia Tye należy nacisnąć w konsoli ctrl + c.

Niech dzieje się magia!

Udało nam się odpalić dwie aplikacje za pomocą jednej instrukcji. Fajne, ale nic imponującego. Sprawmy teraz, aby nasze systemy umiały ze sobą rozmawiać. Przejdźmy do projektu frontend i dodajmy odpowiednie klasy.

    public record WeatherForecast(DateTime Date, int TemperatureC, int TemperatureF, string Summary);

    public class WeatherClient
    {
        private readonly JsonSerializerOptions options = new JsonSerializerOptions()
        {
            PropertyNameCaseInsensitive = true,
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };

        private readonly HttpClient _client;

        public WeatherClient(HttpClient client)
        {
            _client = client;
        }

        public async Task<WeatherForecast[]> GetWeatherAsync()
        {
            using var stream = await _client.GetStreamAsync("/weatherforecast");
            return await JsonSerializer.DeserializeAsync<WeatherForecast[]>(stream, options);
        }
    }

Zmodyfikujmy też pliki Index.cshtml.cs oraz Index.cshtml

using frontend.Core;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace frontend.Pages
{
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }

        public WeatherForecast[] Forecasts { get; private set; }

        public async Task OnGet([FromServices] WeatherClient client)
        {
            Forecasts = await client.GetWeatherAsync();
        }
    }
}
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

Weather Forecast:

<table class="table">
    <thead>
        <tr>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var forecast in @Model.Forecasts)
        {
            <tr>
                <td>@forecast.Date.ToShortDateString()</td>
                <td>@forecast.TemperatureC</td>
                <td>@forecast.TemperatureF</td>
                <td>@forecast.Summary</td>
            </tr>
        }
    </tbody>
</table>

Brakuje ostatniego klocka – najlepsze na koniec! Zobaczmy jak prosta jest rejestracja klienta do backendu. Dodajmy poniższe linijki do pliku Startup.cs oraz zainstalujmy w projekcie frontend paczkę Microsoft.Tye.Extensions.Configuration.

            services.AddHttpClient<WeatherClient>(client =>
            {
                client.BaseAddress = Configuration.GetServiceUri("backend");
            });

Powyższy kod pobiera z konfiguracji adres url, pod jakim wystawiona jest usługa o nazwie backend. Przed uruchomieniem aplikacji zobaczmy, skąd pobierane są nazwy projektów. Wpiszmy w konsoli.

tye init

Powyższa instrukcja powinna utworzyć plik o nazwie tye.yaml z poniższą treścią:

# tye application configuration file
# read all about it at https://github.com/dotnet/tye
#
# when you've given us a try, we'd love to know what you think:
#    https://aka.ms/AA7q20u
#
name: tyetest
services:
- name: frontend
  project: frontend/frontend.csproj
- name: backend
  project: backend/backend.csproj

Jak widać, to tutaj skonfigurowane są nasze serwisy. Tutaj możemy też dodawać serwisy niestandardowe (nie .NET-owe) jak i zarządzać portami czy ilością replik. Szczegółowe informacje można przeczytać tutaj. Zauważmy, że nazwy naszych serwisów są konfigurowane w tym pliku. Odpalmy teraz nasz system! Po uruchomieniu komendy tye run możemy przejść do dashboardu oraz wejść na serwis frontendowy. Powinniśmy zobaczyć poniższą stronę, która zmienia dane przy każdym odświeżeniu.

Super! Udało nam się skomunikować dwie usługi przy praktycznie zerowej konfiguracji. Mnie się podoba! A Wam? Dowiedzieliśmy się już jak użyć wbudowanego Service Discovery. A co z debugowaniem? To również jest możliwe. Możemy nawet wybrać czy chcemy debugować wszystkie projekty (*), czy tylko konkretny. Postawmy breakpoint w pierwszej linii metody Get w klasie WeatherForecastController z projektu backend. Następnie wpiszmy w konsoli polecenie:

tye run --debug backend

Wspominałem już, że z poziomu dashboardu mamy dostęp do logów z wszystkich serwisów? Zajrzyjmy do logów z backendu!

Jak widać, proces czeka na przyłączenie debuggera. Przejdźmy więc do Visual Studio oraz naciśnijmy ctrl + alt + p. W okienku, które wyskoczy musimy znaleźć proces o nazwie backend.exe i dwukrotnie go nacisnąć.

Dzięki temu przy próbie otworzenia aplikacji frontendowej zatrzymamy się na przygotowanym wcześniej breakpointcie!

Dodanie zewnętrznego serwisu

Od tego momentu do działania przykładów wymagany będzie Docker. Dla systemu Windows można go pobrać stąd. Spróbujemy teraz dodać cache Redis do naszego systemu. Zacznijmy od modyfikacji kontrolera z backendu. Zmieńmy metodę Get:

        [HttpGet]
        public async Task<IEnumerable<WeatherForecast>> GetAsync(
            [FromServices] IDistributedCache cache)
        {
            var cached = await cache.GetStringAsync("key");
            if (cached == null)
            {
                var rng = new Random();
                var result = Enumerable.Range(1, 5).Select(index => new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = rng.Next(-20, 55),
                    Summary = Summaries[rng.Next(Summaries.Length)]
                })
                .ToArray();

                await cache.SetStringAsync("key", JsonSerializer.Serialize(result), new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5)
                });

                return result;
            }

            return JsonSerializer.Deserialize<WeatherForecast[]>(cached);
        }

Teraz musimy doinstalować paczkę: Microsoft.Extensions.Caching.StackExchangeRedis, a następnie dodajmy rejestrację w klasie Startup.cs.

    services.AddStackExchangeRedisCache(o =>
    {
      o.Configuration = Configuration.GetConnectionString("redis");
    });

Ponownie korzystamy z pobierania sekretów dotyczących serwisów z konfiguracji stworzonej przez Tye! Dodajmy teraz Redisa do stworzonego wcześniej pliku tye.yaml. Sprecyzujmy również porty, z jakich korzystać będą serwisy backend oraz frontend.

name: tyetest
services:
- name: frontend
  project: frontend/frontend.csproj
  bindings:
  - port: 5000
- name: backend
  project: backend/backend.csproj
  bindings:
  - port: 3009
- name: redis
  image: redis
  bindings:
  - port: 6379
    connectionString: "${host}:${port}"

Warto zwrócić uwagę na ostatnią linię powyższej konfiguracji. Dodaje ona sekret, jakim jest connectionString do serwisu Redis. Dodatkowo, zmienne {host} oraz {port} zostaną zastąpione przez prawidłowe wartości. Dzięki takiemu podejście możemy pobrać tę wartość za pomocą metody Configuration.GetConnectionString. Włączmy teraz nasze projekty! Na dashboardzie zobaczymy, że stworzone zostały trzy serwisy:

Warto zauważyć, że obecnie serwisy frotnend oraz backend są uruchamiane jako procesy na lokalnym komputerze. Możemy uruchomić je w kontenerze, o czym powiemy w kolejnym rozdziale.

Po otworzeniu aplikacji frontendowej możemy się upewnić, że temperatura jest zmieniana wyłącznie co 5 sekund. Super! Udało nam się dodać i skonfigurować zewnętrzny serwis, a następnie skomunikować z nim nasze aplikacje. Póki co jest prosto i przyjemnie!

Możliwości Tye

Na dashboardzie widzimy liczbę replik każdego serwisu. Spróbujmy uruchomić naszą usługę backendową w dwóch instancjach. W tym celu zmodyfikujmy ponownie plik tye.yaml:

name: tyetest
services:
- name: frontend
  project: frontend/frontend.csproj
  bindings:
  - port: 5000
- name: backend
  project: backend/backend.csproj
  replicas: 2
  bindings:
  - port: 3009
- name: redis
  image: redis
  bindings:
  - port: 6379
    connectionString: "${host}:${port}"

Po uruchomieniu systemu zajrzyjmy na dashboard:

Jak widać serwis backend został uruchomiony w dwóch replikach. Co więcej, wciąż znajduje się pod jednym url – pod spodem został dodany load balancer. Żeby sprawdzić, czy rzeczywiście korzystamy z kilku instancji możemy dodać w projekcie backend do kontrolera nową metodę:


        [HttpGet("processId")]
        public int GetProcessId()
            => Environment.ProcessId;

Następnie przejdźmy do projektu frontendowego i dopiszmy poniższy kod.

        public async Task<int> GetProcessId()
        {
            var result = await _client.GetStringAsync("/weatherforecast/processId");
            return int.Parse(result);
        }  
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }

        public WeatherForecast[] Forecasts { get; private set; }
        public string Processes { get; private set; }

        public async Task OnGet([FromServices] WeatherClient client)
        {
            Forecasts = await client.GetWeatherAsync();
            var tasks = Enumerable.Range(0, 50).Select(x => client.GetProcessId());
            var processes = await Task.WhenAll(tasks);
            Processes = string.Join(", ", 
                processes.GroupBy(x => x).Select(x => $"Process {x.Key} was used {x.Count()} times"));
        }
    }
div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

Weather Forecast processed by backend on process ids: @Model.Processes

<table class="table">

Po powyższych zmianach możemy ponownie uruchomić nasze projekty. Zobaczmy efekt!

Powyższy screen udowadnia, że żądania są rozdzielone pomiędzy dwie instancje API. A to wszystko dzięki jednej linijce w konfiguracji. Wspaniale! Skorzystajmy teraz z faktu, że znamy id procesów i wyłączmy jeden z nich za pomocą polecenia:

taskkill /F /PID 28668 
> SUCCESS: The process with PID 28668 has been terminated.

Jeśli spojrzymy teraz na dashboard, to możemy zauważyć, że nastąpił jeden restart aplikacji backendowej. Oznacza to, że Tye dba o to, by odpalona była konkretna liczba instancji. Niestety, nie zadziało to tak kolorowo jeśli skorzystamy z dockera. Zobaczmy!

tye run --docker

Po uruchomieniu systemu zatrzymajmy jeden z kontenerów, które oznaczone są etykietą backend. Dla Docker Desktop proces ten wygląda następująco:

Po tej operacji zauważymy, że kontener nie został odpalony ponownie. W celu poprawnej obsługi restartów kontenerów musimy zmodyfikować plik tye.yaml. Zobaczmy!

name: tyetest
services:
- name: frontend
  project: frontend/frontend.csproj
  bindings:
  - port: 5000
- name: backend
  project: backend/backend.csproj
  replicas: 2
  bindings:
  - port: 3009
  liveness:
    http:
      path: /WeatherForecast/ProcessId
  readiness:
    http:
      path: /WeatherForecast/ProcessId
- name: redis
  image: redis
  bindings:
  - port: 6379
    connectionString: "${host}:${port}"

Do serwisu backendowego dodaliśmy informację, z jakich punktów wejściowych powinien skorzystać Tye, w celu walidacji stanu serwisu. Aktualnie po zatrzymaniu kontenera stan serwisu przejdzie do niepoprawnego, dzięki czemu Tye będzie wiedział, że należy zrestartować kontener. Spróbujmy! Po zatrzymaniu kontenera zobaczymy w logach:

[16:43:25 INF] Killing replica backend_7399485d-c because it has failed the liveness probe
[16:43:25 INF] docker logs collection for backend_7399485d-c complete with exit code 0
[16:43:26 INF] Collecting docker logs for backend_7399485d-c.
[16:43:27 INF] Replica backend_7399485d-c is moving to an healthy state
[16:43:29 INF] Replica backend_7399485d-c is moving to a ready state

Wszystko poszło zgodnie z planem!

Podsumowanie

Tye jest ciągle projektem eksperymentalnym, jednak już teraz oferuje ciekawe możliwości. Jestem przekonany, że uważnie będę śledził losy tego projektu, jak i używał go przy lokalnym testowaniu aplikacji mikroserwisowych. Na pewno narzędzie to ma wielki potencjał oraz, moim zdaniem, niższy próg wejścia niż podobne technologie. Mam nadzieję, że Wam też się spodobało! Dajcie znać co myślicie!

Ten artykuł dotyczył pierwszego celu projektu Tye – ułatwieniu programowania. Drugi cel dotyczy wdrażania, więc w jednym z kolejnych wpisów spróbujemy wdrożyć nasz system przy pomocy Tye oraz Kubernetesa! Chcecie przeczytać wpis na ten temat?

Niezależnie, bardzo Wam dziękuję, że przeczytaliście ten długi wpis! Jeśli dotrwaliście do końca, to jesteście wielcy. Dodatkowo, jeśli przy okazji robiliście wszystko razem ze mną, to jest mi z tego powodu bardzo miło! Dajcie znać, czy udało Wam się dotrwać do końca, czy napotkaliście jakieś problemy? Z chęcią zbiorę każdy feedback w celu poprawy kolejnych wpisów o podobnym stylu! Możecie napisać zarówno w komentarzu jak i wysłać mail na adres: admin@cesarstwo-dev.pl. A jeśli macie jeszcze siłę, to może chcecie poczytać o różnych architekturach? Jeśli tak to zapraszam tutaj!

Wpis został napisany z pomocą tych dwóch artykułów: Enjoy Local Development with Tye, Docker, and .NET oraz Introducing Project Tye.

Na koniec mały bonus! Plik tye.yaml rozszerzonej wersji powyższego systemu. Do powyższych serwisów doszły: redis-cli – do monitoringu redisa, rabbitmq – instancja systemu kolejkowego RabbitMq, worker – aplikacja konsolowa odbierająca wiadomości z kolejki.

# tye application configuration file
# read all about it at https://github.com/dotnet/tye
#
# when you've given us a try, we'd love to know what you think:
#    https://aka.ms/AA7q20u
#
name: tyetest
services:
- name: worker
  project: worker/worker.csproj
- name: frontend
  project: frontend/frontend.csproj
  bindings:
  - port: 5000
- name: backend
  project: backend/backend.csproj
  replicas: 3
  liveness:
    http:
      path: /WeatherForecast
  readiness:
    http:
      path: /WeatherForecast
  bindings:
  - port: 3009
- name: redis
  image: redis
  bindings:
  - port: 6379
    connectionString: "${host}:${port}"
- name: redis-cli
  image: redis
  args: "redis-cli -h redis MONITOR"
- name: rabbit
  image: rabbitmq:3-management
  bindings:
    - name: amqp
      protocol: amqp
      port: 5672
    - name: ui
      protocol: http
      port: 15672
      containerPort: 15672

Dołączam również kod aplikacji konsolowej.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

Console.WriteLine("Worker app");

Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =>
    {
        services.AddHostedService<QueueWorker>();
    })
    .Build()
    .Run();

public sealed class QueueWorker : BackgroundService
{
    private readonly IConfiguration _configuration;
    private readonly ILogger _logger;

    public QueueWorker(IConfiguration configuration, ILogger<QueueWorker> logger)
    {
        _configuration = configuration;
        _logger = logger;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var uri = _configuration.GetServiceUri("rabbit", "amqp");
        var endpoint = new AmqpTcpEndpoint(uri);

        var factory = new ConnectionFactory()
        {
            Endpoint = endpoint,
        };

        var connection = factory.CreateConnection();
        var model = connection.CreateModel();
        model.QueueDeclare(
                    queue: "weather",
                    durable: false,
                    exclusive: false,
                    autoDelete: false,
                    arguments: null);

        var consumer = new EventingBasicConsumer(model);
        consumer.Received += (model, ea) =>
        {
            var text = Encoding.UTF8.GetString(ea.Body.ToArray());
            _logger.LogInformation("Dequeued: {message}", text);
        };

        model.BasicConsume(
            queue: "weather",
            autoAck: true,
            consumer: consumer);

        return Task.CompletedTask;
    }
}

3 Komentarze

gdn · 2020-11-19 o 13:30

Z jednej strony wygląda bardzo atrakcyjnie, bo ułatwia budowanie rozwiązań mikroserwisowych, ale moim zdaniem w mikroserwisach, chyba bardziej chodzi o niezależność komponentów, tak by każdy mógł być rozwijany osobno, przez różne zespoły, bo inaczej to będziemy mieli poszatkowany monolit. Tye sprawdzi się dla kilku komponentów, ale ich będzie 20, to ścisłe powiązania utrudnią rozwijanie systemu.

    Cesarz · 2020-11-19 o 13:48

    Cześć! Wydaje mi się, że Tye nie zmniejsza niezależności komponentów. Zapewnia mechanizmy szeroko stosowane w świecie mikroserwisów, takie jak Service Discovery. Co masz na myśli mówiąc o zmniejszaniu niezależności komponentów? Niemniej, zgodzę się ze stwierdzeniem, że Tye najlepiej się sprawdzi dla małej liczby serwisów (<20).
    Dzięki za komentarz i pozdrawiam!

dotnetomaniak.pl · 2020-11-15 o 17:33

Project Tye – ułatwiona implementacja mikroserwisów

Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *