Tomcat 8 on Java 8 Behind Apache on CentOS

on

I was recently asked by an associate of mine to deploy a Spring application to a CentOS server running cPanel/WHM and Apache. As this build requirement is not entirely obvious I thought I’d share the experience as a simple how-to guide. Note, this is simply a how-to… not a best-practices guide so if you require a security hardened solution I recommend checking out TomcatExpert for an authoritative how-to. That being said, let’s begin.

Personal Tools of Choice

As I favor using GNU tools and I’m running Windows locally, I’m using a Cygwin-derived toolkit based on Arch Linux called MSYS2, and ConEmu  – a superb console emulator for Windows.

ConEmu Rocks

Skills and Dependency Punch-Out List

To get this build working I’m assuming you are at least casually familiar with basic Linux commands, shell access and a understanding of the Vim editor. If not I’ll show the commands as I go along but those skills are beyond the scope of this guide. If you’re not comfortable using Vim you might delegate this task to someone more confident with their skills or using another editor such as Nano or Emacs. For more info you can get a pretty good bootstrap lesson on each technology from the StackOverflow wikis… respectively, SSH , Linux  and Vim. Furthermore, we’re obviously going to need Java and Tomcat so let’s get started by installing or updating to the latest versions. (Currently Java 8 1.8.0_45, Tomcat v8.0.21).

Installing / Updating to the Latest Java 8 Version

In order to install the binaries we’re going to SSH into the server as root and use GNU Wget to download the dependencies. I’m installing Java to /usr/local but if you want you can substitute for another location such as /opt or /usr/share. Note that when you see a command-line with a # hash sign at the beginning this is a unix standard indicating that you are logged in ‘as the root user.’ However, if you are not logged in as root, all of the commands displaying a hash sign, # will require ‘sudo’ permissions. Respectively a $ dollar sign indicates you are a normal user. For more about the sudo command and running as root you can checkout the man pages or the CentOS Wiki.

So let’s cd into your target directory as root and grab the latest java version…

Please note this entry was originally posted in April of 2015 so you may want to download the latest versions of Java and Tomcat and updated the requirements accordingly.

cd /usr/local/ && wget --no-check-certificate -c \
--header "Cookie: oraclelicense=accept-securebackup-cookie" \
http://download.oracle.com/otn-pub/java/jdk/8u45-b14/jdk-8u45-linux-x64.tar.gz


WGet Java

After the archive downloads it may have been saved with the query string AuthParam appended. If so just rename the archive so it ends with a tar.gz extension.

 ls -l | grep jdk


Java Archive

Note: To rename the file you can simply type jdk and press tab twice to complete the filename in lieu of having to type out the entire filename.

mv jdk-8u45-linux-x64.tar.gz\?AuthParam\=1429150407_03533c0686909067456bf8c408ad56c9 jdk-8u45-linux-x64.tar.gz


Rename Archive

Next we need to extract the archive into the target directory using tar. Again, if you’re not installing into /usr/local update your paths accordingly.

tar xvzf jdk-8u45-linux-x64.tar.gz
ln -s /opt/apache-tomcat-8.0.21 tomcat

GNU Tar

To make future JDK updates easier, I like to create a Symlink  referencing the current JDK so all the Java binaries are automatically repointed to the current JDK via the Linux Alternatives  binary version manager. Below I’m simply making the symlink reference the newly extracted JDK.

ln -s /usr/local/jdk1.8.0_45 jdk

Symlink Java

This next series of commands isn’t strictly required. However, if you don’t currently have Java installed on your system it sure makes future updates easier so I recommend just following along so all you have to do is swap out the JDK symlink next time you update your JDK. If however you need to update the current Java version, the Alternatives How-To on the CentOS wiki can get you up to speed pretty quick on the task.

Assuming you don’t have alternatives setup for Java, the next commands will install the alternatives links for the JDK binaries.

update-alternatives --install /usr/bin/java java /usr/local/jdk/bin/java 1
update-alternatives --install /usr/bin/javac javac /usr/local/jdk/bin/javac 1
update-alternatives --install /usr/bin/javaws javaws /usr/local/jdk/bin/javaws 1

Once you’ve issued those commands you can verify the versions by passing the --display argument to the alternatives system.

update-alternatives --display java
update-alternatives --display javac
update-alternatives --display javaws

