Skip to content

fix(kickstart): prefer az acr build and safeguards validation in cloudshell - #2376

Open
David Gamero (davidgamero) wants to merge 3 commits into
Azure:mainfrom
davidgamero:fix/kickstart-cloudshell-acr-build-and-safeguards
Open

fix(kickstart): prefer az acr build and safeguards validation in cloudshell#2376
David Gamero (davidgamero) wants to merge 3 commits into
Azure:mainfrom
davidgamero:fix/kickstart-cloudshell-acr-build-and-safeguards

Conversation

@davidgamero

@davidgamero David Gamero (davidgamero) commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

kickstart agent should always prefer az acr build command to use remote builds instead of docker daemon local builds

add deployment safeguards validation to skills for manifest generation in kickstart

…safeguards

Kickstart runs in Azure Cloud Shell, which has no Docker daemon, but the phase
skills assumed a local one. Generate listed `docker build` first with ACR as a
fallback, and its image-content check (`docker run ... ls`) was local-only
while the exit criteria still demanded the entry point be confirmed present.
Review then made that same unrunnable check a hard FAIL, so in Cloud Shell the
agent would either stall or fabricate a pass.

Images are now built with `az acr build` unconditionally — server-side on the
ACR remote task builders, never `docker build`. One build path keeps the
validated image and the deployed image identical. Entry-point verification is
a build-time `RUN test -f <entrypoint>` assertion in the Dockerfile, which
fails the ACR build directly and works before the cluster exists. Also
documents the Cloud Shell specifics that still matter: clouddrive clone
target, AcrPush requirement, context upload cost, idle-session handling.

Separately, the safeguard checklist was a pod-security rule set, not the AKS
Deployment Safeguards policy set — 9 of the 10 policies were unrepresented,
including the mutating ones (resource requests, anti-affinity/topology spread,
reserved system pool taints). Omitting those doesn't block admission; the
cluster rewrites the object, so the deployed state silently stops matching the
generated YAML. The checklist is now split into Part A (the 10 policies, with
mutation outcomes) and Part B (pod security), Generate emits compliant
manifests up front, and Deploy gains a `safeguard` failure class plus a
server-side dry-run preview.
@davidgamero
David Gamero (davidgamero) force-pushed the fix/kickstart-cloudshell-acr-build-and-safeguards branch from 8234fe9 to b782630 Compare August 7, 2026 20:31
@davidgamero
David Gamero (davidgamero) marked this pull request as ready for review August 7, 2026 20:33
@pauldotyu Paul Yu (pauldotyu) added this to the 2.6.0 milestone Aug 11, 2026
Three corrections after checking the AKS Deployment Safeguards docs against
what the checklist claimed:

- The anti-affinity safeguard is replica-conditional. Its error names two
  replicas, so single-replica workloads aren't flagged. Also documents what
  the mutator actually injects (preferred anti-affinity weight 100, maxSkew 1
  on kubernetes.io/hostname) and its selector-label priority.
- The resource-requests mutator defaults are much larger than assumed:
  500m CPU and 2048Mi memory per container, not a token value. Minimums are
  100m / 100Mi. Worth stating plainly, since omitting requests on a
  multi-container pod is a real scheduling and cost surprise.
- The CSI safeguard evaluates the StorageClass provisioner, not its name.
  In-tree kubernetes.io/azure-disk and azure-file are rejected in favour of
  disk.csi.azure.com and file.csi.azure.com.

Also records that Gatekeeper runs fail-open, so safeguards are a backstop
rather than a guarantee, and that the policy set is all-or-nothing.

Adds provenance markers to generated artifacts. Nothing kickstart produced
was identifiable as kickstart-generated, while sibling features in this
extension already stamp theirs. Kubernetes objects now get an
app.kubernetes.io/managed-by label plus a kickstart.aks.azure.com/version
annotation, Dockerfiles get com.azure.aks.kickstart.* LABELs, and Bicep gets
tags.

The label/annotation split is deliberate. managed-by is selectable identity,
so it belongs in a label; version is read rather than selected, and label
values reject '+' so a semver with build metadata could not be stored in one.
Both go on the object's own metadata rather than the pod template, since
anything on the template changes the pod-template hash and would force a
rollout on every version bump, and neither may enter a selector.

The restricted-labels safeguard only inspects labels, so the version
annotation is outside its scope, but the reserved kubernetes.azure.com prefix
is still avoided on both fields.

Version is omitted rather than guessed when it can't be determined; the
skills are static markdown with no interpolation, so there is not yet a path
for injecting the extension version.
Uses the literal string v1 rather than the extension version. The marker now
versions the kickstart artifact contract — the label/annotation scheme and the
shape of what kickstart emits — instead of tracking releases, so it only
changes when that contract changes.

This also removes the dependency on injecting the extension version into agent
context, which has no path today: the skills are static markdown with no
interpolation. The previous "omit the version if you can't determine it"
fallback is gone, so generated resources are now consistently stamped.

Keeps the version as an annotation rather than a label. It is read, never
selected on, and label values reject '+', so it could not hold a semver with
build metadata if this later tracks a real release.

@Tatsinnit Tatsat (Tats) Mishra 🐉 (Tatsinnit) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving, (sharing some thoughts for your eyes please) the direction is right and the Part A rewrite is genuinely good work. I checked the numbers against MS Learn and they're exact: A2's 500m/2048Mi defaults and 100m/100Mi minimums, the QoS capping, A3's weight-100 anti-affinity and maxSkew: 1/ScheduleAnyway, the app -> app.kubernetes.io/name -> default-antiaffinity-applabel priority, A8 checking provisioner rather than name, fail-open Gatekeeper, all-or-nothing enforcement. Splitting policy-enforced (Part A) from best-practice (Part B) is a real improvement over what was there.

Caveat on everything below: I'm not 100% sure on these, and I don't have full context on where kickstart is headed. They're things I couldn't reconcile while reading, not blockers — please treat them as "worth a second look" rather than findings, and push back freely where I've got it wrong. Not gating the merge on any of them.

One that has no line to attach to: AGENTS.md:17-18 and kickstart-guide.md:77-78 still say "build & inspect the image" / "verify image builds", which reads like the docker build + docker run ... ls loop this PR replaces. Since AGENTS.md gets loaded as instructions it might be worth pulling those two files in — though possibly that's already queued behind this.

Comment on lines +31 to +36
All container images are built with **`az acr build`** — server-side on the ACR remote task builders. Never `docker build`. Kickstart runs in Azure Cloud Shell, which has no Docker daemon, and a single remote build path keeps behavior identical everywhere.

Cloud Shell constraints to honor:

- Clone into `~/clouddrive/` (persistent, ~5 GB) rather than the ephemeral home dir.
- `az acr build` needs the Phase 2 ACR to exist and the caller to hold `AcrPush` (or `Container Registry Tasks Contributor`) — see `/kickstart-handoff`.

@Tatsinnit Tatsat (Tats) Mishra 🐉 (Tatsinnit) Aug 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Purely as some thoughts 💭 am I missing a planned Cloud Shell mode here? I went looking and couldn't find one — rg -in "cloudshell|clouddrive" comes back empty across the repo before this PR, and the launch path reads as local to me: extension.ts:117 -> kickstartLaunch.ts:27 calls workbench.action.closePanel, kickstartChat.ts:39 opens local Copilot chat, and kickstart-handoff:52 suggests brew install Azure/kubelogin/kubelogin.

If it does run locally, the ~/clouddrive/ line on 35 might send people somewhere that doesn't exist — Discover also covers the "use my current workspace" path, so it isn't only the sample-clone flow that sees it.

Worth noting the existing text at kickstart-generate:48 already justified preferring ACR on "no local daemon available" grounds, which gets you the same outcome without depending on where it runs. But entirely possible there's a direction here I don't have visibility into.

Comment on lines +59 to +60
### A6: reserved-system-pool-taints *(mutating)*
- **Check**: No app workload declares a `CriticalAddonsOnly` toleration, and no user node pool config sets that taint. AKS uses it to keep customer pods off the system pool.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went digging on this one specifically because the rest of Part A is sourced so tightly, and I think A6 may be node-pool-only rather than covering pod tolerations.

The constraint template (Azure/azure-policy, samples/KubernetesService/restricted-taints/template.yaml) binds:

node := input.review.object
taints := {x | x = node.spec.taints[_].key}
not is_system_pool(node)

with is_system_pool keyed off the node's kubernetes.azure.com/mode label. Both fixtures in that directory are kind: Node with spec.taints, and the mutation policy's description says the taint "should not be applied to user pools". I couldn't find anywhere that pod tolerations get evaluated.

If that reading is right, the first half of this check ("No app workload declares a CriticalAddonsOnly toleration") wouldn't have a policy behind it, and A6 would be effectively N/A for generated manifests — which would mean A2 and A3 are the two carrying the mutating weight.

That said, the public constraint template may not be the whole story on an Automatic cluster. If you've seen it actually strip a toleration in practice I'd genuinely like the pointer, and I'll happily be wrong here.

- **Readiness *and* liveness probes** on every container — use the real health path and port from the structure map, never a guessed `/healthz`.
- **Unique Service selectors** — each Service must select exactly one workload. In a monorepo do **not** reuse `app: <appName>` across services; use `app: <serviceName>` (or `app.kubernetes.io/name` + `app.kubernetes.io/component`) so no two Services overlap.
- **No AKS-specific labels** — never set `kubernetes.azure.com/*` labels on your own objects. (`azure.workload.identity/*` labels and annotations are fine and required.)
- **No reserved system-pool taints/tolerations** — do not add a `CriticalAddonsOnly` toleration to app pods; it would place them on the system pool and AKS strips it anyway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small one, related to the A6 thread: I don't think "it would place them on the system pool" holds — a toleration only permits scheduling onto a tainted node, it doesn't attract pods there. You'd still need a nodeSelector or node affinity for them to actually land on the system pool. So a stray toleration on its own probably wouldn't move anything.

Not a big deal in practice since you don't want app pods tolerating it either way, but since this is an instruction the agent acts on, the reasoning it's given might be worth tightening.

```
2. **Assert the entry point at build time.** Add to the Dockerfile's final stage so a wrong path fails the ACR build itself:
```dockerfile
RUN test -f <entrypointPath>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this hold for a distroless or FROM scratch final stage? Neither has a shell, so RUN would fail the build outright — and those are common finals for Go and Rust, both of which show up in the Store Demo sample this skill recommends. (The sample's own Dockerfiles land on alpine:3.22 and debian:bookworm-slim, so the sample itself is fine; generated ones might not be.)

Also worth flagging that test -f proves the path exists, whereas the docker run --rm ... ls -la <workdir> it replaces was catching the case the old text explicitly called out — build succeeds but files land in the wrong directory. I'm not sure there's a clean server-side equivalent, so this may just be a tradeoff worth naming rather than something to fix.

1. **Build and push**: `az acr build --registry <acr> --image <image>:<tag> -f <dockerfilePath> <buildContext>`
`az acr build` runs **server-side on the ACR remote task builders** — always use it, never `docker build` + `docker push`. Kickstart runs in Azure Cloud Shell, which has no Docker daemon, and a single remote build keeps the pushed digest and the deployed image identical.
Use the build context and Dockerfile path from the structure map — never assume repo root (`.`). For monorepos, build each service from its own context. Tag with a version (e.g. v1.0.0), never `:latest`.
Requires `AcrPush` or `Container Registry Tasks Contributor` (verified in Pre-Deploy Check). Keep the command in the foreground so its streamed log holds an idle Cloud Shell session open, and make sure `.dockerignore` excludes bulk (the whole context is uploaded on each build).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"(verified in Pre-Deploy Check)" is accurate here in Phase 7 — but Phase 4 now runs az acr build for validation too, and per kickstart-handoff:89 that wants Container Registry Tasks Contributor, which isn't probed until Phase 6.

As far as I can tell it's never granted either: kickstartProvision.ts:337,740 grant only AcrPull to the cluster's kubelet identity, and kickstartAzureBackend.ts:815 is a stage.warn(...) rather than a gate. So someone without that role might hit a 403 at Phase 4, two phases before the remediation command and /kickstart-pim-activation escalation are loaded.

I realise dropping the local fallback was deliberate, so I'm not suggesting bringing docker build back — would moving just that one ACR permission probe earlier be easier than keeping a fallback path?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants