Kubeasy LogoKubeasy

Creating Your First Challenge

Step-by-step guide to building a complete Kubeasy challenge from scratch.

This guide walks you through creating a complete Kubeasy challenge. By the end, you'll have a working, locally-tested challenge ready to submit.

Prerequisites

  • The Kubeasy CLI installed and kubeasy setup completed (you need the Kind cluster)
  • Basic familiarity with Kubernetes manifests and kubectl

Step 1: Design your challenge

Before writing any YAML, answer these questions:

  1. What Kubernetes concept does this teach? (e.g., resource limits, RBAC, probes)
  2. What challenge type fits? fix (something is broken), build (create from scratch), or migrate (transform an existing setup)
  3. What is the broken/initial scenario? (e.g., pod gets OOMKilled due to a low memory limit)
  4. How do you verify it's fixed? (e.g., pod runs stably, no OOM events)
  5. Is this realistic? Does this problem occur in production?

Example design

We'll create a fix challenge about resource limits:

  • Type: fix
  • Concept: Kubernetes memory limits and OOMKilled pods
  • Scenario: Pod keeps crashing because its memory limit is too low
  • Success criteria: Pod runs stably, no OOMKilled events
  • Realistic: Yes — one of the most common production issues

Step 2: Scaffold the challenge

Use kubeasy dev create to scaffold the directory structure:

cd path/to/challenges-repo
kubeasy dev create

In interactive mode, you'll be prompted for each field:

Challenge name: Memory Pressure
Slug [memory-pressure]:
Type (fix/build/migrate): fix
Theme: resources-scaling
Difficulty (easy/medium/hard): easy
Estimated time (minutes) [30]: 15

Created challenge directory: memory-pressure/
  memory-pressure/challenge.yaml
  memory-pressure/manifests/
  memory-pressure/policies/

Step 3: Fill in challenge.yaml

Edit memory-pressure/challenge.yaml. The scaffold generates a template with placeholder values — replace them with your content.

Key rules:

  • Description: symptoms only, never the root cause
  • Objective: the goal, not the method
  • Validation titles: generic, not revealing
title: Memory Pressure
description: |
  A data processing service keeps restarting unexpectedly.
  It worked fine last week — no code changes were made.
theme: resources-scaling
difficulty: easy
type: fix
estimatedTime: 15
initialSituation: |
  A data processing pod is deployed.
  It starts but crashes within seconds and enters CrashLoopBackOff.
objective: |
  Make the application run stably without being killed by Kubernetes.

objectives:
  - key: pod-ready
    title: "Pod Ready"
    description: "The pod must be running and ready"
    order: 1
    type: condition
    spec:
      target:
        kind: Pod
        labelSelector:
          app: data-processor
      checks:
        - type: Ready
          status: "True"

  - key: stable-operation
    title: "Stable Operation"
    description: "No crash or eviction events in the past 5 minutes"
    order: 2
    type: event
    spec:
      target:
        kind: Pod
        labelSelector:
          app: data-processor
      forbiddenReasons:
        - "OOMKilled"
        - "Evicted"
      sinceSeconds: 300

Step 4: Create the broken manifests

Create memory-pressure/manifests/deployment.yaml with the intentionally broken state:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: data-processor
spec:
  replicas: 1
  selector:
    matchLabels:
      app: data-processor
  template:
    metadata:
      labels:
        app: data-processor
    spec:
      containers:
        - name: processor
          image: python:3.11-slim
          command:
            - python
            - -c
            - |
              import time
              data = []
              for i in range(50):
                  data.append("x" * 1024 * 1024)  # ~50MB total
                  time.sleep(0.1)
              print("Done!")
              while True:
                  time.sleep(60)
          resources:
            limits:
              memory: "32Mi"  # BUG: too low for this workload
              cpu: "100m"
            requests:
              memory: "32Mi"
              cpu: "50m"

