agentleFS
Microsoft AzureKnown publisherAzure/containerization-assist/skills/generate-k8s-manifests/SKILL.md

generate-k8s-manifests

Skill43 starsChanged 5 months agoMITView on GitHub

Generate production-ready Kubernetes manifests (Deployment, Service, ConfigMap, Secret, ServiceAccount, optional HPA) for a containerized application. Use AFTER a Dockerfile exists and has been validated by fix-dockerfile. Triggers include "generate kubernetes manifests", "deploy this to k8s", "write k8s yaml", "create deployment yaml", or when running aks-loop and image is built. Writes manifests directly to the target directory.

The file

Show its rules and text

We split this skill into the rules it gives an agent

19 rules. About 0 are common, 6 are distinctive, and 13 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 13

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If modulePath is missing → ask the user.

    line 33
  2. Very specific:

    If the user supplied k8sConfig.resourceDefaults, use those instead.

    line 90
  3. Very specific:

    If you cannot infer a meaningful HTTP endpoint, fall back to a TCP socket probe on containerPort.

    line 103
  4. Very specific:

    If detectedDatabases includes any of these → emit a ServiceAccount called <appName>-sa with the Azure Workload Identity annotations (see …

    line 115
  5. Distinctive:

    If no managed DB is detected → omit the ServiceAccount, the Deployment uses the namespace's default SA.

    line 119
  6. Very specific:

    Always required: Deployment, Service.

    line 124
  7. Very specific:

    Do NOT rename the annotation key to containerization-assist/version.

    line 146
  8. Very specific:

    If readOnlyRootFilesystem: true would break the framework (Java apps that write to /tmp, ASP.NET Core), add a volumeMounts + emptyDir …

    line 236
  9. Distinctive:

    If any check fails, fix the YAML before writing.

    line 362
  10. Very specific:

    Create outputDir (default <modulePath>/k8s/) if it doesn't exist.

    line 367
  11. Distinctive:

    Write one file per resource, using these names (skip files for resources you did not emit):

    line 368
  12. Distinctive:

    Use the chat environment's file-edit/write capability.

    line 380
  13. Distinctive:

    Use this exact format (succinct — the YAML files are the artifact):

    line 385
  14. Distinctive:

    NEVER print full YAML in chat unless the user explicitly asks ("show me", "print the deployment", etc.).

    line 419
  15. Very specific:

    NEVER put real secret values in a Secret.

    line 421
  16. Very specific:

    NEVER omit securityContext.runAsNonRoot: true or allowPrivilegeEscalation: false.

    line 422
  17. Very specific:

    NEVER set privileged: true, hostNetwork: true, hostPID: true, or hostIPC: true.

    line 423
  18. Very specific:

    NEVER pick the latest tag for the image.

    line 424
  19. Very specific:

    NEVER emit helm or kustomize — out of scope for this skill.

    line 426

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Generate Kubernetes Manifests

Produce a complete, production-ready set of Kubernetes manifests for one application in one module and write them to disk. Deterministic — every choice below comes from a fixed rule or table, not the model's judgment. The artifacts are the YAML files on disk, not chat output.

Inputs

FieldRequiredDescription
modulePathyesAbsolute path of the module to deploy (a module from analyze-repo).
appNameoptionalKubernetes resource name. Defaults to the directory basename of modulePath. Must be a valid DNS-1123 label (lowercase, digits, -, ≤ 63 chars). If the resolved value is invalid, normalize it (lowercase, replace invalid chars with -) and tell the user.
imageoptionalContainer image reference (registry/repo:tag). Defaults to <appName>:dev (caller must replace it before deploy). Used for Deployment.spec.template.spec.containers[].image.
namespaceoptionalDefaults to default.
environmentoptionalproduction (default) or development. Affects replicas, probes, resource defaults.
languageoptionalFrom analyze-repo. Used to pick default container port if ports empty.
frameworkoptionalFrom analyze-repo. Same.
portsoptionalFrom analyze-repo. First port becomes containerPort + Service.targetPort.
detectedEnvVarsoptionalFrom analyze-repo. Used to build ConfigMap (config/database types) and Secret (secret type).
detectedDatabasesoptionalFrom analyze-repo. If any database is in the managed set (see below), emit a ServiceAccount with workload-identity annotations + reference it from the Deployment.
trafficLeveloptionalhigh | medium | low. Affects replica count + HPA.
criticalityTieroptionaltier-1 (mission-critical) | tier-2 | tier-3. Same.
manifestTypeoptionalkubernetes (default), helm, kustomize, aca. This skill only handles kubernetes. For helm / kustomize / aca, tell the user it's out of scope and STOP.
outputDiroptionalWhere to write the YAML files. Default: <modulePath>/k8s/.

If modulePath is missing → ask the user. STOP.

Prerequisite: analyze-repo

This skill needs language, framework, ports, detectedDatabases, and detectedEnvVars from the analyze-repo skill. If you don't already have that output for <modulePath> from earlier in this conversation, follow the analyze-repo skill against <modulePath> first, then come back.

Procedure

Step 1 — Resolve defaults

Container port — if ports[0] is set, use it. Otherwise pick from the language default:

Language / frameworkDefault port
Spring Boot8080
Java (other)8080
ASP.NET Core8080
Node / Express / Next / Fastify3000
Python / Flask5000
Python / Django8000
Python / FastAPI / Uvicorn8000
Go (gin / echo / fiber / chi)8080
Rust (axum / actix / rocket)8080
PHP-FPM9000
PHP / Apache80
Ruby on Rails3000
Unknown8080

Replica count — start from this table, then override if both trafficLevel and criticalityTier are given (lower row wins):

EnvironmentDefault replicas
development1
production2
Traffic × Criticalityreplicas
high × tier-15
high × tier-23
medium × tier-13
medium × tier-22
low × any1
tier-3 (any traffic)1

Resources — fixed defaults (requests ≈ 50% of limits):

Environmentrequests.cpurequests.memorylimits.cpulimits.memory
development100m128Mi500m512Mi
production250m256Mi500m512Mi

If the user supplied k8sConfig.resourceDefaults, use those instead.

Probes — pick a probe path:

Frameworkpath
Spring Boot/actuator/health (liveness), /actuator/health/readiness (readiness)
ASP.NET Core/health
Express / Fastify with health-checks dependency/health
FastAPI / Flask / Django/health
Go web frameworks/health
Others/ (TCP socket probe fallback — see below)

If you cannot infer a meaningful HTTP endpoint, fall back to a TCP socket probe on containerPort.

In development environment, omit probes entirely (keeps startup simple).

Managed databases — workload identity is required for any DB whose dbType is in:

{ postgres, mysql, mongodb, redis, mssql, cosmosdb }

If detectedDatabases includes any of these → emit a ServiceAccount called <appName>-sa with the Azure Workload Identity annotations (see Step 2). Use this SA in the Deployment.spec.template.spec.serviceAccountName.

If no managed DB is detected → omit the ServiceAccount, the Deployment uses the namespace's default SA.

Step 2 — Build each manifest

Always required: Deployment, Service. Conditional: ConfigMap (any config / database env var), Secret (any secret env var — values must be placeholders, never real secrets), ServiceAccount (managed DB detected), HorizontalPodAutoscaler (production AND trafficLevel ∈ {high, medium}).

Required labels & annotations (on every resource's metadata)

metadata:
  name: <appName>[-suffix]
  namespace: <namespace>
  labels:
    app.kubernetes.io/name: <appName>
    app.kubernetes.io/managed-by: containerization-assist
  # Include the annotation only if the current `containerization-assist`
  # package version is readable from the environment; otherwise omit
  # the entire `annotations:` block. Never hard-code a stale version.
  annotations:
    com.azure.containerizationassist/version: <version>

Do NOT rename the annotation key to containerization-assist/version.

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: <appName>
  namespace: <namespace>
  labels:
    app.kubernetes.io/name: <appName>
    app.kubernetes.io/managed-by: containerization-assist
spec:
  replicas: <from Step 1>
  selector:
    matchLabels:
      app.kubernetes.io/name: <appName>
  template:
    metadata:
      labels:
        app.kubernetes.io/name: <appName>
        app.kubernetes.io/managed-by: containerization-assist
      # If a managed DB is detected, add this annotations block (matches the ServiceAccount + serviceAccountName below).
      # Otherwise omit the entire `annotations:` key.
      annotations:
        azure.workload.identity/use: "true"
    spec:
      # Include `serviceAccountName` only if a managed DB is detected (matches the workload-identity annotation above).
      serviceAccountName: <appName>-sa
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        fsGroup: 10001
        seccompProfile: { type: RuntimeDefault }
      containers:
        - name: <appName>
          image: <image>
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: <port>
              protocol: TCP
          env:                                  # one entry per env var (see below)
            - name: PORT
              value: "<port>"
          # Include `envFrom` only for the refs whose backing object you emit:
          # `configMapRef` only if a ConfigMap was emitted; `secretRef` only if a Secret was emitted.
          envFrom:
            - configMapRef:
                name: <appName>-config
            - secretRef:
                name: <appName>-secret
          resources:
            requests:
              cpu: <see Step 1>
              memory: <see Step 1>
            limits:
              cpu: <see Step 1>
              memory: <see Step 1>
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          # Include `livenessProbe` and `readinessProbe` only in production. Omit both in development.
          livenessProbe:
            httpGet:
              path: <probe path>
              port: http
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: <probe path>
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5

Env-var emission rules (per entry in detectedEnvVars):

  • classification == "config" or "database" with a defaultValue → becomes a key in the ConfigMap. Reference via envFrom.configMapRef.
  • classification == "config" or "database" without a defaultValue → becomes a key in the ConfigMap with value "" (user must fill in).
  • classification == "secret" → key in the Secret with placeholder value "REPLACE_ME". Reference via envFrom.secretRef. NEVER include any real defaultValue for secret-classified entries.

If readOnlyRootFilesystem: true would break the framework (Java apps that write to /tmp, ASP.NET Core), add a volumeMounts + emptyDir volume for /tmp:

volumes:
  - name: tmp
    emptyDir: {}
# inside container:
volumeMounts:
  - name: tmp
    mountPath: /tmp

Service

apiVersion: v1
kind: Service
metadata:
  name: <appName>
  namespace: <namespace>
  labels:
    app.kubernetes.io/name: <appName>
    app.kubernetes.io/managed-by: containerization-assist
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: <appName>
  ports:
    - name: http
      port: 80
      targetPort: http
      protocol: TCP

ConfigMap (only if any non-secret env vars)

apiVersion: v1
kind: ConfigMap
metadata:
  name: <appName>-config
  namespace: <namespace>
  labels:
    app.kubernetes.io/name: <appName>
    app.kubernetes.io/managed-by: containerization-assist
data:
  <KEY>: "<defaultValue or empty string>"
  ...

Secret (only if any secret env vars)

apiVersion: v1
kind: Secret
metadata:
  name: <appName>-secret
  namespace: <namespace>
  labels:
    app.kubernetes.io/name: <appName>
    app.kubernetes.io/managed-by: containerization-assist
type: Opaque
stringData:
  <KEY>: "REPLACE_ME"
  ...

ServiceAccount (only if managed DB detected)

apiVersion: v1
kind: ServiceAccount
metadata:
  name: <appName>-sa
  namespace: <namespace>
  labels:
    app.kubernetes.io/name: <appName>
    app.kubernetes.io/managed-by: containerization-assist
  annotations:
    azure.workload.identity/client-id: "<REPLACE_WITH_USER_ASSIGNED_IDENTITY_CLIENT_ID>"

Also add the Deployment pod-template annotation: azure.workload.identity/use: "true".

HorizontalPodAutoscaler (only if production AND trafficLevel ∈ {high, medium})

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: <appName>
  namespace: <namespace>
  labels:
    app.kubernetes.io/name: <appName>
    app.kubernetes.io/managed-by: containerization-assist
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: <appName>
  minReplicas: <replicas from Step 1>
  maxReplicas: <minReplicas * 3, min 5, max 20>
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 70 }

Step 3 — Self-check before writing

For each generated YAML, confirm:

  • apiVersion and kind present
  • metadata.name matches <appName> (or <appName>-<suffix>)
  • metadata.namespace matches input
  • Required labels (app.kubernetes.io/name, app.kubernetes.io/managed-by) present
  • Deployment: runAsNonRoot: true, runAsUser ≥ 1000, no privileged, no hostNetwork, no hostPID
  • Deployment: resources requests and limits both set
  • No real secret values (only REPLACE_ME placeholders) in Secret
  • If managed DB present: ServiceAccount exists AND Deployment references it AND azure.workload.identity/use annotation on pod template
  • YAML parses cleanly (no tabs, consistent 2-space indent)

If any check fails, fix the YAML before writing. Never write a manifest that fails the security checks.

Step 4 — Write to disk

  • Create outputDir (default <modulePath>/k8s/) if it doesn't exist.
  • Write one file per resource, using these names (skip files for resources you did not emit):

    FileResource
    deployment.yamlDeployment
    service.yamlService
    configmap.yamlConfigMap
    secret.yamlSecret
    serviceaccount.yamlServiceAccount
    hpa.yamlHorizontalPodAutoscaler
  • Use the chat environment's file-edit/write capability. Do NOT ask the user to copy-paste. Do NOT print the YAML in chat unless the user asks.

Step 5 — Output

Use this exact format (succinct — the YAML files are the artifact):

**Kubernetes manifests** — `<appName>` in `<namespace>`

### Result
✅ Wrote N manifest(s) to `<outputDir>`:
- `deployment.yaml` — N replicas, image `<image>`
- `service.yaml` — ClusterIP on port 80 → containerPort <port>
- `configmap.yaml` — N keys                  *(omit line if not emitted)*
- `secret.yaml` — N keys (all `REPLACE_ME`)  *(omit line if not emitted)*
- `serviceaccount.yaml` — workload identity for <DB list>  *(omit line if not emitted)*
- `hpa.yaml` — min N, max M, target CPU 70%  *(omit line if not emitted)*

### Configuration summary
- **Replicas:** <N> (<reason — e.g. "production default", "trafficLevel=high × tier-1">)
- **Resources:** requests cpu=<v> mem=<v>, limits cpu=<v> mem=<v>
- **Probes:** <"liveness + readiness on <path>" or "disabled (development)" or "TCP fallback">
- **Security context:** runAsNonRoot=true, readOnlyRootFilesystem=true, capabilities dropped
- **Workload identity:** <"enabled — replace client-id annotation with your User-Assigned Identity ID" or "not required">

### Action required before deploy
- <list every `REPLACE_ME` value in secret.yaml and the workload identity client-id if SA was emitted>
- <"Replace placeholder image <appName>:dev" if image was defaulted>

### Next steps
1. `kubectl apply -n <namespace> -f <outputDir>/`
2. Run **verify-deploy** (tool) to confirm rollout succeeds.
3. Run **scan-image** (tool) on the image before promoting to production.

Constraints

  • The artifacts are the files in <outputDir>. Always write them to disk.
  • NEVER print full YAML in chat unless the user explicitly asks ("show me", "print the deployment", etc.).
  • NEVER put real secret values in a Secret. Always REPLACE_ME.
  • NEVER omit securityContext.runAsNonRoot: true or allowPrivilegeEscalation: false.
  • NEVER set privileged: true, hostNetwork: true, hostPID: true, or hostIPC: true.
  • NEVER pick the latest tag for the image. If user gave one, warn and proceed but mark it in "Action required".
  • NEVER emit helm or kustomize — out of scope for this skill.
  • Manifest order in kubectl apply -f matters: this skill writes one file per kind, so kubectl handles ordering naturally.

Failure modes

SymptomAction
modulePath doesn't existEcho path; ask for a valid one. STOP.
appName invalid for DNS-1123Normalize; tell user; continue with normalized name.
User asked for helm / kustomizeDecline politely; suggest manual chart authoring. STOP.
Cannot infer port AND no ports providedUse 8080; note in Configuration summary that the port is a guess.
outputDir is not writableSurface the OS error; ask user for a different path. STOP.
User provided real-looking secret values in detectedEnvVarsStrip them; write REPLACE_ME; surface a warning.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.