A

Blog Post

Hardening vLLM Inference Service di Kubernetes dengan Istio dan OPA

Panduan production-ready untuk menjalankan vLLM di Kubernetes dengan kontrol jaringan, policy admission, mTLS, dan guardrail operasional yang lebih aman.

6 min read
Hardening vLLM Inference Service di Kubernetes dengan Istio dan OPA

TL;DR

  • Menjalankan vLLM di Kubernetes bukan hanya soal GPU scheduling. Boundary jaringan, secret handling, dan policy admission biasanya lebih menentukan apakah service bisa aman di production.
  • Istio membantu mTLS, ingress policy, dan observability jaringan. OPA Gatekeeper membantu menahan konfigurasi pod yang terlalu longgar sebelum masuk cluster.
  • Jalur yang sehat adalah memisahkan namespace inference, membatasi egress, memaksa image pinning, dan memverifikasi request path sebelum service menerima trafik nyata.

Pendahuluan

Inference service untuk model open-weight seperti vLLM sering tumbuh cepat dari proof-of-concept menjadi dependency internal yang penting. Begitu beberapa tim mulai mengandalkan endpoint yang sama, service tersebut berhenti menjadi eksperimen GPU dan berubah menjadi komponen platform. Di tahap ini, ancamannya bukan hanya container crash. Masalah yang lebih sering muncul adalah image tidak dipin, pod bisa keluar ke internet tanpa batas, model token tersimpan sembarangan, atau service account yang terlalu luas dipakai lintas workload.

Kubernetes memang memudahkan scheduling GPU dan rolling deploy, tetapi default cluster jarang cukup ketat untuk inference workload. Service inference biasanya punya surface area yang cukup besar: model download, metrics endpoint, ingress public atau semi-public, cache volume, dan kadang custom sidecar. Jika tidak ada guardrail, satu tim bisa men-deploy image latest, menjalankan privileged pod, atau mengekspor endpoint tanpa auth yang layak.

Tutorial ini fokus pada pola yang pragmatis: vLLM dijalankan di namespace khusus, dilindungi mTLS dan ingress policy melalui Istio, lalu dikunci di admission phase lewat OPA Gatekeeper. Pendekatannya bukan yang paling rumit, tetapi cukup realistis untuk cluster production yang ingin menjaga inference path tetap terkontrol.

Problem di Production

Masalah paling umum pada inference service bukan modelnya salah, tetapi perimeter service terlalu longgar. Tim sering membuka endpoint di LoadBalancer, membiarkan pod menarik artifact dari mana saja, dan menaruh token model registry di environment variable yang bocor ke log. Begitu service mulai populer, abuse juga naik: prompt flood, token burn, scraping, dan lateral movement lewat service account yang salah scope.

Problem kedua adalah drift operasional. Inference workload sering punya kebutuhan khusus seperti toleration GPU, huge memory request, warm cache, dan pre-pull image. Jika hal-hal ini tidak diperlakukan sebagai kebijakan yang terdokumentasi, rollout akan terasa stabil saat trafik rendah lalu runtuh saat concurrency naik.

Prerequisites

  • Cluster Kubernetes 1.29+ dengan node GPU yang sudah punya NVIDIA device plugin.
  • kubectl, helm, dan akses cluster-admin untuk instalasi awal.
  • Istio 1.23+ atau versi yang setara di cluster.
  • OPA Gatekeeper sudah tersedia, atau cluster siap memasangnya.
  • Image vLLM yang sudah diuji, misalnya vllm/vllm-openai:v0.8.4.
  • PVC atau ephemeral storage plan untuk cache model.
  • Secret token untuk model registry seperti Hugging Face jika model diambil dari private repository.

Architecture / Context

Arsitektur yang dipakai cukup sederhana tetapi kuat. Namespace ai-inference menjadi boundary deployment. Ingress publik masuk lewat Istio ingress gateway. Traffic internal ke pod vLLM menggunakan sidecar Istio sehingga mTLS dan policy jaringan bisa dipaksakan. Di depan deployment, OPA Gatekeeper memvalidasi bahwa pod inference tidak memakai latest, tidak privileged, dan wajib runAsNonRoot.

Model operational-nya adalah “secure by default, explicit by exception”. Artinya setiap perubahan deployment baru harus lolos policy admission terlebih dahulu. Kalau tim butuh exception, exception itu harus dinyatakan eksplisit, bukan terjadi karena default cluster terlalu permisif.

Concept diagram

Step-by-Step Implementation

Step 1: Buat namespace inference dan aktifkan Istio injection

kubectl create namespace ai-inference
kubectl label namespace ai-inference istio-injection=enabled --overwrite
kubectl create namespace policy-system

Namespace khusus memudahkan quota, network policy, dan observability per workload. Jangan campur inference service dengan namespace aplikasi umum.

Step 2: Buat secret untuk model registry

kubectl -n ai-inference create secret generic hf-token \
  --from-literal=HF_TOKEN='replace-me'

Jika model berasal dari artifact internal, gunakan secret store atau CSI secret provider. Jangan simpan token di manifest YAML mentah.

Step 3: Terapkan ResourceQuota dan LimitRange

apiVersion: v1
kind: ResourceQuota
metadata:
  name: ai-inference-quota
  namespace: ai-inference
spec:
  hard:
    requests.cpu: "32"
    requests.memory: 128Gi
    requests.nvidia.com/gpu: "4"
    limits.nvidia.com/gpu: "4"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: ai-inference-defaults
  namespace: ai-inference
spec:
  limits:
    - type: Container
      defaultRequest:
        cpu: "4"
        memory: 16Gi
      default:
        cpu: "8"
        memory: 32Gi

Apply:

kubectl apply -f quota.yaml

Quota mencegah satu tim memonopoli GPU dan memory tanpa terlihat.

Step 4: Deploy vLLM dengan security context yang ketat

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-gptq
  namespace: ai-inference
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm-gptq
  template:
    metadata:
      labels:
        app: vllm-gptq
    spec:
      serviceAccountName: vllm-runtime
      nodeSelector:
        nodepool: gpu
      containers:
        - name: vllm
          image: vllm/vllm-openai:v0.8.4
          args:
            - "--model"
            - "meta-llama/Llama-3.1-8B-Instruct"
            - "--host"
            - "0.0.0.0"
            - "--port"
            - "8000"
            - "--max-model-len"
            - "8192"
          env:
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-token
                  key: HF_TOKEN
          ports:
            - containerPort: 8000
          resources:
            requests:
              cpu: "6"
              memory: 24Gi
              nvidia.com/gpu: "1"
            limits:
              cpu: "8"
              memory: 32Gi
              nvidia.com/gpu: "1"
          securityContext:
            runAsNonRoot: true
            runAsUser: 10001
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: cache
              mountPath: /root/.cache/huggingface
      volumes:
        - name: cache
          persistentVolumeClaim:
            claimName: hf-cache-pvc

Poin pentingnya adalah pin image tag, security context eksplisit, dan resource request yang realistis. readOnlyRootFilesystem sering memaksa tim lebih disiplin soal write path.

Step 5: Ekspos service hanya ke mesh internal

apiVersion: v1
kind: Service
metadata:
  name: vllm-gptq
  namespace: ai-inference
spec:
  selector:
    app: vllm-gptq
  ports:
    - name: http
      port: 80
      targetPort: 8000
kubectl apply -f service.yaml

Step 6: Paksa mTLS dan batasi ingress lewat Istio

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: ai-inference-mtls
  namespace: ai-inference
spec:
  mtls:
    mode: STRICT
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: allow-inference-gateway
  namespace: ai-inference
spec:
  selector:
    matchLabels:
      app: vllm-gptq
  rules:
    - from:
        - source:
            principals:
              - "cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"
      to:
        - operation:
            methods: ["POST","GET"]
            paths: ["/v1/chat/completions", "/v1/models", "/health"]

Apply:

kubectl apply -f istio-security.yaml

Step 7: Tambahkan VirtualService untuk endpoint publik

apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: ai-public-gateway
  namespace: ai-inference
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: ai-public-tls
      hosts:
        - "llm.example.com"
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: vllm-public
  namespace: ai-inference
spec:
  hosts:
    - "llm.example.com"
  gateways:
    - ai-public-gateway
  http:
    - match:
        - uri:
            prefix: /v1/
      route:
        - destination:
            host: vllm-gptq.ai-inference.svc.cluster.local
            port:
              number: 80
      timeout: 60s

Step 8: Pasang OPA Gatekeeper dan policy guardrail

Jika Gatekeeper belum ada:

helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update
helm upgrade --install gatekeeper gatekeeper/gatekeeper -n gatekeeper-system --create-namespace

