项目

将 API 网关从 Ocelot 迁移到 YARP

本指南提供了将您现有的微服务应用程序的 API 网关从 Ocelot 迁移到 YARP 的指导。由于 YARP 在 ABP v8.0+ 中可用,您需要更新现有的应用程序才能应用 YARP 更改。

历史背景

在此版本之前,ABP 在微服务启动模板 中使用 Ocelot 作为 API 网关。由于 Ocelot 库没有得到积极维护,我们寻找了替代方案,并决定将 API 网关从 Ocelot 切换到 YARP。YARP 由 Microsoft 维护,正在积极开发中,似乎是比 Ocelot 更好的替代方案,并提供相同的功能集甚至更多。

YARP 迁移步骤

您需要更新不同项目中的各种文件,以将您的 API 网关从 Ocelot 升级到 YARP。所有必需的更改均在以下不同部分列出,请按照以下步骤从 Ocelot 升级到 YARP。

或者,您可以创建一个微服务启动模板,并将您的更改与微服务模板进行比较。这是确保所有必需更改都已完成的好方法。

共享主机网关项目

  • 移除 Ocelot 包并添加 YARP 包,如下所示:
    <ItemGroup>
-        <PackageReference Include="Ocelot" Version="17.0.0" />
-        <PackageReference Include="Ocelot.Provider.Polly" Version="17.0.0" />
+        <PackageReference Include="Yarp.ReverseProxy" Version="2.0.0" />
    </ItemGroup>
  • 打开 *SharedHostingGatewaysModule.cs 并进行以下更改:
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        var configuration = context.Services.GetConfiguration();
        var env = context.Services.GetHostingEnvironment();

-      var ocelotBuilder = context.Services.AddOcelot(configuration)
-           .AddPolly();

-       if (!env.IsProduction())
-       {
-           ocelotBuilder.AddDelegatingHandler<AbpRemoveCsrfCookieHandler>(true);
-       }

+       context.Services.AddReverseProxy()
+           .LoadFromConfig(configuration.GetSection("ReverseProxy"));
    }
  • 按如下所示更新 GatewayHostBuilderExtensions.cs 文件:
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.Hosting;

public static class AbpHostingHostBuilderExtensions
{
    public const string AppYarpJsonPath = "yarp.json";

    public static IHostBuilder AddYarpJson(
        this IHostBuilder hostBuilder,
        bool optional = true,
        bool reloadOnChange = true,
        string path = AppYarpJsonPath)
    {
        return hostBuilder.ConfigureAppConfiguration((_, builder) =>
        {
            builder.AddJsonFile(
                path: AppYarpJsonPath,
                optional: optional,
                reloadOnChange: reloadOnChange
            );
        });
    }
}
  • 从解决方案中删除 OcelotConfiguration.cs 文件。

公共 Web 网关项目

  • 删除 ocelot.json 文件,并创建一个新的 yarp.json 文件,并按如下所示更新其内容(在 PublicWebGateway 项目的根目录下):
{
  "ReverseProxy": {
    "Routes": {
      "AbpApi": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/api/abp/{**catch-all}"
        }
      },
      "AdministrationSwagger": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/swagger-json/Administration/swagger/v1/swagger.json"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/swagger-json/Administration"
          }
        ]
      },
      "Account": {
        "ClusterId": "AuthServer",
        "Match": {
          "Path": "/api/account/{**catch-all}"
        }
      },
      "AuthServerSwagger": {
        "ClusterId": "AuthServer",
        "Match": {
          "Path": "/swagger-json/AuthServer/swagger/v1/swagger.json"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/swagger-json/AuthServer"
          }
        ]
      },
      "Product": {
        "ClusterId": "Product",
        "Match": {
          "Path": "/api/product-service/{**catch-all}"
        }
      },
      "ProductSwagger": {
        "ClusterId": "Product",
        "Match": {
          "Path": "/swagger-json/Product/swagger/v1/swagger.json"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/swagger-json/Product"
          }
        ]
      }
    },
    "Clusters": {
      "AuthServer": {
        "Destinations": {
          "AuthServer": {
            "Address": "https://localhost:44322/"
          }
        }
      },
      "Administration": {
        "Destinations": {
          "Administration": {
            "Address": "https://localhost:44367/"
          }
        }
      },
      "Product": {
        "Destinations": {
          "Product": {
            "Address": "https://localhost:44361/"
          }
        }
      }
    }
  }
}
  • 打开 Program.cs 文件并进行以下更改:
    builder.Host
        .AddAppSettingsSecretsJson()
