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 setupcompleted (you need the Kind cluster) - Basic familiarity with Kubernetes manifests and
kubectl
Step 1: Design your challenge
Before writing any YAML, answer these questions:
- What Kubernetes concept does this teach? (e.g., resource limits, RBAC, probes)
- What challenge type fits?
fix(something is broken),build(create from scratch), ormigrate(transform an existing setup) - What is the broken/initial scenario? (e.g., pod gets OOMKilled due to a low memory limit)
- How do you verify it's fixed? (e.g., pod runs stably, no OOM events)
- 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 createIn 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: 300Step 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-pressureLinting 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 --cleanCheck 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 eventsStep 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-pressureCondition 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 requestStep 10: Clean up and submit
kubeasy dev clean --dir ./memory-pressureCreate 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-pressureSee 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: 3Checklist
Before submitting a PR:
-
challenge.yamlhas 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 lintpasses - 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
- Validation Overview — learn the philosophy and all 8 validation types
- Testing Challenges — comprehensive testing strategies
- Contributing — PR requirements and review process