本文发布已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。

使用开源 Gloo 进行两阶段金丝雀发布

作者: Rick Ducott | GitHub | Twitter

每天,我和我的同事都在与平台所有者、架构师和工程师交谈,他们正在使用 Gloo 作为 API 网关,将其应用程序暴露给最终用户。这些应用程序可能跨越遗留的单体应用、微服务、托管云服务和 Kubernetes 集群。幸运的是,Gloo 可以轻松设置路由来管理、保护和观察应用程序流量,同时支持灵活的部署架构,以满足我们用户不同的生产需求。

除了初始设置之外,平台所有者经常要求我们帮助设计其组织内的操作工作流程:我们如何使新应用程序上线?我们如何升级应用程序?我们如何在我们的平台、运维和开发团队之间分配职责?

在这篇文章中,我们将使用 Gloo 来设计一个用于应用程序升级的两阶段金丝雀发布工作流程

  • 在第一阶段,我们将通过将一小部分流量转移到新版本来进行金丝雀测试。这使您可以安全地执行冒烟测试和正确性测试。
  • 在第二阶段,我们将逐步将流量转移到新版本,从而使我们能够在负载下监控新版本,并最终停用旧版本。

为了简单起见,我们将专注于使用 开源 Gloo 设计工作流程,并且我们将把网关和应用程序部署到 Kubernetes。最后,我们将讨论一些扩展和高级主题,这些主题可能在后续跟进中值得探索。

初始设置

首先,我们需要一个 Kubernetes 集群。此示例不利用任何云特定的功能,并且可以针对本地测试集群(例如 minikube)运行。本文假设对 Kubernetes 以及如何使用 kubectl 与之交互有基本的了解。

我们将最新的 开源 Gloo 安装到 gloo-system 命名空间,并将示例应用程序的 v1 版本部署到 echo 命名空间。我们将通过在 Gloo 中创建一个路由来将此应用程序暴露在集群外部,最终得到如下所示的图片

Setup

部署 Gloo

我们将使用 glooctl 命令行工具安装 gloo,我们可以下载该工具并使用以下命令将其添加到 PATH

curl -sL https://run.solo.io/gloo/install | sh
export PATH=$HOME/.gloo/bin:$PATH

现在,您应该能够运行 glooctl version 来查看它是否已正确安装

➜ glooctl version
Client: {"version":"1.3.15"}
Server: version undefined, could not find any version of gloo running

现在,我们可以使用一个简单的命令将网关安装到我们的集群中

glooctl install gateway

控制台应指示安装成功完成

Creating namespace gloo-system... Done.
Starting Gloo installation...

Gloo was successfully installed!

不久之后,我们可以看到所有 Gloo pod 都在 gloo-system 命名空间中运行

➜ kubectl get pod -n gloo-system
NAME                             READY   STATUS    RESTARTS   AGE
discovery-58f8856bd7-4fftg       1/1     Running   0          13s
gateway-66f86bc8b4-n5crc         1/1     Running   0          13s
gateway-proxy-5ff99b8679-tbp65   1/1     Running   0          13s
gloo-66b8dc8868-z5c6r            1/1     Running   0          13s

部署应用程序

我们的 echo 应用程序是一个简单的容器(感谢我们在 HashiCorp 的朋友),它将响应应用程序版本,以帮助演示当我们开始测试并将流量转移到应用程序的 v2 版本时的金丝雀工作流程。

Kubernetes 在对该应用程序进行建模方面提供了很大的灵活性。我们将采用以下约定

  • 我们将在部署名称中包含版本,以便我们可以并行运行两个版本的应用程序并以不同的方式管理它们的生命周期。
  • 我们将使用应用程序标签(app: echo)和版本标签(version: v1)来标记 pod,以帮助我们的金丝雀发布。
  • 我们将为应用程序部署一个 Kubernetes Service 以设置网络。我们将使用 Gloo 配置来管理发布,而不是更新此服务或使用多个服务来管理到不同版本的路由。

以下是我们的 v1 echo 应用程序

apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: echo
      version: v1
  template:
    metadata:
      labels:
        app: echo
        version: v1
    spec:
      containers:
        # Shout out to our friends at Hashi for this useful test server
        - image: hashicorp/http-echo
          args:
            - "-text=version:v1"
            - -listen=:8080
          imagePullPolicy: Always
          name: echo-v1
          ports:
            - containerPort: 8080

这是 echo Kubernetes Service 对象

apiVersion: v1
kind: Service
metadata:
  name: echo
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  selector:
    app: echo

为了方便起见,我们已将此 yaml 发布在存储库中,因此我们可以使用以下命令进行部署

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo-ref-arch/blog-30-mar-20/platform/prog-delivery/two-phased-with-os-gloo/1-setup/echo.yaml

我们应该看到以下输出

namespace/echo created
deployment.apps/echo-v1 created
service/echo created

我们应该能够看到 echo 命名空间中的所有资源都处于健康状态

➜ kubectl get all -n echo
NAME                           READY   STATUS    RESTARTS   AGE
pod/echo-v1-66dbfffb79-287s5   1/1     Running   0          6s

NAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/echo   ClusterIP   10.55.252.216   <none>        80/TCP    6s

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/echo-v1   1/1     1            1           7s

NAME                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/echo-v1-66dbfffb79   1         1         1       7s

使用 Gloo 暴露在集群外部

我们现在可以使用 Gloo 将此服务暴露在集群外部。首先,我们将应用程序建模为 Gloo Upstream,它是 Gloo 用于流量目的地的抽象

apiVersion: gloo.solo.io/v1
kind: Upstream
metadata:
  name: echo
  namespace: gloo-system
spec:
  kube:
    selector:
      app: echo
    serviceName: echo
    serviceNamespace: echo
    servicePort: 8080
    subsetSpec:
      selectors:
        - keys:
            - version

在这里,我们正在根据 version 标签设置子集。我们不必在我们的路由中使用它,但稍后我们将开始使用它来支持我们的金丝雀工作流程。

我们现在可以通过定义一个 虚拟服务 在 Gloo 中为此上游创建一个路由

apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: echo
  namespace: gloo-system
spec:
  virtualHost:
    domains:
      - "*"
    routes:
      - matchers:
          - prefix: /
        routeAction:
          single:
            upstream:
              name: echo
              namespace: gloo-system

我们可以使用以下命令应用这些资源

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo-ref-arch/blog-30-mar-20/platform/prog-delivery/two-phased-with-os-gloo/1-setup/upstream.yaml
kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo-ref-arch/blog-30-mar-20/platform/prog-delivery/two-phased-with-os-gloo/1-setup/vs.yaml

应用这两个资源后,我们就可以开始通过 Gloo 将流量发送到应用程序

➜ curl $(glooctl proxy url)/
version:v1

我们的设置已完成,我们的集群现在看起来像这样

Setup

两阶段发布策略

现在我们有一个新版本的 v2 echo 应用程序,我们希望将其发布。我们知道,当发布完成后,我们将最终得到这张图片

End State

但是,为了实现这一目标,我们可能需要执行几轮测试,以确保新版本的应用程序满足某些正确性 和/或性能接受标准。在这篇文章中,我们将介绍一种使用 Gloo 进行金丝雀发布的两阶段方法,该方法可用于满足绝大多数验收测试。

在第一阶段,我们将通过将一小部分流量路由到新版本的应用程序来执行冒烟测试和正确性测试。在此演示中,我们将使用标头 stage: canary 来触发到新服务的路由,但在实践中,可能希望根据请求的其他部分(例如经过验证的 JWT 中的声明)做出此决定。

在第二阶段,我们已经建立了正确性,因此我们准备将所有流量转移到新版本的应用程序。我们将配置加权目标,并在监控某些业务指标的同时转移流量,以确保服务质量保持在可接受的水平。一旦 100% 的流量转移到新版本,旧版本就可以停用。

