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

使用开源 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% 的流量转移到新版本,旧版本就可以退役了。

在实践中,可能只需要使用其中一个阶段进行测试,在这种情况下,可以跳过另一个阶段。

阶段 1: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 声明更新请求标头,并根据更新后的标头重新计算路由目标。我们将在未来的文章中讨论涵盖金丝雀测试中更高级用例的内容。

阶段 2:将所有流量转移到 v2 并淘汰 v1

此时,我们已经部署了 v2,并创建了用于金丝雀测试的路由。如果对测试结果满意,我们可以进入阶段 2,开始将负载从 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 的更多信息,请访问

  • 查看 仓库,您可以在其中查看代码和提交问题
  • 查看 文档,其中包含大量指南和示例
  • 加入 Slack 频道,开始与 Solo 工程团队和用户社区聊天

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