All Policies

Ensure Production Matches staging

It is common to have two separate Namespaces such as staging and production in order to test and promote app deployments in a controlled manner. In order to ensure that level of control, certain guardrails must be present so as to minimize regressions or unintended behavior. This policy has a set of three rules to try and provide some sane defaults for app promotion across these two environments (Namespaces) called staging and production. First, it makes sure that every Deployment in production has a corresponding Deployment in staging. Second, that a production Deployment uses same image name as its staging counterpart. Third, that a production Deployment uses an older or equal image version as its staging counterpart.

Policy Definition

/other/ensure-production-matches-staging/ensure-production-matches-staging.yaml

  1apiVersion: kyverno.io/v1
  2kind: ClusterPolicy
  3metadata:
  4  name: ensure-production-matches-staging
  5  annotations:
  6    policies.kyverno.io/title: Ensure Production Matches staging
  7    policies.kyverno.io/category: Other
  8    policies.kyverno.io/minversion: 1.6.0
  9    kyverno.io/kyverno-version: 1.7.0
 10    kyverno.io/kubernetes-version: "1.23"
 11    policies.kyverno.io/subject: Deployment
 12    policies.kyverno.io/description: >-
 13      It is common to have two separate Namespaces such as staging and production
 14      in order to test and promote app deployments in a controlled manner. In order to ensure
 15      that level of control, certain guardrails must be present so as to minimize regressions or
 16      unintended behavior. This policy has a set of three rules to try and provide some sane defaults
 17      for app promotion across these two environments (Namespaces) called staging and production. First,
 18      it makes sure that every Deployment in production has a corresponding Deployment in staging. Second,
 19      that a production Deployment uses same image name as its staging counterpart. Third, that
 20      a production Deployment uses an older or equal image version as its staging counterpart.
 21spec:
 22  validationFailureAction: Enforce
 23  background: true
 24  rules:
 25#######################
 26# Ensures that each Deployment being created in production
 27# has an existing Deployment already in staging of the same name.
 28  - name: require-staging-deployment
 29    match:
 30      any:
 31      - resources:
 32          namespaces:
 33          - production
 34          kinds:
 35          - Deployment
 36    preconditions:
 37      any:
 38      - key: "{{request.operation || 'BACKGROUND'}}"
 39        operator: AnyIn
 40        value:
 41        - CREATE        
 42        - UPDATE
 43    context:
 44    - name: deployment_count
 45      apiCall:
 46        urlPath: "/apis/apps/v1/namespaces/staging/deployments"
 47        jmesPath: "items[?metadata.name=='{{ request.object.metadata.name }}'] || `[]` | length(@)"
 48    validate:
 49      message: "Every Deployment in production requires a corresponding Deployment in staging."
 50      deny:
 51        conditions:
 52          any:
 53          - key: "{{deployment_count}}"
 54            operator: Equals
 55            value: 0
 56#######################
 57# Ensures that each corresponding Deployment in staging and production
 58# Namespaces uses the same image name (not tag).
 59  - name: require-same-image
 60    match:
 61      any:
 62      - resources:
 63          namespaces:
 64          - production
 65          kinds:
 66          - Deployment
 67    preconditions:
 68      all:
 69      - key: "{{request.operation || 'BACKGROUND'}}"
 70        operator: AnyIn
 71        value:
 72        - CREATE        
 73        - UPDATE
 74      - key: "{{ deployment_count }}"
 75        operator: GreaterThan
 76        value: 0
 77    context:
 78    - name: deployment_count
 79      apiCall:
 80        urlPath: "/apis/apps/v1/namespaces/staging/deployments"
 81        jmesPath: "items[?metadata.name=='{{ request.object.metadata.name}}']  || `[]` | length(@)"
 82    - name: deployment_images
 83      apiCall:
 84        urlPath: "/apis/apps/v1/namespaces/staging/deployments/{{ request.object.metadata.name }}"
 85        jmesPath: "spec.template.spec.containers[].image.split(@, ':')[0]"
 86    validate:
 87      message: "Every Deployment in production is required to use the same image(s) as in staging."
 88      deny:
 89        conditions:
 90          any:
 91          - key: "{{ request.object.spec.template.spec.containers[].image.split(@, ':')[0]  }}"
 92            operator: AnyNotIn
 93            value: "{{ deployment_images }}"
 94#######################
 95# Ensures that each image version in production is less than or
 96# equal to its corresponding image version in staging. Uses the container
 97# name as the basis for comparison. Recommended to pair this policy
 98# with another policy or rule which enforces only semver image tags, or otherwise extend this rule
 99# with a precondition which filters out everything else.
100  - name: require-same-or-older-imageversion
101    match:
102      any:
103      - resources:
104          namespaces:
105          - production
106          kinds:
107          - Deployment
108    preconditions:
109      all:
110      - key: "{{request.operation || 'BACKGROUND'}}"
111        operator: AnyIn
112        value:
113        - CREATE        
114        - UPDATE
115      - key: "{{ deployment_count }}"
116        operator: GreaterThan
117        value: 0
118    context:
119    - name: deployment_count
120      apiCall:
121        urlPath: "/apis/apps/v1/namespaces/staging/deployments"
122        jmesPath: "items[? metadata.name == '{{ request.object.metadata.name }}'  ]  | length(@)"
123    - name: deployment_containers
124      apiCall:
125        urlPath: "/apis/apps/v1/namespaces/staging/deployments/{{ request.object.metadata.name }}"
126        jmesPath: "spec.template.spec.containers[]"
127    validate:
128      message: "Every Deployment in production is required to use an image version less than or equal to the one in staging."
129      foreach:
130      - list: "request.object.spec.template.spec.containers[]"
131        deny:
132          conditions:
133            any:
134            # Uses the container name as the basis for comparing the image tag. Only semver has been tested.
135            - key: "{{ element.image.split(@,':')[1] }}"
136              operator: GreaterThan
137              value: "{{ deployment_containers[?name == '{{element.name}}'].image.split(@,':')[1] | [0] }}"