From ec63aa99990a7fc37e0d34c54db90f1a1c40461e Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Fri, 17 May 2019 11:55:15 +0300 Subject: [PATCH 1/4] Add Gloo custom resources to RBAC --- Makefile | 6 ++++++ artifacts/flagger/account.yaml | 15 +++++++++++++++ charts/flagger/templates/rbac.yaml | 15 +++++++++++++++ pkg/router/gloo.go | 7 ++++--- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 15771dcc..2374df72 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,12 @@ run-smi: -slack-url=https://hooks.slack.com/services/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 \ -slack-channel="devops-alerts" +run-gloo: + go run cmd/flagger/* -kubeconfig=$$HOME/.kube/config -log-level=info -mesh-provider=gloo -namespace=gloo \ + -metrics-server=https://prometheus.istio.weavedx.com \ + -slack-url=https://hooks.slack.com/services/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 \ + -slack-channel="devops-alerts" + build: docker build -t weaveworks/flagger:$(TAG) . -f Dockerfile diff --git a/artifacts/flagger/account.yaml b/artifacts/flagger/account.yaml index d31e7568..b7cf0e28 100644 --- a/artifacts/flagger/account.yaml +++ b/artifacts/flagger/account.yaml @@ -64,6 +64,21 @@ rules: resources: - trafficsplits verbs: ["*"] + - apiGroups: + - gloo.solo.io + resources: + - settings + - upstreams + - upstreamgroups + - proxies + - virtualservices + verbs: ["*"] + - apiGroups: + - gateway.solo.io + resources: + - virtualservices + - gateways + verbs: ["*"] - nonResourceURLs: - /version verbs: diff --git a/charts/flagger/templates/rbac.yaml b/charts/flagger/templates/rbac.yaml index 782e1df1..cb5c52ef 100644 --- a/charts/flagger/templates/rbac.yaml +++ b/charts/flagger/templates/rbac.yaml @@ -60,6 +60,21 @@ rules: resources: - trafficsplits verbs: ["*"] + - apiGroups: + - gloo.solo.io + resources: + - settings + - upstreams + - upstreamgroups + - proxies + - virtualservices + verbs: ["*"] + - apiGroups: + - gateway.solo.io + resources: + - virtualservices + - gateways + verbs: ["*"] - nonResourceURLs: - /version verbs: diff --git a/pkg/router/gloo.go b/pkg/router/gloo.go index 62303e20..7cf735c4 100644 --- a/pkg/router/gloo.go +++ b/pkg/router/gloo.go @@ -152,8 +152,10 @@ func (gr *GlooRouter) writeUpstreamGroupRuleForCanary(canary *flaggerv1.Canary, targetName := canary.Spec.TargetRef.Name if oldUg, err := gr.ugClient.Read(ug.Metadata.Namespace, ug.Metadata.Name, solokitclients.ReadOpts{}); err != nil { - // ignore not exist errors.. - if !solokiterror.IsNotExist(err) { + if solokiterror.IsNotExist(err) { + gr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). + Infof("UpstreamGroup %s created", ug.Metadata.Name) + } else { return fmt.Errorf("RoutingRule %s.%s read failed: %v", targetName, canary.Namespace, err) } } else { @@ -181,6 +183,5 @@ func (gr *GlooRouter) writeUpstreamGroupRuleForCanary(canary *flaggerv1.Canary, if err != nil { return fmt.Errorf("UpstreamGroup %s.%s update failed: %v", targetName, canary.Namespace, err) } - gr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).Infof("UpstreamGroup %s updated", ug.Metadata.Name) return nil } From da43a152ba4766e484056ffa2fa68f5b8dc72336 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Fri, 17 May 2019 13:15:53 +0300 Subject: [PATCH 2/4] Add Gloo canary deployment example --- artifacts/gloo/canary.yaml | 36 ++++++++++++++++ artifacts/gloo/deployment.yaml | 67 +++++++++++++++++++++++++++++ artifacts/gloo/hpa.yaml | 19 ++++++++ artifacts/gloo/virtual-service.yaml | 17 ++++++++ 4 files changed, 139 insertions(+) create mode 100644 artifacts/gloo/canary.yaml create mode 100644 artifacts/gloo/deployment.yaml create mode 100644 artifacts/gloo/hpa.yaml create mode 100644 artifacts/gloo/virtual-service.yaml diff --git a/artifacts/gloo/canary.yaml b/artifacts/gloo/canary.yaml new file mode 100644 index 00000000..7c0218ac --- /dev/null +++ b/artifacts/gloo/canary.yaml @@ -0,0 +1,36 @@ +apiVersion: flagger.app/v1alpha3 +kind: Canary +metadata: + name: podinfo + namespace: test +spec: + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + progressDeadlineSeconds: 60 + autoscalerRef: + apiVersion: autoscaling/v2beta1 + kind: HorizontalPodAutoscaler + name: podinfo + service: + port: 9898 + canaryAnalysis: + interval: 10s + threshold: 10 + maxWeight: 50 + stepWeight: 5 + metrics: + - name: request-success-rate + threshold: 99 + interval: 1m + - name: request-duration + threshold: 500 + interval: 30s + webhooks: + - name: load-test + url: http://flagger-loadtester.test/ + timeout: 5s + metadata: + type: cmd + cmd: "hey -z 1m -q 10 -c 2 http://gloo.exmaple.com/" diff --git a/artifacts/gloo/deployment.yaml b/artifacts/gloo/deployment.yaml new file mode 100644 index 00000000..57ed8a41 --- /dev/null +++ b/artifacts/gloo/deployment.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: podinfo + namespace: test + labels: + app: podinfo +spec: + minReadySeconds: 5 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: quay.io/stefanprodan/podinfo:1.4.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 9898 + name: http + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: blue + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi diff --git a/artifacts/gloo/hpa.yaml b/artifacts/gloo/hpa.yaml new file mode 100644 index 00000000..48ec76e8 --- /dev/null +++ b/artifacts/gloo/hpa.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: podinfo + namespace: test +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + minReplicas: 1 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + # scale up if usage is above + # 99% of the requested CPU (100m) + targetAverageUtilization: 99 diff --git a/artifacts/gloo/virtual-service.yaml b/artifacts/gloo/virtual-service.yaml new file mode 100644 index 00000000..53d010dd --- /dev/null +++ b/artifacts/gloo/virtual-service.yaml @@ -0,0 +1,17 @@ +apiVersion: gateway.solo.io/v1 +kind: VirtualService +metadata: + name: podinfo + namespace: test +spec: + virtualHost: + domains: + - '*' + name: podinfo.default + routes: + - matcher: + prefix: / + routeAction: + upstreamGroup: + name: podinfo + namespace: gloo From 88c39d73797344efeb369bb1e4434d71d9efe6f2 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Fri, 17 May 2019 13:16:12 +0300 Subject: [PATCH 3/4] Add Gloo canary deployment docs and diagram --- artifacts/gloo/canary.yaml | 2 +- docs/diagrams/flagger-gloo-overview.png | Bin 0 -> 41729 bytes .../usage/gloo-progressive-delivery.md | 366 ++++++++++++++++++ 3 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 docs/diagrams/flagger-gloo-overview.png create mode 100644 docs/gitbook/usage/gloo-progressive-delivery.md diff --git a/artifacts/gloo/canary.yaml b/artifacts/gloo/canary.yaml index 7c0218ac..3be05a78 100644 --- a/artifacts/gloo/canary.yaml +++ b/artifacts/gloo/canary.yaml @@ -33,4 +33,4 @@ spec: timeout: 5s metadata: type: cmd - cmd: "hey -z 1m -q 10 -c 2 http://gloo.exmaple.com/" + cmd: "hey -z 1m -q 10 -c 2 http://gloo.example.com/" diff --git a/docs/diagrams/flagger-gloo-overview.png b/docs/diagrams/flagger-gloo-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..393428e943b80630751e27a38605c5d7bf7119df GIT binary patch literal 41729 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

