Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 294 additions & 0 deletions .claude/skills/deploy-agent-aks-agentid/SKILL.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: agentid
labels:
app.kubernetes.io/part-of: entra-agentid-sidecar-dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Workload-identity-enabled ServiceAccount.
# Federation is created by scripts/03-federate-blueprint.ps1:
# issuer = AKS cluster OIDC issuer
# subject = system:serviceaccount:agentid:agent-sa
# audience = api://AzureADTokenExchange
# The Blueprint app trusts this subject directly — no UAMI in the middle.
apiVersion: v1
kind: ServiceAccount
metadata:
name: agent-sa
namespace: agentid
annotations:
# client-id of the BLUEPRINT app (NOT the agent app).
# Replaced by 04-apply-manifests.sh via envsubst.
azure.workload.identity/client-id: "${BLUEPRINT_APP_ID}"
azure.workload.identity/tenant-id: "${TENANT_ID}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: weather-api
namespace: agentid
spec:
replicas: 1
selector:
matchLabels: { app: weather-api }
template:
metadata:
labels: { app: weather-api }
spec:
containers:
- name: weather-api
image: "${ACR_NAME}.azurecr.io/agent-id-dev/weather-api:1.0.0"
ports:
- containerPort: 8080
env:
- { name: TENANT_ID, value: "${TENANT_ID}" }
- { name: VALIDATE_TOKEN_SIGNATURE, value: "true" }
volumeMounts:
- name: patched-app
mountPath: /app/app.py
subPath: app.py
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "500m", memory: "512Mi" }
readinessProbe:
httpGet: { path: /health, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: patched-app
configMap:
name: weather-api-patch
---
apiVersion: v1
kind: Service
metadata:
name: weather-api
namespace: agentid
spec:
type: ClusterIP
selector: { app: weather-api }
ports:
- name: http
port: 8080
targetPort: 8080
89 changes: 89 additions & 0 deletions .claude/skills/deploy-agent-aks-agentid/manifests/30-ollama.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ollama-models
namespace: agentid
spec:
accessModes: [ReadWriteOnce]
storageClassName: managed-csi
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ollama
namespace: agentid
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels: { app: ollama }
template:
metadata:
labels: { app: ollama }
spec:
# Init container pulls the model once into the PVC so the main
# container starts fast and the model survives pod restarts.
initContainers:
- name: model-puller
image: ollama/ollama:latest
command: ["/bin/sh","-c"]
# Use $OLLAMA_MODEL (no braces) so envsubst leaves runtime expansion
# to bash. With ${OLLAMA_MODEL}, envsubst would bake the model name
# into the args at apply time, defeating future `kubectl set env`.
args:
- |
set -e
ollama serve &
PID=$!
until ollama list >/dev/null 2>&1; do sleep 1; done
if ! ollama list | awk '{print $1}' | grep -q "^$OLLAMA_MODEL$"; then
echo "Pulling $OLLAMA_MODEL..."
ollama pull "$OLLAMA_MODEL"
else
echo "$OLLAMA_MODEL already present, skipping pull."
fi
kill $PID
wait $PID 2>/dev/null || true
env:
- { name: OLLAMA_MODEL, value: "${OLLAMA_MODEL}" }
- { name: OLLAMA_HOST, value: "0.0.0.0:11434" }
- { name: HOME, value: "/root" }
volumeMounts:
- { name: models, mountPath: /root/.ollama }
containers:
- name: ollama
image: ollama/ollama:latest
ports:
- containerPort: 11434
env:
- { name: OLLAMA_HOST, value: "0.0.0.0:11434" }
volumeMounts:
- { name: models, mountPath: /root/.ollama }
resources:
requests: { cpu: "500m", memory: "2Gi" }
limits: { cpu: "2", memory: "4Gi" }
readinessProbe:
tcpSocket: { port: 11434 }
initialDelaySeconds: 10
periodSeconds: 10
volumes:
- name: models
persistentVolumeClaim:
claimName: ollama-models
---
apiVersion: v1
kind: Service
metadata:
name: ollama
namespace: agentid
spec:
type: ClusterIP
selector: { app: ollama }
ports:
- name: http
port: 11434
targetPort: 11434
81 changes: 81 additions & 0 deletions .claude/skills/deploy-agent-aks-agentid/manifests/40-agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Agent pod = llm-agent + auth-sidecar (true sidecar, same trust boundary).
# weather-api and ollama are reached via in-cluster Services.
apiVersion: apps/v1
kind: Deployment
metadata:
name: llm-agent
namespace: agentid
spec:
replicas: 1
selector:
matchLabels: { app: llm-agent }
template:
metadata:
labels:
app: llm-agent
# Required for the Azure Workload Identity mutating webhook to
# inject AZURE_* env vars and the projected SA token volume.
azure.workload.identity/use: "true"
spec:
serviceAccountName: agent-sa
containers:
- name: llm-agent
image: "${ACR_NAME}.azurecr.io/agent-id-dev/llm-agent:1.0.0"
ports:
- containerPort: 3000
env:
- { name: TENANT_ID, value: "${TENANT_ID}" }
- { name: BLUEPRINT_APP_ID, value: "${BLUEPRINT_APP_ID}" }
- { name: AGENT_APP_ID, value: "${AGENT_CLIENT_ID}" }
- { name: AGENT_CLIENT_ID, value: "${AGENT_CLIENT_ID}" }
- { name: CLIENT_SPA_APP_ID, value: "${CLIENT_SPA_APP_ID}" }
- { name: SIDECAR_URL, value: "http://localhost:5000" }
- { name: WEATHER_API_URL, value: "http://weather-api.agentid.svc.cluster.local:8080" }
- { name: OLLAMA_URL, value: "http://ollama.agentid.svc.cluster.local:11434" }
- { name: OLLAMA_MODEL, value: "${OLLAMA_MODEL}" }
resources:
requests: { cpu: "200m", memory: "256Mi" }
limits: { cpu: "1", memory: "1Gi" }
volumeMounts:
- name: patched-app
mountPath: /app/app.py
subPath: app.py
readinessProbe:
httpGet: { path: /, port: 3000 }
initialDelaySeconds: 10
periodSeconds: 10

- name: sidecar
image: mcr.microsoft.com/entra-sdk/auth-sidecar:1.0.0-azurelinux3.0-distroless
# Sidecar listens on localhost only — never exposed via Service.
env:
- { name: AzureAd__Instance, value: "https://login.microsoftonline.com/" }
- { name: AzureAd__TenantId, value: "${TENANT_ID}" }
- { name: AzureAd__ClientId, value: "${BLUEPRINT_APP_ID}" }
# Workload Identity: the projected SA token IS the signed
# assertion the Blueprint app's FIC accepts. Read it from disk.
- { name: AzureAd__ClientCredentials__0__SourceType,
value: "SignedAssertionFilePath" }
- { name: AzureAd__ClientCredentials__0__SignedAssertionFileDiskPath,
value: "/var/run/secrets/azure/tokens/azure-identity-token" }
# Autonomous (app-only) downstream
- { name: DownstreamApis__graph-app__BaseUrl,
value: "https://graph.microsoft.com/v1.0/" }
- { name: DownstreamApis__graph-app__Scopes__0,
value: "https://graph.microsoft.com/.default" }
- { name: DownstreamApis__graph-app__RequestAppToken,
value: "true" }
# OBO downstream
- { name: DownstreamApis__graph__BaseUrl,
value: "https://graph.microsoft.com/v1.0/" }
- { name: DownstreamApis__graph__Scopes__0,
value: "https://graph.microsoft.com/.default" }
- { name: ASPNETCORE_ENVIRONMENT, value: "Production" }
- { name: ASPNETCORE_URLS, value: "http://+:5000" }
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "500m", memory: "512Mi" }
volumes:
- name: patched-app
configMap:
name: llm-agent-patch
14 changes: 14 additions & 0 deletions .claude/skills/deploy-agent-aks-agentid/manifests/50-ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# LoadBalancer keeps things simple — no ingress controller required.
# Swap to type ClusterIP + an Ingress object if you have NGINX/AGIC installed.
apiVersion: v1
kind: Service
metadata:
name: llm-agent
namespace: agentid
spec:
type: LoadBalancer
selector: { app: llm-agent }
ports:
- name: http
port: 80
targetPort: 3000
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: weather-api
namespace: agentid
spec:
replicas: 1
selector:
matchLabels: { app: weather-api }
template:
metadata:
labels: { app: weather-api }
spec:
containers:
- name: weather-api
image: "${ACR_NAME}.azurecr.io/agent-id-dev/weather-api:1.0.0"
ports:
- containerPort: 8080
env:
- { name: TENANT_ID, value: "${TENANT_ID}" }
- { name: VALIDATE_TOKEN_SIGNATURE, value: "true" }
volumeMounts:
- name: patched-app
mountPath: /app/app.py
subPath: app.py
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "500m", memory: "512Mi" }
readinessProbe:
httpGet: { path: /health, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: patched-app
configMap:
name: weather-api-patch
---
apiVersion: v1
kind: Service
metadata:
name: weather-api
namespace: agentid
spec:
type: ClusterIP
selector: { app: weather-api }
ports:
- name: http
port: 8080
targetPort: 8080
81 changes: 81 additions & 0 deletions .claude/skills/deploy-agent-aks-agentid/references/40-agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Agent pod = llm-agent + auth-sidecar (true sidecar, same trust boundary).
# weather-api and ollama are reached via in-cluster Services.
apiVersion: apps/v1
kind: Deployment
metadata:
name: llm-agent
namespace: agentid
spec:
replicas: 1
selector:
matchLabels: { app: llm-agent }
template:
metadata:
labels:
app: llm-agent
# Required for the Azure Workload Identity mutating webhook to
# inject AZURE_* env vars and the projected SA token volume.
azure.workload.identity/use: "true"
spec:
serviceAccountName: agent-sa
containers:
- name: llm-agent
image: "${ACR_NAME}.azurecr.io/agent-id-dev/llm-agent:1.0.0"
ports:
- containerPort: 3000
env:
- { name: TENANT_ID, value: "${TENANT_ID}" }
- { name: BLUEPRINT_APP_ID, value: "${BLUEPRINT_APP_ID}" }
- { name: AGENT_APP_ID, value: "${AGENT_CLIENT_ID}" }
- { name: AGENT_CLIENT_ID, value: "${AGENT_CLIENT_ID}" }
- { name: CLIENT_SPA_APP_ID, value: "${CLIENT_SPA_APP_ID}" }
- { name: SIDECAR_URL, value: "http://localhost:5000" }
- { name: WEATHER_API_URL, value: "http://weather-api.agentid.svc.cluster.local:8080" }
- { name: OLLAMA_URL, value: "http://ollama.agentid.svc.cluster.local:11434" }
- { name: OLLAMA_MODEL, value: "${OLLAMA_MODEL}" }
resources:
requests: { cpu: "200m", memory: "256Mi" }
limits: { cpu: "1", memory: "1Gi" }
volumeMounts:
- name: patched-app
mountPath: /app/app.py
subPath: app.py
readinessProbe:
httpGet: { path: /, port: 3000 }
initialDelaySeconds: 10
periodSeconds: 10

- name: sidecar
image: mcr.microsoft.com/entra-sdk/auth-sidecar:1.0.0-azurelinux3.0-distroless
# Sidecar listens on localhost only — never exposed via Service.
env:
- { name: AzureAd__Instance, value: "https://login.microsoftonline.com/" }
- { name: AzureAd__TenantId, value: "${TENANT_ID}" }
- { name: AzureAd__ClientId, value: "${BLUEPRINT_APP_ID}" }
# Workload Identity: the projected SA token IS the signed
# assertion the Blueprint app's FIC accepts. Read it from disk.
- { name: AzureAd__ClientCredentials__0__SourceType,
value: "SignedAssertionFilePath" }
- { name: AzureAd__ClientCredentials__0__SignedAssertionFileDiskPath,
value: "/var/run/secrets/azure/tokens/azure-identity-token" }
# Autonomous (app-only) downstream
- { name: DownstreamApis__graph-app__BaseUrl,
value: "https://graph.microsoft.com/v1.0/" }
- { name: DownstreamApis__graph-app__Scopes__0,
value: "https://graph.microsoft.com/.default" }
- { name: DownstreamApis__graph-app__RequestAppToken,
value: "true" }
# OBO downstream
- { name: DownstreamApis__graph__BaseUrl,
value: "https://graph.microsoft.com/v1.0/" }
- { name: DownstreamApis__graph__Scopes__0,
value: "https://graph.microsoft.com/.default" }
- { name: ASPNETCORE_ENVIRONMENT, value: "Production" }
- { name: ASPNETCORE_URLS, value: "http://+:5000" }
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "500m", memory: "512Mi" }
volumes:
- name: patched-app
configMap:
name: llm-agent-patch
Loading