The application needs ~50MB of memory, but the limit is 32Mi. Kubernetes will OOMKill the container when it exceeds the limit.

Custom images

If inline commands aren't suitable for your scenario, add an image/ directory with a Dockerfile. The CLI will build and load it into Kind automatically during local development.

Step 5: Add bypass protection

Create memory-pressure/policies/protect.yaml to prevent users from swapping the broken application for a working one:

apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: protect-memory-pressure
  namespace: memory-pressure   # must match the challenge slug
spec:
  validationFailureAction: Enforce
  rules:
    - name: preserve-image
      match:
        resources:
          kinds: ["Deployment"]
          names: ["data-processor"]
      validate:
        message: "Cannot change the container image"
        pattern:
          spec:
            template:
              spec:
                containers:
                  - name: processor
                    image: "python:3.11-slim"

Step 6: Lint

Validate the challenge structure before deploying:

kubeasy dev lint --dir ./memory-pressure
Linting challenge: memory-pressure
✓ All checks passed (0 errors, 0 warnings)

Fix any reported errors before proceeding.

Step 7: Test the broken state

Deploy the challenge and verify that the problem is reproducible:

kubeasy dev apply --dir ./memory-pressure --clean

Check that the pod is crashing:

kubectl get pods -n memory-pressure
# NAME                              READY   STATUS             RESTARTS   AGE
# data-processor-xxx                0/1     CrashLoopBackOff   3          2m

kubectl describe pod -n memory-pressure -l app=data-processor
# Look for OOMKilled in the events

Step 8: Test the solution

Apply a fix manually and verify your validations pass:

kubectl patch deployment data-processor -n memory-pressure \
  --type='json' -p='[
    {"op": "replace", "path": "/spec/template/spec/containers/0/resources/limits/memory", "value": "128Mi"},
    {"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/memory", "value": "64Mi"}
  ]'

Wait for the pod to stabilize, then run validations:

kubeasy dev validate --dir ./memory-pressure
Condition Validation
  pod-ready: PASSED - All condition checks passed

Event Validation
  stable-operation: PASSED - No forbidden events found

All validations passed! (2/2)

Step 9: Verify bypass protection

Try the bypass that your Kyverno policy should block:

kubectl set image deployment/data-processor processor=nginx:latest -n memory-pressure
# Expected: admission webhook denied the request

Step 10: Clean up and submit

kubeasy dev clean --dir ./memory-pressure

Create a branch and open a pull request:

git checkout -b challenge/memory-pressure
git add memory-pressure/
git commit -m "feat: add memory-pressure challenge"
git push origin challenge/memory-pressure

See Contributing Guidelines for the PR template and review process.

Common mistakes

Revealing the root cause in descriptions

# BAD
description: The memory limit is too low. Increase it to fix the crash.

# GOOD
description: A data processing service keeps restarting unexpectedly.

Validation titles that give away the answer

# BAD
- key: memory-fix
  title: "Memory Limit Set to 128Mi"

# GOOD
- key: stable-operation
  title: "Stable Operation"

No bypass protection

Without Kyverno policies, a user can replace the broken app with a working nginx container and pass all validations without solving the problem.

Checking the implementation, not the outcome

# BAD: only one specific value passes
- field: "containerStatuses[0].restartCount"
  operator: "=="
  value: 0

# GOOD: any low restart count passes
- field: "containerStatuses[0].restartCount"
  operator: "<"
  value: 3

Checklist

Before submitting a PR:

  • challenge.yaml has all required fields
  • Description doesn't reveal the root cause
  • Validation titles don't reveal the solution
  • Manifests create a reproducible broken/initial state
  • Kyverno policies prevent obvious bypasses
  • kubeasy dev lint passes
  • Broken state verified manually (pod crashes, service unreachable, etc.)
  • Fix verified — all validations pass after applying the solution
  • Alternative valid solutions tested where applicable
  • Estimated time is reasonable

Next steps

On this page