Exec
The exec resource executes scripts and commands either locally on the host machine or remotely inside containers. It supports both standalone and targeted container execution with comprehensive configuration options for environment, networking, and output handling.
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 both local host operations and containerized environments.
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. |
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 | — | Maximum time to allow script execution. Format: duration string like '30s', '5m', '1h' 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 | — | Reference to an existing container resource where the script will be executed |
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 container |
Image
Create temporary container for execution
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✓ | Docker image name with optional tag ConstraintsMust match `^[a-z0-9]([a-z0-9-]*[a-z0-9])*(\.[a-z0-9]([a-z0-9-]*[a-z0-9])*)*(/[a-z0-9]+([a-z0-9._-]*[a-z0-9])*)*(:[a-z0-9]+([a-z0-9._-]*[a-z0-9])*)?$` |
username | string | — | Docker registry user to use for private repositories |
password | string | — | Docker registry password to use for private repositories |
Network
Networks to attach container to
| Field | Type | Required | Description |
|---|---|---|---|
id | string | ✓ | ID of the network to attach the container |
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. |
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. |
destination | string | ✓ | The destination in the container to mount the volume to, must be an absolute path |
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 |
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)”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" }}
# Start applicationresource "exec" "start_app" { depends_on = [resource.exec.configure_app]
script = "scripts/exec/start_app/script.sh" daemon = true}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:
#!/bin/bashAPP_PATH="${resource.exec.download_app.output.DOWNLOAD_PATH}"
# 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/bashAPP_PATH="${resource.exec.download_app.output.DOWNLOAD_PATH}"cd "$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
