SaltStack Config – Deploying Apps with vRealize Automation

Disclaimer – This is a re-post of an article I created for the VMware Cloud Management blog site (blogs.vmware.com/management).

Overview

In this blog post I’m going to show how you can use vRealize Automation and vRealize SaltStack Config together to deliver automated application deployment. Both of these products are individually powerful but when you combine them together it provides you with some fantastic functionality.

There are several approaches that you can follow to achieve automated application deployment (including orchestration and custom events). For this article I’m going to show you how to do it by leveraging Salt top files combined with Salt grains.

Our Example Application

Before I dive into Salt top files lets take a look at an example application that I will use to illustrate this method.

This is a 2 tier application that was originally used in the VMware Hands-on-Labs a few years back. I’ve given it a re-work for this article.

It is made up of database and application components which simulates a corporate phone directory. The database content is read via a Python script hosted using Apache on the database server. The application server provides a user web interface that talks to the database server. This is also using a Python script. The application server talks to the database via IP address (more on this later).

It’s a fairly simple distributed application but it allows me to showcase functionality perfectly.

Top Files

A Salt top file sits in the root of a Salt environment and must be named ‘top.sls’. You can have multiple environments, each with their own top file but only one top file per environment.

The top file is not created by default. You won’t find it in your environment unless you have created it before. It maps groups of machines to specific configurations that should be applied to them. The method you use to group machines together can be based on things like minion ID (glob matching), grain values, lists, subnet membership etc.

Here’s my top file sitting at the root of my ‘Example’ environment.

The top file is evaluated by a Salt Minion every time a ‘highstate’ is executed against it. When a Minion picks up a ‘highstate’ event on the event bus or it executes a ‘highstate’ itself, it downloads the top file. The Minion then searches the top file to find formulas that match itself. When it finds a match it applies the Salt states that are listed under the relevant formula.

Top File Configuration

Now you know what a Salt top file is lets talk about how to assign and group machines using it.

I want to mark each component server deployed as having a specific role, in this case an application or database role. This is so I can configure my top file to tell Salt to apply states if a specific role has been identified. I could do this using two different hostname conventions (one per role) but a better way would be to use a Salt grain. I can use a grain to tag each component server deployed with a role value. The top file can then be defined with formulas that use the same grain and values.

Here’s an extract from my top file that relates to the 2-tier application. I have defined 2 configurations within my ‘Example’ Salt environment. Each one matches against the same Salt grain (‘serverRole’) but with different values. The state file to apply for each configuration is also specified which I will cover next.

2-Tier Application State Files

I have placed the state files for the 2-tier application in a single directory within my ‘Example’ environment. To keep things simple I have also put any required configuration files in the same place. These files contain scripts that will power the application (e.g. fetching data from the database when requested).

Database Deployment

The ‘database.sls’ state file deploys all the required software packages, script files and service configuration for the database component server. I also use it to seed the database with some data.

# Install packages and configuration for 2tier-app database role
installDbRolePackages:
pkg.installed:
pkgs:
python
httpd
startApache:
service.running:
name: httpd
enable: True
createDatabaseDirectory:
file.directory:
name: /var/www/db
user: apache
group: apache
dir_mode: 755
file_mode: 755
recurse:
user
group
mode
directory.db:
sqlite3.table_present:
db: /var/www/db/directory.db
schema: CREATE TABLE 'directory' ("PhoneNumber" INTEGER, "FirstName" VARCHAR(30), "Surname" VARCHAR(25), "Department" VARCHAR(20))
seedDatabase:
module.run:
name: sqlite3.modify
db: /var/www/db/directory.db
sql: "INSERT INTO 'directory' VALUES (441536222333,'John','Adams','Billing'), (441536444654,'Sarah','Williams','Sales')"
require:
sqlite3: directory.db
databaseAppFile:
file.managed:
name: /var/www/cgi-bin/database.py
source: salt://2tier-app/dbAppScript.py
user: apache
group: apache
mode: 755
stopFirewall:
service.dead:
name: firewalld
enable: False
view raw database.sls hosted with ❤ by GitHub

The Python script (‘dbAppScript.py’) is deployed by ‘database.sls’ onto the database component server. It is placed into the Apache ‘cgi-bin’ folder making it accessible remotely by the application component server. This script retrieves data from the database with query options.

#! /usr/bin/python
import cgi
import sqlite3
conn=sqlite3.connect('/var/www/db/directory.db')
curs=conn.cursor()
print "Content-type:text/plain\n\n";
form = cgi.FieldStorage()
querystring = form.getvalue("querystring")
if querystring != None:
queryval = "%" + querystring + "%"
select = "SELECT * FROM directory WHERE name LIKE '" + queryval + "'"
else:
select = "SELECT * FROM directory"
for row in curs.execute(select):
if len(row) == 4:
for item in row:
print item,'|'
print "#"
conn.close()
view raw dbAppScript.py hosted with ❤ by GitHub

That takes care of the database side of things.

Application Deployment

The application component server is very similar, also deploying a Python script file (‘app.py’). It does have a key difference however. I need to make sure the Python script is configured with the IP address of the database component server. The end goal is for vRealize Automation to be able to deploy multiple instances of this distributed application fully automated. This means the IP address of the database component server could be anything. It cannot be statically written in the script file that is stored in the Salt environment. I’m going to use Salt templating with jinja and Salt grains to achieve this.

Here’s the application component server state file (‘app.sls’). The deployment of the ‘app.py’ Python script uses the ‘template: jinja’ option. This is so I can use dynamic expressions in the script file that represent the database component server IP address.

# Install packages and configuration for 2tier-app app server role
installAppRolePackages:
pkg.installed:
pkgs:
python
httpd
epel-release
python-pip
policycoreutils-python
startApache:
service.running:
name: httpd
enable: True
setSELinuxBoolean:
selinux.boolean:
name: httpd_can_network_connect
value: True
persist: True
installPipFromCmd:
cmd.run:
name: pip install requests
installAppScript:
file.managed:
name: /var/www/cgi-bin/app.py
source: salt://2tier-app/appScript.py
user: apache
group: apache
mode: 755
template: jinja
stopFirewall:
service.dead:
name: firewalld
enable: False
view raw app.sls hosted with ❤ by GitHub

The ‘app.py’ script file contains the html code to display data from the database and the remote call to the database component server to run the data query.

#!/usr/bin/python
import os, sys, cgi
import requests
print "Content-type:text/html\n\n";
print "<head><title>Company Phone Directory</title></head>\n"
print "<body>\n"
print "<h1>Directory Lookup</h1>\n"
remote = os.getenv("REMOTE_ADDR")
form = cgi.FieldStorage()
querystring = form.getvalue("querystring")
print "Accessed via:",remote,"\n<p>"
if querystring != None:
url = 'http://{{ grains['databaseServer'] }}/cgi-bin/database.py?querystring=' + querystring
else:
url = 'http://{{ grains['databaseServer'] }}/cgi-bin/database.py'
querystring = ""
r = requests.get(url)
print '<form action="/cgi-bin/app.py">'
print ' Name Filter (blank for all records):'
print ' <input type="text" name="querystring" value="'+querystring+'">'
print ' <input type="submit" value="Apply">'
print '</form>'
print "\n<table border=1 bordercolor=black cellpadding=5 cellspacing=0>"
print "\n<th>Phone Number</th><th>First Name</th><th>Surname</th><th>Department</th>"
#deal with the data coming across the wire
a = r.text.split("|\n#")
for row in a:
if len(row) != 1:
print "<tr>"
splitrow = row.split("|")
for item in splitrow:
if item != None:
print "<td>",item,"</td>"
print "</tr>\n"
print "</body></html>\n"
view raw appScript.py hosted with ❤ by GitHub

The two lines in the if/else section have jinja expressions rather than static values. The expressions mean that Salt will replace each one with the value of the ‘databaseServer’ grain as the file is processed.

if querystring != None:
url = 'http://{{ grains['databaseServer'] }}/cgi-bin/database.py?querystring=' + querystring
else:
url = 'http://{{ grains['databaseServer'] }}/cgi-bin/database.py'
view raw appScriptSnippet.py hosted with ❤ by GitHub

The application component server needs to have this grain set with the value of the database IP address for this to work.

vRealize Automation Cloud Template

Up to this point I have just been talking vRealize SaltStack Config and Salt. Now it’s time we spend some time looking at things from a different perspective. I have a top file, state files and Python scripts but I need to create something that deploys the component servers and instructs Salt to process the top file. This is where I can leverage vRealize Automation.

My basic cloud template in vRealize Automation uses two components, one for each role. For simplicity the load balancer is not included. Cloud-init downloads and deploys the Salt Minion from saltstack.com for each component server. A Salt reactor on the Salt Master automatically accepts new Salt Minion keys.

There are two additional steps also performed by cloud-init. The first is to create a Salt grain on the Minion called ‘serverRole’ and set its value. This will enable the top file formula to be matched. The second is to tell Salt to execute a ‘highstate’ triggering the processing of the top file.

In this excerpt from the application component I am also adding the database component IP address as another Salt grain. This allows the jinja template expression in ‘app.py’ to be replaced with the database IP address.

cloudConfig: |
#cloud-config
preserve-hostname: false
hostname: ${self.resourceName}.corp.local
runcmd:
– curl -L https://bootstrap.saltstack.com -o install_salt.sh
– sudo sh install_salt.sh -A ${propgroup.SaltStackConfiguration.masterAddress}
– sudo salt-call grains.set serverRole app
– sudo salt-call grains.set databaseServer ${resource.database_Server_1.networks.address[0]}
– sudo salt-call state.highstate
view raw appServerCT.yaml hosted with ❤ by GitHub

Deploying my cloud template produces two component machines. Accessing the application by targeting the ‘app.py’ script displays my rudimentary web interface and initial data set from the database server.

The cloud template could then be expanded from here to include multiple application servers, pooled behind a load balancer and protected with micro-segmentation from NSX-T.

In the next article I’ll start looking at the windows side of things to show you what we can do from that perspective.