-       .AddOcelotJson()
+       .AddYarpJson()
        .UseAutofac()
        .UseSerilog();
  • 打开 PublicWebGateway 项目的模块类,并按如下所示更新 OnApplicationInitialization 方法:
+   using Yarp.ReverseProxy.Configuration;

    //其他配置...

    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();
        var env = context.GetEnvironment();
        var configuration = context.GetConfiguration();
+       var proxyConfig = app.ApplicationServices.GetRequiredService<IProxyConfigProvider>().GetConfig();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseCorrelationId();
        app.UseStaticFiles();
+       app.UseCors();
+       app.UseRouting();
+       app.UseAuthorization();
        app.UseSwagger();
+       app.UseAbpSwaggerUI(options => { ConfigureSwaggerUI(proxyConfig, options, configuration); });
        app.UseAbpSerilogEnrichers();
+       app.UseRewriter(CreateSwaggerRewriteOptions());
        app.UseEndpoints(endpoints => endpoints.MapReverseProxy());
    }

+    private static void ConfigureSwaggerUI(
+        IProxyConfig proxyConfig,
+        SwaggerUIOptions options,
+        IConfiguration configuration)
+    {
+        foreach (var cluster in proxyConfig.Clusters)
+        {
+            options.SwaggerEndpoint($"/swagger-json/{cluster.ClusterId}/swagger/v1/swagger.json", $"{cluster.ClusterId} API");
+        }

+        options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]);
+        options.OAuthScopes(
+            "AdministrationService",
+            "AccountService",
+            "ProductService"
+        );
+    }

+    private static RewriteOptions CreateSwaggerRewriteOptions()
+    {
+        var rewriteOptions = new RewriteOptions();
+        rewriteOptions.AddRedirect("^(|\\|\\s+)$", "/swagger"); // 用于 "/" 和 ""(空白)的正则表达式
+        return rewriteOptions;
+    }

