Digital Developer Conference on Data and AI: Essential data science, machine learning, and AI skills and certification Register for free

Learn about repeatable and reliable end-to-end app provisioning and configuration

Red Hat Ansible is a popular configuration management and provisioning tool based on Python and YAML. It is easy to learn and use, and comes with an impressive catalog of pre-built content in Ansible Galaxy for application and infrastructure provisioning.

Since IBM Cloud Schematics uses Terraform for resource and service provisioning, why also use Ansible? I see the value of adding Red Hat® Ansible to the mix lying in their very different approaches to provisioning and focus. For me, Terraform’s sweet spot is its immutable infrastructure orchestration. Whereas for Ansible, it’s its configuration management and application provisioning. For a deeper comparison, see Infrastructure as Code: Chef, Ansible, Puppet, or Terraform?. For some applications, or where its desired to reuse existing content from Ansible Galaxy, they make a good team, as represented below.

Figure 1

To bring the power of Ansible to Terraform users, IBM Cloud® Schematics implements the Terraform pattern of a provisioner to extend Terraform’s base functionality. This brings the full potential of Ansible under the control of Terraform, with the ability to run Ansible playbooks, roles, and modules during the provisioning flow. Another benefit of using the Ansible Provisioner for Terraform is that it also masks many of the complexities of the SSH configuration required to use Ansible.

This article looks at how to configure and use the Ansible Provisioner for Terraform with IBM Cloud Schematics for application provisioning. It uses a companion example template to demonstrate the use of out-of-the-box Ansible roles to install a multi-tier open source application and components on to VSIs in IBM Cloud. The provisioner example can be found in the IBM Cloud Schematics examples Github repo. This example is written to be used with VSIs deployed in an IBM Cloud VPC Gen2 environment. See Discover best-practice VPC configuration for application deployment and its associated Terraform template example for details about how to deploy the target environment.

Prerequisites

To get the most out of this article, you should have a general understanding of IBM Cloud Virtual Private Cloud (VPC) and Red Hat Ansible. To run the example in IBM Cloud Schematics, you will also need an IBM Cloud account. The resources deployed by the companion VPC example are chargeable.

Estimated time

Take 20 minutes to read this article, then try the example in IBM Cloud Schematics, which will take 15-20 minutes to deploy.

Other Terraform choices to Ansible

Before diving into using Ansible with Terraform, there are other ways of deploying software that do not require SSH access and may meet your needs without the use of Ansible. Out of the box, Terraform supports deploying VSIs with custom images and also supports software installation using ‘cloud-init’. The major benefit of Ansible, in comparison, is its large catalog of pre-built provisioning content: Ansible Galaxy.

Provisioning software with the Ansible Terraform provisioner

IBM Cloud Schematics integrates both Terraform and Ansible, utilizing Terraform for infrastructure provisioning and Ansible for the software configuration. Within IBM Cloud Schematics, the Ansible Provisioner for Terraform is used as a wrapper to launch the Ansible Engine, passing input parameters from IBM Cloud Schematics about the target IBM Cloud VPC environment and VSIs to be configured. An overview of using the provisioner with IBM Cloud Schematics and Ansible is illustrated below.

Figure 2

The figure depicts the following components of the solution and the process flow:

  • A Terraform template imported into IBM Cloud Schematics defines resource statements with attached provisioner blocks.
  • A provisioner block attached to the resource statement specifies the provisioner to execute (Ansible) and the Ansible playbook to run.
  • At runtime when IBM Cloud Schematics performs the Apply of the Terraform template, IBM Cloud Schematics passes details of the target VPC environment to Ansible.
  • Terraform invokes the Ansible provisioner, which in turn executes the ansible-playbook command to run the playbook.
  • Ansible Engine runs the playbook and executes its tasks on the target VSIs.
  • Communication between Ansible and the VSIs is using SSH via a bastion host to the user’s private VPC network.
  • IBM Cloud Schematics captures the Ansible output and returns it back to the user in the activity log.

The rest of this article will look at how to use the provisioner. We assume the user already has an example VPC environment provisioned by IBM Cloud Schematics with a bastion host and network configuration allowing SSH access to the target VSIs. The details of how to provision a suitable IBM Cloud VPC environment can be found in the IBM Developer article Discover best-practice VPC configuration for application deployment and its associated Terraform template example.

Using the Ansible provisioner with Terraform resources

In Terraform, provisioners are associated with a resource and run after the resource has been created or deleted. There are two main ways of using the Ansible provisioner with a Terraform-provisioned VSI resource: The first is with the ibm_is_instance resource provisioning the VSIs to be configured; the second is by running Ansible independently after all VSIs have been provisioned using a null_resouce. See the Terraform documentation for more information on how to use and configure provisioners.

**ibm_is_instance resource**
The provisioner is attached to an IBM Cloud VPC ibm_is_instance resource definition and IBM Cloud Schematics runs Ansible against the VSIs created by this resource definition. This is good for situations where post-VSI deployment, complex software installation is required. Each ibm_is_instance definition will have its own provisioner block, and Ansible will be run separately for each resource definition and instance created. All provisioning and configuration actions are performed in a single Terraform template. The Ansible roles to be executed are identified by the host group parameter in the playbook block. The downside is the lack of Ansible coordination or data passing between the roles executed on different VSIs.

**null_resource**
The provisioner is attached to a null_resource definition, which can be in the same Terraform template as the target VSIs, or it can be in its own template with the VSIs provisioned via a different template. In the example used here, where the VSIs are provisioned by a prior template, an Ansible dynamic inventory script is used to parse the Terraform state file to determine the target hosts. Ansible is run once against the entire group of hosts defined in the inventory file. The use of dynamic inventory scripts with IBM Cloud Schematics is discussed later.

Configuring the provisioner

For the rest of this article, I will focus on the use of the provisioner with a null_resource in a separate template to the VSI provisioning. Provisioning and configuration can be performed in the same template, but for the purposes of this example, separate templates reduce the complexity and allow for a focused explanation of the Ansible provisioner and dynamic inventory usage.

When using the provisioner, Ansible is used as it would be (stand-alone), so I will not attempt to cover how to use Ansible, but point you to the documentation and the many resources available. The folder structure created to contain Ansible playbooks and roles is built the same as it would be when running Ansible stand-alone, where the user is responsible for downloading the roles from Ansible Galaxy into a roles folder. The Ansible provisioner provides comprehensive support for configuring Ansible and executing plays and roles. I suggest reading the provisioner documentation for a detailed list of all the attributes supported.

The provisioner itself is modelled on the built in Terraform remote-exec provisioner and configured using connection and provisioner blocks on the Terraform resource as shown below. The section of Terraform config shown is from the example IBM Cloud Schematics template. The attributes specified in the figure and discussed here are the minimum required to use the provisioner.

<<<< 

resource "null_resource" "ansible_runner" {
    connection {
        private_key = var.ssh_private_key
        bastion_host = var.bastion_host_ip
        host = 0.0.0.0
    }
    provisioner "ansible" {
        plays {
            playbook = {
                file_path = "${path.module}/ansible-data/playbooks/site.yml"
                roles_path = ["${path.module}/ansible-data/roles"]
            }
           inventory_file = ["${path.module}/terraform_inv.py"]

           ansible_ssh_settings {
insecure_no_strict_host_key_checking = true
connect_timeout_seconds              = 60
}
        }

    }
    depends_on = [local_file.terraform_source_state]
}

There are three elements to the configuration of the provisioner when used with IBM Cloud Schematics: the plays block, inventory, and SSH configuration. Here, I look at configuring the plays block and inventory, and in the next section I take a deeper look at the SSH and bastion host configuration.

Plays block

The plays to be executed are configured as plays blocks. These specify the playbook, roles folders, groups, and inventory.

Ansible and Terraform repo structure

For IBM Cloud Schematics to execute Ansible playbooks using the Terraform provisioner, both the Ansible playbooks and Terraform config files are combined in the same repo folder structure in the Workspace template. Terraform config files and subfolders are placed in the root of the repo. For clarity, the folder ansible-data is used for the Ansible playbooks and roles in subfolders. Similar to the usage of Terraform in IBM Cloud Schematics, this folder of Terraform and Ansible content is initially uploaded to a GitHub or GitLab repo and imported as a template to IBM Cloud Schematics.

The combined Terraform and Ansible folders should be structured similar to the example template shown here. The main.tf file contains the minimal Terraform config required to execute the Ansible provisioner.

├─ main.tf
├─ terraform_inv.tf
└── ansible-data
    ├── playbooks
    │   └── site.yml
    └── roles
        └── geerlingguy.nginx
        └── geerlingguy.nodejs
        └── undergreen.mongodb

If VSI provisioning and configuration is combined into a single template, the structure will be similar to the example here. The root of the folder contains all the required Terraform config files and modules to deploy the target environment.

├─ terraform_inv.tf
├─ outputs.tf
├─ main.tf
├─ vars.tf
├── frontend-app
├── backend-app
│   └── outputs.tf
│   └── main.tf
│   └── vars.tf
└── ansible-data
    ├── playbooks
    │   └── site.yml
    └── roles

Terraform modules and tf files are in the root of the folder structure, with all Ansible files under the ansible-data folder. In the provisioner block, the relative path is defined from the root of the repo to the Ansible playbooks and roles in the ansible-data folder using ${path.module}. The provisioner parameters file_path and roles_path define the folders for playbooks and roles respectively in the IBM Cloud Schematics template.

file_path = "${path.module}/ansible-data/playbooks/site.yml"
roles_path = ["${path.module}/ansible-data/roles"]
inventory_file = ["${path.module}/ansible-data/terraform_inv.py"]

At this time, any required Ansible roles must be downloaded from Ansible Galaxy. Along with the required playbooks, they must be included in the repo used to create the IBM Cloud Schematics workspace.

Inventory

In this example with the IBM Cloud Schematics template using a null_resource, the hosts to be targeted by Ansible are determined via a dynamic inventory script specified by the inventory_file parameter. When the provisioner is attached to an ibm_is_instance resource, the IP address of the created VSI is passed internally by Terraform to the provisioner and does not need to be specified.

Dynamic inventory with IBM Cloud Schematics

To ensure that Ansible has the current inventory of hosts deployed, a dynamic inventory script is used to parse the Terraform state file of the IBM Cloud Schematics workspace used to deploy the VSIs and VPC.

As the provisioning and configuration of the VSIs in this example is performed by two separate workspaces, the IBM Cloud Schematics equivalent of the [terraform_remote_state](https://www.terraform.io/docs/providers/terraform/d/remote_state.html) data source, [ibm_schematics_state](https://cloud.ibm.com/docs/terraform?topic=terraform-schematics-data-sources#schematics-state) is used to retrieve the state file. The process and configuration required to parse the source state file are illustrated below.

Figure 3

When the VSIs are deployed, the Terraform state file associated with the VPC workspace is updated with the attributes of the deployed VSIs. Specifically, it contains the private IP addresses of the deployed VSIs and tagging information required by Ansible to group hosts. The Terraform config of the provisioner workspace uses the ibm_schematics_state data source to retrieve the VPC terraform state file and makes it available as an input to the provisioner workspace. When the provisioner runs, the dynamic inventory script terraform_inv.py is passed to Ansible as the inventory source. It parses the VPC state file into the inventory format that conveys to Ansible details of the target hosts and their inventory grouping. In the plays block, the inventory script is identified with the inventory_file parameter:

inventory_file = ["${path.module}/ansible-data/terraform_inv.py"]

To pass the Ansible inventory group information required for selecting roles and playbooks to execute as defined by the hosts parameter in site.yml, all VSIs in the VPC workspace are tagged with IBM Cloud resource tags. The tags define the desired groupings as specified in the site.yml file. The following Terraform snippet illustrates the resource tag definitions in the companion VPC template example.

resource "ibm_is_instance" "frontend-server" {
  tags           = ["schematics:group:frontend"]
}

resource "ibm_is_instance" "backend-server" {
  tags           = ["schematics:group:backend"]
}

All tags are prefixed with schematics:group: to identify them to the dynamic inventory script terraform_inv.py as Ansible host group tags. If this prefix is not used exactly as defined, the VSIs will not be identified. The resulting inventory script output is illustrated here.

"backend": {
        "hosts": [
            "ssh-vpc-vpc-backend-vsi-1"
        ]
    },
    "frontend": {
        "hosts": [
            "ssh-vpc-vpc-frontend-vsi-1",
            "ssh-vpc-vpc-frontend-vsi-2"
        ]
    }

Configuring secure SSH access

Ansible connects to the target IBM Cloud VSIs using SSH. A secure end-to-end connection must be created between IBM Cloud Schematics and the target VSIs. At the time of this writing, connectivity from IBM Cloud Schematics to a user’s VSIs cannot be routed via the IBM Cloud private network; all SSH traffic must traverse over the public network from IBM Cloud Schematics to the target VSIs.

A bastion host (jump server) configuration is strongly recommended to provide secure access to VSIs on the IBM Cloud private network. See companion article Discover best-practice VPC configuration for application deployment for how to deploy a VPC environment on IBM Cloud with secure bastion host access. When used with IBM Cloud Schematics, the VPC security groups and ACLs are configured to only allow IBM Cloud Schematics access to the bastion host and VSIs; all other IP traffic is denied.

The figure below illustrates the end-to-end SSH connectivity from IBM Cloud Schematics to the user VPC for Ansible provisioning. The VPC template deploys and configures the VPC elements to secure the application VSIs by routing all inbound SSH via the bastion host. The output of the VPC template is the IP addresses of the deployed bastion host and app VSIs.

Figure 4

The VPC template performs the VPC deployment and configuration via the IBM Cloud provider and is shown as the vertical blue arrow. Using the provisioner template, the Ansible configuration is executed using SSH via the public network and passing through the bastion host as shown with the red arrow.

SSH key authentication

Authentication using SSH relies on Ansible and the target VSIs being configured to use the same public-private key pair. VSIs are configured with the desired public key at provisioning time (see README.md for details on SSH key generation and uploading to IBM Cloud). On the ibm_is_instance resource, the keys argument passes the IBM Cloud ID of the uploaded public SSH key. The private key of the pair is specified as an input to the provisioner template. To avoid the risk of this private key being exposed in a GitHub repo, I suggest that an IBM Cloud Schematics input variable is used to pass the private key to the template; use the sensitive option to keep the key hidden from other users. By default, the key specified by the private_key parameter is used for the target hosts and the bastion host. If desired, the provisioner supports the use of different key pairs for the VSIs and bastion by using the bastion_host_key parameter.

SSH known hosts and host key checking

The SSH host key check is intended as a verification step to ensure that SSH is connecting to the desired target host. Though in the cloud, where hosts are created and deleted on demand and IP addresses are reused, this interactive verification check is an issue for any automation. In IBM Cloud Schematics, host key checking is disabled. On public networks, it’s generally not recommended to disable checking due to the risk of man-in-the-middle attacks. In the IBM Cloud Schematics context, these concerns are mitigated by VPC security groups and ACLs allowing only IBM Cloud Schematics to access the bastion host and VSIs. Additionally, the IP addresses of the bastion and target VSIs are only known by IBM Cloud Schematics, having just been created and are referenced by explicit IP address. Host key checking is disabled by using the provisioner option insecure_no_strict_host_key_checking = true in the ansible_ssh_settings block.

The connection block

The role of the connection block in a Terraform resource definition is to pass the details of how Terraform connects to the target host. At the moment, only SSH is supported with these examples. The connection block also provides information about the bastion host used for secure access to the VPC. A major benefit of using the Ansible provisioner over calling Ansible directly using local-exec is that it performs all the SSH configuration required to connect via the bastion host without user involvement. The connection block is configured the same as for the remote-exec provisioner. Details on configuring the connection block can be found in the Terraform provisioner documentation. The minimum is three parameters to configure the connections for Ansible provisioner.

bastion_host = data.ibm_schematics_output.vpc.output_values["bastion_host_ip_address"]
private_key = var.ssh_private_key
host = "0.0.0.0"

The bastion_host parameter defines the IP address of the bastion host to be used. In the example templates, this is passed into this template from the VPC template using an ibm_schematics_output data source. Similar to the Ansible inventory, the dynamically assigned bastion host IP address is passed from the VPC workspace without the need to hardcode a value. Terraform requires that the host parameter is mandatory, though it is not used with the Ansible provisioner. A dummy IP address of 0.0.0.0 has been specified in this example. The value is not important as it is overridden by the use of the inventory_file parameter and dynamic inventory.

Using the Ansible provisioner with IBM Cloud Schematics to deploy applications

The usage of the provisioner with IBM Cloud Schematics to deploy complete application environments can be broken out into a four-step process:

  1. VPC infrastructure deployment
  2. Retrieval of VPC deployment Terraform state file
  3. Provisioner execution – Ansible executes dynamic inventory script
  4. Provisioner execution – Ansible executes playbooks and roles

In this article, the first step of VPC deployment has been isolated in a separate IBM Cloud Schematics workspace and template.

  1. Provisioning of the target VPC environment using Terraform is addressed in the VPC deployment example. In this example, resource tagging is used to identify the VSIs with the host group information required by Ansible to determine the application software to be installed on the VSIs.

Steps 2, 3 and 4 are performed by the Ansible provisioner example:

  1. Terraform state information about the deployed app VSIs and bastion host is retrieved using ibm_schematics_state and ibm_schematics_output data sources.
  2. Ansible parses the state file using the dynamic inventory script to generate an Ansible host file containing host and group information.
  3. Ansible reads the site.yml playbook and executes the roles based on the group and host information specified via the IBM Cloud resource tags in the VPC deployment template.

The successful use of Ansible in IBM Cloud Schematics depends on the coordination between the first and subsequent steps. The linkage is the use of IBM Cloud resource tagging to associate VSIs at the infrastructure layer with the application roles deployed by Ansible.

Instructions

Detailed technical steps for deploying the companion example are in the README.md file. This deploys a sample Hackathon Starter app, which is deployed onto a pair of front-end nginx app servers and deploys MongoDB as a back-end database server. It is assumed that a suitable VPC environment has already been created into which Ansible can deploy the application. Steps:

  1. Create the IBM Cloud Schematics workspace — input SSH private key and input IBM Cloud Schematics workspace_id of VPC deployment workspace.
  2. Generate plan in IBM Cloud Schematics.
  3. Apply plan in IBM Cloud Schematics.

Upon successful execution, the deployed Hackaton Starter app will be accessible on the public internet at the DNS hostname listed in the output section of the IBM Cloud Schematics activity log.

Summary

Red Hat Ansible, Terraform, and IBM Cloud VPC are all key ingredients in enabling the rapid deployment of new applications. By bringing Terraform and Red Hat Ansible together, IBM Cloud Schematics makes it easy to leverage the power of these tools along with IBM Cloud VPC. The result is repeatable and reliable end-to-end application provisioning and configuration on IBM Cloud.