Using Ansible and Terraform with AOS-CX
Terraform & Ansible
In the industry it is common to see implementations of Terraform alongside Ansible. Ansible, an automation framework owned by RedHat, is known for it's ability to be a strong configuration tool. With the networking industry moving towards embracing practices of Infrastructure as Code, these two tools often work together in provisioning and configuring networks and infrastructure.
Terraform | Ansible |
---|---|
Stateful | Stateless |
Keeps a state and looks to ensure the config matches the state. Is aware of all previous changes. | Push out the intent of a playbook. Ansible has no view of what has gone before. |
More focused on cloud automation. | Very much focused on infrastructure automation. |
Providers are written in Go | Modules are written in Python. |
Terraform config file is written in HCL Hashicorp configuration language. | Playbooks are written in YAML. |
Terraform is owned and developed by Hashicorp | Ansible is owned and developed by IBM (RedHat) |
Terraform does not use SSH directly and expects a plugin or provider. | Ansible uses SSH to connect to devices |
Using Ansible with Terraform to configure AOS-CX
As users are transitioning from Ansible to Terraform, they may want to utilize the playbooks they've already written to provision their AOS-CX switches utilizing our AOS-CX collection.
The installation of Ansible can be on the same machine as Terraform or they can be running on separate instances, the only requirement is that they're reachable from each other. First you'll need to define your Ansible playbook with the desired configurations you'd like to execute, for instructions on how to use Ansible and the AOS-CX collection see our Getting Started with Ansible and AOS-CX guide. For example playbooks see our AOS-CX Ansible Workflows repository on Github. Once your Ansible playbook is defined, it's time to write your Terraform configuration file.
Your installation of Ansible (same instance or different instance as TF) will determine how you define your Terraform configuration file. Please follow the corresponding section below to what matches your environment.
Ansible & Terraform installed on the same instance
Since we're not utilizing a specific resource but rather attempting to execute Ansible, we'll utilize Terraform's null_resource
provider which implements the standard resource lifecycle but takes no further action.
We've named this resource "ansible_local"
in this example but you can name it anything that fits your needs. Then we use the "local-exec"
provisioner to specify a command that will be executed on the same machine that Terraform is running. We also have to define a trigger, since Terraform does a state check to determine whether a plan can be created/executed and in this instance our resource has no "state" just execution.
The entire main.tf
file is as follows:
resource "null_resource" "ansible_local" {
# Allows the playbook to always be executed, Ansible has it's own validation
# of idempotency that we'll depend on
triggers = {
always_run = "${timestamp()}"
}
provisioner "local-exec" {
command = "ansible-playbook playbook.yml -i hosts.yml"
}
}
Once that's defined, you run Terraform as you typically would by executing the terraform apply
command and accepting (or auto-approving) the proposed changes, your output should show the execution of the Ansible playbook similar to that below:
administrator@administrator-virtual-machine:~/workspace$ terraform apply -auto-approve null_resource.ansible_local: Refreshing state... [id=3964945263498206328]
null_resource.ansible_remote: Refreshing state... [id=2867856337308266296]
null_resource.ansible: Refreshing state... [id=3548433629585765709]
Terraform used the selected providers to generate the
following execution plan. Resource actions are
indicated with the following symbols:
-/+ destroy and then create replacement
Terraform will perform the following actions:
# null_resource.ansible_local is tainted, so must be replaced
-/+ resource "null_resource" "ansible_local" {
~ id = "3964945263498206328" -> (known after apply)
~ triggers = {
- "always_run" = "2022-01-30T21:39:12Z"
} -> (known after apply)
}
Plan: 1 to add, 0 to change, 1 to destroy.
null_resource.ansible_local: Destroying... [id=3964945263498206328]
null_resource.ansible_local: Destruction complete after 0s
null_resource.ansible_local: Creating...
null_resource.ansible_local: Provisioning with 'local-exec'...
null_resource.ansible_local (local-exec): Executing: ["/bin/sh" "-c" "ansible-playbook playbook.yml -i hosts.yml"]
null_resource.ansible_local (local-exec): PLAY [all] *********************************************************************
null_resource.ansible_local (local-exec): TASK [Create VLAN 300 with description and name] *******************************
null_resource.ansible_local (local-exec): changed: [aoscx_1]
null_resource.ansible_local (local-exec): PLAY RECAP *********************************************************************
null_resource.ansible_local (local-exec): aoscx_1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
null_resource.ansible_local: Creation complete after 5s [id=8741234644291848035]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
Ansible & Terraform installed on different instances
Similar to the previous workflow, since we're not utilizing a specific resource but rather attempting to execute Ansible, we'll utilize Terraform's null_resource
provider which implements the standard resource lifecycle but takes no further action. We've named this resource "ansible_remote"
in this example but you can name it anything that fits your needs. Then we use the "remote-exec"
provisioner to specify a the command that will be executed on the remote machine. Since this is a remote resource, we have to define the connection for this resource, in the below example we are using an SSH connection and hardcoding the credentials to the machine. For variable inputs or secure methods for credentials, refer to Terraform's documentation and best practices.
The entire main.tf
file is as follows:
resource "null_resource" "ansible_remote" {
# Allows the playbook to always be executed, Ansible has it's own validation
# of idempotency that we'll depend on
triggers = {
always_run = "${timestamp()}"
}
# Establishes connection to be used by all
# generic remote provisioners (i.e. file/remote-exec)
connection {
type = "ssh"
user = "administrator"
password = "password"
host = "10.10.12.13"
}
provisioner "remote-exec" {
inline = ["ansible-playbook /home/administrator/workspace/playbook.yml -i /home/administrator/workspace/hosts.yml"]
}
}
Once that's defined, you run Terraform as you typically would by executing the terraform apply
command and accepting (or auto-approving) the proposed changes, your output should show the execution of the Ansible playbook similar to that below:
administrator@administrator-virtual-machine:~/workspace$ terraform apply -auto-approve
null_resource.ansible: Refreshing state... [id=3548433629585765709]
null_resource.ansible_remote: Refreshing state... [id=8072282142250378898]
null_resource.ansible_local: Refreshing state... [id=5506317848985332448]
Terraform used the selected providers to generate the
following execution plan. Resource actions are
indicated with the following symbols:
-/+ destroy and then create replacement
Terraform will perform the following actions:
# null_resource.ansible_remote is tainted, so must be replaced
-/+ resource "null_resource" "ansible_remote" {
~ id = "8072282142250378898" -> (known after apply)
~ triggers = {
- "always_run" = "2022-01-30T23:09:01Z"
} -> (known after apply)
}
Plan: 1 to add, 0 to change, 1 to destroy.
null_resource.ansible_remote: Destroying... [id=8072282142250378898]
null_resource.ansible_remote: Destruction complete after 0s
null_resource.ansible_remote: Creating...
null_resource.ansible_remote: Provisioning with 'remote-exec'...
null_resource.ansible_remote (remote-exec): Connecting to remote host via SSH...
null_resource.ansible_remote (remote-exec): Host: 10.80.2.153
null_resource.ansible_remote (remote-exec): User: administrator
null_resource.ansible_remote (remote-exec): Password: true
null_resource.ansible_remote (remote-exec): Private key: false
null_resource.ansible_remote (remote-exec): Certificate: false
null_resource.ansible_remote (remote-exec): SSH Agent: false
null_resource.ansible_remote (remote-exec): Checking Host Key: false
null_resource.ansible_remote (remote-exec): Target Platform: unix
null_resource.ansible_remote (remote-exec): Connected!
null_resource.ansible_remote (remote-exec): [DEPRECATION WARNING]: Ansible will require Python 3.8 or newer on the
null_resource.ansible_remote (remote-exec): controller starting with Ansible 2.12. Current version: 3.6.9 (default, Dec 8
null_resource.ansible_remote (remote-exec): 2021, 21:08:43) [GCC 8.4.0]. This feature will be removed from ansible-core in
null_resource.ansible_remote (remote-exec): version 2.12. Deprecation warnings can be disabled by setting
null_resource.ansible_remote (remote-exec): deprecation_warnings=False in ansible.cfg.
null_resource.ansible_remote (remote-exec): PLAY [all] *********************************************************************
null_resource.ansible_remote (remote-exec): TASK [Gathering Facts] *********************************************************
null_resource.ansible_remote (remote-exec): ok: [aoscx_1]
null_resource.ansible_remote (remote-exec): TASK [Create VLAN 300 with description and name] *******************************
null_resource.ansible_remote (remote-exec): [WARNING]: Collection ansible.netcommon does not support Ansible version 2.11.7
null_resource.ansible_remote (remote-exec): ok: [aoscx_1]
null_resource.ansible_remote (remote-exec): PLAY RECAP *********************************************************************
null_resource.ansible_remote (remote-exec): aoscx_1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
null_resource.ansible_remote: Creation complete after 6s [id=2930032562156220497]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
Updated almost 2 years ago