Alternatives

After you’ve installed and confirmed the alternatives simply issue java -version to confirm the system is finding the JDK.

Note: If you don’t get a valid response, start back at the beginning and verify you’ve followed each step correctly. It’s very easy to make simple mistakes for both installing Java and working in a Linux environment.

java -version


Java Version

If you’ve made it this far Congratulations! You’re halfway to having a production Java server, but don’t celebrate just yet, installing the JDK was easy, now we have to install and configure Tomcat.

Installing Tomcat 8

Note: I personally like to install Tomcat to the /opt directory. So just as with the location for the JDK, if you’re not installing into /opt update your path accordingly.

First and foremost we need to think about what we’re currently planning to do. It is a well-known generally accepted best-practice to run Tomcat on a non-privileged account, so we are going to be creating a Tomcat user without a login shell. Also, for this build I’m planning on running multiple instances of Tomcat Behind Apache 2.2 and leaving all static HTTP requests for Apache. Tomcat won’t be running HTTP at all. As such we’re going to proxy to Tomcat via AJP. The workflow will essentially look like the depiction below.

Apache Tomcat Mod Proxy

Note: AJP isn’t the only solution, We ‘could’ use Mod Proxy HTTP to proxy all requests to Tomcat on a different port. However, that is not an ideal solution as then you will be essentially using two HTTP servers and your site will be visible on both ports. Again, there are workarounds but for the sake of simplicity I like to disable the Tomcat HTTP server completely and use the AJP connector from Apache to pass requests to and from Tomcat via AJP and let Apache handle HTTP.

So let’s get started by downloading Tomcat. I’m using the Apache download directly from the site but if you want a closer mirror you can find one on the Tomcat Site

cd /opt && wget http://www.us.apache.org/dist/tomcat/tomcat-8/v8.0.21/bin/apache-tomcat-8.0.21.tar.gz

Download Tomcat

Before we move forward we need to verify the integrity of the md5 of the binary release. For this version the sum follows but you can also find the md5 link next to the tar.gz download link on the Tomcat Site. If the md5 doesn’t match, download another archive as the binaries can easily become corrupted during the transfer.

md5sum apache-tomcat-8.0.21.tar.gz

md5sum

If the md5sum matches, extract the archive and symlink tomcat in the /opt directory similarly as we did previously with the JDK.

tar xvzf apache-tomcat-8.0.21.tar.gz
ln -s /opt/apache-tomcat-8.0.21 tomcat

Next we need to set-up our tomcat user, group and change ownership of the binaries to the new tomcat user.

groupadd tomcat
useradd -g tomcat -s /usr/sbin/nologin -m -d /opt/tomcat tomcat
chown -R tomcat.tomcat /opt/tomcat

Now let’s verify that the Tomcat user has been added to the system…

cat /etc/passwd | grep tomcat

If you have output displaying your tomcat user it’s time to celebrate…

WooHoo! Now … back to work.

Build the Tomcat APR Native Library

Next we are going to build out the Tomcat Native Library for optimal performance. The Native APR Library provides performance improvements by adding support for Non-blocking I/O for keep-alive requests. So here’s the gist, taken straight from the Tomcat Native Library site on how Tomcat Native improves performance.

“The Apache Tomcat Native Library is an optional component for use with Apache Tomcat that allows Tomcat to use certain native resources for performance, compatibility, etc.
Specifically, the Apache Tomcat Native Library gives Tomcat access to the Apache Portable Runtime (APR) library network connection (socket) implementation and random-number generator. See the Apache Tomcat documentation for more information on how to configure Tomcat to use the APR connector.”

Though this step isn’t required it makes Tomcat performance much better and we love performance so let’s get to it.

Being on CentOS, an RPM based distro, we’ll be using Yum to install two of the required dependencies we need for Tomcat Native – OpenSSL and the APR library. If your using a Debian based distro (Ubuntu, Linux Mint, etc) you can just as easily use Aptitude or Apt, etc.

yum install -y apr-devel openssl-devel

Once Yum finishes installing the libraries ,cd into the base Tomcat binary directory and extract the tomcat native archive.

cd /opt/tomcat/bin && tar xvzf tomcat-native.tar.gz

