This document explains the correct syntax for defining variables in Tera templates used in the Torrust Tracker Deployer project.
All Tera template variables must use double curly braces with no spaces inside the braces:
# ✅ CORRECT
{{ variable_name }}
{{ username }}
{{ ssh_public_key }}
{{ instance_name }}# ❌ WRONG - Spaces inside braces
{ { variable_name } }
{ { username } }
# ❌ WRONG - Single braces
{ variable_name }
# ❌ WRONG - Mixed spacing
{{ variable_name}}
{{variable_name }}users:
- name: { { username } }
ssh_authorized_keys:
- { { ssh_public_key } }torrust_servers:
hosts:
torrust_vm:
ansible_host: { { ansible_host } }instance_name = "{{ instance_name }}"- Always use double curly braces:
{{and}} - No spaces between braces and variable name:
{{variable}}not{ { variable } } - Variable names are case-sensitive
- Works in any file format (YAML, HCL, etc.)
Problem: When using VS Code with the Prettier extension, saving .tera files automatically adds unwanted spaces inside Tera variables:
- Before saving:
{{ username }}✅ - After saving:
{ { username } }❌
Cause: Prettier doesn't understand Tera template syntax and tries to format .tera files incorrectly.
Solution: Create a .prettierignore file in your project root to exclude Tera template files:
# Ignore Tera template files - they have specific syntax that Prettier doesn't understand
*.teraAlternative Solution: Disable formatting for .tera files in your VS Code settings:
{
"[tera]": {
"editor.formatOnSave": false,
"editor.defaultFormatter": null
}
}After applying the fix, manually correct any existing formatting issues in your .tera files by removing the spaces inside the curly braces.
When adding new Ansible playbooks to the project, you need to understand the difference between static playbooks and dynamic templates, and follow the correct registration process.
Static playbooks are standard Ansible YAML files that don't require variable substitution:
- No
.teraextension - Just.yml - No Tera variables - No
{{ variable }}syntax needed - Direct copy - Copied as-is from
templates/ansible/tobuild/directory - Examples:
install-docker.yml,wait-cloud-init.yml,configure-security-updates.yml
Dynamic playbooks need runtime variable substitution:
.teraextension - Named likeinventory.ini.tera- Contains Tera variables - Uses
{{ ansible_host }},{{ username }}, etc. - Rendered during execution - Variables replaced at runtime
- Examples: Ansible inventory files with instance IPs
Follow these steps when adding a new static playbook:
Create your playbook in templates/ansible/:
# Example: Adding a new security configuration playbook
templates/ansible/configure-security-updates.ymlWrite standard Ansible YAML with no Tera variables:
---
- name: Configure automatic security updates
hosts: all
become: true
tasks:
- name: Install unattended-upgrades package
ansible.builtin.apt:
name: unattended-upgrades
state: present
update_cache: trueThis is the step that's easy to miss!
Add your playbook filename to the array in src/infrastructure/external_tools/ansible/template/renderer/mod.rs:
// Find the copy_static_templates method
async fn copy_static_templates(
&self,
template_manager: &TemplateManager,
destination_dir: &Path,
) -> Result<(), ConfigurationTemplateError> {
// ... existing code ...
// Copy all playbook files
for playbook in &[
"update-apt-cache.yml",
"install-docker.yml",
"install-docker-compose.yml",
"wait-cloud-init.yml",
"configure-security-updates.yml", // 👈 ADD YOUR PLAYBOOK HERE
] {
self.copy_static_file(template_manager, playbook, destination_dir)
.await?;
}
tracing::debug!(
"Successfully copied {} static template files",
6 // 👈 UPDATE THE COUNT: ansible.cfg + N playbooks
);
Ok(())
}Why This is Required:
- The template system uses a two-phase approach (see
docs/technical/template-system-architecture.md) - Phase 1: Static file copying - requires explicit registration
- Phase 2: Dynamic rendering - automatic for
.terafiles - Without registration, your playbook will not be copied to the build directory
- Ansible will fail with:
[ERROR]: the playbook: your-playbook.yml could not be found
In the same method, update the debug log count:
tracing::debug!(
"Successfully copied {} static template files",
6 // ansible.cfg + 5 playbooks 👈 Update this comment
);Run E2E tests to verify the playbook is copied correctly:
# Run E2E config tests (faster, tests configuration only)
cargo run --bin e2e-config-tests
# Or run full E2E tests
cargo run --bin e2e-tests-fullIf you forgot Step 2, you'll see this error:
[ERROR]: the playbook: your-playbook.yml could not be found
Create a step that executes your playbook:
// In src/application/steps/system/your_step.rs
pub struct YourStep {
ansible_client: Arc<dyn AnsibleClient>,
}
impl YourStep {
pub async fn execute(&self) -> Result<(), YourStepError> {
self.ansible_client
.run_playbook("your-playbook.yml")
.await
.map_err(YourStepError::AnsibleExecution)?;
Ok(())
}
}❌ Forgetting to register the playbook in copy_static_templates
- Error: Playbook not found during execution
- Fix: Add playbook name to the array
❌ Forgetting to update the file count in debug log
- Error: Confusing logs during debugging
- Fix: Update the count comment
❌ Using .tera extension for static playbooks
- Error: Unnecessary complexity
- Fix: Only use
.teraif you need variable substitution
❌ Adding dynamic variables without .tera extension
- Error: Variables not resolved, literal
{{ variable }}in output - Fix: Rename to
.yml.teraand handle in rendering phase
When adding a static Ansible playbook:
- Create
.ymlfile intemplates/ansible/ - Write standard Ansible YAML (no Tera variables)
- Add filename to
copy_static_templatesarray insrc/infrastructure/external_tools/ansible/template/renderer/mod.rs - Update file count in debug log
- Run E2E tests to verify
- Create application step to execute the playbook
- Verify playbook appears in
build/directory during execution
When creating new Ansible playbooks that need dynamic variables (ports, paths, etc.), use the centralized variables pattern instead of creating new Tera templates.
Add variables to templates/ansible/variables.yml.tera:
# System Configuration
ssh_port: {{ ssh_port }}
my_service_port: {{ my_service_port }} # ← Add your new variableReference variables in static playbook using vars_files:
# templates/ansible/my-new-service.yml (static playbook, no .tera extension)
---
- name: Configure My Service
hosts: all
vars_files:
- variables.yml # Load centralized variables
tasks:
- name: Configure service port
ansible.builtin.lineinfile:
path: /etc/myservice/config
line: "port={{ my_service_port }}"Register playbook in copy_static_templates() method:
for playbook in &[
"update-apt-cache.yml",
"install-docker.yml",
"my-new-service.yml", // ← Add here
] {
// ...
}- ❌ Create a new
.teratemplate for the playbook - ❌ Create a new renderer/wrapper/context for each playbook
- ❌ Add variables directly in
inventory.yml.tera(unless inventory-specific)
- Minimal Code: No Rust boilerplate (renderer, wrapper, context) needed
- Centralized Management: All variables in one place
- Runtime Resolution: Variables resolved by Ansible, not at template rendering
- Easy Maintenance: Adding new variables requires minimal changes
Only create a new .tera template if:
- The file cannot use Ansible's
vars_filesdirective (e.g., inventory files) - The file requires complex logic that Tera provides but Ansible doesn't
- The file needs different variable scopes than what centralized variables provide
Otherwise, use the centralized variables pattern for simplicity.
- Architecture:
docs/technical/template-system-architecture.md- Understanding the two-phase template system - Tera Syntax: This document (above) - When you DO need dynamic templates with variables
- Testing:
docs/e2e-testing.md- How to run E2E tests to validate your changes