Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/contributing/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,72 @@ When adding a static Ansible playbook:
- [ ] Create application step to execute the playbook
- [ ] Verify playbook appears in `build/` directory during execution

## 🎯 Using Centralized Variables in Ansible Playbooks

When creating new Ansible playbooks that need dynamic variables (ports, paths, etc.), use the **centralized variables pattern** instead of creating new Tera templates.

### DO βœ…

**Add variables to `templates/ansible/variables.yml.tera`:**

```yaml
# System Configuration
ssh_port: {{ ssh_port }}
my_service_port: {{ my_service_port }} # ← Add your new variable
```

**Reference variables in static playbook using `vars_files`:**

```yaml
# 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:**

```rust
for playbook in &[
"update-apt-cache.yml",
"install-docker.yml",
"my-new-service.yml", // ← Add here
] {
// ...
}
```

### DON'T ❌

- ❌ Create a new `.tera` template for the playbook
- ❌ Create a new renderer/wrapper/context for each playbook
- ❌ Add variables directly in `inventory.yml.tera` (unless inventory-specific)

### Benefits

1. **Minimal Code**: No Rust boilerplate (renderer, wrapper, context) needed
2. **Centralized Management**: All variables in one place
3. **Runtime Resolution**: Variables resolved by Ansible, not at template rendering
4. **Easy Maintenance**: Adding new variables requires minimal changes

### When to Create a New Tera Template

Only create a new `.tera` template if:

1. The file **cannot** use Ansible's `vars_files` directive (e.g., inventory files)
2. The file requires **complex logic** that Tera provides but Ansible doesn't
3. The file needs **different variable scopes** than what centralized variables provide

Otherwise, use the centralized variables pattern for simplicity.

### Related Documentation

- **Architecture**: [`docs/technical/template-system-architecture.md`](../technical/template-system-architecture.md) - Understanding the two-phase template system
Expand Down
44 changes: 44 additions & 0 deletions docs/technical/template-system-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,50 @@ The system operates through two levels of indirection to balance portability wit
- **Use Case**: Configuration files requiring runtime parameters (IPs, usernames, paths)
- **Registration**: Automatically discovered by `.tera` extension

## 🎨 Ansible Variables Pattern

For Ansible templates, the system uses a **hybrid approach** combining static playbooks with centralized variables:

### Tera Templates (2 templates)

1. `inventory.yml.tera` - Inventory requires direct variable substitution (Ansible inventories don't support vars_files)
2. `variables.yml.tera` - Centralized variables for all playbooks

### Static Playbooks

- All playbooks are static YAML files (no `.tera` extension)
- Playbooks reference variables via `vars_files: [variables.yml]`
- Variables are resolved at Ansible runtime, not at template rendering time

### Benefits

- **Reduced Rust Boilerplate**: No per-playbook renderer/wrapper/context needed
- **Centralized Variable Management**: All playbook variables in one place
- **Consistency**: Follows the same pattern as OpenTofu's `variables.tfvars.tera`
- **Maintainability**: Adding new playbooks requires minimal code changes

### Example

```yaml
# templates/ansible/configure-firewall.yml (static playbook)
---
- name: Configure UFW firewall
hosts: all
vars_files:
- variables.yml # Load centralized variables