Buat constraint template:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredsecuritycontext
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredSecurityContext
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredsecuritycontext
        violation[{"msg": msg}] {
          container := input.review.object.spec.template.spec.containers[_]
          not container.securityContext.runAsNonRoot
          msg := "runAsNonRoot must be enabled"
        }
        violation[{"msg": msg}] {
          container := input.review.object.spec.template.spec.containers[_]
          container.securityContext.allowPrivilegeEscalation != false
          msg := "allowPrivilegeEscalation must be false"
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredSecurityContext
metadata:
  name: require-secctx-in-ai-namespace
spec:
  match:
    namespaces: ["ai-inference"]
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment"]

Tambahkan policy untuk menolak image latest:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sblocklatest
spec:
  crd:
    spec:
      names:
        kind: K8sBlockLatest
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sblocklatest
        violation[{"msg": msg}] {
          container := input.review.object.spec.template.spec.containers[_]
          endswith(container.image, ":latest")
          msg := sprintf("latest tag is forbidden: %v", [container.image])
        }

Step 9: Batasi egress dan service account

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: vllm-default-deny
  namespace: ai-inference
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-istio-and-dns
  namespace: ai-inference
spec:
  podSelector:
    matchLabels:
      app: vllm-gptq
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: istio-system
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system

Jangan beri RBAC tambahan pada vllm-runtime kecuali memang ada alasan. Inference service jarang butuh akses Kubernetes API langsung.

Architecture flow

Verification

Pastikan pod dan service sehat:

kubectl -n ai-inference get pods -o wide
kubectl -n ai-inference get svc
kubectl -n ai-inference describe pod -l app=vllm-gptq

Verifikasi sidecar Istio ter-inject:

kubectl -n ai-inference get pod -l app=vllm-gptq -o jsonpath='{.items[0].spec.containers[*].name}'

Output seharusnya menampilkan vllm dan istio-proxy.

Uji endpoint dari ingress:

curl -k https://llm.example.com/v1/models

Jika policy admission aktif, coba apply deployment yang melanggar:

kubectl apply -f broken-deployment-using-latest.yaml

Hasil yang benar adalah request ditolak Gatekeeper.

Troubleshooting

Jika pod Pending, cek scheduling GPU:

kubectl describe pod -n ai-inference -l app=vllm-gptq
kubectl get nodes -L nvidia.com/gpu.present,nodepool

Jika request gagal lewat ingress tetapi pod sehat, cek AuthorizationPolicy dan VirtualService:

kubectl -n ai-inference get authorizationpolicy,virtualservice,gateway
kubectl logs -n istio-system deploy/istio-ingressgateway

Jika model tidak mau dimuat, cek path cache dan secret:

kubectl -n ai-inference exec deploy/vllm-gptq -- env | grep HF_
kubectl -n ai-inference logs deploy/vllm-gptq

Masalah yang sering muncul adalah storage cache terlalu kecil, token tidak valid, atau image tidak cocok dengan driver GPU host.

Production Notes

Inference service biasanya perlu HPA atau KEDA berbasis concurrency, tetapi scaling GPU tetap harus konservatif karena cold start model cukup mahal. Tambahkan metric latency per route dan token throughput, bukan hanya CPU dan memory. Batasi log prompt mentah karena prompt bisa berisi data sensitif. Untuk service public, pertimbangkan API gateway atau ext-authz di depan Istio untuk rate limit dan tenant isolation.

Jika model berasal dari external registry, siapkan mekanisme prefetch atau image warm-up. Download model saat pod start bisa memperpanjang blast radius setiap rolling deploy.

Trade-offs

Istio dan Gatekeeper menambah kontrol, tetapi juga menambah lapisan konfigurasi yang harus dipahami tim platform. mTLS dan authz policy membuat boundary lebih sehat, tetapi debugging path jaringan jadi lebih kompleks. readOnlyRootFilesystem dan runAsNonRoot meningkatkan keamanan, tetapi sebagian image community belum siap dan perlu penyesuaian entrypoint atau cache path.

Failure Modes

  • Policy admission terlalu longgar sehingga image latest atau privileged pod lolos ke production.
  • Policy terlalu ketat dan memblokir rollout darurat karena exception path tidak disiapkan.
  • mTLS aktif tetapi ingress atau service identity salah, sehingga service terlihat hidup tetapi tidak bisa diakses.
  • Model warm-up terlalu lama dan autoscaling menciptakan antrian request yang tidak pernah stabil.

Best Practices

  • Pisahkan namespace inference dari workload aplikasi umum.
  • Pin image, model version, dan GPU resource request secara eksplisit.
  • Gunakan Gatekeeper untuk menegakkan baseline minimal, bukan mengandalkan review manual semata.
  • Audit egress dan secret handling sebelum service dibuka ke tenant atau internet.
  • Simpan runbook rollback yang jelas untuk model update dan policy update.

Kesalahan Umum

  • membuka service inference langsung sebagai LoadBalancer tanpa mesh policy
  • menyimpan token model registry di manifest plaintext
  • membiarkan service account default dipakai inference pod
  • menganggap GPU scheduling selesai berarti service sudah production-ready

Kesimpulan

vLLM di Kubernetes bisa menjadi inference layer yang sangat efektif, tetapi hanya jika cluster memperlakukannya sebagai service platform, bukan eksperimen notebook yang dipindahkan ke deployment YAML. Dengan Istio untuk jaringan dan OPA untuk admission guardrail, tim bisa menurunkan risiko paling umum sebelum inference workload benar-benar dipakai lintas organisasi.

Kalau artikel ini membantu, kamu bisa support eksperimen berikutnya.

Apresiasi di Trakteer

Keep Reading

Related posts