Skip to content

AstraDesk

6. Faza Wdrożenia - CI/CD, Sandboxing, Progresywna Dostawa

Cel: bezpiecznie wdrożyć SupportAgent do Kubernetes (EKS/OpenShift/on-prem) z silnymi domyślnymi ustawieniami:
non-root, read-only FS, NetworkPolicy, RBAC, SBOM+scan, polityki OPA i progresywne wdrożenie.



6.1 Obraz Kontenera (Dockerfile)


# plik: docker/Dockerfile
FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

# Zależności systemowe
RUN apt-get update && apt-get install -y --no-install-recommends \
      ca-certificates tini curl && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Kopiuj źródła (read-only w runtime)
COPY agents/ agents/
COPY mcp/ mcp/
COPY telemetry/ telemetry/
COPY configs/ configs/
COPY run_support_agent.py .

# Utwórz użytkownika non-root
RUN useradd -u 10001 -m astradesk
USER 10001:10001

# Eksponuj port web (jeśli dodasz REST shim)
ENV PORT=8080
CMD ["/usr/bin/tini","--","python","run_support_agent.py"]



6.2 Terraform (fragment AWS EKS)

# plik: infra/eks/main.tf
terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "aws" { region = var.region }

module "eks" {
  source         = "terraform-aws-modules/eks/aws"
  version        = "~> 20.0"
  cluster_name   = var.cluster_name
  cluster_version= "1.29"
  vpc_id         = var.vpc_id
  subnet_ids     = var.subnet_ids
  manage_aws_auth= true
}

output "cluster_name" { value = module.eks.cluster_name }


# plik: infra/eks/variables.tf
variable "region" { type = string }
variable "cluster_name" { type = string }
variable "vpc_id" { type = string }
variable "subnet_ids" { type = list(string) }



6.3 Manifesty Kubernetes (bezpieczne domyślne)


6.3.1 Namespace i RBAC

# plik: k8s/00-namespace-rbac.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: astra-agents
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: support-agent-readonly
  namespace: astra-agents
rules:
  - apiGroups: [""]           # core
    resources: ["configmaps","secrets"]
    verbs: ["get","list","watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: support-agent-readonly-binding
  namespace: astra-agents
subjects:
  - kind: ServiceAccount
    name: support-agent
    namespace: astra-agents
roleRef:
  kind: Role
  name: support-agent-readonly
  apiGroup: rbac.authorization.k8s.io


6.3.2 Deployment + Service

# plik: k8s/10-deployment.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: support-agent
  namespace: astra-agents
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: support-agent
  namespace: astra-agents
  labels: { app: support-agent }
spec:
  replicas: 2
  revisionHistoryLimit: 5
  selector:
    matchLabels: { app: support-agent }
  template:
    metadata:
      labels: { app: support-agent }
    spec:
      serviceAccountName: support-agent
      securityContext:
        runAsNonRoot: true
        seccompProfile: { type: RuntimeDefault }
      containers:
        - name: app
          image: ghcr.io/org/astradesk/support-agent:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
          env:
            - name: ASTRA_ENV
              valueFrom: { secretKeyRef: { name: support-agent-secrets, key: env } }
          resources:
            requests: { cpu: "250m", memory: "256Mi" }
            limits:   { cpu: "1",    memory: "512Mi" }
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: { drop: ["ALL"] }
          readinessProbe:
            httpGet: { path: /ready, port: 8080 }
            periodSeconds: 5
          livenessProbe:
            httpGet: { path: /health, port: 8080 }
            periodSeconds: 10
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: { medium: Memory }
---
apiVersion: v1
kind: Service
metadata:
  name: support-agent
  namespace: astra-agents
spec:
  selector: { app: support-agent }
  ports:
    - name: http
      port: 80
      targetPort: 8080


6.3.3 NetworkPolicy (domyślne deny-all + zezwolenie na ingress)

# plik: k8s/20-networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: astra-agents
spec:
  podSelector: {}
  policyTypes: ["Ingress","Egress"]
  ingress: []
  egress: []
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-gateway-to-agent
  namespace: astra-agents
spec:
  podSelector:
    matchLabels: { app: support-agent }
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels: { name: astra-control }   # gdzie działa Gateway
      ports:
        - protocol: TCP
          port: 8080



6.4 Bezpieczeństwo Pod i Admission (baseline)

# plik: k8s/30-podsecurity.yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted-legacy
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'runtime/default'
spec:
  privileged: false
  readOnlyRootFilesystem: true
  runAsUser:
    rule: MustRunAsNonRoot
  seLinux:
    rule: RunAsAny

Na nowoczesnych klastrach używaj Pod Security Standards (PSS) / etykiet namespace lub równoważnego OpenShift SCC.



6.5 Progresywna Dostawa (Canary lub Blue/Green)


# plik: k8s/40-rollout-kubernetes.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: support-agent
  namespace: astra-agents
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0


Lub z Argo Rollouts (przykład canary):

# plik: k8s/40-rollout-argo.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: support-agent
  namespace: astra-agents
spec:
  replicas: 2
  selector:
    matchLabels: { app: support-agent }
  template:
    metadata:
      labels: { app: support-agent }
    spec:
      containers:
        - name: app
          image: ghcr.io/org/astradesk/support-agent:1.0.1
          ports: [{ containerPort: 8080 }]
  strategy:
    canary:
      steps:
        - setWeight: 20
        - pause: { duration: 120 }
        - setWeight: 50
        - pause: { duration: 300 }
        - setWeight: 100



6.6 CI/CD - Jenkinsfile

// plik: ci/Jenkinsfile
pipeline {
  agent any
  environment {
    REG = "ghcr.io/org/astradesk"
    IMG = "support-agent"
    TAG = "1.0.${BUILD_NUMBER}"
  }
  stages {
    stage('Checkout'){ steps { checkout scm } }
    stage('CI'){
      steps {
        sh 'python -V'
        sh 'pip install -r requirements.txt'
        sh 'pytest -q'
      }
    }
    stage('Build'){
      steps { sh 'docker build -f docker/Dockerfile -t $REG/$IMG:$TAG .' }
    }
    stage('SBOM & Scan'){
      steps {
        sh 'syft $REG/$IMG:$TAG -o json > sbom.json || true'
        sh 'trivy image --exit-code 1 $REG/$IMG:$TAG || true'
      }
    }
    stage('Push'){ steps { sh 'docker push $REG/$IMG:$TAG' } }
    stage('Deploy'){
      steps {
        withKubeConfig([credentialsId: 'kubeconfig-eks']) {
          sh 'kubectl -n astra-agents apply -f k8s/10-deployment.yaml'
          sh 'kubectl -n astra-agents apply -f k8s/20-networkpolicy.yaml'
        }
      }
    }
  }
}



6.7 CI/CD - GitHub Actions (alternatywa)

# plik: .github/workflows/ci.yml
name: astra-ci
on:
  push: { branches: [main] }
  pull_request:
jobs:
  build-test-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.13' }
      - name: Instalacja
        run: pip install -r requirements.txt
      - name: Testy
        run: pytest -q
      - name: Budowa
        run: docker build -f docker/Dockerfile -t ghcr.io/org/astradesk/support-agent:${{ github.sha }} .
      - name: Login GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Push
        run: docker push ghcr.io/org/astradesk/support-agent:${{ github.sha }}
      - name: Kubeconfig (OIDC do AWS)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: eu-central-1
      - name: Wdrożenie
        run: |
          aws eks update-kubeconfig --name ${{ secrets.EKS_CLUSTER }}
          kubectl -n astra-agents apply -f k8s/10-deployment.yaml
          kubectl -n astra-agents apply -f k8s/20-networkpolicy.yaml



6.8 OPA/Gatekeeper (bramka polityki)

# plik: policies/deny-privileged.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPPrivilegedContainer
metadata:
  name: disallow-privileged
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]



6.9 Okablowanie Obserwowalności

  • OpenTelemetry: ustaw OTEL_EXPORTER_OTLP_ENDPOINT, eksportuj spany dla plan, tool_call, compose.

  • Prometheus: eksponuj /metrics przez bibliotekę kliencką jeśli dodasz HTTP shim.

  • Dashboardy Grafana: panel SLO (p95 opóźnienia), sukces narzędzi, koszt/zadanie.


flowchart LR
  App[SupportAgent] --> OTEL[Kolektor OTel]
  OTEL --> PRM[Prometheus]
  PRM --> GRF[Grafana]
  App --> L[Logi]




6.10 Lista Kontrolna Wydania (v1.0)

  • [ ] Kontenery są non-root, read-only FS, seccomp=RuntimeDefault.

  • [ ] NetworkPolicy domyślnie deny-all; jawne zezwolenie od Gateway.

  • [ ] RBAC minimalne; ServiceAccount związane z Role tylko do odczytu.

  • [ ] SBOM wygenerowane i obraz przeskanowany; nieznane digesty zablokowane.

  • [ ] OPA/Gatekeeper ograniczenia aktywne; logi admission monitorowane.

  • [ ] Plan canary/rollout zdefiniowany; rollback przetestowany.

  • [ ] Dashboardy i alerty skonfigurowane; kontrakt KPI załadowany w AstraOps.



6.11 Odniesienia Krzyżowe