Looking to automate server setups with Ansible? This guide will introduce Ansible Playbooks and demonstrate how they work through an example deployment of a Flask application on an Apache server with a PostgreSQL database.
We will be using a fictional company, TechCorp, to demonstrate the setup of two web servers and one database server. We’ll provision the virtual machines using Vagrant and VirtualBox, then configure them effortlessly with Ansible playbooks.
Let’s get started and learn more about Ansible Playbooks!
Video Tutorial
TLDR: You can find the main repo here.
What Are Ansible Playbooks?
Ansible Playbooks are YAML-formatted files that define a series of tasks for Ansible to execute on managed hosts. These tasks are grouped into plays, which target specific hosts in a defined order.
Playbooks serve as a blueprint for configuring and managing systems, allowing you to describe the desired state in a clear, human-readable format.
Notably, the use of YAML format (Yet Another Markup Language) is important because it is both easy to read and machine-parsable, making it ideal for configuration files.
This simplicity helps speed up the development and maintenance of automation scripts.
Below are some definitions to understand when working with Playbooks:
- Plays: Define which hosts to target and set up the environment for the tasks.
- Tasks: Individual actions to execute, such as installing a package or editing a file
- Variables: Allow you to store values that can be reused throughout the playbook.
- Handlers: Special tasks that run only when notified, typically used for restarting services.
Ansible's Execution Model
Ansible operates by connecting to your nodes (managed machines) over SSH and pushing out small programs called 'Ansible modules' to perform tasks. It executes tasks defined in your playbooks sequentially, ensuring each task is completed before moving to the next.
During execution, Ansible works idempotently, making changes only when the system isn't already in the desired state. This minimizes unnecessary modifications and reduces the risk of errors.
To illustrate how an Ansible Playbook fits into the overall Ansible process, let's consider a fictional company, TechCorp. TechCorp will be using playbooks to automate the installation and configuration of software on both the web servers and the database server.
Setting Up the Environment
Before writing and running Ansible Playbooks, we need to set up our environment. Below is a diagram of what we will build.
Setting Up Vagrant and VirtualBox
For the sake of our example, we'll use Vagrant and VirtualBox to provision virtual machines (VMs). First, ensure both are installed on your system, using the following guides:
For this demo, I have Vagrant version 2.4.1 and VirtualBox version 7.0.20 r163906 (Qt5.15.2), both running on my Windows machine.
With Vagrant and VirtualBox installed, you can provision the VMs by navigating to the root of your project directory (where your Vagrantfile is located) and running:
vagrant up
This command will set up the control node, two web servers, and one database server as defined in your Vagrantfile found in your repo's root.
Run the following command to check the status of your VMs:
Current machine states:
controlnode running (virtualbox)
webserver1 running (virtualbox)
webserver2 running (virtualbox)
dbserver running (virtualbox)
Below is a screenshot of the VMs in VirtualBox:
The Inventory File
An inventory file is a configuration file where you define the hosts and groups of hosts upon which Ansible will operate. You will find it in the root of your repo.
[webservers]
webserver1 ansible_host=192.168.56.101
webserver2 ansible_host=192.168.56.102
[dbservers]
dbserver ansible_host=192.168.56.103
- [webservers]: A group containing webserver1 and webserver2.
- [dbservers]: A group containing a DB server.
Configuring SSH Connections
Ansible connects to target machines over SSH. To establish secure communication, you need public/private key pairs. Our vagrant file already creates and distributes these.SSH into the Control NodeGo ahead and run the command below to SSH into the control node from your local machine where you ran Vagrant:
vagrant ssh controlnode
Verify SSH Access from the Control Node to the Other Nodes
From the Control Node, try to SSH into the other nodes using:
ssh vagrant@192.168.56.101
ssh vagrant@192.168.56.102
ssh vagrant@192.168.56.103
If successful, you can proceed to use Ansible to manage these hosts.
Writing Your First Ansible Playbook
At the root of the repo, you will find this playbook called techcorp_playbook.yaml. This is our first and basic playbook to get us started. Here are its contents, followed by a detailed explanation:
---
- name: Configure Web Servers
hosts: webservers
become: yes
vars:
domain: techcorp.com
tasks:
- name: Install Apache
apt:
name: apache2
state: latest
notify:
- Restart Apache
- name: Ensure Apache is running
service:
name: apache2
state: started
enabled: true
- name: Deploy Apache config file
template:
src: templates/apache.conf.j2
dest: /etc/apache2/sites-available/{{ domain }}.conf
handlers:
- name: Reload Apache
service:
name: apache2
state: reloaded
- name: Restart Apache
service:
name: apache2
state: restarted
- name: Configure Database Server
hosts: dbservers
become: yes
vars:
postgresql_version: "14" # Set a default version
tasks:
- name: Install PostgreSQL
apt:
name: postgresql
state: present
update_cache: yes
- name: Ensure PostgreSQL is running
service:
name: postgresql
state: started
enabled: true
- name: Configure PostgreSQL
template:
src: templates/pg_hba.conf.j2
dest: "/etc/postgresql/{{ postgresql_version }}/main/pg_hba.conf"
notify:
- Reload PostgreSQL
handlers:
- name: Reload PostgreSQL
service:
name: postgresql
state: reloaded
Using Ansible Modules
Ansible modules are the core components that enable tasks to execute specific actions on target hosts. They are essentially pre-defined units of code that can manage system resources, install software, control services, and perform various other functions. One of the main benefits of using modules is that they simplify complex system interactions, allowing you to perform tasks without writing detailed code for each action.
In our playbook for TechCorp, we're using several essential modules:
- apt: Manages packages on Debian/Ubuntu systems. It allows installing, updating, or removing software packages using the Advanced Packaging Tool (APT).
- service: Controls services on remote hosts. It enables you to start, stop, restart, and manage the state of services.
- template: Transfers files from the control machine to target hosts, with the ability to substitute variables and apply logic using the Jinja2 templating language.
- command: Executes commands on remote hosts. It runs the command specified without using a shell, which is useful for running simple commands that don't require shell features.
By leveraging modules, we can write concise tasks that perform complex operations, making our playbook both readable and efficient. Modules manage the intricacies of interacting with the operating system, enabling us to focus on the desired end state rather than the underlying implementation details.
Now, let's examine each play and its tasks to see how these modules are used within our playbook.
Explaining the Playbook
First Play: Configure Web Servers
- Install Apache: Uses the apt module to ensure Apache is installed and up to date.
- Ensure Apache is running: Starts the Apache service and enables it to start on boot.
- Deploy Apache config file: Uses the template module to deploy a customized Apache configuration file.
- Handlers: They are triggered when the Apache package is installed or updated (Install Apache task). This task uses the notify statement to call the appropriate handler, ensuring Apache is restarted or reloaded after configuration changes.
Second Play: Configure Database Server
- Install PostgreSQL: Installs the latest version of PostgreSQL
- Ensure PostgreSQL is running: Starts and enables the PostgreSQL service
- Configure PostgreSQL: Deploys a custom configuration file using the template module
- Handlers: Reload PostgreSQL as needed
Running Ansible Playbooks
Executing the Playbook
Now it’s time to execute the playbook, using the [.code]ansible-playbook[.code] command from the control node:
cd ansible/env0-ansible-playbooks-tutorial
ansible-playbook -i inventory techcorp_playbook.yaml
The [.code]ansible-playbook[.code] command tells Ansible to use the inventory file and execute the tasks defined in techcorp_playbook.yaml file.
Once the playbook has been successfully completed, you can open a browser on your local machine and go to 192.168.56.101 or 192.168.56.102 to access the default Apache2 page on both web servers.
Understanding Playbook Execution
When you run the [.code]ansible-playbook[.code] command, Ansible initiates a series of actions to carry out the tasks defined in your playbook:
- Connection to target machines: Ansible connects to each target machine listed in your inventory file. It uses SSH for this connection, leveraging the user accounts and SSH keys you've set up earlier.
- Sequential task execution: On each host, Ansible executes the tasks sequentially as they appear in the playbook. This ensures that dependencies are respected and that each step is completed before moving on to the next.
- Idempotent operations: Ansible modules are designed to be idempotent, meaning that running them multiple times won't cause unintended changes if the system is already in the desired state. This is crucial for maintaining consistency across all systems.
- Handler notifications: If a task changes the state of a host—for example, by installing a package or modifying a configuration file—Ansible marks the task as "changed." Any handlers associated with that task via the [.code]notify[.code] directive are triggered but will run only after all tasks in the play have been executed.
- Handler execution: At the end of each play, Ansible executes any pending handlers. This is when services are reloaded or restarted based on the changes made during the tasks.
- Play recap: Upon completion, Ansible provides a summary of the playbook execution, indicating which tasks were successful, which made changes, and if any failed.
Interpreting the Output
Ansible provides a detailed output of the playbook execution. The output indicates:
- ok: The task was already in the desired state or completed successfully without making changes.
- changed: The task made changes to the system to reach the desired state.
- unreachable: Ansible could not connect to the host.
- failed: The task did not complete successfully.
- skipped, rescued, ignored: Provide additional information about task execution flow.
Here is an example of how this output may look:
PLAY [Configure Web Servers] *****************************************************
TASK [Gathering Facts] ***********************************************************
ok: [webserver1]
ok: [webserver2]
TASK [Install Apache] ************************************************************
changed: [webserver1]
changed: [webserver2]
...
PLAY [Configure Database Server] *************************************************
TASK [Gathering Facts] ***********************************************************
ok: [dbserver]
TASK [Install PostgreSQL] ********************************************************
changed: [dbserver]
...
PLAY RECAP ***********************************************************************
dbserver : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
webserver1 : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
webserver2 : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The output confirms that all tasks were executed successfully on the web servers and database server, as indicated by [.code]failed=0[.code] across all hosts in the play recap. This means there were no failures, and all tasks either completed successfully or made necessary changes.
This concludes the first section of our example, where we successfully set up the web and database servers. Now, let's move on to the bonus section, where I'll show you how to enhance the playbook with more advanced features to take this setup to the next level.
Bonus: Enhancing Playbooks with Advanced Features
Remember that bonus I promised? Well, I’ve created an advanced playbook that takes our simple setup to the next level.
This playbook sets up a Flask app, retrieves data from our PostgreSQL database, and displays it on the screen. You can check out the full code in the techcorp_playbook_advanced.yaml file in the root of the repo.
You can run this playbook from the control node with:
ansible-playbook -i inventory techcorp_playbook_advanced.yaml
Once you’ve done that, you can go to either web server's IP address—192.168.56.101 or 192.168.56.102—in your browser, and you'll see the following screen:
This indicates the Flask app running and retrieving items from the database. Now, let’s explore some of the advanced features of this playbook below.
Implementing Conditional Tasks
Conditional tasks allow you to execute tasks only when certain conditions are met. In the advanced playbook, we check if the default Apache site exists before attempting to disable it:
- name: Check if default Apache site exists
stat:
path: /etc/apache2/sites-enabled/000-default.conf
register: default_site
- name: Disable default Apache site
command: a2dissite 000-default.conf
when: default_site.stat.exists
notify: Reload Apache
In this example:
- Check if default Apache site exists: Uses the stat module to check for the existence of the default site configuration file and registers the result in default_site.
- Disable default Apache site: Runs the [.code]a2dissite[.code] command only if default_site.stat.exists is ‘True’. This prevents errors if the site is already disabled or doesn't exist.
Using Handlers and Notifications
Handlers are triggered by the notify directive when a task changes. Multiple tasks can notify the same handler, which will run only once after completing all tasks.
In the advanced playbook, handlers are extensively used to efficiently manage service restarts and reloads. For example, several tasks notify the Reload Apache handler:
- name: Create Apache configuration file for techcorp.com
template:
src: templates/apache_flask.conf.j2
dest: /etc/apache2/sites-available/techcorp.com.conf
notify: Reload Apache
- name: Configure Apache to serve Flask app
template:
src: templates/apache_flask.conf.j2
dest: /etc/apache2/sites-available/{{ domain }}.conf
notify: Reload Apache
The corresponding handler is defined as:
handlers:
- name: Reload Apache
systemd:
name: apache2
state: reloaded
This ensures that Apache is reloaded only once after all required tasks have been executed, optimizing resource usage.
Managing Variables and Templates
Working with Variables
Variables make your playbooks dynamic and reusable. In the advanced playbook, we define variables for the domain name and PostgreSQL version:
vars:
domain: techcorp.com
postgresql_version: 14
These variables are then used throughout the playbook to ensure consistency and make updates easier. Below are a couple of examples of using them:
- name: Enable Apache site
command: a2ensite {{ domain }}.conf
notify:
- Reload Apache
...
- name: Configure PostgreSQL
template:
src: templates/pg_hba.conf.j2
dest: /etc/postgresql/{{ postgresql_version }}/main/pg_hba.conf
notify:
- Reload PostgreSQL
Host variables can be defined at various levels, including global, host-specific, and play-specific scopes.
Using Templates for Configuration Files
Templates allow you to create dynamic configuration files using variables and control structures. They are written in Jinja2 templating language, which integrates seamlessly with Ansible.
Apache Configuration Template (apache_flask.conf.j2)
You can find this template under the templates folder in your repo, and here is its content:
<VirtualHost *:80>
ServerName {{ domain }}
ServerAlias www.{{ domain }}
WSGIDaemonProcess {{ domain }} user=www-data group=www-data threads=5
WSGIScriptAlias / /var/www/{{ domain }}/app.wsgi
<Directory /var/www/{{ domain }}>
WSGIProcessGroup {{ domain }}
WSGIApplicationGroup %{GLOBAL}
Order deny,allow
Allow from all
</Directory>
</VirtualHost>
Note that this template uses the ‘{{ domain }}’ variable to customize the Apache configuration for the specified domain.
To deploy the template, simply use the task below:
- name: Create Apache configuration file for techcorp.com
template:
src: templates/apache_flask.conf.j2
dest: /etc/apache2/sites-available/techcorp.com.conf
notify: Reload Apache
PostgreSQL Configuration Template (pg_hba.conf.j2)
This template is another one found under the templates folder in your repo.
# "local" is for Unix domain socket connections only
local all all peer
# IPv4 local connections:
host all all 127.0.0.1/32 md5
# IPv6 local connections:
host all all ::1/128 md5
# Allow connections from webservers
host all all 192.168.56.0/24 md5
# Allow replication connections from localhost, by a user with the
# replication privilege.
local replication all peer
host replication all 127.0.0.1/32 md5
host replication all ::1/128 md5
As the name suggests, it was created to configure PostgreSQL's client authentication settings, to allow secure connections from both local and remote clients.
The configuration above allows local connections over Unix sockets using peer authentication, permits TCP/IP connections from localhost (both IPv4 and IPv6) with MD5 password authentication, and importantly, enables remote connections from the web servers in the 192.168.56.0/24 subnet using MD5 authentication.
This ensures that your web servers can securely connect to the PostgreSQL database server while enforcing strong authentication methods, thereby maintaining the security and functionality of your application infrastructure.
Here is where we deploy the template in the playbook:
- name: Configure PostgreSQL
template:
src: templates/pg_hba.conf.j2
dest: "/etc/postgresql/{{ postgresql_version }}/main/pg_hba.conf"
notify:
- Reload PostgreSQL
Applying to the Example
In our advanced TechCorp example, using variables and templates provides several key benefits:
- Consistency: The domain variable ensures that all references to the website's domain, such as in Apache configurations, remain consistent throughout the playbook.
- Maintainability: If the domain or other values change, updating the variable in one place applies the change everywhere in the playbook, making maintenance much easier.
- Reusability: The templates for Apache and PostgreSQL configurations can be reused across different environments or projects with minimal adjustments, allowing for quick adaptation to new setups.
Organizing Complex Playbooks
As a further enhancement to our example, we could also do the following.
Working with Multiple Plays
Separating plays for different server roles improves readability and maintenance.
Including and Importing Tasks
You can break down tasks into separate files for better modularity and reuse:
- name: Common Server Configuration
hosts: all
tasks:
- import_tasks: tasks/common.yaml
- name: Web Server Configuration
hosts: webservers
tasks:
- import_tasks: tasks/webservers.yaml
- name: Database Server Configuration
hosts: dbserver
tasks:
- import_tasks: tasks/dbserver.yaml
Roles and Best Practices
Roles allow you to group tasks, variables, files, and handlers into reusable components. Roles are beyond the scope of this blog post and warrant a separate entry.
A Possible Directory Structure:
roles/
webserver/
tasks/
templates/
handlers/
vars/
dbserver/
tasks/
templates/
handlers/
vars/
Enhancing the Example with Roles in the Playbook
Refactor your playbook to use roles:
- name: Configure TechCorp Infrastructure
hosts: all
become: yes
roles:
- common
- { role: webserver, when: "'webservers' in group_names" }
- { role: dbserver, when: "'dbserver' in group_names" }
This approach improves maintainability and scalability as your infrastructure grows.
Troubleshooting and Optimization
Debugging Playbooks
Increase verbosity to get more detailed output for troubleshooting:
ansible-playbook -i inventory techcorp_playbook.yaml -vvv
Perform a syntax check before running the playbook:
ansible-playbook -i inventory techcorp_playbook.yaml --syntax-check
Optimizing Task Execution
Limit Execution to Specific Hosts:
ansible-playbook -i inventory techcorp_playbook.yaml --limit webserver1
Disable Fact Gathering if not needed, to speed up execution:
ANSIBLE_GATHERING=explicit ansible-playbook -i inventory techcorp_playbook.yaml
The [.code]ANSIBLE_GATHERING=explicit[.code] environment variable sets the gathering mode to "explicit" for the duration of this command.
In "explicit" mode, Ansible will only gather facts when explicitly requested in the playbook or with the [.code]gather_facts: true[.code] directive.
Security Considerations
Use Ansible Vault to encrypt sensitive data:
ansible-vault encrypt vars/secrets.yaml
This command encrypts the vars/secrets.yaml file, ensuring sensitive information like passwords, API keys, and confidential variables are secure. Only users with the vault password can access and decrypt the contents of this file.
Ensuring Authorized Access:
- Access Control: It's essential to manage who has access to your playbooks and encrypted files. Limit permissions to authorized users only.
- Version Control: Be cautious when committing encrypted files to version control systems. While the content is encrypted, access to the encrypted files should still be restricted.
Ansible-Vault is beyond the scope of this article, but worth mentioning here for a full-picture view.
Ansible Playbooks and env0
Integrating Ansible Playbooks with env0 enhances your automation capabilities. env0 is a platform that helps manage infrastructure automation and Infrastructure as Code (IaC) workflows.
Benefits of Using env0 with Ansible Playbooks
- Collaboration: Easily work with team members on playbooks.
- Governance: Implement policies to control automation processes.
- Scalability: Manage infrastructure growth efficiently.
How to Integrate Ansible with env0
- Connect your code repositories: Link your repositories containing Ansible Playbooks to an env0 Ansible template.
- Configure projects: Organize your environments within env0 projects where you can call on a template to run.
- Run playbooks: Execute playbooks from env0, which uses the [.code]ansible-playbook[.code] command behind the scenes by deploying an environment.
- Monitor and manage: Use env0's dashboard for execution insights, variable management, and output handling.
Emphasizing Playbooks in env0
With env0, your playbooks become more powerful:
- Parameterized runs: Manage variables across different environments effortlessly.
- Automated workflows: Integrate playbooks into env0's CI/CD pipeline with custom flows.
- Role-based access control: Secure your automation with precise permissions.
env0 with Ansible Playbooks in Action
Near the end of the demo video, at the top of this article, I show you the env0 Ansible template and how you can use it to run Ansible playbooks from env0. You can also reference this blog post, The Ultimate Ansible Tutorial: A Step-by-Step Guide, to learn more about Ansible and how to use it with env0 specifically.
To learn more about how env0’s platform can help with governance and automation, check out this guide: The Four Stages of Infrastructure as Code (IaC) Automation.
Conclusion
Automating infrastructure management with Ansible Playbooks offers significant consistency, efficiency, and scalability benefits. By working through the TechCorp example, we've demonstrated how to set up web and database servers using Ansible Playbooks, incorporating advanced features like variables, templates, and handlers.
Embracing these practices lays a solid foundation for scaling your infrastructure. Automation reduces the likelihood of human error, ensures consistency across environments, and frees up valuable time for more strategic initiatives.
Suggested Next Steps
- Integrate with CI/CD pipelines: Incorporate Ansible Playbooks into your continuous integration and deployment workflows to automate the delivery process.
- Explore Ansible Galaxy: Utilize existing roles and playbooks from the Ansible community to accelerate development.
- Implement roles and secrets: Use roles for better organization and Ansible Vault for managing sensitive information securely.
- Scale with env0: Consider using Ansible with platforms like env0 to enhance collaboration, governance, and scalability in your automation efforts.
By continuing to refine your skills with Ansible and leveraging tools like env0, you'll be well-equipped to manage complex infrastructures efficiently and effectively.
Frequently Asked Questions
Q: What is the [.code]ansible-playbook[.code] command used for?
The [.code]ansible-playbook[.code] command is used to execute Ansible Playbooks. It reads the YAML-formatted playbooks and runs the tasks defined within them against the specified hosts or groups.
Q: How do I run a specific play or task within a playbook?
You can use tags to run specific plays or tasks. Add a tags attribute to your tasks or plays and then run:
ansible-playbook -i inventory techcorp_playbook.yaml --tags "your_tag"
Q: Can I run Ansible Playbooks on multiple environments?
Yes, you can manage multiple environments by defining different inventory files or using dynamic inventory scripts. This allows you to target different sets of hosts without changing your playbooks.
Q: How do I handle secrets and sensitive data in Ansible Playbooks?
Use Ansible Vault to encrypt sensitive variables and files. This ensures that secrets like passwords and API keys are stored securely and are only accessible to authorized users.
Q: What are handlers, and how do they work in Ansible Playbooks?
Handlers are tasks that run only when notified by other tasks. They're typically used to restart or reload services after a configuration change. Handlers ensure that services are only restarted when necessary, optimizing performance.
Looking to automate server setups with Ansible? This guide will introduce Ansible Playbooks and demonstrate how they work through an example deployment of a Flask application on an Apache server with a PostgreSQL database.
We will be using a fictional company, TechCorp, to demonstrate the setup of two web servers and one database server. We’ll provision the virtual machines using Vagrant and VirtualBox, then configure them effortlessly with Ansible playbooks.
Let’s get started and learn more about Ansible Playbooks!
Video Tutorial
TLDR: You can find the main repo here.
What Are Ansible Playbooks?
Ansible Playbooks are YAML-formatted files that define a series of tasks for Ansible to execute on managed hosts. These tasks are grouped into plays, which target specific hosts in a defined order.
Playbooks serve as a blueprint for configuring and managing systems, allowing you to describe the desired state in a clear, human-readable format.
Notably, the use of YAML format (Yet Another Markup Language) is important because it is both easy to read and machine-parsable, making it ideal for configuration files.
This simplicity helps speed up the development and maintenance of automation scripts.
Below are some definitions to understand when working with Playbooks:
- Plays: Define which hosts to target and set up the environment for the tasks.
- Tasks: Individual actions to execute, such as installing a package or editing a file
- Variables: Allow you to store values that can be reused throughout the playbook.
- Handlers: Special tasks that run only when notified, typically used for restarting services.
Ansible's Execution Model
Ansible operates by connecting to your nodes (managed machines) over SSH and pushing out small programs called 'Ansible modules' to perform tasks. It executes tasks defined in your playbooks sequentially, ensuring each task is completed before moving to the next.
During execution, Ansible works idempotently, making changes only when the system isn't already in the desired state. This minimizes unnecessary modifications and reduces the risk of errors.
To illustrate how an Ansible Playbook fits into the overall Ansible process, let's consider a fictional company, TechCorp. TechCorp will be using playbooks to automate the installation and configuration of software on both the web servers and the database server.
Setting Up the Environment
Before writing and running Ansible Playbooks, we need to set up our environment. Below is a diagram of what we will build.
Setting Up Vagrant and VirtualBox
For the sake of our example, we'll use Vagrant and VirtualBox to provision virtual machines (VMs). First, ensure both are installed on your system, using the following guides:
For this demo, I have Vagrant version 2.4.1 and VirtualBox version 7.0.20 r163906 (Qt5.15.2), both running on my Windows machine.
With Vagrant and VirtualBox installed, you can provision the VMs by navigating to the root of your project directory (where your Vagrantfile is located) and running:
vagrant up
This command will set up the control node, two web servers, and one database server as defined in your Vagrantfile found in your repo's root.
Run the following command to check the status of your VMs:
Current machine states:
controlnode running (virtualbox)
webserver1 running (virtualbox)
webserver2 running (virtualbox)
dbserver running (virtualbox)
Below is a screenshot of the VMs in VirtualBox:
The Inventory File
An inventory file is a configuration file where you define the hosts and groups of hosts upon which Ansible will operate. You will find it in the root of your repo.
[webservers]
webserver1 ansible_host=192.168.56.101
webserver2 ansible_host=192.168.56.102
[dbservers]
dbserver ansible_host=192.168.56.103
- [webservers]: A group containing webserver1 and webserver2.
- [dbservers]: A group containing a DB server.
Configuring SSH Connections
Ansible connects to target machines over SSH. To establish secure communication, you need public/private key pairs. Our vagrant file already creates and distributes these.SSH into the Control NodeGo ahead and run the command below to SSH into the control node from your local machine where you ran Vagrant:
vagrant ssh controlnode
Verify SSH Access from the Control Node to the Other Nodes
From the Control Node, try to SSH into the other nodes using:
ssh vagrant@192.168.56.101
ssh vagrant@192.168.56.102
ssh vagrant@192.168.56.103
If successful, you can proceed to use Ansible to manage these hosts.
Writing Your First Ansible Playbook
At the root of the repo, you will find this playbook called techcorp_playbook.yaml. This is our first and basic playbook to get us started. Here are its contents, followed by a detailed explanation:
---
- name: Configure Web Servers
hosts: webservers
become: yes
vars:
domain: techcorp.com
tasks:
- name: Install Apache
apt:
name: apache2
state: latest
notify:
- Restart Apache
- name: Ensure Apache is running
service:
name: apache2
state: started
enabled: true
- name: Deploy Apache config file
template:
src: templates/apache.conf.j2
dest: /etc/apache2/sites-available/{{ domain }}.conf
handlers:
- name: Reload Apache
service:
name: apache2
state: reloaded
- name: Restart Apache
service:
name: apache2
state: restarted
- name: Configure Database Server
hosts: dbservers
become: yes
vars:
postgresql_version: "14" # Set a default version
tasks:
- name: Install PostgreSQL
apt:
name: postgresql
state: present
update_cache: yes
- name: Ensure PostgreSQL is running
service:
name: postgresql
state: started
enabled: true
- name: Configure PostgreSQL
template:
src: templates/pg_hba.conf.j2
dest: "/etc/postgresql/{{ postgresql_version }}/main/pg_hba.conf"
notify:
- Reload PostgreSQL
handlers:
- name: Reload PostgreSQL
service:
name: postgresql
state: reloaded
Using Ansible Modules
Ansible modules are the core components that enable tasks to execute specific actions on target hosts. They are essentially pre-defined units of code that can manage system resources, install software, control services, and perform various other functions. One of the main benefits of using modules is that they simplify complex system interactions, allowing you to perform tasks without writing detailed code for each action.
In our playbook for TechCorp, we're using several essential modules:
- apt: Manages packages on Debian/Ubuntu systems. It allows installing, updating, or removing software packages using the Advanced Packaging Tool (APT).
- service: Controls services on remote hosts. It enables you to start, stop, restart, and manage the state of services.
- template: Transfers files from the control machine to target hosts, with the ability to substitute variables and apply logic using the Jinja2 templating language.
- command: Executes commands on remote hosts. It runs the command specified without using a shell, which is useful for running simple commands that don't require shell features.
By leveraging modules, we can write concise tasks that perform complex operations, making our playbook both readable and efficient. Modules manage the intricacies of interacting with the operating system, enabling us to focus on the desired end state rather than the underlying implementation details.
Now, let's examine each play and its tasks to see how these modules are used within our playbook.
Explaining the Playbook
First Play: Configure Web Servers
- Install Apache: Uses the apt module to ensure Apache is installed and up to date.
- Ensure Apache is running: Starts the Apache service and enables it to start on boot.
- Deploy Apache config file: Uses the template module to deploy a customized Apache configuration file.
- Handlers: They are triggered when the Apache package is installed or updated (Install Apache task). This task uses the notify statement to call the appropriate handler, ensuring Apache is restarted or reloaded after configuration changes.
Second Play: Configure Database Server
- Install PostgreSQL: Installs the latest version of PostgreSQL
- Ensure PostgreSQL is running: Starts and enables the PostgreSQL service
- Configure PostgreSQL: Deploys a custom configuration file using the template module
- Handlers: Reload PostgreSQL as needed
Running Ansible Playbooks
Executing the Playbook
Now it’s time to execute the playbook, using the [.code]ansible-playbook[.code] command from the control node:
cd ansible/env0-ansible-playbooks-tutorial
ansible-playbook -i inventory techcorp_playbook.yaml
The [.code]ansible-playbook[.code] command tells Ansible to use the inventory file and execute the tasks defined in techcorp_playbook.yaml file.
Once the playbook has been successfully completed, you can open a browser on your local machine and go to 192.168.56.101 or 192.168.56.102 to access the default Apache2 page on both web servers.
Understanding Playbook Execution
When you run the [.code]ansible-playbook[.code] command, Ansible initiates a series of actions to carry out the tasks defined in your playbook:
- Connection to target machines: Ansible connects to each target machine listed in your inventory file. It uses SSH for this connection, leveraging the user accounts and SSH keys you've set up earlier.
- Sequential task execution: On each host, Ansible executes the tasks sequentially as they appear in the playbook. This ensures that dependencies are respected and that each step is completed before moving on to the next.
- Idempotent operations: Ansible modules are designed to be idempotent, meaning that running them multiple times won't cause unintended changes if the system is already in the desired state. This is crucial for maintaining consistency across all systems.
- Handler notifications: If a task changes the state of a host—for example, by installing a package or modifying a configuration file—Ansible marks the task as "changed." Any handlers associated with that task via the [.code]notify[.code] directive are triggered but will run only after all tasks in the play have been executed.
- Handler execution: At the end of each play, Ansible executes any pending handlers. This is when services are reloaded or restarted based on the changes made during the tasks.
- Play recap: Upon completion, Ansible provides a summary of the playbook execution, indicating which tasks were successful, which made changes, and if any failed.
Interpreting the Output
Ansible provides a detailed output of the playbook execution. The output indicates:
- ok: The task was already in the desired state or completed successfully without making changes.
- changed: The task made changes to the system to reach the desired state.
- unreachable: Ansible could not connect to the host.
- failed: The task did not complete successfully.
- skipped, rescued, ignored: Provide additional information about task execution flow.
Here is an example of how this output may look:
PLAY [Configure Web Servers] *****************************************************
TASK [Gathering Facts] ***********************************************************
ok: [webserver1]
ok: [webserver2]
TASK [Install Apache] ************************************************************
changed: [webserver1]
changed: [webserver2]
...
PLAY [Configure Database Server] *************************************************
TASK [Gathering Facts] ***********************************************************
ok: [dbserver]
TASK [Install PostgreSQL] ********************************************************
changed: [dbserver]
...
PLAY RECAP ***********************************************************************
dbserver : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
webserver1 : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
webserver2 : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The output confirms that all tasks were executed successfully on the web servers and database server, as indicated by [.code]failed=0[.code] across all hosts in the play recap. This means there were no failures, and all tasks either completed successfully or made necessary changes.
This concludes the first section of our example, where we successfully set up the web and database servers. Now, let's move on to the bonus section, where I'll show you how to enhance the playbook with more advanced features to take this setup to the next level.
Bonus: Enhancing Playbooks with Advanced Features
Remember that bonus I promised? Well, I’ve created an advanced playbook that takes our simple setup to the next level.
This playbook sets up a Flask app, retrieves data from our PostgreSQL database, and displays it on the screen. You can check out the full code in the techcorp_playbook_advanced.yaml file in the root of the repo.
You can run this playbook from the control node with:
ansible-playbook -i inventory techcorp_playbook_advanced.yaml
Once you’ve done that, you can go to either web server's IP address—192.168.56.101 or 192.168.56.102—in your browser, and you'll see the following screen:
This indicates the Flask app running and retrieving items from the database. Now, let’s explore some of the advanced features of this playbook below.
Implementing Conditional Tasks
Conditional tasks allow you to execute tasks only when certain conditions are met. In the advanced playbook, we check if the default Apache site exists before attempting to disable it:
- name: Check if default Apache site exists
stat:
path: /etc/apache2/sites-enabled/000-default.conf
register: default_site
- name: Disable default Apache site
command: a2dissite 000-default.conf
when: default_site.stat.exists
notify: Reload Apache
In this example:
- Check if default Apache site exists: Uses the stat module to check for the existence of the default site configuration file and registers the result in default_site.
- Disable default Apache site: Runs the [.code]a2dissite[.code] command only if default_site.stat.exists is ‘True’. This prevents errors if the site is already disabled or doesn't exist.
Using Handlers and Notifications
Handlers are triggered by the notify directive when a task changes. Multiple tasks can notify the same handler, which will run only once after completing all tasks.
In the advanced playbook, handlers are extensively used to efficiently manage service restarts and reloads. For example, several tasks notify the Reload Apache handler:
- name: Create Apache configuration file for techcorp.com
template:
src: templates/apache_flask.conf.j2
dest: /etc/apache2/sites-available/techcorp.com.conf
notify: Reload Apache
- name: Configure Apache to serve Flask app
template:
src: templates/apache_flask.conf.j2
dest: /etc/apache2/sites-available/{{ domain }}.conf
notify: Reload Apache
The corresponding handler is defined as:
handlers:
- name: Reload Apache
systemd:
name: apache2
state: reloaded
This ensures that Apache is reloaded only once after all required tasks have been executed, optimizing resource usage.
Managing Variables and Templates
Working with Variables
Variables make your playbooks dynamic and reusable. In the advanced playbook, we define variables for the domain name and PostgreSQL version:
vars:
domain: techcorp.com
postgresql_version: 14
These variables are then used throughout the playbook to ensure consistency and make updates easier. Below are a couple of examples of using them:
- name: Enable Apache site
command: a2ensite {{ domain }}.conf
notify:
- Reload Apache
...
- name: Configure PostgreSQL
template:
src: templates/pg_hba.conf.j2
dest: /etc/postgresql/{{ postgresql_version }}/main/pg_hba.conf
notify:
- Reload PostgreSQL
Host variables can be defined at various levels, including global, host-specific, and play-specific scopes.
Using Templates for Configuration Files
Templates allow you to create dynamic configuration files using variables and control structures. They are written in Jinja2 templating language, which integrates seamlessly with Ansible.
Apache Configuration Template (apache_flask.conf.j2)
You can find this template under the templates folder in your repo, and here is its content:
<VirtualHost *:80>
ServerName {{ domain }}
ServerAlias www.{{ domain }}
WSGIDaemonProcess {{ domain }} user=www-data group=www-data threads=5
WSGIScriptAlias / /var/www/{{ domain }}/app.wsgi
<Directory /var/www/{{ domain }}>
WSGIProcessGroup {{ domain }}
WSGIApplicationGroup %{GLOBAL}
Order deny,allow
Allow from all
</Directory>
</VirtualHost>
Note that this template uses the ‘{{ domain }}’ variable to customize the Apache configuration for the specified domain.
To deploy the template, simply use the task below:
- name: Create Apache configuration file for techcorp.com
template:
src: templates/apache_flask.conf.j2
dest: /etc/apache2/sites-available/techcorp.com.conf
notify: Reload Apache
PostgreSQL Configuration Template (pg_hba.conf.j2)
This template is another one found under the templates folder in your repo.
# "local" is for Unix domain socket connections only
local all all peer
# IPv4 local connections:
host all all 127.0.0.1/32 md5
# IPv6 local connections:
host all all ::1/128 md5
# Allow connections from webservers
host all all 192.168.56.0/24 md5
# Allow replication connections from localhost, by a user with the
# replication privilege.
local replication all peer
host replication all 127.0.0.1/32 md5
host replication all ::1/128 md5
As the name suggests, it was created to configure PostgreSQL's client authentication settings, to allow secure connections from both local and remote clients.
The configuration above allows local connections over Unix sockets using peer authentication, permits TCP/IP connections from localhost (both IPv4 and IPv6) with MD5 password authentication, and importantly, enables remote connections from the web servers in the 192.168.56.0/24 subnet using MD5 authentication.
This ensures that your web servers can securely connect to the PostgreSQL database server while enforcing strong authentication methods, thereby maintaining the security and functionality of your application infrastructure.
Here is where we deploy the template in the playbook:
- name: Configure PostgreSQL
template:
src: templates/pg_hba.conf.j2
dest: "/etc/postgresql/{{ postgresql_version }}/main/pg_hba.conf"
notify:
- Reload PostgreSQL
Applying to the Example
In our advanced TechCorp example, using variables and templates provides several key benefits:
- Consistency: The domain variable ensures that all references to the website's domain, such as in Apache configurations, remain consistent throughout the playbook.
- Maintainability: If the domain or other values change, updating the variable in one place applies the change everywhere in the playbook, making maintenance much easier.
- Reusability: The templates for Apache and PostgreSQL configurations can be reused across different environments or projects with minimal adjustments, allowing for quick adaptation to new setups.
Organizing Complex Playbooks
As a further enhancement to our example, we could also do the following.
Working with Multiple Plays
Separating plays for different server roles improves readability and maintenance.
Including and Importing Tasks
You can break down tasks into separate files for better modularity and reuse:
- name: Common Server Configuration
hosts: all
tasks:
- import_tasks: tasks/common.yaml
- name: Web Server Configuration
hosts: webservers
tasks:
- import_tasks: tasks/webservers.yaml
- name: Database Server Configuration
hosts: dbserver
tasks:
- import_tasks: tasks/dbserver.yaml
Roles and Best Practices
Roles allow you to group tasks, variables, files, and handlers into reusable components. Roles are beyond the scope of this blog post and warrant a separate entry.
A Possible Directory Structure:
roles/
webserver/
tasks/
templates/
handlers/
vars/
dbserver/
tasks/
templates/
handlers/
vars/
Enhancing the Example with Roles in the Playbook
Refactor your playbook to use roles:
- name: Configure TechCorp Infrastructure
hosts: all
become: yes
roles:
- common
- { role: webserver, when: "'webservers' in group_names" }
- { role: dbserver, when: "'dbserver' in group_names" }
This approach improves maintainability and scalability as your infrastructure grows.
Troubleshooting and Optimization
Debugging Playbooks
Increase verbosity to get more detailed output for troubleshooting:
ansible-playbook -i inventory techcorp_playbook.yaml -vvv
Perform a syntax check before running the playbook:
ansible-playbook -i inventory techcorp_playbook.yaml --syntax-check
Optimizing Task Execution
Limit Execution to Specific Hosts:
ansible-playbook -i inventory techcorp_playbook.yaml --limit webserver1
Disable Fact Gathering if not needed, to speed up execution:
ANSIBLE_GATHERING=explicit ansible-playbook -i inventory techcorp_playbook.yaml
The [.code]ANSIBLE_GATHERING=explicit[.code] environment variable sets the gathering mode to "explicit" for the duration of this command.
In "explicit" mode, Ansible will only gather facts when explicitly requested in the playbook or with the [.code]gather_facts: true[.code] directive.
Security Considerations
Use Ansible Vault to encrypt sensitive data:
ansible-vault encrypt vars/secrets.yaml
This command encrypts the vars/secrets.yaml file, ensuring sensitive information like passwords, API keys, and confidential variables are secure. Only users with the vault password can access and decrypt the contents of this file.
Ensuring Authorized Access:
- Access Control: It's essential to manage who has access to your playbooks and encrypted files. Limit permissions to authorized users only.
- Version Control: Be cautious when committing encrypted files to version control systems. While the content is encrypted, access to the encrypted files should still be restricted.
Ansible-Vault is beyond the scope of this article, but worth mentioning here for a full-picture view.
Ansible Playbooks and env0
Integrating Ansible Playbooks with env0 enhances your automation capabilities. env0 is a platform that helps manage infrastructure automation and Infrastructure as Code (IaC) workflows.
Benefits of Using env0 with Ansible Playbooks
- Collaboration: Easily work with team members on playbooks.
- Governance: Implement policies to control automation processes.
- Scalability: Manage infrastructure growth efficiently.
How to Integrate Ansible with env0
- Connect your code repositories: Link your repositories containing Ansible Playbooks to an env0 Ansible template.
- Configure projects: Organize your environments within env0 projects where you can call on a template to run.
- Run playbooks: Execute playbooks from env0, which uses the [.code]ansible-playbook[.code] command behind the scenes by deploying an environment.
- Monitor and manage: Use env0's dashboard for execution insights, variable management, and output handling.
Emphasizing Playbooks in env0
With env0, your playbooks become more powerful:
- Parameterized runs: Manage variables across different environments effortlessly.
- Automated workflows: Integrate playbooks into env0's CI/CD pipeline with custom flows.
- Role-based access control: Secure your automation with precise permissions.
env0 with Ansible Playbooks in Action
Near the end of the demo video, at the top of this article, I show you the env0 Ansible template and how you can use it to run Ansible playbooks from env0. You can also reference this blog post, The Ultimate Ansible Tutorial: A Step-by-Step Guide, to learn more about Ansible and how to use it with env0 specifically.
To learn more about how env0’s platform can help with governance and automation, check out this guide: The Four Stages of Infrastructure as Code (IaC) Automation.
Conclusion
Automating infrastructure management with Ansible Playbooks offers significant consistency, efficiency, and scalability benefits. By working through the TechCorp example, we've demonstrated how to set up web and database servers using Ansible Playbooks, incorporating advanced features like variables, templates, and handlers.
Embracing these practices lays a solid foundation for scaling your infrastructure. Automation reduces the likelihood of human error, ensures consistency across environments, and frees up valuable time for more strategic initiatives.
Suggested Next Steps
- Integrate with CI/CD pipelines: Incorporate Ansible Playbooks into your continuous integration and deployment workflows to automate the delivery process.
- Explore Ansible Galaxy: Utilize existing roles and playbooks from the Ansible community to accelerate development.
- Implement roles and secrets: Use roles for better organization and Ansible Vault for managing sensitive information securely.
- Scale with env0: Consider using Ansible with platforms like env0 to enhance collaboration, governance, and scalability in your automation efforts.
By continuing to refine your skills with Ansible and leveraging tools like env0, you'll be well-equipped to manage complex infrastructures efficiently and effectively.
Frequently Asked Questions
Q: What is the [.code]ansible-playbook[.code] command used for?
The [.code]ansible-playbook[.code] command is used to execute Ansible Playbooks. It reads the YAML-formatted playbooks and runs the tasks defined within them against the specified hosts or groups.
Q: How do I run a specific play or task within a playbook?
You can use tags to run specific plays or tasks. Add a tags attribute to your tasks or plays and then run:
ansible-playbook -i inventory techcorp_playbook.yaml --tags "your_tag"
Q: Can I run Ansible Playbooks on multiple environments?
Yes, you can manage multiple environments by defining different inventory files or using dynamic inventory scripts. This allows you to target different sets of hosts without changing your playbooks.
Q: How do I handle secrets and sensitive data in Ansible Playbooks?
Use Ansible Vault to encrypt sensitive variables and files. This ensures that secrets like passwords and API keys are stored securely and are only accessible to authorized users.
Q: What are handlers, and how do they work in Ansible Playbooks?
Handlers are tasks that run only when notified by other tasks. They're typically used to restart or reload services after a configuration change. Handlers ensure that services are only restarted when necessary, optimizing performance.