Minule jsme si prošli Azure Policy pro kontejnery a ukázali si, jak lze pěkně z jednoho místa řešit governance pro Azure Kubernetes Service i pro libovolný Kubernetes běžící kdekoli připojený do Azure prostředí přes Azure Arc. Využili jsme databáze hotových politik a ukázali si, že pod kapotou jsou pravidla napsaná v jazyce Rego, vynucení politik řeší Open policy Agent nativně spravovaný v Kubernetu projektem Gatekeeper a to vše řízeno a reportováno centrálně v cloudu s Azure Policy. Co když ale potřebujete něco speciálního? Pojďme si dnes napsat a aplikovat vlastní politiku.
Open Policy Agent je projekt, který je obecným řešením pro validaci (a v budoucnu i úpravu - “mutaci”) nějaké datové struktury. OPA dokážete předložit JSON, pravidla napsaná v jazyce Rego a OPA řekne, jestli vyhovuje či ne. Typické použití je právě implementace politik v Kubernetes přes Admission Webhook, ale popsány jsou další příklady nasazení jako je validace Terraform plánu před jeho nasazením, kontrola REST volání nebo konfiguračních parametrů Envoy proxy. Principiálně ale lze kontrolovat jakoukoli datovou strukturu - nativní cloudovou šablonu, vaše vlastní konfigurační JSON soubory třeba pro aplikaci, konfigurační soubory síťových prvků (pokud jsou v deklarativním formátu) a tak podobně.
Abychom dokázali v OPA něco dělat, bude potřeba alespoň trošku pochopit jazyk Rego, ve kterém se to píše. Osobně jsem s ním strávil několik hodin a základy jsem nějak pobral, ale rozhodně stát se skutečně znalým by vyžadovalo o dost větší časovou investici. Osvědčilo se mi pohrát si s jazykem na stránkách https://play.openpolicyagent.org/. Za nejzásadnější pro prozření se u mě ukázalo uvědomění si, že jazyk je primárně o práci s množinami. Je to deklarativní model, ne imperativní programovací jazyk.
Po dvou hodinách experimentování jsem došel k tomuto základnímu příkladu:
package play
violation[{"msg": msg, "details": {}}] {
not input.message
msg := "Message must be configured"
}
violation[{"msg": msg, "details": {}}] {
input.message != "world"
msg := "Message must be world"
}
violation[{"msg": msg, "details": {}}] {
not input.items
msg := "Items list must be included"
}
violation[{"msg": msg, "details": {}}] {
input.items[_].name == ""
msg := "Empty item name not allowed"
}
violation[{"msg": msg, "details": {}}] {
input.items[_].name == "NA"
msg := "Item must not be NA"
}
violation[{"msg": msg, "details": {}}] {
not bookExists
msg := "Book must be included in items"
}
bookExists(){
input.items[_].name == "book"
}
violation[{"msg": msg, "details": {}}] {
count(input.items) != count(itemsWithStock)
msg := "Stock must be included with all items"
}
itemsWithStock[a] = array {
array = input.items[a]
array.stock != ""
}
A vstupní JSON, který to kontroluje jsem průběžně měnil a zkoumal co to dělá, ale vypadá do začátku nějak takhle:
{
"message": "world",
"items": [
{
"name": "paper",
"stock": 5
},
{
"name": "book",
"stock": 5
},
{
"name": "NA"
}
]
}
Pojďme rozebrat jednotlivé sekce. Každá violation (to není klíčové slovo nebo příkaz, jmenovat se to může jakkoli) je soupis pravidel, tedy (kromě nějakých pomocných operací) soupis jednoho a více řádků, které vrací true nebo false. Jakmile jeden z nich vrátí true, tak se pravidlo chytne a vypíše hlášku (v mém případě msg) a později v implementaci třeba v Kubernetes může zastavit deployment zdroje.
Začneme pěkně popořadě.
violation[{"msg": msg, "details": {}}] {
not input.message
msg := "Message must be configured"
}
Tohle jednoduché pravidlo zjišťuje existenci klíče “message” v JSONu. input.message vrací true pokud tam je, takže mě zajímá opak, přidám slovo not.
violation[{"msg": msg, "details": {}}] {
input.message != "world"
msg := "Message must be world"
}
Tato validace ověřuje, že message obsahuje slovo world. Pokud tam bude cokoli jiného, hodí hlášku. Představte si například, že je to nějaké políčko u resource říkající úroveň zabezpečení a nabývá hodnot None, Basic, Advanced a vaše politika chce zakázat cokoli, co není Advanced.
violation[{"msg": msg, "details": {}}] {
not input.items
msg := "Items list must be included"
}
Tohle už umíme - ujišťuji se, že existuje klíč items.
violation[{"msg": msg, "details": {}}] {
input.items[_].name == ""
msg := "Empty item name not allowed"
}
Items je pole objektů. Tohle pravidlo říká, že v tomto poli objektů pokud se vyskytuje některý s klíčem name, tento klíč nesmí mít prázdnou hodnotu. Ale pozor - tohle netvrdí, že každý objekt musí mít klíč name. Není to cyklus, který by v takovém případě havaroval, protože saháme na klíč, který tam není. Spíše jsme řekli - z množiny všech items objektů mi vyber hodnoty všech name klíčů, které se v ní vyskytují. Pokud se kterákoli z nich rovná prázdné hodnotě, vrať true (tedy error zprávu). Pro index jsem použil podtržítko, protože mě nezajímá, který ten objekt nevyhovuje mému zadání. V pokročilejších scénářích ale můžu chtít vrátit konkrétní index a pak místo podtržítka použijete proměnnou a v té se pak bude index nacházet.
violation[{"msg": msg, "details": {}}] {
input.items[_].name == "NA"
msg := "Item must not be NA"
}
Variací na totéž je pravidlo, které se ozve, pokud je jakýkoli výskyt atributu name v poli objektů items roven NA. Chtěl jsem si tím uvědomit, jak musím později udělat opak.
violation[{"msg": msg, "details": {}}] {
not bookExists
msg := "Book must be included in items"
}
bookExists(){
input.items[_].name == "book"
}
Tímto pravidlem říkám, že v seznamu items musí být alespoň jeden s názvem book. První intuitivní pokus byl dát rovnou do pravidla statement input.items[_].name != “book” v domnění, že je to opak předchozího příkladu, ale to dělá něco úplně jiného. Takovým zápisem říkám, že vracím true (vyhodím hlášku), jakmile existuje byť jen jeden item, který není book. Ve skutečnosti tedy potřebuji ==, které reaguje na alespoň jeden výskyt book a výsledek negovat. To nelze udělat přímo ve statementu a musí se použít funkce. Mám tedy funkci, která vrací true, pokud je v items alespoň jedna kniha a výstup této funkce v pravidlu převrátím, takže pokud tam kniha je, výsledkem je false, tedy nejde o violation - je to ok, pustíme to, žádná hláška.
violation[{"msg": msg, "details": {}}] {
count(input.items) != count(itemsWithStock)
msg := "Stock must be included with all items"
}
itemsWithStock[a] = array {
array = input.items[a]
array.stock != ""
}
Poslední příklad chce ověřit, že ve všech items se vyskytuje klíč stock. Ověřovat existenci stock jsem se samozřejmě nejdřív pokusil jednoduše zápisem not input.items[_].stock, ale to zas dělá něco jiného. Tenhle zápis zjistí, že existuje alespoň jedna item s klíčem stock (vrátí true) a já ji převrátím přes not - takže stačí aby jen jediný item měl atribut stock a pravidlo by vyhovovalo a to není co potřebuji. Řešení je vytvořit funkci, která nejprve vezme všechny items do množiny s názvem array (v ten okamžik je obsah identický) a následně na ní zavolá array.stock != “”, tedy vznikne podmnožina items, u kterých platí, že stock je definován. V pravidle pak kontroluji velikost obou množin. Pokud items je jiný počet, než takto odfiltrovaných items, tak existují položky bez definovaného stock.
Pro dobrou znalost Rego bych musel ještě o dost dál, ale prozatím to takhle stačí. Základní myšlenka je, že jde o jazyk pro práci s datovou strukturou a funguje na principu množin nebo chcete-li query. To je pro účely OPA myslím velmi dobrý přístup - ubrání vás to od runtime chyb, které by se daly čekat u imperativního přístupu viz příklad projíždění pole ve smyčce s očekáváním, že je tam atribut name, ale on tam jednoho dne být nemusí a pokud to není ošetřeno, spadne to.
Databáze pravidel v rámci Azure Policy pokrývá myslím naprostou většinu běžných scénářů, nicméně napadl mě jeden, který tam není. Představme si, že máme cluster s několik node pooly a máme potřebu, aby určitý namespace vždy dával Pody na konkrétní sadu nodů. Proč?
Existuje funkce PodNodeSelector, ale ta je stále v alpha stavu a to už od v1.5, takže pro produkční prostředí nebo managed clustery typicky není dostupná nebo vhodná. Zkusme jiné řešení.
Určitě vás (velmi správně) napadne, že na tohle jsou přece taint, tolerace a nodeAffinity. Přesně tak! Jenže tohle vyžaduje “dobrou víru” - jsou to způsoby jak nastavit, aby to dělalo co potřebujete, ne, aby to zabránilo někomu dělat to, co nechcete. Pokud v rámci clusteru potřebujete pokrýt riziko, že někdo omylem nebo schválně pravidla poruší, bude vhodné tohle doplnit politikou. Vytvořme tedy politiku, která bude kontrolovat, že objekt Pod má nastavenou nodeAffinity a že v seznamu match pravidel je přesně to co chceme. To mi pak umožní tohle nasadit na konkrétní namespace a mám jistotu, že nebudou moci pustit Pod na jiných nodech, než které chci já - i když si nastaví affinitu jinak, zapnou tolerace apod.
V Rego něco takového vypadá takhle:
package k8snodeaffinity
# Check nodeAffinity rule exist
violation[{"msg": msg, "details": {}}] {
not input.review.object.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0]
msg := "Pods must be assigned to specific nodepool using spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions containing {\"key\": \"type\", \"operator\": \"In\", \"values\": [\"protected\"]}; found no matchExpressions"
}
# Check nodeAffinity rule contains required match expression
violation[{"msg": msg, "details": {}}] {
nodeMatch := input.review.object.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[i].matchExpressions[i]
not nodeMatch == {"key": "type", "operator": "In", "values": ["protected"]}
msg := sprintf("Pods must be assigned to specific nodepool using spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions containing {\"key\": \"type\", \"operator\": \"In\", \"values\": [\"protected\"]}; found `%v`", [nodeMatch])
}
Label nodu tam mám natvrdo jako type:protected. Řekněme, že type natvrdo nechám, ale hodnotu bych rád univerzální. Vytvořím constraint template a použiji proměnou - při aplikaci šablony se zeptám na pole možných hodnot pro node label type.
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8snodeaffinity
spec:
crd:
spec:
names:
kind: K8sNodeAffinity
validation:
openAPIV3Schema:
properties:
nodeLabels:
type: array
items: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8snodeaffinity
# Check nodeAffinity rule exist
violation[{"msg": msg, "details": {}}] {
not input.review.object.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0]
msg := "Pods must be assigned to specific nodepool using spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions containing {\"key\": \"type\", \"operator\": \"In\", \"values\": [\"protected\"]}; found no matchExpressions"
}
# Check nodeAffinity rule contains required match expression
violation[{"msg": msg, "details": {}}] {
nodeMatch := input.review.object.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[i].matchExpressions[i]
not nodeMatch == {"key": "type", "operator": "In", "values": input.parameters.nodeLabels}
msg := sprintf("Pods must be assigned to specific nodepool using spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions containing {\"key\": \"type\", \"operator\": \"In\", \"values\": [\"protected\"]}; found `%v`", [nodeMatch])
}
Tuto šablonu teď buď uložím někam na Internet (to je můj případ - je na mém GitHubu) nebo ji překóduji do base64 a použiji v těle definice Azure Policy. To naštěstí nemusím vymýšlet - vzal jem Visual Studio Code, plugin pro Azure Policy a nechal si to vygenerovat.
{
"properties": {
"policyType": "Custom",
"mode": "Microsoft.Kubernetes.Data",
"displayName": "Require Pods to run on specified Nodes",
"description": "This policy checks for nodeAffinity rules to specify required labels nodes must have configured. This is typically used to make sure namespace Pods are always allocated to specific NodePool.",
"policyRule": {
"if": {
"field": "type",
"in": [
"Microsoft.ContainerService/managedClusters"
]
},
"then": {
"effect": "[parameters('effect')]",
"details": {
"templateInfo": {
"sourceType": "PublicURL",
"url": "https://raw.githubusercontent.com/tkubica12/aks-demo/master/customPolicy/nodeAffinityConstraintTemplate.yaml"
},
"apiGroups": [
"*"
],
"kinds": [
"Pod"
],
"namespaces": "[parameters('namespaces')]",
"excludedNamespaces": "[parameters('excludedNamespaces')]",
"labelSelector": "[parameters('labelSelector')]",
"values": {
"nodeLabels": "[parameters('nodeLabels')]"
}
}
}
},
"parameters": {
"effect": {
"type": "String",
"metadata": {
"displayName": "Effect",
"description": "'audit' allows a non-compliant resource to be created or updated, but flags it as non-compliant. 'deny' blocks the non-compliant resource creation or update. 'disabled' turns off the policy."
},
"allowedValues": [
"audit",
"deny",
"disabled"
],
"defaultValue": "audit"
},
"excludedNamespaces": {
"type": "Array",
"metadata": {
"displayName": "Namespace exclusions",
"description": "List of Kubernetes namespaces to exclude from policy evaluation."
},
"defaultValue": [
"kube-system",
"gatekeeper-system",
"azure-arc"
]
},
"namespaces": {
"type": "Array",
"metadata": {
"displayName": "Namespace inclusions",
"description": "List of Kubernetes namespaces to only include in policy evaluation. An empty list means the policy is applied to all resources in all namespaces."
},
"defaultValue": []
},
"labelSelector": {
"type": "Object",
"metadata": {
"displayName": "Kubernetes label selector",
"description": "Label query to select Kubernetes resources for policy evaluation. An empty label selector matches all Kubernetes resources."
},
"defaultValue": {},
"schema": {
"description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all resources.",
"type": "object",
"properties": {
"matchLabels": {
"description": "matchLabels is a map of {key,value} pairs.",
"type": "object",
"additionalProperties": {
"type": "string"
},
"minProperties": 1
},
"matchExpressions": {
"description": "matchExpressions is a list of values, a key, and an operator.",
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"description": "key is the label key that the selector applies to.",
"type": "string"
},
"operator": {
"description": "operator represents a key's relationship to a set of values.",
"type": "string",
"enum": [
"In",
"NotIn",
"Exists",
"DoesNotExist"
]
},
"values": {
"description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"key",
"operator"
],
"additionalProperties": false
},
"minItems": 1
}
},
"additionalProperties": false
}
},
"nodeLabels": {
"type": "Array",
"metadata": {
"displayName": "Node labels array",
"description": "Array of labels of node that Pods must be assigned to"
}
}
}
}
}
Policy mi umožní při assignmentu zvolit na který namespace a případně pod labely se politika aplikuje a současně se tam objevil i vstupní parametr z mé šablony (pole hodnot pro label type na nodech).
Definici jsem přidal do Azure Policy.
Politiku přiřadím na scope dle potřeby - například konkrétní cluster ať už AKS nebo Arc nebo třeba univerzálně pro všechny clustery v organizaci.
Po několika minutách si mohu ověřit, že Azure Policy v clusteru založila příslušnou šablonu, CRD i jeho výskyt.
$ kubectl get constrainttemplate
NAME AGE
k8sazureallowedcapabilities 28h
k8sazureallowedusersgroups 28h
k8sazureblockautomounttoken 28h
k8sazureblockdefault 28h
k8sazureblockhostnamespace 28h
k8sazurecontainerallowedimages 28h
k8sazurecontainerallowedports 28h
k8sazurecontainerlimits 28h
k8sazurecontainernoprivilege 28h
k8sazurecontainernoprivilegeescalation 28h
k8sazuredisallowedcapabilities 28h
k8sazureenforceapparmor 28h
k8sazurehostfilesystem 28h
k8sazurehostnetworkingports 28h
k8sazureingresshttpsonly 28h
k8sazurepodenforcelabels 26h
k8sazurereadonlyrootfilesystem 28h
k8sazureserviceallowedports 28h
k8snodeaffinity 25h
$ kubectl describe constrainttemplate k8snodeaffinity
Name: k8snodeaffinity
Namespace:
Labels: managed-by=azure-policy-addon
Annotations: azure-policy-definition-id-1:
/subscriptions/a0f4a733-4fce-4d49-b8a8-d30541fc1b45/providers/microsoft.authorization/policydefinitions/b0d0c4b8-ef88-4d83-817e-786327bbb6...
constraint-template: https://raw.githubusercontent.com/tkubica12/aks-demo/master/customPolicy/nodeAffinityConstraintTemplate.yaml
constraint-template-installed-by: azure-policy-addon
API Version: templates.gatekeeper.sh/v1beta1
Kind: ConstraintTemplate
Metadata:
Creation Timestamp: 2021-09-07T07:17:24Z
Generation: 1
Managed Fields:
API Version: templates.gatekeeper.sh/v1beta1
Fields Type: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
.:
f:azure-policy-definition-id-1:
f:constraint-template:
f:constraint-template-installed-by:
f:labels:
.:
f:managed-by:
f:spec:
.:
f:crd:
.:
f:spec:
.:
f:names:
.:
f:kind:
f:validation:
.:
f:openAPIV3Schema:
.:
f:properties:
.:
f:nodeLabels:
.:
f:items:
f:type:
f:targets:
Manager: azurepolicyaddon
Operation: Update
Time: 2021-09-07T07:17:24Z
API Version: templates.gatekeeper.sh/v1beta1
Fields Type: FieldsV1
fieldsV1:
f:status:
.:
f:byPod:
f:created:
Manager: gatekeeper
Operation: Update
Time: 2021-09-07T07:17:25Z
Resource Version: 126018
UID: 05f84ed7-9d50-4804-9817-fcc3be70a792
Spec:
Crd:
Spec:
Names:
Kind: K8sNodeAffinity
Validation:
openAPIV3Schema:
Properties:
Node Labels:
Items: string
Type: array
Targets:
Rego: package k8snodeaffinity
# Check nodeAffinity rule exist
violation[{"msg": msg, "details": {}}] {
not input.review.object.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0]
msg := "Pods must be assigned to specific nodepool using spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions containing {\"key\": \"type\", \"operator\": \"In\", \"values\": [\"protected\"]}; found no matchExpressions"
}
# Check nodeAffinity rule contains required match expression
violation[{"msg": msg, "details": {}}] {
nodeMatch := input.review.object.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[i].matchExpressions[i]
not nodeMatch == {"key": "type", "operator": "In", "values": input.parameters.nodeLabels}
msg := sprintf("Pods must be assigned to specific nodepool using spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions containing {\"key\": \"type\", \"operator\": \"In\", \"values\": [\"protected\"]}; found `%v`", [nodeMatch])
}
Target: admission.k8s.gatekeeper.sh
Status:
By Pod:
Id: gatekeeper-audit-5456bb46b6-zzbmq
Observed Generation: 1
Operations:
audit
status
Template UID: 05f84ed7-9d50-4804-9817-fcc3be70a792
Id: gatekeeper-controller-58d7f44b9-2g296
Observed Generation: 1
Operations:
webhook
Template UID: 05f84ed7-9d50-4804-9817-fcc3be70a792
Id: gatekeeper-controller-58d7f44b9-kvgfl
Observed Generation: 1
Operations:
webhook
Template UID: 05f84ed7-9d50-4804-9817-fcc3be70a792
Created: true
Events: <none>
$ kubectl get crd
NAME CREATED AT
configs.config.gatekeeper.sh 2021-09-07T03:44:00Z
constraintpodstatuses.status.gatekeeper.sh 2021-09-07T03:44:00Z
constrainttemplatepodstatuses.status.gatekeeper.sh 2021-09-07T03:44:00Z
constrainttemplates.templates.gatekeeper.sh 2021-09-07T03:44:00Z
k8sazureallowedcapabilities.constraints.gatekeeper.sh 2021-09-07T03:47:30Z
k8sazureallowedusersgroups.constraints.gatekeeper.sh 2021-09-07T03:47:24Z
k8sazureblockautomounttoken.constraints.gatekeeper.sh 2021-09-07T03:47:39Z
k8sazureblockdefault.constraints.gatekeeper.sh 2021-09-07T03:47:25Z
k8sazureblockhostnamespace.constraints.gatekeeper.sh 2021-09-07T03:47:28Z
k8sazurecontainerallowedimages.constraints.gatekeeper.sh 2021-09-07T03:47:24Z
k8sazurecontainerallowedports.constraints.gatekeeper.sh 2021-09-07T03:47:37Z
k8sazurecontainerlimits.constraints.gatekeeper.sh 2021-09-07T03:47:41Z
k8sazurecontainernoprivilege.constraints.gatekeeper.sh 2021-09-07T03:47:35Z
k8sazurecontainernoprivilegeescalation.constraints.gatekeeper.sh 2021-09-07T03:47:43Z
k8sazuredisallowedcapabilities.constraints.gatekeeper.sh 2021-09-07T03:47:34Z
k8sazureenforceapparmor.constraints.gatekeeper.sh 2021-09-07T03:47:46Z
k8sazurehostfilesystem.constraints.gatekeeper.sh 2021-09-07T03:47:25Z
k8sazurehostnetworkingports.constraints.gatekeeper.sh 2021-09-07T03:47:32Z
k8sazureingresshttpsonly.constraints.gatekeeper.sh 2021-09-07T03:47:48Z
k8sazurepodenforcelabels.constraints.gatekeeper.sh 2021-09-07T06:17:25Z
k8sazurereadonlyrootfilesystem.constraints.gatekeeper.sh 2021-09-07T03:47:37Z
k8sazureserviceallowedports.constraints.gatekeeper.sh 2021-09-07T03:47:28Z
k8snodeaffinity.constraints.gatekeeper.sh 2021-09-08T03:45:38Z
$ kubectl get k8snodeaffinity
NAME AGE
azurepolicy-k8snodeaffinity-4fd8b43de76ea5c3cb7e 4h31m
$ kubectl describe k8snodeaffinity azurepolicy-k8snodeaffinity-4fd8b43de76ea5c3cb7e
Name: azurepolicy-k8snodeaffinity-4fd8b43de76ea5c3cb7e
Namespace:
Labels: managed-by=azure-policy-addon
Annotations: azure-policy-assignment-id:
/subscriptions/a0f4a733-4fce-4d49-b8a8-d30541fc1b45/resourceGroups/akspolicy/providers/Microsoft.Authorization/policyAssignments/be7098902...
azure-policy-definition-id:
/subscriptions/a0f4a733-4fce-4d49-b8a8-d30541fc1b45/providers/Microsoft.Authorization/policyDefinitions/b0d0c4b8-ef88-4d83-817e-786327bbb6...
azure-policy-definition-reference-id:
azure-policy-setdefinition-id:
constraint-installed-by: azure-policy-addon
API Version: constraints.gatekeeper.sh/v1beta1
Kind: K8sNodeAffinity
Metadata:
Creation Timestamp: 2021-09-08T03:48:05Z
Generation: 2
Managed Fields:
API Version: constraints.gatekeeper.sh/v1beta1
Fields Type: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
.:
f:azure-policy-assignment-id:
f:azure-policy-definition-id:
f:azure-policy-definition-reference-id:
f:azure-policy-setdefinition-id:
f:constraint-installed-by:
f:labels:
.:
f:managed-by:
f:spec:
.:
f:enforcementAction:
f:match:
.:
f:excludedNamespaces:
f:kinds:
f:parameters:
.:
f:nodeLabels:
Manager: azurepolicyaddon
Operation: Update
Time: 2021-09-08T03:48:05Z
API Version: constraints.gatekeeper.sh/v1beta1
Fields Type: FieldsV1
fieldsV1:
f:status:
.:
f:auditTimestamp:
f:byPod:
f:totalViolations:
Manager: gatekeeper
Operation: Update
Time: 2021-09-08T06:56:01Z
Resource Version: 149265
UID: 5aa0a522-41b7-482a-9a0f-2c065080f6cc
Spec:
Enforcement Action: deny
Match:
Excluded Namespaces:
kube-system
gatekeeper-system
azure-arc
Kinds:
API Groups:
*
Kinds:
Pod
Parameters:
Node Labels:
protected
Status:
Audit Timestamp: 2021-09-08T08:15:58Z
By Pod:
Constraint UID: 5aa0a522-41b7-482a-9a0f-2c065080f6cc
Enforced: true
Id: gatekeeper-audit-5456bb46b6-zzbmq
Observed Generation: 2
Operations:
audit
status
Constraint UID: 5aa0a522-41b7-482a-9a0f-2c065080f6cc
Enforced: true
Id: gatekeeper-controller-58d7f44b9-2g296
Observed Generation: 2
Operations:
webhook
Constraint UID: 5aa0a522-41b7-482a-9a0f-2c065080f6cc
Enforced: true
Id: gatekeeper-controller-58d7f44b9-kvgfl
Observed Generation: 2
Operations:
webhook
Total Violations: 0
Events: <none>
Pošlu do clusteru tento Pod.
apiVersion: v1
kind: Pod
metadata:
name: myapp
labels:
mylabel: myapp
spec:
containers:
- name: myapp
image: nginx
Dostávám chybu - dle očekávání.
Error from server ([azurepolicy-k8snodeaffinity-4fd8b43de76ea5c3cb7e] Pods must be assigned to specific nodepool using spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions containing {"key": "type", "operator": "In", "values": ["protected"]}; found no matchExpressions): error when creating "pod.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [azurepolicy-k8snodeaffinity-4fd8b43de76ea5c3cb7e] Pods must be assigned to specific nodepool using spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions containing {"key": "type", "operator": "In", "values": ["protected"]}; found no matchExpressions
Totéž u tohoto Podu.
apiVersion: v1
kind: Pod
metadata:
name: myapp
labels:
mylabel: myapp
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: type
operator: In
values: ["notsecured"]
containers:
- name: myapp
image: nginx
Ale tento projde naší politikou bez problému.
apiVersion: v1
kind: Pod
metadata:
name: myapp
labels:
mylabel: myapp
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: type
operator: In
values: ["protected"]
containers:
- name: myapp
image: nginx
Všechno tedy funguje jak má. Pokud stejně jako já vystačíte se základní databází pravidel pro Kubernetes v Azure Policy, bude všechno jednoduché a můžete tak krásně řídit všechny svoje clustery jak v cloudu, tak v on-premises nebo na IoT zařízeních díky Azure Arc. Pokud potřebujete něco speciálního, také to není problém - budete se muset trochu ponořit do jazyka Rego, ale jak jsme dnes vyzkoušeli nic vám v tom nebrání a i vaše vlastní šablony budou žít v rámci Azure Policy. Jinak řečeno vaše zvláštní požadavky se zhmotní v definici Azure Policy, s kterou pak může pracovat i váš kolega, který o Rego nikdy neslyšel a slyšet nechce.