Skip to main content

How to manage Apache web servers using Jinja2 templates and filters

Get your infrastructure running quickly and reliably by automating your configuration using Jinja2 templates for Ansible.
Man sitting and working on a computer

Photo by Vlada Karpovich from Pexels

Jinja2 templates are files that use variables to include static values and dynamic values. One powerful thing about a template is that you can have a basic data file but use variables to generate values dynamically based on the destination host. Ansible processes templates using Jinja2, a templating language for Python.

This article isn't an in-depth Jinja2 tutorial, but it demonstrates how to create Jinja2 templates for Ansible and use filters to manipulate the output of your data.

[ Get the free Jinja2 cheat sheet. ]

About Jinja2 templates

Jinja2 templates are designated with the .j2 file extension, which specifies that a file is a Jinja2 template and should be read as such.

Templates are often used to manage configuration files. They eliminate the need to manually update configuration files, which can be prone to human error. They also allow config files to be pushed to multiple hosts, making them scalable. You can find or use existing variables to generate values within files automatically.

Templates also have access to the same variables as the play calling them. These can be any variables with data that Ansible collects about a remote server using Ansible facts, any variable you define for your hosts or groups, or variables for a specific play.

The template module processes a template and pushes it out to a remote managed server.

As the Ansible docs explain, the Ansible controller performs the templating before sending the data to its target. This minimizes the amount of data transferred, keeping the process as lightweight as possible.

[ Get started with IT automation with the Ansible Automation Platform beginner's guide. ]

Set up your environment

You can fully manage and control Apache web servers by injecting Jinja2 templates and filters into a playbook. You need some basic knowledge about using Ansible and a running lab environment.

I used these two articles to learn and build an efficient home lab with minor adjustments to suit my needs:

My lab consists of four virtual machine (VM) servers; one Ansible control node, and three Ansible managed nodes, all running CentOS Stream. Even though my lab runs four VMs, I'll keep this article brief by explaining how to manage one web server. This process can be modified to run across and manage multiple web servers.

Create the httpd.conf.j2 Apache configuration template file

The httpd.conf.j2 file is a template for a basic Apache config file, which you add to the control node. I have retained all comments from the file, so you can follow the code easily. This config file template has a mixture of variables that are defined in the playbook apache-template.yml , as well as variables gathered by Ansible facts:

# This is the main Apache HTTP server configuration file
# {{ ansible_managed }}

ServerRoot "/etc/httpd"
Listen {{ http_port }}
Include conf.modules.d/*.conf

User apache
Group apache

ServerAdmin {{ admin }}@{{ ansible_hostname }}

<Directory />
  AllowOverride none
  Require all denied

DocumentRoot "{{ content_dir }}"

<Directory "{{ content_dir }}">
  AllowOverride none
# Allow open access:
  Require all granted

<Directory "{{ content_dir }}">
  Options Indexes FollowSymLinks
  AllowOverride none
  Require all granted

<IfModule dir_module>
 DirectoryIndex index.html

<Files ".ht">
  Require all denied

ErrorLog "logs/error_log"

LogLevel warn

<IfModule log_config_module>
  LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
  LogFormat "%h %l %u %t \"%r\" %>s %b" common

  <IfModule logio_module>
    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio

  CustomLog "logs/access_log" combined

<IfModule />
  ScriptAlias /cgi-bin/"/var/www/cgi-bin"

<Directory "/var/www/cgi-bin">
  AllowOverride none
  Options none
  Require all granted

<IfModule mime_module>
  TypesConfig /etc/mime.types
  AddType application/x-compress .Z
  AddType application/x-gzip .gz .tgz
  AddType text/html .shtml
  AddOutputFilter INCLUDES .shtml

AddDefaultCharset UTF-8

<IfModule mime_magic_module>
  MIMEMagicFile conf/magic

EnableSendFile on

IncludeOptional conf.d/*.conf

Create the index.html.j2 template file

The next file is the index.html template implemented inside the playbook to print out the hostname, IPv4 address, current memory usage, block device, and partitions inside the block device. Some things to note:

  • Square bracket notation expresses real, used, and total memory.
  • The first ansible_devices is followed by a filter that pulls in the first device.
  • The next ansible_devices line pulls all the partitions in the vdb block device. It is designated as vdb in this example because this lab is in a Kernel-based Virtual Machine (KVM).
  • The join filter is used to concatenate the values from
    {{ ansible_devices | first }} and {{ ansible_devices['vdb']['partitions']}}. Using a line break (\n -) ensures that every partition is added on a new line, and that a space and a dash are included before listing the partition.

Create a file called index.html.j2 and enter the following text:

Welcome to {{ ansible_hostname }}
-The ipv4 address is {{ ansible_default_ipv4['address']}}
-The current memory usage is {{ ansible_memory_mb['real']['used']}}mb out of {{ ansible_memory_mb['real']['total']}}mb
-The {{ ansible_devices | first }} block device has the following partitions:
  -{{ ansible_devices['vdb']['partitions'] | join('\n -')}}

Create the Apache template playbook

This playbook defines all the values depicted as variables in the configuration files. I could have opted to create a variables file, add the variables under the vars keyword, and then reference them in the playbook using the vars_file keyword. I chose to use the vars keyword to add the variables directly into the playbook.

Create a file called apache-template.yml:


# Host to execute this playbook
- hosts: ansible01.test.lab
  # Become root user
  become: true
    # Apache listen on Port 8080
    http_port: 8080
    admin: ansible-devops
    # DocumentRoot set to content_dir var
    # New DocumentRoot is webcontent
    content_dir: /webcontent
    - name: Create Group for Webcontent
        name: webcontent
        state: present
    - name: Create Webcontent Dir
        path: /webcontent
        state: directory
        group: webcontent
        owner: ansible-devops
        mode: '2775'
    - name: set mode to enforcing
        policy: targeted
        state: enforcing
    - name: enable httpd cgi boolean
        name: httpd_enable_cgi
        state: true
        persistent: true
    - name: Set SELinux Context on Directory
        target: "/webcontent(/.*)?"
        setype: httpd_sys_content_t
        state: present
    - name: run restorecon
      command: restorecon -irv /webcontent
    # Push httpd Config Template
    - name: push config template
        src: /home/vcirrus-consulting/RHCE-Ansible/templates/httpd.conf.j2
        dest: /etc/httpd/conf/httpd.conf
        backup: true
        # Notify handler to Restart Apache
        notify: "restart apache"
    # Push index Config Template
    - name: push index.html template
        src: /home/vcirrus-consulting/RHCE-Ansible/templates/index.html.j2
        dest: /webcontent/index.html
        # Notify this hander, and Roll Out New Changed in Config File
        - name: restart web servers
            name: httpd
            state: restarted
            listen: "restart apache"

Run the Apache-template playbook

Run the following: $ ansible-playbook apache-template.yml

PLAY [ansible01.test.lab]

TASK [Gathering Facts]
ok: [ansible01.test.lab]

TASK [Create Group for Webcontent]
ok: [ansible01.test.lab]

TASK [Create Webcontent Dir]
ok: [ansible01.test.lab]

TASK [set mode to enforcing]
ok: [ansible01.test.lab]

TASK [enable httpd cgi boolean]
ok: [ansible01.test.lab]

TASK [Set SELinux Context on Directory]
ok: [ansible01.test.lab]

TASK [run restorecon]
changed: [ansible01.test.lab]

TASK [push config template]
ok: [ansible01.test.lab]

TASK [push index.html template]
ok: [ansible01.test.lab]

ansible01.test.lab : ok=9 changed=1 unreachable=0 failed=0 skipped=0
rescued=0 ignored=0

Run Ansible ad-hoc commands to verify changes

This command verifies that the a backup of the original httpd configuration was created together with the newly applied configuration:

$ ansible ansible01.test.lab -a "ls /etc/httpd/conf/"
ansible01.test.lab | CHANGED | rc=0 >> httpd.conf

[ Download now: 6 ways to promote organization-wide IT automation. ]

This command verifies that the SELinux context file type was applied to the directory:

$ ansible ansible01.test.lab -a "ls -Zd /webcontent"
ansible01.test.lab | CHANGED | rc=0 >> 
unconfined_u:object_r:httpd_sys_content_t:s0 /webcontent

This command verifies that an index.html file was created as a result of injecting httpd.conf.j2 into the playbook and using the backup keyword:

$ ansible ansible01.test.lab -a "ls -al /webcontent"
ansible01.test.lab | CHANGED | rc=0 >>
total 8
drwxrwsr-x. 2 ansible-devops webcontent 42 Jun 4 21:05 .
dr-xr-xr-x. 19 root root 256 Jun 4 20:45 ..
-rw-r\--r\--. 1 root root 166 Jun 4 21:05 index.html

This command verifies that all the variables and Ansible facts have been supplied with values as a result of injecting index.html.j2 into the playbook:

$ ansible ansible01.test.lab -a "cat /webcontent/index.html"
ansible01.test.lab | CHANGED | rc=0 >>
Welcome to ansible01
-The ipv4 address is
-The current memory usage is 721mb out of 1812mb
-The vdb block device has the following partitions:

This command verifies that Ansible is managing the httpd configuration file and that all the variables have been supplied with values. For example, the server is now listening on port 8080, DocumentRoot is set to /webcontent, and the ServerAdmin is <ansible-devops@ansible01> (as listed in the templates and the playbook). The nl command is like the cat command but offers cleaner and easier-to-read output:

$ ansible ansible01.test.lab -a "nl /etc/httpd/conf/httpd.conf"
ansible01.test.lab | CHANGED | rc=0 >>
1 # This is the main Apache HTTP server configuration file
2 #
3 # Ansible managed
4 ServerRoot "/etc/httpd"
5 Listen 8080
6 Include conf.modules.d/*.conf
7 User apache
8 Group apache
9 ServerAdmin ansible-devops@ansible01
10 <Directory />
11 AllowOverride none
12 Require all denied
13 </Directory>
14 DocumentRoot "/webcontent"
15 <Directory "/webcontent">
16 AllowOverride none
17 # Allow open access:
18 Require all granted
19 </Directory>
20 <Directory "/webcontent">
21 Options Indexes FollowSymLinks
22 AllowOverride none
23 Require all granted
24 </Directory>
25 <IfModule dir_module>
26 DirectoryIndex index.html
27 </IfModule>
28 <Files ".ht">
29 Require all denied
30 </Files>
31 ErrorLog "logs/error_log"
32 LogLevel warn

Give automated configuration a try

Automated configuration is a powerful way to get infrastructure running quickly and reliably. Give this lab a try, and learn for yourself how you can leverage the power of Ansible and Jinja2 templates and filters in your IT environment.

Topics:   Ansible   Automation   Infrastructure  
Author’s photo

Robert Kimani

Robert is a Linux enthusiast and an open source advocate, currently transitioning into a site reliability engineering (SRE) role. Always striving to learn more, he's pursuing Red Hat Certified Architect - Infrastructure path certification. Besides his love for Linux, he believes in helping others More about me

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.