Skip to content

Lab instruction style and voice

About this guide

This page teaches you how to write the instruction file for a lab exercise. Read it through before you build your first lab, and keep it open as you draft. If you would rather start hands-on, the Quickstart walks you through building your first lab, and you return here for the details.

What a lab is

A lab walks a learner through a procedure on one or more sandbox virtual machines, one step at a time. The learner works on those machines through the provided tools: the terminal for command-line work, the IDE for editing files, and a browser when the lab opens a web UI. You write the walk-through as a Markdown file that the learner reads in the instructions tool, the fixed panel on the left of their browser, while they work; they see the rendered Markdown there, not your source. At its core, a lab is a numbered list: each step pairs a short instruction with the command or code it calls for and the expected output.

How this guide is organized

The sections follow the order you work in as you build a lab:

  • Structure — the overall shape of the file, and whether your lab is one page or several.
  • Headings and the opening — the title and the framing that orients the learner.
  • Writing steps — how to build, format, and word a step so a learner can follow it.
  • Commands and prompts — how to present the commands a learner runs.
  • Expected output — how to show what the learner should see.
  • File contents — how to show a file that the learner edits or creates.
  • Paths and ownership — where files live, and how that follows from who owns them.
  • Admonitions — the callouts for overviews, warnings, and references.
  • Labs that span machines or pages — moving between terminals, and the multi-page layout.
  • Closing the lab — how a lab ends.
  • Common pitfalls — the patterns reviewers reject, to check before you publish.

Structure

Your lab's instructions are either a single file (a one-page walk-through) or a directory of files (a multi-page one), and choosing between them is the first decision you make, before you write any steps. The sourcePath field in sandbox.yaml points at the file or directory, resolved relative to the sandbox.yaml.

Layout sandbox.yaml field When to use it
Single file (most common) sourcePath: instructions.md One file is enough to hold the walk-through.
Directory sourcePath: instructions/ (a folder containing index.md plus any sibling .md files) The lab offers both a high-level and a detailed walk-through, or a long lab splits into navigable sections.

The sandbox renders the file (or directory) into the instructions tool.

About these examples

The Source block is exactly what you write in the file. The Rendered preview below it is what the learner sees in the instructions tool.

The smallest complete lab looks like this. Copy it and adapt as you go; each component is covered in detail in the sections ahead.

instructions.md
# 1.1 Hello Cluster

!!! info "Overview"

    Confirm that the cluster is reachable and the default namespace exists.

1. Check the cluster nodes from the cp tab.

    ```bash title="student@cp:~$"
    kubectl get nodes
    ```

    !!! success "Expected Output"
        ```{.text .no-copy}
        NAME     STATUS   ROLES           AGE     VERSION
        cp       Ready    control-plane   2m12s   v1.35.2
        worker   Ready    <none>          1m05s   v1.35.2
        ```

2. List the namespaces.

    ```bash title="student@cp:~$"
    kubectl get namespaces
    ```

1.1 Hello Cluster

Overview

Confirm that the cluster is reachable and the default namespace exists.

  1. Check the cluster nodes from the cp tab.

    student@cp:~$
    kubectl get nodes
    

    Expected Output

    NAME     STATUS   ROLES           AGE     VERSION
    cp       Ready    control-plane   2m12s   v1.35.2
    worker   Ready    <none>          1m05s   v1.35.2
    
  2. List the namespaces.

    student@cp:~$
    kubectl get namespaces
    

Headings and the opening

The top of every lab file holds two things: the title (the H1) and a short opening that frames what the learner is about to do.

Start with the H1

A lab file opens directly with its H1. There is no YAML frontmatter at the top. The lab is identified by its directory name, and the renderer takes the H1 as the title of the instructions tool.

The H1 is the lab's title and its place in the course

The H1 carries the chapter section number and the lab title, in the form # <section>.<subsection> <Title>. The number is not decoration: it anchors the lab in the course outline and matches the numbering the learner already sees there. Because the renderer shows the H1 as the lab's title, a number on its own gives the learner no clue what the lab covers, so always pair the number with a title. Keep the title concise and descriptive of the task, matching the wording in the course outline.

The opening frames the lab

Right below the H1, a short opening explains what the lab covers before the first step. Write it as an ## Overview heading or an !!! info "Overview" admonition (see Admonitions for the full set). An opening is a sentence or two that names what the lab is about and, when it helps, why it matters. The opening is optional: a short, single-procedure lab can go straight from the H1 to the first step.

instructions.md
# 3.1 Install Kubernetes

## Overview

We will install Kubernetes on a single node, then grow the cluster, adding more
compute resources.

3.1 Install Kubernetes

Overview

We will install Kubernetes on a single node, then grow the cluster, adding more compute resources.

H2s mark phases; nothing goes deeper

Beyond ## Overview, the only other H2 is ## <Phase>, used to break a long lab into distinct phases. Open each phase with one plain framing sentence under the heading that explains what the phase covers (for example, We will back up the cluster state before upgrading). It is plain text, not an admonition. Short labs with a single procedure skip H2 sections entirely; the H1 above the numbered steps is enough structure. Do not go below H2 inside a lab; phases that need internal structure use sub-numbered steps (1., 2., 3.), not H3s.