Next cd into the tomcat-native-{version}-src/jni directory, and issue the following commands exactly as you see. Note, the ./configure script is pretty good at picking up most standard OpenSSL implementations so you shouldn’t need to explicitly set the path for ssl.

cd /opt/tomcat/bin/tomcat-native-1.1.33-src/jni
./configure --with-apr=/usr/local/apr \
--with-java-home=/usr/local/jdk \
--with-ssl=yes \
--prefix=/opt/tomcat

After configuring the build run make and make install.

make && make install

Now you should have the libtcnative binaries built in the /usr/local/apr/lib directory. To confirm simply ls the directory. If you ran into troubles building the tomcat native binaries, check your install to make sure all of the required dependencies were correctly found. Most often difficulties arise with finding the APR library.

ls /usr/local/apr/lib

Tomcat Native

To confirm the APR library is loading correctly let’s run a quick test by setting an environmental variable adding the APR library path and testing the configuration.

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/apr/lib
export JAVA_HOME=/usr/local/jdk
cd /opt/tomcat/bin && ./configtest.sh

After issuing ./configtest.sh you want to scroll through the output to confirm the APR library is being loaded correctly.

INFO: Loaded APR based Apache Tomcat Native library 1.1.33 using APR version 1.2.7.

Confirm APR Library

If you’re still with me this far, you’re doing great! So now let’s move on and configure the separate instances.

Note: If you have any problems at this point, such as “The APR based Apache Tomcat Native library was not found”, then check under /opt/tomcat/lib or /usr/local/apr/lib to determine whether you actually have the tcnative binaries.

Configuring Multiple Instances of Tomcat

Currently we have our Tomcat runtime files under /opt/tomcat. However, for this build we want the ability to manage multiple instances – giving each instance a separate JVM. This way, if an application crashes on one instance it won’t bring down the applications on the other. Though an ideal situation, it does however takes a good deal of configuration to setup. So lets get to it.

The Tomcat documentation shows the basics for installing multiple instances but it’s not very intuitive without a walkthrough.

Multiple Instances
In a nutshell the basic directions suggest keeping a core install $CATALINA_HOME and creating separate  instance directories  $CATALINA_BASE containing the assets required for each instance. So that’s what we’ll do.

So let’s now create the separate instances, copy the required files to each instance and remove the extraneous files from $CATALINA_HOME.

cd /opt/tomcat && mkdir tomcat_1 tomcat_2
cp -R bin conf logs temp webapps work tomcat_1
cp -R bin conf logs temp webapps work tomcat_2
rm -rf conf logs temp webapps work

You should now have the following directory structure for the /opt/tomcat directory.

ls -l /opt/tomcat

 

Multiple Instances Structure

For the sake of abstraction, I’m going to use example1.com and example2.com as a reference but of course you’ll have to update those with with your actual host domains. In order to configure our instances were going to make some modifications to a few elements in each instance’s server.xml file. Two variables you will need to provide are your domains network I.P. address and your domain name. Otherwise, there are seven items required for each server instance.

  1. A dedicated Server shutdown port.
  2. Disable the HTTP connector.
  3. A dedicated Server AJP connector port.
  4. The AJP connector address defined.
  5. The default host defined.
  6. The Host element defined.
  7. The Valve element’s logging prefix defined.

Optionally you can also update the UserDatabaseRealm to use digest authentication if you wish by adding a digest attribute with an algorithm value. I personally prefer working with SHA-256.

So let’s get to it… Open up vim and make the changes to /opt/tomcat/tomcat_1/conf/server.xml updating the items with your domain network I.P. address for item 4 and your domain for items 5,6,7. The server.xml should resemble the following absent your own variables.

cd /opt/tomcat/tomcat_1/conf && vim server.xml

Tomcat Instance 1 server.xml

After saving the first instances server.xml lets move on to updating the second. This time we will be making the same changes as we did for the first server.xml with a few extra changes for items 1 and 3. As both instances cannot share the same shutdown port, update the Server element port attribute to use a different port than the first instance. In this example I’m using 8006, incrementing one from the first instances shutdown port. Following suit for item 3, I’m also updating the AJP Connector element port attribute to use port 8010, again simply incrementing one from the first instances AJP port.

cd /opt/tomcat/tomcat_2/conf && vim server.xml

The second instance’s server.xml should look like the following.

Tomcat Instance 2 server.xml

