From 12d26874f8fe9da183fc887a902b9cf2c1205ca9 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sun, 10 Feb 2019 11:48:51 +0200 Subject: [PATCH] Add canary deployment demo chart based on podinfo --- charts/podinfo/.helmignore | 21 +++++ charts/podinfo/Chart.yaml | 12 +++ charts/podinfo/README.md | 79 +++++++++++++++++ charts/podinfo/templates/NOTES.txt | 1 + charts/podinfo/templates/_helpers.tpl | 33 +++++++ charts/podinfo/templates/canary.yaml | 54 ++++++++++++ charts/podinfo/templates/deployment.yaml | 86 +++++++++++++++++++ charts/podinfo/templates/hpa.yaml | 32 +++++++ charts/podinfo/templates/service.yaml | 20 +++++ .../podinfo/templates/tests/test-config.yaml | 26 ++++++ charts/podinfo/templates/tests/test-pod.yaml | 43 ++++++++++ charts/podinfo/values.yaml | 70 +++++++++++++++ 12 files changed, 477 insertions(+) create mode 100644 charts/podinfo/.helmignore create mode 100644 charts/podinfo/Chart.yaml create mode 100644 charts/podinfo/README.md create mode 100644 charts/podinfo/templates/NOTES.txt create mode 100644 charts/podinfo/templates/_helpers.tpl create mode 100644 charts/podinfo/templates/canary.yaml create mode 100644 charts/podinfo/templates/deployment.yaml create mode 100644 charts/podinfo/templates/hpa.yaml create mode 100644 charts/podinfo/templates/service.yaml create mode 100755 charts/podinfo/templates/tests/test-config.yaml create mode 100755 charts/podinfo/templates/tests/test-pod.yaml create mode 100644 charts/podinfo/values.yaml diff --git a/charts/podinfo/.helmignore b/charts/podinfo/.helmignore new file mode 100644 index 00000000..f0c13194 --- /dev/null +++ b/charts/podinfo/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/charts/podinfo/Chart.yaml b/charts/podinfo/Chart.yaml new file mode 100644 index 00000000..d54ecf63 --- /dev/null +++ b/charts/podinfo/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +version: 2.0.0 +appVersion: 1.4.0 +name: podinfo +engine: gotpl +description: Flagger canary deployment demo chart +home: https://github.com/stefanprodan/flagger +maintainers: +- email: stefanprodan@users.noreply.github.com + name: stefanprodan +sources: +- https://github.com/stefanprodan/flagger diff --git a/charts/podinfo/README.md b/charts/podinfo/README.md new file mode 100644 index 00000000..6a626022 --- /dev/null +++ b/charts/podinfo/README.md @@ -0,0 +1,79 @@ +# Podinfo + +Podinfo is a tiny web application made with Go +that showcases best practices of running canary deployments with Flagger and Istio. + +## Installing the Chart + +Add Flagger Helm repository: + +```console +helm repo add flagger https://flagger.app +``` + +To install the chart with the release name `frontend`: + +```console +helm upgrade -i frontend flagger/podinfo \ +--namespace test \ +--set nameOverride=frontend \ +--set backend=http://backend.test:9898/echo \ +--set canary.enabled=true \ +--set canary.istioIngress.enabled=true \ +--set canary.istioIngress.gateway=public-gateway.istio-system.svc.cluster.local \ +--set canary.istioIngress.host=frontend.istio.example.com +``` + +To install the chart as `backend`: + +```console +helm upgrade -i backend flagger/podinfo \ +--namespace test \ +--set nameOverride=backend \ +--set canary.enabled=true +``` + +## Uninstalling the Chart + +To uninstall/delete the `frontend` deployment: + +```console +$ helm delete --purge frontend +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Configuration + +The following tables lists the configurable parameters of the podinfo chart and their default values. + +Parameter | Description | Default +--- | --- | --- +`image.repository` | image repository | `quay.io/stefanprodan/podinfo` +`image.tag` | image tag | `` +`image.pullPolicy` | image pull policy | `IfNotPresent` +`hpa.enabled` | enables HPA | `true` +`hpa.cpu` | target CPU usage per pod | `80` +`hpa.memory` | target memory usage per pod | `512Mi` +`hpa.minReplicas` | maximum pod replicas | `2` +`hpa.maxReplicas` | maximum pod replicas | `4` +`resources.requests/cpu` | pod CPU request | `1m` +`resources.requests/memory` | pod memory request | `16Mi` +`backend` | backend URL | None +`faults.delay` | random HTTP response delays between 0 and 5 seconds | `false` +`faults.error` | 1/3 chances of a random HTTP response error | `false` + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, + +```console +$ helm install flagger/podinfo --name frontend \ + --set=image.tag=1.4.1,hpa.enabled=false +``` + +Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example, + +```console +$ helm install flagger/podinfo --name frontend -f values.yaml +``` + + diff --git a/charts/podinfo/templates/NOTES.txt b/charts/podinfo/templates/NOTES.txt new file mode 100644 index 00000000..92c7709d --- /dev/null +++ b/charts/podinfo/templates/NOTES.txt @@ -0,0 +1 @@ +podinfo {{ .Release.Name }} deployed! \ No newline at end of file diff --git a/charts/podinfo/templates/_helpers.tpl b/charts/podinfo/templates/_helpers.tpl new file mode 100644 index 00000000..60805fe6 --- /dev/null +++ b/charts/podinfo/templates/_helpers.tpl @@ -0,0 +1,33 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "podinfo.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "podinfo.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "podinfo.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + diff --git a/charts/podinfo/templates/canary.yaml b/charts/podinfo/templates/canary.yaml new file mode 100644 index 00000000..92cc5865 --- /dev/null +++ b/charts/podinfo/templates/canary.yaml @@ -0,0 +1,54 @@ +{{- if .Values.canary.enabled }} +apiVersion: flagger.app/v1alpha3 +kind: Canary +metadata: + name: {{ template "podinfo.fullname" . }} + labels: + app: {{ template "podinfo.name" . }} + chart: {{ template "podinfo.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ template "podinfo.fullname" . }} + progressDeadlineSeconds: 60 + autoscalerRef: + apiVersion: autoscaling/v2beta1 + kind: HorizontalPodAutoscaler + name: {{ template "podinfo.fullname" . }} + service: + port: {{ .Values.service.port }} + {{- if .Values.canary.istioIngress.enabled }} + gateways: + - {{ .Values.canary.istioIngress.gateway }} + hosts: + - {{ .Values.canary.istioIngress.host }} + {{- end }} + canaryAnalysis: + interval: {{ .Values.canary.analysis.interval }} + threshold: {{ .Values.canary.analysis.threshold }} + maxWeight: {{ .Values.canary.analysis.maxWeight }} + stepWeight: {{ .Values.canary.analysis.stepWeight }} + metrics: + - name: istio_requests_total + threshold: {{ .Values.canary.thresholds.successRate }} + interval: 1m + - name: istio_request_duration_seconds_bucket + threshold: {{ .Values.canary.thresholds.latency }} + interval: 1m + {{- if .Values.canary.loadtest.enabled }} + webhooks: + - name: load-test-get + url: {{ .Values.canary.loadtest.url }} + timeout: 5s + metadata: + cmd: "hey -z 1m -q 5 -c 2 http://{{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.port }}" + - name: load-test-post + url: {{ .Values.canary.loadtest.url }} + timeout: 5s + metadata: + cmd: "hey -z 1m -q 5 -c 2 -m POST -d '{\"test\": true}' http://{{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.port }}/echo" + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/podinfo/templates/deployment.yaml b/charts/podinfo/templates/deployment.yaml new file mode 100644 index 00000000..20a6751e --- /dev/null +++ b/charts/podinfo/templates/deployment.yaml @@ -0,0 +1,86 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "podinfo.fullname" . }} + labels: + app: {{ template "podinfo.name" . }} + chart: {{ template "podinfo.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + selector: + matchLabels: + app: {{ template "podinfo.fullname" . }} + template: + metadata: + labels: + app: {{ template "podinfo.fullname" . }} + annotations: + prometheus.io/scrape: 'true' + spec: + terminationGracePeriodSeconds: 30 + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - ./podinfo + - --port={{ .Values.service.port }} + - --level={{ .Values.logLevel }} + - --random-delay={{ .Values.faults.delay }} + - --random-error={{ .Values.faults.error }} + env: + {{- if .Values.message }} + - name: PODINFO_UI_MESSAGE + value: {{ .Values.message }} + {{- end }} + {{- if .Values.backend }} + - name: PODINFO_BACKEND_URL + value: {{ .Values.backend }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:{{ .Values.service.port }}/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:{{ .Values.service.port }}/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + volumeMounts: + - name: data + mountPath: /data + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + volumes: + - name: data + emptyDir: {} diff --git a/charts/podinfo/templates/hpa.yaml b/charts/podinfo/templates/hpa.yaml new file mode 100644 index 00000000..dcb36d3a --- /dev/null +++ b/charts/podinfo/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.hpa.enabled -}} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ template "podinfo.fullname" . }} +spec: + scaleTargetRef: + apiVersion: apps/v1beta2 + kind: Deployment + name: {{ template "podinfo.fullname" . }} + minReplicas: {{ .Values.hpa.minReplicas }} + maxReplicas: {{ .Values.hpa.maxReplicas }} + metrics: + {{- if .Values.hpa.cpu }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.hpa.cpu }} + {{- end }} + {{- if .Values.hpa.memory }} + - type: Resource + resource: + name: memory + targetAverageValue: {{ .Values.hpa.memory }} + {{- end }} + {{- if .Values.hpa.requests }} + - type: Pod + pods: + metricName: http_requests + targetAverageValue: {{ .Values.hpa.requests }} + {{- end }} +{{- end }} diff --git a/charts/podinfo/templates/service.yaml b/charts/podinfo/templates/service.yaml new file mode 100644 index 00000000..82b9451c --- /dev/null +++ b/charts/podinfo/templates/service.yaml @@ -0,0 +1,20 @@ +{{- if not .Values.canary.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "podinfo.fullname" . }} + labels: + app: {{ template "podinfo.name" . }} + chart: {{ template "podinfo.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app: {{ template "podinfo.fullname" . }} +{{- end }} \ No newline at end of file diff --git a/charts/podinfo/templates/tests/test-config.yaml b/charts/podinfo/templates/tests/test-config.yaml new file mode 100755 index 00000000..dce297a1 --- /dev/null +++ b/charts/podinfo/templates/tests/test-config.yaml @@ -0,0 +1,26 @@ +{{- $suffix := "" -}} +{{- if .Values.canary.enabled -}} +{{- $suffix = "-primary" -}} +{{- end -}} +{{- $url := printf "%s%s:%v" (include "podinfo.fullname" .) $suffix .Values.service.port -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "podinfo.fullname" . }}-tests + labels: + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + app: {{ template "podinfo.name" . }} +data: + run.sh: |- + @test "HTTP POST /echo" { + run curl --retry 3 --connect-timeout 2 -sSX POST -d 'test' {{ $url }}/echo + [ $output = "test" ] + } + @test "HTTP POST /store" { + curl --retry 3 --connect-timeout 2 -sSX POST -d 'test' {{ $url }}/store + } + @test "HTTP GET /" { + curl --retry 3 --connect-timeout 2 -sS {{ $url }} | grep hostname + } diff --git a/charts/podinfo/templates/tests/test-pod.yaml b/charts/podinfo/templates/tests/test-pod.yaml new file mode 100755 index 00000000..5105ef4b --- /dev/null +++ b/charts/podinfo/templates/tests/test-pod.yaml @@ -0,0 +1,43 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ template "podinfo.fullname" . }}-tests-{{ randAlphaNum 5 | lower }} + annotations: + "helm.sh/hook": test-success + sidecar.istio.io/inject: "false" + labels: + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + app: {{ template "podinfo.name" . }} +spec: + initContainers: + - name: "test-framework" + image: "dduportal/bats:0.4.0" + command: + - "bash" + - "-c" + - | + set -ex + # copy bats to tools dir + cp -R /usr/local/libexec/ /tools/bats/ + volumeMounts: + - mountPath: /tools + name: tools + containers: + - name: {{ .Release.Name }}-ui-test + image: dduportal/bats:0.4.0 + command: ["/tools/bats/bats", "-t", "/tests/run.sh"] + volumeMounts: + - mountPath: /tests + name: tests + readOnly: true + - mountPath: /tools + name: tools + volumes: + - name: tests + configMap: + name: {{ template "podinfo.fullname" . }}-tests + - name: tools + emptyDir: {} + restartPolicy: Never diff --git a/charts/podinfo/values.yaml b/charts/podinfo/values.yaml new file mode 100644 index 00000000..3768d618 --- /dev/null +++ b/charts/podinfo/values.yaml @@ -0,0 +1,70 @@ +# Default values for podinfo. +image: + repository: quay.io/stefanprodan/podinfo + tag: 1.4.0 + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 9898 + +hpa: + enabled: true + minReplicas: 2 + maxReplicas: 4 + cpu: 80 + memory: 512Mi + +canary: + enabled: false + istioIngress: + enabled: false + # Istio ingress gateway name + gateway: public-gateway.istio-system.svc.cluster.local + # external host name eg. podinfo.example.com + host: + analysis: + # schedule interval (default 60s) + interval: 15s + # max number of failed metric checks before rollback + threshold: 10 + # max traffic percentage routed to canary + # percentage (0-100) + maxWeight: 50 + # canary increment step + # percentage (0-100) + stepWeight: 2 + thresholds: + # minimum req success rate (non 5xx responses) + # percentage (0-100) + successRate: 99 + # maximum req duration P99 + # milliseconds + latency: 500 + loadtest: + enabled: true + # load tester address + url: http://flagger-loadtester.test/ + +resources: + limits: + requests: + cpu: 100m + memory: 32Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +nameOverride: "" +fullnameOverride: "" + +logLevel: info +backend: #http://backend-podinfo:9898/echo +message: #UI greetings + +faults: + delay: false + error: false \ No newline at end of file