instructions.md
# 4.1 Basic Node Maintenance

## Backup The etcd Database

We will back up the cluster state before upgrading.

1. Find the data directory of the etcd daemon.

## Upgrade the Cluster

!!! warning "Attention"

    First we will fully upgrade the cp node, therefore ensure you remain on the cp tab.

1. Update the package metadata for `apt`.

4.1 Basic Node Maintenance

Backup The etcd Database

We will back up the cluster state before upgrading.

  1. Find the data directory of the etcd daemon.

Upgrade the Cluster

Attention

First we will fully upgrade the cp node, therefore ensure you remain on the cp tab.

  1. Update the package metadata for apt.

Writing steps

A lab body is a numbered list, and every step is built the same way. This section covers what a step is made of, how to format it, and how to word it so a learner can follow it without stalling.

What a step is made of

Each step has three parts: a short instruction sentence, the command (or code) it calls for, and the expected output. The sentence tells the learner what they are doing, the command is what they run, and the expected output is what they should see when it works. A step that produces no visible output ends after the command. This three-part shape is the building block of every lab; get it right and the rest is formatting.

Keep each step to one action: a single command, or a small group of commands that belong together as one logical move (for example, loading two kernel modules back to back). If a learner has to do two unrelated things, that is two steps. The learner returns to each numbered marker as a checkpoint while they work, so a step that crams several actions together loses that rhythm and makes it easy to lose your place.

instructions.md
# 4.1 Back Up the etcd Database

We will use the included `snapshot` command to back up the cluster state before the upgrade.

1. Find the data directory of the etcd daemon.

    ```bash title="student@cp:~$"
    sudo grep data-dir /etc/kubernetes/manifests/etcd.yaml
    ```

    !!! success "Expected Output"
        ```{.text .no-copy}
            - --data-dir=/var/lib/etcd
        ```

2. Save a snapshot into the data directory.

    ```bash title="student@cp:~$"
    sudo etcdctl --endpoints=https://127.0.0.1:2379 \
      snapshot save /var/lib/etcd/snapshot.db
    ```

4.1 Back Up the etcd Database

We will use the included snapshot command to back up the cluster state before the upgrade.

  1. Find the data directory of the etcd daemon.

    student@cp:~$
    sudo grep data-dir /etc/kubernetes/manifests/etcd.yaml
    

    Expected Output

        - --data-dir=/var/lib/etcd
    
  2. Save a snapshot into the data directory.

    student@cp:~$
    sudo etcdctl --endpoints=https://127.0.0.1:2379 \
      snapshot save /var/lib/etcd/snapshot.db
    

Formatting a step

Once you know the shape, a few mechanics keep the step rendering cleanly:

  • Use plain ordered-list markers (1., 2., 3.). Some editors escape the period as 1\.; that backslash renders literally in the panel.
  • Indent everything inside a step (the command, the expected output, any extra prose) four spaces under the marker, so it stays attached to the step.
  • Skip headings inside a step; the numbered marker is the heading.
  • A short bulleted sub-list is fine when a step has a few small related actions. If the list grows long, turn it into numbered sub-steps instead.
instructions.md
1. Disable swap on every node. Cloud providers disable swap on their images.

    ```bash title="root@cp:~#"
    swapoff -a
    ```

2. Load kernel modules so they are available for the next steps.

    ```bash title="root@cp:~#"
    modprobe overlay
    modprobe br_netfilter
    ```
  1. Disable swap on every node. Cloud providers disable swap on their images.

    root@cp:~#
    swapoff -a
    
  2. Load kernel modules so they are available for the next steps.

    root@cp:~#
    modprobe overlay
    modprobe br_netfilter
    

Voice

Write each step as a direct instruction to the learner: Check the cluster nodes, not You should check the cluster nodes. This gets the learner moving immediately, and the implicit subject is always the learner.

A few habits keep the voice consistent across a content repository:

  • Open every step with a verb that names the learner's action, not the tool's: Check the nodes, not kubectl lists the nodes. Common starters include Check, Create, Apply, Edit, Install, Verify, List, Delete, Configure, Run. The list is illustrative; a different kind of lab leans on different verbs.
  • A short We will sentence fits a section opener (We will install Kubernetes on a single node, then grow the cluster). Reserve it for framing a section; the numbered steps that follow stay subject-free.
  • A step can carry a short explanatory sentence when it helps: what the command does, why, or a heads-up like You may be asked a few questions. Keep it to the essentials; the instruction itself stays direct.
  • Long labs (twenty or thirty steps) still read step-by-step. The learner returns to each numbered marker as they work, so each step opens with its own verb.

Plain language

A learner reads the prompt while they work, often next to dozens of commands, so the wording carries weight. A few habits keep it accessible:

  • Pick the most common word for the meaning: use over leverage, start over commence, after over subsequent to, ignore over disregard.
  • Skip idioms and figures of speech. Many learners read English as a second language, and idioms are hard to follow even when every word is familiar.
  • Use the same name for the same thing across the whole file. If you call the daemon containerd once, keep calling it containerd, not the container engine or the runtime.
  • Spell out an abbreviation the first time it appears. Names the lab is built around (HTTP, YAML, kubectl) do not need expansion.
  • Spell out contractions: cannot, we will, do not, it is. They read more formally next to a numbered command.
  • Keep format names capitalized in prose (YAML, JSON, Markdown); the lowercase form belongs in backticks when it names a file extension.
  • Reserve ALL-CAPS for literal token names like STDOUT, TCP, CPU, not for emphasis.

The two passages below say the same thing. The first leans on idioms and shifts vocabulary. The second is plain.

harder to read
We will leverage the **kubeadm** tool to spin up a cluster. Make sure to wrap up
the apt steps before moving on to the container engine, otherwise the install
process may run into snags.

We will leverage the kubeadm tool to spin up a cluster. Make sure to wrap up the apt steps before moving on to the container engine, otherwise the install process may run into snags.

easier to read
We will use `kubeadm` to install the cluster. Finish the `apt` steps before
installing `containerd`. The install fails if `containerd` is missing.

We will use kubeadm to install the cluster. Finish the apt steps before installing containerd. The install fails if containerd is missing.

Setting a limit or telling a learner not to do something

Sometimes a step needs to constrain what the learner does: a hard Do not, or a limit like only one of these. Place the constraint before the step it applies to, so the learner reads it in time to act on it, and put it in the admonition that matches its severity (a !!! warning for something they must not get wrong). The admonition's label and styling signal how serious it is, so the wording itself stays calm. Two habits keep it that way:

  • Spell out Do not; Don't and Do NOT both lose the calm register.
  • Put **only** right next to the verb or condition it limits, so the learner spots the constraint immediately (Install one container engine only**).
instructions.md
Install one container engine **only**. If more than one is installed, the
`kubeadm init` process picks Docker first.

!!! warning

    Do not use tabs in your YAML files. White space only. Indentation matters.

Install one container engine only. If more than one is installed, the kubeadm init process picks Docker first.

Warning

Do not use tabs in your YAML files. White space only. Indentation matters.

Commands and prompts

Most steps end in a command the learner runs in a terminal. Showing a command well comes down to one idea: the learner is looking at two things at once, your instructions and their own terminal, and your job is to make those line up. Everything in this section serves that. They line up in two ways: the command is easy to copy and run, and its label tells the learner exactly which terminal to use.

Two ways to show a command

A command the learner runs goes in a code block: a block of code that renders with a one-click copy button, so they never retype it. Something you are only naming in a sentence (a command, a file, a tool) goes in inline backticks instead. The test:

Will the learner copy this and run it?

Yes → code block. No, just naming it → inline backticks.

If a runnable command is buried in prose, the learner has to select and copy it by hand. Lift anything they will actually run into its own code block.

Who the learner is

By default the learner acts as the regular user: student in the examples here, though your lab's user comes from its own setup. Some steps need root, either sudo in front of a single command or a root shell (sudo -i) when several root commands run in a row. The learner is always one or the other, regular user or root, and the prompt label shows which, so they can tell at a glance what they are acting as.

Why every command block needs a label

A terminal prints a short piece of text before the spot where you type. That is the prompt, something like student@cp:~$. It is not decoration: it tells the learner who they are acting as and which machine they are on.

Each command block carries a label that copies that prompt. The learner checks your label against the prompt in their own terminal, and when the two match they know they are in the right place before they run anything. A block with no label leaves them guessing which terminal a command belongs to, so every command block gets one.

In the file, you set the label with title= on the block and tag the block as bash. That is the only syntax to remember here.

The prompt comes in a few standard forms. These are only examples; your lab's prompts use your own user, machine, and host names.

Prompt Meaning
student@cp:~$ Default. The learner is logged in as student on the cp tab.
student@worker:~$ The learner has switched to the worker tab.
root@cp:~# The learner ran sudo -i on cp and is now in a root shell. The trailing # is the root prompt.

When the prompt changes, change the label

Because the label mirrors the prompt, it changes whenever the prompt does. Otherwise the learner reads student@cp while they are actually somewhere else. A prompt changes when the learner's user, machine, or working directory changes. The three you hit most often:

The learner moves to a different machine. Update the host in the label, for example student@cp:~$ becomes student@worker:~$.

Note

Some labs run across more than two machines. A high-availability lab might use cp, secondcp, thirdcp, worker, and haproxy. The names vary by lab, but the rule is the same: the host in the label always names the machine the learner is on, so they switch to match.

instructions.md
1. From the cp node, drain the worker.

    ```bash title="student@cp:~$"
    kubectl drain worker --ignore-daemonsets
    ```

2. Switch to the worker tab and upgrade the package.

    ```bash title="student@worker:~$"
    sudo apt install -y kubeadm=1.35.2-1.1
    ```
  1. From the cp node, drain the worker.

    student@cp:~$
    kubectl drain worker --ignore-daemonsets
    
  2. Switch to the worker tab and upgrade the package.

    student@worker:~$
    sudo apt install -y kubeadm=1.35.2-1.1
    

The learner opens a root shell. The prompt ends in #, so update the label to match, and switch it back when they leave the root shell. A command that needs root never sits under a regular-user label.

instructions.md
1. Become root and update the system.

    ```bash title="student@cp:~$"
    sudo -i
    ```

    ```bash title="root@cp:~#"
    apt update && apt upgrade -y
    ```

2. Return to the student shell.

    ```bash title="root@cp:~#"
    exit
    ```

    ```bash title="student@cp:~$"
    whoami
    ```
  1. Become root and update the system.

    student@cp:~$
    sudo -i
    
    root@cp:~#
    apt update && apt upgrade -y
    
  2. Return to the student shell.

    root@cp:~#
    exit
    
    student@cp:~$
    whoami
    

The learner moves into a new directory (an edge case, less common). If the learner moves into a directory that later commands depend on, put the path in the label so they can see where they are. The form is student@cp:<path>$.

instructions.md
1. Move into the discovery cache and inspect the API resource manifests.

    ```bash title="student@cp:~$"
    cd /home/student/.kube/cache/discovery/k8scp_6443
    ```

    ```bash title="student@cp:~/.kube/cache/discovery$"
    cat v1/serverresources.json | jq | grep kind
    ```
  1. Move into the discovery cache and inspect the API resource manifests.

    student@cp:~$
    cd /home/student/.kube/cache/discovery/k8scp_6443
    
    student@cp:~/.kube/cache/discovery$
    cat v1/serverresources.json | jq | grep kind
    

Break long commands across lines

When a command is too long for one line, end each line with a backslash (\) to continue it onto the next. Keep the backslashes in the block so the learner can copy the whole thing and run it as a single command.

instructions.md
1. Save a snapshot of the etcd database from the cp node.

    ```bash title="student@cp:~$"
    kubectl -n kube-system exec -it etcd-cp -- \
      etcdctl --cacert=/etc/kubernetes/pki/etcd/ca.crt \
      --cert=/etc/kubernetes/pki/etcd/server.crt \
      --key=/etc/kubernetes/pki/etcd/server.key \
      --endpoints=https://127.0.0.1:2379 \
      snapshot save /var/lib/etcd/snapshot.db
    ```
  1. Save a snapshot of the etcd database from the cp node.

    student@cp:~$
    kubectl -n kube-system exec -it etcd-cp -- \
      etcdctl --cacert=/etc/kubernetes/pki/etcd/ca.crt \
      --cert=/etc/kubernetes/pki/etcd/server.crt \
      --key=/etc/kubernetes/pki/etcd/server.key \
      --endpoints=https://127.0.0.1:2379 \
      snapshot save /var/lib/etcd/snapshot.db
    

Expected output

When a command produces something the learner can see, show them what to expect. This is how a learner confirms a step worked before moving on: they run the command, then compare what is in their terminal against what you have shown.

Show output for comparison, not copying

Put the expected output in a !!! success "Expected Output" admonition, in a fenced block marked .no-copy. The .no-copy removes the copy button, and that is the point: the expected output is there for the learner to compare against, not to copy and run. A command block is something they copy; an output block is something they check, and dropping the copy button keeps the two from being confused.

Use Expected Output as the title, both words capitalized. It stays the same across every lab, so the learner learns to recognize it.

When there is no visible output

Not every command shows the learner something. When a command runs silently, or its result is not worth showing, the step ends after the command. Add an output block only when there is something the learner can actually compare against; an empty or pointless one is noise.

Match the fence to the output's format

The output goes in a fenced block whose type matches the output. Most output is plain text; JSON and YAML get their own fences so they read clearly:

  • {.text .no-copy} — tables, logs, command output (the common case).
  • {.json .no-copy} — JSON output.
  • {.yaml .no-copy} — YAML, such as part of a manifest.

For partial or specific output, the admonition title can say so, for example !!! success "deployment web (partial)".

Trim the long output to its shape

When output runs long, the learner does not need every line; they need to recognize the correct result. Keep the first few real lines and the last few real lines, and replace the middle with <output_omitted>. That shows the shape of the result without flooding the panel.

Tell the learner what will differ

Sample output rarely matches a real run line for line. Ages, timestamps, versions, and IDs change every time, so a learner comparing against your sample may read a correct result as wrong. When the output will differ, say so: a short note like Your ages and timestamps will differ, pointing them at the part that does have to match (a Ready status, a resource name, a success message). The learner is checking for the right shape and the right key values, not an exact match.

instructions.md
1. Verify the node is ready.

    ```bash title="student@cp:~$"
    kubectl get nodes
    ```

    !!! success "Expected Output"
        ```{.text .no-copy}
        NAME     STATUS   ROLES           AGE     VERSION
        cp       Ready    control-plane   2m12s   v1.35.2
        worker   Ready    <none>          1m05s   v1.35.2
        ```

    Your `AGE` values will differ; what matters is that both nodes show `Ready`.

2. Initialize the control plane. Read through the output to find the join command.

    ```bash title="root@cp:~#"
    kubeadm init --config=kubeadm-config.yaml --upload-certs --node-name=cp
    ```

    !!! success "Expected Output"
        ```{.text .no-copy}
        [init] Using Kubernetes version: v1.35.2
        [preflight] Running pre-flight checks
        <output_omitted>
        kubeadm join k8scp:6443 --token vapzqi.et2p9zbkzk29wwth \
          --discovery-token-ca-cert-hash sha256:f62bf97d4fba6876...
        ```
  1. Verify the node is ready.

    student@cp:~$
    kubectl get nodes
    

    Expected Output

    NAME     STATUS   ROLES           AGE     VERSION
    cp       Ready    control-plane   2m12s   v1.35.2
    worker   Ready    <none>          1m05s   v1.35.2
    

    Your AGE values will differ; what matters is that both nodes show Ready.

  2. Initialize the control plane. Read through the output to find the join command.

    root@cp:~#
    kubeadm init --config=kubeadm-config.yaml --upload-certs --node-name=cp
    

    Expected Output

    [init] Using Kubernetes version: v1.35.2
    [preflight] Running pre-flight checks
    <output_omitted>
    kubeadm join k8scp:6443 --token vapzqi.et2p9zbkzk29wwth \
      --discovery-token-ca-cert-hash sha256:f62bf97d4fba6876...
    

File contents

Sometimes a step shows the contents of a file rather than a command, one the learner is about to edit or has just generated and wants to check. This is its own pattern because the file is not something they run and not something they only compare against; they may be changing it.

Use an example admonition

Put the contents in an !!! example "<filename>" admonition with a .no-copy fence: the same .no-copy fence as expected output, just in an example admonition instead of a success one.

Match the fence to the format

Match the fence language to the file: yaml for manifests, text for plain config like /etc/hosts or /etc/exports, json for JSON.

Match the filename in both places

Use the same filename in the admonition title and the fence, and make it match the real file on disk. A mismatch tells the learner they are looking at the wrong file.

Mark the lines that change

When the learner changes only part of a file, mark the exact lines with an inline #<-- comment (#<-- Add this line). It is a note to the learner, not part of the file they save, so keep it to the lines that actually change.

instructions.md
1. Add an alias for the control plane to `/etc/hosts`.

    ```bash title="root@cp:~#"
    vim /etc/hosts
    ```

    !!! example "/etc/hosts"
        ```{.text .no-copy}
        10.244.0.3 k8scp      #<-- Add this line
        10.244.0.3 cp         #<-- Add this line
        127.0.0.1 localhost
        ```

2. Apply the manifest with the cluster configuration.

    ```bash title="root@cp:~#"
    cp /home/student/LFS258/SOLUTIONS/s_03/kubeadm-config.yaml /root/
    ```

    !!! example "kubeadm-config.yaml"
        ```{.yaml .no-copy title="kubeadm-config.yaml"}
        apiVersion: kubeadm.k8s.io/v1beta4
        kind: ClusterConfiguration
        kubernetesVersion: 1.35.2
        controlPlaneEndpoint: "k8scp:6443"   #<-- Use the alias, not the IP
        networking:
          podSubnet: 192.168.0.0/16
        ```
  1. Add an alias for the control plane to /etc/hosts.

    root@cp:~#
    vim /etc/hosts
    

    /etc/hosts

    10.244.0.3 k8scp      #<-- Add this line
    10.244.0.3 cp         #<-- Add this line
    127.0.0.1 localhost
    
  2. Apply the manifest with the cluster configuration.

    root@cp:~#
    cp /home/student/LFS258/SOLUTIONS/s_03/kubeadm-config.yaml /root/
    

    kubeadm-config.yaml

    kubeadm-config.yaml
    apiVersion: kubeadm.k8s.io/v1beta4
    kind: ClusterConfiguration
    kubernetesVersion: 1.35.2
    controlPlaneEndpoint: "k8scp:6443"   #<-- Use the alias, not the IP
    networking:
      podSubnet: 192.168.0.0/16
    

Paths and ownership

Where a file lives follows from who owns it: files the learner owns sit under their home directory, and system files sit under their usual roots (/etc, /var, /opt, /usr, /srv). Writing paths this way tells the learner exactly where everything is.

Spell out absolute paths in prose

When you name a file in a sentence, give its full path, not a shortcut the learner has to resolve, for example /home/student/LFS258/SOLUTIONS/s_03/kubeadm-config.yaml. The full path tells them exactly where the file is.

Shortcuts are fine inside a command

$HOME and ./<file> are fine inside a command block, because the learner copies and runs it from the current shell, where they resolve correctly. So: the full path when you are telling them where a file is, the shortcut when they are running a command.

instructions.md
1. Copy the manifest from the course tarball.

    ```bash title="student@cp:~$"
    cp /home/student/LFS258/SOLUTIONS/s_07/rs.yaml .
    ```

2. Apply it from the working directory.

    ```bash title="student@cp:~$"
    kubectl create -f rs.yaml
    ```

    !!! success "Expected Output"
        ```{.text .no-copy}
        replicaset.apps/rs-one created
        ```
  1. Copy the manifest from the course tarball.

    student@cp:~$
    cp /home/student/LFS258/SOLUTIONS/s_07/rs.yaml .
    
  2. Apply it from the working directory.

    student@cp:~$
    kubectl create -f rs.yaml
    

    Expected Output

    replicaset.apps/rs-one created
    

Admonitions

An admonition is a titled callout box: a labeled panel set off from the surrounding text, used for asides, warnings, framing, and reference material. You have read them throughout this guide; this section is where they are defined.

Write one with !!!, the type, and a title. Start the body after a blank line, indented four spaces underneath:

tab switch between nodes
!!! warning "Attention"

    The following command runs on the cp node. Click the cp tab, run the
    command, then return to the worker tab.

Attention

The following command runs on the cp node. Click the cp tab, run the command, then return to the worker tab.

The four-space indent is what attaches the body to the admonition; without it, the text falls outside the box.

Which admonition to use

Reach for the type that matches what you are trying to say:

When you want to… Use
Show the output the learner compares against !!! success "Expected Output"
Frame a lab or a section at the top !!! info "Overview"
Flag a tab switch, or a step on a different node !!! warning "Attention"
Show the contents of a file the learner edits or generates !!! example "<filename>"
Point to upstream reference material !!! note "<Topic> Reference"
Give context the learner can skip if they already know it !!! info "Introducing <topic>"
Offer an optional hint the learner opens only when stuck ??? tip "Hint: <topic>"

Handling non-actionable code and references

Learners are conditioned to run any code block they see. When a !!! note admonition shows upstream reference material or background commands, tell the learner plainly whether they need to run them (for example, You do not need to run the commands in this box). Pair these reference code blocks with a .no-copy fence so the panel drops the copy button and discourages accidental pasting.

reference link to upstream docs
!!! note "Cilium Installation Reference"

    Cilium is usually installed with `cilium install` or `helm install`. We
    generated the `cilium-cni.yaml` file for you. You do not need to run the
    commands in this box.

    ```{.text .no-copy}
    helm repo add cilium https://helm.cilium.io/
    helm repo update
    helm template cilium cilium/cilium --version 1.19.1 \
      --namespace kube-system > cilium.yaml
    ```

Cilium Installation Reference

Cilium is usually installed with cilium install or helm install. We generated the cilium-cni.yaml file for you. You do not need to run the commands in this box.

helm repo add cilium https://helm.cilium.io/
helm repo update
helm template cilium cilium/cilium --version 1.19.1 \
  --namespace kube-system > cilium.yaml

Collapsible hints

Write an admonition with ??? instead of !!! and it renders collapsed: the learner sees the title and opens the box only if they want what is inside. That fits a lab, where the learner works at their own pace. They can try a step on their own and expand a nudge when they get stuck, without the answer sitting in front of them the whole time. Collapsible ??? tip hints are encouraged, and they pair especially well with a high-level walk-through where the learner takes on more of the challenge.

Title the block with the topic so the learner knows what is inside before opening it, and keep the body to a nudge (a pointer at the command, its --help, or the field to change) rather than the full solution.

instructions.md
1. Cordon the worker so no new Pods schedule onto it.

    ??? tip "Hint: which command"

        Look at `kubectl cordon`. Run `kubectl cordon --help` to see the
        arguments it takes.
  1. Cordon the worker so no new Pods schedule onto it.

    Hint: which command

    Look at kubectl cordon. Run kubectl cordon --help to see the arguments it takes.

Titles are flexible; types are not

The type (success, info, warning, example, note, tip) carries the meaning and the styling, so it is the part that has to fit. The title can be your own words when a section warrants it: !!! warning "Very Important", !!! info "YAML and White Space". Pick the type that matches what you mean, then title it for what the learner needs to read.

Severity

How serious an admonition looks comes from its label and styling together, not from color alone, so it stays clear for learners who cannot rely on color.

Labs that span machines or pages

When a lab grows complex, you may need to move the learner between terminal tabs (different virtual machines) or split the lab across multiple pages.

Moving between terminal tabs

A long lab often moves the learner between two or more terminal tabs. You already know how to set the bash title to mirror the prompt and how to write an !!! warning "Attention" admonition, so moving the learner is just combining the two.

Nest a short !!! warning "Attention" block inside the step that ends on one tab, then carry the new prompt in the bash title of the next step. The learner reads the callout, clicks the new tab, and sees the matching prompt at the top of the next code block, confirming they are in the right place.

instructions.md
1. Allow the package manager to update on the worker.

    ```bash title="student@worker:~$"
    sudo apt-mark unhold kubeadm
    ```

    !!! warning "Attention"
        The following command runs on the cp node. Click the cp tab, run the
        command, then return to the worker tab.

2. Drain the worker from the cp node.

    ```bash title="student@cp:~$"
    kubectl drain worker --ignore-daemonsets
    ```

3. Return to the worker tab and apply the upgrade.

    ```bash title="student@worker:~$"
    sudo kubeadm upgrade node
    ```
  1. Allow the package manager to update on the worker.

    student@worker:~$
    sudo apt-mark unhold kubeadm
    

    Attention

    The following command runs on the cp node. Click the cp tab, run the command, then return to the worker tab.

  2. Drain the worker from the cp node.

    student@cp:~$
    kubectl drain worker --ignore-daemonsets
    
  3. Return to the worker tab and apply the upgrade.

    student@worker:~$
    sudo kubeadm upgrade node
    

Directory-mode instructions (multi-page labs)

Most labs use a single instructions.md, but a handful of longer labs, or labs with branching paths, use the directory layout. The sandbox points at the folder; the renderer treats index.md as the landing page and any sibling .md files as pages reachable from it.

Two conventions hold inside a directory-mode lab:

  • index.md carries the chapter-numbered H1, the framing paragraph, and the links to the sibling files.
  • The sibling files start at H2 (no second H1), because the H1 belongs to index.md. The renderer concatenates them visually in the panel, so a second H1 would look like a new lab.
instructions/index.md
# 16.1 High Availability

!!! info "Overview"

    In this lab we will use three more nodes. One acts as a load balancer
    (`haproxy`); the other two act as control plane nodes (`secondcp` and
    `thirdcp`) for quorum.

The steps are written two ways. Choose the level of guidance that fits you.

- **[High-Level Steps](high-level-steps.md)** for more of a challenge.
- **[Detailed Steps](detailed-steps.md)** for step-by-step commands and expected output.

16.1 High Availability

Overview

In this lab we will use three more nodes. One acts as a load balancer (haproxy); the other two act as control plane nodes (secondcp and thirdcp) for quorum.

The steps are written two ways. Choose the level of guidance that fits you.

  • High-Level Steps (high-level-steps.md) for more of a challenge.
  • Detailed Steps (detailed-steps.md) for step-by-step commands and expected output.
instructions/high-level-steps.md
## High level steps

1. Deploy a load balancer (`haproxy`) on the proxy node.

2. Install the Kubernetes packages on the second and third control plane nodes.

3. Use `kubeadm join` to add `secondcp` as another control plane.

4. Use `kubeadm join` to add `thirdcp` as another control plane.

5. Update `haproxy` to forward to all three control plane backends.

**[Lab Overview](index.md)** · **[Detailed Steps](detailed-steps.md)**

High level steps

  1. Deploy a load balancer (haproxy) on the proxy node.

  2. Install the Kubernetes packages on the second and third control plane nodes.

  3. Use kubeadm join to add secondcp as another control plane.

  4. Use kubeadm join to add thirdcp as another control plane.

  5. Update haproxy to forward to all three control plane backends.

Lab Overview (index.md) · Detailed Steps (detailed-steps.md)

Closing the lab

A lab usually ends in one of three ways. There is no single canonical closer; the choice follows the shape of the work the learner just completed.

Whatever shape you choose, the closer is one short paragraph or one final numbered step. Do not give it its own H2 section.

Lab shape Closer
Builds and leaves objects A short cleanup step that deletes the objects so later labs run cleanly.
Demonstrates an inspection or read-only flow A final verification command (like kubectl get) that confirms the end state.
Offers room to keep going A one-sentence pointer at what the learner can experiment with in the time they have left.
cleanup closer
12. Delete the deployments to recover system resources.

    ```bash title="student@cp:~$"
    kubectl delete deploy hog
    ```

    ```bash title="student@cp:~$"
    kubectl delete deploy limited-hog -n low-usage-limit
    ```
  1. Delete the deployments to recover system resources.

    student@cp:~$
    kubectl delete deploy hog
    
    student@cp:~$
    kubectl delete deploy limited-hog -n low-usage-limit
    
verification closer
17. Verify both nodes show `Ready` status at the new version.

    ```bash title="student@cp:~$"
    kubectl get nodes
    ```

    !!! success "Expected Output"
        ```{.text .no-copy}
        NAME     STATUS   ROLES           AGE    VERSION
        cp       Ready    control-plane   2h     v1.35.2
        worker   Ready    <none>          1h     v1.35.2
        ```
  1. Verify both nodes show Ready status at the new version.

    student@cp:~$
    kubectl get nodes
    

    Expected Output

    NAME     STATUS   ROLES           AGE    VERSION
    cp       Ready    control-plane   2h     v1.35.2
    worker   Ready    <none>          1h     v1.35.2
    
experiment closer
8. View the etcd cluster status. Experiment with how long it takes for the
   cluster to notice the failure and elect a new leader in the time you have
   left.
  1. View the etcd cluster status. Experiment with how long it takes for the cluster to notice the failure and elect a new leader in the time you have left.

Common pitfalls

Use this as your pre-publish checklist. Each item is a pattern reviewers push back on, shown as the rejected form and its fix. The rules themselves are explained earlier in the guide; this section gathers the failure cases in one place to scan before you ship.

Cramming unrelated actions into one step
1. Drain the node, edit the kubelet config, and reboot the machine.

Three unrelated actions under one marker make the step hard to follow and easy to lose your place in. Keep a step to one command or one logical move, and split unrelated actions into separate numbered steps.

Untitled bash blocks
```bash
kubectl get nodes
```

Without a title the learner cannot tell which tab the command runs in. The prompt belongs in the title: bash title="student@cp:~$", bash title="student@worker:~$", or bash title="root@cp:~#" after sudo -i.

Mismatched prompt on a root command
```bash title="student@cp:~$"
apt update && apt upgrade -y
```

The command needs root, but the title shows the unprivileged $ prompt. Either add sudo to the command, or open a root shell with sudo -i first and switch the title to root@cp:~#.

Copy-enabled expected output
!!! success "Expected Output"
    ```bash
    node/worker drained
    ```

The learner does not run the expected output; they compare against it. Drop the bash tag and add .no-copy so the panel hides the copy button: {.text .no-copy}.

Dumping massive command outputs
!!! success "Expected Output"
    ```{.text .no-copy}
    <50 lines of JSON>
    ```

Pasting dozens of lines floods the instructions tool. When the learner only needs to confirm a few values, keep the first and last real lines and replace the middle with <output_omitted>.

Inline commands the learner has to retype
Run `kubectl create -f /home/student/LFS258/SOLUTIONS/s_07/rs.yaml`
to apply the manifest.

The learner has to select and copy out of running prose. Lift the command into its own bash block so the panel shows a copy button:

Apply the manifest with:

```bash title="student@cp:~$"
kubectl create -f /home/student/LFS258/SOLUTIONS/s_07/rs.yaml
```
Tildes inside the title prompt
```bash title="student@cp:~$"
cd ~/.kube/cache/discovery
cat v1/serverresources.json | jq | grep kind
```

~ is fine inside the command itself, but once the working directory changes, later commands read better with a title that shows the new path. Two blocks make the move explicit:

```bash title="student@cp:~$"
cd /home/student/.kube/cache/discovery
```

```bash title="student@cp:~/.kube/cache/discovery$"
cat v1/serverresources.json | jq | grep kind
```
Escaped ordered-list markers
1\. Install the kubeadm package.

2\. Initialize the control plane.

Some editors emit 1\. to escape the period. The learner sees the backslash literally. Plain 1., 2., 3. render the way the learner expects.

Lowercase format names
Edit the kubeadm-config.yaml file. The format is yaml; tabs are not allowed.

Format names stay capitalized in prose: YAML, JSON, Markdown. The lowercase form belongs inside backticks when it names a file extension.

ALL-CAPS emphasis
Do NOT use tabs in your YAML files.

The admonition severity and **bold** carry weight already. Do not and **only** read calmer and keep the rest of the lab easy to scan. ALL-CAPS is reserved for literal token names like STDOUT, STDERR, TCP, CPU.

Contractions in instruction prose
1. The proxy can't run from outside the cluster, so we'll start it on cp.

Contractions read informal next to a numbered command. Spell them out: cannot, we will, do not, it is.

Idioms and colloquialisms
We will spin up a cluster and get the ball rolling out of the box.

Many learners read English as a second language, and figures of speech do not translate even when every word is familiar. Use plain, direct wording: We will create the cluster.

Stale link syntax
More reading can be found [here] (https://kubernetes.io/docs/tasks/...).

The space between ] and ( breaks the link. The renderer prints the URL in parentheses as plain text. The form is [text](url) with no space.

Numeric H1 without the title
# 3.1

The H1 is the title shown in the instructions tool. Section number alone gives the learner no clue what the lab covers. Pair the number with the lab title: # 3.1 Install Kubernetes.

H1 inside a sibling file in directory mode
instructions/detailed-steps.md
# 16.1 High Availability — Detailed Steps

1. Install haproxy.

The renderer concatenates the sibling pages. A second H1 looks like a new lab. Start sibling pages at H2:

instructions/detailed-steps.md
## Detailed Steps

1. Install haproxy.
Emoji or icons in the lab
# 3.1 Install Kubernetes 🚀

Let's get the cluster running!

Decorative icons and exclamatory framing distract from a procedural prompt and do not localize well. The H1 carries the title; the steps carry the work.

Typos that slipped past review
You can always snoop to see the inbound request and update the file to be
more narrowi scoped.

...you may see a warning message that can be safely ingnored.

Both narrowi (narrowly) and ingnored (ignored) survived to ship in the catalog. A cspell pass or a careful read before the pull request catches these.

Contribute one exercise through separate instruction and environment reviews.

The cp terminal, worker terminal, IDE, browser, and instructions tool the lab uses.

Wire the instructions tool to the file or directory.

Build a lab end-to-end with the sandbox CLI.