diff --git a/clusters/dev/kustomization.yaml b/clusters/dev/kustomization.yaml index 88b1584..656f695 100644 --- a/clusters/dev/kustomization.yaml +++ b/clusters/dev/kustomization.yaml @@ -25,7 +25,7 @@ resources: # GitOps - ../../platform/gitops/argocd-image-updater # Tenants - - ../../tenants/product-team/apps/chat/base + - ../../tenants/product-team/apps/chat/overlays/dev patches: # Override .spec.destination.name diff --git a/policies/kubernetes.rego b/policies/kubernetes.rego index b8dc2d4..22e459a 100644 --- a/policies/kubernetes.rego +++ b/policies/kubernetes.rego @@ -2,68 +2,156 @@ package kubernetes import rego.v1 -# πŸ›‘οΈ 1. ΠšΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΠΈΠΌΠ΅Ρ‚ΡŒ рСсурсы +# ═══════════════════════════════════════════════════════════════════════════════ +# Cluster-scoped resources (Π½Π΅ Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‚ namespace) +# ═══════════════════════════════════════════════════════════════════════════════ +cluster_scoped_kinds := { + "Namespace", + "ClusterRole", + "ClusterRoleBinding", + "ClusterIssuer", + "CustomResourceDefinition", + "StorageClass", + "PersistentVolume", + "IngressClass", + "PriorityClass", + "ValidatingWebhookConfiguration", + "MutatingWebhookConfiguration", +} + +# ArgoCD resources (ΡƒΠΏΡ€Π°Π²Π»ΡΡŽΡ‚ΡΡ ArgoCD, ΠΈΠΌΠ΅ΡŽΡ‚ свой namespace Π² spec) +argocd_kinds := { + "Application", + "ApplicationSet", + "AppProject", +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# πŸ“› 1. Namespace Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΡƒΠΊΠ°Π·Π°Π½ (для namespaced рСсурсов) +# ═══════════════════════════════════════════════════════════════════════════════ +deny contains msg if { + not cluster_scoped_kinds[input.kind] + not argocd_kinds[input.kind] + not input.metadata.namespace + msg := sprintf("[%s/%s] Resource is missing namespace", [input.kind, input.metadata.name]) +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# πŸ›‘οΈ 2. ΠšΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΠΈΠΌΠ΅Ρ‚ΡŒ рСсурсы +# ═══════════════════════════════════════════════════════════════════════════════ deny contains msg if { input.kind == "Deployment" container := input.spec.template.spec.containers[_] not container.resources.limits.memory - msg := sprintf("Container %s missing memory limit", [container.name]) + msg := sprintf("[Deployment/%s] Container '%s' missing memory limit", [input.metadata.name, container.name]) } deny contains msg if { - input.kind == "Deployment" + input.kind == "Deployment" container := input.spec.template.spec.containers[_] not container.resources.limits.cpu - msg := sprintf("Container %s missing CPU limit", [container.name]) + msg := sprintf("[Deployment/%s] Container '%s' missing CPU limit", [input.metadata.name, container.name]) } -# πŸ”¬ 2. Π”ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ probes +# ═══════════════════════════════════════════════════════════════════════════════ +# πŸ”¬ 3. Π”ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ probes (Ρ‚ΠΎΠ»ΡŒΠΊΠΎ для production workloads) +# ═══════════════════════════════════════════════════════════════════════════════ deny contains msg if { input.kind == "Deployment" + is_production_workload container := input.spec.template.spec.containers[_] not container.livenessProbe - msg := sprintf("Container %s missing livenessProbe", [container.name]) + msg := sprintf("[Deployment/%s] Container '%s' missing livenessProbe", [input.metadata.name, container.name]) } deny contains msg if { input.kind == "Deployment" - container := input.spec.template.spec.containers[_] + is_production_workload + container := input.spec.template.spec.containers[_] not container.readinessProbe - msg := sprintf("Container %s missing readinessProbe", [container.name]) + msg := sprintf("[Deployment/%s] Container '%s' missing readinessProbe", [input.metadata.name, container.name]) } -# πŸ“› 3. Namespace Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΡƒΠΊΠ°Π·Π°Π½ -deny contains msg if { - not input.metadata.namespace - msg := "Resource is missing namespace" +# ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅ΠΌ production workloads ΠΏΠΎ labels +is_production_workload if { + input.metadata.labels.env == "prd" } -# πŸ” 4. ΠšΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π·Π°ΠΏΡƒΡΠΊΠ°Ρ‚ΡŒΡΡ Π½Π΅ ΠΎΡ‚ root +is_production_workload if { + input.metadata.labels.environment == "production" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# πŸ” 4. Security context (Ρ€Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ, Π½Π΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ) +# ═══════════════════════════════════════════════════════════════════════════════ +# ΠŸΡ€ΠΈΠΌΠ΅Ρ‡Π°Π½ΠΈΠ΅: ΠΌΠ½ΠΎΠ³ΠΈΠ΅ сторонниС Ρ‡Π°Ρ€Ρ‚Ρ‹ Π½Π΅ ΠΈΠΌΠ΅ΡŽΡ‚ runAsNonRoot +# warn contains msg if { +# input.kind == "Deployment" +# not input.spec.template.spec.securityContext.runAsNonRoot +# msg := sprintf("[Deployment/%s] Recommendation: set runAsNonRoot: true", [input.metadata.name]) +# } + +# ═══════════════════════════════════════════════════════════════════════════════ +# πŸ“¦ 5. Chat-API спСцифичныС ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ +# ═══════════════════════════════════════════════════════════════════════════════ +# ΠŸΡ€ΠΈΠΌΠ΅Ρ‡Π°Π½ΠΈΠ΅: ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΎ, Ρ‚.ΠΊ. annotation задаСтся Ρ‡Π΅Ρ€Π΅Π· Helm values +# deny contains msg if { +# input.kind == "Deployment" +# input.metadata.labels["app.kubernetes.io/name"] == "chat-api" +# not input.spec.template.metadata.annotations["openrouter.model"] +# msg := "[Deployment/chat-api] Missing openrouter.model annotation" +# } + +# ═══════════════════════════════════════════════════════════════════════════════ +# πŸ”­ 6. OTEL (Ρ‚ΠΎΠ»ΡŒΠΊΠΎ для production) +# ═══════════════════════════════════════════════════════════════════════════════ +# ΠŸΡ€ΠΈΠΌΠ΅Ρ‡Π°Π½ΠΈΠ΅: ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΎ, Ρ‚.ΠΊ. OTEL injector добавляСт автоматичСски +# deny contains msg if { +# input.kind == "Deployment" +# is_production_workload +# container := input.spec.template.spec.containers[_] +# not has_otel_endpoint(container) +# msg := sprintf("[Deployment/%s] Container '%s' missing OTEL_EXPORTER_OTLP_ENDPOINT", [input.metadata.name, container.name]) +# } + +# has_otel_endpoint(container) if { +# some env in container.env +# env.name == "OTEL_EXPORTER_OTLP_ENDPOINT" +# } + +# ═══════════════════════════════════════════════════════════════════════════════ +# 🏷️ 7. Labels ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ для workloads +# ═══════════════════════════════════════════════════════════════════════════════ deny contains msg if { input.kind == "Deployment" - not input.spec.template.spec.securityContext.runAsNonRoot - msg := "Deployment must set runAsNonRoot: true" + not input.metadata.labels["app.kubernetes.io/name"] + msg := sprintf("[Deployment/%s] Missing required label: app.kubernetes.io/name", [input.metadata.name]) } -# πŸ“¦ 5. Если ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ LLM β€” Π΄ΠΎΠ»ΠΆΠ½Π° Π±Ρ‹Ρ‚ΡŒ Π·Π°Π΄Π°Π½Π° модСль +# ═══════════════════════════════════════════════════════════════════════════════ +# πŸ”’ 8. SealedSecrets Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΠΈΠΌΠ΅Ρ‚ΡŒ namespace +# ═══════════════════════════════════════════════════════════════════════════════ deny contains msg if { - input.kind == "Deployment" - container := input.spec.template.spec.containers[_] - input.metadata.labels["app.kubernetes.io/name"] == "chat-api" - not input.spec.template.metadata.annotations["openrouter.model"] - msg := "chat-api is missing openrouter.model annotation" + input.kind == "SealedSecret" + not input.metadata.namespace + msg := sprintf("[SealedSecret/%s] Must have namespace specified", [input.metadata.name]) } -# πŸ”­ 6. OTEL ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π·Π°Π΄Π°Π½Ρ‹ +# ═══════════════════════════════════════════════════════════════════════════════ +# 🌐 9. Ingress Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ TLS для production +# ═══════════════════════════════════════════════════════════════════════════════ deny contains msg if { - input.kind == "Deployment" - container := input.spec.template.spec.containers[_] - not has_otel_endpoint(container) - msg := "OpenTelemetry OTLP endpoint is missing" + input.kind == "Ingress" + input.metadata.labels.env == "prd" + not input.spec.tls + msg := sprintf("[Ingress/%s] Production ingress must have TLS configured", [input.metadata.name]) } -# Π’ΡΠΏΠΎΠΌΠΎΠ³Π°Ρ‚Π΅Π»ΡŒΠ½Π°Ρ функция для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ OTEL endpoint -has_otel_endpoint(container) if { - some env in container.env - env.name == "OTEL_EXPORTER_OTLP_ENDPOINT" +# ═══════════════════════════════════════════════════════════════════════════════ +# πŸ“Š 10. HPA Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ Ρ€Π°Π·ΡƒΠΌΠ½Ρ‹Π΅ Π»ΠΈΠΌΠΈΡ‚Ρ‹ +# ═══════════════════════════════════════════════════════════════════════════════ +deny contains msg if { + input.kind == "HorizontalPodAutoscaler" + input.spec.maxReplicas > 100 + msg := sprintf("[HPA/%s] maxReplicas > 100 is likely a mistake", [input.metadata.name]) } \ No newline at end of file diff --git a/tenants/product-team/apps/chat/base/application.yaml b/tenants/product-team/apps/chat/base/application.yaml new file mode 100644 index 0000000..25728f1 --- /dev/null +++ b/tenants/product-team/apps/chat/base/application.yaml @@ -0,0 +1,35 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: chat-api + namespace: argocd + labels: + app.kubernetes.io/name: chat-api + app.kubernetes.io/part-of: chat + app.kubernetes.io/managed-by: argocd + annotations: + argocd.argoproj.io/sync-wave: "5" + # Image Updater annotations + argocd-image-updater.argoproj.io/image-list: chat=ghcr.io/justgithubaccount/chat-api:^1 + argocd-image-updater.argoproj.io/chat.update-strategy: semver + argocd-image-updater.argoproj.io/chat.helm.image-tag: image.tag + argocd-image-updater.argoproj.io/write-back-method: git + argocd-image-updater.argoproj.io/git-branch: main +spec: + project: default + source: + repoURL: https://github.com/justgithubaccount/app-poly-gitops-helm + targetRevision: main + path: . + helm: + valueFiles: + - values.yaml + destination: + name: in-cluster + namespace: chat-api + syncPolicy: + automated: + selfHeal: true + prune: true + syncOptions: + - CreateNamespace=true diff --git a/tenants/product-team/apps/chat/base/kustomization.yaml b/tenants/product-team/apps/chat/base/kustomization.yaml index 893fab5..04b2ef6 100644 --- a/tenants/product-team/apps/chat/base/kustomization.yaml +++ b/tenants/product-team/apps/chat/base/kustomization.yaml @@ -1,6 +1,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -# Application managed by ApplicationSet in platform/gitops/appsets/tenant-apps.yaml -# This directory contains shared resources for the chat application -resources: [] +resources: + - application.yaml diff --git a/tenants/product-team/apps/chat/overlays/dev/kustomization.yaml b/tenants/product-team/apps/chat/overlays/dev/kustomization.yaml index 3329c99..fa5a8e3 100644 --- a/tenants/product-team/apps/chat/overlays/dev/kustomization.yaml +++ b/tenants/product-team/apps/chat/overlays/dev/kustomization.yaml @@ -6,3 +6,32 @@ resources: - postgree-secrets.yaml - openrouter-secrets.yaml - github-secrets.yaml + +patches: + - target: + kind: Application + name: chat-api + patch: | + - op: add + path: /metadata/labels/env + value: dev + - op: replace + path: /spec/source/helm/valueFiles + value: + - values.yaml + - op: add + path: /spec/source/helm/values + value: | + image: + tag: "1.1.7" + replicaCount: 2 + resources: + limits: + cpu: 1000m + memory: 1Gi + requests: + cpu: 500m + memory: 512Mi + ingress: + enabled: true + host: chat-dev.syncjob.ru diff --git a/tenants/product-team/apps/chat/overlays/prd/kustomization.yaml b/tenants/product-team/apps/chat/overlays/prd/kustomization.yaml index 774a422..5a0f1f0 100644 --- a/tenants/product-team/apps/chat/overlays/prd/kustomization.yaml +++ b/tenants/product-team/apps/chat/overlays/prd/kustomization.yaml @@ -3,3 +3,36 @@ kind: Kustomization resources: - ../../base + # TODO: Create prd-specific SealedSecrets + # - postgree-secrets.yaml + # - openrouter-secrets.yaml + # - github-secrets.yaml + +patches: + - target: + kind: Application + name: chat-api + patch: | + - op: add + path: /metadata/labels/env + value: prd + - op: replace + path: /spec/source/helm/valueFiles + value: + - values.yaml + - op: add + path: /spec/source/helm/values + value: | + image: + tag: "1.1.7" + replicaCount: 3 + resources: + limits: + cpu: 2000m + memory: 2Gi + requests: + cpu: 1000m + memory: 1Gi + ingress: + enabled: true + host: chat.syncjob.ru diff --git a/tenants/product-team/apps/chat/overlays/stg/kustomization.yaml b/tenants/product-team/apps/chat/overlays/stg/kustomization.yaml index 774a422..0257c4e 100644 --- a/tenants/product-team/apps/chat/overlays/stg/kustomization.yaml +++ b/tenants/product-team/apps/chat/overlays/stg/kustomization.yaml @@ -3,3 +3,36 @@ kind: Kustomization resources: - ../../base + # TODO: Create stg-specific SealedSecrets + # - postgree-secrets.yaml + # - openrouter-secrets.yaml + # - github-secrets.yaml + +patches: + - target: + kind: Application + name: chat-api + patch: | + - op: add + path: /metadata/labels/env + value: stg + - op: replace + path: /spec/source/helm/valueFiles + value: + - values.yaml + - op: add + path: /spec/source/helm/values + value: | + image: + tag: "1.1.7" + replicaCount: 2 + resources: + limits: + cpu: 1000m + memory: 1Gi + requests: + cpu: 500m + memory: 512Mi + ingress: + enabled: true + host: chat-stg.syncjob.ru diff --git a/tenants/product-team/apps/chat/overlays/tst/kustomization.yaml b/tenants/product-team/apps/chat/overlays/tst/kustomization.yaml index 774a422..520eaf6 100644 --- a/tenants/product-team/apps/chat/overlays/tst/kustomization.yaml +++ b/tenants/product-team/apps/chat/overlays/tst/kustomization.yaml @@ -3,3 +3,36 @@ kind: Kustomization resources: - ../../base + # TODO: Create tst-specific SealedSecrets + # - postgree-secrets.yaml + # - openrouter-secrets.yaml + # - github-secrets.yaml + +patches: + - target: + kind: Application + name: chat-api + patch: | + - op: add + path: /metadata/labels/env + value: tst + - op: replace + path: /spec/source/helm/valueFiles + value: + - values.yaml + - op: add + path: /spec/source/helm/values + value: | + image: + tag: "1.1.7" + replicaCount: 1 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 250m + memory: 256Mi + ingress: + enabled: true + host: chat-tst.syncjob.ru