Web 网关项目

  • 删除 ocelot.json 文件,并创建一个新的 yarp.json 文件,并按如下所示更新其内容(在 WebGateway 项目的根目录下):
{
  "ReverseProxy": {
    "Routes": {
      "AbpApi": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/api/abp/{**catch-all}"
        }
      },
      "SettingManagement": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/api/setting-management/{**catch-all}"
        }
      },
      "FeatureManagement": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/api/feature-management/{**catch-all}"
        }
      },
      "PermissionManagement": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/api/permission-management/{**catch-all}"
        }
      },
      "AuditLogging": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/api/audit-logging/{**catch-all}"
        }
      },
      "LanguageManagement": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/api/language-management/{**catch-all}"
        }
      },
      "TextTemplateManagement": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/api/text-template-management/{**catch-all}"
        }
      },
      "LeptonThemeManagement": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/api/lepton-theme-management/{**catch-all}"
        }
      },
      "GDPR": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/api/gdpr/{**catch-all}"
        }
      },
      "AdministrationSwagger": {
        "ClusterId": "Administration",
        "Match": {
          "Path": "/swagger-json/Administration/swagger/v1/swagger.json"
        },
        "Transforms": [
          { "PathRemovePrefix": "/swagger-json/Administration" }
        ]
      },
      "Account": {
        "ClusterId": "AuthServer",
        "Match": {
          "Path": "/api/account/{**catch-all}"
        }
      },
      "AccountAdmin": {
        "ClusterId": "AuthServer",
        "Match": {
          "Path": "/api/account-admin/{**catch-all}"
        }
      },
      "AuthServerSwagger": {
        "ClusterId": "AuthServer",
        "Match": {
          "Path": "/swagger-json/AuthServer/swagger/v1/swagger.json"
        },
        "Transforms": [
          { "PathRemovePrefix": "/swagger-json/AuthServer" }
        ]
      },
      "Identity": {
        "ClusterId": "Identity",
        "Match": {
          "Path": "/api/identity/{**catch-all}"
        }
      },
      "OpenIddict": {
        "ClusterId": "Identity",
        "Match": {
          "Path": "/api/openiddict/{**catch-all}"
        }
      },
      "IdentitySwagger": {
        "ClusterId": "Identity",
        "Match": {
          "Path": "/swagger-json/Identity/swagger/v1/swagger.json"
        },
        "Transforms": [
          { "PathRemovePrefix": "/swagger-json/Identity" }
        ]
      },
      "Saas": {
        "ClusterId": "Saas",
        "Match": {
          "Path": "/api/saas/{**catch-all}"
        }
      },
      "Payment": {
        "ClusterId": "Saas",
        "Match": {
          "Path": "/api/payment/{**catch-all}"
        }
      },
      "PaymentAdmin": {
        "ClusterId": "Saas",
        "Match": {
          "Path": "/api/payment-admin/{**catch-all}"
        }
      },
      "SaasSwagger": {
        "ClusterId": "Saas",
        "Match": {
          "Path": "/swagger-json/Saas/swagger/v1/swagger.json"
        },
        "Transforms": [
          { "PathRemovePrefix": "/swagger-json/Saas" }
        ]
      },
      "Product": {
        "ClusterId": "Product",
        "Match": {
          "Path": "/api/product-service/{**catch-all}"
        }
      },
      "ProductSwagger": {
        "ClusterId": "Product",
        "Match": {
          "Path": "/swagger-json/Product/swagger/v1/swagger.json"
        },
        "Transforms": [
          { "PathRemovePrefix": "/swagger-json/Product" }
        ]
      }
    },
    "Clusters": {
      "AuthServer": {
        "Destinations": {
          "AuthServer": {
            "Address": "https://localhost:44322/"
          }
        }
      },
      "Administration": {
        "Destinations": {
          "Administration": {
            "Address": "https://localhost:44367/"
          }
        }
      },
      "Identity": {
        "Destinations": {
          "Identity": {
            "Address": "https://localhost:44388/"
          }
        }
      },
      "Saas": {
        "Destinations": {
          "Saas": {
            "Address": "https://localhost:44381/"
          }
        }
      },
      "Product": {
        "Destinations": {
          "Product": {
            "Address": "https://localhost:44361/"
          }
        }
      }
    }
  }
}
  • 打开 Program.cs 文件并进行以下更改:
    builder.Host
        .AddAppSettingsSecretsJson()
-       .AddOcelotJson()
+       .AddYarpJson()
        .UseAutofac()
        .UseSerilog();
  • 打开 WebGateway 项目的模块类,并按如下所示更新 OnApplicationInitialization 方法:
+   using Yarp.ReverseProxy.Configuration;

    //其他配置...

    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();
        var env = context.GetEnvironment();
        var configuration = context.GetConfiguration();
+       var proxyConfig = app.ApplicationServices.GetRequiredService<IProxyConfigProvider>().GetConfig();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseCorrelationId();
        app.UseStaticFiles();
+       app.UseCors();
+       app.UseRouting();
+       app.UseAuthorization();
        app.UseSwagger();
+       app.UseAbpSwaggerUI(options => { ConfigureSwaggerUI(proxyConfig, options, configuration); });
        app.UseAbpSerilogEnrichers();
