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

使用开源 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 标签 (app: echo) 和 version 标签 (version: v1) 标记 Pod,以帮助进行金丝雀发布。
  • 我们将为应用部署一个 Kubernetes Service 来设置网络。我们不会更新此 Service 或使用多个 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 将此 Service 暴露到集群外部。首先,我们将把应用建模为 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 标签设置子集。我们不一定需要在路由中使用它,但稍后我们将开始使用它来支持我们的金丝雀工作流程。

现在我们可以通过定义 Virtual Service 在 Gloo 中创建指向此 Upstream 的路由

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

两阶段发布策略

现在我们有 echo 应用的一个新版本 v2,希望进行发布。我们知道,发布完成后,我们将看到如下结构图

End State

然而,要达到目标,我们可能需要进行几轮测试,以确保应用的新版本满足特定的正确性或性能验收标准。在这篇文章中,我们将介绍一种使用 Gloo 进行金丝雀发布的两阶段方法,它可用于满足绝大多数验收测试。

在第一阶段,我们将通过将一小部分流量路由到应用的新版本来执行冒烟测试和正确性测试。在此演示中,我们将使用一个 header stage: canary 来触发路由到新 Service,尽管在实际操作中,可能希望根据请求的其他部分(例如经验证的 JWT 中的声明)做出此决策。

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

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

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

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

Subset Routing

设置子集路由

在部署我们的 v2 Service 之前,我们将更新我们的 Virtual Service,仅使用 Gloo 的一个名为 子集路由 的功能,将流量路由到带有子集标签 version: v1 的 Pod。

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 header 时,我们将路由到 v2 子集。如果没有提供该 header,我们将继续像以前一样路由到 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

子集路由的高级用例

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

我们见过的一种常见实现方式是,金丝雀路由需要一个有效的 JWT,该 JWT 包含特定的 claim 来表明主体已被授权进行金丝雀测试。企业版 Gloo 对验证 JWT、根据 JWT claim 更新请求 header 以及根据更新后的 header 重新计算路由目标提供了开箱即用的支持。我们将在未来的文章中介绍金丝雀测试中更高级的用例。

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

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

设置加权目标

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

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

我们可以使用以下命令将此 Virtual Service 更新应用到集群

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 header 的请求,集群看起来像这样

Initialize Traffic Shift

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

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

开始发布

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

Load Test

这可以通过调整 Virtual Service 上的权重来表达

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 这样的 Operator 来帮助自动化此 Gloo 工作流程。企业版 Gloo 与您的指标后端集成,并提供开箱即用且动态的、基于 Upstream 的 Dashboard,可用于监控发布健康状况。我们将把这些主题留到未来的文章中,介绍 Gloo 的高级金丝雀测试用例。

完成发布

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

Final Shift

我们的 Virtual Service 将看起来像这样

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 Deployment。

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