在实践中,可能希望仅使用一个阶段进行测试,在这种情况下,可以跳过另一个阶段。

第一阶段:v2 的初始金丝雀发布

在此阶段,我们将部署 v2,然后使用标头 stage: canary 开始将少量特定流量路由到新版本。我们将使用此标头执行一些基本的冒烟测试,并确保 v2 按我们期望的方式工作

Subset Routing

设置子集路由

在部署我们的 v2 服务之前,我们将更新我们的虚拟服务,使其仅路由到具有子集标签 version: v1 的 pod,使用名为 子集路由 的 Gloo 功能。

apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: echo
  namespace: gloo-system
spec:
  virtualHost:
    domains:
      - "*"
    routes:
      - matchers:
          - prefix: /
        routeAction:
          single:
            upstream:
              name: echo
              namespace: gloo-system
            subset:
              values:
                version: v1

我们可以使用以下命令将它们应用于集群

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo-ref-arch/blog-30-mar-20/platform/prog-delivery/two-phased-with-os-gloo/2-initial-subset-routing-to-v2/vs-1.yaml

应用程序应继续像以前一样运行

➜ curl $(glooctl proxy url)/
version:v1

部署 echo v2

现在我们可以安全地部署 echo 应用程序的 v2

apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: echo
      version: v2
  template:
    metadata:
      labels:
        app: echo
        version: v2
    spec:
      containers:
        - image: hashicorp/http-echo
          args:
            - "-text=version:v2"
            - -listen=:8080
          imagePullPolicy: Always
          name: echo-v2
          ports:
            - containerPort: 8080

我们可以使用以下命令进行部署

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo-ref-arch/blog-30-mar-20/platform/prog-delivery/two-phased-with-os-gloo/2-initial-subset-routing-to-v2/echo-v2.yaml

由于我们的网关配置为专门路由到 v1 子集,因此这应该没有任何影响。但是,如果为路由配置了 v2 子集,则它会使 v2 可以从网关路由。

在继续之前,请确保 v2 正在运行

➜ kubectl get pod -n echo
NAME                       READY   STATUS    RESTARTS   AGE
echo-v1-66dbfffb79-2qw86   1/1     Running   0          5m25s
echo-v2-86584fbbdb-slp44   1/1     Running   0          93s

应用程序应继续像以前一样运行

➜ curl $(glooctl proxy url)/
version:v1

为金丝雀测试添加 v2 的路由

当请求中提供 stage: canary 标头时,我们将路由到 v2 子集。如果未提供标头,我们将继续像以前一样路由到 v1 子集。

apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: echo
  namespace: gloo-system
spec:
  virtualHost:
    domains:
      - "*"
    routes:
      - matchers:
          - headers:
              - name: stage
                value: canary
            prefix: /
        routeAction:
          single:
            upstream:
              name: echo
              namespace: gloo-system
            subset:
              values:
                version: v2
      - matchers:
          - prefix: /
        routeAction:
          single:
            upstream:
              name: echo
              namespace: gloo-system
            subset:
              values:
                version: v1

我们可以使用以下命令进行部署

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo-ref-arch/blog-30-mar-20/platform/prog-delivery/two-phased-with-os-gloo/2-initial-subset-routing-to-v2/vs-2.yaml

金丝雀测试

现在我们有了这个路由,我们可以做一些测试。首先,让我们确保现有路由按预期工作

➜ curl $(glooctl proxy url)/
version:v1

现在我们可以开始对新应用程序版本进行金丝雀测试

➜ curl $(glooctl proxy url)/ -H "stage: canary"
version:v2

子集路由的高级用例

我们可能会认为这种使用用户提供的请求头的方法过于开放。相反,我们可能希望将金丝雀测试限制在已知且已授权的用户范围内。

我们常见的一种实现方式是,金丝雀路由需要一个有效的 JWT,其中包含一个特定的声明,以表明该主体已获得金丝雀测试的授权。Enterprise Gloo 提供了开箱即用的 JWT 验证支持,可以根据 JWT 声明更新请求头,并根据更新后的请求头重新计算路由目的地。我们将在以后的文章中介绍金丝雀测试中更高级的用例。