+       app.UseRewriter(CreateSwaggerRewriteOptions());
        app.UseEndpoints(endpoints => endpoints.MapReverseProxy());
    }

+    private static void ConfigureSwaggerUI(
+        IProxyConfig proxyConfig,
+        SwaggerUIOptions options,
+        IConfiguration configuration)
+    {
+        foreach (var cluster in proxyConfig.Clusters)
+        {
+            options.SwaggerEndpoint($"/swagger-json/{cluster.ClusterId}/swagger/v1/swagger.json", $"{cluster.ClusterId} API");
+        }

+        options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]);
+        options.OAuthScopes(
+            "AdministrationService",
+            "AccountService",
+            "IdentityService",
+            "SaasService",
+            "ProductService"
+        );
+    }

+    private static RewriteOptions CreateSwaggerRewriteOptions()
+    {
+        var rewriteOptions = new RewriteOptions();
+        rewriteOptions.AddRedirect("^(|\\|\\s+)$", "/swagger"); // 用于 "/" 和 ""(空白)的正则表达式
+        return rewriteOptions;
+    }

图表更新

  • 按如下所示更新 gateway-web-public-configmap.yaml 文件(etc/k8s//charts/gateway-web-public/templates/gateway-web-public-configmap.yaml):
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}-configmap
data:
  yarp.json: |-
    {
      "ReverseProxy": {
        "Routes": {
          "AbpApi": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/api/abp/{**catch-all}"
            }
          },
          "AdministrationSwagger": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/swagger-json/Administration/swagger/v1/swagger.json"
            },
            "Transforms": [
              { "PathRemovePrefix": "/swagger-json/Administration" }
            ]
          },
          "Account": {
            "ClusterId": "AuthServer",
            "Match": {
              "Path": "/api/account/{**catch-all}"
            }
          },
          "AuthServerSwagger": {
            "ClusterId": "AuthServer",
            "Match": {
              "Path": "/swagger-json/AuthServer/swagger/v1/swagger.json"
            },
            "Transforms": [
              { "PathRemovePrefix": "/swagger-json/AuthServer" }
            ]
          },
          "Product": {
            "ClusterId": "Product",
            "Match": {
              "Path": "/api/product-service/{**catch-all}"
            }
          },
          "ProductSwagger": {
            "ClusterId": "Product",
            "Match": {
              "Path": "/swagger-json/Product/swagger/v1/swagger.json"
            },
            "Transforms": [
              { "PathRemovePrefix": "/swagger-json/Product" }
            ]
          }
        },
        "Clusters": {
          "AuthServer": {
            "Destinations": {
              "AuthServer": {
                "Address": "{{ .Values.reRoutes.accountService.url }}"
              }
            }
          },
          "Administration": {
            "Destinations": {
              "Administration": {
                "Address": "{{ .Values.reRoutes.administrationService.url }}"
              }
            }
          },
          "Product": {
            "Destinations": {
              "Product": {
                "Address": "{{ .Values.reRoutes.productService.url }}"
              }
            }
          }
        }
      }
    }
  • gateway-web-public-deployment.yaml 文件中进行以下更改(etc/k8s//charts/gateway-web-public/templates/gateway-web-public-deployment.yaml):
-          mountPath: /app/ocelot.json
+          mountPath: /app/yarp.json
-          subPath: ocelot.json
+          subPath: yarp.json
  • 按如下所示更新 values.yaml 文件(etc/k8s//charts/gateway-web-public/values.yaml):
config:
  selfUrl: # https://gateway-public.myprojectname.dev
  corsOrigins: # https://myprojectname-st-gateway-web,https://myprojectname-st-gateway-public-web
  authServer:
    authority: # http://myprojectname-st-authserver
    requireHttpsMetadata: # "false"
    metadataAddress: # https://authserver.myprojectname.dev/.well-known/openid-configuration
    swaggerClientId: # WebGateway_Swagger
  dotnetEnv: Staging
  redisHost: #
  rabbitmqHost: #
  elasticsearchUrl: #
  AbpLicenseCode: #

reRoutes:
  accountService:
    url: http://myprojectname-st-authserver
  saasService:
    url: http://saas-st-administration
  administrationService:
    url: http://myprojectname-st-administration
  productService:
    url: http://myprojectname-st-product

ingress:
  host: gateway-public.myprojectname.dev
  tlsSecret: myprojectname-tls

image:
  repository: mycompanyname/myprojectname-gateway-web-public
  tag: latest
  pullPolicy: IfNotPresent

env: {}
  • 按如下所示更新 gateway-web-configmap.yaml 文件(etc/k8s//charts/gateway-web/templates/gateway-web-configmap.yaml):
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}-configmap
data:
  yarp.json: |-
    {
      "ReverseProxy": {
        "Routes": {
          "AbpApi": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/api/abp/{**catch-all}"
            }
          },
          "SettingManagement": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/api/setting-management/{**catch-all}"
            }
          },
          "FeatureManagement": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/api/feature-management/{**catch-all}"
            }
          },
          "PermissionManagement": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/api/permission-management/{**catch-all}"
            }
          },
          "AuditLogging": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/api/audit-logging/{**catch-all}"
            }
          },
          "LanguageManagement": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/api/language-management/{**catch-all}"
            }
          },
          "TextTemplateManagement": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/api/text-template-management/{**catch-all}"
            }
          },
          "LeptonThemeManagement": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/api/lepton-theme-management/{**catch-all}"
            }
          },
          "GDPR": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/api/gdpr/{**catch-all}"
            }
          },
          "AdministrationSwagger": {
            "ClusterId": "Administration",
            "Match": {
              "Path": "/swagger-json/Administration/swagger/v1/swagger.json"
            },
            "Transforms": [
              { "PathRemovePrefix": "/swagger-json/Administration" }
            ]
          },
          "Account": {
            "ClusterId": "AuthServer",
            "Match": {
              "Path": "/api/account/{**catch-all}"
            }
          },
          "AccountAdmin": {
            "ClusterId": "AuthServer",
            "Match": {
              "Path": "/api/account-admin/{**catch-all}"
            }
          },
          "AuthServerSwagger": {
            "ClusterId": "AuthServer",
            "Match": {
              "Path": "/swagger-json/AuthServer/swagger/v1/swagger.json"
            },
            "Transforms": [
              { "PathRemovePrefix": "/swagger-json/AuthServer" }
            ]
          },
          "Identity": {
            "ClusterId": "Identity",
            "Match": {
              "Path": "/api/identity/{**catch-all}"
            }
          },
          "OpenIddict": {
            "ClusterId": "Identity",
            "Match": {
              "Path": "/api/openiddict/{**catch-all}"
            }
          },
          "IdentitySwagger": {
            "ClusterId": "Identity",
            "Match": {
              "Path": "/swagger-json/Identity/swagger/v1/swagger.json"
            },
            "Transforms": [
              { "PathRemovePrefix": "/swagger-json/Identity" }
            ]
          },
          "Saas": {
            "ClusterId": "Saas",
            "Match": {
              "Path": "/api/saas/{**catch-all}"
            }
          },
          "Payment": {
            "ClusterId": "Saas",
            "Match": {
              "Path": "/api/payment/{**catch-all}"
            }
          },
          "PaymentAdmin": {
            "ClusterId": "Saas",
            "Match": {
              "Path": "/api/payment-admin/{**catch-all}"
            }
          },
          "SaasSwagger": {
            "ClusterId": "Saas",
            "Match": {
              "Path": "/swagger-json/Saas/swagger/v1/swagger.json"
            },
            "Transforms": [
              { "PathRemovePrefix": "/swagger-json/Saas" }
            ]
          },
          "Product": {
            "ClusterId": "Product",
            "Match": {
              "Path": "/api/product-service/{**catch-all}"
            }
          },
          "ProductSwagger": {
            "ClusterId": "Product",
            "Match": {
              "Path": "/swagger-json/Product/swagger/v1/swagger.json"
            },
            "Transforms": [
              { "PathRemovePrefix": "/swagger-json/Product" }
            ]
          }
        },
        "Clusters": {
          "AuthServer": {
            "Destinations": {
              "AuthServer": {
                "Address": "{{ .Values.reRoutes.accountService.url }}"
              }
            }
          },
          "Administration": {
            "Destinations": {
              "Administration": {
                "Address": "{{ .Values.reRoutes.administrationService.url }}"
              }
            }
          },
          "Identity": {
            "Destinations": {
              "Identity": {
                "Address": "{{ .Values.reRoutes.identityService.url }}"
              }
            }
          },
          "Saas": {
            "Destinations": {
              "Saas": {
                "Address": "{{ .Values.reRoutes.saasService.url }}"
              }
            }
          },
          "Product": {
            "Destinations": {
              "Product": {
                "Address": "{{ .Values.reRoutes.productService.url }}"
              }
            }
          }
        }
      }
    }
  • gateway-web-deployment.yaml 文件中进行以下更改(etc/k8s//charts/gateway-web/templates/gateway-web-deployment.yaml):
-          mountPath: /app/ocelot.json
+          mountPath: /app/yarp.json
-          subPath: ocelot.json
+          subPath: yarp.json
  • 按如下所示更新 values.yaml 文件(etc/k8s//charts/gateway-web/values.yaml):
config:
  selfUrl: # https://gateway-web.myprojectname.dev
  corsOrigins: # https://myprojectname-st-angular
  globalConfigurationBaseUrl: # http://myprojectname-st-gateway-web
  authServer:
    authority: # http://myprojectname-st-authserver
    requireHttpsMetadata: # "false"
    metadataAddress: # https://authserver.myprojectname.dev/.well-known/openid-configuration
    swaggerClientId: # WebGateway_Swagger
  dotnetEnv: #
  redisHost: #
  rabbitmqHost: #
  elasticsearchUrl: #
  AbpLicenseCode: #

reRoutes:
  accountService:
    url: http://myprojectname-st-authserver
  saasService:
    url: http://saas-st-administration
  administrationService:
    url: http://myprojectname-st-administration
  identityService:
    url: http://myprojectname-st-identity
  productService:
    url: http://myprojectname-st-product
ingress:
  host: # gateway-web.myprojectname.dev
  tlsSecret: myprojectname-tls

image:
  repository: mycompanyname/myprojectname-gateway-web
  tag: latest
  pullPolicy: IfNotPresent

env: {}
  • 按如下所示更新 values.yaml 文件(etc/k8s//values.yaml):
- globalConfigurationBaseUrl: http://myprojectname-st-gateway-web

reRoutes:
    accountService:
+      url: http://myprojectname-st-authserver
-      dns: https://authserver.myprojectname.dev
-      schema: http
-      host: myprojectname-st-authserver
-      port: 80
    identityService:
+      url: http://myprojectname-st-identity
-      dns: https://identity.myprojectname.dev
-      schema: http
-      host: myprojectname-st-identity
-      port: 80
    administrationService:
+      url: http://myprojectname-st-administration
-      dns: https://administration.myprojectname.dev
-      schema: http
-      host: myprojectname-st-administration
-      port: 80
    saasService:
+      url: http://myprojectname-st-saas
-      dns: https://saas.myprojectname.dev
-      schema: http
-      host: myprojectname-st-saas
-      port: 80
    productService:
+      url: http://myprojectname-st-product
-      dns: https://product.myprojectname.dev
-      schema: http
-      host: myprojectname-st-product
-      port: 80

另请参阅

在本文档中