﻿# 使用 Tye 開發工具便利的開發微服務應用程式並部署到 Kubernetes 叢集

微服務是近期相當熱門的主題之一，而在 .NET 領域中，微軟也投入了不少資源進行研究，也推出了許多鮮為人知的好用工具，這篇文章我將介紹 Tye 這兩套微服務開發工具，讓你知道原來用 .NET 開發微服務也可以這麼簡單。

以下我使用 .NET Core 3.1 為基礎進行示範。

### 示範微服務專案建立的過程

1.  先透過 `global.json` 鎖定 .NET Core SDK 版本
    
    ```bash
    dotnet new globaljson --sdk-version 3.1.413
    ```
    
2.  安裝 `tye` 命令列工具 ([Microsoft.Tye](https://www.nuget.org/packages/Microsoft.Tye))
    
    ```bash
    dotnet tool update -g Microsoft.Tye --version "0.10.0-alpha.21420.1"
    ```
    
3.  建立你的第一個微服務：前端網頁
    
    ```bash
    mkdir microservice && cd microservice
    dotnet new razor -n frontend
    ```
    
4.  啟動你的微服務系統（目前只有一個網站）
    
    ```bash
    tye run frontend
    ```
    
    你可以連到 [http://127.0.0.1:8000/](http://127.0.0.1:8000/) 開啟 **Tye Dashboard** 主控台頁面，查看微服務架構下的所有服務運作資訊，真是無腦又方便！
    
    ![Tye Dashboard](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/10957c4086e616a18053-134469571-b8c96ffd-9184-4535-98ac-d0687e64e72f.png)
    
    畫面上的 **Bindings** 欄位，紀錄著該「微服務」的端點，你可以直接透過這些網址連線到該服務，預設他會亂數設定一個可用的 Ports 給你，每次都會不一樣！
    
5.  建立你的第二個微服務：後端 API 專案
    
    ```bash
    dotnet new webapi -n backend
    ```
    
6.  啟動你的微服務系統（目前有 2 個網站）
    
    由於要透過 `tye` 快速啟動多個專案，因此建議建立一個方案檔，方便後續執行！
    
    ```bash
    dotnet new sln
    dotnet sln add frontend backend
    ```
    
    此時執行 `tye run` 就會讀取 `microservice.sln` 方案檔，一次啟動兩個站台：
    
    ```bash
    tye run
    ```
    
    你可以連到 [http://127.0.0.1:8000/](http://127.0.0.1:8000/) 開啟 **Tye Dashboard** 主控台頁面，查看微服務架構下的所有服務運作資訊！
    
    ![Tye Dashboard](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/cc5aed64bfafd689c00a-134469450-3a9d7b8f-7236-4f53-95b2-399721bdff52.png)
    

### 示範如何從「前端服務」呼叫「後端服務」的 Web API

1.  在 `frontend` 專案加入 `WeatherForecast.cs`
    
    這個 `WeatherForecast` 類別跟後端的 `WeatherForecast` 類別是完全一樣的！
    
    ```csharp
    using System;
    
    namespace frontend
    {
        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; }
        }
    }
    ```
    
2.  在 `frontend` 專案加入 `WeatherClient.cs`
    
    在開發 `WeatherClient` 的時候，完全不需要知道後端的位址！
    
    ```csharp
    using System.Net.Http;
    using System.Text.Json;
    using System.Threading.Tasks;
    
    namespace frontend
    {
        public class WeatherClient
        {
            private readonly JsonSerializerOptions options = new JsonSerializerOptions()
            {
                PropertyNameCaseInsensitive = true,
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            };
    
            private readonly HttpClient client;
    
            public WeatherClient(HttpClient client)
            {
                this.client = client;
            }
    
            public async Task<WeatherForecast[]> GetWeatherAsync()
            {
                var responseMessage = await this.client.GetAsync("/weatherforecast");
                var stream = await responseMessage.Content.ReadAsStreamAsync();
                return await JsonSerializer.DeserializeAsync<WeatherForecast[]>(stream, options);
            }
        }
    }
    ```
    
3.  在 `frontend` 專案加入 [Microsoft.Tye.Extensions.Configuration](https://www.nuget.org/packages/Microsoft.Tye.Extensions.Configuration) 套件
    
    ```bash
    dotnet add frontend/frontend.csproj package Microsoft.Tye.Extensions.Configuration  --version "0.4.0-*"
    ```
    
4.  在 `frontend` 專案調整 `Startup.cs` 相依注入
    
    這裡最酷的地方，就是在 [Tye](https://github.com/dotnet/tye) 的協助下，開發人員在寫 Code 的時候只需要知道**服務名稱**(`backend`)就好，而不需要知道**微服務的確切網址** (端點)！
    
    ```csharp
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    
        // 註冊 WeatherClient 服務
        services.AddHttpClient<WeatherClient>(client =>
        {
            client.BaseAddress = Configuration.GetServiceUri("backend");
        });
    }
    ```
    
    > 上述程式的 `Configuration.GetServiceUri()` API 其實是一個 [Microsoft.Tye.Extensions.Configuration](https://www.nuget.org/packages/Microsoft.Tye.Extensions.Configuration) 套件中提供的 C# 擴充方法，他會自動讀入 Tye 傳到應用程式的組態，技術細節不是特別重要，重要的是你可以藉此取得微服務的 Uri 位址。其實這個實作非常簡單，直接看[原始碼](https://github.com/dotnet/tye/blob/main/src/Microsoft.Tye.Extensions.Configuration/TyeConfigurationExtensions.cs#L13-L32)就可以得到相當清楚的解答！
    
5.  在 `frontend` 專案調整 `Pages\Index.cshtml.cs`
    
    ```csharp
    public WeatherForecast[] Forecasts { get; set; }
    
    public async Task OnGet([FromServices]WeatherClient client)
    {
        Forecasts = await client.GetWeatherAsync();
    }
    ```
    
6.  在 `frontend` 專案調整 `Pages\Index.cshtml`
    
    ```html
    @page
    @model IndexModel
    @{
        ViewData["Title"] = "Home page";
    }
    
    <div class="text-center">
        <h1 class="display-4">Welcome</h1>
        <p>Learn about <a href="https://learn.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>
    ```
    

-   重新執行 Tye
    
    ```bash
    tye run
    ```
    
    ![tye run](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/c33d6e23a4144f5392bd-134475982-aeff9416-923a-4128-8e78-37311187e292.png)
    
    你可以連到 [http://127.0.0.1:8000/](http://127.0.0.1:8000/) 開啟 **Tye Dashboard** 主控台頁面，並且透過 `frontend` 微服務的 **Bindings** 網址瀏覽到前端網站，前端程式也會呼叫後端微服務的 API 並取得資料！
    

### 初始化 Tye 開發環境

由於每次執行 `tye run` 都會替每一個微服務產生一個亂數的 Ports，其實還挺困擾的，如果想要固定開發環境的參數，就要初始化 Tye 組態設定，將這些參數都固定下來。

1.  初始化 `tye.yaml` 設定檔
    
    ```bash
    tye init
    ```
    
    ```yaml
    name: microservice
    services:
    - name: frontend
      project: frontend/frontend.csproj
    - name: backend
      project: backend/backend.csproj
    ```
    
2.  替需要固定端點的服務加上 Bindings 資訊
    
    ```yaml
    name: microservice
    services:
    - name: frontend
      project: frontend/frontend.csproj
      bindings:
        - name: http
          protocol: http
          port: 16000
        - name: https
          protocol: https
          port: 16443
    - name: backend
      project: backend/backend.csproj
    ```
    
3.  重新執行 Tye
    
    ```bash
    tye run
    ```
    
    > 請注意：如果你替每一個服務設定 bindings 的話，程式需要做出微調！
    
    ```yaml
    name: microservice
    services:
    - name: frontend
      project: frontend/frontend.csproj
      bindings:
      - name: http
        protocol: http
        port: 16000
      - name: https
        protocol: https
        port: 16443
    - name: backend
      project: backend/backend.csproj
      bindings:
      - name: http
        protocol: http
        port: 17000
      - name: https
        protocol: https
        port: 17443
    ```
    
    > 你在連接後端時，因為有設定 bindings 的關係，你使用 `Configuration.GetServiceUri()` 取得網址時，必須也加上 `服務名稱:繫結名稱` (例如: `backend:http`) 才能得到正確的網址！
    
    ```csharp
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    
        // 註冊 WeatherClient 服務
        services.AddHttpClient<WeatherClient>(client =>
        {
            client.BaseAddress = Configuration.GetServiceUri("backend:http");
        });
    }
    ```
    

### 加入 SQL Server 資料庫到開發環境

在初始化 `tye.yaml` 設定檔之後，你就可以像 Docker Compose 那樣，在一個 YAML 檔案中設定多個不同服務，而且也可以設定 Docker 容器執行！

以下我就來示範如何在現有的 Tye 微服務方案中加入測試用的 SQL Server 資料庫：

1.  加入 `db` 服務，並透過 `image:` 執行一個 Docker 容器
    
    ```yaml
    - name: db
      image: mcr.microsoft.com/mssql/server:2019-latest
      env:
      - name: SA_PASSWORD
        value: "Ver7CompleXPW"
      - name: ACCEPT_EULA
        value: "Y"
      bindings:
      - port: 1433
        connectionString: "Server=${host},${port};Database=MyDB;User Id=sa;Password=Ver7CompleXPW;MultipleActiveResultSets=true"
    ```
    
    > 這裡我們將 `connectionString` 放在 `db` 服務下，以便其他服務可以取用這個連接字串！
    
2.  測試從 `backend` 服務取得連接字串
    
    以下就是從 `db` 服務取得連接字串的方法：
    
    ```bash
    Configuration.GetConnectionString("db")
    ```
    
    > 這個 `GetConnectionString()` 擴充方法的原始碼在此：[tye/TyeConfigurationExtensions.cs at main · dotnet/tye](https://github.com/dotnet/tye/blob/main/src/Microsoft.Tye.Extensions.Configuration/TyeConfigurationExtensions.cs#L34-L50)
    
3.  重新執行 Tye
    
    ```bash
    tye run
    ```
    
    > 如果 SQL Server 容器無法啟動，請嘗試執行 `net stop winnat && net start winnat` 並再重新執行一次。
    
    ![Tye Dashboard](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/50c87dcc4164eaaede08-134502716-c69cf160-959b-4913-8a29-11e430ca81b5.png)
    
    > 這裡有個地方蠻有趣的，那就是我們的 `frontend` 與 `backend` 微服務其實是跑在 **本機** (Local Machine)，而 `db` 卻是跑在容器中。除此之外，Tye 還另外跑了兩個容器，一個名為 `frontend-proxy_6bc59e6c-1`，另一個名為 `backend-proxy_a7e2c2bc-1`，這兩個容器其實是一個 **[Microsoft.Tye.Proxy](https://github.com/dotnet/tye/blob/main/src/Microsoft.Tye.Proxy/Program.cs)** (代理伺服器)，但是這時其實是完全用不上的，因為我們的程式完全跑在本機，不需要透過這兩個容器(代理伺服器)進行 HTTP 轉發！
    

### 部署微服務應用程式到 Kubernetes

1.  啟用 Docker Desktop 的 Kubernetes 功能
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/f18d805c95afc7a4379f-134526497-25391863-d747-401c-8469-1879f0b54917.png)
    
2.  建置容器映像
    
    ```bash
    tye build
    ```
    
3.  設定 registry 位址 (`tye.yaml`)
    
    我們只要加上 `registry: willh` 就可以將建置好的 image 自動 push 到 Docker Hub 的 `willh` 帳號下。
    
    如果你要推送到自架的 Registry 的話，直接設定主機名稱即可，例如：`willh.azureacr.io`
    
    ```bash
    name: microservice
    registry: willh
    services:
    - name: frontend
      project: frontend/frontend.csproj
      bindings:
      - name: http
        protocol: http
        port: 16000
      - name: https
        protocol: https
        port: 16443
    - name: backend
      project: backend/backend.csproj
    ```
    
4.  部署至 Kubernetes 叢集
    
    請先確認 `kubectl` 可以成功連至 Kubernetes 叢集
    
    ```bash
    tye deploy
    ```
    
5.  設定 Port Forwarding 讓本機可以連線測試
    
    以下命令 `8080` 是你本機要使用的 Port，而 `16000` 則是你在 `tye.yaml` 設定的服務 Port
    
    ```bash
    kubectl port-forward services/frontend 8080:16000 -n default
    ```
    
6.  從 Kubernetes 叢集移除已部署的應用程式
    
    ```bash
    tye undeploy
    ```
    

### 這個簡單的實作學習到了些什麼

首先，透過 Tye 可以非常便利的幫助你管理多個不同的微服務，你不用特別在意網址的部分，只需要知道**服務名稱**即可完成服務間通訊！

上述例子，你會發現後端 API 的微服務什麼事都不用做，基本上開發完成之後就等著被呼叫就好。那是因為你的 .NET 應用程式其實是透過 Tye 啟動的，而在啟動的時候 Tye 會自動傳入幾個重要的組態設定 (透過環境變數傳入)，因此你可以在開發微服務的時候不需要知道這些細節，專心把服務開發好即可。

以下是我在 Razor 頁面中顯示目前微服務取得的所有環境變數的程式碼片段：

```html
@{
    var envs = Environment.GetEnvironmentVariables();
    var sorted = from key in envs.Keys.Cast<string>() orderby key select key;
}

<table class="table table-bordered">
@foreach (string envName in sorted)
{
    <tr>
        <td>@envName</td>
        <td>@envs[envName]</td>
    </tr>
}
</table>
```

最後，Tye 自動整合了 Kubernetes 的部署能力，真的是開發人員的好朋友，你幾乎可以在不瞭解 Docker 或 Kubernetes 的情況下，就可以完成本機開發與 Kubernetes 叢集部署，相當便利！ 👍

### 相關連結

-   [dotnet/tye](https://github.com/dotnet/tye)
    -   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 documentation](https://github.com/dotnet/tye/blob/main/docs/README.md)
    -   [Getting Started](https://github.com/dotnet/tye/blob/main/docs/getting_started.md)
    -   [Basic Tutorial](https://github.com/dotnet/tye/blob/main/docs/tutorials/hello-tye/00_run_locally.md)
