Installation
gh skills-hub install azure-kubernetes Don't have the extension? Run gh extension install samueltauil/skills-hub first.
Download and extract to your repository:
.github/skills/azure-kubernetes/ Extract the ZIP to .github/skills/ in your repo. The folder name must match azure-kubernetes for Copilot to auto-discover it.
Skill Files (55)
SKILL.md 11.0 KB
---
name: azure-kubernetes
license: MIT
metadata:
author: Microsoft
version: "1.2.3"
description: "Plan, create, and configure production-ready Azure Kubernetes Service (AKS) clusters. Covers Day-0 checklist, SKU selection (Automatic vs Standard), networking options (private API server, Azure CNI Overlay, egress configuration), security, and operations (autoscaling, upgrade strategy, cost analysis). WHEN: create AKS environment, provision AKS, enable AKS observability, design AKS networking, choose AKS SKU, secure AKS, optimize AKS, AKS spot nodes, AKS cluster-autoscaler, rightsize AKS pod, pod rightsizing, over-provisioned AKS pod, pod resource requests and limits, Vertical Pod Autoscaler, VPA recommendations."
---
# Azure Kubernetes Service
> **AUTHORITATIVE GUIDANCE β MANDATORY COMPLIANCE**
>
> This skill produces a **recommended AKS cluster configuration** based on user requirements, distinguishing **Day-0 decisions** (networking, API server β hard to change later) from **Day-1 features** (can enable post-creation). See [CLI reference](./references/cli-reference.md) for commands.
## Quick Reference
| Property | Value |
|----------|-------|
| Best for | AKS cluster planning and Day-0 decisions |
| MCP Tools | `mcp_azure_mcp_aks` |
| CLI | `az aks create`, `az aks show`, `kubectl get`, `kubectl describe` |
| Related skills | azure-kubernetes-app-deploy (deploy an app to an existing cluster), azure-diagnostics (baseline AKS troubleshooting), azure-validate (readiness checks), azure-kubernetes-automatic-readiness (migrate existing cluster to AKS Automatic), optional `aks-skills` operations add-on (focused incident, exact-signature, packet-capture, and GPU Day-2 workflows) |
## When to Use This Skill
Activate this skill when user wants to:
- Create a new AKS cluster
- Plan AKS cluster configuration for production workloads
- Design AKS networking (API server access, pod IP model, egress)
- Set up AKS identity and secrets management
- Configure AKS governance (Azure Policy, Deployment Safeguards)
- Enable AKS observability (Container Insights, Managed Prometheus, Grafana)
- Define AKS upgrade and patching strategy
- Understand AKS Automatic vs Standard SKU differences
- Get a Day-0 checklist for AKS cluster setup and configuration
> **Deploying an application to an existing cluster?** This skill provisions and
> configures the *cluster*. To containerize an app and deploy it to a cluster
> that already exists (Dockerfile + manifests + Deployment Safeguards), use the
> `azure-kubernetes-app-deploy` sub-skill instead.
> **Deeper operational task?** Keep Day-0 planning and basic operations in this
> base skill. Load [optional-aks-operations.md](./references/optional-aks-operations.md)
> only when a focused optional AKS operational skill could improve the current
> task.
## Rules
1. Start with the user's requirements for provisioning compute, networking, security, and other settings.
2. Use the `azure` MCP server and select `mcp_azure_mcp_aks` first to discover the exact AKS-specific MCP tools surfaced by the client. Choose the smallest discovered AKS tool that fits the task, and fall back to Azure CLI (`az aks`) only when the needed functionality is not exposed through the AKS MCP surface.
3. Determine if AKS Automatic or Standard SKU is more appropriate based on the user's need for control vs convenience. Default to AKS Automatic unless specific customizations are required.
4. Document decisions and rationale for cluster configuration choices, especially for Day-0 decisions that are hard to change later (networking, API server access).
## Required Inputs (Ask only whatβs needed)
If the user is unsure, use safe defaults.
- AKS environment type: dev/test or production
- Region(s), availability zones, preferred node VM sizes
- Expected scale (node/cluster count, workload size)
- Networking requirements (API server access, pod IP model, ingress/egress control)
- Security and identity requirements, including image registry
- Upgrade and observability preferences
- Cost constraints
## Workflow
### 1. Cluster Type
- **AKS Automatic** (default): Best for most production workloads, provides a curated experience with pre-configured best practices for security, reliability, and performance. Use unless you have specific custom requirements for networking, autoscaling, or node pool configurations not supported by Node Auto-Provisioning (NAP).
- **AKS Standard**: Use if you need full control over environment configuration, which requires additional overhead to set up and manage.
### 2. Networking (Pod IP, Egress, Ingress, Dataplane)
**Pod IP Model** (Key Day-0 decision):
- **Azure CNI Overlay** (recommended): pod IPs from private overlay range, not VNet-routable, scales to large environments and good for most workloads
- **Azure CNI (VNet-routable)**: pod IPs directly from VNet (pod subnet or node subnet), use when pods must be directly addressable from VNet or on-prem
- Docs: https://learn.microsoft.com/azure/aks/azure-cni-overlay
**Dataplane & Network Policy**:
- **Azure CNI powered by Cilium** (recommended): eBPF-based for high-performance packet processing, network policies, and observability
**Egress**:
- **Static Egress Gateway** for stable, predictable outbound IPs
- For restricted egress: UDR + Azure Firewall or NVA
**Ingress**:
- **App Routing addon with Gateway API** β recommended default for HTTP/HTTPS workloads
- **Istio service mesh with Gateway API** - for advanced traffic management, mTLS, canary releases
- **Application Gateway for Containers** β for L7 load balancing with WAF integration
**DNS**:
- Enable **LocalDNS** on all node pools for reliable, performant DNS resolution
### 3. Security
- Use **Microsoft Entra ID** everywhere (control plane, Workload Identity for pods, node access). Avoid static credentials.
- Azure Key Vault via **Secrets Store CSI Driver** for secrets
- Enable **Azure Policy** + **Deployment Safeguards**
- Enable **Encryption at rest** for etcd/API server; **in-transit** for node-to-node
- Allow only signed, policy-approved images (Azure Policy + Ratify), prefer **Azure Container Registry**
- **Isolation**: Use namespaces, network policies, scoped logging
### 4. Observability
- Use Managed Prometheus and Container Insights with Grafana for AKS observability (logs + metrics).
- Enable Diagnostic Settings to collect control plane logs and audit logs in a Log Analytics workspace for security monitoring and troubleshooting.
- For other monitoring and troubleshooting tools, use features like the Agentic CLI for AKS, Application Insights, Resource Health Center, AppLens detectors, and Azure Advisors.
### 5. Upgrades & Patching
- Configure **Maintenance Windows** for controlled upgrade timing
- Enable **auto-upgrades** for control plane and node OS to stay up-to-date with security patches and Kubernetes versions
- Consider **LTS versions** for enterprise stability (2-year support) by upgrading your AKS environment to the Premium tier
- **Fleet upgrades**: Use **AKS Fleet Manager** for staged rollout across test to production environments
### 6. Performance
- Use **Ephemeral OS disks** (`--node-osdisk-type Ephemeral`) for faster node startup
- Select **Azure Linux** as node OS (smaller footprint, faster boot)
- Enable **KEDA** for event-driven autoscaling beyond HPA
### 7. Node Pools & Compute
- **Dedicated system node pool**: At least 2 nodes, tainted for system workloads only (`CriticalAddonsOnly`)
- Enable **Node Auto Provisioning (NAP)** on all pools for cost savings and responsive scaling
- Use **latest generation SKUs (v5/v6)** for host-level optimizations
- **Avoid B-series VMs** β burstable SKUs cause performance/reliability issues
- Use SKUs with **at least 4 vCPUs** for production workloads
- Set **topology spread constraints** to distribute pods across hosts/zones per SLO
### 8. Reliability
- Deploy across **3 Availability Zones** (`--zones 1 2 3`)
- Use **Standard tier** for zone-redundant control plane + 99.95% SLA for API server availability
- Enable **Microsoft Defender for Containers** for runtime protection
- Configure **PodDisruptionBudgets** for all production workloads
- Use **topology spread constraints** to ensure pod distribution across failure domains
### 9. Cost Controls
- Use **Spot node pools** for batch/interruptible workloads (up to 90% savings)
- **Stop/Start** dev/test clusters: `az aks stop/start`
- Consider **Reserved Instances** or **Savings Plans** for steady-state workloads
**Deep-dive scenarios** β load only the relevant reference file:
| Scenario | Trigger Keywords | Reference |
|----------|-----------------|-----------|
| Pod Rightsizing | over-provisioned pods, CPU requests, memory requests, rightsize workloads | [azure-aks-rightsizing.md](./references/azure-aks-rightsizing.md) |
| VPA Setup | vertical pod autoscaler, VPA recommendations, VPA enable | [azure-aks-vpa.md](./references/azure-aks-vpa.md) |
| Cluster Autoscaler | idle nodes, CAS off, enable autoscaler, scale-down profile, node utilization | [azure-aks-autoscaler.md](./references/azure-aks-autoscaler.md) |
| Spot Node Pools | Spot VMs, Spot nodes, batch workloads, cheaper nodes | [azure-aks-spot.md](./references/azure-aks-spot.md) |
> **Disambiguation:** If a prompt matches multiple rows (e.g., "cheaper nodes" could suggest both Spot and autoscaler), prefer the most specific match. If ambiguous, ask the user to clarify their intent before loading a reference file.
## Guardrails / Safety
- Do not request or output secrets (tokens, keys).
- Do not ask the user to paste subscription IDs. Discover subscription and resource scope via MCP tools (e.g., list subscriptions, list resource groups) or `az account show` / `az account list` so the agent can resolve context without exposing identifiers.
- If requirements are ambiguous for day-0 critical decisions, ask the user clarifying questions. For day-1 enabled features, propose 2β3 safe options with tradeoffs and choose a conservative default.
- Do not promise zero downtime; advise workload safeguards (PDBs, probes, replicas) and staged upgrades along with best practices for reliability and performance.
## MCP Tools
| Tool | Purpose | Key Parameters |
|------|---------|----------------|
| `mcp_azure_mcp_aks` | AKS MCP entry point used to discover the exact AKS-specific tools exposed by the client | Discover the callable AKS tool first, then use that tool's parameters |
## Error Handling
| Error / Symptom | Likely Cause | Remediation |
|-----------------|--------------|-------------|
| MCP tool call fails or times out | Invalid credentials, subscription, or AKS context | Verify `az login`, confirm the active subscription context with `az account show`, and check the target resource group without echoing subscription identifiers back to the user |
| Quota exceeded | Regional vCPU or resource limits | Request quota increase or select different region/VM SKU |
| Networking conflict (IP exhaustion) | Pod subnet too small for overlay/CNI | Re-plan IP ranges; may require cluster recreation (Day-0) |
| Workload Identity not working | Missing OIDC issuer or federated credential | Enable `--enable-oidc-issuer --enable-workload-identity`, configure federated identity |
SKILL.md 2.0 KB
---
name: azure-kubernetes-app-deploy
license: MIT
metadata:
author: Microsoft
version: "1.0.0"
description: "Use when deploying an existing web application or API to an already-running Azure Kubernetes Service cluster. Detects the framework, generates a Dockerfile and Kubernetes manifests, validates against AKS Deployment Safeguards, and deploys with verification. WHEN: deploy app to AKS, deploy to existing AKS cluster, containerize app for Kubernetes, generate K8s manifests for Azure, set up CI/CD for AKS, my AKS deployment is failing safeguard checks, I have a Django/Express/Spring Boot app to run on AKS. DO NOT USE FOR: creating or provisioning an AKS cluster (use azure-kubernetes), assessing migration to AKS Automatic (use azure-kubernetes-automatic-readiness), or deploying to non-AKS targets like Web Apps, Container Apps, or Functions."
---
# Deploy to AKS
**Use when:** deploying a web app/API to AKS; containerizing for Kubernetes; generating manifests; AKS CI/CD; DS001βDS013 failures.
**Not for:** provisioning clusters (`azure-kubernetes`), AKS Automatic readiness (`azure-kubernetes-automatic-readiness`), non-AKS targets.
## Workflow
Requires: existing AKS cluster, `az login`, `kubectl` configured. Follow `phases/quick-deploy.md`. On failure: `references/rollback.md`.
## References
- [detection.md](./references/detection.md) β framework/port/health detection
- [safeguards.md](./references/safeguards.md) β DS001-DS013 checklist
- [workload-identity.md](./references/workload-identity.md) β Workload Identity setup
- [rollback.md](./references/rollback.md) β recovery procedures
- [base-images.md](./references/base-images.md) β base image policy and `<LATEST_STABLE_*>` resolution
## Knowledge Packs
Load `knowledge-packs/frameworks/<framework>.md` per detected framework. Available: `spring-boot`, `express`, `nextjs`, `fastapi`, `django`, `nestjs`, `aspnet-core`, `go`, `flask`
## Templates
`templates/` (dockerfiles/, k8s/, github-actions/, mermaid/).
aspnet-core.md 7.2 KB
# ASP.NET Core Knowledge Pack
> **Applies to:** Projects detected with `*.csproj` containing `Microsoft.NET.Sdk.Web` or referencing `Microsoft.AspNetCore.*` packages
## Quick Reference
| Property | Value |
|----------|-------|
| Signal files | `*.csproj` with `Microsoft.NET.Sdk.Web` or `Microsoft.AspNetCore.*` |
| Default port | `8080` (.NET 8+) |
| Health path | `/healthz` + `/ready` |
| Base template | `templates/dockerfiles/dotnet.Dockerfile` (+ `references/base-images.md`) |
---
## Health Endpoints
ASP.NET Core has built-in health check middleware via `Microsoft.Extensions.Diagnostics.HealthChecks`:
| Endpoint | Purpose | Probe Type |
|----------|---------|-----------|
| `/healthz` | Overall health | `livenessProbe` |
| `/ready` | Dependency readiness | `readinessProbe` |
### Required configuration
In `Program.cs`:
```csharp
var builder = WebApplication.CreateBuilder(args);
// Register health checks
builder.Services.AddHealthChecks()
.AddNpgSql(builder.Configuration.GetConnectionString("DefaultConnection")!,
name: "postgresql",
tags: new[] { "ready" });
var app = builder.Build();
// Map health endpoints
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
Predicate = _ => false // No dependency checks for liveness
});
app.MapHealthChecks("/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
```
The `AspNetCore.HealthChecks.NpgSql` NuGet package provides the PostgreSQL health check. Install with:
```bash
dotnet add package AspNetCore.HealthChecks.NpgSql
```
### Probe configuration in Deployment manifest
```yaml
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
```
**Note:** ASP.NET Core apps start significantly faster than JVM-based frameworks β `initialDelaySeconds: 5` is typically sufficient.
---
## Database Profiles
ASP.NET Core uses configuration providers and Entity Framework Core for database access:
| Configuration Source | Activation | Typical Usage |
|---------------------|------------|---------------|
| `appsettings.json` | Default | Local dev with SQLite or LocalDB |
| `appsettings.Production.json` | `ASPNETCORE_ENVIRONMENT=Production` | Production connection strings |
| Environment variables | Always override file config | AKS deployments |
### Environment variables for PostgreSQL on AKS
```yaml
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
- name: ConnectionStrings__DefaultConnection
value: "Host={{PG_SERVER_NAME}}.postgres.database.azure.com;Database={{DB_NAME}};Username={{IDENTITY_NAME}};Ssl Mode=Require"
```
The double-underscore (`__`) in `ConnectionStrings__DefaultConnection` maps to the `:` separator in .NET configuration β `ConnectionStrings:DefaultConnection`.
### Workload Identity with Azure.Identity
See `references/workload-identity.md` for connection patterns. Requires `Azure.Identity` and `Npgsql.EntityFrameworkCore.PostgreSQL` packages.
### ConfigMap pattern
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{APP_NAME}}-config
data:
ASPNETCORE_ENVIRONMENT: "Production"
ConnectionStrings__DefaultConnection: "Host={{PG_SERVER_NAME}}.postgres.database.azure.com;Database={{DB_NAME}};Ssl Mode=Require"
DOTNET_EnableDiagnostics: "0"
DOTNET_RUNNING_IN_CONTAINER: "true"
```
---
## Writable Paths (DS012 Compliance)
When `readOnlyRootFilesystem: true` is set, ASP.NET Core needs `/tmp` writable:
- **Data Protection keys** are written to a local directory by default for key persistence
- **Temporary files** from multipart uploads and response buffering use `/tmp`
- **Entity Framework** compiled models may write to temp directories
### Required volume mount
```yaml
volumes:
- name: tmp
emptyDir: {}
containers:
- name: app
volumeMounts:
- name: tmp
mountPath: /tmp
```
### Data Protection key persistence
By default, ASP.NET Core Data Protection stores encryption keys in-memory when no persistent path is available, meaning keys are lost on pod restart. This breaks authentication cookies and anti-forgery tokens across pod restarts or in multi-replica deployments.
For production, persist keys to Azure Blob Storage:
```csharp
builder.Services.AddDataProtection()
.PersistKeysToAzureBlobStorage("<connection-string>", "<container>", "<blob-name>")
.ProtectKeysWithAzureKeyVault(new Uri("<key-vault-uri>"), new DefaultAzureCredential());
```
Alternatively, mount a PVC at a known path and configure:
```csharp
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/keys"));
```
---
## Resource Sizing
ASP.NET Core on the .NET runtime is efficient but needs moderate memory for the CLR.
| Resource | Request | Limit |
|----------|---------|-------|
| CPU | 200m | 500m |
| Memory | 256Mi | 512Mi |
---
## Port Configuration
- **Default port:** 8080 (since .NET 8; previously 80 in .NET 7 and earlier)
- **Env var override:** `ASPNETCORE_URLS=http://+:8080` or `ASPNETCORE_HTTP_PORTS=8080`
- **Code override:** `builder.WebHost.UseUrls("http://+:8080")` in `Program.cs`
The port change from 80 to 8080 in .NET 8 aligns with non-root container best practices β port 80 requires elevated privileges. Set `DOTNET_EnableDiagnostics=0` to disable diagnostic pipes that require writable paths not available in read-only filesystems.
---
## Build Commands
| Variant | Build Command | Output |
|---------|---------------|--------|
| Framework-dependent | `dotnet publish -c Release -o ./publish` | `./publish/<app-name>.dll` β requires .NET runtime on target |
| Self-contained | `dotnet publish -c Release --self-contained -o ./publish` | `./publish/<app-name>` β includes .NET runtime |
| Single-file | `dotnet publish -c Release --self-contained -p:PublishSingleFile=true -o ./publish` | Single executable binary |
The `-c Release` flag enables compiler optimizations and disables debug symbols β always use it for production builds.
---
## EF Core Migrations
Run `dotnet ef database update` as an init container β never in the Dockerfile build stage (no database access) and never in the entrypoint (race condition when multiple replicas start simultaneously).
---
## Common Issues on AKS
| Issue | Symptom | Fix |
|-------|---------|-----|
| Kestrel bound to port 80 | `CrashLoopBackOff` β permission denied binding to port 80 as non-root | Set `ASPNETCORE_HTTP_PORTS=8080` or upgrade to .NET 8+ which defaults to 8080 |
| Data Protection keys lost on restart | Users logged out after pod restart, anti-forgery token validation failures | Persist keys to Azure Blob Storage or a PVC β do not rely on in-memory default |
| EF Core migrations not applied | `NpgsqlException: relation "..." does not exist` | Run `dotnet ef database update` as an init container or at startup with `Database.Migrate()` |
| Image too large (>500MB) | Slow pulls, high ACR storage | Use self-contained + trimmed publish with the runtime-deps Alpine base image |
| HTTPS redirect loop behind gateway | Infinite 307/308 redirects, `ERR_TOO_MANY_REDIRECTS` | Disable HTTPS redirection in `Program.cs` when behind a TLS-terminating gateway β configure `ForwardedHeaders` middleware instead |
django.md 6.0 KB
# Django Knowledge Pack
> **Applies to:** Projects detected with `requirements.txt`, `pyproject.toml`, or `Pipfile` containing `django`, or presence of `manage.py`
## Quick Reference
| Property | Value |
|----------|-------|
| Signal files | `requirements.txt`/`pyproject.toml`/`Pipfile` containing `django`, or `manage.py` |
| Default port | `8000` (gunicorn) |
| Health path | `/health/` (django-health-check) |
| Base template | `templates/dockerfiles/python.Dockerfile` (+ `references/base-images.md`) |
---
## Health Endpoints
Django does not provide health endpoints out of the box. Use the `django-health-check` package:
### Installation
```bash
pip install django-health-check
```
### Configuration in `settings.py`
```python
INSTALLED_APPS = [
# ...existing apps...
"health_check",
"health_check.db",
"health_check.cache",
"health_check.storage",
"health_check.contrib.migrations",
]
```
### URL configuration in `urls.py`
```python
from django.urls import include, path
urlpatterns = [
# ...existing urls...
path("health/", include("health_check.urls")),
]
```
The `/health/` endpoint returns HTTP 200 when all checks pass and HTTP 500 with details when any check fails.
### Probe configuration in Deployment manifest
```yaml
livenessProbe:
httpGet:
path: /health/
port: 8000
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
```
**Note:** `initialDelaySeconds: 10` is sufficient for most Django apps.
---
## Database Profiles
Django does not have a built-in profile system like Spring Boot. Database configuration is driven by `settings.py` with environment variables:
| Pattern | How it works |
|---------|-------------|
| `dj-database-url` | Parse `DATABASE_URL` env var (recommended for 12-factor apps)
### Environment variables for PostgreSQL on AKS
```yaml
env:
- name: DATABASE_URL
value: "postgres://{{IDENTITY_NAME}}@{{PG_SERVER_NAME}}.postgres.database.azure.com:5432/{{DB_NAME}}?sslmode=require"
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: {{APP_NAME}}-secrets
key: secret-key
```
**Important:** `SECRET_KEY` must never be in a ConfigMap or hardcoded. Always store it in a Kubernetes Secret (or Key Vault via Workload Identity).
### ConfigMap pattern
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{APP_NAME}}-config
data:
DJANGO_SETTINGS_MODULE: "config.settings.production"
DJANGO_ALLOWED_HOSTS: "{{INGRESS_HOSTNAME}}"
DATABASE_URL: "postgres://{{IDENTITY_NAME}}@{{PG_SERVER_NAME}}.postgres.database.azure.com:5432/{{DB_NAME}}?sslmode=require"
```
---
## Writable Paths (DS012 Compliance)
When `readOnlyRootFilesystem: true` is set, Django apps need `/tmp` writable and optionally `/app/staticfiles`:
- **`/tmp`** β required for file uploads (`FILE_UPLOAD_TEMP_DIR` defaults to `/tmp`), session data when using file-based sessions, and temporary processing
- **`/app/staticfiles`** β optional, only needed if serving collected static files at runtime from the local filesystem (when not using WhiteNoise or a CDN)
### Volume mount configuration
```yaml
volumes:
- name: tmp
emptyDir: {}
- name: staticfiles
emptyDir: {}
containers:
- name: app
volumeMounts:
- name: tmp
mountPath: /tmp
- name: staticfiles
mountPath: /app/staticfiles
```
If static files are baked into the image at build time via `collectstatic` and served by WhiteNoise, the `staticfiles` volume can be omitted β only `/tmp` is required.
---
## Resource Sizing
Django with Gunicorn runs multiple worker processes. Size for the number of workers (default: 2-4).
| Resource | Request | Limit |
|----------|---------|-------|
| CPU | 200m | 500m |
| Memory | 256Mi | 512Mi |
---
## Port Configuration
- **Default port:** 8000
- **CLI flag:** `--bind 0.0.0.0:8000` passed to `gunicorn`
- **Env var override:** `PORT` (read via `gunicorn --bind 0.0.0.0:$PORT` or `int(os.environ.get("PORT", 8000))`)
- **Workers formula:** `2 * CPU_CORES + 1` (e.g. `--workers 3` for a 1-vCPU container)
- **WSGI module path** varies by project scaffold: `config.wsgi:application`, `myproject.wsgi:application`, or `app.wsgi:application` β check `wsgi.py` location
Gunicorn logs the port on startup: `Listening at: http://0.0.0.0:8000`
---
## Build Commands
| Command | Purpose | When to run |
|---------|---------|-------------|
| `python manage.py collectstatic --noinput` | Gathers static files into `STATIC_ROOT` | In Dockerfile build stage (with `SECRET_KEY=build-placeholder`) |
| `python manage.py migrate --noinput` | Applies database migrations | As a Kubernetes init container β **never in the Dockerfile** |
**Important:** Database migrations must run as an init container, not during the Docker build. The build stage has no access to the production database, and running migrations in the entrypoint creates race conditions when multiple replicas start simultaneously.
### Init container for migrations
```yaml
initContainers:
- name: migrate
image: {{ACR_NAME}}.azurecr.io/{{APP_NAME}}:{{TAG}}
command: ["python", "manage.py", "migrate", "--noinput"]
envFrom:
- configMapRef:
name: {{APP_NAME}}-config
- secretRef:
name: {{APP_NAME}}-secrets
```
---
## Common Issues on AKS
| Issue | Symptom | Fix |
|-------|---------|-----|
| `collectstatic` not run | Static files 404 | Run `python manage.py collectstatic --noinput` in Dockerfile build stage |
| `ALLOWED_HOSTS` not set | `DisallowedHost` error | Set `DJANGO_ALLOWED_HOSTS` env var |
| Dev server in production | Single-threaded, no security | Use `gunicorn` in ENTRYPOINT |
| Migrations not applied | `relation "..." does not exist` | Run `manage.py migrate` as init container |
| `SECRET_KEY` not set | `ImproperlyConfigured` error | Store in Kubernetes Secret |
| Static files 404 in production | CSS/JS/images not loading | Use WhiteNoise or CDN for static files
express.md 6.6 KB
# Express / Fastify Knowledge Pack
> **Applies to:** Projects detected with `package.json` containing `express` or `fastify` as a dependency
## Quick Reference
| Property | Value |
|----------|-------|
| Signal files | `package.json` containing `express` or `fastify` |
| Default port | `3000` |
| Health path | `/healthz` |
| Base template | `templates/dockerfiles/node.Dockerfile` (+ `references/base-images.md`) |
---
## Signal Handling
Node.js does not handle `SIGTERM` correctly when running as PID 1. The base template includes `dumb-init` as the entrypoint to forward signals properly; no application-level changes are needed unless the app registers explicit cleanup handlers.
### Fastify listen caveat
Fastify defaults to listening on `127.0.0.1`, which is unreachable from outside the container. Bind to `0.0.0.0` explicitly:
```js
await fastify.listen({ port: 3000, host: '0.0.0.0' });
```
If the pod starts but health probes fail with `connection refused`, this is almost always the cause. Express already binds to `0.0.0.0` by default β no change needed for Express apps.
### Package manager variants
| Package Manager | Install (all) | Install (prod only) | Lock File |
|----------------|---------------|---------------------|-----------|
| npm | `npm ci` | `npm ci --omit=dev` | `package-lock.json` |
| yarn | `yarn install --frozen-lockfile` | `yarn install --frozen-lockfile --production` | `yarn.lock` |
| pnpm | `pnpm install --frozen-lockfile` | `pnpm install --frozen-lockfile --prod` | `pnpm-lock.yaml` |
Copy the correct lock file in the Dockerfile `COPY` step to match the project's package manager.
---
## Health Endpoints
Node.js frameworks do not provide health endpoints out of the box. Add a `/healthz` route manually.
### Express
```js
app.get('/healthz', (req, res) => {
res.status(200).json({ status: 'UP' });
});
```
### Fastify
```js
fastify.get('/healthz', async () => {
return { status: 'UP' };
});
```
For richer checks (database connectivity, downstream services), extend the handler to verify dependencies and return `503` when unhealthy.
### Probe configuration in Deployment manifest
```yaml
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 5
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
```
**Note:** Node.js apps start in under a second, so `initialDelaySeconds: 5` is sufficient. No `startupProbe` is needed unless the app performs heavy initialization (e.g., loading ML models).
---
## Database Profiles
Node.js projects use a variety of database libraries. The standard pattern is a `DATABASE_URL` connection string injected via environment variable:
| Library | Connection Pattern | Config Property |
|---------|-------------------|-----------------|
| `pg` (node-postgres) | `new Pool({ connectionString: process.env.DATABASE_URL })` | `DATABASE_URL` |
| Prisma | `datasource db { url = env("DATABASE_URL") }` in `schema.prisma` | `DATABASE_URL` |
| Sequelize | `new Sequelize(process.env.DATABASE_URL)` | `DATABASE_URL` |
| Knex | `connection: process.env.DATABASE_URL` in `knexfile.js` | `DATABASE_URL` |
### Environment variables for PostgreSQL on AKS
```yaml
env:
- name: DATABASE_URL
value: "postgresql://{{IDENTITY_NAME}}@{{PG_SERVER_NAME}}.postgres.database.azure.com:5432/{{DB_NAME}}?sslmode=require"
- name: PGHOST
value: "{{PG_SERVER_NAME}}.postgres.database.azure.com"
- name: PGDATABASE
value: "{{DB_NAME}}"
- name: PGUSER
value: "{{IDENTITY_NAME}}"
- name: PGPORT
value: "5432"
- name: PGSSLMODE
value: "require"
```
For Workload Identity with passwordless authentication, use the `@azure/identity` package with `pg` to obtain Azure AD tokens instead of passwords.
---
## Writable Paths (DS012 Compliance)
When `readOnlyRootFilesystem: true` is set, Node.js apps need only `/tmp` writable:
- **Multipart uploads** (e.g., `multer`, `@fastify/multipart`) stage files to `/tmp`
- **Logging libraries** that buffer to disk use `/tmp`
### Required volume mount
```yaml
volumes:
- name: tmp
emptyDir: {}
containers:
- name: app
volumeMounts:
- name: tmp
mountPath: /tmp
```
---
## Resource Sizing
Node.js is single-threaded and relatively lightweight. These are starting-point defaults β tune based on observed usage.
| Resource | Request | Limit |
|----------|---------|-------|
| CPU | 100m | 500m |
| Memory | 128Mi | 256Mi |
For memory-intensive workloads (large payloads, SSR), increase the memory limit and set `--max-old-space-size` to ~75% of the limit.
---
## Port Configuration
- **Default port:** 3000
- **Env var override:** `PORT=3000`
- **Code pattern:** `app.listen(process.env.PORT || 3000)`
Express binds to `0.0.0.0` by default, so it is reachable from outside the container without additional configuration.
Fastify binds to `127.0.0.1` by default β **you must pass `host: '0.0.0.0'`** in the `listen()` call or the pod will start but all probes and traffic will fail with `connection refused`.
---
## Build Commands
| Scenario | Build Command | Output | Entrypoint |
|----------|---------------|--------|------------|
| TypeScript | `npm run build` (invokes `tsc`) | `dist/` | `node dist/index.js` |
| JavaScript (no build) | None | `src/` | `node src/index.js` |
| Bundler (esbuild/webpack) | `npm run build` | `dist/bundle.js` | `node dist/bundle.js` |
For TypeScript projects, ensure `tsconfig.json` has `"outDir": "dist"` and the Dockerfile copies the `dist/` folder to the runtime stage. Do **not** install `typescript` or `ts-node` in the production image.
---
## Common Issues on AKS
| Issue | Symptom | Fix |
|-------|---------|-----|
| No SIGTERM handling | Pod takes 30s to terminate (killed by `SIGKILL` after grace period) | Use `dumb-init` as entrypoint, or add explicit `process.on('SIGTERM', ...)` handler to close the server gracefully |
| ECONNRESET on PostgreSQL | `Error: Connection terminated unexpectedly` | Configure pool `idleTimeoutMillis` and `connectionTimeoutMillis`; Azure PG Flexible Server closes idle connections after ~5 min |
| Fastify localhost binding | Health probes fail with `connection refused` despite app running | Pass `host: '0.0.0.0'` to `fastify.listen()` β Fastify defaults to `127.0.0.1` |
| node_modules bloat | Image > 500MB, slow pulls from ACR | Run `npm ci --omit=dev` in a separate stage; consider esbuild bundling for single-file output |
| Memory leak under load | Pod `OOMKilled` after hours of traffic | Set `--max-old-space-size` to ~75% of container memory limit (e.g., `--max-old-space-size=384` for 512Mi limit); profile with `--inspect` locally |
fastapi.md 5.8 KB
# FastAPI Knowledge Pack
> **Applies to:** Projects detected with `requirements.txt`, `pyproject.toml`, or `Pipfile` containing `fastapi`
## Quick Reference
| Property | Value |
|----------|-------|
| Signal files | `requirements.txt`/`pyproject.toml`/`Pipfile` containing `fastapi` |
| Default port | `8000` |
| Health path | `/health` + `/ready` |
| Base template | `templates/dockerfiles/python.Dockerfile` (+ `references/base-images.md`) |
---
## Health Endpoints
FastAPI health endpoints must be defined explicitly in application code:
### Minimal health route
```python
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
async def health():
return {"status": "ok"}
```
### Readiness route with database check
```python
from fastapi import FastAPI, status
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
@app.get("/ready")
async def ready(db: AsyncSession = Depends(get_db)):
try:
await db.execute(text("SELECT 1"))
return {"status": "ready"}
except Exception:
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={"status": "not ready"},
)
```
### Probe configuration in Deployment manifest
```yaml
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
```
**Note:** FastAPI apps start quickly (typically <2s), so `initialDelaySeconds: 5` is sufficient β much lower than JVM-based frameworks.
---
## Database Profiles
FastAPI does not have a built-in profile system. Database configuration is typically driven by environment variables:
| ORM / Driver | Package(s) | Connection String Format |
|-------------|-----------|--------------------------|
| SQLAlchemy async + asyncpg | `sqlalchemy[asyncio]`, `asyncpg` | `postgresql+asyncpg://user:pass@host:5432/db` |
| Tortoise ORM | `tortoise-orm`, `asyncpg` | `postgres://user:pass@host:5432/db` |
| SQLModel | `sqlmodel`, `asyncpg` | `postgresql+asyncpg://user:pass@host:5432/db` |
| asyncpg direct | `asyncpg` | `postgresql://user:pass@host:5432/db` |
**Important:** SQLAlchemy async requires the `+asyncpg` suffix in the connection URL scheme (`postgresql+asyncpg://`). Omitting it will default to the synchronous `psycopg2` driver, which blocks the event loop.
### Environment variables for PostgreSQL on AKS
```yaml
env:
- name: DATABASE_URL
value: "postgresql+asyncpg://{{IDENTITY_NAME}}@{{PG_SERVER_NAME}}.postgres.database.azure.com:5432/{{DB_NAME}}?sslmode=require"
```
### ConfigMap pattern
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{APP_NAME}}-config
data:
DATABASE_URL: "postgresql+asyncpg://{{IDENTITY_NAME}}@{{PG_SERVER_NAME}}.postgres.database.azure.com:5432/{{DB_NAME}}?sslmode=require"
UVICORN_WORKERS: "1"
```
### Workload Identity with azure-identity
See `references/workload-identity.md` for connection patterns. Requires `azure-identity` package.
---
## Writable Paths (DS012 Compliance)
When `readOnlyRootFilesystem: true` is set, FastAPI apps typically only need `/tmp` writable:
- **Uploaded files** use `/tmp` as the default staging directory for `UploadFile`
- **Temporary processing** may write intermediate results to `/tmp`
### Required volume mount
```yaml
volumes:
- name: tmp
emptyDir: {}
containers:
- name: app
volumeMounts:
- name: tmp
mountPath: /tmp
```
No other writable paths are typically needed for production FastAPI apps.
---
## Resource Sizing
FastAPI with Uvicorn is async and lightweight. Size for workload concurrency.
| Resource | Request | Limit |
|----------|---------|-------|
| CPU | 100m | 500m |
| Memory | 128Mi | 256Mi |
---
## Port Configuration
- **Default port:** 8000
- **CLI flag:** `--host 0.0.0.0 --port 8000` passed to `uvicorn` β must include `--host 0.0.0.0` (uvicorn defaults to `127.0.0.1`, unreachable from Kubernetes probes)
- **Workers:** use `--workers 1` for pure-async apps (async code uses a single event loop); `2 * CPU_CORES + 1` only for sync/blocking handlers
- **Env var override:** `PORT` (read via `uvicorn --port $PORT` or `int(os.environ.get("PORT", 8000))`)
Uvicorn logs the port on startup: `Uvicorn running on http://0.0.0.0:8000`
---
## Build Commands
| Tool | Install Command | Output |
|------|----------------|--------|
| pip | `pip install --no-cache-dir -r requirements.txt` | Packages in site-packages |
| Poetry | `poetry install --only main --no-interaction` | Packages in virtualenv |
| uv | `uv sync --frozen --no-dev` | Packages in virtualenv |
The `--no-cache-dir` flag (pip) and `--no-interaction` flag (Poetry) suppress interactive prompts β important for CI/CD and Docker builds.
---
## Common Issues on AKS
| Issue | Symptom | Fix |
|-------|---------|-----|
| Uvicorn workers misconfigured | High latency under load, single-core CPU usage | Set `--workers` to `2 * CPU_CORES + 1` for sync code, or `1` when using async handlers (async code uses a single event loop) |
| Async DB pool exhaustion | `asyncpg.exceptions.TooManyConnectionsError` | Configure pool size with `create_async_engine(pool_size=5, max_overflow=10)` and match PostgreSQL `max_connections` |
| Alpine build fails | `gcc` errors installing `cryptography`, `psycopg2`, `numpy` | Use the Debian-slim Python base (see `references/base-images.md`) instead of Alpine |
| Uvicorn binds to localhost | Connection refused from Kubernetes probes | Set `--host 0.0.0.0` β uvicorn defaults to `127.0.0.1` which is unreachable from outside the container |
| Missing uvicorn in production | `ModuleNotFoundError: No module named 'uvicorn'` | Ensure `uvicorn[standard]` is in `requirements.txt` β it is often only in dev dependencies |
flask.md 6.2 KB
# Flask Knowledge Pack
> **Applies to:** Projects detected with `requirements.txt`, `pyproject.toml`, or `Pipfile` containing `flask`
## Quick Reference
| Property | Value |
|----------|-------|
| Signal files | `requirements.txt`/`pyproject.toml`/`Pipfile` containing `flask` |
| Default port | `8000` prod (`5000` dev β never in prod) |
| Health path | `/health` + `/ready` |
| Base template | `templates/dockerfiles/python.Dockerfile` (+ `references/base-images.md`) |
---
## Health Endpoints
Flask does not include health check endpoints β they must be defined explicitly in application code:
### Minimal health route
```python
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/health")
def health():
return jsonify(status="ok"), 200
```
### Readiness route with database check
```python
from flask import jsonify
from sqlalchemy import text
@app.route("/ready")
def ready():
try:
db.session.execute(text("SELECT 1"))
return jsonify(status="ready"), 200
except Exception:
return jsonify(status="not ready"), 503
```
### Probe configuration in Deployment manifest
```yaml
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
```
**Note:** Flask apps behind gunicorn start quickly (typically <3s), so `initialDelaySeconds: 5` is sufficient β much lower than JVM-based frameworks.
---
## Database Profiles
Flask does not have a built-in profile system. Database configuration is typically driven by environment variables:
| ORM / Driver | Package(s) | Connection String Env Var |
|-------------|-----------|--------------------------|
| Flask-SQLAlchemy | `flask-sqlalchemy`, `psycopg2-binary` | `SQLALCHEMY_DATABASE_URI` |
| SQLAlchemy direct | `sqlalchemy`, `psycopg2-binary` | `DATABASE_URL` |
| psycopg2 direct | `psycopg2-binary` | `DATABASE_URL` |
**Important:** Flask-SQLAlchemy reads the connection string from `app.config["SQLALCHEMY_DATABASE_URI"]`, which is typically set via `os.environ.get("SQLALCHEMY_DATABASE_URI")` or `os.environ.get("DATABASE_URL")`. Ensure the env var name matches what the app expects.
### Environment variables for PostgreSQL on AKS
```yaml
env:
- name: SQLALCHEMY_DATABASE_URI
value: "postgresql://{{IDENTITY_NAME}}@{{PG_SERVER_NAME}}.postgres.database.azure.com:5432/{{DB_NAME}}?sslmode=require"
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: {{APP_NAME}}-secrets
key: secret-key
```
### Secret for SECRET_KEY
Flask requires `SECRET_KEY` for session signing, CSRF tokens, and any use of `flask.session`. Never hardcode it β store it in a Kubernetes Secret:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: {{APP_NAME}}-secrets
type: Opaque
stringData:
secret-key: "<generate-a-random-string>"
```
### ConfigMap pattern
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{APP_NAME}}-config
data:
SQLALCHEMY_DATABASE_URI: "postgresql://{{IDENTITY_NAME}}@{{PG_SERVER_NAME}}.postgres.database.azure.com:5432/{{DB_NAME}}?sslmode=require"
```
### Workload Identity with azure-identity
See `references/workload-identity.md` for connection patterns. Requires `azure-identity` package.
---
## Writable Paths (DS012 Compliance)
When `readOnlyRootFilesystem: true` is set, Flask apps typically only need `/tmp` writable:
- **Uploaded files** use `/tmp` as the default staging directory for `request.files`
- **Temporary processing** may write intermediate results to `/tmp`
### Required volume mount
```yaml
volumes:
- name: tmp
emptyDir: {}
containers:
- name: app
volumeMounts:
- name: tmp
mountPath: /tmp
```
No other writable paths are typically needed for production Flask apps.
---
## Resource Sizing
Flask with Gunicorn runs multiple worker processes. Size for the number of workers (default: 2-4).
| Resource | Request | Limit |
|----------|---------|-------|
| CPU | 150m | 500m |
| Memory | 128Mi | 256Mi |
---
## Port Configuration
- **Development port:** 5000 (`flask run` default β do not use in production)
- **Production port:** 8000 (gunicorn convention)
- **CLI flag:** `--bind 0.0.0.0:8000` passed to `gunicorn`
- **Env var override:** `PORT` (read via `gunicorn --bind 0.0.0.0:$PORT` or in app code `int(os.environ.get("PORT", 8000))`)
- **Workers formula:** `2 * CPU_CORES + 1` β override at runtime via `WEB_CONCURRENCY` env var
- **Entry point variants:** `gunicorn "app:app"` (module-level) or `gunicorn "myapp:create_app()"` (application factory)
Gunicorn logs the port on startup: `Listening at: http://0.0.0.0:8000`
---
## Build Commands
| Tool | Install Command |
|------|----------------|
| pip | `pip install --no-cache-dir -r requirements.txt` |
| Poetry | `poetry install --only main --no-interaction` |
Ensure `gunicorn` is listed in `requirements.txt` or `pyproject.toml` production dependencies.
---
## Common Issues on AKS
| Issue | Symptom | Fix |
|-------|---------|-----|
| Running dev server in production | Single-threaded, poor performance, `WARNING: This is a development server` in logs | Use `gunicorn` as the ENTRYPOINT β never use `flask run` or `app.run()` in production containers |
| `SECRET_KEY` not set | `RuntimeError: The session is unavailable because no secret key was set`, CSRF failures | Set `SECRET_KEY` via a Kubernetes Secret and reference it as an env var in the Deployment manifest |
| Flask binds to localhost | Connection refused from Kubernetes probes | Pass `--bind 0.0.0.0:8000` to gunicorn β the Flask dev server defaults to `127.0.0.1` which is unreachable from outside the container |
| Gunicorn not installed | `ModuleNotFoundError: No module named 'gunicorn'` | Ensure `gunicorn` is in `requirements.txt` or `pyproject.toml` main dependencies β it is often only in dev dependencies or missing entirely |
| DB connections not closed | `sqlalchemy.exc.TimeoutError: QueuePool limit`, PostgreSQL `max_connections` exhaustion | Configure pool size with `SQLALCHEMY_ENGINE_OPTIONS = {"pool_size": 5, "max_overflow": 10, "pool_recycle": 300}` and match PostgreSQL `max_connections` |
go.md 6.8 KB
# Go Knowledge Pack
> **Applies to:** Projects detected with `go.mod` containing `github.com/gin-gonic/gin`, `github.com/labstack/echo`, `github.com/gofiber/fiber`, or any Go project using the standard library `net/http` for HTTP serving
## Quick Reference
| Property | Value |
|----------|-------|
| Signal files | `go.mod` (gin/echo/fiber or stdlib `net/http`) |
| Default port | `8080` |
| Health path | `/healthz` + `/ready` |
| Base template | `templates/dockerfiles/go.Dockerfile` (+ `references/base-images.md`) |
---
## Build Flags
Two flags are required for a correct production build:
- **`CGO_ENABLED=0`** produces a fully static binary with no libc dependency β required when targeting the distroless static image. If CGO is needed (e.g., for sqlite3 or cgo bindings), use the distroless cc image instead.
- **`-ldflags="-s -w"`** strips debug symbols and DWARF info, reducing binary size by ~30%.
---
## Health Endpoints
Go does not provide health check endpoints out of the box β you must implement them manually. Example using standard library:
```go
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
})
http.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
if err := db.Ping(); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte(`{"status":"not ready"}`))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ready"}`))
})
```
### Probe configuration in Deployment manifest
```yaml
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 3
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 3
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
```
**Note:** Go binaries start in milliseconds β `initialDelaySeconds: 3` is generous. No JVM warmup or interpreter startup to wait for.
---
## Graceful Shutdown
Implement `signal.NotifyContext` with `srv.Shutdown(ctx)` to allow in-flight requests to complete before the pod exits during a rolling update. Without this, connections are dropped and callers receive 502 errors.
---
## Database Profiles
Go does not have a built-in profile system. Database configuration is typically driven by environment variables:
| Library | Driver | Connection Env Var |
|---------|--------|--------------------|
| `database/sql` + `pgx` | `github.com/jackc/pgx/v5/stdlib` | `DATABASE_URL` |
| GORM | `gorm.io/driver/postgres` | `DATABASE_URL` |
| sqlx | `github.com/jmoiron/sqlx` + `pgx` | `DATABASE_URL` |
| pgx direct | `github.com/jackc/pgx/v5` | `DATABASE_URL` |
### Environment variables for PostgreSQL on AKS
```yaml
env:
- name: DATABASE_URL
value: "host={{PG_SERVER_NAME}}.postgres.database.azure.com port=5432 dbname={{DB_NAME}} user={{IDENTITY_NAME}} sslmode=require"
```
### Workload Identity with pgx
Use `azidentity` to obtain Azure AD tokens and inject them via pgx's `BeforeConnect` hook β no password stored:
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/jackc/pgx/v5"
)
cred, _ := azidentity.NewDefaultAzureCredential(nil)
config, _ := pgx.ParseConfig(os.Getenv("DATABASE_URL"))
config.BeforeConnect = func(ctx context.Context, cfg *pgx.ConnConfig) error {
token, err := cred.GetToken(ctx, policy.TokenRequestOptions{
Scopes: []string{"https://ossrdbms-aad.database.windows.net/.default"},
})
if err != nil {
return err
}
cfg.Password = token.Token
return nil
}
```
### ConfigMap pattern
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{APP_NAME}}-config
data:
DATABASE_URL: "host={{PG_SERVER_NAME}}.postgres.database.azure.com port=5432 dbname={{DB_NAME}} user={{IDENTITY_NAME}} sslmode=require"
```
---
## Writable Paths (DS012 Compliance)
When `readOnlyRootFilesystem: true` is set, Go apps typically need **no writable paths**:
- Go compiles to a static binary β no temp files, no interpreted bytecode, no session storage
- The distroless static base image has no shell or package manager that writes to disk
### Optional `/tmp` mount
If your application explicitly writes temporary files (e.g., file uploads, report generation):
```yaml
volumes:
- name: tmp
emptyDir: {}
containers:
- name: app
volumeMounts:
- name: tmp
mountPath: /tmp
```
Most Go web APIs do not need this.
---
## Resource Sizing
Go compiles to a static binary with no runtime β it is the most resource-efficient option.
| Resource | Request | Limit |
|----------|---------|-------|
| CPU | 50m | 200m |
| Memory | 64Mi | 128Mi |
---
## Port Configuration
- **Default port:** 8080 (Go convention, not enforced by any framework)
- **Env var override:** `PORT` (commonly used pattern)
- **Bind port >= 1024** β lower ports require elevated privileges; running as non-root (uid 65534) means port 80 or 443 will fail with `permission denied`.
### Code pattern
```go
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Listening on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, router))
```
All major Go frameworks (Gin, Echo, Fiber) accept the listen address as a string β no special configuration property needed.
---
## Build Commands
| Variant | Command | Notes |
|---------|---------|-------|
| Standard | `CGO_ENABLED=0 go build -ldflags="-s -w" -o server ./cmd/server` | Production binary, stripped |
| Race detector (test only) | `go build -race -o server ./cmd/server` | Do **not** use in production β 10x overhead |
| Multiple binaries | `CGO_ENABLED=0 go build -ldflags="-s -w" -o migrate ./cmd/migrate` | Build each binary target separately |
The `./cmd/server` path is conventional for Go projects using the [Standard Go Project Layout](https://github.com/golang-standards/project-layout). Adjust to match the actual `main` package location.
---
## Common Issues on AKS
| Issue | Symptom | Fix |
|-------|---------|-----|
| Binary not statically linked | `exec format error` or `not found` in distroless | Ensure `CGO_ENABLED=0` is set during build; if CGO is required, use the distroless cc image instead of the distroless static image |
| DNS resolution issues during build | `dial tcp: lookup ... no such host` | Add `ca-certificates` to the build stage or use a Debian-based build image |
| Graceful shutdown not implemented | Connections dropped during rolling update, 502 errors | Implement `signal.NotifyContext` with `srv.Shutdown(ctx)` β give in-flight requests time to complete before exit |
| Binary name mismatch | `exec /server: no such file or directory` | Verify the `-o` flag in `go build` matches the `ENTRYPOINT` path in the Dockerfile |
| Port < 1024 with non-root user | `bind: permission denied` | Use port 8080 (or any port >= 1024); never bind to 80 or 443 inside the container |
nestjs.md 6.7 KB
# NestJS Knowledge Pack
> **Applies to:** Projects detected with `package.json` containing `@nestjs/core` as a dependency
## Quick Reference
| Property | Value |
|----------|-------|
| Signal files | `package.json` containing `@nestjs/core` |
| Default port | `3000` |
| Health path | `/health` |
| Base template | `templates/dockerfiles/node.Dockerfile` (+ `references/base-images.md`) |
---
## Signal Handling
NestJS lifecycle events (`OnModuleDestroy`, `BeforeApplicationShutdown`) fire only when shutdown hooks are enabled. Call `app.enableShutdownHooks()` in `main.ts` so `SIGTERM` from Kubernetes triggers graceful teardown of HTTP connections, database pools, and message queue consumers:
```typescript
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();
await app.listen(process.env.PORT || 3000);
```
The base template uses `dumb-init` as the entrypoint to forward `SIGTERM` to the Node process when running as PID 1. Both are required: `dumb-init` routes the signal, `enableShutdownHooks()` handles it.
---
## Health Endpoints
NestJS provides health checks via the `@nestjs/terminus` package.
### Installation
```bash
npm install @nestjs/terminus
```
### HealthModule
```typescript
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './health.controller';
@Module({
imports: [TerminusModule],
controllers: [HealthController],
})
export class HealthModule {}
```
Register `HealthModule` in `AppModule` imports.
### HealthController with database check
```typescript
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(private health: HealthCheckService, private db: TypeOrmHealthIndicator) {}
@Get()
@HealthCheck()
check() {
return this.health.check([() => this.db.pingCheck('database')]);
}
}
```
For Prisma, use `PrismaHealthIndicator`; for MikroORM, use `MikroOrmHealthIndicator`. If no database is used, omit the indicator and return a simple status check.
### Probe configuration in Deployment manifest
```yaml
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
```
**Note:** NestJS apps start quickly (typically under 2 seconds), so `initialDelaySeconds: 5` is sufficient. If the app performs heavy initialization (e.g., loading large config, running migrations), increase to 10β15s or add a `startupProbe`.
---
## Database Profiles
NestJS supports multiple ORM libraries. The standard pattern is a connection string or individual env vars injected via environment variables:
| ORM | Connection Pattern | Config Property |
|-----|-------------------|-----------------|
| TypeORM | `TypeOrmModule.forRoot({ url: process.env.DATABASE_URL })` | `DATABASE_URL` |
| Prisma | `datasource db { url = env("DATABASE_URL") }` in `schema.prisma` | `DATABASE_URL` |
| MikroORM | `MikroOrmModule.forRoot({ clientUrl: process.env.DATABASE_URL })` | `DATABASE_URL` |
| Sequelize | `SequelizeModule.forRoot({ uri: process.env.DATABASE_URL })` | `DATABASE_URL` |
### Environment variables for PostgreSQL on AKS
```yaml
env:
- name: DATABASE_URL
value: "postgresql://{{IDENTITY_NAME}}@{{PG_SERVER_NAME}}.postgres.database.azure.com:5432/{{DB_NAME}}?sslmode=require"
```
For Workload Identity, see `references/workload-identity.md`.
---
## Writable Paths (DS012 Compliance)
When `readOnlyRootFilesystem: true` is set, NestJS apps need only `/tmp` writable:
- **Multipart uploads** (e.g., `@nestjs/platform-express` with `multer`) stage files to `/tmp`
- **Logging libraries** that buffer to disk use `/tmp`
- **`node_modules` and `dist/`** are read-only at runtime
### Required volume mount
```yaml
volumes:
- name: tmp
emptyDir: {}
containers:
- name: app
volumeMounts:
- name: tmp
mountPath: /tmp
```
---
## Resource Sizing
NestJS is Node.js-based and single-threaded. Similar to Express/Fastify.
| Resource | Request | Limit |
|----------|---------|-------|
| CPU | 100m | 500m |
| Memory | 128Mi | 256Mi |
---
## Port Configuration
- **Default port:** 3000
- **Env var override:** `PORT=3000`
- **Code pattern:** `await app.listen(process.env.PORT || 3000)` in `main.ts`
NestJS (via Express adapter) binds to `0.0.0.0` by default. For Fastify adapter, pass `'0.0.0.0'` explicitly: `await app.listen(process.env.PORT || 3000, '0.0.0.0')`.
---
## Build Commands
| Scenario | Build Command | Output | Entrypoint |
|----------|---------------|--------|------------|
| Standard | `npm run build` (invokes `nest build`) | `dist/` | `node dist/main.js` |
| Monorepo | `npx nest build <app-name>` | `dist/apps/<app-name>/` | `node dist/apps/<app-name>/main.js` |
| SWC compiler | `nest build --builder swc` | `dist/` | `node dist/main.js` |
The **SWC compiler** is ~20x faster than the default TypeScript compiler for large projects. Enable it by installing `@swc/cli @swc/core` and passing `--builder swc` or setting `"builder": "swc"` in `nest-cli.json`. SWC does not perform type checking β run `tsc --noEmit` separately in CI if type safety is required.
---
## Common Issues on AKS
| Issue | Symptom | Fix |
|-------|---------|-----|
| SIGTERM not handled | Pod takes 30s to terminate (killed by `SIGKILL` after grace period) | Call `app.enableShutdownHooks()` in `main.ts` so NestJS lifecycle events (`OnModuleDestroy`, `BeforeApplicationShutdown`) fire on `SIGTERM`; also use `dumb-init` as the container entrypoint |
| TypeORM connection pool exhaustion | `Error: Connection pool exhausted` or `ETIMEDOUT` under load | Set `extra: { max: 10 }` in TypeORM config to limit pool size; Azure PG Flexible Server has a connection limit based on SKU β monitor with `pg_stat_activity` |
| Circular dependency | `Error: Nest cannot create the ... instance` at startup | Use `forwardRef(() => Module)` in module imports; refactor shared logic into a dedicated module to break the cycle |
| dist/ not included in image | `Error: Cannot find module '/app/dist/main.js'` at container start | Ensure `COPY --from=build /app/dist ./dist` is present in the Dockerfile runtime stage; verify `nest build` runs successfully in the build stage |
| Global prefix breaks probes | Health probes return `404` after setting `app.setGlobalPrefix('api')` | The health endpoint moves to `/api/health` β update probe paths in the Deployment manifest, or exclude the health controller from the global prefix using `app.setGlobalPrefix('api', { exclude: ['health'] })` |
nextjs.md 7.1 KB
# Next.js Knowledge Pack
> **Applies to:** Projects detected with `package.json` containing `next` as a dependency
## Quick Reference
| Property | Value |
|----------|-------|
| Signal files | `package.json` containing `next` |
| Default port | `3000` |
| Health path | `/api/health` |
| Base template | `templates/dockerfiles/node.Dockerfile` (+ `references/base-images.md`) |
---
## Standalone Output (Required)
Next.js standalone output mode is critical for containerized deployments β it reduces the image from ~1GB to ~100MB by bundling only the files needed to run the server. Without it, the build copies all of `node_modules` into the image.
Enable it in `next.config.js` (or `next.config.mjs` / `next.config.ts`):
```js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
module.exports = nextConfig;
```
The build then produces `.next/standalone/server.js`, a self-contained HTTP server that does not require the `next` CLI at runtime.
### Three required COPY targets
The Dockerfile runtime stage needs exactly three items from the build stage:
1. `public/` β static assets served directly
2. `.next/standalone/` β the standalone server and its bundled dependencies
3. `.next/static/` β client-side JS/CSS bundles (must be copied into `.next/static` inside the standalone directory, not alongside it)
### Hostname binding
Set `HOSTNAME="0.0.0.0"` as a runtime environment variable (required for Next.js 14+). The standalone `server.js` reads this variable on startup to listen on all interfaces. Without it, the server binds to `127.0.0.1` and Kubernetes probes fail.
Set `NEXT_TELEMETRY_DISABLED=1` in both the build stage and the runtime stage to prevent outbound telemetry calls to `telemetry.nextjs.org` from the container.
---
## Health Endpoints
Next.js does not provide health endpoints out of the box. Add a custom API route β the implementation depends on whether the project uses App Router or Pages Router.
### App Router (Next.js 13.4+)
Create `app/api/health/route.ts`:
```ts
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ status: 'UP' });
}
export const dynamic = 'force-dynamic';
```
The `force-dynamic` export prevents Next.js from statically caching the health response at build time.
### Pages Router
Create `pages/api/health.ts`:
```ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ status: 'UP' });
}
```
### Probe configuration in Deployment manifest
```yaml
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
```
**Note:** Next.js standalone server starts in 1β3 seconds, but `initialDelaySeconds: 10` provides a safe margin for cold starts and environment variable resolution. No `startupProbe` is needed unless the app performs heavy server-side initialization.
---
## Database Profiles
Next.js apps commonly use Prisma, Drizzle, or `pg` (node-postgres) for database access. All follow the `DATABASE_URL` connection string pattern:
| Library | Connection Pattern | Config Property |
|---------|-------------------|-----------------|
| Prisma | `datasource db { url = env("DATABASE_URL") }` in `schema.prisma` | `DATABASE_URL` |
| Drizzle | `postgres(process.env.DATABASE_URL!)` or `drizzle(process.env.DATABASE_URL!)` | `DATABASE_URL` |
| `pg` (node-postgres) | `new Pool({ connectionString: process.env.DATABASE_URL })` | `DATABASE_URL` |
### Environment variables for PostgreSQL on AKS
```yaml
env:
- name: DATABASE_URL
value: "postgresql://{{IDENTITY_NAME}}@{{PG_SERVER_NAME}}.postgres.database.azure.com:5432/{{DB_NAME}}?sslmode=require"
```
For Workload Identity, see `references/workload-identity.md`.
---
## Writable Paths (DS012 Compliance)
When `readOnlyRootFilesystem: true` is set, Next.js needs **two** writable paths:
- **`/tmp`** β general-purpose temporary file storage
- **`/app/.next/cache`** β ISR (Incremental Static Regeneration) page cache and `next/image` optimization cache; without this, ISR and image optimization fail with `EROFS: read-only file system` errors
### Required volume mounts
```yaml
volumes:
- name: tmp
emptyDir: {}
- name: next-cache
emptyDir: {}
containers:
- name: app
volumeMounts:
- name: tmp
mountPath: /tmp
- name: next-cache
mountPath: /app/.next/cache
```
Both mounts are required. Missing the cache mount is the most common cause of ISR failures on AKS.
---
## Resource Sizing
Next.js SSR needs more memory than a plain API due to React rendering. Static-only exports can use lower limits.
| Resource | Request | Limit |
|----------|---------|-------|
| CPU | 200m | 1000m |
| Memory | 256Mi | 512Mi |
---
## Port Configuration
- **Default port:** 3000
- **Env var override:** `PORT=3000`
- **Hostname binding:** `HOSTNAME="0.0.0.0"` (Next.js 14+)
The standalone `server.js` reads the `PORT` and `HOSTNAME` environment variables automatically. No code changes are needed to customize the port.
---
## Build Commands
| Scenario | Build Command | Output | Entrypoint |
|----------|---------------|--------|------------|
| Standalone build (required) | `npm run build` with `output: 'standalone'` | `.next/standalone/server.js` | `node server.js` |
| Standard build (not for containers) | `npm run build` | `.next/` (full) | `next start` |
Always use the standalone build for container deployments. The standard build requires the full `node_modules` directory at runtime, resulting in images 5β10x larger.
### sharp for next/image
If the app uses `next/image`, install `sharp` explicitly in the runtime stage. Without it, Next.js falls back to the slower `squoosh` library and image optimization may fail under load.
---
## Common Issues on AKS
| Issue | Symptom | Fix |
|-------|---------|-----|
| Image too large without standalone | Image > 1GB, slow pulls from ACR | Set `output: 'standalone'` in `next.config.js` β reduces image to ~100MB |
| Static assets 404 | CSS/JS files return 404 after deployment | Ensure `.next/static` is copied to `.next/static` in the runtime stage (not into `standalone/.next/static`) |
| ISR fails with read-only filesystem | `EROFS: read-only file system` when revalidating pages | Mount `emptyDir` volume at `/app/.next/cache` β ISR writes regenerated pages to the cache directory |
| next/image optimization fails | Images return 500 or timeout under load | Install `sharp` explicitly; the standalone build may not include it automatically |
| Env vars undefined (`NEXT_PUBLIC_` prefix) | Client-side code sees `undefined` for environment variables | `NEXT_PUBLIC_` vars are inlined at **build time**, not runtime; set them as build args in the Dockerfile or use runtime config via `publicRuntimeConfig` |
| Telemetry calls from container | Unexpected outbound network requests to `telemetry.nextjs.org` | Set `NEXT_TELEMETRY_DISABLED=1` in both the build stage and runtime stage of the Dockerfile |
spring-boot.md 6.0 KB
# Spring Boot Knowledge Pack
> **Applies to:** Projects detected with `pom.xml` containing `spring-boot-starter-web` or `build.gradle`/`build.gradle.kts` containing `org.springframework.boot`
## Quick Reference
| Property | Value |
|----------|-------|
| Signal files | `pom.xml` with `spring-boot-starter-web` or `build.gradle(.kts)` with `org.springframework.boot` |
| Default port | `8080` |
| Health path | `/actuator/health/liveness` + `/actuator/health/readiness` |
| Base template | `templates/dockerfiles/java.Dockerfile` (+ `references/base-images.md`) |
---
## Dockerfile Patterns
The base template handles the multi-stage build. One Spring Boot-specific optimization worth applying: use layered JAR extraction (`java -Djarmode=layertools -jar app.jar extract`) so that dependencies, Spring Boot loader, and application code land in separate Docker layers β only changed layers are rebuilt or pushed on each deployment.
---
## Health Endpoints
Spring Boot Actuator provides health endpoints out of the box:
| Endpoint | Purpose | Probe Type |
|----------|---------|-----------|
| `/actuator/health` | Overall health | General |
| `/actuator/health/liveness` | Liveness group | `livenessProbe` |
| `/actuator/health/readiness` | Readiness group | `readinessProbe` |
### Required configuration
In `application.properties` or `application.yml`:
```properties
management.endpoints.web.exposure.include=health
management.endpoint.health.probes.enabled=true
management.endpoint.health.show-details=always
```
The probes are automatically enabled when running in Kubernetes (detected via the `KUBERNETES_SERVICE_HOST` env var), but it's best practice to enable them explicitly.
### Probe configuration in Deployment manifest
```yaml
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
failureThreshold: 30 # allows up to 300s for JVM warmup + Spring context init
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
```
**Important:** Spring Boot apps require a `startupProbe`. JVM warmup and Spring context initialization typically take 15β60 seconds. Without it, the liveness probe may kill the pod before it finishes starting. The startup probe gives the app up to 300 seconds to become healthy before the liveness probe takes over. Uncomment the `startupProbe` section in the deployment template.
---
## Database Profiles
Spring Boot uses Spring Profiles to switch database configurations:
| Profile | Activation | Typical Config File |
|---------|------------|-------------------|
| `default` | No profile set | `application.properties` β usually H2 in-memory |
| `mysql` | `SPRING_PROFILES_ACTIVE=mysql` | `application-mysql.properties` |
| `postgres` | `SPRING_PROFILES_ACTIVE=postgres` | `application-postgres.properties` |
### Environment variables for PostgreSQL on AKS
```yaml
env:
- name: SPRING_PROFILES_ACTIVE
value: postgres
- name: POSTGRES_URL
value: "jdbc:postgresql://{{PG_SERVER_NAME}}.postgres.database.azure.com:5432/{{DB_NAME}}"
- name: POSTGRES_USER
value: "{{IDENTITY_NAME}}"
- name: SPRING_DATASOURCE_AZURE_PASSWORDLESS_ENABLED
value: "true"
```
With Workload Identity and the `spring-cloud-azure-starter-jdbc-postgresql` dependency, Spring Boot can authenticate to PostgreSQL without a password using Azure AD tokens.
### ConfigMap pattern
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{APP_NAME}}-config
data:
SPRING_PROFILES_ACTIVE: "postgres"
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE: "health"
MANAGEMENT_ENDPOINT_HEALTH_PROBES_ENABLED: "true"
```
---
## Writable Paths (DS012 Compliance)
When `readOnlyRootFilesystem: true` is set, Spring Boot needs `/tmp` writable:
- **Tomcat** writes session data and compiled JSPs to `/tmp`
- **Multipart file uploads** use `/tmp` as the staging directory
- **Spring Boot DevTools** (if accidentally included) writes to `/tmp`
### Required volume mount
```yaml
volumes:
- name: tmp
emptyDir: {}
containers:
- name: app
volumeMounts:
- name: tmp
mountPath: /tmp
```
No other writable paths are typically needed for production Spring Boot apps.
---
## Resource Sizing
Spring Boot apps running on the JVM need more memory than interpreted languages. These are starting-point defaults β tune based on observed usage.
| Resource | Request | Limit |
|----------|---------|-------|
| CPU | 250m | 1000m |
| Memory | 512Mi | 1Gi |
Set `-XX:MaxRAMPercentage=75.0` in `JAVA_OPTS` so the JVM uses at most 75% of the container's memory limit, leaving headroom for the OS and non-heap memory.
---
## Port Configuration
- **Default port:** 8080
- **Config property:** `server.port` in `application.properties`
- **Env var override:** `SERVER_PORT=8080`
Spring Boot always logs the port on startup: `Tomcat started on port(s): 8080 (http)`
---
## Build Commands
| Build Tool | Build Command | Output |
|-----------|---------------|--------|
| Maven | `./mvnw package -DskipTests -B` | `target/*.jar` |
| Gradle | `./gradlew bootJar` | `build/libs/*.jar` |
The `-B` flag (batch mode) suppresses interactive Maven output β important for CI/CD and Docker builds.
---
## Common Issues on AKS
| Issue | Symptom | Fix |
|-------|---------|-----|
| JVM OOM in container | `OOMKilled` pod status | Set `-XX:MaxRAMPercentage=75.0` in `JAVA_OPTS` and ensure memory limit >= 256Mi |
| Slow startup | Readiness probe fails, pod restarted | Increase `initialDelaySeconds` to 45-60s, or add a `startupProbe` with higher `failureThreshold` |
| H2 in-memory on AKS | Data lost on pod restart | Switch to PostgreSQL profile β H2 is for local dev only |
| Connection refused to PostgreSQL | `PSQLException: Connection refused` | Verify firewall rules on PostgreSQL Flexible Server allow AKS subnet |
| Image too large (>500MB) | Slow pulls, high ACR storage | Use Alpine base image + layered JAR extraction |
quick-deploy.md 7.8 KB
# Quick Deploy
Deploy an application to an existing AKS cluster with production-grade artifacts.
## Section 1: Detection
Scan the project and Azure environment. Ask at most one clarifying question if genuinely ambiguous (multiple Dockerfiles, ACRs, or identities).
### Framework Detection
Follow the framework detection table in `references/detection.md`. Scan for signal files at the project root (and one level deep for monorepos).
### Port and Health Endpoint Detection
Follow the port and health endpoint detection tables in `references/detection.md` (first match wins). If none found, use `/health` as default in probes.
### Existing Artifact Detection
Check for existing `Dockerfile` and `k8s/` (or `manifests/`, `deploy/`) directories.
### Azure Infrastructure Detection
```bash
kubectl config current-context
az aks show -g <rg> -n <cluster> -o json
```
Extract from cluster details:
- **AKS flavor**: `nodeProvisioningProfile.mode` β `"Auto"` = AKS Automatic, otherwise Standard
- **OIDC issuer**: `oidcIssuerProfile.issuerUrl`
- **Azure RBAC**: `aadProfile.enableAzureRBAC`
### Routing Detection
```bash
az aks show -g <rg> -n <cluster> --query '{webAppRoutingEnabled: ingressProfile.webAppRouting.enabled, istioMode: serviceMeshProfile.istio.mode}' -o json
```
- If `webAppRoutingEnabled` is not `true`, stop with error: `az aks approuting enable -g <rg> -n <cluster>`
- If `istioMode` is `"Enabled"` β use **Gateway API** (`gateway.yaml` + `httproute.yaml`, `gatewayClassName: istio`)
- Otherwise β use **Ingress** (`ingress.yaml`, `ingressClassName: webapprouting.kubernetes.azure.com`)
```bash
az acr list -g <rg> -o json
az identity list -g <rg> -o json
```
### ACR-AKS Integration
Verify the AKS kubelet identity can pull images from the detected ACR:
```bash
az aks check-acr --resource-group <rg> --name <cluster> --acr <acr-name>.azurecr.io
```
If the check fails, attach the ACR (requires confirmation): `az aks update -g <rg> -n <cluster> --attach-acr <acr-name>`
If Azure RBAC is enabled, verify namespace create permission: `kubectl auth can-i create namespaces` β if `no`, stop and offer alternatives: have an admin create it, or deploy to an existing namespace.
If any detection command fails, suggest: `az login`, `az account set -s <subscription-id>`, `az aks get-credentials -g <rg> -n <cluster>`.
### Knowledge Pack
After framework detection, load the matching pack from `knowledge-packs/frameworks/` if available (see `SKILL.md`).
Knowledge packs influence Dockerfile optimization, probe configuration, and writable paths.
---
## Section 2: File Generation
Write all files in a single response turn.
### Dockerfile
**If existing Dockerfile:** Validate against best practices (multi-stage build, non-root USER, base tags pinned to a stable major tag (not :latest, not a frozen patch), layer caching, .dockerignore). Apply targeted fixes for failures β do not regenerate the file.
**If no Dockerfile:** Generate from the appropriate template:
| Language | Template |
|----------|----------|
| Node.js | `templates/dockerfiles/node.Dockerfile` |
| Python | `templates/dockerfiles/python.Dockerfile` |
| Java | `templates/dockerfiles/java.Dockerfile` |
| Go | `templates/dockerfiles/go.Dockerfile` |
| .NET | `templates/dockerfiles/dotnet.Dockerfile` |
| Rust | `templates/dockerfiles/rust.Dockerfile` |
**Resolve the base image version.** The templates carry `<LATEST_STABLE_*>`
placeholders. Before writing the Dockerfile, replace each with the current
stable major the project targets, following `references/base-images.md`
(explicit registry/release check; fall back to latest-known with a verify
comment). The generated Dockerfile must end up pinned to a concrete major tag
β this is required for DS009.
Generate `.dockerignore` if missing β use the matching template from `templates/dockerfiles/<language>.dockerignore`.
### Kubernetes Manifests
**If existing manifests found** (in `k8s/`, `manifests/`, or `deploy/`): Validate against AKS Deployment Safeguards (Section 3) and apply targeted fixes. Do not regenerate β improve in place.
**If no manifests found:** Generate from `templates/k8s/` templates. Replace `<angle-bracket>` placeholders with detected values.
| Manifest | Template | Notes |
|----------|----------|-------|
| `k8s/namespace.yaml` | `templates/k8s/namespace.yaml` | |
| `k8s/serviceaccount.yaml` | `templates/k8s/serviceaccount.yaml` | Workload Identity |
| `k8s/deployment.yaml` | `templates/k8s/deployment.yaml` | image tag set after `az acr build`, not at generation time |
| `k8s/service.yaml` | `templates/k8s/service.yaml` | |
| `k8s/gateway.yaml` | `templates/k8s/gateway.yaml` | Istio only |
| `k8s/httproute.yaml` | `templates/k8s/httproute.yaml` | Istio only |
| `k8s/ingress.yaml` | `templates/k8s/ingress.yaml` | Ingress only |
| `k8s/hpa.yaml` | `templates/k8s/hpa.yaml` | min: 2, max: 10 |
| `k8s/pdb.yaml` | `templates/k8s/pdb.yaml` | minAvailable: 1 |
| `k8s/configmap.yaml` | `templates/k8s/configmap.yaml` | If env config needed |
| `k8s/networkpolicy.yaml` | `templates/k8s/networkpolicy.yaml` | Ingress-controller-scoped |
### Hostname
Omit the `host` field from Ingress `rules` (or Gateway `listeners`) for initial deployments β traffic routes to the external IP directly. Add `host` and TLS once the user has a domain.
### Resource Sizing
Use the framework-specific defaults from the knowledge pack's "Resource Sizing" section. If no pack is loaded, use `requests: {cpu: 100m, memory: 128Mi}` and `limits: {cpu: 500m, memory: 256Mi}`.
### Startup Probe
For slow-start frameworks (Java/Spring Boot, .NET with heavy DI), uncomment `startupProbe` in the deployment template to prevent liveness restarts during init.
---
## Section 3: Safeguards Validation
Validate all generated manifests against DS001-DS013 (reference `references/safeguards.md`).
- 12 of 13 rules are auto-fixable. DS009 (no `:latest` tag) is resolved by tagging with git SHA.
- Apply the writable paths from the loaded pack (for `readOnlyRootFilesystem: true` compliance with DS012).
- Reference `references/workload-identity.md` for Workload Identity configuration.
**AKS Automatic:** all violations must be fixed.
**AKS Standard:** check `safeguardsProfile.level`:
```bash
az aks show -g <rg> -n <cluster> --query 'safeguardsProfile.level' -o tsv
```
- `Enforcement`: fix all violations
- `Warning` or `Off`: warn, don't block
---
## Section 4: Deploy
### Ensure kubectl context
```bash
az aks get-credentials -g <resource_group> -n <aks_cluster_name> --overwrite-existing
```
### Verify Gateway API CRDs (only if Istio Gateway API detected)
```bash
kubectl get crd gateways.gateway.networking.k8s.io httproutes.gateway.networking.k8s.io 2>/dev/null
```
If missing: `kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/latest/download/standard-install.yaml`
### Build and push
```bash
IMAGE_TAG=$(git rev-parse --short HEAD) # fallback: date +%Y%m%d%H%M%S
az acr build --registry <acr_name> --image <app-name>:$IMAGE_TAG --file Dockerfile .
```
**Monorepo:** Adjust `--file` and context to the app subdirectory (e.g., `--file apps/myapp/Dockerfile apps/myapp/`).
### Deploy to cluster
```bash
# 1. Create namespace (must succeed before proceeding)
kubectl apply -f k8s/namespace.yaml
kubectl get namespace <namespace> -o name # verify
# 2. Apply remaining manifests
kubectl apply -f k8s/ --recursive
# 3. Wait for rollout
kubectl rollout status deployment/<app-name> -n <namespace> --timeout=300s
```
If any step fails, show the error and stop. See `references/rollback.md` for recovery.
---
## Section 5: Verify
```bash
kubectl get pods -n <namespace> -l app=<app-name>
kubectl get gateway -n <namespace> -o jsonpath='{.items[0].status.addresses[0].value}' # if Gateway API
kubectl get ingress -n <namespace> -o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}' # if Ingress
```
Wait up to 3 minutes for external IP, then curl the health endpoint.
base-images.md 3.6 KB
# Base Image Policy
How the skill chooses container base images. Goal: **no version number is
stored in this repo** β the concrete tag is resolved when the Dockerfile is
generated, so templates never go stale.
## Rules
1. **Never pin minor/patch.** Use a floating **major** (or major.minor) tag.
Floating tags receive security patches automatically β a frozen patch tag
does not.
2. **Resolve `<LATEST_STABLE_*>` at generation time.** When generating a
Dockerfile, replace each placeholder with the current stable major the
project targets (see "Resolution" below), then keep that major tag in the
output. A major tag is concrete, so it satisfies Deployment Safeguard DS009
(no `:latest`).
3. **Prefer the Microsoft/Azure Linux image when one exists and the project
has no reason to avoid it.** These are first-party, signed, and rebuilt for
CVEs. They are listed below as the preferred option; the current default
source is kept for drop-in compatibility.
## Per-language images
| Language | Default source (template today) | Tag policy | Preferred Microsoft / Azure Linux image |
|----------|---------------------------------|-----------|------------------------------------------|
| .NET | `mcr.microsoft.com/dotnet/{sdk,aspnet}` | floating major (e.g. `9.0`) | already Microsoft β |
| Java | `eclipse-temurin:<maj>-{jdk,jre}-alpine` | floating major LTS | `mcr.microsoft.com/openjdk/jdk:<maj>-azurelinux` (also `-distroless`) |
| Python | `python:<maj.min>-slim` | floating major.minor | `mcr.microsoft.com/azurelinux/base/python:<maj.min>` |
| Node | `node:<maj>-alpine` | floating major LTS | `mcr.microsoft.com/azurelinux/base/nodejs:<maj>` |
| Go | build `golang:<ver>-alpine`, runtime `gcr.io/distroless/static-debian12` | floating major.minor | `mcr.microsoft.com/oss/go/microsoft/golang:<ver>-azurelinux3.0` |
| Rust | build `rust:<ver>-slim`, runtime `gcr.io/distroless/cc-debian12` | floating major.minor | `mcr.microsoft.com/azurelinux/base/rust:<maj.min>` |
> The Go and Rust **runtime** stages keep the literal
> `gcr.io/distroless/*-debian12` names and do **not** use a `<LATEST_STABLE_*>`
> placeholder: distroless images are identified by base-OS variant (the Debian
> release), not by language version, and they float their patch level within
> that Debian release. This asymmetry with the build stages is intentional β
> don't "fix" it by adding a placeholder.
> Adopting the Microsoft Azure Linux images for Python/Node/Go/Rust changes the
> in-image package manager (`tdnf`, not `apk`/`apt`) and the non-root-user
> setup. That migration is intentionally **out of scope** here β this file
> documents the target so a later change can adopt it.
## Resolution
To fill a `<LATEST_STABLE_*>` placeholder at generation time, in order of
preference:
1. **Explicit check.** Query the registry or release channel for the current
stable major, e.g.:
- .NET / Java / Go (Microsoft): `az acr manifest list-metadata` against the
MCR repo, or the image's tag list.
- Python / Node / Rust: the language's published release channel (Docker
Hub tag list, or the language's downloads page).
2. **Fallback.** If no check is possible (offline, no registry access), use the
latest stable major you know, and add a comment in the generated Dockerfile:
`# verify this is still the current stable major`.
Why floating majors are maintenance-free: Microsoft major tags always carry
the latest minor and are rebuilt for CVEs on a regular cadence; Docker Hub
`-slim`/`-alpine` tags float their patch level the same way. A major tag is
therefore both stable to reference and current for security.
detection.md 3.5 KB
# Detection Reference
Shared detection logic used by Section 1 (Detection) in Quick Deploy.
## Framework Detection
Scan for signal files at the project root (and one level deep for monorepos). Map each signal to a framework and, where possible, a sub-framework:
| Signal File | Framework | Sub-framework Detection |
|---|---|---|
| `package.json` | Node.js | Inspect `dependencies` for: **Express** (`express`), **Fastify** (`fastify`), **NestJS** (`@nestjs/core`), **Next.js** (`next`), **Remix** (`@remix-run/node`), **Hono** (`hono`), **Koa** (`koa`) |
| `requirements.txt` | Python | Scan for: **FastAPI** (`fastapi`), **Django** (`django`), **Flask** (`flask`), **Starlette** (`starlette`), **Gunicorn** (`gunicorn`) |
| `pyproject.toml` | Python | Parse `[project.dependencies]` or `[tool.poetry.dependencies]` for the same libraries as above |
| `Pipfile` | Python | Parse `[packages]` section for the same libraries as above |
| `pom.xml` | Java | Search for `<artifactId>spring-boot-starter-web</artifactId>` β **Spring Boot**; `<artifactId>quarkus-resteasy</artifactId>` β **Quarkus**; `<artifactId>micronaut-http-server-netty</artifactId>` β **Micronaut** |
| `build.gradle` / `build.gradle.kts` | Java / Kotlin | Search for `org.springframework.boot` β **Spring Boot**; `io.quarkus` β **Quarkus**; `io.micronaut` β **Micronaut** |
| `go.mod` | Go | Parse `require` block for: `github.com/gin-gonic/gin` β **Gin**; `github.com/labstack/echo` β **Echo**; `github.com/gofiber/fiber` β **Fiber**. For `net/http` (stdlib): search `.go` source files for `"net/http"` import β stdlib packages never appear in the `require` block |
| `*.csproj` | .NET | Search for `<PackageReference Include="Microsoft.AspNetCore.*"` β **ASP.NET Core**; check `<TargetFramework>` for version (e.g. `net8.0`) |
| `Cargo.toml` | Rust | Parse `[dependencies]` for: `actix-web` β **Actix**; `axum` β **Axum**; `rocket` β **Rocket**; `warp` β **Warp** |
**If multiple signal files are found** (e.g. both `package.json` and `requirements.txt`), record all of them β this may indicate a monorepo or polyglot project. Flag for clarification.
## Port Detection
Check these sources in priority order (first match wins):
| Source | What to Look For | Example |
|---|---|---|
| `Dockerfile` | `EXPOSE <port>` directive | `EXPOSE 3000` |
| `.env` / `.env.example` | `PORT=<number>` | `PORT=8080` |
| `package.json` (`scripts.start`) | `--port <number>` or `-p <number>` | `next start --port 3000` |
| Source code | `app.listen(<number>)`, `.listen(<number>)`, `server.port=<number>` | `app.listen(3000)` |
| `application.properties` / `application.yml` (Java) | `server.port=<number>` | `server.port=8080` |
| `appsettings.json` (.NET) | `"Urls": "http://*:<number>"` | `"Urls": "http://*:8080"` |
| Framework defaults | Use known defaults if nothing explicit found | Express: 3000, FastAPI: 8000, Spring Boot: 8080, ASP.NET: 8080, Gin: 8080 |
## Health Endpoint Detection
Grep the source tree for route registrations matching these patterns:
| Pattern | Endpoint Type |
|---|---|
| `/health` | Generic health check |
| `/healthz` | Kubernetes-style health check |
| `/ready`, `/readiness` | Readiness probe |
| `/liveness` | Liveness probe |
| `/startup` | Startup probe |
| `/ping` | Simple ping (sometimes used as health) |
| `/status` | Status endpoint |
| `/api/health`, `/api/healthz` | Prefixed health check |
Record the **HTTP method** (GET/HEAD) and **expected response code** (200) for each detected endpoint. If no health endpoints are found, flag it β probes will use `/health` as default.
rollback.md 2.0 KB
# Rollback Guidance
Recovery procedures for deployment failures. Referenced from Section 4 (Deploy).
---
## Image Build Failed
```bash
# No cloud resources were persisted β nothing to roll back.
# Fix the issue and retry:
# Common fixes:
# - Dockerfile syntax error β edit Dockerfile
# - Missing file in build context β check .dockerignore
# - Dependency install failure β fix package.json / requirements.txt / go.mod
# Retry:
az acr build --registry <acr-name> --image <app-name>:<git-sha> .
```
## kubectl apply Failed (Section 4 β Deploy to Cluster)
```bash
# Remove the partially applied resources:
kubectl delete -f k8s/
# Common fixes:
# - YAML syntax error β validate with: kubectl apply -f k8s/ --dry-run=client
# - Invalid resource field β check API version matches cluster version
# - Image pull error β verify ACR name in deployment.yaml matches actual ACR
# - Namespace doesn't exist β create it first or remove namespace from manifests
# Fix and retry:
kubectl apply -f k8s/
```
## Pods Not Starting (Section 5 β Verify)
```bash
# Diagnose:
kubectl get pods -l app=myapp
kubectl describe pod -l app=myapp
kubectl logs -l app=myapp --tail=50
# Common error patterns:
# CrashLoopBackOff β app crashes on startup
# β Check logs for the crash reason
# β Usually: missing env var, bad database connection string, port mismatch
# ImagePullBackOff β can't pull the container image
# β Verify image name: kubectl get deployment myapp -o jsonpath='{.spec.template.spec.containers[0].image}'
# β Verify ACR access: az aks check-acr --resource-group <rg> --name <aks> --acr <acr>.azurecr.io
# Pending β pod can't be scheduled
# β Check node status: kubectl get nodes
# β Check resource requests vs available capacity: kubectl describe nodes
# OOMKilled β app exceeded memory limit
# β Increase memory limit in k8s/deployment.yaml and re-apply
# After fixing, re-apply:
kubectl apply -f k8s/
kubectl rollout status deployment/myapp --timeout=300s
```
safeguards.md 2.9 KB
# AKS Deployment Safeguards Reference
> **Source of truth:** the Deployment Safeguards policy initiative is defined
> once in
> `../../azure-kubernetes-automatic-readiness/references/constraint-spec-v1.yaml`
> (initiative `c047ea8e-β¦`). This file is the **deploy-time checklist**: which
> rules the app-deploy workflow auto-fixes vs. warns on, and how. When the
> policy set changes, update the constraint spec; only the app-deploy-specific
> fix behavior below is maintained here.
This checklist maps each safeguard to how the quick-deploy workflow handles it.
## DS001 β Resource Limits Required (Error)
Every container needs `resources.requests` AND `resources.limits` for both `cpu` and `memory`.
## DS002 β Liveness Probe Required (Warning)
Every container needs a `livenessProbe`. Use `httpGet`, `tcpSocket`, or `exec`.
## DS003 β Readiness Probe Required (Warning)
Every container needs a `readinessProbe`.
## DS004 β runAsNonRoot Required (Error)
Set at **both** pod and container level.
## DS005 β No hostNetwork (Error)
Remove `hostNetwork: true` or set to `false`.
## DS006 β No hostPID (Error)
Remove `hostPID: true` or set to `false`.
## DS007 β No hostIPC (Error)
Remove `hostIPC: true` or set to `false`.
## DS008 β No Privileged Containers (Error)
Remove `securityContext.privileged: true` or set to `false`.
## DS009 β No :latest Image Tag (Error, NOT auto-fixable)
Use a semantic version, git SHA, or digest β never `:latest` or omit the tag.
## DS010 β Minimum 2 Replicas (Warning)
Set `spec.replicas: 2` or higher. Pair with a PodDisruptionBudget.
## DS011 β allowPrivilegeEscalation: false (Error)
Every container must set `securityContext.allowPrivilegeEscalation: false`.
## DS012 β readOnlyRootFilesystem: true (Warning)
Every container must set `securityContext.readOnlyRootFilesystem: true`.
If the app writes to specific paths, mount `emptyDir` volumes:
```yaml
volumes:
- name: tmp
emptyDir: {}
containers:
- volumeMounts:
- name: tmp
mountPath: /tmp
```
Common writable paths: Spring Boot `/tmp`, ASP.NET `/tmp`, Django `/tmp`, Express `/tmp`, Go `/tmp`.
## DS013 β automountServiceAccountToken: false (Warning)
Set `spec.automountServiceAccountToken: false`. Set to `true` only if the app genuinely calls the K8s API (scope with RBAC).
---
## Quick Reference
| Rule | What | Severity | Auto-Fix |
|------|------|----------|----------|
| DS001 | Resource limits | Error | Yes |
| DS002 | Liveness probe | Warning | Yes |
| DS003 | Readiness probe | Warning | Yes |
| DS004 | runAsNonRoot | Error | Yes |
| DS005 | No hostNetwork | Error | Yes |
| DS006 | No hostPID | Error | Yes |
| DS007 | No hostIPC | Error | Yes |
| DS008 | No privileged | Error | Yes |
| DS009 | No :latest tag | Error | No |
| DS010 | Min 2 replicas | Warning | Yes |
| DS011 | No privilege escalation | Error | Yes |
| DS012 | Read-only root FS | Warning | Yes |
| DS013 | No SA token mount | Warning | Yes |
workload-identity.md 7.3 KB
# Azure Workload Identity for AKS
> **Last updated:** 2026-04-02
## What Is Workload Identity?
Workload Identity lets pods in AKS authenticate to Azure services (Key Vault, Storage,
PostgreSQL, etc.) without storing any secrets. Instead of injecting connection strings or
passwords, your pod proves its identity through a short-lived token issued by the
cluster's OIDC provider, which Microsoft Entra ID trusts because you've set up a federation
between the cluster and a Managed Identity. The pod gets a token automatically β your
app code just uses the standard Azure SDK credential chain.
---
## Three Components
### 1. User-Assigned Managed Identity
A Managed Identity in Azure that has RBAC role assignments on the target resources
(e.g., `Key Vault Secrets User`, `Storage Blob Data Contributor`).
```
Managed Identity
βββ Client ID: <AZURE_CLIENT_ID>
βββ Tenant ID: <AZURE_TENANT_ID>
βββ Role assignments:
βββ Key Vault Secrets User β /subscriptions/.../vaults/my-kv
βββ Storage Blob Data Contributor β /subscriptions/.../storageAccounts/my-sa
βββ ...
```
### 2. Federated Identity Credential
A trust relationship that says: "When the AKS cluster's OIDC issuer presents a token
for ServiceAccount `<namespace>/<sa-name>`, treat it as this Managed Identity."
```
Federated Credential
βββ Issuer: https://oidc.prod-aks.azure.com/<tenant-id>/<cluster-id>
βββ Subject: system:serviceaccount:<namespace>:<sa-name>
βββ Audience: api://AzureADTokenExchange
```
### 3. Kubernetes ServiceAccount
A standard K8s ServiceAccount annotated with the Managed Identity's client ID.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: <app-name>
namespace: <namespace>
annotations:
azure.workload.identity/client-id: "<AZURE_CLIENT_ID>"
```
---
## How They Link Together
```
Pod (with label azure.workload.identity/use: "true")
β
βββ References ServiceAccount (annotated with client-id)
β
βΌ
AKS OIDC Issuer issues a projected service account token
β
βββ Issuer URL matches the Federated Credential's issuer
βββ Subject (system:serviceaccount:ns:sa) matches the Federated Credential's subject
β
βΌ
Microsoft Entra ID validates the federation and issues a token
β
βΌ
Azure SDK (DefaultAzureCredential) uses the token to access Azure resources
```
The Workload Identity webhook in AKS automatically:
- Projects the service account token into the pod at a well-known path
- Sets the `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_FEDERATED_TOKEN_FILE`
environment variables in the container
Your app code does **not** need to know about any of this β `DefaultAzureCredential`
picks it up automatically.
---
## Per-Service Patterns
### PostgreSQL (Flexible Server with Microsoft Entra ID Auth)
The Managed Identity needs the `<db-name> Admin` or a custom PostgreSQL role.
```python
# Python β psycopg2 + DefaultAzureCredential
import psycopg2
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
token = credential.get_token("https://ossrdbms-aad.database.windows.net/.default")
conn = psycopg2.connect(
host="<server>.postgres.database.azure.com",
dbname="<database>",
user="<managed-identity-name>",
password=token.token,
sslmode="require",
)
```
```csharp
// C# β Npgsql + Azure.Identity
var credential = new DefaultAzureCredential();
var token = await credential.GetTokenAsync(
new TokenRequestContext(new[] { "https://ossrdbms-aad.database.windows.net/.default" }));
var connString = $"Host=<server>.postgres.database.azure.com;Database=<database>;"
+ $"Username=<managed-identity-name>;Password={token.Token};SSL Mode=Require";
await using var conn = new NpgsqlConnection(connString);
```
**Required env vars** (injected by Workload Identity webhook):
- `AZURE_CLIENT_ID` β used by `DefaultAzureCredential`
### Key Vault
Role assignment: `Key Vault Secrets User` (or `Key Vault Crypto User` for keys).
```python
# Python
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
credential = DefaultAzureCredential()
client = SecretClient(vault_url="https://<vault-name>.vault.azure.net", credential=credential)
secret = client.get_secret("my-secret")
```
```csharp
// C#
var credential = new DefaultAzureCredential();
var client = new SecretClient(new Uri("https://<vault-name>.vault.azure.net"), credential);
KeyVaultSecret secret = await client.GetSecretAsync("my-secret");
```
### Azure Blob Storage
Role assignment: `Storage Blob Data Contributor` (or `Reader` for read-only).
```python
# Python
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
credential = DefaultAzureCredential()
client = BlobServiceClient(
account_url="https://<account>.blob.core.windows.net",
credential=credential,
)
```
```csharp
// C#
var credential = new DefaultAzureCredential();
var client = new BlobServiceClient(
new Uri("https://<account>.blob.core.windows.net"), credential);
```
### Azure Cache for Redis (Microsoft Entra ID Token Auth)
Role assignment: `Redis Cache Contributor` or custom data-plane role.
```python
# Python β redis-py with Microsoft Entra ID token
import os
from azure.identity import DefaultAzureCredential
import redis
credential = DefaultAzureCredential()
token = credential.get_token("https://redis.azure.com/.default")
r = redis.Redis(
host="<cache-name>.redis.cache.windows.net",
port=6380,
ssl=True,
username=os.environ["AZURE_CLIENT_ID"],
password=token.token,
)
```
```csharp
// C#
var credential = new DefaultAzureCredential();
var token = await credential.GetTokenAsync(
new TokenRequestContext(new[] { "https://redis.azure.com/.default" }));
var muxer = await ConnectionMultiplexer.ConnectAsync(new ConfigurationOptions
{
EndPoints = { "<cache-name>.redis.cache.windows.net:6380" },
Ssl = true,
User = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"),
Password = token.Token,
});
```
---
## Required Pod Labels and ServiceAccount Annotations
### Pod Label (on the Deployment's `spec.template.metadata.labels`)
```yaml
labels:
azure.workload.identity/use: "true"
```
This label tells the Workload Identity webhook to inject the projected token volume
and environment variables into the pod.
### ServiceAccount Annotation
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: <app-name>
annotations:
azure.workload.identity/client-id: "<AZURE_CLIENT_ID>"
```
This annotation tells the webhook which Managed Identity to federate with.
### Complete Deployment Snippet
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: <app-name>
spec:
template:
metadata:
labels:
app: <app-name>
azure.workload.identity/use: "true" # β required label
spec:
serviceAccountName: <app-name> # β references annotated SA
automountServiceAccountToken: false # β DS013 (Workload Identity uses projected volume, not SA token)
containers:
- name: <app-name>
# AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE
# are injected automatically by the webhook
```
> **Note:** `automountServiceAccountToken: false` disables the *default* SA token mount.
> Workload Identity uses a separate projected volume that the webhook manages independently,
> so both can coexist without conflict.
dotnet.Dockerfile 2.6 KB
# =============================================================================
# .NET (ASP.NET Core) Production Dockerfile
# =============================================================================
# Customize the following before use:
# - PROJECT_NAME: Replace "MyApp" with your .csproj name (without extension)
# - PORT: Change EXPOSE port if not 8080
# - ASSEMBLY: Adjust the DLL name in ENTRYPOINT if it differs from the project
#
# Notes:
# - .NET 8+ defaults to port 8080 (ASPNETCORE_HTTP_PORTS), not 80
# - The "app" user is built into the aspnet runtime image since .NET 8
# - For self-contained deployment, add --self-contained to dotnet publish
# and switch the runtime image to mcr.microsoft.com/dotnet/runtime-deps:<LATEST_STABLE_DOTNET_ASPNET>
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Build
# ---------------------------------------------------------------------------
# Base: current .NET LTS (Microsoft official). See references/base-images.md.
FROM mcr.microsoft.com/dotnet/sdk:<LATEST_STABLE_DOTNET_SDK> AS build
WORKDIR /src
# Layer caching: restore NuGet packages before copying the full source.
# Copy only project files first so the restore layer is cached independently.
COPY *.sln ./
COPY src/MyApp/*.csproj src/MyApp/
RUN dotnet restore src/MyApp/MyApp.csproj
# Copy everything and publish a Release build
COPY . .
RUN dotnet publish src/MyApp/MyApp.csproj \
--configuration Release \
--no-restore \
--output /app/publish
# ---------------------------------------------------------------------------
# Stage 2: Runtime
# ---------------------------------------------------------------------------
# Base: current .NET LTS (Microsoft official). See references/base-images.md.
FROM mcr.microsoft.com/dotnet/aspnet:<LATEST_STABLE_DOTNET_ASPNET>
WORKDIR /app
# Copy published output from the build stage
COPY --from=build /app/publish ./
# AKS Deployment Safeguards DS004: run as non-root.
# The "app" user is built into the aspnet image since .NET 8.
USER app
EXPOSE 8080
ENV ASPNETCORE_URLS="http://+:8080" \
DOTNET_RUNNING_IN_CONTAINER=true \
DOTNET_EnableDiagnostics=0
# No HEALTHCHECK: the aspnet runtime image does not include curl or wget.
# Kubernetes liveness/readiness probes (configured in deployment.yaml) handle
# health checking in AKS. For local Docker usage, install wget or add
# app.MapHealthChecks("/healthz") and use a custom health check binary.
ENTRYPOINT ["dotnet", "MyApp.dll"]
dotnet.dockerignore 0.1 KB
**/bin
**/obj
**/out
*.user
*.suo
.vs
.env
.env.*
.git
.gitignore
.dockerignore
Dockerfile
*.md
.vscode
.idea
**/TestResults
go.Dockerfile 2.1 KB
# =============================================================================
# Go Production Dockerfile
# =============================================================================
# Customize the following before use:
# - APP_NAME: Replace "app" in the binary name (go build -o /bin/app)
# and in the ENTRYPOINT ["/app"] line
# - PORT: Change EXPOSE port if not 8080
# - MODULE_PATH: Ensure go.mod module path matches your project
#
# Notes:
# - CGO_ENABLED=0 produces a fully static binary that runs on distroless
# - The distroless runtime has no shell β use the exec form for ENTRYPOINT
# - To debug, swap the runtime to gcr.io/distroless/static-debian12:debug
# which includes busybox
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Build
# ---------------------------------------------------------------------------
# Base: current stable Go. See references/base-images.md.
FROM golang:<LATEST_STABLE_GO>-alpine AS build
WORKDIR /src
# Layer caching: download module dependencies before copying source.
# This layer is only rebuilt when go.mod or go.sum changes.
COPY go.mod go.sum ./
RUN go mod download && go mod verify
# Copy source and compile a static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux \
go build -ldflags="-s -w" -o /bin/app ./cmd/app
# ---------------------------------------------------------------------------
# Stage 2: Runtime
# ---------------------------------------------------------------------------
FROM gcr.io/distroless/static-debian12
# Copy the compiled binary from the build stage
COPY --from=build /bin/app /app
# AKS Deployment Safeguards DS004: run as non-root.
# 65534 is the "nobody" user in distroless images.
USER 65534
EXPOSE 8080
# Distroless has no shell, curl, or wget. Kubernetes liveness/readiness probes
# (configured in deployment.yaml) handle health checking in AKS.
# For local Docker usage, consider adding a /healthz handler and using a
# statically-compiled health check binary, or swap to the :debug variant.
ENTRYPOINT ["/app"]
go.dockerignore 0.1 KB
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
vendor
.env
.env.*
.git
.gitignore
.dockerignore
Dockerfile
*.md
.vscode
.idea
tmp
java.Dockerfile 3.0 KB
# =============================================================================
# Java (Spring Boot / Maven) Production Dockerfile
# =============================================================================
# Customize the following before use:
# - JAR_FILE: Adjust the glob pattern if your build output differs
# - PORT: Change EXPOSE port if not 8080
# - JVM_OPTS: Tune -Xmx, -Xms, GC flags, etc. via JAVA_OPTS env var
#
# Gradle users:
# Replace the Maven wrapper commands in the build stage with:
# COPY gradlew build.gradle.kts settings.gradle.kts ./
# COPY gradle ./gradle
# RUN ./gradlew dependencies --no-daemon
# COPY . .
# RUN ./gradlew bootJar --no-daemon
# And adjust the JAR_FILE path to "build/libs/*.jar"
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Build
# ---------------------------------------------------------------------------
# Base: current Java LTS (Temurin). See references/base-images.md for the Microsoft OpenJDK alternative.
FROM eclipse-temurin:<LATEST_STABLE_JAVA_JDK>-jdk-alpine AS build
WORKDIR /app
# Layer caching: copy Maven wrapper and POM first so dependency resolution is
# cached independently of source changes.
COPY mvnw pom.xml ./
COPY .mvn .mvn
# Download dependencies (offline-friendly layer)
RUN chmod +x mvnw \
&& ./mvnw dependency:go-offline -B
# Copy source and build the fat JAR
COPY src ./src
# -Dspring-boot.repackage.finalName=app ensures a single predictably named fat JAR,
# avoiding glob ambiguity when Maven produces both thin and fat JARs.
RUN ./mvnw package spring-boot:repackage -DskipTests -B \
-Dspring-boot.repackage.finalName=app \
&& mv target/app.jar app.jar
# ---------------------------------------------------------------------------
# Stage 2: Runtime
# ---------------------------------------------------------------------------
FROM eclipse-temurin:<LATEST_STABLE_JAVA_JRE>-jre-alpine
WORKDIR /app
# AKS Deployment Safeguards DS004: create and switch to a non-root user
RUN addgroup -S appuser && adduser -S appuser -G appuser
# Copy only the built JAR from the build stage
COPY --from=build --chown=appuser:appuser /app/app.jar ./app.jar
# Spring Boot Layered JARs: if using layered JARs, replace the COPY above
# with the extract + copy approach for even better layer caching:
# RUN java -Djarmode=layertools -jar app.jar extract
# COPY --from=build /app/dependencies/ ./
# COPY --from=build /app/spring-boot-loader/ ./
# COPY --from=build /app/snapshot-dependencies/ ./
# COPY --from=build /app/application/ ./
USER appuser
EXPOSE 8080
# MaxRAMPercentage caps heap relative to the container memory limit
# (container-aware by default in JDK 21).
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
# No HEALTHCHECK: the JRE Alpine image does not include wget or curl.
# Kubernetes liveness/readiness probes (configured in deployment.yaml) handle
# health checking in AKS.
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"]
java.dockerignore 0.2 KB
target
build
.gradle
*.class
*.jar
*.war
!*.jar
.env
.env.*
.git
.gitignore
.dockerignore
Dockerfile
*.md
.vscode
.idea
*.iml
.settings
.project
.classpath
node.Dockerfile 3.0 KB
# =============================================================================
# Node.js Production Dockerfile
# =============================================================================
# Customize the following before use:
# - APP_NAME: Replace in comments as needed
# - PORT: Change EXPOSE port if not 3000
# - ENTRY_POINT: Change the final CMD to your main file (e.g. dist/main.js)
# - BUILD_CMD: Adjust "npm run build --if-present" if your build script differs
#
# Package manager support:
# - npm: This file is configured for npm by default
# - yarn: Replace "npm ci" with "yarn install --frozen-lockfile"
# Replace "package-lock.json" with "yarn.lock"
# - pnpm: Replace "npm ci" with "corepack enable && pnpm install --frozen-lockfile"
# Replace "package-lock.json" with "pnpm-lock.yaml"
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Build
# ---------------------------------------------------------------------------
# Base: current Node LTS, Alpine variant. See references/base-images.md.
FROM node:<LATEST_STABLE_NODE>-alpine AS build
WORKDIR /app
# Layer caching: copy dependency manifests first so the install layer is
# only rebuilt when dependencies change, not on every source edit.
COPY package.json package-lock.json ./
RUN npm ci
# Copy the rest of the source and build
COPY . .
RUN npm run build --if-present
# Guard: verify build output exists at expected location
RUN test -d /app/dist || (echo "ERROR: Build output directory '/app/dist' not found." && echo "Your build script did not produce output in the 'dist/' directory." && echo "Update the 'COPY --from=build /app/dist ./dist' line in the runtime stage" && echo "to match your build script's output directory (e.g., 'build/', 'out/', 'public/')." && exit 1)
# Remove dev dependencies to slim down the production node_modules
RUN npm prune --omit=dev
# ---------------------------------------------------------------------------
# Stage 2: Runtime
# ---------------------------------------------------------------------------
FROM node:<LATEST_STABLE_NODE>-alpine
# Security: install dumb-init so Node runs as PID > 1 and signals propagate
# correctly β avoids zombie processes inside the container.
RUN apk add --no-cache dumb-init
WORKDIR /app
# Copy only production artifacts from the build stage.
# If your build script outputs to a different directory (e.g. build/ or out/),
# update the /app/dist path below to match.
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
# AKS Deployment Safeguards DS004: never run as root.
# The "node" user (uid 1000) is built into the node-alpine image.
USER node
EXPOSE 3000
# HEALTHCHECK is omitted β Kubernetes liveness/readiness probes handle health
# checks in AKS. See deployment.yaml for probe configuration.
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/main.js"]
node.dockerignore 0.2 KB
node_modules
npm-debug.log*
.npm
.env
.env.*
dist
build
.git
.gitignore
.dockerignore
Dockerfile
*.md
.vscode
.idea
coverage
.nyc_output
tests
__tests__
*.test.js
*.spec.js
python.Dockerfile 2.5 KB
# =============================================================================
# Python Production Dockerfile
# =============================================================================
# Customize the following before use:
# - APP_MODULE: Change the uvicorn target (e.g. "app.main:app" for FastAPI,
# "myproject.wsgi:application" for Django with gunicorn)
# - PORT: Change EXPOSE port if not 8000
# - DEPS FILE: If using Poetry, replace requirements.txt steps with
# "poetry export -f requirements.txt" in the build stage
# - ENTRY_POINT: Adjust the final CMD for your framework (gunicorn, uvicorn,
# flask run, etc.)
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Build
# ---------------------------------------------------------------------------
# Base: latest stable Python, Debian-slim (NOT Alpine β musl breaks many C extensions). See references/base-images.md.
FROM python:<LATEST_STABLE_PYTHON>-slim AS build
WORKDIR /app
# Create a virtual environment so we can copy it cleanly to the runtime stage
RUN python -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
# Layer caching: install dependencies before copying source
COPY requirements.txt ./
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt
# Copy application source
COPY . .
# If you have a build step (e.g. Django collectstatic), run it here:
# RUN python manage.py collectstatic --noinput
# ---------------------------------------------------------------------------
# Stage 2: Runtime
# ---------------------------------------------------------------------------
FROM python:<LATEST_STABLE_PYTHON>-slim
WORKDIR /app
# AKS Deployment Safeguards DS004: create and switch to a non-root user
RUN groupadd --gid 1000 appuser \
&& useradd --uid 1000 --gid appuser --shell /bin/sh --create-home appuser
# Copy the virtual environment and application source from the build stage
COPY --from=build --chown=appuser:appuser /app /app
ENV PATH="/app/venv/bin:$PATH" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
USER appuser
EXPOSE 8000
# HEALTHCHECK is omitted β Kubernetes liveness/readiness probes handle health
# checks in AKS. Adding a Dockerfile HEALTHCHECK would require installing curl
# in the runtime image, increasing size and attack surface.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
python.dockerignore 0.2 KB
__pycache__
*.pyc
*.pyo
*.egg-info
dist
build
.eggs
.env
.env.*
.venv
venv
env
.git
.gitignore
.dockerignore
Dockerfile
*.md
.vscode
.idea
.pytest_cache
.mypy_cache
.ruff_cache
htmlcov
.coverage
tests
rust.Dockerfile 3.1 KB
# =============================================================================
# Rust Production Dockerfile
# =============================================================================
# Customize the following before use:
# - APP_NAME: Replace "app" with your binary name from Cargo.toml
# - PORT: Change EXPOSE port if not 8080
#
# Notes:
# - The dependency-caching trick creates a dummy main.rs, builds
# dependencies, then replaces it with real source β this avoids
# rebuilding all deps on every source change
# - The final image uses distroless/cc which includes libgcc/libstdc++
# needed by the default Rust allocator; if you use musl
# (--target x86_64-unknown-linux-musl) switch to distroless/static
# - For workspace builds, copy the whole workspace in one shot and adjust
# the binary path in the final COPY
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Build
# ---------------------------------------------------------------------------
# Base: current stable Rust, Debian-slim. See references/base-images.md.
FROM rust:<LATEST_STABLE_RUST>-slim AS build
WORKDIR /app
# Install build dependencies (if any native libs are needed, add them here)
RUN apt-get update \
&& apt-get install -y --no-install-recommends pkg-config libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Layer caching: build dependencies separately from application code.
# 1. Copy only the manifests and create a dummy main to compile deps.
COPY Cargo.toml Cargo.lock ./
RUN mkdir src \
&& echo 'fn main() { println!("placeholder"); }' > src/main.rs \
&& cargo build --release \
&& echo "IMPORTANT: Update 'app' below to match your [[bin]] name in Cargo.toml." \
&& echo "If the name doesn't match, this cache trick will silently fail." \
&& rm -rf src target/release/deps/app* target/release/app*
# 2. Copy real source and build the actual binary.
COPY src ./src
RUN cargo build --release
# Verify binary exists with expected name
RUN test -f /app/target/release/app || (echo "ERROR: Binary 'app' not found at /app/target/release/app"; echo "The binary name in Cargo.toml must be 'app'."; echo "Update [[bin]] section in Cargo.toml to set name = \"app\""; echo "Also verify the COPY step above uses the correct binary name."; exit 1)
# ---------------------------------------------------------------------------
# Stage 2: Runtime
# ---------------------------------------------------------------------------
FROM gcr.io/distroless/cc-debian12
WORKDIR /app
# Update source path if your Cargo.toml binary name differs from "app"
COPY --from=build /app/target/release/app /app/app
# AKS Deployment Safeguards DS004: run as non-root.
# 65534 is the "nobody" user in distroless images.
USER 65534
EXPOSE 8080
# Distroless has no shell, curl, or wget. Kubernetes liveness/readiness probes
# (configured in deployment.yaml) handle health checking in AKS.
# For local Docker usage, consider adding a /healthz handler and using a
# statically-compiled health check binary.
# Update "/app/app" if your binary name differs
ENTRYPOINT ["/app/app"]
rust.dockerignore 0.1 KB
target
*.rs.bk
.env
.env.*
.git
.gitignore
.dockerignore
Dockerfile
*.md
.vscode
.idea
deploy.yml 7.4 KB
# GitHub Actions workflow: Deploy to AKS
#
# This workflow builds a container image, pushes it to Azure Container Registry,
# and deploys it to an Azure Kubernetes Service cluster.
#
# Authentication uses OIDC federation (workload identity) β no stored passwords.
# Required GitHub secrets: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID
#
# Placeholders to replace (uses __DOUBLE_UNDERSCORE__ style; K8s templates use angle-bracket style):
# __ACR_NAME__ β Azure Container Registry name (e.g. myappacr)
# __AKS_CLUSTER__ β AKS cluster name (e.g. myapp-aks)
# __RG_NAME__ β Azure resource group containing ACR and AKS
# __APP_NAME__ β Application / deployment name in Kubernetes
# __NAMESPACE__ β Kubernetes namespace to deploy into
name: Deploy to AKS
on:
# Trigger on push to main branch (app code changes only)
push:
branches:
- main
paths-ignore:
- 'docs/**'
- '*.md'
- '.github/**'
- '.vscode/**'
# Allow manual trigger from the Actions tab
workflow_dispatch:
# OIDC federation requires these permissions so GitHub can issue
# an ID token that Microsoft Entra ID will accept.
permissions:
id-token: write # Required for requesting the JWT
contents: read # Required for actions/checkout
# Prevent parallel deployments on the same branch.
# Uses workflow + ref so staging and production runs can proceed independently.
# cancel-in-progress: false ensures the running deploy finishes
# before the queued deploy starts (avoids mid-rollout conflicts).
# Note: env context is not available here β use github or vars contexts only.
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
env:
ACR_NAME: __ACR_NAME__
AKS_CLUSTER: __AKS_CLUSTER__
RESOURCE_GROUP: __RG_NAME__
APP_NAME: __APP_NAME__
NAMESPACE: __NAMESPACE__
defaults:
run:
shell: bash
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
# -----------------------------------------------------------
# Validate that no placeholders remain unreplaced
#
# Checks for __PLACEHOLDER__ and <placeholder> patterns in
# env vars and k8s/ directory. Fails fast with clear error
# if any found.
# -----------------------------------------------------------
- name: Validate β no unreplaced placeholders
run: |
PLACEHOLDERS_FOUND=0
# Check env variables
for VAR in ACR_NAME AKS_CLUSTER RESOURCE_GROUP APP_NAME NAMESPACE; do
VALUE="${!VAR}"
if [[ "$VALUE" =~ __[A-Z_]+__ ]]; then
echo "β Placeholder found in \$${VAR}: ${VALUE}"
PLACEHOLDERS_FOUND=1
fi
done
# Check k8s/ directory if it exists
if [ -d k8s ]; then
# __PLACEHOLDER__ style (env var style used in this workflow)
if grep -rq '__[A-Z_]\+__' k8s/; then
echo "β Placeholders found in k8s/ manifests:"
grep -rn '__[A-Z_]\+__' k8s/ || true
PLACEHOLDERS_FOUND=1
fi
# <placeholder> style (angle-bracket style used in K8s manifest templates).
# Exclude <image>: it is intentionally left in place here and replaced
# with the SHA-tagged image in the "Substitute image tag" step below.
if grep -rnP '<[a-z][a-z0-9-]*>' k8s/ | grep -vq '<image>'; then
echo "β Angle-bracket placeholders found in k8s/ manifests:"
grep -rnP '<[a-z][a-z0-9-]*>' k8s/ | grep -v '<image>' || true
PLACEHOLDERS_FOUND=1
fi
fi
if [ $PLACEHOLDERS_FOUND -eq 1 ]; then
echo ""
echo "β οΈ Workflow failed: unreplaced placeholders detected."
echo "Replace the following in your deploy.yml:"
echo " - __ACR_NAME__ β Your Container Registry name"
echo " - __AKS_CLUSTER__ β Your AKS cluster name"
echo " - __RG_NAME__ β Your resource group name"
echo " - __APP_NAME__ β Your application name"
echo " - __NAMESPACE__ β Your Kubernetes namespace"
exit 1
fi
echo "β All placeholders replaced"
# -----------------------------------------------------------
# Authenticate to Azure using OIDC (workload identity)
#
# This exchanges the GitHub-issued OIDC token for an Azure
# access token β no client secret required.
# -----------------------------------------------------------
- name: Azure Login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# -----------------------------------------------------------
# Build container image and push to ACR
#
# `az acr build` runs the Docker build remotely on ACR,
# so no local Docker daemon is needed. The image is tagged
# with the commit SHA for traceability.
# -----------------------------------------------------------
- name: Build and push image to ACR
run: |
az acr build \
--registry ${{ env.ACR_NAME }} \
--image ${{ env.APP_NAME }}:${{ github.sha }} \
.
- name: Set AKS context
uses: azure/aks-set-context@v4
with:
resource-group: ${{ env.RESOURCE_GROUP }}
cluster-name: ${{ env.AKS_CLUSTER }}
# -----------------------------------------------------------
# Deploy to AKS
#
# Applies all K8s resources (with substituted image tag),
# then waits for the rollout to complete successfully.
# Sets a step output flag used to gate the rollback step.
# -----------------------------------------------------------
- name: Substitute image tag in manifests
env:
IMAGE: ${{ env.ACR_NAME }}.azurecr.io/${{ env.APP_NAME }}:${{ github.sha }}
run: |
if [ ! -d k8s ]; then
echo "β k8s/ directory not found β cannot deploy without manifests"
exit 1
fi
# Use xargs to preserve sed exit codes (find|while swallows them)
find k8s -name "*.yaml" -o -name "*.yml" \
| xargs -I{} sed -i "s|<image>|${IMAGE}|g" "{}"
echo "β Image tag substituted in all manifests"
- name: Deploy to AKS
id: deploy
run: |
# Ensure the namespace exists before applying manifests
kubectl create namespace ${{ env.NAMESPACE }} --dry-run=client -o yaml \
| kubectl apply -f -
kubectl apply -f k8s/ --namespace ${{ env.NAMESPACE }}
kubectl rollout status deployment/${{ env.APP_NAME }} \
--namespace ${{ env.NAMESPACE }} \
--timeout=300s
# Signal that the deployment was applied β used to gate rollback
echo "deployed=true" >> "$GITHUB_OUTPUT"
- name: Rollback on failure
if: failure() && steps.deploy.outputs.deployed == 'true'
run: |
kubectl rollout undo deployment/${{ env.APP_NAME }} \
--namespace ${{ env.NAMESPACE }}
kubectl rollout status deployment/${{ env.APP_NAME }} \
--namespace ${{ env.NAMESPACE }} \
--timeout=120s
echo "β οΈ Rolled back to previous revision"
configmap.yaml 1.0 KB
# =============================================================================
# Kubernetes ConfigMap Template β AKS Deploy Skill
# =============================================================================
# Stores non-sensitive configuration data as key-value pairs. Values are
# injected into pods as environment variables via envFrom or env/valueFrom.
#
# Do NOT store secrets here β use Azure Key Vault + Workload Identity instead.
#
# REPLACE: <app-name> β your application name (e.g., order-api)
# REPLACE: <namespace> β target namespace (e.g., production)
# =============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: <app-name>-config
namespace: <namespace>
labels:
app: <app-name>
data: {}
# Add application configuration as key-value pairs. Remove the `{}` above
# when you add real entries (a populated `data:` map cannot also be `{}`).
# Example:
# LOG_LEVEL: "info"
# ASPNETCORE_ENVIRONMENT: "Production"
# SPRING_PROFILES_ACTIVE: "prod"
deployment.yaml 3.4 KB
# Kubernetes Deployment Template β AKS Deploy Skill
# Satisfies Deployment Safeguard rules DS001βDS013. Replace <placeholder> values before applying.
apiVersion: apps/v1
kind: Deployment
metadata:
name: <app-name>
namespace: <namespace>
labels:
app: <app-name>
spec:
# DS010: Minimum 2 replicas for high availability.
# If HPA is enabled, remove this field or set it to the HPA minReplicas value
# to prevent kubectl apply from resetting the replica count on each deploy.
replicas: 2
selector:
matchLabels:
app: <app-name>
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: <app-name>
# Workload Identity: enables the mutating webhook to inject
# AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_FEDERATED_TOKEN_FILE
azure.workload.identity/use: "true"
spec:
serviceAccountName: <app-name>
# DS013: Do not auto-mount the default ServiceAccount token.
# Workload Identity uses a separate projected volume managed by its webhook.
automountServiceAccountToken: false
# DS004 (pod-level): Run as non-root
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: <app-name>
# DS009: Always use an explicit tag β never :latest or bare image
image: <image>
ports:
- name: http
containerPort: <port>
protocol: TCP
# DS001: Resource requests AND limits for cpu and memory
resources:
requests:
cpu: "<cpu-request>"
memory: "<memory-request>"
limits:
cpu: "<cpu-limit>"
memory: "<memory-limit>"
# DS002: Liveness probe
livenessProbe:
httpGet:
path: <health-path>
port: <port>
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
# DS003: Readiness probe
readinessProbe:
httpGet:
path: <ready-path>
port: <port>
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
# Startup probe β uncomment for slow-start frameworks (Java/Spring Boot,
# .NET with heavy DI). Prevents the liveness probe from killing the pod
# before it finishes initializing. The pod has up to 30 * 10s = 300s to start.
# startupProbe:
# httpGet:
# path: <health-path>
# port: <port>
# periodSeconds: 10
# failureThreshold: 30
# DS004, DS008, DS011, DS012
securityContext:
runAsNonRoot: true
privileged: false
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
# If the app needs to write to specific paths (logs, tmp, cache),
# mount emptyDir volumes below instead of disabling readOnlyRootFilesystem.
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
gateway.yaml 1.5 KB
# =============================================================================
# Gateway API β Gateway Resource Template β AKS Deploy Skill
# =============================================================================
# Use this template for AKS clusters with Istio Gateway API enabled
# (appRoutingIstio.mode: Enabled). This applies to both AKS Automatic and Standard.
#
# For clusters using the default Web App Routing add-on, use ingress.yaml instead.
#
# REPLACE: <gateway-name> β name for the gateway (e.g., app-gateway)
# REPLACE: <namespace> β target namespace (e.g., production)
# REPLACE: <hostname> β FQDN for the listener (e.g., api.example.com)
# =============================================================================
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: <gateway-name>
namespace: <namespace>
labels:
app: <gateway-name>
spec:
# Istio gateway controller β available on both AKS Automatic and Standard when enabled
gatewayClassName: istio
listeners:
- name: http
protocol: HTTP
port: 80
hostname: "<hostname>"
allowedRoutes:
namespaces:
from: Same
# Uncomment for TLS β requires a Secret with the certificate
# - name: https
# protocol: HTTPS
# port: 443
# hostname: "<hostname>"
# tls:
# mode: Terminate
# certificateRefs:
# - kind: Secret
# name: <tls-secret-name>
# allowedRoutes:
# namespaces:
# from: Same
hpa.yaml 1.4 KB
# =============================================================================
# HorizontalPodAutoscaler Template β AKS Deploy Skill
# =============================================================================
# Scales the Deployment between min and max replicas based on CPU utilization.
# Minimum of 2 replicas ensures HA even at low load (aligns with DS010).
#
# REPLACE: <app-name> β your application name (e.g., order-api)
# REPLACE: <namespace> β target namespace (e.g., production)
# REPLACE: <min> β minimum replicas (default: 2, must be >= 2 for DS010)
# REPLACE: <max> β maximum replicas (e.g., 10)
# =============================================================================
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: <app-name>
namespace: <namespace>
labels:
app: <app-name>
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: <app-name>
minReplicas: <min>
maxReplicas: <max>
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Pods
value: 2
periodSeconds: 60
httproute.yaml 1.3 KB
# =============================================================================
# Gateway API β HTTPRoute Template β AKS Deploy Skill
# =============================================================================
# Routes HTTP traffic from a Gateway to a backend Service.
# Use this together with gateway.yaml on clusters with Istio Gateway API enabled.
#
# REPLACE: <app-name> β your application name (e.g., order-api)
# REPLACE: <namespace> β target namespace (e.g., production)
# REPLACE: <gateway-name> β name of the Gateway resource (e.g., app-gateway)
# REPLACE: <hostname> β FQDN matching the Gateway listener (e.g., api.example.com)
# REPLACE: <path-prefix> β URL path prefix to match (e.g., /)
# REPLACE: <service-port> β port on the backend Service (e.g., 80)
# =============================================================================
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: <app-name>
namespace: <namespace>
labels:
app: <app-name>
spec:
parentRefs:
- name: <gateway-name>
namespace: <namespace>
hostnames:
- "<hostname>"
rules:
- matches:
- path:
type: PathPrefix
value: "<path-prefix>"
backendRefs:
- name: <app-name>
port: <service-port>
kind: Service
ingress.yaml 1.9 KB
# =============================================================================
# Kubernetes Ingress Template β AKS Deploy Skill
# =============================================================================
# Use this template for AKS clusters with the Web App Routing add-on
# This is the default for both AKS Automatic and AKS Standard.
# For clusters with Istio Gateway API enabled, use gateway.yaml + httproute.yaml instead.
#
# REPLACE: <app-name> β your application name (e.g., order-api)
# REPLACE: <namespace> β target namespace (e.g., production)
# REPLACE: <path> β URL path (e.g., /)
# REPLACE: <service-port> β port on the backend Service (e.g., 80)
# NOTE: <hostname> is in the commented host-based rule β fill it in when DNS is configured
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <app-name>
namespace: <namespace>
labels:
app: <app-name>
spec:
ingressClassName: webapprouting.kubernetes.azure.com
# Uncomment for TLS β requires a Secret with the certificate
# tls:
# - hosts:
# - <hostname>
# secretName: <tls-secret-name>
rules:
# Initial deploy (no custom domain) β traffic routes to the external IP directly.
# Once DNS is configured, replace this rule with the host-based variant below.
- http:
paths:
- path: "<path>"
pathType: Prefix
backend:
service:
name: <app-name>
port:
number: <service-port>
# Host-based rule β uncomment and replace the rule above once DNS is configured:
# - host: "<hostname>"
# http:
# paths:
# - path: "<path>"
# pathType: Prefix
# backend:
# service:
# name: <app-name>
# port:
# number: <service-port>
namespace.yaml 0.7 KB
# =============================================================================
# Kubernetes Namespace Template β AKS Deploy Skill
# =============================================================================
# Creates an isolated namespace for the application workload. Using a dedicated
# namespace (rather than "default") improves resource organization, access
# control, and makes cleanup easier (delete the namespace to remove everything).
#
# REPLACE: <namespace> β target namespace (e.g., myapp, production)
# =============================================================================
apiVersion: v1
kind: Namespace
metadata:
name: <namespace>
labels:
app.kubernetes.io/managed-by: azure-kubernetes-app-deploy
networkpolicy.yaml 1.6 KB
# =============================================================================
# Kubernetes NetworkPolicy Template β AKS Deploy Skill
# =============================================================================
# Restricts ingress to the application pod so only the ingress controller
# (or gateway) namespace can reach it. Denies all other inbound traffic.
#
# REPLACE: <app-name> β your application name (e.g., order-api)
# REPLACE: <namespace> β target namespace (e.g., production)
# REPLACE: <ingress-namespace> β namespace of the ingress controller
# AKS Web App Routing: app-routing-system
# Istio Gateway: aks-istio-ingress
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: <app-name>-allow-ingress
namespace: <namespace>
labels:
app: <app-name>
spec:
podSelector:
matchLabels:
app: <app-name>
policyTypes:
- Ingress
# Uncomment to also restrict egress (recommended for production):
# - Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: <ingress-namespace>
# Uncomment and customize to restrict egress (e.g., allow only DNS + database):
# egress:
# - ports:
# - port: 53
# protocol: UDP
# - port: 53
# protocol: TCP
# - to:
# - namespaceSelector:
# matchLabels:
# kubernetes.io/metadata.name: <database-namespace>
pdb.yaml 0.7 KB
# =============================================================================
# PodDisruptionBudget Template β AKS Deploy Skill
# =============================================================================
# Ensures at least one pod remains available during voluntary disruptions
# (node drains, cluster upgrades, spot evictions).
#
# REPLACE: <app-name> β your application name (e.g., order-api)
# REPLACE: <namespace> β target namespace (e.g., production)
# =============================================================================
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: <app-name>
namespace: <namespace>
labels:
app: <app-name>
spec:
minAvailable: 1
selector:
matchLabels:
app: <app-name>
service.yaml 0.8 KB
# =============================================================================
# Kubernetes Service Template β AKS Deploy Skill
# =============================================================================
# ClusterIP Service that routes traffic to application pods.
#
# REPLACE: <app-name> β your application name (e.g., order-api)
# REPLACE: <namespace> β target namespace (e.g., production)
# REPLACE: <port> β service port (e.g., 80)
# REPLACE: <target-port> β container port (e.g., 8080)
# =============================================================================
apiVersion: v1
kind: Service
metadata:
name: <app-name>
namespace: <namespace>
labels:
app: <app-name>
spec:
type: ClusterIP
selector:
app: <app-name>
ports:
- name: http
port: <port>
targetPort: <target-port>
protocol: TCP
serviceaccount.yaml 1.4 KB
# =============================================================================
# ServiceAccount Template β AKS Deploy Skill
# =============================================================================
# Kubernetes ServiceAccount with Azure Workload Identity annotation.
# The annotation links this SA to an Azure Managed Identity via OIDC federation.
#
# Prerequisites:
# 1. A User-Assigned Managed Identity exists in Azure
# 2. A Federated Identity Credential is configured with:
# - Issuer: <AKS cluster OIDC issuer URL>
# - Subject: system:serviceaccount:<namespace>:<app-name>
# - Audience: api://AzureADTokenExchange
#
# REPLACE: <app-name> β your application name (e.g., order-api)
# REPLACE: <namespace> β target namespace (e.g., production)
# REPLACE: <azure-client-id> β client ID of the Managed Identity
# =============================================================================
apiVersion: v1
kind: ServiceAccount
metadata:
name: <app-name>
namespace: <namespace>
labels:
app: <app-name>
annotations:
# Workload Identity: maps this ServiceAccount to an Azure Managed Identity.
# The Workload Identity webhook reads this annotation and injects
# AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_FEDERATED_TOKEN_FILE
# into any pod that references this ServiceAccount AND has the label
# azure.workload.identity/use: "true".
azure.workload.identity/client-id: "<azure-client-id>"
architecture-diagram.md 1.6 KB
# Architecture Diagram Template
Render this mermaid diagram in the terminal, replacing all `{{PLACEHOLDER}}` tokens with detected values from Section 1 (Detection) and the chosen backing services.
## Diagram
~~~mermaid
flowchart LR
Users([Users]) -->|HTTPS| GW
subgraph AKS["AKS Cluster: {{AKS_CLUSTER_NAME}}"]
direction LR
GW[{{INGRESS_TYPE}}] --> SVC[Service\n{{APP_NAME}}:{{PORT}}]
SVC --> DEP[Deployment\n{{REPLICA_COUNT}} replicas]
end
DEP -.->|Workload Identity| MI[Managed Identity\n{{IDENTITY_NAME}}]
ACR[ACR\n{{ACR_NAME}}.azurecr.io] -->|pull| AKS
CICD[GitHub Actions] -->|push| ACR
%% Backing services β include only those in the architecture contract
%% Delete lines for services not selected
DEP -.->|Workload Identity| PG[(PostgreSQL\n{{PG_SERVER_NAME}})]
DEP -.->|Workload Identity| REDIS[(Redis\n{{REDIS_NAME}})]
DEP -.->|Workload Identity| KV[Key Vault\n{{KV_NAME}}]
MON[Log Analytics\n{{LAW_NAME}}] -..- AKS
style AKS fill:#e8f5e9,stroke:#107C10,stroke-width:2px
style ACR fill:#e3f2fd,stroke:#0078D4
style PG fill:#fff3e0,stroke:#f57c00
style REDIS fill:#fce4ec,stroke:#c62828
style KV fill:#f3e5f5,stroke:#7b1fa2
style MON fill:#f5f5f5,stroke:#757575
~~~
## Rendering instructions
Output this diagram as a fenced mermaid code block in the terminal. The developer will see it rendered if their terminal/tool supports mermaid, or as readable text if not.
After the diagram, output a cost estimate table listing each Azure resource with its SKU/tier and approximate monthly cost. Use your knowledge of Azure pricing to provide estimates.
summary-dashboard.md 1.5 KB
# Deployment Summary Template
After successful deployment, render this summary in the terminal.
## Template
```
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DEPLOYMENT SUCCESSFUL β
β {{APP_NAME}} is live at {{APP_URL}} β
β Deployed: {{DEPLOY_TIMESTAMP}} β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
### Azure Resources
| Resource | Type | Name | Portal Link |
|----------|------|------|-------------|
| Resource Group | resourceGroups | {{RG_NAME}} | `https://portal.azure.com/...` |
| AKS Cluster | managedClusters | {{AKS_NAME}} | `https://portal.azure.com/...` |
| Container Registry | registries | {{ACR_NAME}} | `https://portal.azure.com/...` |
| {{BACKING_SERVICE}} | {{TYPE}} | {{NAME}} | `https://portal.azure.com/...` |
Replace each portal link with the full URL using the subscription ID, resource group, and resource name.
### Files Created / Modified
List all files generated during the workflow with `+` for created and `~` for modified.
### Monthly Cost Estimate
List each Azure resource with its SKU/tier and approximate monthly cost.
### Next Steps
1. **Custom Domain** β Point DNS to external IP, update Gateway/Ingress
2. **TLS Certificate** β Enable HTTPS via cert-manager or Azure-managed TLS
3. **Monitoring Dashboard** β Set up Azure Monitor / Prometheus + Grafana
4. **Scaling** β Tune HPA min/max replicas and resource requests/limits
5. **CI/CD Trigger** β Push to default branch to trigger pipeline
SKILL.md 17.1 KB
---
name: azure-kubernetes-automatic-readiness
license: MIT
metadata:
author: Microsoft
version: "1.1.0"
description: "Assess Kubernetes workloads and cluster configuration for AKS Automatic compatibility. Identifies incompatibilities, generates fixes, and guides migration from AKS Standard to AKS Automatic. WHEN: migrate to AKS Automatic, check AKS Automatic readiness, validate manifests for Automatic, assess cluster for Automatic compatibility, fix deployment for Automatic compatibility, identify AKS Automatic migration blockers, is my cluster ready for AKS Automatic."
---
# AKS Automatic Readiness Assessment
> **AUTHORITATIVE GUIDANCE β MANDATORY COMPLIANCE**
>
> This skill assesses existing AKS clusters or local manifests for AKS Automatic compatibility.
> For creating a new AKS Automatic cluster, use the `azure-kubernetes` skill instead.
> See [constraint spec](./references/constraint-spec-v1.yaml) for all safeguard rules, [common fixes](./references/common-fixes.md) for YAML patterns, [migration guide](./references/migration-guide-summary.md) for end-to-end steps, and [MCP integration](./references/mcp-integration.md) for tool details and fallback handling.
You are an AKS Automatic compatibility assessment agent. Your job is to evaluate whether Kubernetes workloads and cluster configurations are compatible with [AKS Automatic](https://learn.microsoft.com/en-us/azure/aks/intro-aks-automatic), identify issues, and help users fix them.
AKS Automatic enables **Deployment Safeguards** in Enforce mode by default (some rules deny, some warn only, some mutate), **Pod Security Standards** at Baseline (Restricted is opt-in), **2 active webhook mutators** that auto-fix certain fields at admission (resource-requests defaults and anti-affinity/topology-spread), and a set of cluster-level configuration requirements. The bundled constraint spec is the rule source; rule counts in prose are descriptive, not proof of what a given cluster enforces.
## Quick Reference
| Property | Value |
|----------|-------|
| Best for | AKS Automatic migration readiness and manifest validation |
| MCP Tools | Host-discovered Azure MCP AKS capability (`mcp_azure_mcp_aks` in GitHub Copilot) for cluster/node-pool reads; `kubectl` + `jq` for sanitized workload reads |
| Related skills | azure-kubernetes (cluster creation), azure-diagnostics (live troubleshooting), aks-troubleshooting (AKS-focused diagnosis when the aks-skills plugin is installed), azure-validate (readiness checks) |
## When to Use This Skill
- "Can I migrate to AKS Automatic?"
- "Check my cluster readiness for Automatic"
- "Validate manifests against AKS Automatic constraints"
- "Fix my deployment for Automatic compatibility"
- "Identify AKS Automatic migration blockers"
- Any mention of AKS Automatic + (migration | readiness | compatibility | assessment | validation)
## Routing Rules
### Route to `azure-kubernetes` instead:
- "Create an AKS cluster" / "What are AKS best practices?" / "How do I deploy to AKS?"
- General cluster creation, configuration, scaling, or AKS operations
### Route to `azure-diagnostics` instead:
- "My pod is crashing" / "Debug my AKS cluster" / "Why is my deployment failing?"
- Live troubleshooting, debugging, error diagnosis on a running cluster
## Guardrails β READ FIRST
1. **Read-only**: NEVER modify cluster state. Assessment is read-only. Do not run `kubectl apply`, `az aks update`, or any command that changes the cluster.
2. **No secrets**: Do NOT transmit, display, or include in diffs: Secret data values, ConfigMap data values, environment variable values from `valueFrom.secretKeyRef`, service account tokens, or connection strings.
3. **User approval for file changes**: Present every fix as a diff. The user must explicitly accept before you write to any file.
4. **Scope boundaries**: Route cluster creation/deletion questions β `azure-kubernetes` skill. Route live troubleshooting β `azure-diagnostics` skill.
## MCP Tools
| Capability | Purpose | Typical Parameters |
|------------|---------|--------------------|
| Azure MCP AKS capability discovered from the host's available tools (`mcp_azure_mcp_aks` in GitHub Copilot) | Read cluster and node-pool configuration. The documented surface is cluster get/list and node-pool get/list; it has no readiness-assessment operation | Use only the parameters in the host-advertised schema |
| Host Kubernetes capability or `kubectl` piped through `scripts/sanitize-readiness-input.jq` | Read allowlisted workload fields for local evaluation against the bundled constraint spec | Cluster context, resource kinds, namespaces |
## Workflow
### Step 1: Determine Scope
Ask the user what they want to assess:
**Option A β Cluster-connected assessment**
Use when the user has a connected cluster context (subscription + resource group + cluster name).
**Option B β Offline manifest validation**
Use when the user has local Kubernetes manifests, Helm charts, or Kustomize overlays in their workspace. Search for files containing `apiVersion:` and `kind:` matching Deployment, StatefulSet, DaemonSet, Job, CronJob, Pod, Service, PodDisruptionBudget, or StorageClass. For Helm charts, look for `Chart.yaml` and rendered templates under `templates/`.
**Option C β Single manifest check**
If the user pastes or points to a single YAML manifest, validate it directly without asking for scope.
### Step 2: Run Assessment
#### Cluster-Connected Mode
1. Discover the host's approved AKS metadata capabilities; use their host-assigned names and schemas. Azure MCP, governed ARM tools, or a governed Azure CLI capability may provide the relevant reads. A missing literal name is not proof of absence.
2. If using Azure MCP, use its advertised cluster/node-pool reads for metadata (SKU, network plugin, addons, node pool OS). The documented surface has no readiness-assessment operation β do not expect one ([Azure MCP AKS tools](https://learn.microsoft.com/azure/developer/azure-mcp-server/tools/azure-kubernetes)).
3. Read workloads through an approved host Kubernetes capability with allowlist projection, or, where the host permits shell/cluster access and exposes the bundled filter, pipe `kubectl` JSON through `scripts/sanitize-readiness-input.jq` (requires `jq`) before anything reaches the model. Never fetch `Secret`/ConfigMap resources or paste raw `kubectl -o json`. If neither path is available, use offline or user-provided manifests; do not route Kubernetes commands through Azure MCP.
4. Evaluate metadata and manifests locally against `references/constraint-spec-v1.yaml`.
5. Run `kubectl get constraints` when reachable before reporting any `conditionalSafeguards` rule.
```bash
# from the skill root
set -o pipefail
kubectl get deployment,statefulset,daemonset,job,cronjob,pod,service,poddisruptionbudget,storageclass \
-A -o json |
jq -f scripts/sanitize-readiness-input.jq
```
#### Fallback Chain
```
Cluster metadata:
1. Host-discovered Azure MCP AKS cluster/node-pool read capability
β no matching capability, operation absent, or access fails
2. Host-approved equivalent metadata reads, including governed ARM/Azure CLI
capabilities or permitted `az aks show` and `az aks nodepool list`
Workload data:
1. Approved host Kubernetes read with projection, or permitted sanitized `kubectl | jq`
β execution/cluster access unavailable, or jq/bundled filter missing
2. Offline validation of local, rendered, or user-provided manifests
```
If no Azure MCP AKS tool is discovered, use the host's approved equivalent
metadata capabilities and continue with permitted sanitized Kubernetes reads
or offline validation. A local or external MCP server is not a prerequisite.
Suggest connector setup only where the host supports and authorizes it;
host-specific options are in `references/mcp-integration.md`. Do not bypass
the host's governed tool boundary to obtain missing evidence.
#### Offline Mode
Load `references/constraint-spec-v1.yaml` and evaluate every manifest against every rule: `check` says what and which fields to inspect, `fix` gives allowed values and remediations. Suggest needed fixes.
Key checks:
**Per container** (containers, initContainers, ephemeralContainers):
- Resource requests/limits β `safeguard-container-resource-requests`
- Readiness and liveness probes β `safeguard-probes-configured` *(warning-only β not blocked at admission; treat as informational)*
- Image tag not `:latest` β `safeguard-images-no-latest`
- `securityContext.privileged` not true β `safeguard-no-privileged-containers`
- `capabilities.add` only adds allowed capabilities β `safeguard-container-capabilities`
- `seccompProfile` is RuntimeDefault/Localhost β `safeguard-allowed-seccomp-profiles`
- no `host` field in probes and lifecycle hooks β `safeguard-host-probes` *(advisory/unverified β not a documented Automatic block)*
**Per pod spec:**
- `hostPID`/`hostIPC` not true β `safeguard-block-host-namespaces` (incompatible)
- `hostNetwork`/`hostPort` not true β `safeguard-host-network-ports` (incompatible)
- No `hostPath` volumes β `safeguard-no-host-path-volumes` (incompatible)
**Conditional (`conditionalSafeguards` β report only when applicable, never as default blockers):**
- `allowPrivilegeEscalation`, run-as-non-root, allowed volume types β PSS Restricted opt-in only (Learn marks these "PSS Restricted Only")
- Windows `ContainerAdministrator` β Windows node pools only, when the constraint is active
**Per workload type:**
- Deployments/StatefulSets with replicas > 1: podAntiAffinity or topologySpreadConstraints β `safeguard-pod-enforce-antiaffinity`
- StorageClass: CSI provisioner (not in-tree) β `safeguard-csi-driver-storage-class`
### Severity Classification
| Severity | Meaning | Action |
|----------|---------|--------|
| `incompatible` | Fundamental architecture issue; cannot run on Automatic without redesign | Must fix before migration β flag prominently |
| `requiresChanges` | Manifest changes needed; will be denied at admission | Generate fix diffs |
| `autoFixed` | AKS Automatic will mutate this at admission; no user action needed | Informational β show what will change |
| `informational` | Warning-only, advisory, or not enforced on the default Automatic Baseline | Mention briefly; never list as a blocker |
| conditional (`enforcement: conditional`) | Enforced only under PSS Restricted, on Windows nodes, or when the cluster's active constraints include it | Report with its `appliesWhen` condition; confirm with `kubectl get constraints` when possible |
### Step 3: Present Findings
Always start with the summary:
```
## AKS Automatic Readiness Assessment
| Status | Count |
|--------|-------|
| β
Compatible | X workloads |
| β οΈ Requires changes | Y workloads |
| β Incompatible | Z workloads |
| π§ Auto-fixed by Automatic | W workloads |
| ποΈ Cluster config issues | N issues |
```
Grouping: β€ 10 issues β list individually; > 10 β group by constraint ID. Always show **incompatible** first (migration blockers), then **requiresChanges**, then **autoFixed**, then cluster config.
Per-issue format:
```
### β [constraint-id] β Short description
**Severity:** incompatible | requiresChanges
**Affected:** namespace/resource-name (Kind)
**Current:** <what the manifest has>
**Required:** <what AKS Automatic requires>
**Fix:** <remediation summary>
**Docs:** <documentation URL>
```
### Step 4: Offer Fixes
**Deterministic fixes** (the constraint rule and `references/common-fixes.md` define a direct field transformation β generate a YAML diff):
- `safeguard-container-resource-requests` β add `resources.requests`
- `safeguard-container-capabilities` β remove `capabilities.add`
- `safeguard-allowed-seccomp-profiles` β patch only when `seccompProfile.type: Unconfined` is present
- `safeguard-no-privilege-escalation` β set `allowPrivilegeEscalation: false` (only when the conditional rule applies)
- `safeguard-enforce-apparmor` β add AppArmor annotation
- `safeguard-csi-driver-storage-class` β replace in-tree provisioner
Use patterns in `references/common-fixes.md` and generate a before/after diff. Starting resource values use safe defaults β VPA (enabled on Automatic) will auto-tune after deployment.
**Context-dependent fixes** (the constraint spec's `fix` guidance requires application-specific input):
- `safeguard-images-no-latest` β correct tag is user- and release-specific; ask the user: _"What specific version tag or SHA digest should I pin this image to?"_ Do not guess
- `safeguard-pod-enforce-antiaffinity` β needs app labels for selector
- `safeguard-no-host-path-volumes` β replacement depends on what hostPath is used for
- `safeguard-block-host-namespaces` β may require architecture redesign
- `safeguard-host-network-ports` β needs alternative networking approach
For incompatible findings (e.g., hostPath volumes), explain the issue and propose alternatives. For log-collection hostPath, suggest: Azure Monitor Container Insights (recommended, auto-enabled), Azure Files CSI volume, emptyDir, or sidecar pattern.
**Fix application flow:**
1. Generate the fix as a YAML diff
2. Show the diff with explanation
3. Wait for explicit approval: "apply", "edit", or "skip"
4. On approval, apply the change to the file
5. Move to the next finding
If the user says "fix all" or "apply all deterministic fixes", first generate a single combined diff containing only the constraint rules with direct, context-independent transformations, show that combined diff with an explanation, and wait for one explicit approval before applying any writes. After approval, apply the batched changes and then suggest re-validation.
### Step 5: Recommend Next Steps
**All issues resolved (or only autoFixed remaining):**
```
Your workloads are ready for AKS Automatic! Next steps:
1. Review auto-fixed items β AKS Automatic will mutate N fields at admission.
2. Apply cluster configuration changes (see cluster config issues above).
3. Create the AKS Automatic target cluster and move workloads β follow the migration guide.
4. Verify β after migration, check all workloads are running and healthy.
```
There is no documented in-place Standard β Automatic SKU switch; migration to Automatic is a new target cluster plus a workload move (the documented in-place path is Automatic β Standard, `--sku base`). See `references/migration-guide-summary.md` for supported journeys and the full checklist.
**Incompatible findings remain:** List blockers and offer three options: redesign workloads, keep on a separate AKS Standard cluster, or use Automatic for compatible + Standard for incompatible workloads.
**Cluster config issues remain (Day-0 decisions):** API Server VNet Integration, node pool OS SKU (requires recreating system node pools), and ephemeral OS disks require a new cluster β redirect to `azure-kubernetes` skill for cluster creation help.
## Error Handling
| Error / Symptom | Likely Cause | Remediation |
|-----------------|--------------|-------------|
| No Azure MCP AKS capability in the host's tools | Host uses another approved surface or does not expose these reads | Use approved equivalent metadata reads, including governed ARM/Azure CLI capabilities, or continue offline. Suggest a connector only if the host supports and authorizes setup |
| Discovered AKS capability has no readiness operation | Expected β the documented surface is cluster/node-pool reads only | Collect sanitized manifests via `kubectl \| jq` and evaluate the bundled spec locally |
| Azure/Kubernetes read fails (401/403/404, no context) | Credentials, RBAC, scope, or wrong target | See `references/mcp-integration.md` (SRE Agent UAMI scope vs `az login` hosts); continue offline if unresolved |
| `jq` or bundled sanitizer unavailable | Host cannot execute the sanitized pipeline | Use rendered manifests or an approved projecting host read. Install tools only where host policy and user authorization permit; never send raw cluster JSON to the model |
| Helm chart uses Go templating β cannot evaluate | Template values not resolved | Ask for `helm template` output or values files |
| Constraint spec version mismatch | Skill bundles spec v1.2.0 | Note version in output; recommend re-running after spec update |
## Reference Files
| File | When to load |
|------|--------------|
| `references/constraint-spec-v1.yaml` | Always load for offline validation β all constraint IDs, severities, and fix patterns |
| `references/common-fixes.md` | When generating deterministic fixes β before/after YAML patterns |
| `references/migration-guide-summary.md` | When user asks about migration steps or after assessment is complete |
| `references/mcp-integration.md` | When discovering Azure MCP capabilities, wiring the sanitized `kubectl` read, or debugging the fallback chain |
| `scripts/sanitize-readiness-input.jq` | Allowlist projection for `kubectl -o json` output before it reaches the model (requires `jq`) |
> β οΈ **Warning:** This skill bundles **constraint spec v1.2.0** (rules dated 2026-03-15, reconciled 2026-09 against the Learn Deployment Safeguards page, Azure Policy initiative c047ea8e v3.0.0, and the AKS Automatic SKU migration article). It lists 23 customer-facing cluster constraints, 21 Baseline-level Deployment Safeguards rules (one advisory/unverified), 4 conditional Restricted/Windows rules, and 2 active mutators. Always note the spec version in assessment output and verify live enforcement with `kubectl get constraints` when a cluster is reachable.
common-fixes.md 7.2 KB
# Common Fix Patterns for AKS Automatic Compatibility
Loaded on demand when generating YAML fixes during assessment.
Maps to constraint IDs in `constraint-spec-v1.yaml`.
---
## `safeguard-container-resource-requests` β Add resource requests/limits
**Before:**
```yaml
containers:
- name: web
image: myapp:v1.0.0
```
**After:**
```yaml
containers:
- name: web
image: myapp:v1.0.0
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
```
> π‘ **Tip:** Use safe minimums as starting values. VPA (auto-enabled on AKS Automatic) will tune these after deployment based on actual usage.
---
## `safeguard-container-capabilities` β Drop all capabilities
**Before:**
```yaml
securityContext:
capabilities:
add: ["NET_ADMIN"]
```
**After:**
```yaml
securityContext:
capabilities:
drop: ["ALL"]
```
> β οΈ **Warning:** If the app genuinely requires `NET_ADMIN` or similar, it is **incompatible** with AKS Automatic. Do not silently drop β explain the incompatibility and suggest redesign.
---
## `safeguard-no-privilege-escalation` β Disable privilege escalation (conditional: PSS Restricted)
Apply when the target opts into PSS Restricted or the cluster's active constraints require it; on the default Automatic Baseline this is optional hardening.
**Before:**
```yaml
securityContext: {}
```
**After:**
```yaml
securityContext:
allowPrivilegeEscalation: false
```
---
## `safeguard-allowed-seccomp-profiles` β Add seccomp profile
**Before:**
```yaml
spec:
containers:
- name: web
```
**After:**
```yaml
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: web
```
---
## `safeguard-allowed-seccomp-profiles` β Remove 'Unconfined' seccomp profile
**Before:**
```yaml
spec:
securityContext:
seccompProfile:
type: Unconfined
containers:
- name: web
```
**After:**
```yaml
spec:
containers:
- name: web
```
---
## `safeguard-enforce-apparmor` β Add AppArmor annotation
**Before:**
```yaml
metadata:
name: my-deployment
```
**After:**
```yaml
metadata:
name: my-deployment
annotations:
container.apparmor.security.beta.kubernetes.io/web: runtime/default
```
> π‘ **Tip:** Replace `web` with the actual container name. Add one annotation per container.
---
## `safeguard-images-no-latest` β Pin image tag *(LLM-reasoned β ask user)*
**Before:**
```yaml
image: myapp:latest
```
**After:**
```yaml
image: myapp:v1.2.3 # β version confirmed with user
```
> β οΈ **Warning:** Do not guess the version. Ask the user: _"What specific version tag or SHA digest should I pin this image to?"_ If from a public registry, suggest checking Docker Hub or the registry for the latest stable tag.
---
## `safeguard-probes-configured` β Add probes *(best-practice recommendation β warning-only, not blocked at admission)*
**HTTP app (most common):**
```yaml
readinessProbe:
httpGet:
path: /healthz # β ask user for their health endpoint
port: 8080 # β ask user for port
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3
```
**TCP-only app (databases, Redis, etc.):**
```yaml
readinessProbe:
tcpSocket:
port: 6379 # β service port
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 15
periodSeconds: 20
```
**gRPC app:**
```yaml
readinessProbe:
grpc:
port: 50051
initialDelaySeconds: 5
periodSeconds: 10
```
---
## `safeguard-host-probes` β Remove host field in probes and lifecycle hooks
Advisory only: this rule is not part of the public Deployment Safeguards initiative (c047ea8e) and is not verified as an Automatic admission block. Present it as a recommendation, not a migration blocker.
**Before:**
```yaml
spec:
containers:
- name: my-container
image: nginx:v1.2.3
livenessProbe:
httpGet:
host: "my-host"
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3
```
**After:**
Remove the `host` field
Example:
```yaml
spec:
containers:
- name: my-container
image: nginx:v1.2.3
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3
```
---
## `safeguard-pod-enforce-antiaffinity` β Add topology spread *(LLM-reasoned β ask user for label)*
Ask user: _"What label key/value identifies your workload's pods?"_
```yaml
spec:
template:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: <app-label> # β from user
containers:
- name: web
```
---
## `safeguard-csi-driver-storage-class` β Migrate in-tree to CSI
**Before (Azure Disk in-tree):**
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-storage
provisioner: kubernetes.io/azure-disk
parameters:
skuName: Premium_LRS
reclaimPolicy: Delete
volumeBindingMode: Immediate
```
**After (Azure Disk CSI):**
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-storage
provisioner: disk.csi.azure.com
parameters:
skuName: Premium_LRS
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer # β preferred for zonal disks
```
| In-tree provisioner | CSI replacement |
|---|---|
| `kubernetes.io/azure-disk` | `disk.csi.azure.com` |
| `kubernetes.io/azure-file` | `file.csi.azure.com` |
---
## PodDisruptionBudget β Add missing PDB
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: <app-name>-pdb
namespace: <namespace>
spec:
maxUnavailable: 1
selector:
matchLabels:
app: <app-label>
```
## PodDisruptionBudget β Fix blocking `maxUnavailable: 0`
**Before:**
```yaml
spec:
maxUnavailable: 0
```
**After:**
```yaml
spec:
maxUnavailable: 1
```
> β οΈ **Warning:** `maxUnavailable: 0` completely blocks node drain during AKS Automatic upgrades. At least 1 pod must be allowed unavailable for upgrades to proceed.
---
## `safeguard-no-host-path-volumes` β Replace hostPath *(incompatible β suggest alternatives)*
| hostPath use case | Recommended replacement |
|---|---|
| Log collection (`/var/log`) | Azure Monitor Container Insights (auto-enabled on AKS Automatic) |
| Container runtime socket (`/var/run/docker.sock`) | Use the AKS Automatic node observability features β direct socket access not supported |
| Shared config files | `configMap` volume |
| Secrets / credentials | Kubernetes `secret` volume or Azure Key Vault CSI Driver |
| Ephemeral scratch space | `emptyDir` volume |
| Persistent app data | Azure Disk CSI via PVC (`disk.csi.azure.com`) |
| Shared file storage across pods | Azure Files CSI via PVC (`file.csi.azure.com`) |
**emptyDir example:**
```yaml
volumes:
- name: scratch
emptyDir: {}
```
**Azure Files CSI PVC example:**
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: logs-pvc
spec:
accessModes:
- ReadWriteMany
storageClassName: azurefile-csi
resources:
requests:
storage: 10Gi
```
constraint-spec-v1.yaml 24.3 KB
# AKS Automatic Compatibility Constraint Spec β Condensed Reference
# Version: 1.2.0 | AKS: 2026-03-15 rules, reconciled 2026-09 against:
# - Learn: Deployment Safeguards (Automatic = Baseline PSS + Enforce by default)
# - Azure Policy built-in initiative c047ea8e v3.0.0 (Azure/azure-policy)
# - Learn: AKS Automatic cluster SKU migration (supported journeys)
# This condensed version is optimized for LLM context.
# Rule counts in comments describe THIS file; they are not a claim about the
# exact number of policies active on any cluster. Verify live enforcement with
# `kubectl get constraints` when a cluster is reachable.
apiVersion: aks-automatic.azure.com/v1
kind: ConstraintSpecReference
metadata:
version: "1.2.0"
aksVersion: "2026-03-15"
policyInitiatives:
deploymentSafeguards: c047ea8e-9c78-49b2-958b-37e56d291a44
podSecurityBaseline: a8640138-9b0a-4a28-b8cb-1666c838647d
podSecurityRestricted: 42b8ef37-b724-4e24-bbc8-7a7708edfe00
# =============================================================================
# CLUSTER CONSTRAINTS (23 customer-facing)
# AKS-managed HOBO taint rules (CriticalAddonsOnly NoSchedule/NoExecute, hosted
# VM taint) are internal and intentionally omitted from customer assessment.
# =============================================================================
clusterConstraints:
# -- Addons --
- id: cluster-azure-policy-addon
severity: requiresChanges
field: addonProfiles.azurepolicy.enabled
required: true
fix: "az aks addon enable --addon azure-policy"
- id: cluster-keyvault-secrets-provider
severity: requiresChanges
field: addonProfiles.azureKeyvaultSecretsProvider.enabled
required:
enabled: true
enableSecretRotation: true
fix: "az aks addon enable --addon azure-keyvault-secrets-provider --enable-secret-rotation"
# -- Networking --
- id: cluster-api-server-vnet-integration
severity: requiresChanges
field: privateConnectProfile.enabled
required: true
fix: "az aks update --enable-apiserver-vnet-integration --apiserver-subnet-id <subnet-id>"
- id: cluster-azure-cni-overlay-cilium
severity: requiresChanges
field: networkPlugin/networkPluginMode/networkPolicy/ebpfDataplane
required: azure/overlay/cilium/cilium
fix: |
Step 1: az aks update --network-plugin-mode overlay --pod-cidr 192.168.0.0/16
Step 2: az aks update --network-dataplane cilium
Note: Irreversible. Disable NAP before Cilium update.
- id: cluster-standard-load-balancer
severity: requiresChanges
field: loadBalancerSku
required: standard
fix: "az aks update --load-balancer-sku standard (in-place upgrade from Basic supported)"
- id: cluster-nat-gateway-managed-vnet
severity: requiresChanges
condition: AKS-managed VNet only
field: outboundType
required: managedNATGateway
fix: "az aks update --outbound-type managedNATGateway"
# -- Upgrades --
- id: cluster-auto-upgrade
severity: requiresChanges
field: autoUpgradeProfile
required: upgradeChannel=stable, nodeOSUpgradeChannel=NodeImage
fix: "az aks update --auto-upgrade-channel stable --node-os-upgrade-channel NodeImage"
# -- Ingress --
- id: cluster-web-app-routing
severity: requiresChanges
field: ingressProfile.webAppRouting.enabled
required: true
fix: "az aks addon enable --addon web_application_routing"
# -- Identity --
- id: cluster-workload-identity-oidc
severity: requiresChanges
field: workloadIdentity.enabled + oidcProfile.enabled
required: true
fix: "az aks update --enable-oidc-issuer --enable-workload-identity"
- id: cluster-azure-rbac
severity: requiresChanges
field: aadProfile (managed + enableAzureRBAC)
required: true
fix: "az aks update --enable-aad --enable-azure-rbac"
- id: cluster-disable-local-accounts
severity: requiresChanges
field: disableLocalAccounts
required: true
fix: "az aks update --disable-local-accounts"
- id: cluster-system-assigned-managed-identity
severity: requiresChanges
condition: AKS-managed VNet only
field: identity.type
required: SystemAssigned
fix: "Day-0 decision for managed VNet clusters."
# -- Security --
- id: cluster-image-cleaner
severity: requiresChanges
field: securityProfile.imageCleaner.enabled
required: true
fix: "az aks update --enable-image-cleaner"
# -- Autoscaling --
- id: cluster-vpa
severity: requiresChanges
field: verticalPodAutoscaler
required: enabled=true, updateMode=Off
fix: "az aks update --enable-vpa"
- id: cluster-keda
severity: requiresChanges
field: keda.enabled
required: true
fix: "az aks update --enable-keda"
- id: cluster-node-auto-provisioning
severity: requiresChanges
field: nodeProvisioningProfile.mode
required: Auto
fix: "az aks update --node-provisioning-mode Auto"
# -- Governance --
- id: cluster-node-rg-readonly
severity: requiresChanges
field: nodeResourceGroupProfile.restrictionLevel
required: ReadOnly
fix: "Day-0 setting. May require new cluster."
# -- Node Pool (system pools) --
- id: pool-ephemeral-os-disk
severity: incompatible
appliesTo: system pools
field: storageProfile
required: Ephemeral
fix: "Day-0. Recreate system node pool."
- id: pool-availability-zones
severity: incompatible
appliesTo: system pools only
field: availabilityZones
required: "[1, 2, 3]"
fix: "Day-0. Recreate system pool in 3-AZ region. User pools not affected."
- id: pool-critical-addons-taint
severity: requiresChanges
appliesTo: system pools
field: taints
required: CriticalAddonsOnly=true:NoSchedule
fix: "az aks nodepool update --node-taints CriticalAddonsOnly=true:NoSchedule"
- id: pool-vmss-type
severity: incompatible
appliesTo: system pools
field: type
required: VirtualMachineScaleSets
fix: "Day-0. Recreate as VMSS."
- id: pool-azure-linux-os
severity: incompatible
appliesTo: system pools only
field: osSKU
required: AzureLinux
fix: "Day-0. Recreate system pool with --os-sku AzureLinux. User pools can use any OS."
- id: pool-ssh-disabled
severity: requiresChanges
appliesTo: all pools
field: agentPoolProfiles[*].securityProfile.sshAccess
required: Disabled
fix: "az aks nodepool update --cluster-name CLUSTER --name POOL_NAME --ssh-access disabled"
# =============================================================================
# WORKLOAD CONSTRAINTS β Deployment Safeguards (21 rules: 20 Baseline-level
# rules active on Automatic + safeguard-host-probes, which is advisory/unverified)
# Initiative: c047ea8e | Effect: Mixed(Deny/Warn/Mutate) on Automatic
# Restricted-only and Windows-only rules are listed separately under
# `conditionalSafeguards` below.
# =============================================================================
safeguards:
# -- AKS Best Practices (9 policies) --
- id: safeguard-restricted-node-edits
policyId: 53a4a537
severity: requiresChanges
category: nodeProtection
check: Check if a rolebinding for a service account references a role with node edit permissions. The application might try to edit node objects directly
fix: Manage node pools through the AKS API (az aks nodepool) instead of direct Node object edits
- id: safeguard-container-resource-requests
policyId: 03a4ecdb
severity: autoFixed
category: resources
check: Every container must have cpu + memory requests and limits
effect: "ResourceRequestsWorkloadMutator sets defaults cpu=500m, memory=2Gi for requests+limits; enforces minimums cpu=100m, memory=100Mi; fixes QoS if requests > limits"
- id: safeguard-pod-enforce-antiaffinity
policyId: 34c88cd4
severity: autoFixed
category: availability
check: Replicated workloads with >1 replica should have podAntiAffinity or topologySpreadConstraints
effect: "AntiAffinityTopologySpreadWorkloadMutator adds preferred anti-affinity (weight=100, hostname) + topology spread (maxSkew=1, hostname, ScheduleAnyway) if neither exists"
- id: safeguard-restricted-labels
policyId: a22123bd
severity: requiresChanges
category: labeling
check: AKS-reserved label prefixes blocked
fix: Remove/rename labels with kubernetes.azure.com/ prefix
- id: safeguard-restricted-taints
policyId: 48940d92
severity: requiresChanges
category: nodeProtection
check: AKS-reserved taint CriticalAddonsOnly key blocked for users
fix: Remove reserved taints, use custom taint keys
- id: safeguard-probes-configured
policyId: b1a9997f
severity: informational
enforcement: warn # Warning-only β deployments are admitted with a kubectl warning, not denied
category: reliability
check: Every container should have readinessProbe + livenessProbe (recommended best practice)
fix: Add probes (app-specific β HTTP/TCP/exec/gRPC) β recommended, not required for migration
- id: safeguard-csi-driver-storage-class
policyId: 4f3823b6
severity: requiresChanges
category: storage
check: StorageClass must use CSI provisioner (not in-tree)
fix: "Replace kubernetes.io/azure-disk β disk.csi.azure.com, also replace kubernetes.io/azure-file with file.csi.azure.com"
- id: safeguard-unique-service-selectors
policyId: b0fdedee
severity: requiresChanges
category: networking
check: Services must have unique selectors per namespace
fix: Deduplicate Service selectors
- id: safeguard-images-no-latest
policyId: 021f8078
severity: requiresChanges
category: imagePolicy
check: Image tag must not be :latest or untagged (no colon)
patch: "replace image tag with specific version or sha256 digest"
# -- PSS-related policies in Safeguards (12 policies) --
- id: safeguard-block-host-namespaces
policyId: 47a1ee2f
severity: incompatible
category: podSecurity
check: |
Sharing the host PID or IPC namespaces is disallowed in the Baseline policy.
Check the following fields:
- spec.hostPID
- spec.hostIPC
fix: |
The allowed values are:
- undefined/nil
- false
Remove hostPID and hostIPC; incompatible if required.
- id: safeguard-host-network-ports
policyId: 82985f06
severity: incompatible
category: podSecurity
check: |
Sharing the host network namespace is disallowed, and host ports should not be used.
Check the following fields:
- spec.hostNetwork
- spec.containers[*].ports[*].hostPort
- spec.initContainers[*].ports[*].hostPort
- spec.ephemeralContainers[*].ports[*].hostPort
fix: |
The allowed values are:
- spec.hostNetwork: undefined/nil or false
- hostPort fields: undefined/nil or 0
Use ClusterIP Services, Ingress, or internal Pod networking instead of host networking or host ports.
- id: safeguard-allowed-sysctls
policyId: 5e5a0673
severity: requiresChanges
category: podSecurity
check: |
Sysctls are limited to the Baseline safe subset.
Check the following field:
- spec.securityContext.sysctls[*].name
fix: |
The allowed values are:
- undefined/nil
- kernel.shm_rmid_forced
- net.ipv4.ip_local_port_range
- net.ipv4.ip_unprivileged_port_start
- net.ipv4.tcp_syncookies
- net.ipv4.ping_group_range
- net.ipv4.ip_local_reserved_ports
- net.ipv4.tcp_keepalive_time
- net.ipv4.tcp_fin_timeout
- net.ipv4.tcp_keepalive_intvl
- net.ipv4.tcp_keepalive_probes
Remove any sysctl not in this list.
- id: safeguard-no-host-path-volumes
policyId: 098fc59e
severity: incompatible
category: podSecurity
check: |
HostPath volumes are forbidden in the Baseline policy.
Check the following field:
- spec.volumes[*].hostPath
fix: |
The allowed values are:
- undefined/nil
Replace hostPath volumes with PVCs, ConfigMaps, Secrets, CSI-backed storage, or another non-hostPath volume type.
- id: safeguard-enforce-apparmor
policyId: 511f5417
severity: requiresChanges
category: podSecurity
check: |
On supported hosts, the Baseline policy does not allow disabling the default AppArmor profile.
Check the following fields:
- spec.securityContext.appArmorProfile.type
- spec.containers[*].securityContext.appArmorProfile.type
- spec.initContainers[*].securityContext.appArmorProfile.type
- spec.ephemeralContainers[*].securityContext.appArmorProfile.type
- metadata.annotations["container.apparmor.security.beta.kubernetes.io/*"]
fix: |
The allowed values are:
- appArmorProfile.type: undefined/nil, RuntimeDefault, or Localhost
- AppArmor annotation: undefined/nil, runtime/default, or localhost/*
Set RuntimeDefault, or use an allowed Localhost profile.
- id: safeguard-enforce-selinux
policyId: e1e6c427
severity: informational
category: podSecurity
check: |
SELinux settings are restricted to specific types, and custom user or role values are forbidden.
Sanitized input represents non-empty user and role values as
seLinuxOptions.userConfigured and seLinuxOptions.roleConfigured booleans;
true violates the corresponding empty-value requirement. The raw values
are deliberately not retained. If these booleans are absent from an older
projection, report that portion of the check as unavailable, not compliant.
Check the following fields:
- spec.securityContext.seLinuxOptions.type
- spec.containers[*].securityContext.seLinuxOptions.type
- spec.initContainers[*].securityContext.seLinuxOptions.type
- spec.ephemeralContainers[*].securityContext.seLinuxOptions.type
- spec.securityContext.seLinuxOptions.user
- spec.containers[*].securityContext.seLinuxOptions.user
- spec.initContainers[*].securityContext.seLinuxOptions.user
- spec.ephemeralContainers[*].securityContext.seLinuxOptions.user
- spec.securityContext.seLinuxOptions.role
- spec.containers[*].securityContext.seLinuxOptions.role
- spec.initContainers[*].securityContext.seLinuxOptions.role
- spec.ephemeralContainers[*].securityContext.seLinuxOptions.role
fix: |
The allowed values are:
- seLinuxOptions.type: undefined/"", container_t, container_init_t, container_kvm_t, or container_engine_t
- seLinuxOptions.user: undefined/""
- seLinuxOptions.role: undefined/""
Optional hardening only: remove custom seLinuxOptions or use one of the allowed types.
- id: safeguard-windows-block-host-process
policyId: 077f0ce1
severity: incompatible
category: podSecurity
check: |
Windows Pods offer the ability to run HostProcess containers which enables privileged access to the Windows host machine. Privileged access to the host is disallowed in the Baseline policy.
Check the following fields:
- spec.securityContext.windowsOptions.hostProcess
- spec.containers[*].securityContext.windowsOptions.hostProcess
- spec.initContainers[*].securityContext.windowsOptions.hostProcess
- spec.ephemeralContainers[*].securityContext.windowsOptions.hostProcess
fix: |
The allowed values are:
- undefined/nil
- false
Remove hostProcess; incompatible if required.
- id: safeguard-no-privileged-containers
policyId: 95edb821
severity: incompatible
category: podSecurity
check: |
Privileged containers are disallowed in the Baseline policy.
Check the following fields:
- spec.containers[*].securityContext.privileged
- spec.initContainers[*].securityContext.privileged
- spec.ephemeralContainers[*].securityContext.privileged
fix: |
The allowed values are:
- undefined/nil
- false
Remove privileged mode or set privileged to false; incompatible if privileged access is required.
- id: safeguard-no-custom-proc-mount
policyId: f85eb0dd
severity: requiresChanges
category: podSecurity
check: |
Custom /proc mount types are disallowed.
Check the following fields:
- spec.containers[*].securityContext.procMount
- spec.initContainers[*].securityContext.procMount
- spec.ephemeralContainers[*].securityContext.procMount
fix: |
The allowed values are:
- undefined/nil
- Default
Remove custom procMount values or set procMount to Default.
- id: safeguard-container-capabilities
policyId: c26596ff
severity: requiresChanges
category: podSecurity
check: |
Adding capabilities is limited to the Baseline allowlist.
Check the following fields:
- spec.containers[*].securityContext.capabilities.add
- spec.initContainers[*].securityContext.capabilities.add
- spec.ephemeralContainers[*].securityContext.capabilities.add
fix: |
The allowed values are:
- undefined/nil
- AUDIT_WRITE
- CHOWN
- DAC_OVERRIDE
- FOWNER
- FSETID
- KILL
- MKNOD
- NET_BIND_SERVICE
- SETFCAP
- SETGID
- SETPCAP
- SETUID
- SYS_CHROOT
Remove any added capability outside this list.
- id: safeguard-host-probes
policyId: acdf8909
severity: informational
enforcement: unverified # Not in initiative c047ea8e v3.0.0 or the Learn Safeguards PSS table; keep as advisory, do not report as a blocker
category: podSecurity
check: |
The host field in probes and lifecycle hooks is disallowed
Restricted fields:
- spec.containers[*].livenessProbe.httpGet.host
- spec.containers[*].readinessProbe.httpGet.host
- spec.containers[*].startupProbe.httpGet.host
- spec.containers[*].livenessProbe.tcpSocket.host
- spec.containers[*].readinessProbe.tcpSocket.host
- spec.containers[*].startupProbe.tcpSocket.host
- spec.containers[*].lifecycle.postStart.tcpSocket.host
- spec.containers[*].lifecycle.preStop.tcpSocket.host
- spec.containers[*].lifecycle.postStart.httpGet.host
- spec.containers[*].lifecycle.preStop.httpGet.host
- spec.initContainers[*].livenessProbe.httpGet.host
- spec.initContainers[*].readinessProbe.httpGet.host
- spec.initContainers[*].startupProbe.httpGet.host
- spec.initContainers[*].livenessProbe.tcpSocket.host
- spec.initContainers[*].readinessProbe.tcpSocket.host
- spec.initContainers[*].startupProbe.tcpSocket.host
- spec.initContainers[*].lifecycle.postStart.tcpSocket.host
- spec.initContainers[*].lifecycle.preStop.tcpSocket.host
- spec.initContainers[*].lifecycle.postStart.httpGet.host
- spec.initContainers[*].lifecycle.preStop.httpGet.host
fix: |
The allowed values are:
- undefined/nil
- ""
Remove the `host` field from probes and lifecycle hooks; the kubelet uses the pod IP by default.
- id: safeguard-allowed-seccomp-profiles
policyId: 975ce327
severity: requiresChanges
category: podSecurity
check: |
Seccomp must not be explicitly set to Unconfined.
Check the following fields:
- spec.securityContext.seccompProfile.type
- spec.containers[*].securityContext.seccompProfile.type
- spec.initContainers[*].securityContext.seccompProfile.type
- spec.ephemeralContainers[*].securityContext.seccompProfile.type
fix: |
The allowed values are:
- undefined/nil
- RuntimeDefault
- Localhost
Remove Unconfined, or set seccompProfile.type to RuntimeDefault or Localhost.
# =============================================================================
# CONDITIONAL SAFEGUARDS β present in initiative c047ea8e but NOT part of the
# Baseline set AKS Automatic enforces by default. Report these only when the
# cluster's active constraints show them, when the user opts into PSS
# Restricted, or when the workload targets Windows nodes. Never present them
# as default Automatic migration blockers.
# =============================================================================
conditionalSafeguards:
- id: safeguard-no-privilege-escalation
policyId: 1c6e92c9
severity: requiresChanges
enforcement: conditional # Learn: "Privilege Escalation (PSS Restricted Only)"
appliesWhen: PSS Restricted is enabled on the cluster, or an active constraint forces allowPrivilegeEscalation=false
category: podSecurity
check: |
allowPrivilegeEscalation must not be true; under Restricted it must be explicitly false.
Check the following fields:
- spec.containers[*].securityContext.allowPrivilegeEscalation
- spec.initContainers[*].securityContext.allowPrivilegeEscalation
- spec.ephemeralContainers[*].securityContext.allowPrivilegeEscalation
patch: "add securityContext.allowPrivilegeEscalation: false"
- id: safeguard-allowed-users-groups
policyId: f06ddb64
severity: requiresChanges
enforcement: conditional # Learn: "Running as Non-Root (PSS Restricted Only)"
appliesWhen: PSS Restricted is enabled, or an active constraint sets runAsUserRule=MustRunAsNonRoot
category: podSecurity
check: |
Containers must not run as root under Restricted.
Check the following fields:
- spec.securityContext.runAsNonRoot / runAsUser
- spec.containers[*].securityContext.runAsNonRoot / runAsUser
patch: "add securityContext.runAsNonRoot: true and a non-zero runAsUser the image supports"
- id: safeguard-allowed-volume-types
policyId: 16697877
severity: informational
enforcement: conditional # Learn: "Volume Types (PSS Restricted Only)"
appliesWhen: PSS Restricted is enabled on the cluster
category: podSecurity
check: |
Under Restricted only these volume types are allowed:
configMap, csi, downwardAPI, emptyDir, ephemeral, persistentVolumeClaim, projected, secret.
Check the following field:
- spec.volumes[*]
fix: "Optional hardening: replace other volume types with an allowed type (hostPath is already covered by safeguard-no-host-path-volumes)"
- id: safeguard-windows-block-container-admin
policyId: 5485eac0
severity: requiresChanges
enforcement: conditional # In initiative c047ea8e; not listed on the Learn Safeguards page. Windows node pools are optional on Automatic
appliesWhen: The workload targets Windows nodes and the constraint is active on the cluster
category: podSecurity
check: |
Windows containers must not run as ContainerAdministrator.
Check the following fields:
- spec.securityContext.windowsOptions.runAsUserName
- spec.containers[*].securityContext.windowsOptions.runAsUserName
fix: "Run as ContainerUser or another non-administrator Windows account"
# =============================================================================
# POD SECURITY STANDARDS β initiative metadata
# =============================================================================
podSecurityBaseline:
initiative: a8640138-9b0a-4a28-b8cb-1666c838647d
enforcement: mandatory on AKS Automatic (Enforce; namespace exclusions allowed, cannot switch to Warn)
note: Overlaps with the Baseline-level Safeguards rules above
policies: [NoPrivilegedContainers, BlockUsingHostNetwork, BlockUsingHostProcessIDAndIPC, ContainerCapabilities, NoHostPathVolume]
podSecurityRestricted:
initiative: 42b8ef37-b724-4e24-bbc8-7a7708edfe00
enforcement: optional opt-in (not enforced by default on Automatic)
note: Adds the conditionalSafeguards marked "PSS Restricted Only"
additionalPolicies: [NoPrivilegeEscalation, AllowedVolumeTypes, AllowedUsersGroups, AllowedSeccompProfiles]
# =============================================================================
# WEBHOOK MUTATIONS (2 active mutators) β auto-applied at admission
# =============================================================================
mutations:
- id: mutation-anti-affinity-topology-spread
policyId: implicit
target: [Deployment, StatefulSet, ReplicaSet]
effect: "Adds preferred pod anti-affinity (weight=100, kubernetes.io/hostname) + topology spread (maxSkew=1, kubernetes.io/hostname, ScheduleAnyway). Skips if any existing anti-affinity or topology spread. Label priority: app > app.kubernetes.io/name > default-antiaffinity-applabel."
- id: mutation-resource-requests-default
policyId: implicit
target: containers
effect: "Sets resources.requests+limits defaults cpu=500m, memory=2Gi. Minimums cpu=100m, memory=100Mi. If only limits set, requests=limits. If requests > limits, requests capped at limits (QoS fix)."
mcp-integration.md 8.4 KB
# MCP Integration Reference
Loaded when discovering Azure MCP capabilities, collecting live cluster metadata, or debugging the fallback chain.
---
## Capability Discovery
Inspect the host's available tool catalog, descriptions, and input schemas for an Azure MCP capability that advertises AKS operations. In GitHub Copilot this is surfaced as `mcp_azure_mcp_aks`; other hosts assign their own names. Invoke the matching capability under its host-assigned name and advertised schema.
- If the host exposes a list, learn, or discovery operation, use it.
- Otherwise, rely on the tool catalog and schema the host already supplied.
- Never infer that Azure MCP is absent because a literal tool name is missing.
- Do not translate names between hosts or create a mapping layer.
Azure MCP is not a prerequisite for this assessment. If it is absent, use
equivalent reads exposed by the host's approved ARM, Azure CLI, or other
metadata capabilities, within their advertised schemas and authorized scope.
If no approved read fits, collect operator-provided evidence or validate
manifests offline. Do not add an external or local MCP server to work around a
governed host's tool policy.
The currently documented Azure MCP AKS surface provides read-only operations for:
- AKS cluster details (get/list)
- AKS node-pool details (get/list)
It does **not** document an AKS Automatic readiness-assessment operation, a workload assessment response schema, or an assessment polling action. Earlier versions of this skill described such an API; it does not exist. See [Azure Kubernetes Service tools for the Azure MCP Server](https://learn.microsoft.com/azure/developer/azure-mcp-server/tools/azure-kubernetes).
---
## Role in Readiness Assessment
Use the discovered Azure MCP AKS capability only for operations its schema advertises:
1. Read cluster configuration and status.
2. Read node-pool configuration and status.
3. Collect sanitized workload manifests through a host capability with an equivalent allowlist projection, or pipe `kubectl` JSON through [`scripts/sanitize-readiness-input.jq`](../scripts/sanitize-readiness-input.jq) (requires `jq`) before the result reaches the model.
4. Evaluate cluster metadata and manifests locally against `constraint-spec-v1.yaml`.
Do not request `Secret` or ConfigMap resources. The assessment needs selected workload fields, Services, PodDisruptionBudgets, and StorageClass provisioners, not secret values. The bundled filter uses an allowlist projection so unrecognized fields (env values, `envFrom`, volume sources, non-AppArmor annotations, `managedFields`, `status`) are dropped by default.
Representative redacted Kubernetes read (run from the skill root only where
the host authorizes shell and cluster access and exposes `kubectl`, `jq`, and
the bundled filter):
```bash
set -o pipefail
kubectl get deployment,statefulset,daemonset,job,cronjob,pod,service,poddisruptionbudget,storageclass \
-A -o json |
jq -f scripts/sanitize-readiness-input.jq
```
If the execution capability, `jq`, or the filter is unavailable, do not paste
raw `kubectl -o json` output to the model. Ask the user for rendered manifests,
or use an approved host Kubernetes read that supports field projection.
Azure MCP's AKS metadata area is not a Kubernetes command runner.
All assessment conclusions and suggested patches come from the bundled constraint spec and the collected manifests, not from an assumed MCP response contract.
---
## Fallback Chain
Attempt the applicable path without asking the user to identify tool availability:
```text
Cluster and node-pool metadata:
1. Inspect host tools for an Azure MCP AKS read capability
-> invoke the matching capability under its host-assigned name and schema
-> if unavailable or insufficient, use approved equivalent metadata reads
(governed ARM/Azure CLI capabilities or permitted `az aks show` / `az aks nodepool list`)
Workload manifests:
1. Use an approved host Kubernetes read with field projection, or a permitted `kubectl | jq` pipeline
-> if execution, cluster access, or the sanitizer is unavailable, use local, rendered, or user-provided manifests
Assessment:
1. Evaluate collected data locally against `constraint-spec-v1.yaml`
2. When the cluster is reachable, confirm which conditional rules are active:
`kubectl get constraints` (Gatekeeper) β report Restricted-only/Windows rules only if present
```
CLI metadata fallback:
```bash
az aks show \
--resource-group <resource-group> \
--name <cluster>
az aks nodepool list \
--resource-group <resource-group> \
--cluster-name <cluster>
```
---
## Azure SRE Agent
Azure SRE Agent's built-in Azure operations, diagnostics, monitoring, and `kubectl` tools use the agent's managed identity and require no connector. Prefer those built-in capabilities when they satisfy the read. See [Tools in Azure SRE Agent](https://learn.microsoft.com/azure/sre-agent/tools).
Installing this plugin records its `.mcp.json` requirement but does not provision the external connector. **Connector setup required** is a non-blocking plugin status. If the external Azure MCP surface is needed and this host supports and authorizes connector setup:
1. Open the plugin details and select **Add as connector**, or go to **Builder > Connectors**.
2. Complete the connector authentication.
3. Wait for the connector status to show **Connected**.
4. Select the required Azure MCP tools for the agent.
See the official [plugin marketplace guidance](https://learn.microsoft.com/azure/sre-agent/plugin-marketplace#what-the-plugin-marketplace-does) and [MCP connector tutorial](https://learn.microsoft.com/azure/sre-agent/mcp-connector).
If a built-in Azure or Kubernetes read fails, check that the target resource group is in the agent's scope and that its user-assigned managed identity has the required RBAC. Do not tell an SRE Agent user to configure Azure MCP "in the editor" or default to local `az login` remediation. See [SRE Agent permissions](https://learn.microsoft.com/azure/sre-agent/permissions).
---
## Other Hosts
If capability discovery finds no Azure MCP AKS tool, explain that the current
host does not expose it. Use approved equivalent metadata capabilities, such
as governed ARM or Azure CLI reads, and approved projected Kubernetes reads or
the permitted `kubectl`/`jq` fallback. Continue with offline validation when
live access is unavailable. Do not present a local or external MCP server as
a prerequisite. Only if the host supports and authorizes connector setup,
point to its configuration flow or the official
[Azure MCP Server setup overview](https://learn.microsoft.com/azure/developer/azure-mcp-server/get-started).
For CLI-backed hosts, verify:
```bash
az account show --query "{name:name, id:id, state:state}" -o table
kubectl config current-context
kubectl cluster-info
```
---
## Common Errors
| Error | Cause | Fix |
|---|---|---|
| No Azure MCP AKS capability appears in the host catalog | Host uses another approved surface or does not expose these reads | Use approved equivalent metadata capabilities or offline validation. Suggest connector setup only where the host supports and authorizes it; absence is not a requirement to install MCP |
| Discovered AKS capability lacks a readiness-assessment operation | Expected for the currently documented Azure MCP AKS surface | Use advertised cluster/node-pool reads, collect sanitized manifests through Kubernetes-native tools, and evaluate `constraint-spec-v1.yaml` locally |
| Azure or Kubernetes read fails in Azure SRE Agent | Agent UAMI lacks target scope or RBAC | Check managed resource groups and role assignments in [SRE Agent permissions](https://learn.microsoft.com/azure/sre-agent/permissions) |
| `HTTP 401 Unauthorized` in a CLI-backed host | Azure CLI session is not authenticated | Run `az login` and verify `az account show` |
| `HTTP 403 Forbidden` | Identity lacks read access | Grant the least-privilege read role at the target scope (for example, Azure Kubernetes Service Cluster User Role for `kubectl` access) |
| `HTTP 404 Not Found` | Wrong subscription, resource group, or cluster name | Verify the target with the discovered Azure capability or `az aks list -o table` |
| `kubectl` cannot read workloads | Missing context or Kubernetes RBAC | Verify context and read permissions, or use local/rendered manifests |
| `jq` or bundled sanitizer unavailable | Host cannot execute the sanitized pipeline | Use rendered manifests or an approved projecting host read. Install tools only where host policy and user authorization permit; never send raw cluster JSON to the model |
migration-guide-summary.md 6.9 KB
# AKS Automatic Migration Guide
Loaded when user asks about migration steps or after assessment is complete.
---
## Supported Journeys β Read First
Migrating **to** AKS Automatic means creating a new AKS Automatic target cluster and moving workloads to it. There is **no** documented in-place `az aks update --sku automatic` path for an existing Standard cluster.
| Journey | Documented today | Guidance |
|---|---|---|
| AKS Standard β AKS Automatic | Not in place; new target cluster + workload move | Follow the checklist below |
| AKS Automatic (managed system pool) β AKS Standard (`base` SKU) | Yes, in place: `az aks nodepool add --mode System β¦` then `az aks update --sku base` | Rollback/exit path; see the Learn article |
| Standard β Automatic in place; Automatic without managed system pool β with managed system pool | Listed by Microsoft as "coming soon" | Do not promise it or script it; re-check the Learn article |
Source: [AKS Automatic cluster SKU migration](https://learn.microsoft.com/azure/aks/automatic/aks-automatic-sku-migration). If that article changes, it wins over this summary.
---
## Migration Checklist
### Phase 1 β Assessment (this skill)
- [ ] Collect cluster/node-pool metadata through a host-discovered Azure MCP AKS capability (or `az`), then evaluate sanitized Kubernetes manifests locally against the bundled constraint spec
- [ ] Resolve all `incompatible` findings β these are hard blockers
- [ ] Apply all `requiresChanges` fixes β these will be denied at admission
- [ ] Review `autoFixed` items β understand what AKS Automatic will mutate at runtime
- [ ] Decide whether the target will opt into PSS Restricted or run Windows node pools; only then apply the `conditionalSafeguards` rules
- [ ] Address cluster-level Day-0 config issues (see below)
### Phase 2 β Create AKS Automatic Cluster (`azure-kubernetes` handoff)
This skill does not implement cluster creation. Hand off the assessed target requirements to the `azure-kubernetes` skill, including region, network and private-connectivity requirements, identities, policy constraints, and the Day-0 boundaries below.
```bash
az aks create \
--resource-group <resource-group> \
--name <new-cluster-name> \
--sku automatic \
--location <location> \
--generate-ssh-keys
```
Do not continue to validation or cutover until the target cluster exists and its cluster/resource-group context is verified.
### Phase 3 β Dry-run and Staging Evidence on the New Cluster
```bash
# Keep target credentials in a dedicated file so source and target contexts never mix
az aks get-credentials \
--resource-group <target-resource-group> \
--name <target-cluster-name> \
--file <target-kubeconfig>
# Dry-run server-side apply β catches admission policy rejections
KUBECONFIG=<target-kubeconfig> kubectl apply \
--dry-run=server -f <manifests-directory>/
```
Creating a staging namespace and applying workloads mutate the target cluster. Run them only after the dry-run evidence is reviewed and the owner explicitly approves staging:
```bash
KUBECONFIG=<target-kubeconfig> kubectl create namespace <staging-namespace>
KUBECONFIG=<target-kubeconfig> kubectl apply \
-f <manifests-directory>/ -n <staging-namespace>
# Preserve workload and admission evidence
KUBECONFIG=<target-kubeconfig> kubectl get pods \
-n <staging-namespace> -o wide
KUBECONFIG=<target-kubeconfig> kubectl get events \
-n <staging-namespace> --sort-by=.metadata.creationTimestamp
```
### Phase 4 β Cutover and Rollback Window
Cutover changes traffic and requires an owner-approved plan. Keep the old cluster available for an owner-approved rollback window. Close that window only after the owner accepts explicit evidence that:
- workload health and service objectives remain stable on the target;
- required data is synchronized and integrity checks pass;
- production traffic is fully served by the target without unresolved errors or regressions; and
- the rollback path has remained executable until the closure decision.
### Phase 5 β Decommission Old Cluster
```bash
# Destructive: only after the owner closes the rollback window
az aks delete \
--resource-group <source-resource-group> \
--name <old-cluster-name> \
--yes --no-wait
```
---
## Day-0 Decisions β Cluster-Level Configuration Requirements
AKS Automatic is a new target cluster, and `azure-kubernetes` owns its creation. Preserve these source-to-target boundaries in the handoff:
| Boundary | Required target state | Handoff evidence |
|---|---|---|
| Network plugin | A kubenet source maps to Azure CNI Overlay on the Automatic target | Record source CIDRs, UDR/firewall dependencies, private endpoints, DNS, and non-overlapping target address spaces |
| API Server VNet Integration | Select the target VNet/subnet design at cluster creation | Record private/public API access, DNS, peering, and authorized administration paths |
| System node pool OS | The Automatic target system-pool OS is Azure Linux | Inventory source user-pool OS (including Windows) and workload OS requirements separately |
| OIDC issuer and workload identity | Use the Automatic target capabilities | Inventory service accounts, federated credentials, and identity/RBAC dependencies for staging validation |
---
## What AKS Automatic Auto-Enables
No manual setup needed for these β show this list when user asks "what do I get for free":
| Feature | Benefit |
|---|---|
| Node Auto Provisioning (NAP) | Replaces cluster autoscaler; right-sizes node pools automatically |
| Vertical Pod Autoscaler (VPA) | Auto-tunes resource requests after deployment |
| Azure Monitor Container Insights | Logs, metrics, and dashboards out of the box |
| Deployment Safeguards | Enabled in Enforce by default; admission policies plus webhook mutations for resource defaults and placement. Namespaces can be excluded; the level cannot be switched to Warn |
| Pod Security Standards (Baseline) | Enforced cluster-wide; Restricted available opt-in |
| Managed OIDC Issuer | Required for workload identity |
| Azure Key Vault CSI Driver | Secret injection without static credentials |
| Ephemeral OS disks | Faster node provisioning by default |
| Azure Linux node OS | Smaller footprint, faster boot times |
---
## Post-Migration Verification Commands
```bash
KUBECONFIG=<target-kubeconfig> kubectl get pods -A |
grep -v Running | grep -v Completed
KUBECONFIG=<target-kubeconfig> kubectl get pods -A \
--field-selector status.phase=Pending
# Deployment Safeguards / PSS constraints active on the target
KUBECONFIG=<target-kubeconfig> kubectl get constrainttemplate
KUBECONFIG=<target-kubeconfig> kubectl get constraints
KUBECONFIG=<target-kubeconfig> kubectl get vpa -A
az aks nodepool list \
--resource-group <target-resource-group> \
--cluster-name <target-cluster-name> \
--query "[].{name:name, mode:mode, osType:osType, count:count}" \
-o table
az aks show \
--resource-group <target-resource-group> \
--name <target-cluster-name> \
--query addonProfiles.omsagent.enabled
```
sanitize-readiness-input.jq 7.8 KB
# sanitize-readiness-input.jq β allowlist projection for AKS Automatic readiness.
#
# Usage (from the skill root; requires jq 1.6+):
# kubectl get deployment,statefulset,daemonset,job,cronjob,pod,service,poddisruptionbudget,storageclass \
# -A -o json | jq -f scripts/sanitize-readiness-input.jq
#
# Keeps only the fields the constraint spec evaluates. Everything else β env
# values, envFrom, volume sources, annotations other than AppArmor, managedFields,
# status, Secret/ConfigMap data β is dropped by default because it is never
# selected. Never feed Secret or ConfigMap resources into this pipeline.
# Ported from Azure/AKS-Skills skills/aks-automatic-readiness (PR #71).
def compact:
walk(
if type == "object" then
with_entries(select(.value != null and .value != {} and .value != []))
else
.
end
);
def metadata_projection:
{
name: .name,
namespace: .namespace,
restrictedLabelKeys: (
(.labels // {})
| keys
| map(select(startswith("kubernetes.azure.com/")))
),
legacyAppArmorProfiles: [
(.annotations // {})
| to_entries[]
| select(
.key
| startswith(
"container.apparmor.security.beta.kubernetes.io/"
)
)
| {
container: (
.key
| ltrimstr(
"container.apparmor.security.beta.kubernetes.io/"
)
),
valid: (
.value == null
or .value == "runtime/default"
or (if (.value | type) == "string"
then (.value | startswith("localhost/"))
else false
end)
)
}
]
}
| compact;
def security_context_projection:
{
privileged: .privileged,
allowPrivilegeEscalation: .allowPrivilegeEscalation,
runAsNonRoot: .runAsNonRoot,
runAsUser: .runAsUser,
procMount: .procMount,
capabilities: {
add: (.capabilities.add // []),
drop: (.capabilities.drop // [])
},
seccompProfile: {type: .seccompProfile.type},
appArmorProfile: {type: .appArmorProfile.type},
seLinuxOptions: {
type: .seLinuxOptions.type,
userConfigured: (.seLinuxOptions.user != null and .seLinuxOptions.user != ""),
roleConfigured: (.seLinuxOptions.role != null and .seLinuxOptions.role != "")
},
windowsOptions: {
hostProcess: .windowsOptions.hostProcess,
runsAsContainerAdministrator:
(.windowsOptions.runAsUserName == "ContainerAdministrator")
},
sysctls: [(.sysctls // [])[] | {name: .name}]
}
| compact;
def container_projection:
(.image // "") as $image
| ($image | test("@sha256:[0-9A-Fa-f]{64}$")) as $uses_digest
| ($image | split("@")[0] | split("/") | last) as $name_and_tag
| ($name_and_tag | test(":[^:]+$")) as $has_tag
| ($name_and_tag | endswith(":latest")) as $uses_latest_tag
|
{
name: .name,
imagePolicy: {
usesLatestTag: (
($uses_digest | not)
and (($has_tag | not) or $uses_latest_tag)
),
pinned: (
$uses_digest
or ($has_tag and ($uses_latest_tag | not))
)
},
resources: .resources,
readinessProbeConfigured: has("readinessProbe"),
livenessProbeConfigured: has("livenessProbe"),
securityContext: (.securityContext | security_context_projection),
ports: [
(.ports // [])[]
| {
containerPort: .containerPort,
hostPort: .hostPort,
protocol: .protocol
}
| compact
]
}
| compact;
def volume_projection:
. as $volume
| {
name: .name,
types: (
[
"awsElasticBlockStore",
"azureDisk",
"azureFile",
"cephfs",
"cinder",
"configMap",
"csi",
"downwardAPI",
"emptyDir",
"ephemeral",
"fc",
"flexVolume",
"flocker",
"gcePersistentDisk",
"gitRepo",
"glusterfs",
"hostPath",
"iscsi",
"nfs",
"persistentVolumeClaim",
"photonPersistentDisk",
"portworxVolume",
"projected",
"quobyte",
"rbd",
"scaleIO",
"secret",
"storageos",
"vsphereVolume"
]
| map(. as $type | select($volume | has($type)))
)
}
| compact;
def pod_spec_projection:
{
hostPID: (.hostPID // false),
hostIPC: (.hostIPC // false),
hostNetwork: (.hostNetwork // false),
securityContext: (.securityContext | security_context_projection),
containers: [(.containers // [])[] | container_projection],
initContainers: [(.initContainers // [])[] | container_projection],
ephemeralContainers: [(.ephemeralContainers // [])[] | container_projection],
volumes: [(.volumes // [])[] | volume_projection],
podAntiAffinityConfigured: (.affinity.podAntiAffinity != null),
topologySpreadConstraintsConfigured:
(((.topologySpreadConstraints // []) | length) > 0)
}
| compact;
def workload_projection:
{
apiVersion: .apiVersion,
kind: .kind,
metadata: (.metadata | metadata_projection),
spec: {
replicas: .spec.replicas,
template: {
metadata: (.spec.template.metadata | metadata_projection),
spec: (.spec.template.spec | pod_spec_projection)
}
}
}
| compact;
def cronjob_projection:
{
apiVersion: .apiVersion,
kind: .kind,
metadata: (.metadata | metadata_projection),
spec: {
jobTemplate: {
spec: {
template: {
metadata:
(.spec.jobTemplate.spec.template.metadata | metadata_projection),
spec:
(.spec.jobTemplate.spec.template.spec | pod_spec_projection)
}
}
}
}
}
| compact;
def pod_projection:
{
apiVersion: .apiVersion,
kind: .kind,
metadata: (.metadata | metadata_projection),
spec: (.spec | pod_spec_projection)
}
| compact;
def service_selector_key:
(.spec.selector // {})
| to_entries
| sort_by(.key)
| tojson;
def service_projection($all):
. as $service
| {
apiVersion: .apiVersion,
kind: .kind,
metadata: (.metadata | metadata_projection),
selectorDuplicateCount: (
if (($service.spec.selector // {}) | length) == 0 then
0
else
[
$all[]
| select(.kind == "Service")
| select(
(.metadata.namespace // "default")
== ($service.metadata.namespace // "default")
)
| select(
service_selector_key
== ($service | service_selector_key)
)
]
| length
end
)
}
| compact;
def pdb_projection:
{
apiVersion: .apiVersion,
kind: .kind,
metadata: (.metadata | metadata_projection),
spec: {
minAvailable: .spec.minAvailable,
maxUnavailable: .spec.maxUnavailable
}
}
| compact;
def storageclass_projection:
{
apiVersion: .apiVersion,
kind: .kind,
metadata: (.metadata | metadata_projection),
provisioner: .provisioner
}
| compact;
(.items // []) as $all
| {
apiVersion: .apiVersion,
kind: .kind,
items: [
$all[]
| if .kind == "CronJob" then
cronjob_projection
elif .kind == "Pod" then
pod_projection
elif .kind == "Service" then
service_projection($all)
elif .kind == "PodDisruptionBudget" then
pdb_projection
elif .kind == "StorageClass" then
storageclass_projection
elif (
.kind == "Deployment"
or .kind == "StatefulSet"
or .kind == "DaemonSet"
or .kind == "Job"
) then
workload_projection
else
empty
end
]
}
azure-aks-autoscaler.md 4.0 KB
# AKS Cluster Autoscaler (CAS)
Enable and tune the Cluster Autoscaler to automatically scale down idle nodes.
## Check CAS Status
```bash
az aks show \
--name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--query "agentPoolProfiles[].{name:name, casEnabled:enableAutoScaling, min:minCount, max:maxCount, count:count}" \
-o table
az aks show \
--name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--query "autoScalerProfile" -o json
```
## Check Node Utilization (7 days)
Follow the metrics discovery steps in [azure-aks-rightsizing.md](./azure-aks-rightsizing.md#historical-metrics-azure-monitor--use-when-prometheus-or-container-insights-is-enabled) to list available metric definitions and query node CPU utilization. Use metric names such as `node_cpu_usage_percentage` or `cpuUsagePercentage` depending on what's available on the cluster.
## Enable CAS
```bash
# Cluster-level
az aks update \
--name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--enable-cluster-autoscaler \
--min-count <MIN_NODES> --max-count <MAX_NODES>
# Specific node pool
az aks nodepool update \
--cluster-name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--name "<NODEPOOL_NAME>" \
--enable-cluster-autoscaler \
--min-count <MIN_NODES> --max-count <MAX_NODES>
```
## Recommended min/max Defaults
| Scenario | min-count | max-count |
|----------|-----------|-----------|
| Dev/test | 1 | current_count |
| Production (web/API) | 2 | current_count * 3 |
| Production (batch) | 0 | current_count * 5 |
> Risk: Low. CAS only scales down when pods can be safely rescheduled. Set min-count >= 2 for production HA.
## Tune CAS Profile
Apply when CAS is already on but idle nodes persist. The following
cost-oriented profile is an example, not a universal default. The two scale-down
timers are shorter than their documented 10-minute defaults, while the
utilization threshold remains at its 0.5 default:
```bash
az aks update \
--name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--cluster-autoscaler-profile \
scale-down-delay-after-add=5m \
scale-down-unneeded-time=5m \
scale-down-utilization-threshold=0.5 \
max-graceful-termination-sec=600
```
> Risk: Medium. Shorter scale-down timers can reduce idle cost, but they can
> also cause node reprovisioning after brief demand changes. This thrashing
> adds provisioning latency; use a less aggressive profile for workloads with
> frequent short scale-out and scale-in cycles. See
> [AKS cluster autoscaler profile settings](https://learn.microsoft.com/azure/aks/cluster-autoscaler#cluster-autoscaler-profile-settings)
> and [aggressive scale-down guidance](https://learn.microsoft.com/azure/aks/cluster-autoscaler#configure-cluster-autoscaler-profile-for-aggressive-scale-down).
>
> Keep `skip-nodes-with-system-pods` at its default `true`. It prevents
> scale-down of nodes that host non-DaemonSet `kube-system` pods. Do not use
> `skip-nodes-with-system-pods=false` as a cost lever or assume every
> Microsoft-managed system component can be protected by a customer-managed
> PDB. See [AKS support policies](https://learn.microsoft.com/azure/aks/support-policies#managed-features-in-aks).
To roll back to CAS defaults:
```bash
az aks update \
--name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--cluster-autoscaler-profile ""
```
## Profile Comparison
| Profile | scale-down-delay-after-add | scale-down-unneeded-time | utilization-threshold | Best For |
|---------|----------------------------|--------------------------|----------------------|----------|
| Default | 10m | 10m | 0.5 | General workloads |
| Cost-Optimized | 5m | 5m | 0.5 | Cost-sensitive, non-critical |
| Conservative | 30m | 30m | 0.7 | Stateful / production |
| Aggressive | 2m | 2m | 0.4 | Dev/test, batch |
> Risk: High for aggressive tuning. Ensure PodDisruptionBudgets (PDBs) are set on critical workloads before tuning. Always confirm with user before applying.
>
> Check existing PDBs before tuning:
> ```bash
> kubectl get pdb --all-namespaces
> ```
azure-aks-rightsizing.md 4.3 KB
# AKS Pod Rightsizing
Identify pods requesting far more CPU/memory than they use and recommend reduced resource requests.
## Prerequisites β Check Monitoring State First
Before collecting usage data, determine what monitoring is available on the cluster:
```bash
# 1. Check if Azure Managed Prometheus is enabled
az aks show \
--name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--query "azureMonitorProfile.metrics.enabled" -o tsv
# 2. Check if Container Insights (Log Analytics) is enabled
az aks show \
--name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--query "addonProfiles.omsagent.enabled" -o tsv
# 3. Check if Metrics Server is running (pre-installed on AKS, but may be unhealthy)
kubectl get deployment metrics-server -n kube-system
```
Based on the result, follow the appropriate path:
| State | Rightsizing Possible? | Data Source | Accuracy |
|-------|-----------------------|-------------|----------|
| Azure Managed Prometheus enabled | Yes | Prometheus metrics via Azure Monitor | Best β full P95/7-day history |
| Container Insights (Log Analytics) enabled | Yes | KQL queries on `Perf` / `KubePodInventory` | Good β 7-day trends |
| Only Metrics Server (no Azure Monitor) | Limited | `kubectl top pods` β live data only | Low β no historical trends |
> If nothing is enabled, Metrics Server is pre-installed on AKS β confirm it is healthy and use it for live rightsizing data:
> ```bash
> kubectl get deployment metrics-server -n kube-system
> kubectl top pods --all-namespaces --sort-by=cpu
> ```
> For historical P95 trends (more accurate rightsizing), recommend enabling Azure Managed Prometheus. Warn user this incurs cost and wait for confirmation before proceeding.
---
## Detection
```bash
# Authenticate to cluster
az aks get-credentials --name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>"
# List requests/limits for ALL containers per pod (includes sidecars)
# Using [*] ensures multi-container pods are not misrepresented
kubectl get pods --all-namespaces \
-o custom-columns="NAMESPACE:.metadata.namespace,POD:.metadata.name,CONTAINERS:.spec.containers[*].name,CPU_REQ:.spec.containers[*].resources.requests.cpu,MEM_REQ:.spec.containers[*].resources.requests.memory,CPU_LIM:.spec.containers[*].resources.limits.cpu,MEM_LIM:.spec.containers[*].resources.limits.memory"
# Live per-container usage (shows each container individually, including sidecars)
kubectl top pods --all-namespaces --containers --sort-by=cpu
```
## Historical Metrics (Azure Monitor β use when Prometheus or Container Insights is enabled)
First discover available metric names, then query:
```bash
az monitor metrics list-definitions \
--resource "<AKS_RESOURCE_ID>" \
--query "[].name.value" -o tsv
```
```bash
az monitor metrics list \
--resource "<AKS_RESOURCE_ID>" \
--metric "<METRIC_NAME_FROM_ABOVE>" \
--interval PT1H --aggregation Average \
--start-time "<YYYY-MM-DDTHH:mm:ssZ>" \
--end-time "<YYYY-MM-DDTHH:mm:ssZ>"
```
## Optimization Rules
| Condition | Recommendation | Risk |
|-----------|----------------|------|
| CPU request >5x P95 actual | Reduce to `P95 * 1.2` | Medium |
| Memory request >3x P95 actual | Reduce to `P95 * 1.2` | Medium |
| CPU request >2x P95 actual | Recommend rightsizing with 20% buffer | Low |
| No resource limits set | Add limits to prevent noisy-neighbor waste | Low |
| No VPA/HPA configured | Suggest enabling Vertical Pod Autoscaler | Low |
> For VPA setup and configuration, see [azure-aks-vpa.md](./azure-aks-vpa.md).
## YAML Patch Format
```yaml
# Rightsizing patch for <NAMESPACE>/<DEPLOYMENT_NAME>
# Current: CPU request=<CURRENT>, P95 actual=<ACTUAL>
# Recommended: CPU request=<NEW> (P95 * 1.2 buffer)
apiVersion: apps/v1
kind: Deployment
metadata:
name: <DEPLOYMENT_NAME>
namespace: <NAMESPACE>
spec:
template:
spec:
containers:
- name: <CONTAINER_NAME>
resources:
requests:
cpu: "<NEW_CPU>"
memory: "<NEW_MEM>"
limits:
cpu: "<NEW_CPU_LIMIT>" # e.g. CPU limit = 1.5x CPU request, or preserve existing limit-to-request ratio
memory: "<NEW_MEM_LIMIT>" # e.g. memory limit = 1.25x memory request, or preserve existing limit-to-request ratio
```
> Risk: Medium-High. Always review patches before applying. Test in non-production first. Get explicit user confirmation before applying to production.
azure-aks-spot.md 4.6 KB
# AKS Spot Node Pools
Recommend and create Spot VM node pools for batch, dev/test, or fault-tolerant workloads (60-90% cost reduction vs regular nodes).
## Check Existing Node Pools
```bash
az aks nodepool list \
--cluster-name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--query "[].{name:name, vmSize:vmSize, priority:scaleSetPriority, count:count, mode:mode}" \
-o table
```
## Identify Spot-Suitable Workloads
Before creating a Spot pool, identify which workloads can tolerate interruptions:
```bash
# List deployments without PodDisruptionBudgets (single-replica or no PDB = higher eviction risk)
kubectl get deployments --all-namespaces -o json | \
jq -r '.items[] | select(.spec.replicas == 1) | "\(.metadata.namespace)/\(.metadata.name)"'
# Check which pods already have spot tolerations
kubectl get pods --all-namespaces -o json | \
jq -r '.items[] | select(.spec.tolerations[]?.key == "kubernetes.azure.com/scalesetpriority") | "\(.metadata.namespace)/\(.metadata.name)"'
```
Use the suitability table below to decide which workloads to migrate.
## Mixed Node Pool Pattern (Spot + Regular)
For workloads that need resilience but want cost savings, use a mixed approach:
```bash
# Keep existing regular node pool as fallback (min 1-2 nodes)
az aks nodepool update \
--cluster-name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--name "<REGULAR_POOL>" \
--enable-cluster-autoscaler --min-count 1 --max-count 3
# Add Spot pool for the majority of workload capacity
# -1 means pay up to on-demand price (no cap); set e.g. 0.05 to cap hourly spend
az aks nodepool add \
--cluster-name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--name "<SPOT_POOL_NAME>" \
--priority Spot --eviction-policy Delete --spot-max-price -1 \
--node-vm-size "<VM_SIZE>" \
--node-count 3 --min-count 0 --max-count 10 \
--enable-cluster-autoscaler \
--node-taints "kubernetes.azure.com/scalesetpriority=spot:NoSchedule" \
--labels "kubernetes.azure.com/scalesetpriority=spot"
```
The toleration below makes Spot nodes eligible, while preferred affinity biases
scheduling toward Spot when it is available. Because the affinity is preferred
rather than required, compatible regular nodes remain eligible when Spot
capacity is unavailable. This does not promise failover or capacity: the
regular pool must have compatible labels, resources, taints, and available
capacity. Pods pinned to Spot with a `nodeSelector` or required affinity remain
pending when Spot capacity is unavailable.
## Workload Toleration and Preference (add to Deployment YAML)
```yaml
tolerations:
- key: "kubernetes.azure.com/scalesetpriority"
operator: "Equal"
value: "spot"
effect: "NoSchedule"
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: "kubernetes.azure.com/scalesetpriority"
operator: In
values:
- "spot"
```
## Suitability
| Workload | Spot-Suitable? |
|----------|----------------|
| Batch / data processing | Yes |
| Dev / test environments | Yes |
| Stateless web/API (replicas >= 2) | Yes (with care) |
| Jobs with checkpointing | Yes |
| Stateful workloads (databases) | No |
| Single-replica critical services | No |
> Risk: Low for batch/dev. High for production stateful workloads. Spot VMs evict with 30-second notice. Eviction policy Delete is recommended for AKS.
## Handling Eviction Gracefully
Configure workloads to handle the 30-second eviction notice:
```yaml
# Add to Deployment spec β terminationGracePeriodSeconds should be < 30s for Spot
spec:
template:
spec:
terminationGracePeriodSeconds: 25
containers:
- name: <CONTAINER_NAME>
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"] # Drain in-flight requests
```
> A PodDisruptionBudget does not prevent Azure Spot node reclamation. PDBs
> constrain voluntary disruptions performed through the Kubernetes Eviction
> API, such as drains, upgrades, and cluster-autoscaler scale-down. Azure can
> reclaim or delete the underlying Spot VM involuntarily, reducing availability
> below the budget. See [AKS PDB guidance](https://learn.microsoft.com/azure/aks/operator-best-practices-scheduler#limit-disruption-impact-by-using-pod-disruption-budgets-pdbs)
> and [Spot node pool limitations](https://learn.microsoft.com/azure/aks/spot-node-pool).
>
> Use multiple replicas distributed across nodes or other failure domains, and
> retain enough compatible regular-pool capacity for the workload's availability
> requirements. Preferred Spot affinity permits that fallback but cannot
> guarantee scheduling capacity.
azure-aks-vpa.md 1.4 KB
# AKS Vertical Pod Autoscaler (VPA)
Use VPA to get data-driven resource recommendations for rightsizing pods. Always start in recommendation-only mode before considering auto-apply.
## Enable VPA (Recommendation Mode)
```bash
# Enable VPA addon on AKS cluster (if not already enabled)
az aks update --enable-vpa --resource-group <RESOURCE_GROUP> --name <CLUSTER_NAME>
# Create a VPA object in recommendation mode for a deployment
kubectl apply -f - <<EOF
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: <DEPLOYMENT_NAME>-vpa
namespace: <NAMESPACE>
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: <DEPLOYMENT_NAME>
updatePolicy:
updateMode: "Off" # Recommendation only β does not modify pods
EOF
# Read recommendations after 24+ hours of data collection
kubectl describe vpa <DEPLOYMENT_NAME>-vpa -n <NAMESPACE>
```
> Risk: Low in "Off" mode. **Do not use `updateMode: Auto` in production** without thorough testing and explicit user confirmation.
## Read VPA Recommendations
```bash
kubectl get vpa <DEPLOYMENT_NAME>-vpa -n <NAMESPACE> -o jsonpath='{.status.recommendation}'
```
The output shows `lowerBound`, `target`, and `upperBound` for CPU and memory. Use the `target` values as rightsized requests.
## Apply Recommendations Manually
After reviewing VPA output, patch the deployment β see [azure-aks-rightsizing.md](./azure-aks-rightsizing.md#yaml-patch-format) for the patch format.
cli-reference.md 1.2 KB
# CLI Reference for AKS
```bash
# List AKS clusters
az aks list --output table
# Show cluster details
az aks show --name <cluster-name> --resource-group <resource-group>
# Get available Kubernetes versions
az aks get-versions --location <location> --output table
# Create AKS Automatic cluster
az aks create --name <cluster-name> --resource-group <resource-group> --sku automatic \
--network-plugin azure --network-plugin-mode overlay \
--enable-oidc-issuer --enable-workload-identity
# Create AKS Standard cluster
az aks create --name <cluster-name> --resource-group <resource-group> \
--node-count 3 --zones 1 2 3 \
--network-plugin azure --network-plugin-mode overlay \
--enable-cluster-autoscaler --min-count 1 --max-count 10 \
--enable-oidc-issuer --enable-workload-identity
# Get credentials
az aks get-credentials --name <cluster-name> --resource-group <resource-group>
# List node pools
az aks nodepool list --cluster-name <cluster-name> --resource-group <resource-group> --output table
# Enable monitoring
az aks enable-addons --name <cluster-name> --resource-group <resource-group> \
--addons monitoring --workspace-resource-id <workspace-resource-id>
```
optional-aks-operations.md 2.6 KB
# Optional AKS Operations Add-on
The base Azure plugin owns AKS recommendation, Day-0 planning, cluster setup,
application deployment, readiness, and basic operations. The sibling
`aks-skills` plugin is optional and contains focused, deeper operational
workflows.
## Capability and consent flow
1. Check the host-provided available-skill inventory. An approved read-only
native capability query, including a host-supported CLI query where
available, may be used instead. Do not require a CLI on skill-only remote
hosts and do not guess availability from an `aks-` name prefix.
2. If the focused skill is available, invoke it through the host's native skill
capability. Do not install or load every AKS operational skill.
3. If it is missing and useful, explain the task-specific benefit and ask for
consent before installation. After approval, use the host-supported plugin
manager or installation path. Do not invent a host-specific command or
bypass host policy. After installation, confirm the selected skill appears
in the host's available-skill inventory before invoking it. If the host
activates newly installed components only in a new session or after a
restart, say so, continue the base workflow now, and report the focused
skill as installed but not yet active rather than used.
4. If the customer declines, or the host cannot install or execute the add-on,
continue the base workflow with available Azure/Kubernetes reads or supplied
evidence. Do not dead-end or imply that optional execution occurred.
Installation consent does not approve resource mutation, packet capture, or
other execution. Apply each focused skill's own approval gates separately.
## Focused routing
| Skill | Use only when |
|-------|---------------|
| `aks-troubleshooting` | The customer has a live AKS incident in any scope, including workload crashes, node, networking, ingress, upgrade, or scaling problems, that benefits from deeper target-bound investigation. An application-level cause does not exclude an AKS workload incident. |
| `aks-known-issues` | The supplied or retrieved failure includes an exact, fully qualified catalog signature. A quota error, unqualified capacity symptom, or incomplete wrapper is not enough; an exact fully qualified catalog capacity signature remains eligible. |
| `aks-network-capture` | The customer explicitly requests and separately approves a packet capture (`pcap`) workflow. Generic connectivity troubleshooting stays in baseline diagnostics. |
| `aks-gpu-inference` | An existing AKS GPU or inference deployment has a Day-2 incident. Initial GPU cluster, provider, or model setup stays with the base setup workflow. |
License (MIT)
View full license text
MIT License Copyright 2025 (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.