tasks:
- name: Allow SSH access
community.general.ufw:
port: "{{ ssh_port }}" # Variable from variables.yml
```

```yaml
# templates/ansible/variables.yml.tera (rendered once)
---
ssh_port: {{ ssh_port }}
```

## πŸ”§ Key Components

### Template Manager
Expand Down
33 changes: 25 additions & 8 deletions src/adapters/ansible/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ impl AnsibleClient {
}
}

/// Run an Ansible playbook
/// Run an Ansible playbook with optional extra arguments
///
/// # Arguments
///
/// * `playbook` - Name of the playbook file (without .yml extension)
/// * `extra_args` - Optional extra arguments to pass to ansible-playbook
///
/// # Returns
///
Expand All @@ -62,7 +63,23 @@ impl AnsibleClient {
/// * The Ansible playbook execution fails
/// * The playbook file does not exist in the working directory
/// * There are issues with the inventory or configuration
pub fn run_playbook(&self, playbook: &str) -> Result<String, CommandError> {
///
/// # Examples
///
/// ```rust,no_run
/// # use torrust_tracker_deployer_lib::adapters::ansible::AnsibleClient;
/// # let client = AnsibleClient::new("/path/to/config");
/// // Run without extra args (backward compatible)
/// client.run_playbook("install-docker", &[]).unwrap();
///
/// // Run with extra variables
/// client.run_playbook("configure-firewall", &["-e", "@variables.yml"]).unwrap();
/// ```
pub fn run_playbook(
&self,
playbook: &str,
extra_args: &[&str],
) -> Result<String, CommandError> {
info!(
"Running Ansible playbook '{}' in directory: {}",
playbook,
Expand All @@ -71,14 +88,14 @@ impl AnsibleClient {

let playbook_file = format!("{playbook}.yml");

// Build command arguments: -v flag + playbook + extra args
let mut args = vec!["-v", &playbook_file];
args.extend_from_slice(extra_args);

// Use -v flag for verbose output showing task progress
// This helps track progress during long-running operations like Docker installation
self.command_executor
.run_command(
"ansible-playbook",
&["-v", &playbook_file],
Some(&self.working_dir),
)
.run_command("ansible-playbook", &args, Some(&self.working_dir))
.map(|result| result.stdout)
}

Expand Down Expand Up @@ -140,7 +157,7 @@ mod tests {

// This tests the structure - we expect the method to exist and accept a &str
// The actual execution would fail without Ansible, but we're testing the interface
let result = client.run_playbook("install-docker");
let result = client.run_playbook("install-docker", &[]);

// We expect it to fail because ansible-playbook is not available in test environment
// But this confirms the method signature and basic functionality works
Expand Down
2 changes: 1 addition & 1 deletion src/application/steps/software/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ impl InstallDockerStep {
"Installing Docker via Ansible"
);

self.ansible_client.run_playbook("install-docker")?;
self.ansible_client.run_playbook("install-docker", &[])?;

info!(
step = "install_docker",
Expand Down
3 changes: 2 additions & 1 deletion src/application/steps/software/docker_compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ impl InstallDockerComposeStep {
"Installing Docker Compose via Ansible"
);

self.ansible_client.run_playbook("install-docker-compose")?;
self.ansible_client
.run_playbook("install-docker-compose", &[])?;

info!(
step = "install_docker_compose",
Expand Down
11 changes: 8 additions & 3 deletions src/application/steps/system/configure_firewall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,16 @@ impl ConfigureFirewallStep {
warn!(
step = "configure_firewall",
action = "configure_ufw",
"Configuring UFW firewall - CRITICAL: SSH access will be restricted to configured port"
"Configuring UFW firewall with variables from variables.yml"
);

// Run Ansible playbook (SSH port already resolved during template rendering)
match self.ansible_client.run_playbook("configure-firewall") {
// Run Ansible playbook with variables file
// Note: The @ symbol in Ansible means "load variables from this file"
// Equivalent to: ansible-playbook -e @variables.yml configure-firewall.yml
match self
.ansible_client
.run_playbook("configure-firewall", &["-e", "@variables.yml"])
{
Ok(_) => {
info!(
step = "configure_firewall",
Expand Down
2 changes: 1 addition & 1 deletion src/application/steps/system/configure_security_updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ impl ConfigureSecurityUpdatesStep {
);

self.ansible_client
.run_playbook("configure-security-updates")?;
.run_playbook("configure-security-updates", &[])?;

info!(
step = "configure_security_updates",
Expand Down
2 changes: 1 addition & 1 deletion src/application/steps/system/wait_cloud_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl WaitForCloudInitStep {
"Waiting for cloud-init completion"
);

self.ansible_client.run_playbook("wait-cloud-init")?;
self.ansible_client.run_playbook("wait-cloud-init", &[])?;

info!(
step = "wait_cloud_init",
Expand Down
Loading