Skip to content

You are viewing documentation for Instruqt 2.0 Labs. For Tracks documentation, please visit docs.instruqt.com.

Defined insandbox.hcl

The exec resource runs a script on the sandbox host, in a new container, or in an existing container. target and image control where the script runs:

  • No target and no image block: the script runs on the sandbox host, not inside any container or VM. Local scripts can’t use network or volume blocks.
  • An image block: the script runs in a new container created from that image.
  • target set to a container, for example resource.container.web: the script runs inside that existing container.

target accepts only containers. To run scripts on a virtual machine, use one of these:

  • The VM’s startup_script, for setup at boot.
  • A task whose config.target is the VM. Task scripts (setup, check, solve, cleanup) run on VMs.

As a lab author, you can use exec resources to:

  • Environment Setup: Install dependencies, configure systems, and prepare development environments for hands-on activities
  • System Configuration: Configure services, update system settings, and customize environments for specific lab scenarios
  • Custom Automation: Implement custom setup logic, cleanup operations, and specialized workflows unique to your lab content

Exec resources provide flexible script execution capabilities for the sandbox host and containers. For scripts on virtual machines, use the VM’s startup_script or a task targeting the VM.

resource "exec" "name" {
script = "scripts/exec/name/script.sh"
}
resource "exec" "local" {
script = "scripts/exec/local/script.sh"
working_directory = "/tmp"
timeout = "300s"
daemon = false
environment = {
ENV_VAR = "value"
PATH = "/usr/local/bin:/usr/bin:/bin"
}
}
resource "exec" "new_container" {
script = "scripts/exec/new_container/script.sh"
timeout = "300s"
environment = {
ENV_VAR = "value"
}
image {
name = "alpine:latest"
username = "registry_user"
password = "registry_pass"
}
network {
id = resource.network.main
ip_address = "10.0.0.100"
aliases = ["exec-container"]
}
volume {
source = "./scripts"
destination = "/scripts"
type = "bind"
read_only = true
}
run_as {
user = "1000"
group = "1000"
}
}

Remote Execution in Existing Container Syntax

Section titled “Remote Execution in Existing Container Syntax”
resource "exec" "existing_container" {
script = "scripts/exec/existing_container/script.sh"
timeout = "300s"
environment = {
ENV_VAR = "value"
}
target = resource.container.web
}

Fields

FieldTypeRequiredDescription
scriptstring✓Relative or absolute path to a script file. Inline script content is not allowed. To pass values to other resources, write KEY=value lines to the EXEC_OUTPUT file.
working_directorystring—The working directory where the script will be executed. Defaults to root directory.
daemonbool—Enable to run the script as a background daemon process
Defaultfalse
timeoutstring—Set the maximum time the script can run before it is stopped automatically
Default300s
ConstraintsTimeout must be a valid duration (e.g., 30s, 5m, 1h)
environmentmap(string)—Environment variables that will be available to the script during execution
imageblock—Specify a Docker image to create a temporary container for script execution
targetreferencecontainer, vm, k8s_cluster, kubernetes_cluster, nomad_cluster—Reference to an existing container, VM or cluster resource where the script will be executed. On a cluster, the script runs on its server node.
networkblock—Networks to connect the temporary container to (only used with image)repeatable
volumeblock—Volumes to mount in the container (only used with image or target)repeatable
run_asblock—Configure the user and group to execute the script as inside the target

Image

exec→Image

Create temporary container for execution

FieldTypeRequiredDescription
namestring✓Full Docker image reference, including the registry and an optional tag. Include a specific tag to ensure consistent lab environments.
ConstraintsMust be a valid Docker image reference (e.g. ubuntu:24.04, docker.io/hashicorp/vault:1.14)
usernamestring—Only required for private Docker registries. Leave empty for public images.
passwordstring—Only required for private Docker registries. Leave empty for public images.

Network

exec→Network

Networks to attach container to

FieldTypeRequiredDescription
idstring—ID of the network to attach the container
targetreferencenetwork—The network to attach the container to
ip_addressstring—Static IP address to assign container for the network, the ip address must be within range defined by the network subnet. If this parameter is omitted an IP address will be automatically assigned.
ConstraintsMust be a valid IPv4 address (e.g., 192.168.1.10)
aliaseslist(string)—Aliases allow alternate names to specified for the container

Volume

exec→Volume

Volumes to mount

FieldTypeRequiredDescription
sourcestring✓The source volume to mount in the container, can be specified as a relative `./` or absolute path `/usr/local/bin`. Relative paths are relative to the file declaring the container.
ConstraintsSource path must not be empty
destinationstring✓The destination in the container to mount the volume to, must be an absolute path
ConstraintsDestination path must not be empty
typestring—The type of the mount, can be one of the following values: - bind: bind the source path to the destination path in the container - volume: source is a Docker volume - tmpfs: create a temporary filesystem
Allowedbind volume tmpfs
Defaultbind
read_onlybool—Whether or not the volume is read only
bind_propagationstring—Configures bind propagation for Docker volume mounts, only applies to bind mounts, can be one of the following values: - shared - slave - private - rslave - rprivate For more information please see the Docker documentation
Allowedshared private slave rslave rprivate
Defaultshared
bind_propagation_non_recursivebool—Configures recursiveness of the bind mount. By default Docker mounts with the equivalent of `mount --rbind` meaning that mounts below the the source directory are visible in the container. For instance running `docker run --rm --mount type=bind,src=/,target=/host,readonly` busybox will make `/run` of the host available as `/host/run` in the container. To make matters even worse it will be writable (since only the toplevel bind is set readonly, not the children). If `bind_propagation_non_recursive` is set to true then the container will only see an empty `/host/run`, meaning the `tmpfs` which is typically mounted to `/run` on the host is not propagated into the container.
selinux_relabelstring—Configures Selinux relabeling for the container (usually specified as :z or :Z) and can be one of the following values: - shared (Equivalent to :z) - private (Equivalent to :Z)
Allowed shared private

Run As

exec→Run As

User to run the script as

FieldTypeRequiredDescription
userstring✓The user to run the container as
groupstring✓The group to run the container as

Computed Attributes

These attributes are set by the system and are read-only.

AttributeTypeDescription
image.idstringUnique identifier for the image, this is independent of tag and changes each time the image is built. An image that has been tagged multiple times also shares the same ID.
network[].namestringName will equal the name of the network as created by jumppad
network[].assigned_addressstringAssignedAddress will equal if IPAddress is set, else it will be the value automatically assigned from the network
pidnumberProcess ID of the executed script
exit_codenumberExit code of the completed script
outputstringScript output (stdout)
checksumstringChecksum of the script content

Sandbox creation waits for every exec script to exit. The timeout field caps that wait — 300s by default — and when it expires, the session fails:

resource.exec.NAME: timeout waiting for command to complete after 300s

What daemon = true changes depends on where the script runs:

  • On the sandbox host (no target, no image): the platform detaches the entire script and continues provisioning immediately. timeout is ignored, and the process is stopped when the sandbox shuts down. The script can run its server in the foreground — see the daemon example below.

  • In a container (target or an image block): daemon has no effect — the platform still waits for the script to exit. To start a long-running process from a container exec, detach it, then exit:

    Terminal window
    setsid myserver > /var/log/myserver.log 2>&1 &
    exit 0

    Often the better fit is not an exec at all: run the service as the container’s command, or give it its own container resource with a health_check, and the platform supervises it for you.

Scripts can set output variables by writing to the $EXEC_OUTPUT file:

#!/bin/bash
# Set output variables
echo "STATUS=completed" >> $EXEC_OUTPUT
echo "COUNT=42" >> $EXEC_OUTPUT
echo "MESSAGE=Hello World" >> $EXEC_OUTPUT

Access outputs in other resources:

output "status" {
value = resource.exec.deploy.output.STATUS
}
resource "exec" "setup" {
script = "scripts/exec/setup/script.sh"
environment = {
DEBIAN_FRONTEND = "noninteractive"
}
timeout = "600s"
}

scripts/exec/setup/script.sh:

#!/bin/bash
echo "Setting up environment..."
# Install dependencies
sudo apt-get update
sudo apt-get install -y curl wget
# Set output variables
echo "SETUP_COMPLETE=true" >> $EXEC_OUTPUT
echo "VERSION=1.0.0" >> $EXEC_OUTPUT

In local mode the platform detaches the whole script, so the server below can run in the foreground. In a container exec the same script would hang until the timeout — see Long-Running Scripts.

resource "exec" "web_server" {
script = "scripts/exec/web_server/script.sh"
daemon = true
working_directory = "/var/www/html"
environment = {
PORT = "8080"
}
}

scripts/exec/web_server/script.sh:

#!/bin/bash
cd /var/www/html
python3 -m http.server 8080

Script in New Container (Remote Mode - New Container)

Section titled “Script in New Container (Remote Mode - New Container)”
resource "exec" "build" {
image {
name = "node:18-alpine"
}
script = "scripts/exec/build/script.sh"
volume {
source = "./src"
destination = "/app"
type = "bind"
}
environment = {
NODE_ENV = "production"
}
}

scripts/exec/build/script.sh:

#!/bin/sh
cd /app
npm install
npm run build
echo "BUILD_STATUS=success" >> $EXEC_OUTPUT
echo "BUILD_TIME=$(date -Iseconds)" >> $EXEC_OUTPUT

Script in Existing Container (Remote Mode - Existing Container)

Section titled “Script in Existing Container (Remote Mode - Existing Container)”
resource "container" "database" {
image {
name = "postgres:15"
}
environment = {
POSTGRES_PASSWORD = "password"
POSTGRES_DB = "myapp"
}
port {
local = 5432
host = 5432
}
}
resource "exec" "init_database" {
target = resource.container.database
script = "scripts/exec/init_database/script.sh"
timeout = "120s"
}

scripts/exec/init_database/script.sh:

#!/bin/bash
# Wait for database to be ready
until pg_isready -U postgres; do
echo "Waiting for database..."
sleep 2
done
# Create tables
psql -U postgres -d myapp -c "
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
"
echo "DB_INITIALIZED=true" >> $EXEC_OUTPUT
# Download and extract application
resource "exec" "download_app" {
script = "scripts/exec/download_app/script.sh"
timeout = "300s"
}
# Configure application
resource "exec" "configure_app" {
depends_on = [resource.exec.download_app]
script = "scripts/exec/configure_app/script.sh"
environment = {
CONFIG_ENV = "production"
# Pass upstream output variables into the script's environment
APP_PATH = resource.exec.download_app.output.DOWNLOAD_PATH
}
}
# Start application
resource "exec" "start_app" {
depends_on = [resource.exec.configure_app]
script = "scripts/exec/start_app/script.sh"
daemon = true
environment = {
APP_PATH = resource.exec.download_app.output.DOWNLOAD_PATH
}
}

scripts/exec/download_app/script.sh:

#!/bin/bash
mkdir -p /tmp/app
cd /tmp/app
wget https://github.com/mycompany/app/releases/download/v1.2.0/app-v1.2.0.tar.gz
tar -xzf app-v1.2.0.tar.gz
echo "DOWNLOAD_PATH=/tmp/app/app-v1.2.0" >> $EXEC_OUTPUT

scripts/exec/configure_app/script.sh (APP_PATH arrives through the environment block — script files are not templates, so HCL interpolation does not run inside them):

#!/bin/bash
# Copy configuration
cp ./config/app.yml "$APP_PATH/config/"
# Set permissions
chmod +x "$APP_PATH/bin/app"
echo "CONFIG_COMPLETE=true" >> $EXEC_OUTPUT
echo "APP_READY=true" >> $EXEC_OUTPUT

scripts/exec/start_app/script.sh:

#!/bin/bash
cd "$APP_PATH"
nohup ./bin/app > app.log 2>&1 &
APP_PID=$!
echo "APP_PID=$APP_PID" >> $EXEC_OUTPUT
echo "APP_STARTED=true" >> $EXEC_OUTPUT

Container Network Setup (Remote Mode - New Container)

Section titled “Container Network Setup (Remote Mode - New Container)”
resource "network" "app" {
subnet = "10.0.1.0/24"
}
resource "exec" "network_setup" {
image {
name = "alpine:latest"
}
network {
id = resource.network.app
ip_address = "10.0.1.10"
aliases = ["setup-container"]
}
script = "scripts/exec/network_setup/script.sh"
}

scripts/exec/network_setup/script.sh:

#!/bin/sh
# Install network tools
apk add --no-cache curl netcat-openbsd
# Test network connectivity
ping -c 3 google.com
# Set network status
echo "NETWORK_READY=true" >> $EXEC_OUTPUT
echo "IP_ADDRESS=$(hostname -i)" >> $EXEC_OUTPUT

Configuration File Generation (Local Mode)

Section titled “Configuration File Generation (Local Mode)”
resource "exec" "generate_config" {
script = "scripts/exec/generate_config/script.sh"
working_directory = "/tmp"
timeout = "60s"
}

scripts/exec/generate_config/script.sh:

#!/bin/bash
set -e
echo "Generating application configuration..."
# Create config directory
mkdir -p /etc/myapp
# Generate configuration file
cat > /etc/myapp/config.yml <<CONFIG
server:
host: 0.0.0.0
port: 8080
database:
host: localhost
port: 5432
name: myapp_db
logging:
level: info
file: /var/log/myapp.log
CONFIG
# Set proper permissions
chmod 644 /etc/myapp/config.yml
echo "CONFIG_PATH=/etc/myapp/config.yml" >> $EXEC_OUTPUT
echo "CONFIG_GENERATED=true" >> $EXEC_OUTPUT

Script files are paths relative to the HCL config file. You can pair the exec resource with a template resource to generate a script file before execution:

resource "template" "install_script" {
source = "./templates/install.sh.tpl"
destination = "./scripts/exec/install/script.sh"
variables = {
version = "1.2.0"
environment = "production"
}
}
resource "exec" "install" {
depends_on = [resource.template.install_script]
script = resource.template.install_script.destination
}
  1. Error Handling: Use set -e to exit on errors and proper error checking
  2. Timeouts: Set appropriate timeouts for long-running operations
  3. Output Variables: Use EXEC_OUTPUT for passing data to other resources
  4. Idempotency: Write scripts that can be run multiple times safely
  5. Security: Avoid hardcoding secrets; use environment variables
  6. Logging: Include meaningful output for debugging and monitoring
  7. Dependencies: Use depends_on to ensure proper execution order
  8. Resource Cleanup: Clean up temporary resources and processes
  9. Script Organization: Separate scripts into subdirectories — one per exec resource — for easier readability. Use scripts/exec/<id>/script.sh where <id> matches the resource name.
  • Use working_directory for scripts that need a specific starting location
  • Leverage daemon = true for background services that should persist
  • Consider process lifecycle management for long-running processes
  • Use absolute paths when working with files outside the working directory
  • Choose appropriate base images that include required tools and dependencies
  • Use volume mounts to persist data and share files with the host
  • Configure network attachments for multi-container communication
  • Set proper user permissions with run_as for security and file access
  • Keep containers lightweight by installing only necessary packages

Remote Execution in Existing Container Mode

Section titled “Remote Execution in Existing Container Mode”
  • Ensure target containers are running and healthy before execution
  • Verify required tools and dependencies are available in the target container
  • Consider container resource limits when running intensive operations
  • Use this mode for operations that need to interact with container-specific state