{"id":58579,"date":"2026-01-28T11:16:53","date_gmt":"2026-01-28T11:16:53","guid":{"rendered":"https:\/\/www.devopsschool.com\/blog\/?p=58579"},"modified":"2026-01-28T11:16:53","modified_gmt":"2026-01-28T11:16:53","slug":"aws-eks-cluster-level-multi-az-enforcement-solutions","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/blog\/aws-eks-cluster-level-multi-az-enforcement-solutions\/","title":{"rendered":"AWS EKS &#8211; Cluster-Level Multi-AZ Enforcement Solutions"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\"><strong>\u274c What&#8217;s NOT Available in EKS<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Kubernetes Scheduler Configuration<\/strong>: EKS doesn&#8217;t allow modification of\u00a0<code>KubeSchedulerConfiguration<\/code>\u00a0to set custom default topology spread constraints<\/li>\n\n\n\n<li><strong>Built-in Cluster Defaults<\/strong>: No native EKS setting to enforce multi-AZ distribution automatically<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>\u2705 Available Solutions (Ranked by Effectiveness)<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Solution 1: Mutating Admission Webhook (Recommended)<\/strong><\/h3>\n\n\n\n<p>This automatically injects topology spread constraints into all deployments at creation time.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>A. Deploy the Mutating Webhook<\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># mutating-webhook-deployment.yaml\n<\/code><code>apiVersion: apps\/v1\n<\/code><code>kind: Deployment\n<\/code><code>metadata:\n<\/code><code>  name: multiaz-webhook\n<\/code><code>  namespace: kube-system\n<\/code><code>  labels:\n<\/code><code>    app: multiaz-webhook\n<\/code><code>spec:\n<\/code><code>  replicas: 2\n<\/code><code>  selector:\n<\/code><code>    matchLabels:\n<\/code><code>      app: multiaz-webhook\n<\/code><code>  template:\n<\/code><code>    metadata:\n<\/code><code>      labels:\n<\/code><code>        app: multiaz-webhook\n<\/code><code>    spec:\n<\/code><code>      serviceAccountName: multiaz-webhook\n<\/code><code>      containers:\n<\/code><code>      - name: webhook\n<\/code><code>        image: your-registry\/multiaz-webhook:latest\n<\/code><code>        ports:\n<\/code><code>        - containerPort: 8443\n<\/code><code>        env:\n<\/code><code>        - name: TLS_CERT_FILE\n<\/code><code>          value: \/etc\/certs\/tls.crt\n<\/code><code>        - name: TLS_PRIVATE_KEY_FILE\n<\/code><code>          value: \/etc\/certs\/tls.key\n<\/code><code>        volumeMounts:\n<\/code><code>        - name: certs\n<\/code><code>          mountPath: \/etc\/certs\n<\/code><code>          readOnly: true\n<\/code><code>        resources:\n<\/code><code>          requests:\n<\/code><code>            cpu: 100m\n<\/code><code>            memory: 128Mi\n<\/code><code>          limits:\n<\/code><code>            cpu: 200m\n<\/code><code>            memory: 256Mi\n<\/code><code>      volumes:\n<\/code><code>      - name: certs\n<\/code><code>        secret:\n<\/code><code>          secretName: multiaz-webhook-certs\n<\/code><code>---\n<\/code><code>apiVersion: v1\n<\/code><code>kind: Service\n<\/code><code>metadata:\n<\/code><code>  name: multiaz-webhook-service\n<\/code><code>  namespace: kube-system\n<\/code><code>spec:\n<\/code><code>  selector:\n<\/code><code>    app: multiaz-webhook\n<\/code><code>  ports:\n<\/code><code>  - port: 443\n<\/code><code>    targetPort: 8443\n<\/code><code>    protocol: TCP\n<\/code><code>---\n<\/code><code>apiVersion: v1\n<\/code><code>kind: ServiceAccount\n<\/code><code>metadata:\n<\/code><code>  name: multiaz-webhook\n<\/code><code>  namespace: kube-system\n<\/code><code>---\n<\/code><code>apiVersion: rbac.authorization.k8s.io\/v1\n<\/code><code>kind: ClusterRole\n<\/code><code>metadata:\n<\/code><code>  name: multiaz-webhook\n<\/code><code>rules:\n<\/code><code>- apiGroups: [\"\"]\n<\/code><code>  resources: [\"nodes\"]\n<\/code><code>  verbs: [\"get\", \"list\"]\n<\/code><code>- apiGroups: [\"apps\"]\n<\/code><code>  resources: [\"deployments\", \"replicasets\"]\n<\/code><code>  verbs: [\"get\", \"list\"]\n<\/code><code>---\n<\/code><code>apiVersion: rbac.authorization.k8s.io\/v1\n<\/code><code>kind: ClusterRoleBinding\n<\/code><code>metadata:\n<\/code><code>  name: multiaz-webhook\n<\/code><code>roleRef:\n<\/code><code>  apiGroup: rbac.authorization.k8s.io\n<\/code><code>  kind: ClusterRole\n<\/code><code>  name: multiaz-webhook\n<\/code><code>subjects:\n<\/code><code>- kind: ServiceAccount\n<\/code><code>  name: multiaz-webhook\n<\/code><code>  namespace: kube-system\n<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>B. Webhook Configuration<\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># mutating-webhook-config.yaml\n<\/code><code>apiVersion: admissionregistration.k8s.io\/v1\n<\/code><code>kind: MutatingAdmissionWebhook\n<\/code><code>metadata:\n<\/code><code>  name: multiaz-enforcer\n<\/code><code>webhooks:\n<\/code><code>- name: multiaz.enforcer.eks.aws\n<\/code><code>  clientConfig:\n<\/code><code>    service:\n<\/code><code>      name: multiaz-webhook-service\n<\/code><code>      namespace: kube-system\n<\/code><code>      path: \"\/mutate\"\n<\/code><code>  rules:\n<\/code><code>  - operations: [\"CREATE\", \"UPDATE\"]\n<\/code><code>    apiGroups: [\"apps\"]\n<\/code><code>    apiVersions: [\"v1\"]\n<\/code><code>    resources: [\"deployments\"]\n<\/code><code>  - operations: [\"CREATE\", \"UPDATE\"]\n<\/code><code>    apiGroups: [\"apps\"]\n<\/code><code>    apiVersions: [\"v1\"]\n<\/code><code>    resources: [\"replicasets\"]\n<\/code><code>  namespaceSelector:\n<\/code><code>    matchExpressions:\n<\/code><code>    - key: name\n<\/code><code>      operator: NotIn\n<\/code><code>      values: [\"kube-system\", \"kube-public\", \"kube-node-lease\"]\n<\/code><code>  objectSelector:\n<\/code><code>    matchExpressions:\n<\/code><code>    - key: multiaz.enforcer.eks.aws\/skip\n<\/code><code>      operator: DoesNotExist\n<\/code><code>  admissionReviewVersions: [\"v1\", \"v1beta1\"]\n<\/code><code>  sideEffects: None\n<\/code><code>  failurePolicy: Fail\n<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>C. Sample Webhook Code (Go)<\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>\/\/ webhook-server.go\n<\/code><code>package main\n<\/code><code>\n<\/code><code>import (\n<\/code><code>    \"context\"\n<\/code><code>    \"encoding\/json\"\n<\/code><code>    \"fmt\"\n<\/code><code>    \"net\/http\"\n<\/code><code>    \n<\/code><code>    admissionv1 \"k8s.io\/api\/admission\/v1\"\n<\/code><code>    appsv1 \"k8s.io\/api\/apps\/v1\"\n<\/code><code>    corev1 \"k8s.io\/api\/core\/v1\"\n<\/code><code>    metav1 \"k8s.io\/apimachinery\/pkg\/apis\/meta\/v1\"\n<\/code><code>    \"k8s.io\/apimachinery\/pkg\/runtime\"\n<\/code><code>)\n<\/code><code>\n<\/code><code>type WebhookServer struct {\n<\/code><code>    server *http.Server\n<\/code><code>}\n<\/code><code>\n<\/code><code>func (ws *WebhookServer) mutate(w http.ResponseWriter, r *http.Request) {\n<\/code><code>    var body []byte\n<\/code><code>    if r.Body != nil {\n<\/code><code>        if data, err := ioutil.ReadAll(r.Body); err == nil {\n<\/code><code>            body = data\n<\/code><code>        }\n<\/code><code>    }\n<\/code><code>\n<\/code><code>    var admissionResponse *admissionv1.AdmissionResponse\n<\/code><code>    ar := admissionv1.AdmissionReview{}\n<\/code><code>    \n<\/code><code>    if err := json.Unmarshal(body, &amp;ar); err != nil {\n<\/code><code>        admissionResponse = &amp;admissionv1.AdmissionResponse{\n<\/code><code>            Result: &amp;metav1.Status{\n<\/code><code>                Message: err.Error(),\n<\/code><code>            },\n<\/code><code>        }\n<\/code><code>    } else {\n<\/code><code>        admissionResponse = ws.mutateDeployment(&amp;ar)\n<\/code><code>    }\n<\/code><code>\n<\/code><code>    admissionReview := admissionv1.AdmissionReview{}\n<\/code><code>    if admissionResponse != nil {\n<\/code><code>        admissionReview.Response = admissionResponse\n<\/code><code>        if ar.Request != nil {\n<\/code><code>            admissionReview.Response.UID = ar.Request.UID\n<\/code><code>        }\n<\/code><code>    }\n<\/code><code>\n<\/code><code>    respBytes, _ := json.Marshal(admissionReview)\n<\/code><code>    w.Header().Set(\"Content-Type\", \"application\/json\")\n<\/code><code>    w.Write(respBytes)\n<\/code><code>}\n<\/code><code>\n<\/code><code>func (ws *WebhookServer) mutateDeployment(ar *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {\n<\/code><code>    req := ar.Request\n<\/code><code>    var deployment appsv1.Deployment\n<\/code><code>    \n<\/code><code>    if err := json.Unmarshal(req.Object.Raw, &amp;deployment); err != nil {\n<\/code><code>        return &amp;admissionv1.AdmissionResponse{\n<\/code><code>            Result: &amp;metav1.Status{\n<\/code><code>                Message: err.Error(),\n<\/code><code>            },\n<\/code><code>        }\n<\/code><code>    }\n<\/code><code>\n<\/code><code>    \/\/ Check if topology spread constraints already exist\n<\/code><code>    if hasTopologySpreadConstraints(&amp;deployment) {\n<\/code><code>        return &amp;admissionv1.AdmissionResponse{Allowed: true}\n<\/code><code>    }\n<\/code><code>\n<\/code><code>    \/\/ Add multi-AZ topology spread constraints\n<\/code><code>    patches := createMultiAZPatches(&amp;deployment)\n<\/code><code>    \n<\/code><code>    patchBytes, _ := json.Marshal(patches)\n<\/code><code>    \n<\/code><code>    return &amp;admissionv1.AdmissionResponse{\n<\/code><code>        Allowed: true,\n<\/code><code>        Patch:   patchBytes,\n<\/code><code>        PatchType: func() *admissionv1.PatchType {\n<\/code><code>            pt := admissionv1.PatchTypeJSONPatch\n<\/code><code>            return &amp;pt\n<\/code><code>        }(),\n<\/code><code>    }\n<\/code><code>}\n<\/code><code>\n<\/code><code>func hasTopologySpreadConstraints(deployment *appsv1.Deployment) bool {\n<\/code><code>    return len(deployment.Spec.Template.Spec.TopologySpreadConstraints) &gt; 0\n<\/code><code>}\n<\/code><code>\n<\/code><code>func createMultiAZPatches(deployment *appsv1.Deployment) []map[string]interface{} {\n<\/code><code>    patches := []map[string]interface{}{}\n<\/code><code>    \n<\/code><code>    \/\/ Add topology spread constraints for AZ distribution\n<\/code><code>    topologyConstraints := []corev1.TopologySpreadConstraint{\n<\/code><code>        {\n<\/code><code>            MaxSkew:           1,\n<\/code><code>            TopologyKey:       \"topology.kubernetes.io\/zone\",\n<\/code><code>            WhenUnsatisfiable: corev1.DoNotSchedule,\n<\/code><code>            MinDomains:        func() *int32 { i := int32(3); return &amp;i }(),\n<\/code><code>            LabelSelector: &amp;metav1.LabelSelector{\n<\/code><code>                MatchLabels: deployment.Spec.Selector.MatchLabels,\n<\/code><code>            },\n<\/code><code>        },\n<\/code><code>        {\n<\/code><code>            MaxSkew:           2,\n<\/code><code>            TopologyKey:       \"kubernetes.io\/hostname\",\n<\/code><code>            WhenUnsatisfiable: corev1.ScheduleAnyway,\n<\/code><code>            LabelSelector: &amp;metav1.LabelSelector{\n<\/code><code>                MatchLabels: deployment.Spec.Selector.MatchLabels,\n<\/code><code>            },\n<\/code><code>        },\n<\/code><code>    }\n<\/code><code>    \n<\/code><code>    patches = append(patches, map[string]interface{}{\n<\/code><code>        \"op\":    \"add\",\n<\/code><code>        \"path\":  \"\/spec\/template\/spec\/topologySpreadConstraints\",\n<\/code><code>        \"value\": topologyConstraints,\n<\/code><code>    })\n<\/code><code>    \n<\/code><code>    return patches\n<\/code><code>}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Solution 2: OPA Gatekeeper (Policy-Based Approach)<\/strong><\/h3>\n\n\n\n<p>This validates and can mutate deployments to enforce multi-AZ distribution.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>A. Install Gatekeeper<\/strong><\/h4>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl apply -f https:<span class=\"hljs-comment\">\/\/raw.githubusercontent.com\/open-policy-agent\/gatekeeper\/release-3.14\/deploy\/gatekeeper.yaml<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h4 class=\"wp-block-heading\"><strong>B. Create Constraint Template<\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># gatekeeper-multiaz-template.yaml\n<\/code><code>apiVersion: templates.gatekeeper.sh\/v1beta1\n<\/code><code>kind: ConstraintTemplate\n<\/code><code>metadata:\n<\/code><code>  name: k8smultiazrequired\n<\/code><code>spec:\n<\/code><code>  crd:\n<\/code><code>    spec:\n<\/code><code>      names:\n<\/code><code>        kind: K8sMultiAZRequired\n<\/code><code>      validation:\n<\/code><code>        openAPIV3Schema:\n<\/code><code>          type: object\n<\/code><code>          properties:\n<\/code><code>            message:\n<\/code><code>              type: string\n<\/code><code>            exemptNamespaces:\n<\/code><code>              type: array\n<\/code><code>              items:\n<\/code><code>                type: string\n<\/code><code>  targets:\n<\/code><code>    - target: admission.k8s.gatekeeper.sh\n<\/code><code>      rego: |\n<\/code><code>        package k8smultiazrequired\n<\/code><code>\n<\/code><code>        violation[{\"msg\": msg}] {\n<\/code><code>          input.review.kind.kind == \"Deployment\"\n<\/code><code>          input.review.object.spec.replicas &gt; 1\n<\/code><code>          not input.review.object.spec.template.spec.topologySpreadConstraints\n<\/code><code>          not is_exempt_namespace\n<\/code><code>          msg := sprintf(\"Deployment %s must have topology spread constraints for multi-AZ distribution\", [input.review.object.metadata.name])\n<\/code><code>        }\n<\/code><code>\n<\/code><code>        violation[{\"msg\": msg}] {\n<\/code><code>          input.review.kind.kind == \"Deployment\"\n<\/code><code>          input.review.object.spec.replicas &gt; 1\n<\/code><code>          input.review.object.spec.template.spec.topologySpreadConstraints\n<\/code><code>          not has_zone_topology_constraint\n<\/code><code>          not is_exempt_namespace\n<\/code><code>          msg := sprintf(\"Deployment %s must have zone topology spread constraint\", [input.review.object.metadata.name])\n<\/code><code>        }\n<\/code><code>\n<\/code><code>        has_zone_topology_constraint {\n<\/code><code>          constraint := input.review.object.spec.template.spec.topologySpreadConstraints[_]\n<\/code><code>          constraint.topologyKey == \"topology.kubernetes.io\/zone\"\n<\/code><code>        }\n<\/code><code>\n<\/code><code>        is_exempt_namespace {\n<\/code><code>          exempt_namespaces := input.parameters.exemptNamespaces\n<\/code><code>          input.review.object.metadata.namespace == exempt_namespaces[_]\n<\/code><code>        }\n<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>C. Create Constraint<\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># gatekeeper-multiaz-constraint.yaml\n<\/code><code>apiVersion: constraints.gatekeeper.sh\/v1beta1\n<\/code><code>kind: K8sMultiAZRequired\n<\/code><code>metadata:\n<\/code><code>  name: must-have-multiaz-topology\n<\/code><code>spec:\n<\/code><code>  match:\n<\/code><code>    kinds:\n<\/code><code>      - apiGroups: [\"apps\"]\n<\/code><code>        kinds: [\"Deployment\"]\n<\/code><code>    excludedNamespaces: [\"kube-system\", \"kube-public\", \"gatekeeper-system\"]\n<\/code><code>  parameters:\n<\/code><code>    message: \"All deployments with more than 1 replica must have multi-AZ topology spread constraints\"\n<\/code><code>    exemptNamespaces: [\"kube-system\", \"kube-public\", \"gatekeeper-system\"]\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Solution 3: Kyverno (Alternative Policy Engine)<\/strong><\/h3>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>A. Install Kyverno<\/strong><\/h4>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl create -f https:<span class=\"hljs-comment\">\/\/github.com\/kyverno\/kyverno\/releases\/latest\/download\/install.yaml<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h4 class=\"wp-block-heading\"><strong>B. Create Multi-AZ Policy<\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># kyverno-multiaz-policy.yaml\n<\/code><code>apiVersion: kyverno.io\/v1\n<\/code><code>kind: ClusterPolicy\n<\/code><code>metadata:\n<\/code><code>  name: require-multiaz-topology\n<\/code><code>spec:\n<\/code><code>  validationFailureAction: enforce\n<\/code><code>  background: true\n<\/code><code>  rules:\n<\/code><code>  - name: check-multiaz-topology\n<\/code><code>    match:\n<\/code><code>      any:\n<\/code><code>      - resources:\n<\/code><code>          kinds:\n<\/code><code>          - Deployment\n<\/code><code>          namespaces:\n<\/code><code>          - \"!kube-system\"\n<\/code><code>          - \"!kube-public\"\n<\/code><code>          - \"!kyverno\"\n<\/code><code>    validate:\n<\/code><code>      message: \"Deployments with replicas &gt; 1 must have topology spread constraints for multi-AZ distribution\"\n<\/code><code>      pattern:\n<\/code><code>        spec:\n<\/code><code>          =(replicas): \"1\"\n<\/code><code>  - name: require-zone-topology\n<\/code><code>    match:\n<\/code><code>      any:\n<\/code><code>      - resources:\n<\/code><code>          kinds:\n<\/code><code>          - Deployment\n<\/code><code>          namespaces:\n<\/code><code>          - \"!kube-system\"\n<\/code><code>          - \"!kube-public\"\n<\/code><code>          - \"!kyverno\"\n<\/code><code>    validate:\n<\/code><code>      message: \"Deployments with replicas &gt; 1 must have zone topology spread constraints\"\n<\/code><code>      anyPattern:\n<\/code><code>      - spec:\n<\/code><code>          replicas: 1\n<\/code><code>      - spec:\n<\/code><code>          template:\n<\/code><code>            spec:\n<\/code><code>              topologySpreadConstraints:\n<\/code><code>              - topologyKey: \"topology.kubernetes.io\/zone\"\n<\/code><code>---\n<\/code><code>apiVersion: kyverno.io\/v1\n<\/code><code>kind: ClusterPolicy\n<\/code><code>metadata:\n<\/code><code>  name: add-multiaz-topology\n<\/code><code>spec:\n<\/code><code>  validationFailureAction: enforce\n<\/code><code>  background: false\n<\/code><code>  rules:\n<\/code><code>  - name: add-topology-constraints\n<\/code><code>    match:\n<\/code><code>      any:\n<\/code><code>      - resources:\n<\/code><code>          kinds:\n<\/code><code>          - Deployment\n<\/code><code>          namespaces:\n<\/code><code>          - \"!kube-system\"\n<\/code><code>          - \"!kube-public\"\n<\/code><code>          - \"!kyverno\"\n<\/code><code>    preconditions:\n<\/code><code>      all:\n<\/code><code>      - key: \"{{ request.object.spec.replicas }}\"\n<\/code><code>        operator: GreaterThan\n<\/code><code>        value: 1\n<\/code><code>      - key: \"{{ request.object.spec.template.spec.topologySpreadConstraints || `[]` | length(@) }}\"\n<\/code><code>        operator: Equals\n<\/code><code>        value: 0\n<\/code><code>    mutate:\n<\/code><code>      patchStrategicMerge:\n<\/code><code>        spec:\n<\/code><code>          template:\n<\/code><code>            spec:\n<\/code><code>              topologySpreadConstraints:\n<\/code><code>              - maxSkew: 1\n<\/code><code>                topologyKey: \"topology.kubernetes.io\/zone\"\n<\/code><code>                whenUnsatisfiable: DoNotSchedule\n<\/code><code>                minDomains: 3\n<\/code><code>                labelSelector:\n<\/code><code>                  matchLabels:\n<\/code><code>                    \"{{ request.object.spec.selector.matchLabels }}\"\n<\/code><code>              - maxSkew: 2\n<\/code><code>                topologyKey: \"kubernetes.io\/hostname\"\n<\/code><code>                whenUnsatisfiable: ScheduleAnyway\n<\/code><code>                labelSelector:\n<\/code><code>                  matchLabels:\n<\/code><code>                    \"{{ request.object.spec.selector.matchLabels }}\"\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Solution 4: Namespace-Level Defaults with LimitRanges<\/strong><\/h2>\n\n\n\n<p>While not directly enforcing topology constraints, you can use this approach combined with other solutions:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># namespace-defaults.yaml\n<\/code><code>apiVersion: v1\n<\/code><code>kind: LimitRange\n<\/code><code>metadata:\n<\/code><code>  name: multiaz-defaults\n<\/code><code>  namespace: production\n<\/code><code>spec:\n<\/code><code>  limits:\n<\/code><code>  - default:\n<\/code><code>      cpu: \"500m\"\n<\/code><code>      memory: \"512Mi\"\n<\/code><code>    defaultRequest:\n<\/code><code>      cpu: \"100m\"\n<\/code><code>      memory: \"128Mi\"\n<\/code><code>    type: Container\n<\/code><code>---\n<\/code><code>apiVersion: v1\n<\/code><code>kind: ResourceQuota\n<\/code><code>metadata:\n<\/code><code>  name: multiaz-quota\n<\/code><code>  namespace: production\n<\/code><code>spec:\n<\/code><code>  hard:\n<\/code><code>    requests.cpu: \"10\"\n<\/code><code>    requests.memory: 20Gi\n<\/code><code>    limits.cpu: \"20\"\n<\/code><code>    limits.memory: 40Gi\n<\/code><code>    pods: \"50\"\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Implementation Guide<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Step 1: Choose Your Approach<\/strong><\/h3>\n\n\n\n<p><strong>For Maximum Control<\/strong>: Use&nbsp;<strong>Mutating Admission Webhook<\/strong>&nbsp;<strong>For Policy Management<\/strong>: Use&nbsp;<strong>Kyverno<\/strong>&nbsp;(easier) or&nbsp;<strong>OPA Gatekeeper<\/strong>&nbsp;(more powerful)&nbsp;<strong>For Simple Validation<\/strong>: Use&nbsp;<strong>Gatekeeper<\/strong>&nbsp;validation only<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Step 2: Deploy the Solution<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># For Kyverno (Recommended for simplicity)\n<\/code><code>kubectl create -f https:\/\/github.com\/kyverno\/kyverno\/releases\/latest\/download\/install.yaml\n<\/code><code>kubectl apply -f kyverno-multiaz-policy.yaml\n<\/code><code>\n<\/code><code># Verify installation\n<\/code><code>kubectl get clusterpolicy\n<\/code>Run in CloudShell<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Step 3: Test the Enforcement<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># test-deployment.yaml\n<\/code><code>apiVersion: apps\/v1\n<\/code><code>kind: Deployment\n<\/code><code>metadata:\n<\/code><code>  name: test-app\n<\/code><code>  namespace: default\n<\/code><code>spec:\n<\/code><code>  replicas: 3\n<\/code><code>  selector:\n<\/code><code>    matchLabels:\n<\/code><code>      app: test-app\n<\/code><code>  template:\n<\/code><code>    metadata:\n<\/code><code>      labels:\n<\/code><code>        app: test-app\n<\/code><code>    spec:\n<\/code><code>      containers:\n<\/code><code>      - name: nginx\n<\/code><code>        image: nginx:1.21\n<\/code><code>        resources:\n<\/code><code>          requests:\n<\/code><code>            cpu: 100m\n<\/code><code>            memory: 128Mi\n<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># This should either be automatically modified (mutating webhook\/Kyverno)\n<\/code><code># or rejected (validation-only policies)\n<\/code><code>kubectl apply -f test-deployment.yaml\n<\/code><code>\n<\/code><code># Check if topology constraints were added\n<\/code><code>kubectl get deployment test-app -o yaml | grep -A 20 topologySpreadConstraints\n<\/code>Run in CloudShell<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Step 4: Create Exemption Mechanism<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># For deployments that should skip multi-AZ enforcement\n<\/code><code>apiVersion: apps\/v1\n<\/code><code>kind: Deployment\n<\/code><code>metadata:\n<\/code><code>  name: single-az-app\n<\/code><code>  namespace: default\n<\/code><code>  annotations:\n<\/code><code>    multiaz.enforcer.eks.aws\/skip: \"true\"  # For webhook\n<\/code><code>    policies.kyverno.io\/skip: \"true\"       # For Kyverno\n<\/code><code>spec:\n<\/code><code>  replicas: 1\n<\/code><code>  # ... rest of deployment\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Monitoring and Validation<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Check Policy Compliance<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># For Kyverno\n<\/code><code>kubectl get cpol\n<\/code><code>kubectl describe cpol require-multiaz-topology\n<\/code><code>\n<\/code><code># For Gatekeeper\n<\/code><code>kubectl get constraints\n<\/code><code>kubectl describe k8smultiazrequired must-have-multiaz-topology\n<\/code><code>\n<\/code><code># Check violations\n<\/code><code>kubectl get events --field-selector reason=PolicyViolation\n<\/code>Run in CloudShell<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Validate Existing Deployments<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>#!\/bin\/bash\n<\/code><code># check-multiaz-compliance.sh\n<\/code><code>\n<\/code><code>echo \"Checking Multi-AZ compliance for all deployments...\"\n<\/code><code>\n<\/code><code>kubectl get deployments --all-namespaces -o json | jq -r '\n<\/code><code>.items[] | \n<\/code><code>select(.spec.replicas &gt; 1) |\n<\/code><code>select(.spec.template.spec.topologySpreadConstraints == null or \n<\/code><code>       (.spec.template.spec.topologySpreadConstraints | \n<\/code><code>        map(select(.topologyKey == \"topology.kubernetes.io\/zone\")) | \n<\/code><code>        length == 0)) |\n<\/code><code>\"\\(.metadata.namespace)\/\\(.metadata.name) - Missing multi-AZ topology constraints\"\n<\/code><code>'\n<\/code>Run in CloudShell<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Recommended Implementation<\/strong><\/h2>\n\n\n\n<p><strong>For your EKS Auto Mode cluster, I recommend:<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Start with Kyverno<\/strong>\u00a0&#8211; easier to implement and maintain<\/li>\n\n\n\n<li><strong>Use the mutating policy<\/strong>\u00a0to automatically add topology constraints<\/li>\n\n\n\n<li><strong>Set up monitoring<\/strong>\u00a0to track compliance<\/li>\n\n\n\n<li><strong>Create exemption mechanisms<\/strong>\u00a0for special cases<\/li>\n\n\n\n<li><strong>Test thoroughly<\/strong>\u00a0in a non-production environment first<\/li>\n<\/ol>\n\n\n\n<p>This approach ensures that&nbsp;<strong>every deployment with more than 1 replica automatically gets multi-AZ distribution<\/strong>&nbsp;without requiring developers to remember to add topology spread constraints manually.<\/p>\n\n\n\n<p>Sources<\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/prescriptive-guidance\/latest\/ha-resiliency-amazon-eks-apps\/spread-workloads.html\">Spread workloads across nodes and Availability Zones &#8211; AWS Prescriptive Guidance&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/whitepapers\/latest\/security-practices-multi-tenant-saas-applications-eks\/use-admission-controllers-to-enforce-security-policies.html\">Use admission controllers to enforce security policies &#8211; Security Practices for Multi-Tenant SaaS Applications using Amazon EKS&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/best-practices\/application.html\">Running highly-available applications &#8211; Amazon EKS&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/best-practices\/control-plane.html\">EKS Control Plane &#8211; Amazon EKS&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/best-practices\/tenant-isolation.html\">Tenant Isolation &#8211; Amazon EKS&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/repost.aws\/articles\/ARr5AEAlgSSh2sdX30gnMGDQ\/managing-webhook-failures-on-amazon-eks\">Managing webhook failures on Amazon EKS | AWS re:Post&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/best-practices\/pod-security.html\">Pod Security &#8211; Amazon EKS&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/repost.aws\/questions\/QUgRykNbniS7uWhclT72u72Q\/topologyspreadconstraints-in-eks-auto-mode-does-not-seem-to-work\">TopologySpreadConstraints in EKS auto mode does not seem to work | AWS re:Post&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/repost.aws\/knowledge-center\/eks-troubleshoot-cluster-scaling-with-karpenter\">Troubleshoot cluster scaling in Amazon EKS with Karpenter autoscaler | AWS re:Post&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/best-practices\/image-security.html\">Image security &#8211; Amazon EKS&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/whitepapers\/latest\/containers-on-aws\/scheduling.html\">Scheduling &#8211; Containers on AWS&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/best-practices\/scale-data-plane.html\">Kubernetes Data Plane &#8211; Amazon EKS&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/aws.amazon.com\/blogs\/containers\/customizing-scheduling-on-amazon-eks\/\">Customizing scheduling on Amazon EKS | Containers&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/best-practices\/data-plane.html\">EKS Data Plane &#8211; Amazon EKS&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/best-practices\/subnets.html\">VPC and Subnet Considerations &#8211; Amazon EKS&nbsp;<\/a><\/p>\n\n\n\n<p><a target=\"_blank\" rel=\"noreferrer noopener\" href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/userguide\/managing-coredns.html\">Manage CoreDNS for DNS in Amazon EKS clusters &#8211; Amazon EKS&nbsp;<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u274c What&#8217;s NOT Available in EKS \u2705 Available Solutions (Ranked by Effectiveness) Solution 1: Mutating Admission Webhook (Recommended) This automatically injects topology spread constraints into all deployments&#8230; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_joinchat":[],"footnotes":""},"categories":[11138],"tags":[],"class_list":["post-58579","post","type-post","status-publish","format-standard","hentry","category-best-tools"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/58579","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/comments?post=58579"}],"version-history":[{"count":1,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/58579\/revisions"}],"predecessor-version":[{"id":58580,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/58579\/revisions\/58580"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/media?parent=58579"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/categories?post=58579"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/tags?post=58579"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}