第二阶段:将所有流量转移到 v2 并停用 v1

此时,我们已经部署了 v2,并创建了用于金丝雀测试的路由。如果我们对测试结果满意,我们可以进入第二阶段,开始将负载从 v1 转移到 v2。我们将使用 Gloo 中的加权目的地来管理迁移期间的负载。

设置加权目的地

我们可以更改 Gloo 路由以路由到这两个目的地,并使用权重来决定多少流量应该路由到 v1 子集,多少流量应该路由到 v2 子集。首先,我们将将其设置为 100% 的流量继续路由到 v1 子集,除非像以前一样提供了 stage: canary 请求头。

apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: echo
  namespace: gloo-system
spec:
  virtualHost:
    domains:
      - "*"
    routes:
      # We'll keep our route from before if we want to continue testing with this header
      - matchers:
          - headers:
              - name: stage
                value: canary
            prefix: /
        routeAction:
          single:
            upstream:
              name: echo
              namespace: gloo-system
            subset:
              values:
                version: v2
      # Now we'll route the rest of the traffic to the upstream, load balanced across the two subsets.
      - matchers:
          - prefix: /
        routeAction:
          multi:
            destinations:
              - destination:
                  upstream:
                    name: echo
                    namespace: gloo-system
                  subset:
                    values:
                      version: v1
                weight: 100
              - destination:
                  upstream:
                    name: echo
                    namespace: gloo-system
                  subset:
                    values:
                      version: v2
                weight: 0

我们可以使用以下命令将此虚拟服务更新应用于集群

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo-ref-arch/blog-30-mar-20/platform/prog-delivery/two-phased-with-os-gloo/3-progressive-traffic-shift-to-v2/vs-1.yaml

现在,对于任何不包含 stage: canary 请求头的请求,集群看起来像这样

Initialize Traffic Shift

使用初始权重,我们应该看到网关继续为所有流量提供 v1 服务。

➜ curl $(glooctl proxy url)/
version:v1

开始推出

为了模拟负载测试,让我们将一半的流量转移到 v2

Load Test

这可以通过调整虚拟服务上的权重来表示

apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: echo
  namespace: gloo-system
spec:
  virtualHost:
    domains:
      - "*"
    routes:
      - matchers:
          - headers:
              - name: stage
                value: canary
            prefix: /
        routeAction:
          single:
            upstream:
              name: echo
              namespace: gloo-system
            subset:
              values:
                version: v2
      - matchers:
          - prefix: /
        routeAction:
          multi:
            destinations:
              - destination:
                  upstream:
                    name: echo
                    namespace: gloo-system
                  subset:
                    values:
                      version: v1
                # Update the weight so 50% of the traffic hits v1
                weight: 50
              - destination:
                  upstream:
                    name: echo
                    namespace: gloo-system
                  subset:
                    values:
                      version: v2
                # And 50% is routed to v2
                weight: 50

我们可以使用以下命令将其应用到集群

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo-ref-arch/blog-30-mar-20/platform/prog-delivery/two-phased-with-os-gloo/3-progressive-traffic-shift-to-v2/vs-2.yaml

现在,当我们向网关发送流量时,我们应该看到一半的请求返回 version:v1,另一半返回 version:v2

➜ curl $(glooctl proxy url)/
version:v1
➜ curl $(glooctl proxy url)/
version:v2
➜ curl $(glooctl proxy url)/
version:v1

在实践中,在此过程中,您很可能正在监控一些性能和业务指标,以确保流量转移不会导致整体服务质量下降。我们甚至可以利用 Flagger 等操作符来帮助自动化此 Gloo 工作流程。Gloo Enterprise 与您的指标后端集成,并提供开箱即用和动态的、基于上游的仪表板,可用于监控部署的健康状况。我们将在以后的关于使用 Gloo 进行高级金丝雀测试用例的文章中介绍这些主题。

完成部署