While we’re working in the conf directories let’s go ahead and update the tomcat-users.xml file for each instance. Starting with /opt/tomcat/tomcat_1/conf/tomcat-users.xml add the following roles and user tags at the end of the file before the closing tomcat-users tag. Update the username and password values with whatever you wish now.

<role rolename="manager-gui"/>
<role rolename="manager-script"/>
<role rolename="manager-jmx"/>
<role rolename="manager-status"/>
<role rolename="admin-gui"/>
<role rolename="admin-script"/>
<user username="admin" password="admin" roles="manager-gui,manager-script,manager-jmx,manager-
status,admin-gui,admin-script"/>

Now we need to configure each instances initialization by creating an environment file located under the instance bin directory. Respectively, /opt/tomcat/{instance}/bin/setenv.sh

I’ve made a custom one especially suited for this purpose so you can simply copy mine if you wish. Though obvious, you need to update the $CATALINA_BASE and $CATALINA_PID for instance_2. Moreover, if you’re installing more than two instances, follow suit. Note, the options below are for my own server configuration. YMMV.

#!/bin/sh
# Edit this file to set custom options
#
# Notes:
#
# * Tomcat accepts the JAVA_OPTS and CATALINA_OPTS parameters
# * JAVA_OPTS are used during START/STOP/RUN
# Edit this file to set custom options
# Edit this file to set custom options
#
# Notes:
#
# * Tomcat accepts the JAVA_OPTS and CATALINA_OPTS parameters
# * JAVA_OPTS are used during START/STOP/RUN
# * CATALINA_OPTS are used during START/RUN
# * This script is sourced by catalina.sh when it placed inside
# * $CATALINA_BASE/bin (instance specify setup) or $CATALINA_HOME/bin directories.
# ------------------------------------------------------------

JAVA_HOME="/usr/local/jdk"
CATALINA_HOME="/opt/tomcat"
CATALINA_BASE="/opt/tomcat/tomcat_1"
CATALINA_PID="$CATALINA_BASE/temp/tomcat_1.pid"

#################################
# *** JAVA OPTIONS SETTINGS *** #
#################################

JVM_MODE="-server"
HEADLESS_MODE="-Djava.awt.headless=true"
BIT_MODE="-d64"
HEAP_OPTS="-Xms128m -Xmx768m"
GC_OPTS="-XX:+AlwaysPreTouch -XX:+AggressiveOpts -XX:+UseG1GC -XX:ReservedCodeCacheSize=256m -XX:+UseCompressedOops -Dsun.io.useCanonCaches=false -Djava.net.preferIPv4Stack=true -Djsse.enableSNIExtension=false -XX:SoftRefLRUPolicyMSPerMB=50"

#
# Example:
# conf/jmxremote.access
# john readwrite
# conf/jmxremote.password
# john johnpasswd
# Make sure both jmxremote.{access,passsword} files only readable/writeable (600) by the owner, and
# jxmremote.port must be unique for each server instance.
#
#JMXREMOTE_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false
#-Dcom.sun.management.jmxremote.authenticate=true -Djava.rmi.server.hostname=localhost -Dcom.sun.management.jmxremote.password.file
#=$CATALINA_BASE/conf/jmxremote.password -Dcom.sun.management.jmxremote.access.file=$CATALINA_BASE/conf/jmxremote.access"

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/apr/lib
JAVA_OPTS="$JVM_MODE $HEADLESS_MODE $BIT_MODE $HEAP_OPTS $GC_OPTS"

Next, change ownership and set the executable bit for each setenv.sh instance and then we’ll be off to do some Apache configs.


chown tomcat.tomcat /opt/tomcat/tomcat_1/bin/setenv.sh && chmod +x /opt/tomcat/tomcat_1/bin/setenv.sh
chown tomcat.tomcat /opt/tomcat/tomcat_2/bin/setenv.sh && chmod +x /opt/tomcat/tomcat_2/bin/setenv.sh   

 Configuring The Apache Virtual Hosts Includes

Next we need to make sure Apache is loading both mod_proxy and mod_proxy_ajp as we are proxying to Tomcat. Simply issue the following command to verify both modules are indeed loaded.

httpd -M | grep -i 'proxy'

You should see both modules in the output.

 ...
proxy_module (static)
proxy_ajp_module (static)
...

Though nearly all cPanel/WHM profiles have both modules active by default, if you do not see both modules in the output, you’ll need to refer to the cPanel/WHM documentation for including the modules.

Next we’ll need to include a tomcat configuration file for each domain in a specific way so when modifications are done via cPanel/WHM they won’t overwrite our configurations. To support this we’ll be adding a specific directory structure and the tomcat.conf file for both domains.

We do not need to update the default /usr/local/apache/conf/httpd.conf to include our configurations. 

Further, if you update the default /usr/local/apache/conf/httpd.conf your changes will be lost after updating updating cPanel/WHM.

Consider yourself warned.

To include our configurations the directory structure must reflect the proper user and domain structure for apache to load the configuration files. Again I’m using domain1.com, domain2.com as examples. To illustrate further, to include our new tomcat.conf for user1, domain1.com and user2, domain2.com we need to create the tomcat.conf using the following structure.

/usr/local/apache/conf/userdata/std/2/user1/domain1.com/tomcat.conf
/usr/local/apache/conf/userdata/std/2/user2/domain2.com/tomcat.conf

So let’s get to it. First cd into the correct location for whatever username your user1 is using and create the directories. If you are only using one user and two domains then simply go there now. Otherwise update your path accordingly.

cd /usr/local/apache/conf/userdata/std/2 && mkdir user1/domain1.com && mkdir user2/domain2.com

After creating the directories create a tomcat.conf file for the first user, add the following Proxy statements and save the file.

cd /usr/local/apache/conf/userdata/std/2/user1/domain1.com && \
touch tomcat.conf && vim tomcat.conf
ProxyRequests off
ProxyPreserveHost on

<Proxy *>
Order allow,deny
Allow from all
</Proxy>

ProxyPass / ajp://domain1.com:8009/
ProxyPassReverse / ajp://domain1.com:8009/

Repeat the same for the second user, second domain substituting the necessary values.

cd /usr/local/apache/conf/userdata/std/2/user2/domain2.com && \
touch tomcat.conf && vim tomcat.conf

 

ProxyRequests off
ProxyPreserveHost on

<Proxy *>
Order allow,deny
Allow from all
</Proxy>

ProxyPass / ajp://domain2.com:8010/
ProxyPassReverse / ajp://domain2.com:8010/

Next we need to invoke the scripts to update the httpd.conf configurations. Execute the following commands below to do just that.

/usr/local/cpanel/bin/apache_conf_distiller --update
/scripts/rebuildhttpdconf
service httpd restart

At this point we should now have Apache proxying requests to Tomcat but we haven’t configured the server to automatically start our Tomcat instances. Again, I’ve created a service script you can use for each instance if you wish. Create a file named tomcat_1 under the /etc/init.d directory with the following content. After creating tomcat_1 simply copy it as tomcat_2 and update the $CATALINA_BASE and $CATALINA_PID values as we did previously with the setenv.sh configurations.


#!/bin/bash
# chkconfig: 2345 20 80
# description: Tomcat 8 start/stop/status init.d script

JAVA_HOME=/usr/local/jdk
export JAVA_HOME

#Add Java binary files to PATH
PATH=$JAVA_HOME/bin:$PATH
export PATH

#CATALINA_HOME is the location of the bin files of Tomcat
CATALINA_HOME=/opt/tomcat
export CATALINA_HOME

#CATALINA_BASE is the location of the configuration files of this instance of Tomcat
CATALINA_BASE=$CATALINA_HOME/tomcat_1
export CATALINA_BASE


#CATALINA_PID is the Tomcat instance process ID.
CATALINA_PID="$CATALINA_BASE/temp/tomcat_1.pid"

export CATALINA_PID

#TOMCAT_USER is the default user of tomcat
TOMCAT_USER=tomcat
export TOMCAT_USER

#TOMCAT_USAGE is the message if this script is called without any options
TOMCAT_USAGE="Usage: $0 {\e[00;32mstart\e[00m|\e[00;31mstop\e[00m|\e[00;31mkill\e[00m|\e[00;32mstatus\e[00m|\e[00;31mrestart\e[00m}"

#SHUTDOWN_WAIT is wait time in seconds for java proccess to stop
SHUTDOWN_WAIT=20

tomcat_pid() {
        echo ps -fe | grep $CATALINA_BASE | grep -v grep | tr -s " "|cut -d" " -f2
}

#USER LEVEL CHECK
if [[ $EUID -ne 0 ]]; then
echo "basename $0 requires root privileges." 1>&2
   exit 1
fi

start() {
  pid=$(tomcat_pid)
  if [ -n "$pid" ]
  then
    echo -e "\e[00;31mTomcat is already running (pid: $pid)\e[00m"
  else
    # Start tomcat
    echo -e "\e[00;32mStarting tomcat\e[00m"
        if [ user_exists $TOMCAT_USER = "1" ]
        then
                /bin/su - $TOMCAT_USER -s /bin/bash -c $CATALINA_BASE/bin/startup.sh
        else
                sh $CATALINA_BASE/bin/startup.sh
        fi
        status
  fi
  return 0
}

status(){
          pid=$(tomcat_pid)
          if [ -n "$pid" ]; then echo -e "\e[00;32mTomcat is running with pid: $pid\e[00m"
          else echo -e "\e[00;31mTomcat is not running\e[00m"
          fi
}

terminate() {
        echo -e "\e[00;31mTerminating Tomcat\e[00m"
        kill -9 $(tomcat_pid)
}

stop() {
  pid=$(tomcat_pid)
  if [ -n "$pid" ]
  then
    echo -e "\e[00;31mStoping Tomcat\e[00m"
          sh $CATALINA_BASE/bin/shutdown.sh

    let kwait=$SHUTDOWN_WAIT
    count=0;
    until [ ps -p $pid | grep -c $pid = '0' ] || [ $count -gt $kwait ]
    do
      echo -n -e "\n\e[00;31mwaiting for processes to exit\e[00m";
      sleep 1
      let count=$count+1;
    done

    if [ $count -gt $kwait ]; then
      echo -n -e "\n\e[00;31mkilling processes didn't stop after $SHUTDOWN_WAIT seconds\e[00m"
      terminate
    fi
  else
    echo -e "\e[00;31mTomcat is not running\e[00m"
  fi

  return 0
}

user_exists(){
        if id -u $1 >/dev/null 2>&1; then
            echo "1"
        else
            echo "0"
        fi
}

case $1 in
        start)
          start
        ;;
        stop)
          stop
        ;;
        restart)
          stop
          start
        ;;
        status)
          status
        ;;
        kill)
          terminate
        ;;
        *)
          echo -e $TOMCAT_USAGE
        ;;
esac
exit 0

Once you’ve created the instance service files, we need to set the execute bit and add the files to the chkconfig application to set the system runlevels Tomcat will operate on.

chmod +x /etc/init.d/tomcat_1 && chmod +x /etc/init.d/tomcat_2
chkconfig --add tomcat_1 && chkconfig --add tomcat_2
chkconfig --level 2345 tomcat_1 on && chkconfig --level 2345 tomcat_2 on

We should now be able to query our Tomcat server instance status by using the system service commands.

service tomcat_1

Tomcat Service

Usage: /etc/init.d/tomcat_1 {start|stop|kill|status|restart}

Now we can simply call up each script to initialize the Tomcat instances, restart Apache and each domain will be forwarding to it’s own instance of Tomcat.

service tomcat_1 start && service tomcat_2 start && service httpd restart

Congrats! You now have multiple instances of Tomcat 8 installed on Java 8 running Behind an Apache Web Server on Centos with cPanel/WHM.

Till next time…

The following two tabs change content below.
Edward J. Beckett is a passionate software engineer, web developer, server administrator and polyglot programmer with nearly a decade experience building desktop and web applications ranging from simple personal web sites to enterprise level applications on many technology stacks including Java, Java EE, Spring, Spring MVC, Spring Data, Hibernate, SQL, JPA, JMS, HTML, CSS, JavaScript, ColdFusion, PHP, Node.js and more...
  • http://roberto-blog.com Roberto Rodriguez

    Very good post for mastering Tomcat, and take it to the limits creating multiple instances. I hope you share more like this about others subjects soon.. Thanks!!

  • Edward

    @Roberto Thanks man… I appreciate the kind words.

  • http://www.integritymcseo.com Brian Hughes

    Ed – very impressive post! Especially since it’s been quite some time since you’ve written a blog post. I hope to read more from you bro.

    • Edward

      Thanks Brian! I appreciate the vote.