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 (53)
SKILL.md 10.6 KB
---
name: azure-kubernetes
license: MIT
metadata:
author: Microsoft
version: "1.2.2"
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 (troubleshooting AKS), azure-validate (readiness checks), azure-kubernetes-automatic-readiness (migrate existing cluster to AKS Automatic) |
## 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.
## 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 14.4 KB
---
name: azure-kubernetes-automatic-readiness
license: MIT
metadata:
author: Microsoft
version: "1.0.1"
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 enforces **Deployment Safeguards** (21 active policies, some deny, some warn only), **Pod Security Standards** (Baseline mandatory, Restricted optional), **2 active webhook mutators** that auto-fix certain fields at admission (resource-requests defaults and anti-affinity/topology-spread), and **23 cluster-level configuration requirements**.
## Quick Reference
| Property | Value |
|----------|-------|
| Best for | AKS Automatic migration readiness and manifest validation |
| MCP Tools | `mcp_azure_mcp_aks` |
| Related skills | azure-kubernetes (cluster creation), azure-diagnostics (live troubleshooting), 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
| Tool | Purpose | Key Parameters |
|------|---------|----------------|
| `mcp_azure_mcp_aks` | AKS MCP entry point β call `discover` first, then use the assessment action name returned in the response | `subscriptionId`, `resourceGroupName`, `resourceName`, `scope` |
## Workflow
### Step 1: Determine Scope
Ask the user what they want to assess:
**Option A β Cluster-connected assessment (via AKS MCP)**
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
Call the AKS MCP tool β this is the preferred path. Always call `discover` first to get the available actions, then use the assessment action name returned in the response:
```javascript
// Step 1: Discover available actions
mcp_azure_mcp_aks({ action: "discover" })
// Step 2: Use the assessment action name from the discover response
mcp_azure_mcp_aks({
action: "<action-from-discover>",
subscriptionId: "<subscription-id>",
resourceGroupName: "<resource-group>",
resourceName: "<cluster-name>",
scope: {
excludeNamespaces: ["kube-system", "gatekeeper-system"],
workloadTypes: ["Deployment", "StatefulSet", "DaemonSet", "CronJob", "Job"]
}
})
```
**Required permissions:**
- `Microsoft.ContainerService/managedClusters/read`
- `Microsoft.ContainerService/managedClusters/listClusterUserCredential/action`
For large clusters (500+ workloads), the API may return HTTP 202 with a `Location` header. Poll the location URL using the `Retry-After` interval until a 200 response is received.
**Parsing the MCP response:**
1. **`summary`** β aggregate counts: `compatible`, `requiresChanges`, `incompatible`, `autoFixed`, `totalWorkloads`, `clusterConfigIssues`
2. **`clusterConfiguration`** β cluster-level issues with `constraintId`, `severity`, `remediation` (az CLI commands), and `documentationUrl`
3. **`workloads[]`** β per-workload array, each with `name`, `namespace`, `kind`, `overallStatus`, and `issues[]`
Each issue in `workloads[].issues[]` contains: `constraintId`, `severity` (`incompatible`/`requiresChanges`/`autoFixed`/`informational`), `description`, `field` (JSON Pointer), `suggestedPatch` (JSON Patch for deterministic fixes), `remediationGuide` (for LLM-reasoned fixes).
#### Fallback Chain
```
1. MCP tool (mcp_azure_mcp_aks) β preferred, live cluster data
β fails (tool not found β Azure MCP server not configured)
2. Offline validation β works on local manifests without any cluster
```
If `mcp_azure_mcp_aks` is not available, inform the user:
> "The Azure MCP server is not configured in your editor. To enable live cluster assessment, follow the setup guide at [aka.ms/azure-mcp-setup](https://aka.ms/azure-mcp-setup). For now, I can validate your local manifests offline."
Then proceed to offline mode.
#### Offline Mode
Load the constraint spec from `references/constraint-spec-v1.yaml` and evaluate each manifest. The check field tells you what to check for and what fields to check. The fix field will tell you any allowed values and possible fixes. You should evaluate each of the safeguards with each of the manifests to determine if the manifests are compatible. Suggest any fixes that are needed.
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 any container probes and lifecycle hooks β `safeguard-host-probes`
**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)
**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` | No enforcement | Mention briefly |
### 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** (have `suggestedPatch` β generate YAML diff directly):
- `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, or when the MCP `suggestedPatch` explicitly requires a seccomp change
- `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.
**LLM-reasoned fixes** (require app context; use `remediationGuide`):
- `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 all eligible `suggestedPatch`-based fixes, 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. Perform the SKU switch β follow the migration guide.
4. Verify β after migration, check all workloads are running and healthy.
```
See `references/migration-guide-summary.md` for the full migration 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 |
|-----------------|--------------|-------------|
| MCP tool call fails or times out | Invalid credentials or subscription context | Verify `az login`, confirm active subscription with `az account show`; if MCP remains unavailable, continue with offline validation using local or exported manifests and the bundled constraint spec |
| HTTP 403 on assessment action | Missing permission | Ensure caller has sufficient RBAC access to read and assess the cluster via AKS APIs |
| API returns HTTP 202 | Large cluster (500+ workloads) β async operation | Poll the `Location` header URL using `Retry-After` interval |
| Helm chart uses Go templating β cannot evaluate | Template values not resolved | Ask user for rendered output (`helm template`) or values files |
| Constraint spec version mismatch | Skill bundles spec v1.1.1 (2026-03-15) | 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 troubleshooting MCP tool calls or debugging the fallback chain |
> β οΈ **Warning:** This skill bundles **constraint spec v1.1.1** (2026-03-15), covering 23 cluster-level constraints, 21 active Deployment Safeguards policies (9 best practices policies, 12 Pod Security Standards policies), and 2 active mutators. Always note the spec version in assessment output.
common-fixes.md 6.6 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-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
**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 18.9 KB
# AKS Automatic Compatibility Constraint Spec β Condensed Reference
# Version: 1.1.1 | AKS: 2026-03-15
# This condensed version is optimized for LLM context.
apiVersion: aks-automatic.azure.com/v1
kind: ConstraintSpecReference
metadata:
version: "1.1.1"
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 total)
# =============================================================================
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 active policies)
# Initiative: c047ea8e | Effect: Mixed(Deny/Warn/Mutate) on Automatic
# =============================================================================
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.
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: requiresChanges
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.
# =============================================================================
# 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 6.5 KB
# MCP Integration Reference
Loaded when troubleshooting MCP tool calls, debugging the fallback chain, or understanding the API response format.
---
## Tool Discovery
Always call `mcp_azure_mcp_aks` first to discover the current available tool surface. Do not assume a fixed action name β the available actions depend on the MCP server version deployed to the client.
```javascript
mcp_azure_mcp_aks({ action: "discover" })
```
The response lists available actions and their parameter schemas. Use the returned schema β do not hardcode parameter names.
---
## Assessment Call
After calling `discover`, use the assessment action name returned in the response. Pass parameters according to the discovered schema β do not hardcode action names or API versions.
Typical parameters include:
- `subscriptionId` β Azure subscription ID
- `resourceGroupName` β resource group containing the cluster
- `resourceName` β AKS cluster name
- `scope` (optional) β filter by namespaces or workload types
Example shape (use actual action name and schema from discover output):
```javascript
mcp_azure_mcp_aks({
action: "<action-from-discover>",
subscriptionId: "<subscription-id>",
resourceGroupName: "<resource-group>",
resourceName: "<cluster-name>",
scope: {
excludeNamespaces: ["kube-system", "gatekeeper-system", "azure-arc"],
workloadTypes: ["Deployment", "StatefulSet", "DaemonSet", "CronJob", "Job"]
}
})
```
All `scope` parameters are optional. If omitted, the API assesses all workloads excluding `kube-system` and `gatekeeper-system`.
---
## Required Permissions
```bash
# Check current role assignments
az role assignment list \
--assignee $(az ad signed-in-user show --query id -o tsv) \
--scope /subscriptions/<subscription-id>/resourceGroups/<rg>/providers/Microsoft.ContainerService/managedClusters/<cluster>
# Minimum permissions required:
# - Microsoft.ContainerService/managedClusters/read
# - Microsoft.ContainerService/managedClusters/listClusterUserCredential/action
# Assign if missing (requires Owner or User Access Administrator)
az role assignment create \
--assignee <principal-id> \
--role "Azure Kubernetes Service Cluster User Role" \
--scope /subscriptions/<subscription-id>/resourceGroups/<rg>/providers/Microsoft.ContainerService/managedClusters/<cluster>
```
---
## Response Schema
The API returns three top-level sections:
### `summary`
```json
{
"summary": {
"totalWorkloads": 42,
"compatible": 27,
"requiresChanges": 12,
"incompatible": 3,
"autoFixed": 8,
"clusterConfigIssues": 4
}
}
```
### `clusterConfiguration`
```json
{
"clusterConfiguration": [
{
"constraintId": "cluster-oidc-issuer",
"severity": "requiresChanges",
"description": "OIDC issuer not enabled",
"remediation": "az aks update --enable-oidc-issuer --resource-group <rg> --name <cluster>",
"documentationUrl": "https://learn.microsoft.com/azure/aks/..."
}
]
}
```
### `workloads[]`
```json
{
"workloads": [
{
"name": "sample-app",
"namespace": "default",
"kind": "Deployment",
"overallStatus": "requiresChanges",
"issues": [
{
"constraintId": "safeguard-images-no-latest",
"severity": "requiresChanges",
"description": "Container 'web' uses :latest image tag",
"field": "/spec/containers/0/image",
"suggestedPatch": null,
"remediationGuide": "Pin the image to a specific version or SHA digest"
}
]
}
]
}
```
---
## Async Response Handling (HTTP 202 β Large Clusters)
For clusters with 500+ workloads, the API returns HTTP 202 Accepted with a `Location` header. Poll until complete:
```javascript
// Initial call returns: { status: 202, headers: { Location: "...", "Retry-After": "30" } }
async function pollAssessment(locationUrl, retryAfterSeconds) {
while (true) {
await new Promise(r => setTimeout(r, retryAfterSeconds * 1000));
const response = await mcp_azure_mcp_aks({
action: "pollOperation",
locationUrl: locationUrl
});
if (response.status === "Succeeded") return response.result;
if (response.status === "Failed") throw new Error(response.error.message);
retryAfterSeconds = response.retryAfter ?? retryAfterSeconds;
}
}
```
---
## Fallback Chain
Attempt each step in order. Do not ask the user which is available β just try:
```
1. mcp_azure_mcp_aks β discover, then call the assessment action returned
β fails (tool not found β Azure MCP server not configured)
2. Inform user to install Azure MCP, then fall back to offline validation
kubectl get deployment,statefulset,daemonset,job,cronjob -A -o yaml > /tmp/workloads.yaml
kubectl get pdb,storageclass -A -o yaml > /tmp/policies.yaml
```
If `mcp_azure_mcp_aks` is not available, say:
> "The Azure MCP server is not configured. To enable live cluster assessment, install it following [aka.ms/azure-mcp-setup](https://aka.ms/azure-mcp-setup). For now, I can validate your local manifests offline β export them with `kubectl get ... -o yaml` or share your manifest files."
Then proceed to offline manifest validation against `constraint-spec-v1.yaml`.
---
## Prerequisites Verification
Run these before attempting MCP or CLI assessment:
```bash
# 1. Verify Azure login
az account show --query "{name:name, id:id, state:state}" -o table
# 2. Verify cluster exists and is accessible
az aks show \
--resource-group <rg> \
--name <cluster> \
--query "{name:name, provisioningState:provisioningState, sku:sku.name}" \
-o table
# 3. Verify kubectl context
kubectl config current-context
kubectl cluster-info
```
```javascript
// 4. Verify MCP server is reachable (Azure MCP)
// If this returns available actions, MCP is configured
mcp_azure_mcp_aks({ action: "discover" })
```
---
## Common MCP Errors
| Error | Cause | Fix |
|---|---|---|
| `tool not found: mcp_azure_mcp_aks` | Azure MCP server not configured | Guide user to install: [aka.ms/azure-mcp-setup](https://aka.ms/azure-mcp-setup), then fall back to offline |
| `HTTP 401 Unauthorized` | Not logged in | `az login` |
| `HTTP 403 Forbidden` | Insufficient RBAC permissions | Ensure caller has read access to the cluster via AKS APIs |
| `HTTP 404 Not Found` | Wrong subscription, RG, or cluster name | Verify with `az aks list -o table` |
| `HTTP 202` with no Location header | API version mismatch | Ensure the MCP server version supports async polling; retry with the latest server |
| Timeout after 30s | Cluster too large (500+ workloads) | Implement async polling β see section above |
migration-guide-summary.md 4.4 KB
# AKS Automatic Migration Guide
Loaded when user asks about migration steps or after assessment is complete.
---
## Migration Checklist
### Phase 1 β Assessment (this skill)
- [ ] Run the AKS Automatic compatibility assessment (via `mcp_azure_mcp_aks({ action: "discover" })` then the assessment action returned, or the offline manifest scan)
- [ ] 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
- [ ] Address cluster-level Day-0 config issues (see below)
### Phase 2 β Create AKS Automatic Cluster (use `azure-kubernetes` skill)
```bash
az aks create \
--resource-group <resource-group> \
--name <new-cluster-name> \
--sku automatic \
--location <location> \
--generate-ssh-keys
```
> π‘ **Tip:** AKS Automatic auto-enables: OIDC issuer, workload identity, Azure CNI Overlay, NAP, VPA, Azure Monitor Container Insights, Deployment Safeguards, and Pod Security Standards (Baseline). No manual configuration needed for these.
### Phase 3 β Validate on New Cluster
```bash
# Get credentials
az aks get-credentials \
--resource-group <resource-group> \
--name <new-cluster-name>
# Dry-run server-side apply β catches admission policy rejections
kubectl apply --dry-run=server -f <manifests-directory>/
# Deploy to a staging namespace first
kubectl create namespace staging
kubectl apply -f <manifests-directory>/ -n staging
# Watch pod startup
kubectl get pods -n staging -w
# Check events for admission rejections
kubectl get events -n staging --sort-by=.lastTimestamp | grep -i "denied\|error\|failed"
```
> β οΈ **Keep the old cluster running** for a rollback window (recommended: 48 hours minimum) while you validate workloads on the new AKS Automatic cluster.
### Phase 4 β Decommission Old Cluster
```bash
# Only after confirming workloads are stable on AKS Automatic
az aks delete \
--resource-group <resource-group> \
--name <old-cluster-name> \
--yes --no-wait
```
---
## Day-0 Decisions β Cluster-Level Configuration Requirements
Some settings require creating a **new** cluster; others can be enabled on existing clusters. Route to `azure-kubernetes` skill for cluster creation.
| Requirement | AKS Automatic default | What to do |
|---|---|---|
| API Server VNet Integration | Required, auto-enabled | Requires a new cluster |
| Network plugin | Azure CNI Overlay | Requires a new cluster if currently on kubenet |
| System node pool OS | Azure Linux | Recreate system node pool (user pools unaffected) |
| OIDC Issuer | Auto-enabled | Can be enabled on existing: `az aks update --enable-oidc-issuer` |
| Workload Identity | Auto-enabled | Can be enabled on existing: `az aks update --enable-workload-identity` |
---
## 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 | 25 active deny policies + 2 webhook mutators at admission (resource-requests defaults + anti-affinity/topology-spread) |
| 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
# Verify all pods running
kubectl get pods -A | grep -v Running | grep -v Completed
# Check for pods stuck in Pending (may indicate resource quota or node issues)
kubectl get pods -A --field-selector status.phase=Pending
# Check Deployment Safeguards are active
kubectl get constrainttemplate -A
# Verify VPA is running
kubectl get vpa -A
# Check NAP node pools
az aks nodepool list \
--resource-group <resource-group> \
--cluster-name <cluster-name> \
--query "[].{name:name, mode:mode, osType:osType, count:count}" \
-o table
# View Container Insights metrics
az aks show \
--resource-group <resource-group> \
--name <cluster-name> \
--query addonProfiles.omsagent.enabled
```
azure-aks-autoscaler.md 3.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:
> β οΈ **Warning:** Setting `skip-nodes-with-system-pods=false` allows CAS to evict system pods. Ensure all system pods in `kube-system` have PodDisruptionBudgets before enabling this.
```bash
az aks update \
--name "<CLUSTER_NAME>" --resource-group "<RESOURCE_GROUP>" \
--cluster-autoscaler-profile \
scale-down-delay-after-add=10m \
scale-down-unneeded-time=10m \
scale-down-utilization-threshold=0.5 \
max-graceful-termination-sec=600 \
skip-nodes-with-system-pods=false
```
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 3.7 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"
```
Pods that tolerate Spot but don't require it (no `nodeSelector` or required node affinity pinning them to the Spot pool) will be rescheduled onto the regular pool after eviction. Pods pinned to Spot via `nodeSelector` cannot reschedule and will remain pending until a Spot node is available again.
## Workload Toleration (add to Deployment YAML)
```yaml
tolerations:
- key: "kubernetes.azure.com/scalesetpriority"
operator: "Equal"
value: "spot"
effect: "NoSchedule"
nodeSelector:
kubernetes.azure.com/scalesetpriority: 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
```
Set a PodDisruptionBudget to limit simultaneous evictions:
```bash
kubectl apply -f - <<EOF
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: <APP_NAME>-pdb
namespace: <NAMESPACE>
spec:
minAvailable: 1
selector:
matchLabels:
app: <APP_NAME>
EOF
```
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>
```
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.