我们将继续调整权重,直到最终所有流量都路由到 v2

Final Shift

我们的虚拟服务将如下所示

apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: echo
  namespace: gloo-system
spec:
  virtualHost:
    domains:
      - "*"
    routes:
      - matchers:
          - headers:
              - name: stage
                value: canary
            prefix: /
        routeAction:
          single:
            upstream:
              name: echo
              namespace: gloo-system
            subset:
              values:
                version: v2
      - matchers:
          - prefix: /
        routeAction:
          multi:
            destinations:
              - destination:
                  upstream:
                    name: echo
                    namespace: gloo-system
                  subset:
                    values:
                      version: v1
                # No traffic will be sent to v1 anymore
                weight: 0
              - destination:
                  upstream:
                    name: echo
                    namespace: gloo-system
                  subset:
                    values:
                      version: v2
                # Now all the traffic will be routed to v2
                weight: 100

我们可以使用以下命令将其应用于集群

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo-ref-arch/blog-30-mar-20/platform/prog-delivery/two-phased-with-os-gloo/3-progressive-traffic-shift-to-v2/vs-3.yaml

现在,当我们向网关发送流量时,我们应该看到所有请求都返回 version:v2

➜ curl $(glooctl proxy url)/
version:v2
➜ curl $(glooctl proxy url)/
version:v2
➜ curl $(glooctl proxy url)/
version:v2

停用 v1

此时,我们已经部署了新版本的应用程序,使用子集路由进行了正确性测试,通过逐步将流量转移到新版本进行了负载和性能测试,并完成了部署。剩下的唯一任务是清理我们的 v1 资源。

首先,我们将清理我们的路由。我们将保留在路由上指定的子集,以便为将来的升级做好准备。

apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: echo
  namespace: gloo-system
spec:
  virtualHost:
    domains:
      - "*"
    routes:
      - matchers:
          - prefix: /
        routeAction:
          single:
            upstream:
              name: echo
              namespace: gloo-system
            subset:
              values:
                version: v2

我们可以使用以下命令应用此更新

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo-ref-arch/blog-30-mar-20/platform/prog-delivery/two-phased-with-os-gloo/4-decommissioning-v1/vs.yaml

然后我们可以删除 v1 部署,该部署不再为任何流量提供服务。

kubectl delete deploy -n echo echo-v1

现在我们的集群看起来像这样

End State

对网关的请求返回此结果

➜ curl $(glooctl proxy url)/
version:v2

我们现在已经使用 Gloo 完成了应用程序更新的两阶段金丝雀部署!

其他高级主题

在本篇文章中,我们收集了一些主题,这些主题可能是高级探索的良好起点

  • 使用 JWT 过滤器验证 JWT,将声明提取到请求头中,并根据声明值路由到金丝雀版本。
  • 查看 Gloo 创建的 Prometheus 指标Grafana 仪表板,以监控部署的健康状况。
  • 通过将 FlaggerGloo 集成来自动化部署。

其他一些值得进一步探索的主题

  • 通过让团队拥有对其上游和路由配置的所有权来支持 自助服务 升级
  • 利用 Gloo 的 委托 功能和 Kubernetes RBAC 来安全地分散配置管理
  • 通过应用 GitOps 原则并使用 Flux 等工具将配置推送到集群来完全自动化持续交付过程
  • 通过使用不同的部署模式设置 Gloo 来支持 混合非 Kubernetes 应用程序用例
  • 利用 流量影子 在将生产流量转移到新版本之前,开始使用真实数据测试新版本

参与 Gloo 社区

Gloo 除了拥有企业客户群外,还拥有庞大且不断增长的开源用户社区。要了解有关 Gloo 的更多信息

  • 查看 repo,您可以在其中查看代码并提交问题
  • 查看 docs,其中有大量的指南和示例
  • 加入 slack 频道,开始与 Solo 工程团队和用户社区聊天

如果您想与我联系(欢迎反馈!),可以在 Solo slack 上找到我,或者给我发送电子邮件至 rick.ducott@solo.io