From b9eec94e6057656807153921f847bd32a15565ae Mon Sep 17 00:00:00 2001 From: Jesse Suen Date: Fri, 29 Jun 2018 01:26:59 -0700 Subject: [PATCH] Add blue green deployment example app --- blue-green-deploy/.gitignore | 4 + blue-green-deploy/app.yaml | 11 ++ .../components/bg-guestbook.jsonnet | 64 ++++++++ .../components/bg-workflow.jsonnet | 20 +++ blue-green-deploy/components/params.libsonnet | 19 +++ .../components/wf/bluegreen.yaml | 150 ++++++++++++++++++ blue-green-deploy/environments/base.libsonnet | 4 + .../environments/default/globals.libsonnet | 2 + .../environments/default/main.jsonnet | 8 + .../environments/default/params.libsonnet | 17 ++ 10 files changed, 299 insertions(+) create mode 100644 blue-green-deploy/.gitignore create mode 100644 blue-green-deploy/app.yaml create mode 100644 blue-green-deploy/components/bg-guestbook.jsonnet create mode 100644 blue-green-deploy/components/bg-workflow.jsonnet create mode 100644 blue-green-deploy/components/params.libsonnet create mode 100644 blue-green-deploy/components/wf/bluegreen.yaml create mode 100644 blue-green-deploy/environments/base.libsonnet create mode 100644 blue-green-deploy/environments/default/globals.libsonnet create mode 100644 blue-green-deploy/environments/default/main.jsonnet create mode 100644 blue-green-deploy/environments/default/params.libsonnet diff --git a/blue-green-deploy/.gitignore b/blue-green-deploy/.gitignore new file mode 100644 index 0000000..f8714d3 --- /dev/null +++ b/blue-green-deploy/.gitignore @@ -0,0 +1,4 @@ +/lib +/.ksonnet/registries +/app.override.yaml +/.ks_environment diff --git a/blue-green-deploy/app.yaml b/blue-green-deploy/app.yaml new file mode 100644 index 0000000..5178e12 --- /dev/null +++ b/blue-green-deploy/app.yaml @@ -0,0 +1,11 @@ +apiVersion: 0.1.0 +environments: + default: + destination: + namespace: default + server: https://kubernetes.default.svc + k8sVersion: v1.10.0 + path: default +kind: ksonnet.io/app +name: blue-green-deploy +version: 0.0.1 diff --git a/blue-green-deploy/components/bg-guestbook.jsonnet b/blue-green-deploy/components/bg-guestbook.jsonnet new file mode 100644 index 0000000..a059cc3 --- /dev/null +++ b/blue-green-deploy/components/bg-guestbook.jsonnet @@ -0,0 +1,64 @@ +local env = std.extVar("__ksonnet/environments"); +local params = std.extVar("__ksonnet/params").components["bg-guestbook"]; +[ + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": params.name, + "annotations": { + "argocd.argoproj.io/hook": "Skip", + }, + }, + "spec": { + "ports": [ + { + "port": params.servicePort, + "targetPort": params.containerPort + } + ], + "selector": { + "app": params.name + }, + "type": params.type + } + }, + { + "apiVersion": "apps/v1beta2", + "kind": "Deployment", + "metadata": { + "name": params.name, + "annotations": { + "argocd.argoproj.io/hook": "Skip", + }, + }, + "spec": { + "replicas": params.replicas, + "selector": { + "matchLabels": { + "app": params.name + }, + }, + "template": { + "metadata": { + "labels": { + "app": params.name + } + }, + "spec": { + "containers": [ + { + "image": params.image, + "name": params.name, + "ports": [ + { + "containerPort": params.containerPort + } + ] + } + ] + } + } + } + } +] diff --git a/blue-green-deploy/components/bg-workflow.jsonnet b/blue-green-deploy/components/bg-workflow.jsonnet new file mode 100644 index 0000000..c14af2a --- /dev/null +++ b/blue-green-deploy/components/bg-workflow.jsonnet @@ -0,0 +1,20 @@ +local bgGuestbook = std.extVar("__ksonnet/components")["bg-guestbook"]; +local bgGuestbookSvc = bgGuestbook[0]; +local bgGuestbookDeploy = bgGuestbook[1]; +local parseYaml = std.native("parseYaml"); +local bgWorkflow = parseYaml(importstr 'wf/bluegreen.yaml')[0]; + +[ + bgWorkflow + { + spec +: { + arguments +: { + parameters : [ + {name: "deployment-name", value: bgGuestbookDeploy.metadata.name}, + {name: "service-name", value: bgGuestbookSvc.metadata.name}, + {name: "new-deployment-manifest", value: std.manifestJson(bgGuestbookDeploy)}, + {name: "new-service-manifest", value: std.manifestJson(bgGuestbookSvc)}, + ], + }, + }, + } +] \ No newline at end of file diff --git a/blue-green-deploy/components/params.libsonnet b/blue-green-deploy/components/params.libsonnet new file mode 100644 index 0000000..dc9944f --- /dev/null +++ b/blue-green-deploy/components/params.libsonnet @@ -0,0 +1,19 @@ +{ + global: { + // User-defined global parameters; accessible to all component and environments, Ex: + // replicas: 4, + }, + components: { + // Component-level parameters, defined initially from 'ks prototype use ...' + // Each object below should correspond to a component in the components/ directory + "bg-guestbook": { + containerPort: 80, + image: "gcr.io/heptio-images/ks-guestbook-demo:0.1", + name: "bg-guestbook", + replicas: 3, + servicePort: 80, + type: "LoadBalancer", + }, + "bg-workflow": {}, + }, +} diff --git a/blue-green-deploy/components/wf/bluegreen.yaml b/blue-green-deploy/components/wf/bluegreen.yaml new file mode 100644 index 0000000..3749fdf --- /dev/null +++ b/blue-green-deploy/components/wf/bluegreen.yaml @@ -0,0 +1,150 @@ +# This workflow performs a "blue-green" deployment, while preserving the original deployment object +# and name. It accomplishes this by temporarily redirecting traffic to a *clone* of the original +# deployment. Then after upgrading the original deployment to a later version, redirects traffic +# back to the original (now upgraded) deployment. +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: k8s-bluegreen- + annotations: + argocd.argoproj.io/hook: Sync +spec: + entrypoint: k8s-bluegreen + arguments: + parameters: + - name: deployment-name + - name: service-name + - name: new-deployment-manifest + - name: new-service-manifest + + templates: + - name: k8s-bluegreen + steps: + # 1. Create a parallel Kubernetes deployment with tweaks to name and selectors + - - name: create-blue-deployment + template: clone-deployment + + # 2. Wait for parallel deployment to become ready + - - name: wait-for-blue-deployment + template: wait-deployment-ready + arguments: + parameters: + - name: deployment-name + value: '{{workflow.parameters.deployment-name}}-blue' + + # 3. Patch the named service to point to the parallel deployment app + - - name: switch-service-to-blue-deployment + template: patch-service + + # 4. Update the original deployment (receiving no traffic) with a new version + - - name: apply-green-deployment + template: apply-manifest + arguments: + parameters: + - name: manifest + value: '{{workflow.parameters.new-deployment-manifest}}' + + # 5. Wait for the original deployment, now updated, to become ready + - - name: wait-for-green-deployment + template: wait-deployment-ready + arguments: + parameters: + - name: deployment-name + value: '{{workflow.parameters.deployment-name}}' + + # dummy approval step for demo purposes. Sleeps for 30 seconds + - - name: approve + template: approve + + # 6. Patch the named service to point to the original, now updated app + - - name: switch-service-to-green-deployment + template: apply-manifest + arguments: + parameters: + - name: manifest + value: '{{workflow.parameters.new-service-manifest}}' + + # 7. Remove the cloned deployment (no longer receiving traffic) + - - name: delete-cloned-deployment + template: delete-deployment + +# end of steps + + # clone-deployment creates a "blue" clone of an existing deployment. The string -blue is appended to: + # - metadata.name + # - spec.selector.matchLabels + # - spec.template.metadata.labels + - name: clone-deployment + container: + image: argoproj/argoexec:v2.1.1 + command: [sh, -c, -x] + args: + - kubectl get --export -o json deployment.apps/{{workflow.parameters.deployment-name}} > /original-deploy && + jq -r '.metadata.name+="-blue" | + .spec.template.metadata.labels += (.spec.template.metadata.labels | to_entries | map(select(.key != "applications.argoproj.io/app-name")) | map(.value+="-blue") | from_entries) | + .spec.selector.matchLabels += (.spec.selector.matchLabels | to_entries | map(select(.key != "applications.argoproj.io/app-name")) | map(.value+="-blue") | from_entries)' + /original-deploy > /cloned-deploy && + cat /cloned-deploy && + kubectl apply -o yaml -f /cloned-deploy + + # apply-manifest takes a kubernetes manifest and carrys over the app-name label (if present) + # before running `kubectl apply`. The label is used by ArgoCD for monitoring. + - name: apply-manifest + inputs: + parameters: + - name: manifest + artifacts: + - name: manifest-file + path: /manifest + raw: + data: '{{inputs.parameters.manifest}}' + container: + image: argoproj/argoexec:v2.1.1 + command: [sh, -c, -x] + args: + - cp /manifest /manifest-new && + APP_NAME=$(kubectl get -n default -f /manifest-new -o json | jq -r '.metadata.labels."applications.argoproj.io/app-name"') && + if [ "$APP_NAME" != "null" ]; then + jq -r --arg APP_NAME "$APP_NAME" '.metadata.labels."applications.argoproj.io/app-name" = $APP_NAME' /manifest-new > /manifest-new.tmp && + mv /manifest-new.tmp /manifest-new && + if [ "$(jq -r .spec.template.metadata.labels /manifest-new)" != "null" ]; then + jq -r --arg APP_NAME "$APP_NAME" '.spec.template.metadata.labels."applications.argoproj.io/app-name" = $APP_NAME' /manifest-new > /manifest-new.tmp && + mv /manifest-new.tmp /manifest-new ; + fi ; + fi && + cat /manifest-new && + kubectl apply -f /manifest-new + + # wait-deployment-ready waits for a deployment to become fully deployed and ready, using the + # `kubectl rollout` command + - name: wait-deployment-ready + inputs: + parameters: + - name: deployment-name + container: + image: argoproj/argoexec:v2.1.1 + command: [kubectl, rollout, status, --watch=true, 'deployments/{{inputs.parameters.deployment-name}}'] + + # patch-service updates the service selector labels to point to the "blue" deployment + - name: patch-service + container: + image: argoproj/argoexec:v2.1.1 + command: [sh, -c, -x] + args: + - kubectl get -n default service {{workflow.parameters.service-name}} --export -o json > /original-svc && + jq '.spec.selector = (.spec.selector | with_entries(.value+="-blue"))' /original-svc > /blue-svc && + kubectl apply -o yaml -f /blue-svc + + - name: delete-deployment + resource: + action: delete + manifest: | + apiVersion: apps/v1 + kind: Deployment + metadata: + name: {{workflow.parameters.deployment-name}}-blue + + - name: approve + container: + image: argoproj/argoexec:v2.1.1 + command: [sleep, "30"] diff --git a/blue-green-deploy/environments/base.libsonnet b/blue-green-deploy/environments/base.libsonnet new file mode 100644 index 0000000..a129aff --- /dev/null +++ b/blue-green-deploy/environments/base.libsonnet @@ -0,0 +1,4 @@ +local components = std.extVar("__ksonnet/components"); +components + { + // Insert user-specified overrides here. +} diff --git a/blue-green-deploy/environments/default/globals.libsonnet b/blue-green-deploy/environments/default/globals.libsonnet new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/blue-green-deploy/environments/default/globals.libsonnet @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/blue-green-deploy/environments/default/main.jsonnet b/blue-green-deploy/environments/default/main.jsonnet new file mode 100644 index 0000000..58695a8 --- /dev/null +++ b/blue-green-deploy/environments/default/main.jsonnet @@ -0,0 +1,8 @@ +local base = import "base.libsonnet"; +// uncomment if you reference ksonnet-lib +// local k = import "k.libsonnet"; + +base + { + // Insert user-specified overrides here. For example if a component is named \"nginx-deployment\", you might have something like:\n") + // "nginx-deployment"+: k.deployment.mixin.metadata.labels({foo: "bar"}) +} diff --git a/blue-green-deploy/environments/default/params.libsonnet b/blue-green-deploy/environments/default/params.libsonnet new file mode 100644 index 0000000..b6eb32d --- /dev/null +++ b/blue-green-deploy/environments/default/params.libsonnet @@ -0,0 +1,17 @@ +local params = std.extVar("__ksonnet/params"); +local globals = import "globals.libsonnet"; +local envParams = params + { + components +: { + // Insert component parameter overrides here. Ex: + // guestbook +: { + // name: "guestbook-dev", + // replicas: params.global.replicas, + // }, + }, +}; + +{ + components: { + [x]: envParams.components[x] + globals, for x in std.objectFields(envParams.components) + }, +}