Create 2020-04-22-two-phased-canary-rollout-with-gloo.md

pull/19999/head
Rick Ducott 2020-03-30 10:57:02 -04:00 committed by bryan
parent 0e7d4a0aba
commit a592696b81
7 changed files with 761 additions and 0 deletions

View File

@ -0,0 +1,761 @@
---
title: "Two-phased Canary Rollout with Open Source Gloo"
date: 2020-04-22
slug: two-phased-canary-rollout-with-gloo
url: /blog/2020/04/Two-phased-Canary-Rollout-With-Gloo
---
**Author:** Rick Ducott | [GitHub](https://github.com/rickducott/) | [Twitter](https://twitter.com/ducott)
Every day, my colleagues and I are talking to platform owners, architects, and engineers who are using [Gloo](https://github.com/solo-io/gloo) as an API gateway
to expose their applications to end users. These applications may span legacy monoliths, microservices, managed cloud services, and Kubernetes
clusters. Fortunately, Gloo makes it easy to set up routes to manage, secure, and observe application traffic while
supporting a flexible deployment architecture to meet the varying production needs of our users.
Beyond the initial set up, platform owners frequently ask us to help design the operational workflows within their organization:
How do we bring a new application online? How do we upgrade an application? How do we divide responsibilities across our
platform, ops, and development teams?
In this post, we're going to use Gloo to design a two-phased canary rollout workflow for application upgrades:
* In the first phase, we'll do canary testing by shifting a small subset of traffic to the new version. This allows you to safely perform smoke and correctness tests.
* In the second phase, we'll progressively shift traffic to the new version, allowing us to monitor the new version under load, and eventually, decommission the old version.
To keep it simple, we're going to focus on designing the workflow using [open source Gloo](https://github.com/solo-io/gloo), and we're going to deploy the gateway and
application to Kubernetes. At the end, we'll talk about a few extensions and advanced topics that could be interesting to explore in a follow up.
## Initial setup
To start, we need a Kubernetes cluster. This example doesn't take advantage of any cloud specific
features, and can be run against a local test cluster such as [minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/).
This post assumes a basic understanding of Kubernetes and how to interact with it using `kubectl`.
We'll install the latest [open source Gloo](https://github.com/solo-io/gloo) to the `gloo-system` namespace and deploy
version `v1` of an example application to the `echo` namespace. We'll expose this application outside the cluster
by creating a route in Gloo, to end up with a picture like this:
![Setup](/static/images/blog/2020-04-17-two-phased-canary-rollout-with-gloo/setup.png)
### Deploying Gloo
We'll install gloo with the `glooctl` command line tool, which we can download and add to the `PATH` with the following
commands:
```
curl -sL https://run.solo.io/gloo/install | sh
export PATH=$HOME/.gloo/bin:$PATH
```
Now, you should be able to run `glooctl version` to see that it is installed correctly:
```
➜ glooctl version
Client: {"version":"1.3.15"}
Server: version undefined, could not find any version of gloo running
```
Now we can install the gateway to our cluster with a simple command:
```
glooctl install gateway
```
The console should indicate the install finishes successfully:
```
Creating namespace gloo-system... Done.
Starting Gloo installation...
Gloo was successfully installed!
```
Before long, we can see all the Gloo pods running in the `gloo-system` namespace:
```
➜ 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
```
### Deploying the application
Our `echo` application is a simple container (thanks to our friends at HashiCorp) that will
respond with the application version, to help demonstrate our canary workflows as we start testing and
shifting traffic to a `v2` version of the application.
Kubernetes gives us a lot of flexibility in terms of modeling this application. We'll adopt the following
conventions:
* We'll include the version in the deployment name so we can run two versions of the application
side-by-side and manage their lifecycle differently.
* We'll label pods with an app label (`app: echo`) and a version label (`version: v1`) to help with our canary rollout.
* We'll deploy a single Kubernetes `Service` for the application to set up networking. Instead of updating
this or using multiple services to manage routing to different versions, we'll manage the rollout with Gloo configuration.
The following is our `v1` echo application:
```yaml
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
```
And here is the `echo` Kubernetes `Service` object:
```yaml
apiVersion: v1
kind: Service
metadata:
name: echo
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
selector:
app: echo
```
For convenience, we've published this yaml in a repo so we can deploy it with the following command:
```
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
```
We should see the following output:
```
namespace/echo created
deployment.apps/echo-v1 created
service/echo created
```
And we should be able to see all the resources healthy in the `echo` namespace:
```
➜ 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
```
### Exposing outside the cluster with Gloo
We can now expose this service outside the cluster with Gloo. First, we'll model the application as a Gloo
[Upstream](https://docs.solo.io/gloo/latest/introduction/architecture/concepts/#upstreams), which is Gloo's abstraction
for a traffic destination:
```yaml
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
```
Here, we're setting up subsets based on the `version` label. We don't have to use this in our routes, but later
we'll start to use it to support our canary workflow.
We can now create a route to this upstream in Gloo by defining a
[Virtual Service](https://docs.solo.io/gloo/latest/introduction/architecture/concepts/#virtual-services):
```yaml
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
```
We can apply these resources with the following commands:
```
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
```
Once we apply these two resources, we can start to send traffic to the application through Gloo:
```
➜ curl $(glooctl proxy url)/
version:v1
```
Our setup is complete, and our cluster now looks like this:
![Setup](/static/images/blog/2020-04-17-two-phased-canary-rollout-with-gloo/setup.png)
## Two-Phased Rollout Strategy
Now we have a new version `v2` of the echo application that we wish to roll out. We know that when the
rollout is complete, we are going to end up with this picture:
![End State](/static/images/blog/2020-04-17-two-phased-canary-rollout-with-gloo/end-state.png)
However, to get there, we may want to perform a few rounds of testing to ensure the new version of the application
meets certain correctness and/or performance acceptance criteria. In this post, we'll introduce a two-phased approach to
canary rollout with Gloo, that could be used to satisfy the vast majority of acceptance tests.
In the first phase, we'll perform smoke and correctness tests by routing a small segment of the traffic to the new version
of the application. In this demo, we'll use a header `stage: canary` to trigger routing to the new service, though in
practice it may be desirable to make this decision based on another part of the request, such as a claim in a verified JWT.
In the second phase, we've already established correctness, so we are ready to shift all of the traffic over to the new
version of the application. We'll configure weighted destinations, and shift the traffic while monitoring certain business
metrics to ensure the service quality remains at acceptable levels. Once 100% of the traffic is shifted to the new version,
the old version can be decommissioned.
In practice, it may be desirable to only use one of the phases for testing, in which case the other phase can be
skipped.
## Phase 1: Initial canary rollout of v2
In this phase, we'll deploy `v2`, and then use a header `stage: canary` to start routing a small amount of specific
traffic to the new version. We'll use this header to perform some basic smoke testing and make sure `v2` is working the
way we'd expect:
![Subset Routing](/static/images/blog/2020-04-17-two-phased-canary-rollout-with-gloo/subset-routing.png)
### Setting up subset routing
Before deploying our `v2` service, we'll update our virtual service to only route to pods that have the subset label
`version: v1`, using a Gloo feature called [subset routing](https://docs.solo.io/gloo/latest/guides/traffic_management/destination_types/subsets/).
```yaml
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
```
We can apply them to the cluster with the following commands:
```
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
```
The application should continue to function as before:
```
➜ curl $(glooctl proxy url)/
version:v1
```
### Deploying echo v2
Now we can safely deploy `v2` of the echo application:
```yaml
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
```
We can deploy with the following command:
```
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
```
Since our gateway is configured to route specifically to the `v1` subset, this should have no effect. However, it does enable
`v2` to be routable from the gateway if the `v2` subset is configured for a route.
Make sure `v2` is running before moving on:
```bash
➜ 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
```
The application should continue to function as before:
```
➜ curl $(glooctl proxy url)/
version:v1
```
### Adding a route to v2 for canary testing
We'll route to the `v2` subset when the `stage: canary` header is supplied on the request. If the header isn't
provided, we'll continue to route to the `v1` subset as before.
```yaml
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
```
We can deploy with the following command:
```
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
```
### Canary testing
Now that we have this route, we can do some testing. First let's ensure that the existing route is working as expected:
```
➜ curl $(glooctl proxy url)/
version:v1
```
And now we can start to canary test our new application version:
```
➜ curl $(glooctl proxy url)/ -H "stage: canary"
version:v2
```
### Advanced use cases for subset routing
We may decide that this approach, using user-provided request headers, is too open. Instead, we may
want to restrict canary testing to a known, authorized user.
A common implementation of this that we've seen is for the canary route to require a valid JWT that contains
a specific claim to indicate the subject is authorized for canary testing. Enterprise Gloo has out of the box
support for verifying JWTs, updating the request headers based on the JWT claims, and recomputing the
routing destination based on the updated headers. We'll save that for a future post covering more advanced use
cases in canary testing.
## Phase 2: Shifting all traffic to v2 and decommissioning v1
At this point, we've deployed `v2`, and created a route for canary testing. If we are satisfied with the
results of the testing, we can move on to phase 2 and start shifting the load from `v1` to `v2`. We'll use
[weighted destinations](https://docs.solo.io/gloo/latest/guides/traffic_management/destination_types/multi_destination/)
in Gloo to manage the load during the migration.
### Setting up the weighted destinations
We can change the Gloo route to route to both of these destinations, with weights to decide how much of the traffic should
go to the `v1` versus the `v2` subset. To start, we're going to set it up so 100% of the traffic continues to get routed to the
`v1` subset, unless the `stage: canary` header was provided as before.
```yaml
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
```
We can apply this virtual service update to the cluster with the following commands:
```
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
```
Now the cluster looks like this, for any request that doesn't have the `stage: canary` header:
![Initialize Traffic Shift](/static/images/blog/2020-04-17-two-phased-canary-rollout-with-gloo/init-traffic-shift.png)
With the initial weights, we should see the gateway continue to serve `v1` for all traffic.
```bash
➜ curl $(glooctl proxy url)/
version:v1
```
### Commence rollout
To simulate a load test, let's shift half the traffic to `v2`:
![Load Test](/static/images/blog/2020-04-17-two-phased-canary-rollout-with-gloo/load-test.png)
This can be expressed on our virtual service by adjusting the weights:
```yaml
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
```
We can apply this to the cluster with the following command:
```
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
```
Now when we send traffic to the gateway, we should see half of the requests return `version:v1` and the
other half return `version:v2`.
```
➜ curl $(glooctl proxy url)/
version:v1
➜ curl $(glooctl proxy url)/
version:v2
➜ curl $(glooctl proxy url)/
version:v1
```
In practice, during this process it's likely you'll be monitoring some performance and business metrics
to ensure the traffic shift isn't resulting in a decline in the overall quality of service. We can even
leverage operators like [Flagger](https://github.com/weaveworks/flagger) to help automate this Gloo
workflow. Gloo Enterprise integrates with your metrics backend and provides out of the box and dynamic,
upstream-based dashboards that can be used to monitor the health of the rollout.
We will save these topics for a future post on advanced canary testing use cases with Gloo.
### Finishing the rollout
We will continue adjusting weights until eventually, all of the traffic is now being routed to `v2`:
![Final Shift](/static/images/blog/2020-04-17-two-phased-canary-rollout-with-gloo/final-shift.png)
Our virtual service will look like this:
```yaml
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
```
We can apply that to the cluster with the following command:
```
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
```
Now when we send traffic to the gateway, we should see all of the requests return `version:v2`.
```
➜ curl $(glooctl proxy url)/
version:v2
➜ curl $(glooctl proxy url)/
version:v2
➜ curl $(glooctl proxy url)/
version:v2
```
### Decommissioning v1
At this point, we have deployed the new version of our application, conducted correctness tests using subset routing,
conducted load and performance tests by progressively shifting traffic to the new version, and finished
the rollout. The only remaining task is to clean up our `v1` resources.
First, we'll clean up our routes. We'll leave the subset specified on the route so we are all setup for future upgrades.
```yaml
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
```
We can apply this update with the following command:
```
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
```
And we can delete the `v1` deployment, which is no longer serving any traffic.
```
kubectl delete deploy -n echo echo-v1
```
Now our cluster looks like this:
![End State](/static/images/blog/2020-04-17-two-phased-canary-rollout-with-gloo/end-state.png)
And requests to the gateway return this:
```
➜ curl $(glooctl proxy url)/
version:v2
```
We have now completed our two-phased canary rollout of an application update using Gloo!
## Other Advanced Topics
Over the course of this post, we collected a few topics that could be a good starting point for advanced exploration:
* Using the **JWT** filter to verify JWTs, extract claims onto headers, and route to canary versions depending on a claim value.
* Looking at **Prometheus metrics** and **Grafana dashboards** created by Gloo to monitor the health of the rollout.
* Automating the rollout by integrating **Flagger** with **Gloo**.
A few other topics that warrant further exploration:
* Supporting **self-service** upgrades by giving teams ownership over their upstream and route configuration
* Utilizing Gloo's **delegation** feature and Kubernetes **RBAC** to decentralize the configuration management safely
* Fully automating the continuous delivery process by applying **GitOps** principles and using tools like **Flux** to push config to the cluster
* Supporting **hybrid** or **non-Kubernetes** application use-cases by setting up Gloo with a different deployment pattern
* Utilizing **traffic shadowing** to begin testing the new version with realistic data before shifting production traffic to it
## Get Involved in the Gloo Community
Gloo has a large and growing community of open source users, in addition to an enterprise customer base. To learn more about
Gloo:
* Check out the [repo](https://github.com/solo-io/gloo), where you can see the code and file issues
* Check out the [docs](https://docs.solo.io/gloo/latest), which have an extensive collection of guides and examples
* Join the [slack channel](http://slack.solo.io/) and start chatting with the Solo engineering team and user community
If you'd like to get in touch with me (feedback is always appreciated!), you can find me on the
[Solo slack](http://slack.solo.io/) or email me at **rick.ducott@solo.io**.

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB