Lab Structure
A lab is a directory: a set of HCL configuration files at its root, plus the content those files reference — Markdown instructions, scripts, assets, and other supporting files in subdirectories. The lab directory can be the root of a git repository or a subdirectory inside a shared one.
Three rules govern the structure:
- File names are conventions, not requirements. The platform loads every
.hclfile at the top level of the lab directory, whatever it is called. You are free to keep everything in one file or split resources across many. - Discovery is flat. Only top-level
.hclfiles are loaded. Configuration in a subdirectory is parsed only when amoduleblock references it. - Exactly one
labresource must exist across all files. The file that declares it is the lab’s entrypoint — platform-managed metadata, such as theinstruqtconfig-version block, is written there.
How Files Are Loaded
Section titled “How Files Are Loaded”When the CLI or the platform loads a lab, it reads the top level of the lab
directory and parses every file with the .hcl extension. Directories are
skipped, so an .hcl file inside a subdirectory is ignored — unless a
module block points at it:
module "chapter" { source = "./modules/chapter"}Local module sources must start with ./ or ../ and must stay inside the
lab directory. Within a module directory the same rule applies again: its
top-level .hcl files are loaded, and deeper nesting requires further
module blocks (up to 10 levels).
File paths in configuration — a page’s file, a note’s file, task and
exec scripts — are resolved relative to the lab root, and validation
checks they exist. Functions that read files, like file(), refuse paths
that leave the lab directory (no ../ escapes).
Core Configuration Files
Section titled “Core Configuration Files”None of these file names is required — they are the conventions used by the CLI skeleton, the tutorials, and the visual editor. When you add a resource in the UI, it is written to the conventional file for that resource type, creating the file if needed.
| File | Typical contents |
|---|---|
main.hcl | The lab resource: metadata, settings, and the content block that arranges chapters and pages. The entrypoint of the lab. |
layouts.hcl | layout resources defining the panel arrangement. |
sandboxes.hcl | Infrastructure: networks, containers, VMs, clusters, cloud accounts, and supporting resources such as exec, copy, and template. |
tabs.hcl | What learners see: terminal, service, editor, note, external_website, virtual_browser, and cloud_credentials resources. |
pages.hcl | page resources pointing at instruction Markdown files. The UI writes pages to content.hcl instead. |
tasks.hcl | task resources with their check/solve conditions. |
quizzes.hcl | quiz resources and their question resources. |
secrets.hcl | secret resources referencing team-managed secrets. |
certificates.hcl | certificate_ca and certificate_leaf resources. |
variables.hcl, locals.hcl, outputs.hcl, modules.hcl | Where the UI places variable, local, output, and module definitions. |
The CLI skeleton (instruqt lab init, template skeleton) starts with only
main.hcl and layouts.hcl — the minimum needed to launch a lab — plus
placeholder READMEs in the content directories. It defines no network and no
sandbox resources; the infrastructure tutorial
adds those in sandboxes.hcl.
Directory Structure
Section titled “Directory Structure”A fully grown lab, using every convention from these docs, looks like this:
Directorymy-lab/
- main.hcl the lab resource, settings, chapters and pages
- layouts.hcl layout resources
- pages.hcl page resources
- tasks.hcl task resources
- quizzes.hcl quiz and question resources
- sandboxes.hcl networks, containers, VMs, clusters
- tabs.hcl terminals, services, editors, notes
- secrets.hcl team secret references
- certificates.hcl certificate resources
- variables.hcl variable definitions
- outputs.hcl output values
Directoryinstructions/ page content, one Markdown file per page
Directorygetting-started/
- welcome.md
- first-steps.md
Directorynotes/ note tab content
- cheat-sheet.md
Directoryscripts/ task and exec scripts
Directorytask/
Directoryedit_homepage/
- check.sh
- solve.sh
Directoryexec/
Directorysetup/
- script.sh
Directoryassets/ media served at /assets inside the sandbox
- diagram.png
Directoryfiles/ arbitrary files used by sandbox resources
- app.conf
Directorytemplates/ template resource sources
- nginx.conf.tpl
Directorymodules/ reusable configuration, loaded via module blocks
Directoryweb-server/
- main.hcl
- variables.hcl
- outputs.hcl
- README.md
Only a few of these names carry actual behavior:
assets/is the one enforced directory name. The lab’siconmust live underassets/(or be an external URL), and the directory is served from the sandbox at/assets, which is why instructions reference images as/assets/diagram.png.- Page and note files must be Markdown (
*.md) and must exist — validation fails otherwise. Task andexecscript files must exist too; their names are free since configuration references them by path. - Everything else —
instructions/organized by chapter,scripts/task/<name>/andscripts/exec/<name>/script.sh,files/,templates/,notes/— is the convention the platform itself follows when it generates files for you, and the layoutinstruqt lab initscaffolds.
Resources that consume directories of input files use free-form names as
well: Kubernetes manifests (k8s/), Nomad jobs (jobs/) and agent configs
(nomad/, consul/), Helm charts (charts/), Terraform sources
(terraform/), IAM policies (policies/), and configuration inputs
(config/) are all just paths you pass to the corresponding resource.
Resource Relationships
Section titled “Resource Relationships”Everything hangs off the lab resource: it arranges the content through its
content block and picks the presentation through its layout reference.
This is the wiring in its shortest form:
resource "lab" "main" { title = "My First Lab" description = "A lab showing how the pieces wire together." layout = resource.layout.two_column # default layout for every page
content { chapter "getting_started" { # chapters are blocks, not resources title = "Getting Started"
page "welcome" { reference = resource.page.welcome # each page slot points at a page resource } } }}
resource "page" "welcome" { title = "Welcome" file = "instructions/getting_started/welcome.md"
activities = { edit_homepage = resource.task.edit_homepage # ids used in the page's markdown }}
resource "terminal" "shell" { target = resource.container.workstation # tabs attach to sandbox resources}Four things to know about this graph:
- Chapters and pages are blocks inside the
labresource, not standalone resources. The only standalone content resource ispage, and every page slot in thecontentblock mustreferenceone. Chapters and pages can override the lab’s defaultlayout. - Pages carry the interactivity. A page’s
fileis its Markdown content, and itsactivitiesmap connects ids totaskandquizresources. The pairing is validated in both directions: every id in the map must appear as an<instruqt-task id="...">or<instruqt-quiz id="...">tag in that page’s Markdown. A quiz references itsquestions; tasks and quizzes can gate onprerequisites(chapters, tasks, or quizzes). - Layouts decide what is on screen. A layout’s
instructionspanel renders the current page, and every other tab has atarget: anote(itsfileis Markdown, by convention innotes/), anexternal_websiteorvirtual_browser(a URL), or a sandbox-facing resource. - Sandbox-facing targets are type-checked. A
terminaloreditorattaches to acontainerorvm; aservicealso to ak8s_cluster,nomad_cluster, oringress;cloud_credentialsto cloud accounts. A task runs its condition scripts on acontainerorvm;exectargets acontainer.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1f2440', 'primaryTextColor':'#e2e8f0', 'primaryBorderColor':'#2a2f45', 'lineColor':'#ffffff', 'secondaryColor':'#141829', 'tertiaryColor':'#181c32', 'background':'#181c32', 'mainBkg':'#181c32', 'secondBkg':'#181c32', 'tertiaryTextColor':'#e2e8f0', 'edgeLabelBackground':'#181c32', 'clusterBkg':'#181c32'}}}%%
graph TD
LAB["lab"] -->|layout| LAYOUT["layout"]
LAB -->|"content: chapter, page"| PAGE["page"]
PAGE -->|activities| ACT["task / quiz"]
LAYOUT -->|"tab target"| TAB["terminal / service / editor"]
ACT -->|"task target: container / vm"| SANDBOX["sandbox resources"]
TAB -->|target| SANDBOX
style LAB fill:#667eea,stroke:#2a2f45,color:#ffffff
style PAGE fill:#667eea,stroke:#2a2f45,color:#ffffff
style LAYOUT fill:#667eea,stroke:#2a2f45,color:#ffffff
style ACT fill:#667eea,stroke:#2a2f45,color:#ffffff
style TAB fill:#667eea,stroke:#2a2f45,color:#ffffff
style SANDBOX fill:#43e97b,stroke:#2a2f45,color:#1a1a2e
The diagram shows references only — the files each resource reads
(instructions/, notes/, scripts/) are covered in the directory
structure above. Layout tabs can also point at a note, external_website,
virtual_browser, or cloud_credentials resource; those render content
rather than attaching to a sandbox machine.
References resolve within their scope: resources inside a module see only
that module’s resources, and the parent configuration reads module results
through outputs, as module.NAME.output.OUTPUT.