NTxyzGn~kAjmm# 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 literal 0 HcmV?d00001 diff --git a/docs/gitbook/usage/gloo-progressive-delivery.md b/docs/gitbook/usage/gloo-progressive-delivery.md new file mode 100644 index 00000000..bff29cee --- /dev/null +++ b/docs/gitbook/usage/gloo-progressive-delivery.md @@ -0,0 +1,366 @@ +# NGNIX Ingress Controller Canary Deployments + +This guide shows you how to use the [Gloo](https://gloo.solo.io/) ingress controller and Flagger to automate canary deployments. + +![Flagger Gloo Ingress Controller](https://raw.githubusercontent.com/weaveworks/flagger/master/docs/diagrams/flagger-gloo-overview.png) + +### Prerequisites + +Flagger requires a Kubernetes cluster **v1.11** or newer and Gloo ingress **0.13.29** or newer. + +Install Gloo with Helm: + +```bash +helm repo add gloo https://storage.googleapis.com/solo-public-helm + +helm upgrade -i gloo gloo/gloo \ +--namespace gloo-system +``` + +Install Flagger and the Prometheus add-on in the same namespace as Gloo: + +```bash +helm repo add flagger https://flagger.app + +helm upgrade -i flagger flagger/flagger \ +--namespace gloo-system \ +--set prometheus.install=true \ +--set meshProvider=gloo +``` + +Optionally you can enable Slack notifications: + +```bash +helm upgrade -i flagger flagger/flagger \ +--reuse-values \ +--namespace gloo-system \ +--set slack.url=https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK \ +--set slack.channel=general \ +--set slack.user=flagger +``` + +### Bootstrap + +Flagger takes a Kubernetes deployment and optionally a horizontal pod autoscaler (HPA), +then creates a series of objects (Kubernetes deployments, ClusterIP services and Gloo upstream groups). +These objects expose the application outside the cluster and drive the canary analysis and promotion. + +Create a test namespace: + +```bash +kubectl create ns test +``` + +Create a deployment and a horizontal pod autoscaler: + +```bash +kubectl apply -f ${REPO}/artifacts/gloo/deployment.yaml +kubectl apply -f ${REPO}/artifacts/gloo/hpa.yaml +``` + +Deploy the load testing service to generate traffic during the canary analysis: + +```bash +helm upgrade -i flagger-loadtester flagger/loadtester \ +--namespace=test +``` + +Create an virtual service definition that references an upstream group that will be generated by Flagger +(replace `app.example.com` with your own domain): + +```yaml +apiVersion: gateway.solo.io/v1 +kind: VirtualService +metadata: + name: podinfo + namespace: test +spec: + virtualHost: + domains: + - 'app.example.com' + name: podinfo.test + routes: + - matcher: + prefix: / + routeAction: + upstreamGroup: + name: podinfo + namespace: test +``` + +Save the above resource as podinfo-virtualservice.yaml and then apply it: + +```bash +kubectl apply -f ./podinfo-virtualservice.yaml +``` + +Create a canary custom resource (replace `app.example.com` with your own domain): + +```yaml +apiVersion: flagger.app/v1alpha3 +kind: Canary +metadata: + name: podinfo + namespace: test +spec: + # deployment reference + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + # HPA reference (optional) + autoscalerRef: + apiVersion: autoscaling/v2beta1 + kind: HorizontalPodAutoscaler + name: podinfo + # the maximum time in seconds for the canary deployment + # to make progress before it is rollback (default 600s) + progressDeadlineSeconds: 60 + service: + # container port + port: 9898 + canaryAnalysis: + # schedule interval (default 60s) + interval: 10s + # max number of failed metric checks before rollback + threshold: 5 + # max traffic percentage routed to canary + # percentage (0-100) + maxWeight: 50 + # canary increment step + # percentage (0-100) + stepWeight: 5 + # Gloo Prometheus checks + metrics: + - name: request-success-rate + # minimum req success rate (non 5xx responses) + # percentage (0-100) + threshold: 99 + interval: 1m + - name: request-duration + # maximum req duration P99 + # milliseconds + threshold: 500 + interval: 30s + # load testing (optional) + webhooks: + - name: load-test + url: http://flagger-loadtester.test/ + timeout: 5s + metadata: + type: cmd + cmd: "hey -z 1m -q 10 -c 2 http://app.example.com/" +``` + +Save the above resource as podinfo-canary.yaml and then apply it: + +```bash +kubectl apply -f ./podinfo-canary.yaml +``` + +After a couple of seconds Flagger will create the canary objects: + +```bash +# applied +deployment.apps/podinfo +horizontalpodautoscaler.autoscaling/podinfo +virtualservices.gateway.solo.io/podinfo +canary.flagger.app/podinfo + +# generated +deployment.apps/podinfo-primary +horizontalpodautoscaler.autoscaling/podinfo-primary +service/podinfo +service/podinfo-canary +service/podinfo-primary +upstreamgroups.gloo.solo.io/podinfo +``` + +When the bootstrap finishes Flagger will set the canary status to initialized: + +```bash +kubectl -n test get canary podinfo + +NAME STATUS WEIGHT LASTTRANSITIONTIME +podinfo Initialized 0 2019-05-17T08:09:51Z +``` + +### Automated canary promotion + +Flagger implements a control loop that gradually shifts traffic to the canary while measuring key performance indicators +like HTTP requests success rate, requests average duration and pod health. +Based on analysis of the KPIs a canary is promoted or aborted, and the analysis result is published to Slack. + +![Flagger Canary Stages](https://raw.githubusercontent.com/weaveworks/flagger/master/docs/diagrams/flagger-canary-steps.png) + +Trigger a canary deployment by updating the container image: + +```bash +kubectl -n test set image deployment/podinfo \ +podinfod=quay.io/stefanprodan/podinfo:1.4.1 +``` + +Flagger detects that the deployment revision changed and starts a new rollout: + +```text +kubectl -n test describe canary/podinfo + +Status: + Canary Weight: 0 + Failed Checks: 0 + Phase: Succeeded +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Synced 3m flagger New revision detected podinfo.test + Normal Synced 3m flagger Scaling up podinfo.test + Warning Synced 3m flagger Waiting for podinfo.test rollout to finish: 0 of 1 updated replicas are available + Normal Synced 3m flagger Advance podinfo.test canary weight 5 + Normal Synced 3m flagger Advance podinfo.test canary weight 10 + Normal Synced 3m flagger Advance podinfo.test canary weight 15 + Normal Synced 2m flagger Advance podinfo.test canary weight 20 + Normal Synced 2m flagger Advance podinfo.test canary weight 25 + Normal Synced 1m flagger Advance podinfo.test canary weight 30 + Normal Synced 1m flagger Advance podinfo.test canary weight 35 + Normal Synced 55s flagger Advance podinfo.test canary weight 40 + Normal Synced 45s flagger Advance podinfo.test canary weight 45 + Normal Synced 35s flagger Advance podinfo.test canary weight 50 + Normal Synced 25s flagger Copying podinfo.test template spec to podinfo-primary.test + Warning Synced 15s flagger Waiting for podinfo-primary.test rollout to finish: 1 of 2 updated replicas are available + Normal Synced 5s flagger Promotion completed! Scaling down podinfo.test +``` + +**Note** that if you apply new changes to the deployment during the canary analysis, Flagger will restart the analysis. + +You can monitor all canaries with: + +```bash +watch kubectl get canaries --all-namespaces + +NAMESPACE NAME STATUS WEIGHT LASTTRANSITIONTIME +test podinfo Progressing 15 2019-05-17T14:05:07Z +prod frontend Succeeded 0 2019-05-17T16:15:07Z +prod backend Failed 0 2019-05-17T17:05:07Z +``` + +### Automated rollback + +During the canary analysis you can generate HTTP 500 errors and high latency to test if Flagger pauses and rolls back the faulted version. + +Trigger another canary deployment: + +```bash +kubectl -n test set image deployment/podinfo \ +podinfod=quay.io/stefanprodan/podinfo:1.4.2 +``` + +Generate HTTP 500 errors: + +```bash +watch curl http://app.example.com/status/500 +``` + +Generate high latency: + +```bash +watch curl http://app.example.com/delay/2 +``` + +When the number of failed checks reaches the canary analysis threshold, the traffic is routed back to the primary, +the canary is scaled to zero and the rollout is marked as failed. + +```text +kubectl -n test describe canary/podinfo + +Status: + Canary Weight: 0 + Failed Checks: 10 + Phase: Failed +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Synced 3m flagger Starting canary deployment for podinfo.test + Normal Synced 3m flagger Advance podinfo.test canary weight 5 + Normal Synced 3m flagger Advance podinfo.test canary weight 10 + Normal Synced 3m flagger Advance podinfo.test canary weight 15 + Normal Synced 3m flagger Halt podinfo.test advancement success rate 69.17% < 99% + Normal Synced 2m flagger Halt podinfo.test advancement success rate 61.39% < 99% + Normal Synced 2m flagger Halt podinfo.test advancement success rate 55.06% < 99% + Normal Synced 2m flagger Halt podinfo.test advancement success rate 47.00% < 99% + Normal Synced 2m flagger (combined from similar events): Halt podinfo.test advancement success rate 38.08% < 99% + Warning Synced 1m flagger Rolling back podinfo.test failed checks threshold reached 10 + Warning Synced 1m flagger Canary failed! Scaling down podinfo.test +``` + +### Custom metrics + +The canary analysis can be extended with Prometheus queries. + +The demo app is instrumented with Prometheus so you can create a custom check that will use the HTTP request duration +histogram to validate the canary. + +Edit the canary analysis and add the following metric: + +```yaml + canaryAnalysis: + metrics: + - name: "404s percentage" + threshold: 5 + query: | + 100 - sum( + rate( + http_request_duration_seconds_count{ + kubernetes_namespace="test", + kubernetes_pod_name=~"podinfo-[0-9a-zA-Z]+(-[0-9a-zA-Z]+)" + status!="404" + }[1m] + ) + ) + / + sum( + rate( + http_request_duration_seconds_count{ + kubernetes_namespace="test", + kubernetes_pod_name=~"podinfo-[0-9a-zA-Z]+(-[0-9a-zA-Z]+)" + }[1m] + ) + ) * 100 +``` + +The above configuration validates the canary by checking if the HTTP 404 req/sec percentage is below 5 +percent of the total traffic. If the 404s rate reaches the 5% threshold, then the canary fails. + +Trigger a canary deployment by updating the container image: + +```bash +kubectl -n test set image deployment/podinfo \ +podinfod=quay.io/stefanprodan/podinfo:1.4.3 +``` + +Generate 404s: + +```bash +watch curl http://app.example.com/status/400 +``` + +Watch Flagger logs: + +``` +kubectl -n gloo-system logs deployment/flagger -f | jq .msg + +Starting canary deployment for podinfo.test +Advance podinfo.test canary weight 5 +Advance podinfo.test canary weight 10 +Advance podinfo.test canary weight 15 +Halt podinfo.test advancement 404s percentage 6.20 > 5 +Halt podinfo.test advancement 404s percentage 6.45 > 5 +Halt podinfo.test advancement 404s percentage 7.60 > 5 +Halt podinfo.test advancement 404s percentage 8.69 > 5 +Halt podinfo.test advancement 404s percentage 9.70 > 5 +Rolling back podinfo.test failed checks threshold reached 5 +Canary failed! Scaling down podinfo.test +``` + +If you have Slack configured, Flagger will send a notification with the reason why the canary failed. + + From 1e29e2c4ebe313d976927a1f8f767ad8df622ca9 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sun, 19 May 2019 10:34:36 +0300 Subject: [PATCH 4/4] Fix Grafana Prometheus URL --- docs/gitbook/install/flagger-install-on-eks-appmesh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gitbook/install/flagger-install-on-eks-appmesh.md b/docs/gitbook/install/flagger-install-on-eks-appmesh.md index 761ba436..7e4edde1 100644 --- a/docs/gitbook/install/flagger-install-on-eks-appmesh.md +++ b/docs/gitbook/install/flagger-install-on-eks-appmesh.md @@ -163,7 +163,7 @@ Deploy Grafana in the _**appmesh-system**_ namespace: ```bash helm upgrade -i flagger-grafana flagger/grafana \ --namespace=appmesh-system \ ---set url=http://prometheus.appmesh-system:9090 +--set url=http://flagger-prometheus.appmesh-system:9090 ``` You can access Grafana using port forwarding: