Exec
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
targetand noimageblock: the script runs on the sandbox host, not inside any container or VM. Local scripts can’t usenetworkorvolumeblocks. - An
imageblock: the script runs in a new container created from that image. targetset to a container, for exampleresource.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.targetis the VM. Task scripts (setup, check, solve, cleanup) run on VMs.
Use Cases
Section titled “Use Cases”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.
HCL Syntax
Section titled “HCL Syntax”Basic Syntax
Section titled “Basic Syntax”resource "exec" "name" { script = "scripts/exec/name/script.sh"}Local Execution Syntax
Section titled “Local Execution Syntax”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" }}Remote Execution in New Container Syntax
Section titled “Remote Execution in New Container Syntax”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
| Field | Type | Required | Description |
|---|---|---|---|
script | string | ✓ | 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_ | string | — | The working directory where the script will be executed. Defaults to root directory. |
daemon | bool | — | Enable to run the script as a background daemon process Default false |
timeout | string | — | Set the maximum time the script can run before it is stopped automatically Default 300sConstraintsTimeout must be a valid duration (e.g., 30s, 5m, 1h) |
environment | map(string) | — | Environment variables that will be available to the script during execution |
image | block | — | Specify a Docker image to create a temporary container for script execution |
target | referencecontainer, 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. |
network | block | — | Networks to connect the temporary container to (only used with image)repeatable |
volume | block | — | Volumes to mount in the container (only used with image or target)repeatable |
run_ | block | — | Configure the user and group to execute the script as inside the target |
Image
Create temporary container for execution
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✓ | 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) |
username | string | — | Only required for private Docker registries. Leave empty for public images. |
password | string | — | Only required for private Docker registries. Leave empty for public images. |
Network
Networks to attach container to
| Field | Type | Required | Description |
|---|---|---|---|
id | string | — | ID of the network to attach the container |
target | referencenetwork | — | The network to attach the container to |
ip_ | string | — | 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) |
aliases | list(string) | — | Aliases allow alternate names to specified for the container |
Volume
Volumes to mount
| Field | Type | Required | Description |
|---|---|---|---|
source | string | ✓ | 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 |
destination | string | ✓ | The destination in the container to mount the volume to, must be an absolute path ConstraintsDestination path must not be empty |
type | string | — | 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 Allowed bind volume tmpfsDefault bind |
read_ | bool | — | Whether or not the volume is read only |
bind_ | string | — | 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 Allowed shared private slave rslave rprivateDefault shared |
bind_ | bool | — | 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_ | string | — | 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
User to run the script as
| Field | Type | Required | Description |
|---|---|---|---|
user | string | ✓ | The user to run the container as |
group | string | ✓ | The group to run the container as |
Computed Attributes
These attributes are set by the system and are read-only.
| Attribute | Type | Description |
|---|---|---|
image.id | string | Unique 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[].name | string | Name will equal the name of the network as created by jumppad |
network[].assigned_address | string | AssignedAddress will equal if IPAddress is set, else it will be the value automatically assigned from the network |
pid | number | Process ID of the executed script |
exit_code | number | Exit code of the completed script |
output | string | Script output (stdout) |
checksum | string | Checksum of the script content |
Long-Running Scripts
Section titled “Long-Running Scripts”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 300sWhat daemon = true changes depends on where the script runs:
-
On the sandbox host (no
target, noimage): the platform detaches the entire script and continues provisioning immediately.timeoutis 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 (
targetor animageblock):daemonhas 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 0Often the better fit is not an exec at all: run the service as the container’s
command, or give it its owncontainerresource with ahealth_check, and the platform supervises it for you.
Output Variables
Section titled “Output Variables”Scripts can set output variables by writing to the $EXEC_OUTPUT file:
#!/bin/bash# Set output variablesecho "STATUS=completed" >> $EXEC_OUTPUTecho "COUNT=42" >> $EXEC_OUTPUTecho "MESSAGE=Hello World" >> $EXEC_OUTPUTAccess outputs in other resources:
output "status" { value = resource.exec.deploy.output.STATUS}Examples
Section titled “Examples”Local Script Execution (Local Mode)
Section titled “Local Script Execution (Local Mode)”resource "exec" "setup" { script = "scripts/exec/setup/script.sh"
environment = { DEBIAN_FRONTEND = "noninteractive" }
timeout = "600s"}scripts/exec/setup/script.sh:
#!/bin/bashecho "Setting up environment..."
# Install dependenciessudo apt-get updatesudo apt-get install -y curl wget
# Set output variablesecho "SETUP_COMPLETE=true" >> $EXEC_OUTPUTecho "VERSION=1.0.0" >> $EXEC_OUTPUTBackground Daemon Process (Local Mode)
Section titled “Background Daemon Process (Local Mode)”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/bashcd /var/www/htmlpython3 -m http.server 8080Script 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/shcd /appnpm installnpm run build
echo "BUILD_STATUS=success" >> $EXEC_OUTPUTecho "BUILD_TIME=$(date -Iseconds)" >> $EXEC_OUTPUTScript 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 readyuntil pg_isready -U postgres; do echo "Waiting for database..." sleep 2done
# Create tablespsql -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_OUTPUTMulti-Step Local Deployment (Local Mode)
Section titled “Multi-Step Local Deployment (Local Mode)”# Download and extract applicationresource "exec" "download_app" { script = "scripts/exec/download_app/script.sh" timeout = "300s"}
# Configure applicationresource "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 applicationresource "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/bashmkdir -p /tmp/appcd /tmp/app
wget https://github.com/mycompany/app/releases/download/v1.2.0/app-v1.2.0.tar.gztar -xzf app-v1.2.0.tar.gz
echo "DOWNLOAD_PATH=/tmp/app/app-v1.2.0" >> $EXEC_OUTPUTscripts/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 configurationcp ./config/app.yml "$APP_PATH/config/"
# Set permissionschmod +x "$APP_PATH/bin/app"
echo "CONFIG_COMPLETE=true" >> $EXEC_OUTPUTecho "APP_READY=true" >> $EXEC_OUTPUTscripts/exec/start_app/script.sh:
#!/bin/bashcd "$APP_PATH"
nohup ./bin/app > app.log 2>&1 &APP_PID=$!
echo "APP_PID=$APP_PID" >> $EXEC_OUTPUTecho "APP_STARTED=true" >> $EXEC_OUTPUTContainer 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 toolsapk add --no-cache curl netcat-openbsd
# Test network connectivityping -c 3 google.com
# Set network statusecho "NETWORK_READY=true" >> $EXEC_OUTPUTecho "IP_ADDRESS=$(hostname -i)" >> $EXEC_OUTPUTConfiguration 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/bashset -e
echo "Generating application configuration..."
# Create config directorymkdir -p /etc/myapp
# Generate configuration filecat > /etc/myapp/config.yml <<CONFIGserver: host: 0.0.0.0 port: 8080database: host: localhost port: 5432 name: myapp_dblogging: level: info file: /var/log/myapp.logCONFIG
# Set proper permissionschmod 644 /etc/myapp/config.yml
echo "CONFIG_PATH=/etc/myapp/config.yml" >> $EXEC_OUTPUTecho "CONFIG_GENERATED=true" >> $EXEC_OUTPUTScript Templates
Section titled “Script Templates”Using Script Files with Templates
Section titled “Using Script Files with Templates”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}Best Practices
Section titled “Best Practices”General Best Practices
Section titled “General Best Practices”- Error Handling: Use
set -eto exit on errors and proper error checking - Timeouts: Set appropriate timeouts for long-running operations
- Output Variables: Use EXEC_OUTPUT for passing data to other resources
- Idempotency: Write scripts that can be run multiple times safely
- Security: Avoid hardcoding secrets; use environment variables
- Logging: Include meaningful output for debugging and monitoring
- Dependencies: Use
depends_onto ensure proper execution order - Resource Cleanup: Clean up temporary resources and processes
- Script Organization: Separate scripts into subdirectories — one per exec resource — for easier readability. Use
scripts/exec/<id>/script.shwhere<id>matches the resource name.
Mode-Specific Best Practices
Section titled “Mode-Specific Best Practices”Local Execution Mode
Section titled “Local Execution Mode”- Use
working_directoryfor scripts that need a specific starting location - Leverage
daemon = truefor background services that should persist - Consider process lifecycle management for long-running processes
- Use absolute paths when working with files outside the working directory
Remote Execution in New Container Mode
Section titled “Remote Execution in New Container Mode”- 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_asfor 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
